import { describe, expect, test } from 'bun:test'; import { buildClickScript, buildInspectScript, buildScrollScript, buildSnapshotScript, buildTypeScript, } from './pageActions'; /** * These scripts are source text evaluated inside another page: nothing * type-checks them, and a value interpolated without escaping either breaks the * script or runs as code. */ const parses = (source: string): boolean => { try { new Function(source); return true; } catch { return false; } }; describe('page action scripts', () => { test('every script parses', () => { expect(parses(buildSnapshotScript())).toBe(true); expect(parses(buildSnapshotScript({ selector: '#main' }))).toBe(true); expect(parses(buildClickScript({ selector: '#save' }))).toBe(true); expect(parses(buildClickScript({ text: 'Save' }))).toBe(true); expect(parses(buildTypeScript({ selector: '#q', value: 'hello', submit: true }))).toBe(true); expect(parses(buildInspectScript({ selector: '#save' }))).toBe(true); expect(parses(buildScrollScript({ direction: 'bottom' }))).toBe(true); expect(parses(buildScrollScript({ selector: 'footer' }))).toBe(true); }); test('a hostile selector is embedded as data, not as code', () => { const hostile = `'); window.__owned = true; ('`; const script = buildClickScript({ selector: hostile }); expect(parses(script)).toBe(true); // Present only inside a quoted literal: that is what makes it inert. expect(script).toContain(JSON.stringify(hostile)); }); test('a typed value is embedded as data too', () => { const value = '"); alert(1); ("'; const script = buildTypeScript({ selector: '#q', value, submit: false }); expect(parses(script)).toBe(true); expect(script).toContain(JSON.stringify(value)); }); test('whitespace regexes survive interpolation', () => { // A doubled backslash here would produce a literal backslash-s and match // nothing, silently collapsing no whitespace at all. expect(buildSnapshotScript()).toContain('replace(/\\s+/g'); }); test('scrolling asks for instant behaviour, not the page preference', () => { // A page with scroll-behavior: smooth would otherwise still be animating // when the position is read. expect(buildScrollScript({ direction: 'bottom' })).toContain("behavior: 'instant'"); expect(buildScrollScript({ selector: 'footer' })).toContain("behavior: 'instant'"); }); test('a scoped snapshot reads only the subtree it was given', () => { const script = buildSnapshotScript({ selector: '#changelog' }); expect(script).toContain('"#changelog"'); expect(script).toContain('root.querySelectorAll'); }); });