Files
orca/config/scripts/mobile-web-app-session-terminal-closure.test.mjs
Jinwoo Hong d9954000b3 refactor(mobile): the rich editor's document becomes scope-threaded modules and a bundled factory (OTA phase C, C7.10 C1) (#21969)
* refactor(mobile): split the rich editor document's stylesheet and markup apart

The body constant carried the tail of a `:root` block, every CSS rule and the
editable surface's markup in one string, which only the HTML builder could
splice. A page mounting the document needs the stylesheet and the markup
separately, so they become a function over the theme and a constant.

Byte-for-byte inert: `mobile-rich-markdown-editor-document.test.ts`'s digest of
the shipped document is unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): give the keyboard-inset normaliser its own module

It is the host's half of the inset, read by the controller, and it sat in the
module holding the document's in-page script. The script is about to become
ordinary TypeScript under `rich-markdown/`, where a native-side normaliser does
not belong.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): the rich editor's document becomes scope-threaded modules and a factory

The editor's ~600-line program lived in seven string constants a concatenator
glued into one `<script>`: unreadable, untypeable, and unreachable from a page,
which is where the OTA shell has to run it (ruling 26).

It is now ordinary TypeScript under `src/components/rich-markdown/`. Every
function that touches editor state takes `scope: RichMarkdownEditorScope` first,
`createRichMarkdownEditorDocument(host)` builds the scope, runs the start
sequence and returns `{ send, stop }`, and the six window reads the script did
are host seams with those reads as their defaults: `postToHost`, `promptForUrl`,
`keyboardInsetSource`, `clearTimer`, `getSelection`, `getDocument`.
`runCommand` is async because a host that answers the URL prompt with a modal
cannot answer synchronously; the thirteen commands that never wait stay one
synchronous act.

No module holds a `let` and none does work at parse time (rulings 20, 21), so a
second mount starts from its own state and `stop` takes back both the surface's
four listeners and the viewport's two.

The native document is an esbuild IIFE bundle of `native-document-entry.ts`,
written beside the terminal document's artifact by a fifth postinstall
generator. Nothing ships it yet: the HTML builder still splices the old strings,
which the next commit changes.

Red-first: `rich-markdown-document-parse-time.test.ts` and
`rich-markdown-host-seams.test.ts`. Their readers are the terminal census's,
extracted to `src/test-support/webview-document-census.ts` and pointed at both
documents rather than copied.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): ship the bundled document and retire the editor's script strings

`buildMobileRichMarkdownEditorHtml` splices the esbuild bundle of
`src/components/rich-markdown/`, and the seven string constants and their
concatenator go. `escapeInjectedJavaScriptString` stays: it is the escape for
`injectJavaScript`, which is still how the native host reaches the document.

Equivalence, since a byte golden over the script cannot survive a bundler:

- `rich-markdown/native-document-bundle.test.ts` evaluates the shipped artifact
  exactly as the WebView does — its markup, its bridge, its `execCommand`, its
  `prompt`, its `visualViewport` — and drives it through the injected handle:
  `keyboardInset` then `ready`, all five members, a markdown round trip through
  the real escape, an edit under the host's generation, every toolbar command's
  engine verb, the `javascript:` refusal, a tapped link, and the module list.
- `mobile-rich-markdown-editor-document.test.ts` keeps a byte pin, now over the
  page around the document. Measured on main's own document with its script
  region removed and on this one: 5,621 bytes, both
  `5054e1d5c87e4ce1805d4856ddc8bf36804e697675e6013d84da453d3e81af25`. The
  whole-document digest it replaces was `1ef29c88…`, 29,852 bytes.

Every assertion `mobile-rich-markdown-editor-html.test.ts` made by extracting
functions out of the emitted text is kept, aimed at the modules:

- nested/ordered/task list rendering and serialization, entities, explicit
  numbering, the parent-start fallback, read-only checkboxes →
  `markdown-round-trip.test.ts`, over real elements rather than shaped objects.
