diff --git a/extern/dogfood/dockerfiles/watirspec/html/page_1.html b/extern/dogfood/dockerfiles/watirspec/html/page_1.html new file mode 100644 index 000000000..a53c79548 --- /dev/null +++ b/extern/dogfood/dockerfiles/watirspec/html/page_1.html @@ -0,0 +1,9 @@ + + + + Handling multiple page - Page 1 + + + Open page 2 in new tab + + diff --git a/extern/dogfood/dockerfiles/watirspec/html/page_2.html b/extern/dogfood/dockerfiles/watirspec/html/page_2.html new file mode 100644 index 000000000..e620db2b3 --- /dev/null +++ b/extern/dogfood/dockerfiles/watirspec/html/page_2.html @@ -0,0 +1,9 @@ + + + + Handling multiple page - Page 2 + + + This page was opened in a new tab + + diff --git a/packages/core/src/page/TargetLocator.ts b/packages/core/src/page/TargetLocator.ts index c7997e181..a272cde2c 100644 --- a/packages/core/src/page/TargetLocator.ts +++ b/packages/core/src/page/TargetLocator.ts @@ -7,10 +7,16 @@ import { getFrames } from '../runtime/Browser' * @internal */ export class TargetLocator implements ITargetLocator { - constructor(private page: Page, private apply: (frame: Frame | null) => void) {} + constructor( + private currentPage: Page, + private applyFrame: (frame: Frame | null) => void, + private applyPage: (page: number | Page) => void, + ) {} public async activeElement(): Promise { - const jsHandle = await this.page.evaluateHandle(() => document.activeElement || document.body) + const jsHandle = await this.currentPage.evaluateHandle( + () => document.activeElement || document.body, + ) if (!jsHandle) return null const element = jsHandle.asElement() @@ -23,7 +29,7 @@ export class TargetLocator implements ITargetLocator { * Navigates to the topmost frame */ public async defaultContent(): Promise { - this.apply(null) + this.applyFrame(null) } /** @@ -38,18 +44,18 @@ export class TargetLocator implements ITargetLocator { * @param id number | string | ElementHandle */ public async frame(id: number | string | IElementHandle) { - let nextFrame: Frame | null - if (id === null) { this.defaultContent() return } - const frames = getFrames(this.page.frames()) + let nextFrame: Frame | null + + const frames = getFrames(this.currentPage.frames()) if (typeof id === 'number') { // Assume frame index - const frameElementName = await this.page.evaluate((index: number) => { + const frameElementName = await this.currentPage.evaluate((index: number) => { // NOTE typescript lib.dom lacks proper index signature for frames: Window to work const frame = (window as any).frames[Number(index)] @@ -60,13 +66,13 @@ export class TargetLocator implements ITargetLocator { nextFrame = frames.find(frame => frame.name() === frameElementName) || null if (!nextFrame) throw new Error(`Could not match frame by name or id: '${frameElementName}'`) - this.apply(nextFrame) + this.applyFrame(nextFrame) } else if (typeof id === 'string') { // Assume id or name attr nextFrame = frames.find(frame => frame.name() === id) || null if (nextFrame == null) { - const frameElementName = await this.page.evaluate((id: string) => { + const frameElementName = await this.currentPage.evaluate((id: string) => { // NOTE typescript lib.dom lacks proper index signature for frames: Window to work const frame = Array.from(window.frames).find(frame => frame.frameElement.id === id) @@ -78,7 +84,7 @@ export class TargetLocator implements ITargetLocator { } if (!nextFrame) throw new Error(`Could not match frame by name or id: '${id}'`) - this.apply(nextFrame) + this.applyFrame(nextFrame) } else if (id instanceof ElementHandle) { const tagName = await id.tagName() if (!tagName || !['FRAME', 'WINDOW', 'IFRAME'].includes(tagName)) @@ -93,7 +99,21 @@ export class TargetLocator implements ITargetLocator { nextFrame = frames.find(frame => frame.name() === name) || null if (!nextFrame) throw new Error(`Could not match frame by name or id: '${name}'`) - this.apply(nextFrame) + this.applyFrame(nextFrame) } } + + /** + * Switch the focus to another page in the browser. + * + * Accepts either: + * + * number: The index of the page in Browser.pages, + * Page: The page to switch to. + * + * @param page number | Page + */ + public async page(page: number | Page) { + await this.applyPage(page) + } } diff --git a/packages/core/src/runtime/Browser.spec.ts b/packages/core/src/runtime/Browser.spec.ts index 3659ab4ee..2b4375a5b 100644 --- a/packages/core/src/runtime/Browser.spec.ts +++ b/packages/core/src/runtime/Browser.spec.ts @@ -195,4 +195,27 @@ describe('Browser', () => { expect(text).toBe('aA') }) }) + + test('multiple pages handling', async () => { + const browser = new Browser(workRoot, puppeteer, DEFAULT_SETTINGS) + const url = await serve('page_1.html') + + await browser.visit(url) + await browser.click(By.tagName('a')) + const newPage = await browser.waitForNewPage() + expect(newPage.url()).toContain('/page_2.html') + + const pages = await browser.pages + + // 3 tabs - about:blank, page_1.html & page_2.html + expect(pages.length).toEqual(3) + + // switch page using page index + await browser.switchTo().page(1) + expect(browser.url).toContain('/page_1.html') + + // switch page using the page itself + await browser.switchTo().page(newPage) + expect(browser.url).toContain('/page_2.html') + }) }) diff --git a/packages/core/src/runtime/Browser.ts b/packages/core/src/runtime/Browser.ts index 49ce91050..6a92f85aa 100644 --- a/packages/core/src/runtime/Browser.ts +++ b/packages/core/src/runtime/Browser.ts @@ -50,6 +50,9 @@ export class Browser implements BrowserInterface { public screenshots: string[] customContext: T + private newPageCallback: (resolve: (page: Page) => void) => void + private newPagePromise: Promise + constructor( public workRoot: WorkRoot, private client: PuppeteerClientLike, @@ -60,6 +63,19 @@ export class Browser implements BrowserInterface { ) { this.beforeFunc && this.afterFunc this.screenshots = [] + + this.newPageCallback = resolve => { + this.client.browser.once('targetcreated', async target => { + const newPage = await target.page() + this.client.page = newPage + await newPage.bringToFront() + resolve(newPage) + }) + } + + this.newPagePromise = new Promise(resolve => { + this.newPageCallback(resolve) + }) } private get context(): Promise { @@ -88,6 +104,10 @@ export class Browser implements BrowserInterface { return this.client.page } + public get pages(): Promise { + return this.client.browser.pages() + } + public get frames(): Frame[] { return getFrames(this.page.frames()) } @@ -505,9 +525,13 @@ export class Browser implements BrowserInterface { * Switch the focus of the browser to another frame or window */ public switchTo(): TargetLocator { - return new TargetLocator(this.page, frame => { - this.activeFrame = frame - }) + return new TargetLocator( + this.page, + frame => { + this.activeFrame = frame + }, + page => this.switchPage(page), + ) } public async performanceTiming(): Promise { @@ -578,4 +602,24 @@ export class Browser implements BrowserInterface { // }) } } + + private async switchPage(page: Page | number): Promise { + if (typeof page === 'number') { + this.client.page = (await this.pages)[page] + } else { + this.client.page = page + } + await this.client.page.bringToFront() + } + + public async waitForNewPage(): Promise { + const newPage = await this.newPagePromise + + // wait for another page to be opened + this.newPagePromise = new Promise(resolve => { + this.newPageCallback(resolve) + }) + + return newPage + } } diff --git a/packages/core/src/runtime/types.ts b/packages/core/src/runtime/types.ts index f5f9bf79b..6a721a96a 100644 --- a/packages/core/src/runtime/types.ts +++ b/packages/core/src/runtime/types.ts @@ -101,6 +101,11 @@ export interface Browser { */ page: Page + /** + * The list of current puppeteer Pages in the browser + */ + pages: Promise + /** * The list of puppeteer Frames */ @@ -332,6 +337,11 @@ export interface Browser { switchTo(): TargetLocator setViewport(viewport: Viewport): Promise + + /** + * Wait for a new page to be opened in the browser then return that page. + */ + waitForNewPage(): Promise } /**