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
}
/**