- the emitChange/setEditable guards and the generation carried through a
  replacement → `editor-content.test.ts`, behaviourally.
- dismissKeyboard, the tapped caret, the label tap, the restored caret, the
  end-of-document fallback, the detached caret → `editor-selection.test.ts`,
  with a blur that drops the ranges the way WebKit does.
- parseable script and the injection escape stay in the HTML test.

New with the factory: `document-lifecycle.test.ts` — stop takes the four surface
listeners and the viewport observer off, a second mount is its own document, two
documents do not share `editable`, and a start that throws unwinds.

`use-mobile-rich-markdown-editor-controller`, `MobileRichMarkdownEditor` and the
web fallback tests are untouched and green.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read the document's mutable bindings from the tree, not the line start

The census matched `/^(let|var) /gm`, so `export let`, a declaration indented
inside a top-level block and a `for (let …)` head were all invisible — three
shapes of the one binding two documents would share — and its single
precondition proved only the shape it could already see.

`moduleLevelMutableBindings` walks the program instead and stops at every
function body, because a binding one call owns is not module state. Its
preconditions are one per shape, with the kind each reports, and a negative case
over a `const` and a function-local `let`/`var` so the empty list is a
measurement rather than a reader that refuses everything.

Red-first: `export let pendingReport = 0` planted in `keyboard-inset.ts` reds it
with `keyboard-inset: let pendingReport`, which the old matcher passed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): pin both WebView document bundles to the mobile root

esbuild writes each module's path into a bundle as a comment relative to the
working directory, and neither generator set `absWorkingDir`. So the artifact's
bytes followed the cwd of whatever postinstall run wrote it: measured from the
repo root, `mobile/`, and `mobile/src`, three digests — and from outside the
repo the comments carried `/Users/<name>/…`, a machine path in the one file
every bundle test compares against a build it makes itself.

Both generators now pin the mobile root, so the four cwds measured agree, and
both bundle tests carry the pin: a digest built in a child process from the OS
temp directory equals the committed artifact's, and no comment in either
artifact is an absolute path or climbs out with `../`.

`build-terminal-document-script.mjs` had the defect verbatim on main; C1 copied
its shape, so both are fixed here rather than leaving the original to be found
again. Neither artifact's bytes move: both were generated from `mobile/`, which
is what `absWorkingDir` now names.

Red-first: deleting the `absWorkingDir` line from either generator reds that
generator's case.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): cover the getSelection seam's override, not just its default

Five of the six seams had both halves and this one had only its window default,
which is the half that cannot fail on the page: there the caret has to come from
the object the host hands over, because a document mounted inside a screen
shares `window` with every other field on it.

The case gives the document a selection of its own, blurs the surface the way
WebKit does — dropping the ranges, which is the whole reason a caret is saved —
and reads the restored caret back out of the host's object. The window's own
selection stays empty throughout, which is what says the default was never
consulted.

Red-first: `rememberSelection` reading `window.getSelection()` instead of the
field reds it; every other case in the file stays green.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): make the editor document's stop cancel its pending timer

`stop` took the surface's four listeners and the viewport's observer off and
left the input timer, while the scope kept the handle and the `clearTimer` seam
kept the means to cancel it. A listener comes off with the element it was on; a
scheduled callback holds the scope and fires into a document the host has
already unmounted, posting a change under the generation of content it has
replaced.

`stopEditorContent` cancels it through the seam and clears the field, and the
sequence runs it last — after the listeners that could have scheduled another
one are gone.

Nothing schedules the handle today. The cancel is here because the seam and the
field exist for the day something does, and that is not the moment to discover
`stop` never reached it. The case plants the pending change rather than waiting
for a debounce, and carries its own control: the same timer posts while the
document is running, and posts nothing once it is stopped.

