Files

1851 lines
90 KiB
TypeScript
Raw Permalink Normal View History

import http from 'node:http';
import { createHash } from 'node:crypto';
import { mkdtempSync, rmSync } from 'node:fs';
import { mkdir, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { createRequire } from 'node:module';
import { parseByteRange, resolveProjectFilePath } from '../src/projects.js';
import { startServer } from '../src/server.js';
import { load } from 'cheerio';
// Keep DOM globals out of the daemon's Node/undici type environment, as in the
// other daemon JSDOM tests. Only script evaluation and lifecycle cross this seam.
const { JSDOM } = createRequire(import.meta.url)('jsdom') as {
JSDOM: new (html: string, options: { url: string; runScripts: 'outside-only'; pretendToBeVisual: boolean }) => {
window: { eval(source: string): unknown; close(): void };
};
};
// ---------------------------------------------------------------------------
// parseByteRange — RFC 7233 unit tests
// ---------------------------------------------------------------------------
describe('parseByteRange', () => {
it('returns null when header is undefined', () => {
expect(parseByteRange(undefined, 1000)).toBeNull();
});
it('returns null when header is an empty string', () => {
expect(parseByteRange('', 1000)).toBeNull();
});
it('returns null for non-bytes unit', () => {
expect(parseByteRange('none=0-100', 1000)).toBeNull();
});
it('returns null for multi-range (caller falls back to full 200)', () => {
expect(parseByteRange('bytes=0-100, 200-300', 1000)).toBeNull();
});
it('parses a standard start-end range', () => {
expect(parseByteRange('bytes=0-499', 1000)).toEqual({ start: 0, end: 499 });
});
it('clamps an over-long end to fileSize - 1', () => {
expect(parseByteRange('bytes=0-9999', 1000)).toEqual({ start: 0, end: 999 });
});
it('parses an open-ended range (bytes=N-)', () => {
expect(parseByteRange('bytes=500-', 1000)).toEqual({ start: 500, end: 999 });
});
it('parses a suffix range (bytes=-N)', () => {
expect(parseByteRange('bytes=-200', 1000)).toEqual({ start: 800, end: 999 });
});
it('clamps suffix larger than fileSize to the whole file', () => {
expect(parseByteRange('bytes=-9999', 1000)).toEqual({ start: 0, end: 999 });
});
it('returns unsatisfiable when start equals fileSize', () => {
expect(parseByteRange('bytes=1000-1999', 1000)).toBe('unsatisfiable');
});
it('returns unsatisfiable when start exceeds fileSize', () => {
expect(parseByteRange('bytes=5000-5999', 1000)).toBe('unsatisfiable');
});
it('returns unsatisfiable for a zero-length suffix range (bytes=-0)', () => {
expect(parseByteRange('bytes=-0', 1000)).toBe('unsatisfiable');
});
it('returns unsatisfiable for a negative suffix', () => {
expect(parseByteRange('bytes=--1', 1000)).toBe('unsatisfiable');
});
it('returns null for non-integer start', () => {
expect(parseByteRange('bytes=1.5-499', 1000)).toBeNull();
});
it('returns null for non-integer end', () => {
expect(parseByteRange('bytes=0-499.9', 1000)).toBeNull();
});
it('returns null when end < start', () => {
expect(parseByteRange('bytes=500-100', 1000)).toBeNull();
});
it('returns null for alphabetic range values', () => {
expect(parseByteRange('bytes=abc-xyz', 1000)).toBeNull();
});
it('handles a single-byte range (bytes=0-0)', () => {
expect(parseByteRange('bytes=0-0', 1000)).toEqual({ start: 0, end: 0 });
});
it('handles a range that exactly covers the last byte', () => {
expect(parseByteRange('bytes=999-999', 1000)).toEqual({ start: 999, end: 999 });
});
});
// ---------------------------------------------------------------------------
// resolveProjectFilePath — integration test (real temp files)
// ---------------------------------------------------------------------------
describe('resolveProjectFilePath', () => {
let projectsRoot = '';
const projectId = 'proj-range-test';
beforeEach(async () => {
projectsRoot = mkdtempSync(path.join(tmpdir(), 'od-range-'));
const dir = path.join(projectsRoot, projectId);
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, 'clip.mp4'), Buffer.alloc(2048));
await writeFile(path.join(dir, 'index.html'), '<html/>');
});
afterEach(() => {
if (projectsRoot) rmSync(projectsRoot, { recursive: true, force: true });
});
it('returns the correct size and mime for a video file', async () => {
const result = await resolveProjectFilePath(projectsRoot, projectId, 'clip.mp4');
expect(result.size).toBe(2048);
expect(result.mime).toBe('video/mp4');
expect(result.kind).toBe('video');
expect(path.isAbsolute(result.filePath)).toBe(true);
});
it('returns the correct mime for an html file', async () => {
const result = await resolveProjectFilePath(projectsRoot, projectId, 'index.html');
expect(result.mime).toBe('text/html; charset=utf-8');
});
it('throws ENOENT for a missing file', async () => {
await expect(
resolveProjectFilePath(projectsRoot, projectId, 'missing.mp4'),
).rejects.toMatchObject({ code: 'ENOENT' });
});
it('rejects path traversal attempts', async () => {
await expect(
resolveProjectFilePath(projectsRoot, projectId, '../other-project/secret.mp4'),
).rejects.toThrow();
});
it('rejects symlink escapes inside managed projects', async () => {
const outsideRoot = mkdtempSync(path.join(tmpdir(), 'od-range-outside-'));
try {
await writeFile(path.join(outsideRoot, 'secret.txt'), 'secret');
await symlink(
path.join(outsideRoot, 'secret.txt'),
path.join(projectsRoot, projectId, 'linked-secret.txt'),
);
await expect(
resolveProjectFilePath(projectsRoot, projectId, 'linked-secret.txt'),
).rejects.toMatchObject({ code: 'EPATHESCAPE' });
} finally {
rmSync(outsideRoot, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// GET /api/projects/:id/raw/* — HTTP route-level tests
// Exercises the actual endpoint the VideoViewer and AudioViewer components
// call, confirming 206 / Accept-Ranges / Content-Range behaviour end-to-end.
// ---------------------------------------------------------------------------
describe('GET /api/projects/:id/raw/* range request route', () => {
let server: http.Server;
let baseUrl: string;
let projectsRoot: string;
const projectId = 'proj-raw-range-test';
const FILE_SIZE = 512;
beforeAll(async () => {
const started = await startServer({ port: 0, returnServer: true }) as {
url: string;
server: http.Server;
};
baseUrl = started.url;
server = started.server;
const createResponse = await fetch(`${baseUrl}/api/projects`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Raw range fixture' }),
});
expect(createResponse.status).toBe(200);
// Write a test video file into the daemon's projects root.
// OD_DATA_DIR is set by tests/setup.ts so we can derive the path.
projectsRoot = path.join(process.env.OD_DATA_DIR!, 'projects');
const dir = path.join(projectsRoot, projectId);
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, 'clip.mp4'), Buffer.alloc(FILE_SIZE, 0x42));
await writeFile(path.join(dir, 'audio.mp3'), Buffer.alloc(FILE_SIZE, 0x43));
await writeFile(path.join(dir, 'page.html'), Buffer.from('<html/>'));
await writeFile(
path.join(dir, 'large.html'),
Buffer.from(`<!doctype html><html><body><main>Large Preview</main>${'x'.repeat((2 * 1024 * 1024) + 256)}</body></html>`),
);
await writeFile(
path.join(dir, 'large-powered.html'),
Buffer.from(`<!doctype html><html><body>${'x'.repeat((2 * 1024 * 1024) + 256)}<script>new Worker("worker.js")</script></body></html>`),
);
await writeFile(
path.join(dir, 'large-storage.html'),
Buffer.from(`<!doctype html><html><body>${'x'.repeat((2 * 1024 * 1024) + 256)}<script>localStorage.setItem("task", "saved")</script></body></html>`),
);
await writeFile(path.join(dir, 'body.html'), Buffer.from('<html><body><main>Preview</main></body></html>'));
// `<head>` is optional markup, so a document can legally have none while a
// script string contains one — the head-open half of
// nexu-io/open-design#7410. `<header>` is here because a bare
// `/<head[^>]*>/` matches it too.
await writeFile(
path.join(dir, 'script-literal-head.html'),
Buffer.from(
'<!doctype html><html><body><header>Nav</header>'
+ '<script>\n'
+ ' const doc = `<head><title>Slip</title></head>`;\n'
+ " document.getElementById('slot').textContent = doc.length;\n"
+ '</script>'
+ '<main id="slot"></main></body></html>',
),
);
// Three more from review: a raw-text *name* under SVG is a foreign
// element (its CDATA is still a section), `=` may be followed by
// whitespace before an unquoted value, and CR is a tag-name delimiter.
await writeFile(
path.join(dir, 'foreign-and-delimiters.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><script><![CDATA[x > <\/script></svg><body>slip</body>]]><\/script></svg>'
+ '<svg data= http://x/><![CDATA[x > </svg><body>slip</body>]]></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// CR delimits a tag name at *both* ends: it opens `<script\r>` and it
// closes `</textarea\r>`. Missing the closing half leaves the scan inside
// the raw text, where the authored `</body>` reads as a real end tag.
// Namespace is a stack, not a depth. `<![CDATA[` is character data in
// foreign content and a bogus comment in HTML, and `<script>` inverts the
// same way, so both rules have to follow the adjusted current node —
// including a `<math>` that re-enters MathML beneath an HTML integration
// point, and `annotation-xml`, which is only an integration point when its
// `encoding` names an HTML type.
await writeFile(
path.join(dir, 'foreign-namespaces.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><![CDATA[label > <\/body>]]></svg>'
+ '<svg><foreignObject><math><ms><![CDATA[y > <\/body>]]></ms></math></foreignObject></svg>'
+ '<svg><foreignObject><script>var q = "<\/body>";<\/script></foreignObject></svg>'
+ '<math><annotation-xml encoding="text/html"><script>var r = "<\/body>";<\/script></annotation-xml></math>'
+ '<math><annotation-xml><![CDATA[z > <\/body>]]></annotation-xml></math>'
+ '<template><svg><![CDATA[x > </template><body>slip</body>]]></svg></template>'
+ '<main id="slot">real</main></body></html>',
),
);
// The foreign-content dispatcher has exceptions that depend on the token
// as well as the namespace: a breakout tag (`<p>`, `<font color>`, …) pops
// the foreign element and is reprocessed as HTML, while `<mglyph>` and
// `<malignmark>` stay in MathML beneath a text integration point. Read by
// namespace alone, each of these puts the scan back inside author content.
await writeFile(
path.join(dir, 'foreign-breakout.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><p><script>const x = "<\/svg><body>slip</body>";<\/script></p></svg>'
+ '<svg><font color="red"><script>const y = "<\/svg><body>slip</body>";<\/script></font></svg>'
+ '<math><mi><mglyph><![CDATA[x > </math><body>slip</body>]]></mglyph></mi></math>'
+ '<math><mtext><malignmark><![CDATA[q > </math><body>slip</body>]]></malignmark></mtext></math>'
+ '<main id="slot">real</main></body></html>',
),
);
// The two attribute-driven rules above (`<font color>` breaking out of
// foreign content, `annotation-xml encoding` becoming an integration
// point) must read parsed attributes, not tag text: a value may contain
// anything, including something spelled like another attribute.
await writeFile(
path.join(dir, 'quoted-fake-attrs.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><font data-note=" color=red"><![CDATA[x > <\/svg><body>slip</body>]]></font></svg>'
+ '<math><annotation-xml data-note=" encoding=text/html">'
+ '<![CDATA[x > <\/math><body>slip</body>]]></annotation-xml></math>'
// The mirror case: the tokenizer resolves character references in a
// value, so this one *is* an integration point and its `<script>` is
// HTML raw text — reading the source spelling keeps it MathML.
+ '<math><annotation-xml encoding="text&#x2f;html">'
+ '<script>const enc = "<\/math><body>slip</body>";<\/script></annotation-xml></math>'
+ '<main id="slot">real</main></body></html>',
),
);
await writeFile(
path.join(dir, 'encoding-near-misses.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<math><annotation-xml encoding=" text/html ">'
+ '<![CDATA[p > <\/math><body>slip</body>]]></annotation-xml></math>'
+ '<math><annotation-xml encoding="text&solhtml">'
+ '<![CDATA[u > <\/math><body>slip</body>]]></annotation-xml></math>'
+ '<main id="slot">real</main></body></html>',
),
);
// `<head data==">` is a complete start tag: the second `=` opens an
// *unquoted* value and the quote is a character of it, so the tag ends at
// the very next `>`. Reading that quote as a value delimiter runs the scan
// on to the next quote in the document and reports a `>` from author text.
await writeFile(
path.join(dir, 'unquoted-equals-tag.html'),
Buffer.from(
'<!doctype html><html><head data==">'
+ '<script>const marker = "inside>";<\/script></head>'
+ '<body><main id="slot">real</main></body></html>',
),
);
// Named character references are case-sensitive: `&sol;` is one, `&SOL;`
// is not, and the parser leaves the latter as literal text — so this
// `annotation-xml` stays MathML and its CDATA stays character data.
await writeFile(
path.join(dir, 'encoding-case.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<math><annotation-xml encoding="text&SOL;html">'
+ '<![CDATA[c > <\/math><body>slip</body>]]></annotation-xml></math>'
+ '<main id="slot">real</main></body></html>',
),
);
// Only TAB, LF, FF, CR and SPACE separate tokens. Every other C0 code
// point is an ordinary character, so `color\0=x` names an attribute the
// parser writes as `color\uFFFD` — not `color`, and therefore not a
// `<font>` presentational attribute that breaks out of foreign content.
await writeFile(
path.join(dir, 'control-char-attr.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ `<svg><font color\u0000=x>`
+ '<![CDATA[q > <\/svg><body>slip</body>]]></font></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// Leaving script-data-double-escaped through `-->`. In the
// double-escaped *dash-dash* state a `>` returns the tokenizer to plain
// script data, so the `</script>` that follows is a real close and what
// comes after it is markup.
await writeFile(
path.join(dir, 'double-escape-exit.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
// The shape from review, which has a second `</script>` to fall back
// on, so both readings eventually find some boundary.
+ '<script><!--<script>--><\/script><body>slip</body><\/script>'
// The same state without that fallback. Read as staying
// double-escaped, this script never closes, swallows `<main>` and
// the real body close, and the scan has no boundary left at all.
+ '<script><!--<script>--><\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// A solidus only self-closes when `>` comes immediately after it; the
// self-closing start tag state reconsumes anything else in the
// before-attribute-name state. So `<svg/ >` is an ordinary open tag, and a
// scan that treats it as closed reads the element's contents as markup.
await writeFile(
path.join(dir, 'solidus-then-space.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg/ ><![CDATA[x > <\/svg><body>slip</body>]]></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// The dispatcher hands `<svg>` beneath a MathML `annotation-xml` to the
// HTML rules even with no `encoding`, so that `<svg>` lands in SVG rather
// than inheriting MathML — and the `<foreignObject>` under it is then a
// real integration point whose `<script>` is HTML raw text again.
await writeFile(
path.join(dir, 'annotation-xml-svg.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<math><annotation-xml><svg><foreignObject>'
+ `<script>const x = '<\/math><body>slip</body>';<\/script>`
+ '</foreignObject></svg></annotation-xml></math>'
+ '<main id="slot">real</main></body></html>',
),
);
// Every token the tokenizer turns into a comment may precede the doctype
// without changing the document's mode — an XML prologue and a stray
// `<!foo>` among them. Putting the payload in front of one puts a character
// before the doctype, which silently drops the artifact into quirks mode.
await writeFile(
path.join(dir, 'bogus-prologue.html'),
Buffer.from(
`<?xml version='1.0'?><!bogus><!doctype html>`
+ '<plaintext>tail</body></html>',
),
);
// The in-select insertion mode ignores an `<svg>` / `<math>` start tag
// outright, so no foreign element exists and what follows is still
// ordinary HTML — here a `<script>`, whose string contains both a
// breakout tag and a `</body>`.
await writeFile(
path.join(dir, 'select-foreign-start.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<select><svg></select>'
+ '<script>const x = "<table><\/body>";<\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// A nested `<svg>` beneath `<foreignObject>` shares the root's name but is
// an ordinary element. Tracking the subtree with a name counter alongside
// the stack lets the two disagree the moment `</foreignObject>` unwinds
// past that inner element, and the walk then never ends.
await writeFile(
path.join(dir, 'nested-svg-foreignobject.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><foreignObject><svg></foreignObject></svg>'
+ '<script>const x = "<table><\/body>";<\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// Same shape in MathML.
await writeFile(
path.join(dir, 'nested-math-mi.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<math><mi><math></mi></math>'
+ '<script>var y = "<p><\/body>";<\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// A `<select>` start tag while a select is open closes it rather than
// nesting, so the SVG that follows really is foreign content.
await writeFile(
path.join(dir, 'nested-select.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<select><select></select>'
+ '<svg><![CDATA[x > <\/body>]]></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// The in-select mode ends in more ways than `</select>`: `input`, `keygen`
// and `textarea` close it and are then reprocessed, so the SVG that
// follows really is foreign content and its CDATA really is character
// data. `textarea` is also raw text, so the select state has to be updated
// before that branch consumes the tag.
for (const [name, closer] of [
['select-exit-input.html', '<input>'],
['select-exit-keygen.html', '<keygen>'],
['select-exit-textarea.html', '<textarea></textarea>'],
] as const) {
await writeFile(
path.join(dir, name),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ `<select>${closer}`
+ '<svg><![CDATA[x > <\/body>]]></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
}
// A `<table>` cannot put itself in a table. In select mode with no table
// already open the token is ignored outright, so it neither ends the mode
// nor joins the table stack, and the `<svg>` after it is ignored too.
await writeFile(
path.join(dir, 'select-table-token.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<select><table><svg>'
+ '<script>const x = "<table><\/body>";<\/script></select>'
+ '<main id="slot">real</main></body></html>',
),
);
// A `<base>` under an HTML integration point is an HTML-namespace element
// and governs the document, even though the structural scan skips the
// whole foreign subtree. One directly under `<svg>` is an SVG element and
// inert, and one inside a `<template>` belongs to an inert fragment.
await writeFile(
path.join(dir, 'integration-point-base.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><foreignObject><base href="https://author.example/assets/">'
+ '</foreignObject></svg><main id="slot">real</main></body></html>',
),
);
await writeFile(
path.join(dir, 'foreign-namespace-base.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><base href="https://ignored.example/"></svg>'
+ '<template><base href="https://inert.example/"></template>'
+ '<main id="slot">real</main></body></html>',
),
);
// A template's contents are parsed with the same insertion modes as the
// document, so the in-select mode applies inside one too. Modelling it at
// the top level and not here made the two walkers disagree on the same
// bytes.
await writeFile(
path.join(dir, 'template-select-foreign.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<template><select><svg></select>'
+ '<script>const x = "<p></template><body>slip</body>";<\/script></template>'
+ '<main id="slot">real</main></body></html>',
),
);
// A foreign element may also be called `template`, and it creates no inert
// fragment — so a `<base>` beneath it is live and must still suppress the
// generated containment base.
await writeFile(
path.join(dir, 'svg-template-base.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><template><foreignObject>'
+ '<base href="https://author.example/assets/">'
+ '</foreignObject></template></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// A stray end tag inside foreign content is usually ignored, but `</p>`
// and `</br>` each have an "in body" rule that synthesises the element and
// closes it, popping the foreign element on the way. The scan does not
// model that, so it refuses rather than keeping a frame the parser dropped.
await writeFile(
path.join(dir, 'foreign-stray-p.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg></p><script>const x = "<body>slip</body>";<\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// Every other stray end tag leaves the foreign element open, so this one
// still resolves precisely — the refusal above must not widen to it.
await writeFile(
path.join(dir, 'foreign-stray-div.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg></div><![CDATA[q > <\/body>]]></svg>'
+ '<main id="slot">real</main></body></html>',
),
);
// A nested template's contents get a fresh insertion mode and the outer one
// is restored at its close, so the inner `<svg>` is real foreign content
// even though the outer fragment is in select mode.
await writeFile(
path.join(dir, 'nested-template-select.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<template><select><template><svg>'
+ '<![CDATA[x > </template></template><body>slip</body>]]>'
+ '</svg></template></select></template>'
+ '<main id="slot">real</main></body></html>',
),
);
// A leading BOM is the encoding signature and only counts at byte zero, so
// the no-boundary fallback has to insert after it rather than in front of
// it — otherwise the doctype stops applying and the artifact silently
// renders in quirks mode. `<plaintext>` is what removes the boundary here.
await writeFile(
path.join(dir, 'bom-plaintext.html'),
Buffer.from(
// No `<head>` and no `<html>`: the head/html anchors must miss so the
// doctype fallback is the branch under test.
'\uFEFF<!doctype html><plaintext>tail</body></html>',
),
);
await writeFile(
path.join(dir, 'cr-delimiter.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<script\r>const doc = "<body>slip</body>";<\/script>'
+ '<textarea>note </textarea\r>'
+ '<main id="slot">real</main></body></html>',
),
);
// Token shapes an audit against parse5 turned up, each of which had the
// scan resume inside author text: an end-tag-open on a non-letter (bogus
// comment), the `--!>` and `<!-->` comment ends, a custom element whose
// name merely starts with a raw-text name, and an unquoted attribute value
// ending in `/` (not a self-closing solidus).
await writeFile(
path.join(dir, 'token-shapes.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '</ note: use </body> carefully>'
+ '<!-- a --!>'
+ '<iframe-x>a</iframe-x>'
+ '<svg data-href=http://x/><text>t</text></svg>'
+ '<script>var s = "</iframe> --> </body>";<\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// `<plaintext>` has no way out of PLAINTEXT state: everything after it is
// character data, `</plaintext>` included. A scan that honours the close
// tag resumes in text that the parser never treats as markup.
await writeFile(
path.join(dir, 'plaintext-tail.html'),
Buffer.from(
'<!doctype html><html><head></head><body><plaintext></body></plaintext>'
+ '<main id="slot">real</main></body></html>',
),
);
// Foreign content is the one place `<![CDATA[ … ]]>` is real markup. The
// `>` inside the section is still text, so a scan that ends declarations at
// the next `>` resumes inside it and returns the `</body>` it holds. Inside
// an HTML integration point the parser is back in HTML, where `<![CDATA[`
// is a bogus comment again.
await writeFile(
path.join(dir, 'foreign-content.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<svg><foreignObject><![CDATA[x > </svg><body>slip</body>]]></foreignObject></svg>'
+ '<template><svg><![CDATA[x > </template><body>slip</body>]]></svg></template>'
+ '<math><annotation-xml><![CDATA[x > </math><body>slip</body>]]></annotation-xml></math>'
+ '<main id="slot">real</main></body></html>',
),
);
// Tokenizer states the scan has to follow, not just tag text: a
// `</template>` inside a nested script is content, and after `<!--` a
// nested `<script` puts script data in double-escaped state, where
// `</script>` steps back out instead of closing.
await writeFile(
path.join(dir, 'tokenizer-states.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<template><script>const a = "</template><body>slip</body>";<\/script></template>'
+ '<script><!--\nconst open = "<script>";\nconst b = "</script><body>slip</body>";\n//--><\/script>'
+ '<main id="slot">real</main></body></html>',
),
);
// A raw-text element only closes on `</name` followed by whitespace, `/` or
// `>`. A longer name that merely starts with it stays character data, so a
// scan that accepts the prefix resumes inside the author's script and hands
// back a boundary from inside their string.
await writeFile(
path.join(dir, 'raw-text-prefix.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<script>const doc = "</script-template><body>slip</body>";<\/script>'
+ '<textarea id="t"></textarea-note></textarea>'
+ '<main id="slot"></main></body></html>',
),
);
// The other places a `</body>` is character data rather than a boundary:
// raw-text elements the parser never reads as markup, and `<template>`
// content, which the tree builder keeps out of the document entirely (an
// injection placed there would silently never run).
await writeFile(
path.join(dir, 'inert-literal-body.html'),
Buffer.from(
'<!doctype html><html><head></head><body>'
+ '<template></body></template>'
+ '<noscript></body></noscript>'
+ '<xmp></body></xmp>'
+ '<main id="slot"></main></body></html>',
),
);
// The same tags are just as ordinary inside an attribute value — storing a
// template on a `data-` attribute is the other half of the #7410 shape.
// Browsers accept a literal `<` there, and the injected bridge carries
// quotes, so splicing into the value would close the attribute early.
await writeFile(
path.join(dir, 'attr-literal-body.html'),
Buffer.from(
'<!doctype html><html><head><title>Print</title></head><body>'
+ '<div id="tpl" data-tpl="<body>slip</body>"></div>'
+ '<main id="slot"></main></body></html>',
),
);
// A prototype whose inline script builds an HTML document string — the
// shape behind nexu-io/open-design#7410. The literal `</body>` inside the
// template literal precedes the document's real one, so an injector that
// splices at the FIRST `</body>` lands inside the script and truncates it.
await writeFile(
path.join(dir, 'script-literal-body.html'),
Buffer.from(
'<!doctype html><html><head><title>Print</title></head><body>'
+ '<script type="text/babel">\n'
+ ' const doc = `<body>slip</body>`;\n'
+ " document.getElementById('slot').textContent = doc.length;\n"
+ '</script>'
+ '<main id="slot"></main></body></html>',
),
);
await writeFile(
path.join(dir, 'guarded.html'),
Buffer.from('<!doctype html><html><head><script src="./boot.js"></script></head><body><input autofocus></body></html>'),
);
const complexPreviewDir = path.join(dir, 'prototypes', 'booking');
await mkdir(path.join(complexPreviewDir, 'styles'), { recursive: true });
await mkdir(path.join(complexPreviewDir, 'scripts'), { recursive: true });
await mkdir(path.join(complexPreviewDir, 'components'), { recursive: true });
await mkdir(path.join(complexPreviewDir, 'assets'), { recursive: true });
const babelScripts = Array.from(
{ length: 43 },
(_, index) => `<script type="text/babel" src="./components/screen-${index + 1}.jsx"></script>`,
).join('');
await writeFile(
path.join(complexPreviewDir, 'index.html'),
Buffer.from([
'<!doctype html><html><head>',
'<link rel="stylesheet" href="./styles/app.css">',
'<script src="./scripts/support.js"></script>',
babelScripts,
'<script type="module" src="./scripts/module.js"></script>',
'</head><body>',
'<img src="./assets/card.svg" srcset="./assets/card.svg 1x, ./assets/card@2x.svg 2x">',
'<main id="root"></main>',
'</body></html>',
].join('')),
);
await writeFile(
path.join(complexPreviewDir, 'styles', 'app.css'),
'@import "./theme.css"; .card { background-image: url("../assets/card.svg"); }',
);
await writeFile(path.join(complexPreviewDir, 'styles', 'theme.css'), ':root { --accent: #0a7; }');
await writeFile(
path.join(complexPreviewDir, 'scripts', 'support.js'),
'window.__supportLoaded = true; fetch("./data.json").then((response) => response.json());',
);
await writeFile(path.join(complexPreviewDir, 'scripts', 'module.js'), 'export const ready = true;');
for (let index = 1; index <= 43; index += 1) {
await writeFile(
path.join(complexPreviewDir, 'components', `screen-${index}.jsx`),
`window.__screen${index} = () => <section>Screen ${index}</section>;`,
);
}
await writeFile(path.join(complexPreviewDir, 'data.json'), '{"ready":true}');
await writeFile(path.join(complexPreviewDir, 'assets', 'card.svg'), '<svg xmlns="http://www.w3.org/2000/svg"/>');
await writeFile(path.join(complexPreviewDir, 'assets', 'card@2x.svg'), '<svg xmlns="http://www.w3.org/2000/svg" width="2"/>');
await writeFile(
path.join(dir, 'bridged.html'),
Buffer.from('<html><body><script data-od-url-scroll-bridge></script><main>Preview</main></body></html>'),
);
await writeFile(
path.join(dir, 'selection-bridged.html'),
Buffer.from('<html><body><script data-od-url-selection-bridge></script><main>Preview</main></body></html>'),
);
await writeFile(
path.join(dir, 'snapshot-bridged.html'),
Buffer.from('<html><body><script data-od-url-snapshot-bridge></script><main>Preview</main></body></html>'),
);
await writeFile(
path.join(dir, 'observability-bridged.html'),
Buffer.from('<html><head><script data-od-preview-observability></script></head><body><main>Preview</main></body></html>'),
);
await mkdir(path.join(dir, 'dist', 'assets'), { recursive: true });
await writeFile(
path.join(dir, 'vite-entry.html'),
Buffer.from('<!doctype html><html><head><script type="module" src="/src/main.tsx"></script></head><body><div id="root"></div></body></html>'),
);
await writeFile(
path.join(dir, 'dist', 'index.html'),
Buffer.from(
'<!doctype html><html><head>' +
'<script type="module" crossorigin src="/assets/app.js"></script>' +
'<link rel="stylesheet" crossorigin href="/assets/app.css">' +
'</head><body><div id="root"></div></body></html>',
),
);
});
afterAll(() => new Promise<void>((resolve) => server.close(() => resolve())));
const rawUrl = (name: string) => `${baseUrl}/api/projects/${projectId}/raw/${name}`;
const poweredUrl = (name: string) => `${baseUrl}/api/projects/${projectId}/powered/${name}`;
const poweredOrigin = () => {
const url = new URL(baseUrl);
url.hostname = url.hostname === '127.0.0.1' ? 'localhost' : '127.0.0.1';
return url.origin;
};
it('advertises Accept-Ranges: bytes for a video file with no Range header', async () => {
const res = await fetch(rawUrl('clip.mp4'));
expect(res.status).toBe(200);
expect(res.headers.get('accept-ranges')).toBe('bytes');
expect(res.headers.get('content-type')).toContain('video/mp4');
expect(Number(res.headers.get('content-length'))).toBe(FILE_SIZE);
});
it('returns 206 with correct Content-Range for a partial video request', async () => {
const res = await fetch(rawUrl('clip.mp4'), {
headers: { Range: 'bytes=0-99' },
});
expect(res.status).toBe(206);
expect(res.headers.get('content-range')).toBe(`bytes 0-99/${FILE_SIZE}`);
expect(res.headers.get('content-length')).toBe('100');
expect(res.headers.get('accept-ranges')).toBe('bytes');
const buf = Buffer.from(await res.arrayBuffer());
expect(buf.length).toBe(100);
expect(buf[0]).toBe(0x42);
});
it('returns 206 for an open-ended range on an audio file', async () => {
const res = await fetch(rawUrl('audio.mp3'), {
headers: { Range: 'bytes=256-' },
});
expect(res.status).toBe(206);
expect(res.headers.get('content-range')).toBe(`bytes 256-${FILE_SIZE - 1}/${FILE_SIZE}`);
expect(res.headers.get('content-length')).toBe(String(FILE_SIZE - 256));
});
it('returns 206 for a suffix range', async () => {
const res = await fetch(rawUrl('clip.mp4'), {
headers: { Range: 'bytes=-128' },
});
expect(res.status).toBe(206);
expect(res.headers.get('content-range')).toBe(`bytes ${FILE_SIZE - 128}-${FILE_SIZE - 1}/${FILE_SIZE}`);
expect(res.headers.get('content-length')).toBe('128');
});
it('returns 416 for an out-of-bounds range', async () => {
const res = await fetch(rawUrl('clip.mp4'), {
headers: { Range: 'bytes=9999-99999' },
});
expect(res.status).toBe(416);
expect(res.headers.get('content-range')).toBe(`bytes */${FILE_SIZE}`);
});
it('does not stream small transformed HTML files (HTML returns full 200 without Accept-Ranges)', async () => {
const res = await fetch(rawUrl('page.html'));
expect(res.status).toBe(200);
expect(res.headers.get('accept-ranges')).toBeNull();
const text = await res.text();
expect(text).toBe('<html/>');
});
it('returns a truncated text preview for large HTML without reading the full file', async () => {
const res = await fetch(`${baseUrl}/api/projects/${projectId}/text-preview/large.html?limit=64`);
expect(res.status).toBe(200);
const body = await res.json() as {
text: string;
truncated: boolean;
size: number;
limit: number;
mime: string;
poweredPreview: {
required: boolean;
scannedBytes: number;
complete: boolean;
};
};
expect(body.text).toContain('<!doctype html>');
expect(body.text.length).toBeLessThanOrEqual(1024);
expect(body.truncated).toBe(true);
expect(body.size).toBeGreaterThan(2 * 1024 * 1024);
expect(body.limit).toBe(1024);
expect(body.mime).toContain('text/html');
expect(body.poweredPreview.required).toBe(false);
expect(body.poweredPreview.complete).toBe(true);
});
it('returns powered-preview hints even when the Worker/WASM signal is late in a large HTML file', async () => {
const res = await fetch(`${baseUrl}/api/projects/${projectId}/text-preview/large-powered.html?limit=64`);
expect(res.status).toBe(200);
const body = await res.json() as {
text: string;
poweredPreview: {
required: boolean;
scannedBytes: number;
complete: boolean;
};
};
expect(body.text.length).toBeLessThanOrEqual(1024);
expect(body.text).not.toContain('new Worker');
expect(body.poweredPreview.required).toBe(true);
expect(body.poweredPreview.scannedBytes).toBeGreaterThan(2 * 1024 * 1024);
});
it('returns a real-storage preview hint when the storage call is outside the returned HTML prefix', async () => {
const res = await fetch(`${baseUrl}/api/projects/${projectId}/text-preview/large-storage.html?limit=64`);
expect(res.status).toBe(200);
const body = await res.json() as {
text: string;
poweredPreview: { required: boolean; scannedBytes: number };
};
expect(body.text).not.toContain('localStorage');
expect(body.poweredPreview.required).toBe(true);
expect(body.poweredPreview.scannedBytes).toBeGreaterThan(2 * 1024 * 1024);
});
it('skips URL preview bridge injection for large HTML so first paint can stream', async () => {
const res = await fetch(`${rawUrl('large.html')}?odPreviewBridge=scroll&odPreviewBridge=selection&odPreviewBridge=snapshot&odPreviewBridge=observability`, {
headers: { Range: 'bytes=0-127' },
});
expect(res.status).toBe(206);
expect(res.headers.get('accept-ranges')).toBe('bytes');
expect(res.headers.get('content-range')).toMatch(/^bytes 0-127\//);
const html = await res.text();
expect(html).toContain('Large Preview');
expect(html).not.toContain('data-od-url-scroll-bridge');
expect(html).not.toContain('data-od-url-selection-bridge');
expect(html).not.toContain('data-od-url-snapshot-bridge');
expect(html).not.toContain('data-od-preview-observability');
});
it('injects the URL preview scroll bridge only when requested', async () => {
const plain = await fetch(rawUrl('page.html'));
expect(await plain.text()).toBe('<html/>');
const bridged = await fetch(`${rawUrl('page.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-url-scroll-bridge');
expect(html).toContain("type: 'od:preview-scroll'");
expect(html).toContain("type: 'od:preview-content-size'");
expect(html).toContain('od:preview-content-size-request');
expect(html).toContain('lastContentSizeRequest.measurementId');
expect(html).toContain('lastContentSizeRequest.generation');
expect(html).toContain('documentEpoch: contentSizeDocumentEpoch');
expect(html).toContain("get('odPreviewEpoch')");
expect(html).toContain('scrollWidth: size && size.scrollWidth');
expect(html).toContain('clientWidth: size && size.clientWidth');
});
it('injects the URL preview scroll bridge before the closing body tag', async () => {
const bridged = await fetch(`${rawUrl('body.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html.indexOf('data-od-url-scroll-bridge')).toBeGreaterThan(-1);
expect(html.indexOf('data-od-url-scroll-bridge')).toBeLessThan(html.indexOf('</body>'));
});
it('injects the URL preview scroll bridge after a `</body>` written inside a script string', async () => {
// nexu-io/open-design#7410: splicing at the first raw-text `</body>` puts
// the bridge's own `</script>` inside the author's script, which ends that
// script early and dumps the rest of it on the page as text.
const bridged = await fetch(`${rawUrl('script-literal-body.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
// The author's script must survive intact...
expect(html).toContain('const doc = `<body>slip</body>`;');
// ...and the bridge must land after it closes, not inside it.
expect(injectedAt).toBeGreaterThan(html.indexOf('</script>'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('injects the containment base after a `<head>` written inside a script string', async () => {
// The containment `<base>` runs after the bridges on every URL preview, and
// it used its own first-textual-`<head>` match. Requesting only the scroll
// bridge keeps any head-open guard from synthesizing a real `<head>` first,
// so the base injector has to find the boundary on its own.
const bridged = await fetch(`${rawUrl('script-literal-head.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-project-preview-base');
expect(html).toContain('const doc = `<head><title>Slip</title></head>`;');
});
it('injects the URL preview sandbox shim after a `<head>` written inside a script string', async () => {
// nexu-io/open-design#7410, head-open half: the document has no real
// `<head>`, so the first textual match is the one inside the script.
const bridged = await fetch(`${rawUrl('script-literal-head.html')}?odPreviewBridge=sandbox`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-sandbox-shim');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('const doc = `<head><title>Slip</title></head>`;');
expect(injectedAt).toBeLessThan(html.indexOf('<script>'));
});
it('injects the URL preview scroll bridge after a `</body>` written in an attribute value', async () => {
const bridged = await fetch(`${rawUrl('attr-literal-body.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('data-tpl="<body>slip</body>"');
expect(injectedAt).toBeGreaterThan(html.indexOf('data-tpl'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('injects the URL preview scroll bridge after `</body>` in inert and raw-text content', async () => {
const bridged = await fetch(`${rawUrl('inert-literal-body.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('<template></body></template>');
expect(html).toContain('<noscript></body></noscript>');
expect(html).toContain('<xmp></body></xmp>');
// Injected into the real body, after every inert copy.
expect(injectedAt).toBeGreaterThan(html.indexOf('<xmp>'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('injects the URL preview scroll bridge after a raw-text close-tag prefix', async () => {
const bridged = await fetch(`${rawUrl('raw-text-prefix.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
// `</script-template>` does not close the script, so the `</body>` it
// contains is still the author's string, not this document's boundary.
expect(html).toContain('const doc = "</script-template><body>slip</body>";');
expect(html).toContain('<textarea id="t"></textarea-note></textarea>');
expect(injectedAt).toBeGreaterThan(html.indexOf('<main id="slot">'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('injects the URL preview scroll bridge after template and script escape states', async () => {
const bridged = await fetch(`${rawUrl('tokenizer-states.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('const a = "</template><body>slip</body>";');
expect(html).toContain('const b = "</script><body>slip</body>";');
expect(injectedAt).toBeGreaterThan(html.indexOf('<main id="slot">real</main>'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('injects the URL preview scroll bridge after CDATA in foreign content', async () => {
const bridged = await fetch(`${rawUrl('foreign-content.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
// Whether these are CDATA sections or bogus comments depends on the
// adjusted current node's namespace, and the two spellings disagree about
// where the body ends. Beneath `<template><svg>` and `<math>` the parser is
// in foreign content, so those sections are character data and the
// `</body>` inside each is text — both survive byte for byte.
expect(html).toContain('<![CDATA[x > </template><body>slip</body>]]>');
expect(html).toContain('<![CDATA[x > </math><body>slip</body>]]>');
// Beneath `<foreignObject>` the parser is back in HTML, where `<![CDATA[x >`
// is a bogus comment and the `</body>` after it really does close the body.
// The scan stops at that first real boundary, which is what the tree builder
// does, so the bridge lands in body ahead of it.
const page = load(html);
expect(page('[data-od-url-scroll-bridge]').length).toBe(1);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
});
it('appends the URL preview scroll bridge rather than splicing into plaintext', async () => {
const bridged = await fetch(`${rawUrl('plaintext-tail.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
// Everything after `<plaintext>` is character data — both `</body>` copies
// included — so there is no boundary to splice at. Appending would not
// work either: PLAINTEXT never exits, so appended markup is text and the
// bridge would not exist as an element at all. The fallback therefore goes
// in at the top of the document, which costs the end-of-body placement but
// keeps the bridge live. Asserting the parsed result, not just the bytes.
expect(html).toContain('<plaintext></body></plaintext>');
expect(injectedAt).toBeLessThan(html.indexOf('<plaintext>'));
expect(load(html)('[data-od-url-scroll-bridge]').length).toBe(1);
});
it('injects the URL preview scroll bridge past bogus comments, custom elements and unquoted slashes', async () => {
const bridged = await fetch(`${rawUrl('token-shapes.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('</ note: use </body> carefully>');
expect(html).toContain('<iframe-x>a</iframe-x>');
expect(html).toContain('var s = "</iframe> --> </body>";');
expect(injectedAt).toBeGreaterThan(html.indexOf('<main id="slot">real</main>'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
});
it('refuses a boundary through CDATA under a raw-text-named foreign element', async () => {
const bridged = await fetch(`${rawUrl('foreign-and-delimiters.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
// `<svg><script>` is a foreign element, not HTML raw text, so its CDATA is
// still a section; and `data= http://x/` is an unquoted value, not a
// self-closing solidus. Both sections survive byte for byte.
expect(html).toContain('<![CDATA[x > <\/script></svg><body>slip</body>]]>');
expect(html).toContain('<![CDATA[x > </svg><body>slip</body>]]>');
// No boundary is picked here, so the bridge goes in at the top of the
// document rather than at the end of body — it still exists as an element,
// which is what actually matters, and every authored section is untouched.
expect(load(html)('[data-od-url-scroll-bridge]').length).toBe(1);
});
it('follows the adjusted current node through foreign content', async () => {
const bridged = await fetch(`${rawUrl('foreign-namespaces.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
// A live bridge, in body, with every authored section still intact.
expect(page('[data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
// None of the six `</body>` spellings above may be read as a boundary.
expect(html.indexOf('data-od-url-scroll-bridge'))
.toBeGreaterThan(html.indexOf('<main id="slot">real</main>'));
});
it('leaves foreign content on a breakout tag and keeps mglyph foreign', async () => {
const bridged = await fetch(`${rawUrl('foreign-breakout.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
// None of the four authored `</body>` spellings may be read as a boundary.
expect(html).toContain('const x = "</svg><body>slip</body>";');
expect(html).toContain('const y = "</svg><body>slip</body>";');
expect(html).toContain('<![CDATA[x > </math><body>slip</body>]]>');
expect(html).toContain('<![CDATA[q > </math><body>slip</body>]]>');
});
it('does not treat a padded or unterminated encoding as an integration point', async () => {
// `annotation-xml` is an integration point only on an exact match, so a
// padded value and an ambiguous ampersand both stay MathML — and their
// CDATA stays character data.
const bridged = await fetch(`${rawUrl('encoding-near-misses.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[p > </math><body>slip</body>]]>');
expect(html).toContain('<![CDATA[u > </math><body>slip</body>]]>');
});
it('reads parsed attributes, not attribute-like text in a quoted value', async () => {
const bridged = await fetch(`${rawUrl('quoted-fake-attrs.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
// Neither section carries a real `color` or `encoding`, so both stay
// foreign and the `</body>` inside each is character data.
expect(html).toContain('<![CDATA[x > </svg><body>slip</body>]]>');
expect(html).toContain('<![CDATA[x > </math><body>slip</body>]]>');
expect(html).toContain('const enc = "</math><body>slip</body>";');
});
it('ends a tag whose unquoted value contains a quote at the next angle bracket', async () => {
const bridged = await fetch(`${rawUrl('unquoted-equals-tag.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('[data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
// The authored script survives; nothing was spliced into its string.
expect(html).toContain('const marker = "inside>";');
});
it('treats a named character reference as case-sensitive', async () => {
const bridged = await fetch(`${rawUrl('encoding-case.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[c > </math><body>slip</body>]]>');
});
it('separates tokens on HTML whitespace only, not the whole C0 range', async () => {
const bridged = await fetch(`${rawUrl('control-char-attr.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[q > </svg><body>slip</body>]]>');
});
it('leaves script-data-double-escaped through the dash-dash greater-than', async () => {
const bridged = await fetch(`${rawUrl('double-escape-exit.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
// A live bridge inside `<body>`, and `<main>` still an element rather than
// script text. Read as staying double-escaped, the second script never
// closes: `#slot` is swallowed and the bridge falls back into `<head>`, so
// both assertions below fail. The bridge lands at the first `</body>` — the
// one the tree builder acts on — which is ahead of `<main>`; that position
// and the document's last `</body>` leave an identical tree, which is why
// this asserts live elements and not an offset.
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<script><!--<script>--></script>');
});
it('does not treat a solidus followed by whitespace as self-closing', async () => {
const bridged = await fetch(`${rawUrl('solidus-then-space.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[x > </svg><body>slip</body>]]>');
});
it('gives an svg beneath annotation-xml the SVG namespace', async () => {
const bridged = await fetch(`${rawUrl('annotation-xml-svg.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain(`const x = '</math><body>slip</body>';`);
});
it('keeps a standards-mode doctype behind a bogus-comment prologue', async () => {
const bridged = await fetch(`${rawUrl('bogus-prologue.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
// The prologue and the doctype both still precede every injected token, so
// the served document keeps the mode it had before injection.
const doctypeAt = html.toLowerCase().indexOf('<!doctype');
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(doctypeAt).toBeGreaterThan(-1);
expect(injectedAt).toBeGreaterThan(doctypeAt);
expect(html.indexOf('<?xml')).toBeLessThan(doctypeAt);
expect(load(html)('[data-od-url-scroll-bridge]').length).toBe(1);
});
it('does not enter foreign content for an svg start tag inside select', async () => {
const bridged = await fetch(`${rawUrl('select-foreign-start.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('const x = "<table></body>";');
});
it('ends a foreign subtree by its stack, not by a name counter', async () => {
for (const [fixture, authored] of [
['nested-svg-foreignobject.html', 'const x = "<table></body>";'],
['nested-math-mi.html', 'var y = "<p></body>";'],
] as const) {
const bridged = await fetch(`${rawUrl(fixture)}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain(authored);
}
});
it('treats a second select start tag as closing the first', async () => {
const bridged = await fetch(`${rawUrl('nested-select.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[x > </body>]]>');
});
it('leaves the in-select mode on input, keygen and textarea', async () => {
for (const fixture of [
'select-exit-input.html',
'select-exit-keygen.html',
'select-exit-textarea.html',
]) {
const bridged = await fetch(`${rawUrl(fixture)}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[x > </body>]]>');
}
});
it('does not let a table token put itself in a table while in select', async () => {
const bridged = await fetch(`${rawUrl('select-table-token.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('[data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('const x = "<table></body>";');
});
it('respects an authored base inside an HTML integration point', async () => {
const bridged = await fetch(`${rawUrl('integration-point-base.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
// The generated base would land in `<head>`, ahead of the authored one, and
// the first base with an href wins — so it must not be generated at all.
expect(html).not.toContain('data-od-project-preview-base');
expect(html).toContain('href="https://author.example/assets/"');
expect(load(html)('[data-od-url-scroll-bridge]').length).toBe(1);
});
it('does not treat a foreign-namespace or template base as authored', async () => {
const bridged = await fetch(`${rawUrl('foreign-namespace-base.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
// Neither of those governs the document, so containment still applies.
expect(html).toContain('data-od-project-preview-base');
expect(html).toContain('href="https://ignored.example/"');
expect(html).toContain('href="https://inert.example/"');
});
it('applies the in-select mode inside template contents too', async () => {
const bridged = await fetch(`${rawUrl('template-select-foreign.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('const x = "<p></template><body>slip</body>";');
});
it('treats a foreign element named template as live, not inert', async () => {
const bridged = await fetch(`${rawUrl('svg-template-base.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).not.toContain('data-od-project-preview-base');
expect(html).toContain('href="https://author.example/assets/"');
});
it('refuses a boundary past a stray end tag that reprocessing acts on', async () => {
const bridged = await fetch(`${rawUrl('foreign-stray-p.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('[data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('const x = "<body>slip</body>";');
});
it('keeps precise placement past a stray end tag that is ignored', async () => {
const bridged = await fetch(`${rawUrl('foreign-stray-div.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[q > </body>]]>');
});
it('gives a nested template its own insertion mode', async () => {
const bridged = await fetch(`${rawUrl('nested-template-select.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
expect(html).toContain('<![CDATA[x > </template></template><body>slip</body>]]>');
});
it('keeps a leading BOM at byte zero when there is no boundary', async () => {
const bridged = await fetch(`${rawUrl('bom-plaintext.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
// `Response.text()` runs a UTF-8 decode, which *removes* a leading BOM —
// reading the body that way would assert nothing about the byte this spec
// exists for. Decode the bytes with the BOM preserved instead.
const html = new TextDecoder('utf-8', { ignoreBOM: true })
.decode(await bridged.arrayBuffer());
// The BOM stays the first code unit, and the doctype still precedes every
// injected token — the two conditions the browser needs to stay out of
// quirks mode.
expect(html.charCodeAt(0)).toBe(0xfeff);
const doctypeAt = html.toLowerCase().indexOf('<!doctype');
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(doctypeAt).toBe(1);
expect(injectedAt).toBeGreaterThan(doctypeAt);
expect(load(html)('[data-od-url-scroll-bridge]').length).toBe(1);
});
it('treats a carriage return as a tag-name delimiter', async () => {
const bridged = await fetch(`${rawUrl('cr-delimiter.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const injectedAt = html.indexOf('data-od-url-scroll-bridge');
expect(injectedAt).toBeGreaterThan(-1);
expect(html).toContain('const doc = "<body>slip</body>";');
expect(injectedAt).toBeGreaterThan(html.indexOf('<main id="slot">real</main>'));
expect(injectedAt).toBeLessThan(html.lastIndexOf('</body>'));
// The bridge is a live element in body — not pushed into the head by a
// raw-text region the scan failed to close.
const page = load(html);
expect(page('body > [data-od-url-scroll-bridge]').length).toBe(1);
expect(page('#slot').text()).toBe('real');
});
it('injects the URL preview selection bridge only when requested', async () => {
const plain = await fetch(rawUrl('page.html'));
expect(await plain.text()).toBe('<html/>');
const bridged = await fetch(`${rawUrl('page.html')}?odPreviewBridge=selection`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-url-selection-bridge');
expect(html).toContain("type: 'od:comment-target'");
expect(html).toContain("type: 'od:preview-runtime-state-captured'");
expect(html).toContain("type: 'od:preview-open-file'");
expect(html).toContain('function previewHtmlFileForLink(');
expect(html).toContain('bodyHtml: bodyHtml');
expect(html).toContain("scriptAttrName.indexOf('data-od-url-') === 0");
expect(html).toContain('roots: roots');
expect(html).toContain("querySelectorAll('[id]')");
expect(html).toContain('candidate.contains(roots[r])');
expect(html).toContain('function postReady(');
expect(html).toContain('href: window.location.href');
expect(html).not.toContain('data-od-url-scroll-bridge');
});
it('URL selection falls back from an empty od-id to a matching screen-label selector', async () => {
const response = await fetch(`${rawUrl('body.html')}?odPreviewBridge=selection`);
expect(response.status).toBe(200);
const bridge = load(await response.text())('script[data-od-url-selection-bridge]').text();
expect(bridge).not.toBe('');
const dom = new JSDOM('<section data-od-id="" data-screen-label="Home">Home</section>', {
url: rawUrl('body.html'), runScripts: 'outside-only', pretendToBeVisual: true,
});
try {
dom.window.eval(`
window.posted = [];
window.postMessage = data => window.posted.push(data);
document.querySelector('section').getBoundingClientRect = () => new DOMRect(0, 0, 100, 100);
`);
dom.window.eval(bridge);
const result = dom.window.eval(`(() => {
window.dispatchEvent(new MessageEvent('message', {
data: { type: 'od:comment-mode', enabled: true, mode: 'picker' },
}));
const element = document.querySelector('section');
element.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: 10, clientY: 10 }));
const target = window.posted.find(data => data.type === 'od:comment-target');
return { target, matches: !!target && document.querySelector(target.selector) === element };
})()`);
expect(result).toMatchObject({
target: { elementId: 'Home', selector: '[data-screen-label="Home"]' }, matches: true,
});
} finally {
dom.window.close();
}
});
it('URL comment save validates the live selector without requiring the old identity', async () => {
const response = await fetch(`${rawUrl('body.html')}?odPreviewBridge=selection`);
const bridge = load(await response.text())('script[data-od-url-selection-bridge]').text();
const dom = new JSDOM('<body><button data-od-id="new">Changed copy</button></body>', {
url: rawUrl('body.html'), runScripts: 'outside-only', pretendToBeVisual: true,
});
try {
dom.window.eval(`window.posted=[]; window.postMessage=data=>window.posted.push(data);
document.querySelector('button').getBoundingClientRect=()=>new DOMRect(0,0,100,40);`);
dom.window.eval(bridge);
const result = dom.window.eval(`(()=>{
var query={type:'od:comment-active-target',validateForSave:true,elementId:'old-id',selector:'body > button:nth-of-type(1)',requestId:'save'};
window.dispatchEvent(new MessageEvent('message',{data:query}));
var found=window.posted.at(-1);
var navigationResults=[];
window.location.hash='section';
window.dispatchEvent(new MessageEvent('message',{data:query}));
navigationResults.push(window.posted.at(-1));
history.pushState({},'', '?view=next#section');
window.dispatchEvent(new MessageEvent('message',{data:query}));
navigationResults.push(window.posted.at(-1));
history.replaceState({},'', '?view=last');
window.dispatchEvent(new MessageEvent('message',{data:query}));
navigationResults.push(window.posted.at(-1));
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-mode',enabled:true,mode:'picker'}}));
document.querySelector('button').setAttribute('data-od-comment-id','12345678-1234-4234-8234-123456789abc');
window.dispatchEvent(new MessageEvent('message',{data:query}));
var replacement=window.posted.at(-1);
document.querySelector('button').remove();
window.dispatchEvent(new MessageEvent('message',{data:query}));
var missing=window.posted.at(-1);
var fallbackResults=['span','div'].map(function(tag){
document.body.innerHTML='<'+tag+'>Original</'+tag+'>';
var node=document.querySelector(tag);
node.getBoundingClientRect=()=>new DOMRect(0,0,100,40);
node.innerHTML=tag==='span'?'':'<span>A</span><span>B</span>';
window.dispatchEvent(new MessageEvent('message',{data:Object.assign({},query,{selector:'body > '+tag+':nth-of-type(1)'})}));
return window.posted.at(-1);
});
return {found,navigationResults,replacement,missing,fallbackResults};
})()`);
expect(result).toMatchObject({ fallbackResults: [
{ type: 'od:comment-active-target-update', text: '' },
{ type: 'od:comment-active-target-update', text: 'AB' },
] });
expect(result).toMatchObject({ navigationResults: Array.from({ length: 3 }, () => ({
type: 'od:comment-active-target-update', documentVersion: `url:${rawUrl('body.html')}`, elementId: 'new',
})) });
expect(result).toMatchObject({ found: { type: 'od:comment-active-target-update', documentVersion: `url:${rawUrl('body.html')}`, elementId: 'new', text: 'Changed copy' },
replacement: { elementId: 'cmt:12345678-1234-4234-8234-123456789abc',
selector: '[data-od-comment-id="12345678-1234-4234-8234-123456789abc"]', requestedSelector: 'body > button:nth-of-type(1)' },
missing: { type: 'od:comment-location-missing', requestId: 'save' } });
} finally { dom.window.close(); }
});
it('URL served complete bridge strictly describes and locates persisted comment identities', async () => {
const response = await fetch(`${rawUrl('body.html')}?odPreviewBridge=selection`);
expect(response.status).toBe(200);
const bridge = load(await response.text())('script[data-od-url-selection-bridge]').text();
const uuid = '12345678-1234-4234-8234-123456789abc';
const dom = new JSDOM(`<body><h2 data-od-id="tool" data-od-comment-id="${uuid}">Target</h2></body>`, {
url: rawUrl('body.html'), runScripts: 'outside-only', pretendToBeVisual: true,
});
try {
dom.window.eval(`window.posted=[]; window.postMessage=data=>window.posted.push(data);
window.requestAnimationFrame=fn=>{ fn(); return 1; };
document.querySelector('h2').getBoundingClientRect=()=>new DOMRect(0,0,100,100);
document.querySelector('h2').scrollIntoView=()=>{window.scrolled=true;};`);
dom.window.eval(bridge);
const result = dom.window.eval(`(()=>{
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-mode',enabled:true,mode:'picker'}}));
document.querySelector('h2').dispatchEvent(new MouseEvent('click',{bubbles:true}));
var selected=window.posted.find(data=>data.type==='od:comment-target');
var duplicate=document.createElement('h2'); duplicate.hidden=true; duplicate.setAttribute('data-od-comment-id','${uuid}'); document.body.appendChild(duplicate);
window.posted=[];
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-active-target',elementId:'cmt:${uuid}',selector:'[data-od-comment-id="${uuid}"]',locate:true,requestId:'hidden-duplicate'}}));
return {selected,scrolled:!!window.scrolled,messages:window.posted};
})()`);
expect(result).toMatchObject({ selected: { elementId: `cmt:${uuid}`, selector: `[data-od-comment-id="${uuid}"]` }, scrolled: false,
messages: expect.arrayContaining([expect.objectContaining({type:'od:comment-location-missing',requestId:'hidden-duplicate'})]) });
} finally { dom.window.close(); }
});
it.each(['hidden', 'wrong-prefix'] as const)('HTTP-served complete URL reader refuses %s after a healthy visible locate', async (failure) => {
const response = await fetch(`${rawUrl('body.html')}?odPreviewBridge=selection`);
expect(response.status).toBe(200);
const bridge = load(await response.text())('script[data-od-url-selection-bridge]').text();
const uuid = '12345678-1234-4234-8234-123456789abc';
const dom = new JSDOM(`<body><h2 data-od-comment-id="${uuid}">Target</h2></body>`, { url: rawUrl('body.html'), runScripts: 'outside-only', pretendToBeVisual: true });
try {
dom.window.eval(`window.posted=[]; window.scrollCount=0; window.postMessage=data=>window.posted.push(data);
window.requestAnimationFrame=fn=>{fn();return 1;};
document.querySelector('h2').getBoundingClientRect=()=>new DOMRect(0,0,100,100);
document.querySelector('h2').scrollIntoView=()=>{window.scrollCount++;};`);
dom.window.eval(bridge);
const healthy = dom.window.eval(`(()=>{
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-mode',enabled:true}}));
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-active-target',elementId:'cmt:${uuid}',selector:'[data-od-comment-id="${uuid}"]',locate:true,requestId:'healthy'}}));
return {scrolls:window.scrollCount,messages:window.posted};})()`);
expect(healthy).toMatchObject({ scrolls: 1, messages: expect.arrayContaining([expect.objectContaining({ type: 'od:comment-active-target-update', requestId: 'healthy' })]) });
const result = dom.window.eval(`(()=>{
window.posted=[];
${failure === 'hidden' ? "document.querySelector('h2').style.visibility='hidden';" : ''}
window.dispatchEvent(new MessageEvent('message',{data:{type:'od:comment-active-target',elementId:'${failure === 'wrong-prefix' ? 'bad:' : 'cmt:'}${uuid}',selector:'[data-od-comment-id="${uuid}"]',locate:true,requestId:'refused'}}));
return {scrolls:window.scrollCount,messages:window.posted};})()`);
expect(result).toMatchObject({ scrolls: 1, messages: expect.arrayContaining([expect.objectContaining({ type: 'od:comment-location-missing', requestId: 'refused' })]) });
} finally { dom.window.close(); }
});
it('injects the URL preview snapshot bridge only when requested', async () => {
const plain = await fetch(rawUrl('page.html'));
expect(await plain.text()).toBe('<html/>');
const bridged = await fetch(`${rawUrl('page.html')}?odPreviewBridge=snapshot`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-url-snapshot-bridge');
expect(html).toContain("type: 'od:snapshot:result'");
expect(html).not.toContain('data-od-url-scroll-bridge');
expect(html).not.toContain('data-od-url-selection-bridge');
});
it('injects URL preview observability before author scripts when requested', async () => {
const bridged = await fetch(`${rawUrl('body.html')}?odPreviewBridge=observability`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-preview-observability');
expect(html).toContain("send('runtime_error'");
expect(html).toContain("send('white_screen'");
expect(html.indexOf('data-od-preview-observability')).toBeLessThan(html.indexOf('<body>'));
});
it('injects passive URL guards before authored scripts', async () => {
const bridged = await fetch(
`${rawUrl('guarded.html')}?odPreviewBridge=sandbox&odPreviewBridge=focus&odPreviewBridge=redirect`,
);
expect(bridged.status).toBe(200);
const html = await bridged.text();
const authorScriptIndex = html.indexOf('<script src="./boot.js">');
expect(authorScriptIndex).toBeGreaterThan(-1);
expect(html).toContain('data-od-sandbox-shim');
expect(html).toContain('data-od-preview-focus-guard');
expect(html).toContain('data-od-preview-redirect-guard');
expect(html.indexOf('data-od-sandbox-shim')).toBeLessThan(authorScriptIndex);
expect(html.indexOf('data-od-preview-focus-guard')).toBeLessThan(authorScriptIndex);
expect(html.indexOf('data-od-preview-redirect-guard')).toBeLessThan(authorScriptIndex);
});
it('preserves and serves complex nested external resources through a guarded URL preview', async () => {
const response = await fetch(
`${rawUrl('prototypes/booking/index.html')}?odPreviewBridge=scroll&odPreviewBridge=selection&odPreviewBridge=snapshot&odPreviewBridge=observability&odPreviewBridge=sandbox&odPreviewBridge=focus`,
);
expect(response.status).toBe(200);
const html = await response.text();
const firstAuthorScript = html.indexOf('<script src="./scripts/support.js">');
expect(firstAuthorScript).toBeGreaterThan(-1);
expect(html.indexOf('data-od-sandbox-shim')).toBeLessThan(firstAuthorScript);
expect(html.indexOf('data-od-preview-focus-guard')).toBeLessThan(firstAuthorScript);
expect(html.match(/type="text\/babel"/g)).toHaveLength(43);
expect(html).toContain('<script type="module" src="./scripts/module.js"></script>');
expect(html).toContain('srcset="./assets/card.svg 1x, ./assets/card@2x.svg 2x"');
const baseHref = html.match(/<base href="([^"]+)" data-od-project-preview-base>/)?.[1];
expect(baseHref).toBeTruthy();
const previewBase = new URL(baseHref!, baseUrl);
const expectedResources = new Map([
['./styles/app.css', '@import "./theme.css"; .card { background-image: url("../assets/card.svg"); }'],
['./styles/theme.css', ':root { --accent: #0a7; }'],
['./scripts/support.js', 'window.__supportLoaded = true; fetch("./data.json").then((response) => response.json());'],
['./scripts/module.js', 'export const ready = true;'],
['./components/screen-43.jsx', 'window.__screen43 = () => <section>Screen 43</section>;'],
['./data.json', '{"ready":true}'],
['./assets/card.svg', '<svg xmlns="http://www.w3.org/2000/svg"/>'],
['./assets/card@2x.svg', '<svg xmlns="http://www.w3.org/2000/svg" width="2"/>'],
]);
for (const [relativePath, expectedBody] of expectedResources) {
const assetResponse = await fetch(new URL(relativePath, previewBase));
expect(assetResponse.status, relativePath).toBe(200);
expect(await assetResponse.text(), relativePath).toBe(expectedBody);
}
});
it('serves built dist HTML for Vite dev entries so previews do not load /src from daemon root', async () => {
const res = await fetch(rawUrl('vite-entry.html'));
expect(res.status).toBe(200);
const html = await res.text();
expect(html).not.toContain('/src/main.tsx');
expect(html).not.toContain('src="/assets/app.js"');
expect(html).not.toContain('href="/assets/app.css"');
expect(html).toContain('src="dist/assets/app.js"');
expect(html).toContain('href="dist/assets/app.css"');
});
it('does not expose powered preview project files to foreign browser origins through CORS', async () => {
const browserOrigin = new URL(baseUrl);
browserOrigin.hostname = browserOrigin.hostname === '127.0.0.1'
? 'localhost'
: '127.0.0.1';
const res = await fetch(poweredUrl('page.html'), {
headers: { Origin: browserOrigin.origin },
});
expect(res.status).toBe(200);
expect(res.headers.get('document-isolation-policy')).toBe('isolate-and-credentialless');
expect(res.headers.get('access-control-allow-origin')).toBeNull();
expect(await res.text()).toBe('<html/>');
const foreign = await fetch(poweredUrl('page.html'), {
headers: { Origin: 'https://foreign.example' },
});
expect(foreign.status).toBe(403);
expect(foreign.headers.get('access-control-allow-origin')).toBeNull();
const preflight = await fetch(poweredUrl('page.html'), {
method: 'OPTIONS',
headers: {
Origin: browserOrigin.origin,
'Access-Control-Request-Method': 'GET',
},
});
expect(preflight.status).toBe(204);
expect(preflight.headers.get('access-control-allow-origin')).toBeNull();
});
it('injects the URL preview scroll bridge for powered previews when requested', async () => {
const bridged = await fetch(`${poweredUrl('page.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
expect(bridged.headers.get('document-isolation-policy')).toBe('isolate-and-credentialless');
const html = await bridged.text();
expect(html).toContain('data-od-url-scroll-bridge');
expect(html).toContain("type: 'od:preview-content-size'");
expect(html).toContain('od:preview-content-size-request');
expect(html).toContain('lastContentSizeRequest.measurementId');
expect(html).toContain('lastContentSizeRequest.generation');
expect(html).toContain('documentEpoch: contentSizeDocumentEpoch');
expect(html).toContain("get('odPreviewEpoch')");
expect(html).toContain('scrollWidth: size && size.scrollWidth');
expect(html).toContain('clientWidth: size && size.clientWidth');
});
it('injects preview observability for powered previews when requested', async () => {
const bridged = await fetch(`${poweredUrl('page.html')}?odPreviewBridge=observability`);
expect(bridged.status).toBe(200);
expect(bridged.headers.get('document-isolation-policy')).toBe('isolate-and-credentialless');
const html = await bridged.text();
expect(html).toContain('data-od-preview-observability');
expect(html).toContain("send('runtime_error'");
expect(html).toContain("send('white_screen'");
});
it('does not let the powered preview origin call normal daemon APIs', async () => {
const origin = poweredOrigin();
const poweredReferer = `${origin}/api/projects/${projectId}/powered/page.html`;
const poweredFile = await fetch(`${origin}/api/projects/${projectId}/powered/page.html`, {
headers: {
Referer: poweredReferer,
'Sec-Fetch-Site': 'same-origin',
},
});
expect(poweredFile.status).toBe(200);
expect(await poweredFile.text()).toBe('<html/>');
const api = await fetch(`${origin}/api/projects`, {
headers: {
Referer: poweredReferer,
'Sec-Fetch-Site': 'same-origin',
},
});
expect(api.status).toBe(403);
expect(await api.json()).toEqual({
error: 'Powered preview origin cannot access this API route',
});
});
// Node fetch replaces a supplied Host header. Use the real HTTP transport
// to exercise the same request host that the browser's project URL sends.
function projectOriginRequest(
routePath: string,
options: { method?: string; headers: http.OutgoingHttpHeaders },
): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> {
return new Promise((resolve, reject) => {
const request = http.request(new URL(routePath, baseUrl), options, (response) => {
let body = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => { body += chunk; });
response.on('end', () => resolve({
status: response.statusCode ?? 0,
headers: response.headers,
body,
}));
});
request.on('error', reject);
request.end();
});
}
it('serves project-scoped powered files on their matching browser origin', async () => {
const host = `od-${createHash('sha256').update(projectId).digest('hex').slice(0, 32)}.localhost:${new URL(baseUrl).port}`;
const response = await projectOriginRequest(`/api/projects/${projectId}/powered/page.html`, {
headers: { Host: host, Origin: `http://${host}`, 'Sec-Fetch-Site': 'same-origin' },
});
expect(response.status).toBe(200);
expect(response.headers['document-isolation-policy']).toBe('isolate-and-credentialless');
expect(response.headers['access-control-allow-origin']).toBeUndefined();
expect(response.body).toBe('<html/>');
const capability = await fetch(`${baseUrl}/api/preview/isolation`);
expect(await capability.json()).toMatchObject({ projectOrigins: true });
});
it.each([
{ method: 'GET', path: '/api/projects' },
{ method: 'GET', path: '/api/preview/isolation' },
{ method: 'GET', path: '/api/library/clipper-probe' },
{ method: 'GET', path: '/api/live-artifacts/missing/preview' },
{ method: 'GET', path: '/api/projects/another-project/powered/page.html' },
{ method: 'GET', path: '/api/projects/proj-raw-range-test/raw/page.html' },
{ method: 'POST', path: '/api/projects/proj-raw-range-test/powered/page.html' },
{ method: 'GET', path: '/artifacts/private.png' },
{ method: 'GET', path: '/frames/index.html' },
{ method: 'GET', path: '/' },
])('restricts project-scoped powered origins before serving $method $path', async ({ method, path: routePath }) => {
const host = `od-${createHash('sha256').update(projectId).digest('hex').slice(0, 32)}.localhost:${new URL(baseUrl).port}`;
const response = await projectOriginRequest(routePath, {
method,
headers: { Host: host, 'Sec-Fetch-Site': 'same-origin' },
});
expect(response.status).toBe(403);
});
it('rejects foreign project-scoped powered subresources and API reads through the normal host', async () => {
const host = `od-${createHash('sha256').update(projectId).digest('hex').slice(0, 32)}.localhost:${new URL(baseUrl).port}`;
const otherHost = `od-${createHash('sha256').update('another-project').digest('hex').slice(0, 32)}.localhost:${new URL(baseUrl).port}`;
const foreignAsset = await projectOriginRequest(`/api/projects/${projectId}/powered/page.html`, {
headers: {
Host: host,
Referer: `http://${otherHost}/api/projects/another-project/powered/page.html`,
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Dest': 'script',
},
});
expect(foreignAsset.status).toBe(403);
const api = await projectOriginRequest('/api/projects', {
headers: {
Referer: `http://${host}/api/projects/${projectId}/powered/page.html`,
'Sec-Fetch-Site': 'cross-site',
},
});
expect(api.status).toBe(403);
});
it('injects all URL preview bridges together', async () => {
const bridged = await fetch(`${rawUrl('body.html')}?odPreviewBridge=scroll&odPreviewBridge=selection&odPreviewBridge=snapshot&odPreviewBridge=observability`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html).toContain('data-od-url-scroll-bridge');
expect(html).toContain('data-od-url-selection-bridge');
expect(html).toContain('data-od-url-snapshot-bridge');
expect(html).toContain('data-od-preview-observability');
expect(html.indexOf('data-od-preview-observability')).toBeLessThan(html.indexOf('<body>'));
expect(html.indexOf('data-od-url-scroll-bridge')).toBeLessThan(html.indexOf('</body>'));
expect(html.indexOf('data-od-url-selection-bridge')).toBeLessThan(html.indexOf('</body>'));
expect(html.indexOf('data-od-url-snapshot-bridge')).toBeLessThan(html.indexOf('</body>'));
});
it('does not inject the URL preview scroll bridge twice', async () => {
const bridged = await fetch(`${rawUrl('bridged.html')}?odPreviewBridge=scroll`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html.match(/data-od-url-scroll-bridge/g)?.length).toBe(1);
});
it('does not inject the URL preview selection bridge twice', async () => {
const bridged = await fetch(`${rawUrl('selection-bridged.html')}?odPreviewBridge=selection`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html.match(/data-od-url-selection-bridge/g)?.length).toBe(1);
});
it('does not inject the URL preview snapshot bridge twice', async () => {
const bridged = await fetch(`${rawUrl('snapshot-bridged.html')}?odPreviewBridge=snapshot`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html.match(/data-od-url-snapshot-bridge/g)?.length).toBe(1);
});
it('does not inject the URL preview observability bridge twice', async () => {
const bridged = await fetch(`${rawUrl('observability-bridged.html')}?odPreviewBridge=observability`);
expect(bridged.status).toBe(200);
const html = await bridged.text();
expect(html.match(/data-od-preview-observability/g)?.length).toBe(1);
});
it('returns 404 for a missing file', async () => {
const res = await fetch(rawUrl('missing.mp4'));
expect(res.status).toBe(404);
});
});