chore: add injection framework (#8862)
* chore: add injection framework
This commit is contained in:
parent
9f5cb670e8
commit
292216652b
@ -214,7 +214,7 @@ export class Frame {
|
|||||||
this.#client = client;
|
this.#client = client;
|
||||||
this.worlds = {
|
this.worlds = {
|
||||||
[MAIN_WORLD]: new IsolatedWorld(this),
|
[MAIN_WORLD]: new IsolatedWorld(this),
|
||||||
[PUPPETEER_WORLD]: new IsolatedWorld(this),
|
[PUPPETEER_WORLD]: new IsolatedWorld(this, true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {Protocol} from 'devtools-protocol';
|
import {Protocol} from 'devtools-protocol';
|
||||||
|
import {source as injectedSource} from '../generated/injected.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
createDeferredPromise,
|
|
||||||
DeferredPromise,
|
|
||||||
} from '../util/DeferredPromise.js';
|
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {TimeoutError} from './Errors.js';
|
import {TimeoutError} from './Errors.js';
|
||||||
@ -117,8 +115,9 @@ export interface IsolatedWorldChart {
|
|||||||
*/
|
*/
|
||||||
export class IsolatedWorld {
|
export class IsolatedWorld {
|
||||||
#frame: Frame;
|
#frame: Frame;
|
||||||
|
#injected: boolean;
|
||||||
#document?: ElementHandle<Document>;
|
#document?: ElementHandle<Document>;
|
||||||
#contextPromise: DeferredPromise<ExecutionContext> = createDeferredPromise();
|
#contextPromise = createDeferredPromise<ExecutionContext>();
|
||||||
#detached = false;
|
#detached = false;
|
||||||
|
|
||||||
// Set of bindings that have been registered in the current context.
|
// Set of bindings that have been registered in the current context.
|
||||||
@ -140,10 +139,11 @@ export class IsolatedWorld {
|
|||||||
return `${name}_${contextId}`;
|
return `${name}_${contextId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(frame: Frame) {
|
constructor(frame: Frame, injected = false) {
|
||||||
// Keep own reference to client because it might differ from the FrameManager's
|
// Keep own reference to client because it might differ from the FrameManager's
|
||||||
// client for OOP iframes.
|
// client for OOP iframes.
|
||||||
this.#frame = frame;
|
this.#frame = frame;
|
||||||
|
this.#injected = injected;
|
||||||
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
this.#client.on('Runtime.bindingCalled', this.#onBindingCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,10 +169,9 @@ export class IsolatedWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContext(context: ExecutionContext): void {
|
setContext(context: ExecutionContext): void {
|
||||||
assert(
|
if (this.#injected) {
|
||||||
this.#contextPromise,
|
context.evaluate(injectedSource).catch(debugError);
|
||||||
`ExecutionContext ${context._contextId} has already been set.`
|
}
|
||||||
);
|
|
||||||
this.#ctxBindings.clear();
|
this.#ctxBindings.clear();
|
||||||
this.#contextPromise.resolve(context);
|
this.#contextPromise.resolve(context);
|
||||||
for (const waitTask of this._waitTasks) {
|
for (const waitTask of this._waitTasks) {
|
||||||
|
@ -1 +1,12 @@
|
|||||||
export * from './Poller.js';
|
import * as Poller from './Poller.js';
|
||||||
|
import * as util from './util.js';
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
self,
|
||||||
|
Object.freeze({
|
||||||
|
InjectedUtil: {
|
||||||
|
...Poller,
|
||||||
|
...util,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
18
src/injected/util.ts
Normal file
18
src/injected/util.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function from a string.
|
||||||
|
*/
|
||||||
|
export const createFunction = (
|
||||||
|
functionValue: string
|
||||||
|
): ((...args: unknown[]) => unknown) => {
|
||||||
|
let fn = createdFunctions.get(functionValue);
|
||||||
|
if (fn) {
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
fn = new Function(`return ${functionValue}`)() as (
|
||||||
|
...args: unknown[]
|
||||||
|
) => unknown;
|
||||||
|
createdFunctions.set(functionValue, fn);
|
||||||
|
return fn;
|
||||||
|
};
|
9
src/templates/injected.ts.tmpl
Normal file
9
src/templates/injected.ts.tmpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import * as Poller from '../injected/Poller.js';
|
||||||
|
import * as util from '../injected/util.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const InjectedUtil: Readonly<typeof Poller & typeof util>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const source = SOURCE_CODE;
|
@ -8,5 +8,6 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{"path": "../vendor/tsconfig.esm.json"},
|
{"path": "../vendor/tsconfig.esm.json"},
|
||||||
{"path": "../compat/esm/tsconfig.json"}
|
{"path": "../compat/esm/tsconfig.json"}
|
||||||
]
|
],
|
||||||
|
"exclude": ["injected/injected.ts"]
|
||||||
}
|
}
|
||||||
|
40
test/src/injected.spec.ts
Normal file
40
test/src/injected.spec.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2022 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import '../../lib/cjs/puppeteer/generated/injected.js';
|
||||||
|
import {PUPPETEER_WORLD} from '../../lib/cjs/puppeteer/common/IsolatedWorld.js';
|
||||||
|
import {
|
||||||
|
getTestState,
|
||||||
|
setupTestBrowserHooks,
|
||||||
|
setupTestPageAndContextHooks,
|
||||||
|
} from './mocha-utils.js';
|
||||||
|
|
||||||
|
describe('InjectedUtil tests', function () {
|
||||||
|
setupTestBrowserHooks();
|
||||||
|
setupTestPageAndContextHooks();
|
||||||
|
|
||||||
|
it('should work', async () => {
|
||||||
|
const {page} = getTestState();
|
||||||
|
|
||||||
|
const handle = await page
|
||||||
|
.mainFrame()
|
||||||
|
.worlds[PUPPETEER_WORLD].evaluate(() => {
|
||||||
|
return typeof InjectedUtil === 'object';
|
||||||
|
});
|
||||||
|
expect(handle).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -851,10 +851,13 @@ describe('network', function () {
|
|||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
await page.goto(httpsServer.PREFIX + '/setcookie.html');
|
await page.goto(httpsServer.PREFIX + '/setcookie.html');
|
||||||
|
const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
|
||||||
const response = await new Promise<HTTPResponse>(resolve => {
|
const response = await new Promise<HTTPResponse>(resolve => {
|
||||||
page.on('response', resolve);
|
page.on('response', response => {
|
||||||
const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
|
if (response.url() === url) {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
page.evaluate(src => {
|
page.evaluate(src => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', src);
|
xhr.open('GET', src);
|
||||||
|
@ -17,27 +17,37 @@ import {job} from './internal/job.js';
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
await job('', async ({name, inputs, outputs}) => {
|
await job('', async ({name, inputs, outputs}) => {
|
||||||
|
const input = inputs.find(input => {
|
||||||
|
return input.endsWith('injected.ts');
|
||||||
|
})!;
|
||||||
|
const template = await readFile(
|
||||||
|
inputs.find(input => {
|
||||||
|
return input.includes('injected.ts.tmpl');
|
||||||
|
})!,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
const tmp = await mkdtemp(name);
|
const tmp = await mkdtemp(name);
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
entryPoints: [inputs[0]!],
|
entryPoints: [input],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outdir: tmp,
|
outdir: tmp,
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
target: 'ES2019',
|
target: 'ES2019',
|
||||||
});
|
});
|
||||||
const baseName = path.basename(inputs[0]!);
|
const baseName = path.basename(input);
|
||||||
const content = await readFile(
|
const content = await readFile(
|
||||||
path.join(tmp, baseName.replace('.ts', '.js')),
|
path.join(tmp, baseName.replace('.ts', '.js')),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
const scriptContent = `/** @internal */
|
const scriptContent = template.replace(
|
||||||
export const source = ${JSON.stringify(content)};
|
'SOURCE_CODE',
|
||||||
`;
|
JSON.stringify(content)
|
||||||
|
);
|
||||||
await writeFile(outputs[0]!, scriptContent);
|
await writeFile(outputs[0]!, scriptContent);
|
||||||
await rimraf.sync(tmp);
|
rimraf.sync(tmp);
|
||||||
})
|
})
|
||||||
.inputs(['src/injected/**.ts'])
|
.inputs(['src/templates/injected.ts.tmpl', 'src/injected/**.ts'])
|
||||||
.outputs(['src/generated/injected.ts'])
|
.outputs(['src/generated/injected.ts'])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -59,8 +59,8 @@ class JobBuilder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
outputStats.reduce(reduceMaxTime, 0) >=
|
outputStats.reduce(reduceMinTime, Infinity) >
|
||||||
inputStats.reduce(reduceMinTime, Infinity)
|
inputStats.reduce(reduceMaxTime, 0)
|
||||||
) {
|
) {
|
||||||
shouldRun = false;
|
shouldRun = false;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user