Red-first: dropping `stopEditorContent` from the sequence reds both that case
and the parse-time census's start/stop set comparison.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): correct the postinstall generator count in both censuses

Two comments said four generators and six generated files. There are five
generators writing six files, and the six are not the six either comment
described: `census-source-files.ts` still named the page's copy of the terminal
document, which ruling 25 retired and #21962 stopped ignoring, while C7.10 C1
added the rich Markdown editor's.

Both now name the lists of record — `mobile/package.json`'s postinstall for the
generators, `mobile/.gitignore` for the files — and say the count is a reading
that grows rather than a fence, which is what made the old numbers wrong twice
over.

Verified against both lists: 5 and 6.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): say which digest is the document and which is the page around it

The docstring put main's whole-document digest and byte count in the sentence
introducing the shell pin, so it read as if `1ef29c88…` and 29,852 bytes were
what the constant below asserts. They are not: that digest is of main's whole
document, script included, and nothing in the file reproduces it. The constant
is of the document with its `<script>` region emptied, taken on main's document
and on this one.

Both are now named and separated, with what each covers and why the shell one
was read twice.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): name the parse-time fixture by its role

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): drop an editor command whose dialog answered after the host moved on

C7.10 C1 made `runCommand` async so a host can answer the URL prompt with a
modal. Inside the WebView that changes nothing — `window.prompt` resolves within
a microtask, and the host reaches the document through `injectJavaScript`, which
is a later task — but on the page the modal is a real task boundary, and while
it is open the host can replace the content, make the editor read-only or
unmount it entirely. The continuation ran anyway: `createLink` against markdown
nobody chose, and a change posted under the new generation carrying an edit made
against the old one.

`acceptsCommands` is the question both halves ask: not stopped, still editable,
still the same generation, still contenteditable. `insertUrl` asks it before
`execCommand` and `runCommand` asks it again before emitting, each against the
generation read before its own wait.

The scope gains `stopped`, which `stopRichMarkdownEditorDocument` sets.

Inert on native, where no state can change across a microtask, so the answer to
both questions is the one the old code assumed.

Red-first: with either check removed, the new case reports
`[ 'createLink', 'createLink' ]` against `[ 'createLink' ]`. The case carries its
own control — an answer that arrives while nothing has moved is still applied
and still reported.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): make the editor's block reader always consume a line

`markdownToHtml` looped forever on `# `, `- ` and `1. `. `isBlockStart` admits a
marker followed by a space, and the list test admits the same, but the heading
reader requires text after the hashes and `parseListLine` requires text after
the marker — so on those lines the list branch consumed nothing and returned the
index it was given, and the paragraph loop gathered nothing and pushed an empty
paragraph without advancing. A one-line file the host handed to `setMarkdown`
froze the WebView.

Two guards, both by the same rule: a branch may only commit if it moved the
index. The list branch falls through when its run is empty, and the paragraph
falls back to the line itself when it gathered none.

Present on main verbatim, so this is inherited rather than introduced — but the
fix is observationally inert, because the only inputs it changes are the ones
that previously never returned. Every input that produced output produces the
same output.

Evidence, from a probe that bounds the loop from the inside rather than waiting
on it: before, `# ` and `- ` both UNBOUNDED; after, twenty marker and fence
shapes all return. The pinned cases carry their own control, `# ok` and `- ok`,
so the fallback is not swallowing the readers it falls back from.

A red-first case is not possible here: without the fix the case does not fail,
it hangs the worker. The probe above is the measurement.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): see every declaration that runs as a document module is evaluated

The parse-time reader inspected only variable declarations, while
`DECLARATION_KINDS` admits classes and default exports. So
`class A { static value = install() }`, a static block, and
`export default install()` all passed a census whose whole job is to refuse
exactly that — and a static field reading `document` passed too, which is the
remount defect the rule exists for, wearing a different shape.

