Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: make execution context frame-independent #8845

Merged
merged 1 commit into from Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/common/ElementHandle.ts
Expand Up @@ -92,6 +92,13 @@ export class ElementHandle<
return this.#frame.page();
}

/**
* @internal
*/
get frame(): Frame {
return this.#frame;
}

/**
* Queries the current element for an element matching the given selector.
*
Expand Down Expand Up @@ -294,8 +301,7 @@ export class ElementHandle<
selector: Selector,
options: Exclude<WaitForSelectorOptions, 'root'> = {}
): Promise<ElementHandle<NodeFor<Selector>> | null> {
const frame = this.executionContext().frame();
assert(frame);
const frame = this.#frame;
const adoptedRoot = await frame.worlds[PUPPETEER_WORLD].adoptHandle(this);
const handle = await frame.worlds[PUPPETEER_WORLD].waitForSelector(
selector,
Expand Down
83 changes: 16 additions & 67 deletions src/common/ExecutionContext.ts
Expand Up @@ -15,9 +15,7 @@
*/

import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js';
import {Frame} from './Frame.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {EvaluateFunc, HandleFor} from './types.js';
Expand Down Expand Up @@ -88,18 +86,6 @@ export class ExecutionContext {
this._contextName = contextPayload.name;
}

/**
* @returns The frame associated with this execution context.
*
* @remarks
* Not every execution context is associated with a frame. For example,
* {@link WebWorker | workers} have execution contexts that are not associated
* with frames.
*/
frame(): Frame | null {
return this._world ? this._world.frame() : null;
}

/**
* Evaluates the given function.
*
Expand Down Expand Up @@ -355,61 +341,24 @@ export class ExecutionContext {
}
return {value: arg};
}
}
}

function rewriteError(error: Error): Protocol.Runtime.EvaluateResponse {
if (error.message.includes('Object reference chain is too long')) {
return {result: {type: 'undefined'}};
}
if (error.message.includes("Object couldn't be returned by value")) {
return {result: {type: 'undefined'}};
}

if (
error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed')
) {
throw new Error(
'Execution context was destroyed, most likely because of a navigation.'
);
}
throw error;
}
const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => {
if (error.message.includes('Object reference chain is too long')) {
return {result: {type: 'undefined'}};
}
if (error.message.includes("Object couldn't be returned by value")) {
return {result: {type: 'undefined'}};
}

/**
* Iterates through the JavaScript heap and finds all the objects with the
* given prototype.
*
* @example
*
* ```ts
* // Create a Map object
* await page.evaluate(() => (window.map = new Map()));
* // Get a handle to the Map object prototype
* const mapPrototype = await page.evaluateHandle(() => Map.prototype);
* // Query all map instances into an array
* const mapInstances = await page.queryObjects(mapPrototype);
* // Count amount of map objects in heap
* const count = await page.evaluate(maps => maps.length, mapInstances);
* await mapInstances.dispose();
* await mapPrototype.dispose();
* ```
*
* @param prototypeHandle - a handle to the object prototype
* @returns A handle to an array of objects with the given prototype.
*/
async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<HandleFor<Prototype[]>> {
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert(
remoteObject.objectId,
'Prototype JSHandle must not be referencing primitive value'
if (
error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed')
) {
throw new Error(
'Execution context was destroyed, most likely because of a navigation.'
);
const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: remoteObject.objectId,
});
return createJSHandle(this, response.objects) as HandleFor<Prototype[]>;
}
}
throw error;
};
6 changes: 2 additions & 4 deletions src/common/util.ts
Expand Up @@ -25,7 +25,6 @@ import {ElementHandle} from './ElementHandle.js';
import {TimeoutError} from './Errors.js';
import {CommonEventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {JSHandle} from './JSHandle.js';

/**
Expand Down Expand Up @@ -218,9 +217,8 @@ export function createJSHandle(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle | ElementHandle<Node> {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame instanceof Frame) {
return new ElementHandle(context, remoteObject, frame);
if (remoteObject.subtype === 'node' && context._world) {
return new ElementHandle(context, remoteObject, context._world.frame());
}
return new JSHandle(context, remoteObject);
}
Expand Down
4 changes: 2 additions & 2 deletions test/src/ariaqueryhandler.spec.ts
Expand Up @@ -334,7 +334,7 @@ describeChromeOnly('AriaQueryHandler', () => {
await otherFrame!.evaluate(addElement, 'button');
await page.evaluate(addElement, 'button');
const elementHandle = await watchdog;
expect(elementHandle!.executionContext().frame()).toBe(page.mainFrame());
expect(elementHandle!.frame).toBe(page.mainFrame());
});

it('should run in specified frame', async () => {
Expand All @@ -350,7 +350,7 @@ describeChromeOnly('AriaQueryHandler', () => {
await frame1!.evaluate(addElement, 'button');
await frame2!.evaluate(addElement, 'button');
const elementHandle = await waitForSelectorPromise;
expect(elementHandle!.executionContext().frame()).toBe(frame2);
expect(elementHandle!.frame).toBe(frame2);
});

it('should throw when frame is detached', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/src/frame.spec.ts
Expand Up @@ -42,8 +42,8 @@ describe('Frame specs', function () {
expect(context1).toBeTruthy();
expect(context2).toBeTruthy();
expect(context1 !== context2).toBeTruthy();
expect(context1.frame()).toBe(frame1);
expect(context2.frame()).toBe(frame2);
expect(context1._world?.frame()).toBe(frame1);
expect(context2._world?.frame()).toBe(frame2);

await Promise.all([
context1.evaluate(() => {
Expand Down
6 changes: 3 additions & 3 deletions test/src/waittask.spec.ts
Expand Up @@ -477,7 +477,7 @@ describe('waittask specs', function () {
await otherFrame.evaluate(addElement, 'div');
await page.evaluate(addElement, 'div');
const eHandle = await watchdog;
expect(eHandle?.executionContext().frame()).toBe(page.mainFrame());
expect(eHandle?.frame).toBe(page.mainFrame());
}
);

Expand All @@ -492,7 +492,7 @@ describe('waittask specs', function () {
await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div');
const eHandle = await waitForSelectorPromise;
expect(eHandle?.executionContext().frame()).toBe(frame2);
expect(eHandle?.frame).toBe(frame2);
});

itFailsFirefox('should throw when frame is detached', async () => {
Expand Down Expand Up @@ -749,7 +749,7 @@ describe('waittask specs', function () {
await frame1.evaluate(addElement, 'div');
await frame2.evaluate(addElement, 'div');
const eHandle = await waitForXPathPromise;
expect(eHandle?.executionContext().frame()).toBe(frame2);
expect(eHandle?.frame).toBe(frame2);
});
itFailsFirefox('should throw when frame is detached', async () => {
const {page, server} = getTestState();
Expand Down