diff --git a/config/scripts/build-mobile-web-app-bundle.test.mjs b/config/scripts/build-mobile-web-app-bundle.test.mjs index d89acf462e0..fa68caaaa1e 100644 --- a/config/scripts/build-mobile-web-app-bundle.test.mjs +++ b/config/scripts/build-mobile-web-app-bundle.test.mjs @@ -536,15 +536,17 @@ describe('the Phase C budget', () => { }) it('derives the chunk ceiling from the route count, not from a measured number', async () => { - // A chunk is emitted per distinct set of importers, so the count is combinatorial rather than - // one per route. Measured while building this: 8 routes emit 23 chunks, 10 emit 40, 12 emit - // 47, 14 emit 53 -- about 3 more per route at the top. The ceiling allows 4 and starts 16 - // above zero, so the next few routes land under it instead of failing on a pinned number. + // A chunk is emitted per distinct set of importers, so the count is not a function of the + // route count alone. Re-measured on this head, by copying the route tree and dropping routes + // from the end of the sorted key list -- both siblings of each, because deleting a .web.tsx + // alone leaves the native file for the builder to resolve and measures a different closure. + // The 14-route reading is the real tree and includes the one script the deferred mermaid + // artifact costs. for (const [routes, measured] of [ - [8, 23], - [10, 40], - [12, 47], - [14, 53] + [8, 32], + [10, 43], + [12, 61], + [14, 69] ]) { expect(mobileWebAppBundleMaxChunks(routes), `${String(routes)} routes`).toBeGreaterThan( measured @@ -552,6 +554,29 @@ describe('the Phase C budget', () => { } expect(mobileWebAppBundleMaxChunks(14)).toBe(72) expect(mobileWebAppBundleMaxChunks(15) - mobileWebAppBundleMaxChunks(14)).toBe(4) + // Between four and nine more per route above, so the ceiling is a bound and not a fit -- and + // at 14 routes it is a close one. 69 measured against 72, with the last two routes having cost + // the 8 the ceiling grants for two: the next route that shares less than its neighbours fails + // here, which is what this is for. + expect(mobileWebAppBundleMaxChunks(14) - mobileWebAppBundleMaxChunks(12)).toBe(8) + }) + + it('refuses an engine chunked along its own lazy boundaries, and passes one artifact', () => { + // The two builds this ceiling has to tell apart, both measured at 14 routes. + // + // The page reaches mermaid through one pre-bundled artifact and the bundle emits 69 scripts + // (68 of them the page's own split, one the deferred engine). Importing the package instead + // emitted 172: mermaid lazily imports each of its own diagram types and esbuild splits along + // those boundaries, all of it inside the generation the phone has already downloaded. The + // route term is the only term precisely so that the second of those fails here -- a ceiling + // raised to admit 172 would have admitted any split at all. + const ROUTES = 14 + const WITH_ONE_ARTIFACT = 69 + const CHUNKED_ALONG_THE_ENGINE = 172 + expect(WITH_ONE_ARTIFACT).toBeLessThanOrEqual(mobileWebAppBundleMaxChunks(ROUTES)) + expect(CHUNKED_ALONG_THE_ENGINE).toBeGreaterThan(mobileWebAppBundleMaxChunks(ROUTES)) + // And the assets that came with it: 215 against 112, of the 256 the shell will load. + expect(mobileWebAppBundleMaxAssets(ROUTES, 42)).toBeLessThan(CHUNKED_ALONG_THE_ENGINE + 42 + 1) }) it('derives the asset ceiling so the chunk ceiling is always the one that trips first', () => { @@ -592,7 +617,8 @@ describe('the Phase C budget', () => { it('fails the build when the derived ceiling passes what the phone will accept', async () => { // The shell hands back null for a manifest over its own ceiling, so a derived ceiling above // that ships a green build no device can open. At the 42 images the tree carries, 4r + 16 + - // 42 + 1 crosses 256 at 50 routes, which Phase C reaches. + // 42 + 1 crosses 256 at 50 routes, which Phase C reaches. A deferred engine kept to one + // artifact leaves that where it is; the 103-script version of it moved the crossing to 24. expect(await readMobileWebBundleMaxAssets()).toBe(MOBILE_WEB_BUNDLE_MAX_ASSETS) expect(assertAssetCeilingFitsShell(49, 42, MOBILE_WEB_BUNDLE_MAX_ASSETS)).toBe(255) expect(() => assertAssetCeilingFitsShell(50, 42, MOBILE_WEB_BUNDLE_MAX_ASSETS)).toThrow( diff --git a/config/scripts/mobile-web-app-mermaid-render.test.mjs b/config/scripts/mobile-web-app-mermaid-render.test.mjs new file mode 100644 index 00000000000..f89dd02195b --- /dev/null +++ b/config/scripts/mobile-web-app-mermaid-render.test.mjs @@ -0,0 +1,564 @@ +/** + * Mermaid rendered in the page, in a real browser, under the policy the shell ships. + * + * The native component seals an untrusted diagram inside a `WebView` whose document embeds the + * whole engine as a string. The page has no second content process, so what replaces it is + * `import('mermaid')` on demand and mermaid's own `securityLevel: 'strict'` output. That makes + * three claims this file measures rather than asserts: that rendering violates no directive and + * asks for no JIT, that what the page paints is the diagram the phone already paints, and that a + * hostile diagram reaches the document inert. + * + * The equality oracle is the native `buildHtml` itself, bundled and served as its own document in + * the same browser. Two differences survive and are normalised away: the diagram id (mermaid's own + * `mermaid-` on the native path, the component's `useId` on the page) and the `xmlns:xlink` + * declaration the native document's `innerHTML` serialization adds. Everything else — the viewBox, + * the `max-width`, the injected `