71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
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');
|
||
|
|
});
|
||
|
|
});
|