Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/browser/base-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,43 @@ describe('BasePage.fetchJson', () => {
});
});

describe('BasePage.evaluateWithArgs', () => {
class RealEvalPage extends BasePage {
scripts: string[] = [];
async goto(): Promise<void> {}
async evaluate<T = unknown>(_js: string): Promise<T>;
async evaluate<Args extends unknown[], T>(_fn: BrowserEvaluateFunction<Args, T>, ..._args: Args): Promise<Awaited<T>>;
async evaluate(input: string | BrowserEvaluateFunction<unknown[], unknown>): Promise<unknown> {
this.scripts.push(typeof input === 'string' ? input : input.toString());
return null;
}
async getCookies(): Promise<[]> { return []; }
async screenshot(): Promise<string> { return ''; }
async tabs(): Promise<unknown[]> { return []; }
async selectTab(): Promise<void> {}
}

it('wraps declarations and body in an IIFE so const bindings are block-scoped', async () => {
const page = new RealEvalPage();
await page.evaluateWithArgs('(() => ({ ok: true }))()', { markerAttr: 'data-x', markerValue: 'v1' });
expect(page.scripts).toHaveLength(1);
const script = page.scripts[0];
// Declarations must live inside an IIFE so a second call into the same
// Runtime.evaluate context does not throw "Identifier 'markerAttr' has
// already been declared".
expect(script.startsWith('(() => {')).toBe(true);
expect(script.endsWith('})()')).toBe(true);
expect(script).toContain('const markerAttr = "data-x";');
expect(script).toContain('const markerValue = "v1";');
expect(script).toContain('return ((() => ({ ok: true }))());');
});

it('rejects keys that are not valid JS identifiers', async () => {
const page = new RealEvalPage();
await expect(page.evaluateWithArgs('(() => 1)()', { 'bad-key': 1 })).rejects.toThrow(/invalid key/);
});
});

describe('BasePage annotatedScreenshot', () => {
it('refreshes DOM refs, captures with a temporary visual overlay, and cleans up', async () => {
const page = new ActionPage();
Expand Down
8 changes: 7 additions & 1 deletion src/browser/base-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ export abstract class BasePage implements IPage {
* Each key in `args` becomes a `const` declaration with JSON-serialized value,
* prepended to the JS code. Prevents injection by design.
*
* The declarations and the caller-provided expression are wrapped in an IIFE
* so the `const` bindings are block-scoped. Without the wrapper, the bindings
* land in the script's top-level realm and the second call into the same
* Runtime.evaluate context throws `SyntaxError: Identifier '<key>' has already
* been declared` (the `markerAttr` regression hit by `browser upload`).
*
* Usage:
* page.evaluateWithArgs(`(async () => { return sym; })()`, { sym: userInput })
*/
Expand All @@ -166,7 +172,7 @@ export abstract class BasePage implements IPage {
return `const ${key} = ${JSON.stringify(value)};`;
})
.join('\n');
return this.evaluate(`${declarations}\n${js}`);
return this.evaluate(`(() => { ${declarations}\nreturn (${js}); })()`);
}

async fetchJson(url: string, opts: FetchJsonOptions = {}): Promise<unknown> {
Expand Down
Loading