import { describe, expect, it } from 'vitest'; import { hasTweaksTemplate, hasUrlModeBridge, htmlNeedsFocusGuard, htmlNeedsPoweredPreview, htmlNeedsSandboxShim, parseForceInline, shouldUrlLoadHtmlPreview, } from '../../src/components/file-viewer-render-mode'; describe('shouldUrlLoadHtmlPreview', () => { const base = { mode: 'preview' as const, isDeck: false, commentMode: false, forceInline: false }; it('URL-loads a plain HTML preview by default', () => { expect(shouldUrlLoadHtmlPreview(base)).toBe(true); }); it('falls back to srcDoc when the file is a deck (deck bridge required)', () => { expect(shouldUrlLoadHtmlPreview({ ...base, isDeck: true })).toBe(false); }); it('falls back to srcDoc when comment mode is active without a URL bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, commentMode: true })).toBe(false); }); it('keeps URL-load when comment mode is active and the artifact owns the bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, commentMode: true, urlModeBridge: true })).toBe(true); }); it('keeps URL-load when comment mode is active and the raw route injects the comment bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, commentMode: true, urlCommentBridge: true })).toBe(true); }); it('falls back to srcDoc when direct edit mode is active without an artifact-owned bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, editMode: true })).toBe(false); }); it('falls back to srcDoc when direct edit mode is active even if the artifact owns a URL bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, editMode: true, urlModeBridge: true })).toBe(false); }); it('falls back to srcDoc when inspect mode is active (selection bridge required)', () => { expect(shouldUrlLoadHtmlPreview({ ...base, inspectMode: true })).toBe(false); }); it('falls back to srcDoc when draw mode is active without a URL snapshot bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, drawMode: true })).toBe(false); }); it('keeps URL-load when draw mode is active and the raw route injected the snapshot bridge', () => { expect(shouldUrlLoadHtmlPreview({ ...base, drawMode: true, urlSnapshotBridge: true })).toBe(true); }); it('falls back to srcDoc when the artifact ships the class based tweaks template', () => { // Without this, a plain `.tw-panel` artifact would URL load on first // open, skip the tweaks bridge entirely, and leave the toolbar toggle // disabled (no `od:tweaks-available` ever fires). expect(shouldUrlLoadHtmlPreview({ ...base, tweaksBridge: true })).toBe(false); }); it('falls back to srcDoc when the user opts in via forceInline', () => { expect(shouldUrlLoadHtmlPreview({ ...base, forceInline: true })).toBe(false); }); it('falls back to srcDoc when the HTML source needs a focus guard', () => { expect(shouldUrlLoadHtmlPreview({ ...base, needsFocusGuard: true })).toBe(false); }); it('falls back to srcDoc when the source references project files by site-root path', () => { // URL-load serves the document untouched, so `/reference-assets/main.css` // resolves against the app origin root and 404s; only the srcDoc pipeline // rewrites confirmed root-relative refs into resolvable URLs. expect(shouldUrlLoadHtmlPreview({ ...base, projectRootAssetRefs: true })).toBe(false); }); it('does not URL-load while the source-code tab is active', () => { expect(shouldUrlLoadHtmlPreview({ ...base, mode: 'source' })).toBe(false); }); it('treats any disqualifying flag as sufficient on its own', () => { expect(shouldUrlLoadHtmlPreview({ ...base, isDeck: true, commentMode: true })).toBe(false); expect(shouldUrlLoadHtmlPreview({ ...base, isDeck: true, forceInline: true })).toBe(false); expect(shouldUrlLoadHtmlPreview({ ...base, commentMode: true, forceInline: true })).toBe(false); expect(shouldUrlLoadHtmlPreview({ ...base, tweaksBridge: true, forceInline: true })).toBe(false); expect(shouldUrlLoadHtmlPreview({ ...base, commentMode: true, urlModeBridge: true, inspectMode: true })).toBe(false); expect(shouldUrlLoadHtmlPreview({ ...base, drawMode: true, urlSnapshotBridge: true, inspectMode: true })).toBe(false); }); }); describe('hasTweaksTemplate', () => { it('matches a plain `.tw-panel` artifact', () => { const source = ''; expect(hasTweaksTemplate(source)).toBe(true); }); it('matches the `.tw-hidden` toggle class even without an explicit `.tw-panel`', () => { // Defensive: the template ships both selectors and either one signals a // tweaks-template artifact that needs the bridge. const source = ''; expect(hasTweaksTemplate(source)).toBe(true); }); it('does not match unrelated identifiers that merely contain `tw`', () => { expect(hasTweaksTemplate('
tweet
')).toBe(false); expect(hasTweaksTemplate('twk-panel, btw-panel, mtw-hidden')).toBe(false); }); it('returns false for empty / null / undefined input', () => { expect(hasTweaksTemplate('')).toBe(false); expect(hasTweaksTemplate(null)).toBe(false); expect(hasTweaksTemplate(undefined)).toBe(false); }); }); describe('hasUrlModeBridge', () => { it('detects an artifact-owned direct-edit bridge script', () => { expect(hasUrlModeBridge('')).toBe(true); expect(hasUrlModeBridge('')).toBe(true); }); it('ignores comments, text nodes, and inline script bodies that only mention the bridge name', () => { expect(hasUrlModeBridge('')).toBe(false); expect(hasUrlModeBridge('

Use od-direct-edit.js for editing

')).toBe(false); expect(hasUrlModeBridge('')).toBe(false); }); it('ignores unrelated script URLs', () => { expect(hasUrlModeBridge('')).toBe(false); expect(hasUrlModeBridge(null)).toBe(false); }); }); describe('parseForceInline', () => { it('returns false when the parameter is absent', () => { expect(parseForceInline('')).toBe(false); expect(parseForceInline('?other=1')).toBe(false); expect(parseForceInline(null)).toBe(false); expect(parseForceInline(undefined)).toBe(false); }); it('returns true for the documented opt-in values', () => { expect(parseForceInline('?forceInline=1')).toBe(true); expect(parseForceInline('?forceInline=true')).toBe(true); expect(parseForceInline('?forceInline=TRUE')).toBe(true); expect(parseForceInline('?forceInline=yes')).toBe(true); expect(parseForceInline('?forceInline=on')).toBe(true); }); it('returns false for explicit opt-out values and unrelated strings', () => { expect(parseForceInline('?forceInline=0')).toBe(false); expect(parseForceInline('?forceInline=false')).toBe(false); expect(parseForceInline('?forceInline=no')).toBe(false); expect(parseForceInline('?forceInline=off')).toBe(false); expect(parseForceInline('?forceInline=banana')).toBe(false); }); it('treats an empty value as absent (defensive: ?forceInline= shows up as "")', () => { expect(parseForceInline('?forceInline=')).toBe(false); }); it('accepts a pre-built URLSearchParams', () => { const params = new URLSearchParams('forceInline=1&other=foo'); expect(parseForceInline(params)).toBe(true); }); it('survives surrounding whitespace in the value', () => { const params = new URLSearchParams(); params.set('forceInline', ' 1 '); expect(parseForceInline(params)).toBe(true); }); }); describe('htmlNeedsSandboxShim', () => { it('returns false for plain static HTML', () => { expect(htmlNeedsSandboxShim('

hello

')).toBe(false); }); it('detects ', ), ).toBe(true); // Single quotes. expect(htmlNeedsSandboxShim("")).toBe(true); // Extra attributes before type=. expect( htmlNeedsSandboxShim(''), ).toBe(true); // Whitespace around the equals sign. expect(htmlNeedsSandboxShim('')).toBe(true); // Case-insensitive type value. expect(htmlNeedsSandboxShim('')).toBe(true); }); it('detects unquoted ')).toBe(true); // Unquoted with an unquoted src= following — terminates on whitespace. expect( htmlNeedsSandboxShim(''), ).toBe(true); // Mixed: unquoted type=, then a quoted src=. expect( htmlNeedsSandboxShim(''), ).toBe(true); // Trailing `\b` rejects word continuations: `type=text/babelish` does // not match because `l`→`i` is a word-internal transition. Hyphenated // variants like `type=text/babel-other` still match per the helper // docstring (`l`→`-` is a word boundary) — that's the documented safe // false-positive direction, so it is intentionally not asserted here. expect(htmlNeedsSandboxShim('')).toBe(false); }); it('does not match unrelated MIME types or inline-only ')).toBe(false); // Substring-only matches must not trigger (e.g. text/babel-like custom type). expect(htmlNeedsSandboxShim('')).toBe(false); // A bare inline ')).toBe(false); }); it('detects direct localStorage / sessionStorage references in the source', () => { expect(htmlNeedsSandboxShim('')).toBe(true); expect(htmlNeedsSandboxShim('')).toBe(true); // Inside an external script tag's surrounding markup still trips the // scan when the literal name appears in the document the iframe loads. expect(htmlNeedsSandboxShim('// uses localStorage to persist theme')).toBe(true); }); it('does not match incidental substrings that are not the storage globals', () => { expect(htmlNeedsSandboxShim('Storage')).toBe(false); expect(htmlNeedsSandboxShim('mylocalStorageWrapper')).toBe(false); expect(htmlNeedsSandboxShim('SuperLocalStorage')).toBe(false); }); // Issue #2361 — Tweaks and animations problems // Agent-emitted artifacts commonly read `localStorage` from an *external* // script (e.g. `')).toBe(true); // ES module import. expect(htmlNeedsSandboxShim('')).toBe(true); // Attributes between ')).toBe(true); expect(htmlNeedsSandboxShim('')).toBe(true); // Single-quoted src. expect(htmlNeedsSandboxShim("")).toBe(true); // Whitespace around the equals sign. expect(htmlNeedsSandboxShim('')).toBe(true); // Unquoted src value (HTML5 permits unquoted attrs). expect(htmlNeedsSandboxShim('')).toBe(true); // Case-insensitive tag name. expect(htmlNeedsSandboxShim('')).toBe(true); }); it('does not match incidental "src=" in non-script contexts (issue #2361 regression)', () => { // `` is not an executable subresource for our purposes. expect(htmlNeedsSandboxShim('')).toBe(false); // `` similarly does not run JavaScript. expect(htmlNeedsSandboxShim('')).toBe(false); // Text content mentioning `script src=` (e.g. a docs page) must not trigger. expect(htmlNeedsSandboxShim('

Use <script src="app.js">

')).toBe(false); }); }); describe('htmlNeedsFocusGuard', () => { it('returns false for plain static HTML', () => { expect(htmlNeedsFocusGuard('

hello

')).toBe(false); }); it('detects window.focus() calls', () => { expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); }); it('detects document.body.focus() calls', () => { expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); }); it('detects querySelector(...).focus() and chained focus calls', () => { expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); }); it('detects autofocus attributes', () => { expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); }); it('detects external script references that may call focus at load', () => { expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); expect(htmlNeedsFocusGuard('')).toBe(true); }); it('does not match inline scripts without focus calls', () => { expect(htmlNeedsFocusGuard('')).toBe(false); expect(htmlNeedsFocusGuard('')).toBe(false); }); it('does not match unrelated focus mentions', () => { expect(htmlNeedsFocusGuard('
')).toBe(false); expect(htmlNeedsFocusGuard('// focus the element')).toBe(false); expect(htmlNeedsFocusGuard(':focus')).toBe(false); expect(htmlNeedsFocusGuard('focus-visible')).toBe(false); }); }); describe('htmlNeedsPoweredPreview', () => { it('returns false for empty / null input', () => { expect(htmlNeedsPoweredPreview('')).toBe(false); expect(htmlNeedsPoweredPreview(null)).toBe(false); expect(htmlNeedsPoweredPreview(undefined)).toBe(false); }); it('matches SharedArrayBuffer (needs cross-origin isolation)', () => { expect(htmlNeedsPoweredPreview('const b = new SharedArrayBuffer(16);')).toBe(true); }); it('matches Web Worker / SharedWorker construction', () => { expect(htmlNeedsPoweredPreview("const w = new Worker('sort.js');")).toBe(true); expect(htmlNeedsPoweredPreview('new SharedWorker(url)')).toBe(true); expect(htmlNeedsPoweredPreview('new Worker(blobUrl)')).toBe(true); }); it('matches importScripts inside a worker', () => { expect(htmlNeedsPoweredPreview("importScripts('lib.js')")).toBe(true); }); it('matches WASM streaming and .wasm references', () => { expect(htmlNeedsPoweredPreview('WebAssembly.instantiateStreaming(fetch(u))')).toBe(true); expect(htmlNeedsPoweredPreview('WebAssembly.compileStreaming(r)')).toBe(true); expect(htmlNeedsPoweredPreview('fetch("engine.wasm")')).toBe(true); }); it('matches WebGL2 / OffscreenCanvas / WebGPU', () => { expect(htmlNeedsPoweredPreview('canvas.getContext("webgl2")')).toBe(true); expect(htmlNeedsPoweredPreview("el.getContext('webgl2', {})")).toBe(true); expect(htmlNeedsPoweredPreview('new OffscreenCanvas(8, 8)')).toBe(true); expect(htmlNeedsPoweredPreview('const a = navigator.gpu')).toBe(true); }); it('does NOT match a plain WebGL1 canvas demo (runs fine in the opaque sandbox)', () => { expect(htmlNeedsPoweredPreview("canvas.getContext('webgl')")).toBe(false); expect(htmlNeedsPoweredPreview("canvas.getContext('2d')")).toBe(false); }); it('does NOT match unrelated prose that mentions the words', () => { expect(htmlNeedsPoweredPreview('

Our web worker culture is great

')).toBe(false); expect(htmlNeedsPoweredPreview('

A workshop about 3D printing

')).toBe(false); }); });