Files
nexu-io__open-design/apps/web/tests/runtime/export-injection-points.test.ts
lefarcen 7f65c795d9 refactor: share one structural HTML injection-point locator (#7422)
* refactor: share one structural HTML injection-point locator

#7410's root cause was two copies of the same lookup drifting apart: the srcDoc
path anchored on the real `</body>` while the daemon copy still matched the
first one in the text. This puts the locator in `packages/contracts` and points
every injector at it — daemon preview bridges and containment base, web srcDoc
and export paths, desktop PDF/artifact/deck capture, and the brand build.

Rebuilt on `main` rather than rebased onto it. This branch was cut before #7421
and carried its 28 commits, so a rebase replayed them against a main that
already had them and conflicted on the first one; replaying 17 commits through
that would have been the easy way to silently drop one of the fixes. The net
change is the same 26 files, and only four of them had also moved on main.

The shared module is the daemon implementation as merged, not a reconstruction.
Verified as equivalence before the daemon's copy was deleted: against `main`'s
`apps/daemon/src/routes/project/index.ts` on identical inputs, 2,544 repository
HTML files × 7 lookups and 15,000 generated adversarial documents × 6 lookups
produce byte-identical answers — 0 divergences. The generated corpus is built
from every hazard #7421's review rounds turned up, which is the comparison that
would have caught the drift this PR exists to prevent.

Three call sites finished their migration on the way: `daemonSanitizeTitleInDoc`,
`assembleExample` and the collab-sync title read all located `<title>`
structurally and then searched for its close as text, so `</title >` and
`</title\n>` were missed and a `</title` inside the open tag's own attributes
could match first. All three go through `findRealElementRange` now.
`findRealElementRange` itself built its shadow with `toLowerCase()`, which is
not length-preserving; it uses `asciiLower` like the rest of the module.

`pnpm guard` gains an "HTML structural boundaries" check that fails any new
hand-rolled boundary lookup outside the shared module — the safeguard #7421's
rationale comment once claimed and did not have.

Validation: 100 daemon route/containment specs (#7421's full set, now running
against the shared locator), 45 contracts specs, 817 web runtime specs including
the jsdom differential oracle, guard exit 0 with the new check reporting, daemon
and contracts typecheck clean.

* fix: finish the title migration in srcdoc and the prefetch reader

Two from review, both the same leftover: the open tag was located structurally
and then the close was searched for as text. I had claimed this migration
finished in three call sites and it was five — a sweep of the repository for
`indexOf('</…')` found exactly these two remaining, and nothing else (the other
hits are `<artifact>` / `<PANELIST>` marker streams, which are not HTML).

`sanitizeTitleInDoc` used `indexOf('</title>')`, which accepts one spelling. A
document titled with `</title >` fell through to the next `</title>` in the
source — here one inside an authored script string — and the rewrite spanned
everything between, splicing into author content exactly as #7410 did.
`readRealTitleText` used `indexOf('</title')` with no boundary check, so
`</title-page>` ended the title. Both go through `findRealElementRange` now, and
with it the `toLowerCase()` shadow both were building, which is not
length-preserving.

Two specs. The `</title >` one is red on the previous commit. The
`</title-page>` one is green on both — `indexOf('</title>')` happened to skip it
too, since it demands the full spelling — so it is regression cover rather than
a red spec, and it is worth having because the new code reaches that case
through a different rule.

Writing them cost three wrong assertions before one held, all mine rather than
the code's: the title text is rewritten by `sanitizePreviewTitle`, so the raw
authored bytes are not observable in the output, and `b</title>` — my second
attempt at something that would be — is a substring of the sanitized
`a-title-page-b</title>`. The spec asserts the sanitized title itself now.

Validation: 819 web runtime specs, 100 daemon route/containment specs, `pnpm
guard` exit 0 with the structural-boundary check reporting, web and daemon
typecheck clean.
2026-08-27 04:53:21 +00:00

65 lines
3.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
injectDeckPrintStylesheet,
injectPrintReadyHandshake,
injectPrintScript,
} from '../../src/runtime/exports';
import { upsertSpeakerNotesInHtml } from '../../src/runtime/speaker-notes';
// Export splices bridges into the artifact's own bytes exactly like the preview
// transports do, so it carries the same hazard: a prototype that builds an HTML
// document string writes `</head>` and `</body>` as ordinary content. #7410 was
// reported against preview only because `od export` skips the daemon's
// URL-preview injection — the export path had the same defect all along.
//
// Each fixture puts the authored copy of the boundary *before* the document's
// own, which is the only arrangement a first-match injector gets wrong. These
// run with no `DOMParser` (the web suite's default environment is node), so
// each injector is judged on its own rather than on an upstream round-trip.
const AUTHORED = 'const doc = `<head><title>Slip</title></head><body>slip</body>`;';
/** The authored copy sits in `<head>`, ahead of the document's own `</head>`. */
const HEAD_FIRST = '<!doctype html><html><head>'
+ `<script>${AUTHORED}<\/script><title>Deck</title></head>`
+ '<body><div class="slide">one</div></body></html>';
/** The authored copy sits in `<body>`, ahead of the document's own `</body>`. */
const BODY_FIRST = '<!doctype html><html><head><title>Deck</title></head><body>'
+ `<script>${AUTHORED}<\/script><div class="slide">one</div></body></html>`;
/** The authored script's text, as the parser would read it after injection. */
function authoredScript(html: string): string {
const start = html.indexOf('const doc = `');
return html.slice(start, html.indexOf('<\/script>', start));
}
describe('export injection points', () => {
it.each([
['print script', HEAD_FIRST, (doc: string) => injectPrintScript(doc, 'Title')],
['print-ready handshake', HEAD_FIRST, (doc: string) => injectPrintReadyHandshake(doc, 'nonce-1')],
['deck print stylesheet', HEAD_FIRST, (doc: string) => injectDeckPrintStylesheet(doc)],
['speaker notes', BODY_FIRST, (doc: string) => upsertSpeakerNotesInHtml(doc, ['note'])],
])('leaves a script that builds an HTML document string intact — %s', (_name, doc, inject) => {
const out = inject(doc);
expect(out).toContain(AUTHORED);
expect(authoredScript(out)).toBe(AUTHORED);
});
it('puts speaker notes in the real body, after the deck content', () => {
const out = upsertSpeakerNotesInHtml(BODY_FIRST, ['note']);
expect(out.indexOf('id="speaker-notes"')).toBeGreaterThan(out.indexOf('class="slide"'));
expect(out.indexOf('id="speaker-notes"')).toBeLessThan(out.lastIndexOf('</body>'));
});
it('puts the deck print stylesheet in the real head', () => {
const out = injectDeckPrintStylesheet(HEAD_FIRST);
expect(out.indexOf('data-deck-print')).toBeLessThan(out.lastIndexOf('</head>'));
expect(out.indexOf('data-deck-print')).toBeGreaterThan(out.indexOf('<title>Deck</title>'));
});
});