Three shapes now, each reported by what it does rather than what it looks like:
a variable initialiser, a class's static members, and a default export that is
an expression. `DECLARES_WITHOUT_RUNNING` keeps the last one from walking into
the body of `export default function () {}`, whose calls run when something
calls it.

The preconditions are one per shape, with a negative case beside them: an
instance field runs per `new` and nothing in a document is ever constructed, and
a default-exported function declares a body rather than running one.

Inherited from the terminal's census, which had the same reader; both use this
one, and both are green.

Red-first: removing the class branch reds the new precondition case.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read lifecycle exports from the tree, not from one exact spelling

The reader was a regular expression needing `export function`, one line, the
scope parameter and no return type. `export async function startX(`, a return
type, or a parameter list the formatter wrapped made a real lifecycle export
vanish — and the comparison it feeds is a set against the names the sequence
calls, so a function missing from *both* lists makes them agree. A start nobody
runs would have read as a start nobody needs.

It now qualifies a function by what it is: exported, named for its lifecycle,
and taking the document's scope as its only parameter. That last clause is
ruling 20's own wording — a start takes nothing the scope does not already carry
— and the regex was enforcing it by accident, through the single parameter its
pattern happened to allow.

Surfaced by the change: the terminal's `startEdgeScroll(scope, dir)`, which the
regex never matched and the sequence never calls. It takes a direction, so it is
the overlay's act for a drag rather than a module's lifecycle, and the one-
parameter rule refuses it for the stated reason instead of by accident. Both
censuses are green.

Red-first: restoring the regex reds the new precondition case, which covers
`async`, a return type and wrapped parameters, with refusals beside them for a
two-parameter start, another document's scope type, and an unexported function.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-21 07:16:23 -04:00

257 lines
15 KiB
JavaScript

