Skip to content

Commit

Permalink
chore: make execution context frame-independent
Browse files Browse the repository at this point in the history
  • Loading branch information
jrandolf committed Aug 25, 2022
1 parent 2f33237 commit 4c089f3
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 80 deletions.
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

0 comments on commit 4c089f3

Please sign in to comment.