feat(addInitScript): support cjs modules when passing both path and arg (#32240)
This works with scripts bundled by:
- `esbuild entrypoint.ts --bundle --format=cjs --outfile=injected.js`
- webpack with a typical config
```js
module.exports = {
entry: { 'injected': './entrypoint.js', },
output: {
path: require('path').resolve(__dirname),
filename: '[name].js',
libraryTarget: 'commonjs2',
},
};
```
This commit is contained in:
parent
837e2a883b
commit
d5a7495041
|
|
@ -415,13 +415,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
|
||||||
[`method: Page.addInitScript`] is not defined.
|
[`method: Page.addInitScript`] is not defined.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
**Bundling**
|
||||||
|
|
||||||
|
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
|
||||||
|
|
||||||
|
```js browser title="mocks/mockRandom.ts"
|
||||||
|
// This script can import other files.
|
||||||
|
import { defaultValue } from './defaultValue';
|
||||||
|
|
||||||
|
export default function(value?: number) {
|
||||||
|
window.Math.random = () => value ?? defaultValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# bundle with esbuild
|
||||||
|
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
|
||||||
|
```
|
||||||
|
|
||||||
|
```js title="tests/example.spec.ts"
|
||||||
|
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
|
||||||
|
|
||||||
|
// Passing 42 as an argument to the default export function.
|
||||||
|
await context.addInitScript({ path: mockPath }, 42);
|
||||||
|
|
||||||
|
// Make sure to pass undefined even if you do not need to pass an argument.
|
||||||
|
// This instructs Playwright to treat the file as a commonjs module.
|
||||||
|
await context.addInitScript({ path: mockPath }, undefined);
|
||||||
|
```
|
||||||
|
|
||||||
### param: BrowserContext.addInitScript.script
|
### param: BrowserContext.addInitScript.script
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: js
|
* langs: js
|
||||||
- `script` <[function]|[string]|[Object]>
|
- `script` <[function]|[string]|[Object]>
|
||||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||||
current working directory. Optional.
|
current working directory.
|
||||||
- `content` ?<[string]> Raw script content. Optional.
|
- `content` ?<[string]> Raw script content.
|
||||||
|
|
||||||
Script to be evaluated in all pages in the browser context.
|
Script to be evaluated in all pages in the browser context.
|
||||||
|
|
||||||
|
|
@ -437,7 +466,9 @@ Script to be evaluated in all pages in the browser context.
|
||||||
* langs: js
|
* langs: js
|
||||||
- `arg` ?<[Serializable]>
|
- `arg` ?<[Serializable]>
|
||||||
|
|
||||||
Optional argument to pass to [`param: script`] (only supported when passing a function).
|
Optional JSON-serializable argument to pass to [`param: script`].
|
||||||
|
* When `script` is a function, the argument is passed to it directly.
|
||||||
|
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
|
||||||
|
|
||||||
### param: BrowserContext.addInitScript.path
|
### param: BrowserContext.addInitScript.path
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
|
||||||
|
|
@ -619,13 +619,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
|
||||||
[`method: Page.addInitScript`] is not defined.
|
[`method: Page.addInitScript`] is not defined.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
**Bundling**
|
||||||
|
|
||||||
|
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
|
||||||
|
|
||||||
|
```js browser title="mocks/mockRandom.ts"
|
||||||
|
// This script can import other files.
|
||||||
|
import { defaultValue } from './defaultValue';
|
||||||
|
|
||||||
|
export default function(value?: number) {
|
||||||
|
window.Math.random = () => value ?? defaultValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# bundle with esbuild
|
||||||
|
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
|
||||||
|
```
|
||||||
|
|
||||||
|
```js title="tests/example.spec.ts"
|
||||||
|
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
|
||||||
|
|
||||||
|
// Passing 42 as an argument to the default export function.
|
||||||
|
await page.addInitScript({ path: mockPath }, 42);
|
||||||
|
|
||||||
|
// Make sure to pass undefined even if you do not need to pass an argument.
|
||||||
|
// This instructs Playwright to treat the file as a commonjs module.
|
||||||
|
await page.addInitScript({ path: mockPath }, undefined);
|
||||||
|
```
|
||||||
|
|
||||||
### param: Page.addInitScript.script
|
### param: Page.addInitScript.script
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: js
|
* langs: js
|
||||||
- `script` <[function]|[string]|[Object]>
|
- `script` <[function]|[string]|[Object]>
|
||||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||||
current working directory. Optional.
|
current working directory.
|
||||||
- `content` ?<[string]> Raw script content. Optional.
|
- `content` ?<[string]> Raw script content.
|
||||||
|
|
||||||
Script to be evaluated in the page.
|
Script to be evaluated in the page.
|
||||||
|
|
||||||
|
|
@ -641,7 +670,9 @@ Script to be evaluated in all pages in the browser context.
|
||||||
* langs: js
|
* langs: js
|
||||||
- `arg` ?<[Serializable]>
|
- `arg` ?<[Serializable]>
|
||||||
|
|
||||||
Optional argument to pass to [`param: script`] (only supported when passing a function).
|
Optional JSON-serializable argument to pass to [`param: script`].
|
||||||
|
* When `script` is a function, the argument is passed to it directly.
|
||||||
|
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
|
||||||
|
|
||||||
### param: Page.addInitScript.path
|
### param: Page.addInitScript.path
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
||||||
const source = await evaluationScript(script, arg);
|
const source = await evaluationScript(script, arg, arguments.length > 1);
|
||||||
await this._channel.addInitScript({ source });
|
await this._channel.addInitScript({ source });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,20 +28,37 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, hasArg: boolean, addSourceUrl: boolean = true): Promise<string> {
|
||||||
if (typeof fun === 'function') {
|
if (typeof fun === 'function') {
|
||||||
const source = fun.toString();
|
const source = fun.toString();
|
||||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||||
return `(${source})(${argString})`;
|
return `(${source})(${argString})`;
|
||||||
}
|
}
|
||||||
if (arg !== undefined)
|
if (isString(fun)) {
|
||||||
throw new Error('Cannot evaluate a string with arguments');
|
if (arg !== undefined)
|
||||||
if (isString(fun))
|
throw new Error('Cannot evaluate a string with arguments');
|
||||||
return fun;
|
return fun;
|
||||||
if (fun.content !== undefined)
|
}
|
||||||
|
if (fun.content !== undefined) {
|
||||||
|
if (arg !== undefined)
|
||||||
|
throw new Error('Cannot evaluate a string with arguments');
|
||||||
return fun.content;
|
return fun.content;
|
||||||
|
}
|
||||||
if (fun.path !== undefined) {
|
if (fun.path !== undefined) {
|
||||||
let source = await fs.promises.readFile(fun.path, 'utf8');
|
let source = await fs.promises.readFile(fun.path, 'utf8');
|
||||||
|
if (hasArg) {
|
||||||
|
// Assume a CJS module that has a function default export.
|
||||||
|
source = `(() => {
|
||||||
|
var exports = {}; var module = { exports };
|
||||||
|
${source}
|
||||||
|
let __pw_result__ = module.exports;
|
||||||
|
if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__))
|
||||||
|
__pw_result__ = __pw_result__['default'];
|
||||||
|
if (typeof __pw_result__ !== 'function')
|
||||||
|
return __pw_result__;
|
||||||
|
return __pw_result__(${JSON.stringify(arg)});
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
if (addSourceUrl)
|
if (addSourceUrl)
|
||||||
source = addSourceUrlToScript(source, fun.path);
|
source = addSourceUrlToScript(source, fun.path);
|
||||||
return source;
|
return source;
|
||||||
|
|
|
||||||
|
|
@ -492,7 +492,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
||||||
const source = await evaluationScript(script, arg);
|
const source = await evaluationScript(script, arg, arguments.length > 1);
|
||||||
await this._channel.addInitScript({ source });
|
await this._channel.addInitScript({ source });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export class Selectors implements api.Selectors {
|
||||||
private _registrations: channels.SelectorsRegisterParams[] = [];
|
private _registrations: channels.SelectorsRegisterParams[] = [];
|
||||||
|
|
||||||
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||||
const source = await evaluationScript(script, undefined, false);
|
const source = await evaluationScript(script, undefined, false, false);
|
||||||
const params = { ...options, name, source };
|
const params = { ...options, name, source };
|
||||||
for (const channel of this._channels)
|
for (const channel of this._channels)
|
||||||
await channel._channel.register(params);
|
await channel._channel.register(params);
|
||||||
|
|
|
||||||
70
packages/playwright-core/types/types.d.ts
vendored
70
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -288,8 +288,41 @@ export interface Page {
|
||||||
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
|
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
|
||||||
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
||||||
* defined.
|
* defined.
|
||||||
|
*
|
||||||
|
* **Bundling**
|
||||||
|
*
|
||||||
|
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
|
||||||
|
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
|
||||||
|
* commonjs module and pass `path` and `arg`.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // mocks/mockRandom.ts
|
||||||
|
* // This script can import other files.
|
||||||
|
* import { defaultValue } from './defaultValue';
|
||||||
|
*
|
||||||
|
* export default function(value?: number) {
|
||||||
|
* window.Math.random = () => value ?? defaultValue;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // tests/example.spec.ts
|
||||||
|
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
|
||||||
|
*
|
||||||
|
* // Passing 42 as an argument to the default export function.
|
||||||
|
* await page.addInitScript({ path: mockPath }, 42);
|
||||||
|
*
|
||||||
|
* // Make sure to pass undefined even if you do not need to pass an argument.
|
||||||
|
* // This instructs Playwright to treat the file as a commonjs module.
|
||||||
|
* await page.addInitScript({ path: mockPath }, undefined);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* @param script Script to be evaluated in the page.
|
* @param script Script to be evaluated in the page.
|
||||||
* @param arg Optional argument to pass to `script` (only supported when passing a function).
|
* @param arg Optional JSON-serializable argument to pass to `script`.
|
||||||
|
* - When `script` is a function, the argument is passed to it directly.
|
||||||
|
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
|
||||||
|
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
|
||||||
|
* argument.
|
||||||
*/
|
*/
|
||||||
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
|
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
|
||||||
|
|
||||||
|
|
@ -7666,8 +7699,41 @@ export interface BrowserContext {
|
||||||
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
|
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
|
||||||
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
||||||
* defined.
|
* defined.
|
||||||
|
*
|
||||||
|
* **Bundling**
|
||||||
|
*
|
||||||
|
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
|
||||||
|
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
|
||||||
|
* commonjs module and pass `path` and `arg`.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // mocks/mockRandom.ts
|
||||||
|
* // This script can import other files.
|
||||||
|
* import { defaultValue } from './defaultValue';
|
||||||
|
*
|
||||||
|
* export default function(value?: number) {
|
||||||
|
* window.Math.random = () => value ?? defaultValue;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // tests/example.spec.ts
|
||||||
|
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
|
||||||
|
*
|
||||||
|
* // Passing 42 as an argument to the default export function.
|
||||||
|
* await context.addInitScript({ path: mockPath }, 42);
|
||||||
|
*
|
||||||
|
* // Make sure to pass undefined even if you do not need to pass an argument.
|
||||||
|
* // This instructs Playwright to treat the file as a commonjs module.
|
||||||
|
* await context.addInitScript({ path: mockPath }, undefined);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* @param script Script to be evaluated in all pages in the browser context.
|
* @param script Script to be evaluated in all pages in the browser context.
|
||||||
* @param arg Optional argument to pass to `script` (only supported when passing a function).
|
* @param arg Optional JSON-serializable argument to pass to `script`.
|
||||||
|
* - When `script` is a function, the argument is passed to it directly.
|
||||||
|
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
|
||||||
|
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
|
||||||
|
* argument.
|
||||||
*/
|
*/
|
||||||
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
|
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
|
||||||
|
|
||||||
|
|
|
||||||
33
tests/assets/injectedmodule.js
Normal file
33
tests/assets/injectedmodule.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"use strict";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
|
||||||
|
// one.ts
|
||||||
|
var one_exports = {};
|
||||||
|
__export(one_exports, {
|
||||||
|
default: () => one_default
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(one_exports);
|
||||||
|
|
||||||
|
// two.ts
|
||||||
|
var value = 42;
|
||||||
|
|
||||||
|
// one.ts
|
||||||
|
function one_default(arg) {
|
||||||
|
window.injected = arg ?? value;
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,18 @@ it('should work with a path', async ({ page, server, asset }) => {
|
||||||
expect(await page.evaluate(() => window['result'])).toBe(123);
|
expect(await page.evaluate(() => window['result'])).toBe(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should assume CJS module with a path and arg', async ({ page, server, asset }) => {
|
||||||
|
await page.addInitScript({ path: asset('injectedmodule.js') }, 17);
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
expect(await page.evaluate(() => window['injected'])).toBe(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assume CJS module with a path and undefined arg', async ({ page, server, asset }) => {
|
||||||
|
await page.addInitScript({ path: asset('injectedmodule.js') }, undefined);
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
expect(await page.evaluate(() => window['injected'])).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with content @smoke', async ({ page, server }) => {
|
it('should work with content @smoke', async ({ page, server }) => {
|
||||||
await page.addInitScript({ content: 'window["injected"] = 123' });
|
await page.addInitScript({ content: 'window["injected"] = 123' });
|
||||||
await page.goto(server.PREFIX + '/tamperable.html');
|
await page.goto(server.PREFIX + '/tamperable.html');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue