Skip to content

Commit

Permalink
Handle multiple Tabs/Windows (#149)
Browse files Browse the repository at this point in the history
* chore: add methods to handle multiple pages (tabs)

* chore: switch page using TargetLocator

* chore: make activeFrame private again

* chore: add test for handing multiple pages

* chore: new page as return value of waitForNewPage
  • Loading branch information
levanhieu8396 committed May 20, 2020
1 parent 3892201 commit 373c861
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 14 deletions.
9 changes: 9 additions & 0 deletions extern/dogfood/dockerfiles/watirspec/html/page_1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Handling multiple page - Page 1</title>
</head>
<body>
<a href="page_2.html" target="_blank">Open page 2 in new tab</a>
</body>
</html>
9 changes: 9 additions & 0 deletions extern/dogfood/dockerfiles/watirspec/html/page_2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Handling multiple page - Page 2</title>
</head>
<body>
This page was opened in a new tab
</body>
</html>
42 changes: 31 additions & 11 deletions packages/core/src/page/TargetLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementHandle | null> {
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()
Expand All @@ -23,7 +29,7 @@ export class TargetLocator implements ITargetLocator {
* Navigates to the topmost frame
*/
public async defaultContent(): Promise<void> {
this.apply(null)
this.applyFrame(null)
}

/**
Expand All @@ -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)]

Expand All @@ -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)

Expand All @@ -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))
Expand All @@ -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)
}
}
23 changes: 23 additions & 0 deletions packages/core/src/runtime/Browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
50 changes: 47 additions & 3 deletions packages/core/src/runtime/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export class Browser<T> implements BrowserInterface {
public screenshots: string[]
customContext: T

private newPageCallback: (resolve: (page: Page) => void) => void
private newPagePromise: Promise<Page>

constructor(
public workRoot: WorkRoot,
private client: PuppeteerClientLike,
Expand All @@ -60,6 +63,19 @@ export class Browser<T> 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<ExecutionContext> {
Expand Down Expand Up @@ -88,6 +104,10 @@ export class Browser<T> implements BrowserInterface {
return this.client.page
}

public get pages(): Promise<Page[]> {
return this.client.browser.pages()
}

public get frames(): Frame[] {
return getFrames(this.page.frames())
}
Expand Down Expand Up @@ -505,9 +525,13 @@ export class Browser<T> 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<PerformanceTiming> {
Expand Down Expand Up @@ -578,4 +602,24 @@ export class Browser<T> implements BrowserInterface {
// })
}
}

private async switchPage(page: Page | number): Promise<void> {
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<Page> {
const newPage = await this.newPagePromise

// wait for another page to be opened
this.newPagePromise = new Promise(resolve => {
this.newPageCallback(resolve)
})

return newPage
}
}
10 changes: 10 additions & 0 deletions packages/core/src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export interface Browser {
*/
page: Page

/**
* The list of current puppeteer Pages in the browser
*/
pages: Promise<Page[]>

/**
* The list of puppeteer Frames
*/
Expand Down Expand Up @@ -332,6 +337,11 @@ export interface Browser {
switchTo(): TargetLocator

setViewport(viewport: Viewport): Promise<void>

/**
* Wait for a new page to be opened in the browser then return that page.
*/
waitForNewPage(): Promise<Page>
}

/**
Expand Down

0 comments on commit 373c861

Please sign in to comment.