import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs'
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
import { mobileWebAppRouteChunkClosure } from './mobile-web-app-route-chunk-closure.mjs'
import {
textInputFontSizeOffenders,
unresolvedTextInputStyles
} from './mobile-web-app-text-input-font-size-seam.mjs'
/**
* What putting the terminal on the page costs the session route's closure.
*
* The route is not served on the page until C7.7 — its module is still the native switch and
* there is no `.web.tsx` beside it — but the closure the bundler would walk is the same one, and
* the terminal is by far the largest thing in it. Measured here so the trade is a number rather
* than a claim, and so that a later change cannot quietly put the engine string back.
*
* Re-anchored on main at 4a3a32206d, the squash that landed C7.5b, and re-measured there. The
* reading has been re-taken at each merge rather than adjusted, because the arithmetic keeps not
* working: main has re-pinned this count three times for modules that arrived from three other PRs,
* and a number carried forward would have been wrong about every one of them.
*
* modules 4286 -> 4328 (+42)
* local modules 936 -> 978 (+42)
*
* The +42 is this lane's, and it is 43 modules in and one out. Out:
* `terminal-webview-document-factory.generated.ts`, the one emitted file C7.5b's page imported,
* which carried the whole document. In: the document's 39 source modules the page imports directly
* under ruling 25, the three page modules their tap group reaches — `terminal-webview-url-tap`,
* `terminal-path-tap` and `terminal-file-url-tap` — which the generator used to substitute as
* literals, and `terminal-text-scales`, the leaf the presets moved to so the WebView's own bundle
* cannot reach `storage/preferences` and the AsyncStorage import behind it. Nothing generated is in
* this reading now: the phone's script is built from these same modules and is not imported here.
*
* The three modules the base gained, none of them this branch's and all of them in its 4286 by
* main's own route:
*
* - `src/mobile-web-shell/bridge/bridge-haptics-notify.ts`, which `haptics.web.ts` reaches. C7.10
* item E (#21864) and mermaid (#21871) were each green against a main that lacked the other, so
* main held 4323 while measuring 4324, and #21908 re-pinned it there.
* - `src/mobile-web-shell/bridge/bridge-page-route-grants.ts` and the
* `mobile-web-bundle/manifest-contract.ts` whose grant grammar it imports rather than restates,
* both C2.9's. They reach every page closure through `bridge-envelope.ts`, which the page reads
* to parse `init`, so this count moves for any route the page serves and not for the session
* alone.
*
* Which is the point of re-measuring rather than summing. The merged total was one above the sum
* the first time, when main had drifted the haptics module after recording its own number, and a
* sum would have read 4283 and been wrong about a module neither side of that merge touched.
*
* Both sides read with `mobileWebAppRouteClosure(SESSION_ROUTE)` and every postinstall generator
* `mobile/package.json` names run first — five at this reading, since C7.10 C1 added the rich
* Markdown editor's document, and the list is read there rather than counted from here because it
* grows. The before side is a scratch worktree detached at the same sha, and the
* three modules above read out of the after side's list by name rather than inferred from the
* total. Measured rather than taken from main's pin because the pin covers only the module count,
* so the local count beside it would otherwise be a number nobody had read.
*
* The byte reading is not re-measured and stays anchored where it was taken, against main at
* ec82173130: 3,768,122 -> 3,766,312 minified (-1,810). `mobileWebAppRouteClosure` reads
* `metafile.inputs` and returns no byte total, so a figure produced here would be a different
* computation rather than a newer reading of that one.
*
* The bytes fall because threading the scope deletes a closure: every function names its state as a
* parameter, and a parameter minifies to one character where a shared module-level object could not.
* Folding the seventeen never-written fields out of that object takes the rest: a constant read
* through `scope.X` is a property access the minifier must keep, and the same constant as a module
* `const` is inlined.
*
* What moved is which files carry the document, not whether the page carries it. C7.5 put the
* document's own source modules in this closure and started them per mount; ruling 23 gave the
* page the factory the WebView's script was generated from, so the same program arrived as one
* emitted file and its 41 inputs leave. The bytes barely move because it is the same program: what
* goes is the import and export plumbing between the modules, and what the generator substitutes.
*
* The two commits inside the -40, because only one of them is the factory arriving: making the
* document a factory put the `host` argument on `createTerminalDocumentScope`, the lane's only edit
* to a module this closure already carried, and cost 80 bytes on its own -- 3,768,202 measured at
* that commit. The -1,890 from there is the page importing the emitted factory instead.
*
* xterm was already a static import of the mount before this, so nothing here is xterm arriving: it
* and its two addons are 607,945 bytes minified ESM on their own, and they are on both sides of the
* reading above.
*
* Two earlier readings of the same measurement, against the bases this branch sat on before:
* -47,255 at 51ae7b1b03 and -55,561 at 0ce0fc99a2. They differ because C7.1's own round-1 fold
* deleted `URL_TAP_WEBVIEW_JS` from a module only the page's component brings into this closure,
* so the saving lands on the after side and no base can show it.
*
* Then C7.10 item B put mermaid on the page, and the module list moved again. Its own reading, at
* the base it was taken against:
*
* modules 4320 -> 4323 (+3)
* local modules 970 -> 973 (+3)
*
* Three modules: the configuration both hosts read, the loader, and the pre-bundled engine the
* loader imports on demand. The engine's own 66 files and the d3, dagre, katex and cytoscape trees
* under them are inside that one artifact rather than in this graph, which is why the count barely
* moves. Importing the package here instead read +2,056 and emitted 103 scripts, a package
* splitting along its own lazy diagram-type boundaries -- every one of them inside the OTA generation
* the phone had already downloaded, so the split moved no bytes and spent 103 of the 256 manifest
* assets the shell will load. One artifact costs one script and one module.
*
* What the generation weighs, because every chunk ships in it whether or not a phone ever fetches
* one: the built bundle is 8,016,714 bytes across 112 assets, against the 9 MiB ceiling in
* `verify-mobile-web-app-bundle.mjs`. That is 84.9% of it, with 1,420,470 bytes left for the rest
* of C7.10 and for C7.7. Before item B the same bundle was 4,539,090 bytes, and the engine is the
* difference -- deferring it defers evaluation and a fetch, never the download.
*
* `mobileWebAppRouteClosure` reads `metafile.inputs`, which holds dynamically imported modules
* under `splitting: true` just as it does under `splitting: false`, so it cannot express "on
* demand" about anything. Ruling 28: the fence for this route is `entryStaticClosure`, which
* follows `import-statement` edges only, and the module list's total is a recorded number rather
* than a budget. It moves whenever main adds a module this route reaches, and is re-recorded rather
* than argued with.
*/
const projectDir = fileURLToPath(new URL('../..', import.meta.url))
const mobileDir = join(projectDir, 'mobile')
const SESSION_ROUTE = 'app/h/[hostId]/session/[worktreeId].tsx'
/** Gone with the WebView: string literals of a program the page has no way to run. */
const SHED = [
'src/terminal/TerminalWebView.tsx',
'src/terminal/terminal-webview-engine.generated.ts',
'src/terminal/terminal-webview-document-script.generated.ts',
'src/terminal/terminal-webview-html.ts',
'src/terminal/terminal-webview-html/document-shell.ts',
'src/terminal/terminal-webview-html/document-close.ts'
]
/** The component, its mount, the stylesheet and the markup, and the modules the splits made. */
const GAINED_OUTSIDE_THE_DOCUMENT = [
'src/terminal/TerminalWebView.web.tsx',
'src/terminal/terminal-web-document-mount.ts',
'src/terminal/terminal-webview-engine-css.generated.ts',
'src/terminal/terminal-webview-html.web.ts',
'src/terminal/terminal-webview-html/document-markup.ts',
'src/terminal/terminal-webview-html/document-style.ts',
// The page's half of the stylesheet: the document-level rules are dropped and the rest is held
// under the host, so what the page injects can only reach what the terminal owns.
'src/terminal/terminal-webview-html/document-style-scoping.ts',
'src/terminal/terminal-webview-ready-promises.ts',
'src/terminal/use-terminal-webview-controller.ts'
]
const XTERM_PACKAGES = ['@xterm/xterm', '@xterm/addon-unicode11', '@xterm/addon-webgl']
/**
* The 16 px seam's verdict for this route, which C7.5 must leave exactly where C7.2 left it.
*
* Design §3 counted nine inputs under the floor here and C7.2 moved all nine onto the seam, so the
* answer is now none. Asserted rather than left unmeasured because the terminal's own modules
* joining this closure is precisely the kind of change that could add a tenth unread.
*/
const EXPECTED_OFFENDERS = 0
/** The deferred engine, as the page reaches it: one artifact, not the package's own file tree. */
const MERMAID_PAGE_ENGINE = 'src/components/pr-sidebar/mermaid-page-engine.generated.ts'
const MERMAID_PACKAGE = 'node_modules/mermaid/'
/**
* The module list on the merge, recorded at the base in the docstring above, which is where every
* part of it is accounted for: the document's own modules replacing the factory that carried them,
* mermaid's three, and the three bridge modules #21908 and C2.9 pin on main.
*
* Then C7.10 item D put dictation's capture on the page, and the list moved down rather than up.
*
* modules 4328 -> 4323 (-5)
* local modules 978 -> 981 (+3)
*
* Three local modules join — `src/platform/dictation-capture.web.ts`, its contract
* `src/platform/dictation-capture-contract.ts`, and the verb shapes in
* `src/mobile-web-shell/bridge/bridge-audio-verbs.ts` — and eight vendored ones leave, because the
* capture seam is what stops the page importing a microphone it does not have. Five are
* `@orca/expo-two-way-audio` (its web module, `core`, `events`, `hooks` and the index) and three
* are `expo-keep-awake`; the page asks the shell for both over `native.audio.start|read|stop` and
* `native.wakelock.set` instead. The native halves of the seam resolve out of this closure
* entirely, which is the -8 + 3.
*
* Measured, not derived: `mobile-web-app-session-dictation-capture.test.mjs` moves the web file
* aside and walks the closure again, which puts those eight back.
*/
const SESSION_ROUTE_MODULES = 4323
const artifactModules = (inputs) => inputs.filter((input) => input.includes(MERMAID_PAGE_ENGINE))
const packageModules = (inputs) => inputs.filter((input) => input.includes(MERMAID_PACKAGE))
const bundles = mobileWebAppDependenciesPresent()
const describeClosure = bundles ? describe : describe.skip
describeClosure(
"the session route's page closure with the terminal on it",
() => {
it('gains the document, xterm and the addons, and sheds the engine string', async () => {
const { local, modules } = await mobileWebAppRouteClosure(SESSION_ROUTE)
for (const gone of SHED) {
expect(local, `${gone} is still in the closure`).not.toContain(gone)
}
for (const gained of GAINED_OUTSIDE_THE_DOCUMENT) {
expect(local, `${gained} is not in the closure`).toContain(gained)
}
for (const name of XTERM_PACKAGES) {
expect(
modules.some((module) => module.includes(`node_modules/${name}/`)),
`${name} is not in the closure`
).toBe(true)
}
// The document, whole, and as modules: ruling 25 makes it ordinary TypeScript that the page
// imports and calls, so the closure carries every module — including `message-bridge`, whose
// two host facts are seams now — and nothing generated at all.
const documentModules = local.filter((module) => module.startsWith('src/terminal/document/'))
expect(documentModules).toContain('src/terminal/document/create-terminal-document.ts')
expect(documentModules).toContain('src/terminal/document/message-bridge.ts')
expect(documentModules.length).toBeGreaterThanOrEqual(36)
// The bundle and its entry belong to the phone: a page reaching either would ship the
// document twice, once as modules and once as a string.
expect(documentModules).not.toContain('src/terminal/document/native-document-entry.ts')
expect(local).not.toContain('src/terminal/terminal-webview-document-script.generated.ts')
}, 300_000)
it('reaches the engine as one deferred module and never as part of the download', async () => {
const { modules } = await mobileWebAppRouteClosure(SESSION_ROUTE)
// The engine is here, as the one artifact the loader imports.
expect(artifactModules(modules)).toHaveLength(1)
// And the package's own file tree is not, anywhere: it is inside that artifact. Meaningful
// only beside the line above, which is why the two sit together.
expect(packageModules(modules)).toEqual([])
expect(modules).toHaveLength(SESSION_ROUTE_MODULES)
const download = await mobileWebAppRouteChunkClosure(SESSION_ROUTE)
// The fence: nothing of the engine is reachable from the route's own chunk by an import
// statement, so opening the session pays none of it.
expect(artifactModules(download.staticInputs)).toEqual([])
// The precondition that absence needs. The artifact is in the bundle, in a chunk the route
// reaches by a `dynamic-import` edge instead -- a deferred engine, not a dropped one.
expect(artifactModules(download.deferredInputs)).toHaveLength(1)
// And the walk read a real download rather than one chunk: the route's own chunk is in it.
expect(download.staticChunks).toContain(download.routeChunk)
expect(download.staticInputs.length).toBeGreaterThan(1000)
}, 600_000)
it('leaves the 16px seam census exactly where C7.2 left it', async () => {
const closure = await mobileWebAppRouteClosure(SESSION_ROUTE)
// Two preconditions, because zero offenders is what a walk that read nothing also reports:
// the seam's own web module has to be in the closure, and no style may be unresolved.
expect(closure.local).toContain('src/platform/text-input-font-size.web.ts')
expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([])
expect(textInputFontSizeOffenders(mobileDir, closure)).toHaveLength(EXPECTED_OFFENDERS)
}, 300_000)
},
900_000
)