Skip to content

Commit

Permalink
feat(frame): introduce frame.frameElement (#856)
Browse files Browse the repository at this point in the history
Fixes #839.
  • Loading branch information
dgozman authored Feb 6, 2020
1 parent 4be39f8 commit 6318ba6
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 0 deletions.
14 changes: 14 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@ An example of getting text from an iframe element:
- [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args)
- [frame.fill(selector, value, options)](#framefillselector-value-options)
- [frame.focus(selector, options)](#framefocusselector-options)
- [frame.frameElement()](#frameframeelement)
- [frame.goto(url[, options])](#framegotourl-options)
- [frame.hover(selector[, options])](#framehoverselector-options)
- [frame.isDetached()](#frameisdetached)
Expand Down Expand Up @@ -1916,6 +1917,19 @@ If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matchi
This method fetches an element with `selector` and focuses it.
If there's no element matching `selector`, the method throws an error.

#### frame.frameElement()
- returns: <[Promise]<[ElementHandle]>> Promise that resolves with a `frame` or `iframe` element handle which corresponds to this frame.

This is an inverse of [elementHandle.contentFrame()](#elementhandlecontentframe). Note that returned handle actually belongs to the parent frame.

This method throws an error if the frame has been detached before `frameElement()` returns.

```js
const frameElement = await frame.frameElement();
const contentFrame = await frameElement.contentFrame();
console.log(frame === contentFrame); // -> true
```

#### frame.goto(url[, options])
- `url` <[string]> URL to navigate frame to. The url should include scheme, e.g. `https://`.
- `options` <[Object]> Navigation parameters which might have the following properties:
Expand Down
12 changes: 12 additions & 0 deletions src/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,18 @@ export class CRPage implements PageDelegate {
coverage(): Coverage | undefined {
return this._coverage;
}

async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
const { backendNodeId } = await this._client.send('DOM.getFrameOwner', { frameId: frame._id }).catch(e => {
if (e instanceof Error && e.message.includes('Frame with the given id was not found.'))
e.message = 'Frame has been detached.';
throw e;
});
const parent = frame.parentFrame();
if (!parent)
throw new Error('Frame has been detached.');
return this.adoptBackendNodeId(backendNodeId, await parent._mainContext());
}
}

function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
Expand Down
17 changes: 17 additions & 0 deletions src/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,23 @@ export class FFPage implements PageDelegate {
coverage(): Coverage | undefined {
return undefined;
}

async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
const parent = frame.parentFrame();
if (!parent)
throw new Error('Frame has been detached.');
const context = await parent._utilityContext();
const handles = await context._$$('iframe');
const items = await Promise.all(handles.map(async handle => {
const frame = await handle.contentFrame().catch(e => null);
return { handle, frame };
}));
const result = items.find(item => item.frame === frame);
await Promise.all(items.map(item => item === result ? Promise.resolve() : item.handle.dispose()));
if (!result)
throw new Error('Frame has been detached.');
return result.handle;
}
}

export function normalizeWaitUntil(waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[]): frames.LifecycleEvent[] {
Expand Down
4 changes: 4 additions & 0 deletions src/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ export class Frame {
throw error;
}

async frameElement(): Promise<dom.ElementHandle> {
return this._page._delegate.getFrameElement(this);
}

_context(contextType: ContextType): Promise<dom.FrameExecutionContext> {
if (this._detached)
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
Expand Down
1 change: 1 addition & 0 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface PageDelegate {
layoutViewport(): Promise<{ width: number, height: number }>;
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;

getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
pdf?: (options?: types.PDFOptions) => Promise<platform.BufferType>;
Expand Down
17 changes: 17 additions & 0 deletions src/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,23 @@ export class WKPage implements PageDelegate {
return undefined;
}

async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
const parent = frame.parentFrame();
if (!parent)
throw new Error('Frame has been detached.');
const context = await parent._utilityContext();
const handles = await context._$$('iframe');
const items = await Promise.all(handles.map(async handle => {
const frame = await handle.contentFrame().catch(e => null);
return { handle, frame };
}));
const result = items.find(item => item.frame === frame);
await Promise.all(items.map(item => item === result ? Promise.resolve() : item.handle.dispose()));
if (!result)
throw new Error('Frame has been detached.');
return result.handle;
}

_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
if (event.request.url.startsWith('data:'))
return;
Expand Down
30 changes: 30 additions & 0 deletions test/frame.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
});
});

describe('Frame.frameElement', function() {
it('should work', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const frame2 = await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE);
const frame3 = await utils.attachFrame(page, 'frame3', server.EMPTY_PAGE);
const frame1handle1 = await page.$('#frame1');
const frame1handle2 = await frame1.frameElement();
const frame3handle1 = await page.$('#frame3');
const frame3handle2 = await frame3.frameElement();
expect(await frame1handle1.evaluate((a, b) => a === b, frame1handle2)).toBe(true);
expect(await frame3handle1.evaluate((a, b) => a === b, frame3handle2)).toBe(true);
expect(await frame1handle1.evaluate((a, b) => a === b, frame3handle1)).toBe(false);
});
it('should work with contentFrame', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
const handle = await frame.frameElement();
const contentFrame = await handle.contentFrame();
expect(contentFrame).toBe(frame);
});
it('should throw when detached', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
await page.$eval('#frame1', e => e.remove());
const error = await frame1.frameElement().catch(e => e);
expect(error.message).toBe('Frame has been detached.');
});
});

describe('Frame.evaluate', function() {
it('should throw for detached frames', async({page, server}) => {
const frame1 = await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE);
Expand Down

0 comments on commit 6318ba6

Please sign in to comment.