Skip to content

Commit

Permalink
feat(web): support for switching browsing context
Browse files Browse the repository at this point in the history
re #805
  • Loading branch information
jan-molak committed Dec 3, 2021
1 parent 44877bb commit a73a635
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 221 deletions.
365 changes: 214 additions & 151 deletions integration/web-specs/spec/screenplay/models/Page.spec.ts

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions integration/web-specs/static/screenplay/models/page/main_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<html lang="en">
<head>
<title>Main page title</title>
</head>
<body>
<h1>Main page</h1>
<ul>
<li><a href="/screenplay/models/page/new_tab.html" target="_blank" id="new-tab-link">open new tab</a></li>
<li><a href="javascript:void(0)" onclick="popup()" id="new-popup-link">open new popup</a></li>
</ul>


<script>
function popup() {
var w = window.open('about:blank', 'popup-window', 'width=512,height=256');
w.document.write('<h1>Example pop-up window</h1>');
w.document.close();
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html lang="en">
<head>
<title>New tab title</title>
</head>
<body>
<h1>New tab</h1>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -426,19 +426,30 @@ export class BrowseTheWebWithProtractor extends BrowseTheWeb {
return promiseOf(this.browser.close());
}

/**
* @desc
* Returns a {@link Page} representing the currently active top-level browsing context.
*
* @returns {Promise<Page>}
*/
async currentPage(): Promise<Page> {

const windowHandle = await this.browser.getWindowHandle();

return new ProtractorPage(this.browser, windowHandle);
}

async pageCalled(nameOrHandleOrIndex: string | number): Promise<Page> {

// const windowHandles = await this.browser.getWindowHandle();
/**
* @desc
* Returns an array of {@link Page} objects representing all the available
* top-level browsing context, e.g. all the open browser tabs.
*
* @returns {Promise<Array<Page>>}
*/
async allPages(): Promise<Array<Page>> {
const windowHandles = await this.browser.getAllWindowHandles();

// return new ProtractorPage(this.browser, windowHandle);
throw new Error('Not implemented, yet');
return windowHandles.map(windowHandle => new ProtractorPage(this.browser, windowHandle));
}

/**
Expand Down
113 changes: 88 additions & 25 deletions packages/protractor/src/screenplay/models/ProtractorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,104 @@ export class ProtractorPage extends Page {
}

title(): Promise<string> {
return promiseOf(this.browser.getTitle());
return this.switchToAndPerform(async browser => {
return promiseOf(browser.getTitle());
});
}

async url(): Promise<URL> {
return new URL(await promiseOf(this.browser.getCurrentUrl()));
name(): Promise<string> {
return this.switchToAndPerform(async browser => {
return promiseOf(browser.executeScript('return window.name'));
});
}

url(): Promise<URL> {
return this.switchToAndPerform(async browser => {
return new URL(await promiseOf(browser.getCurrentUrl()));
});
}

async viewportSize(): Promise<{ width: number, height: number }> {
return this.switchToAndPerform(async browser => {
const calculatedViewportSize = await promiseOf(browser.executeScript(
`return {
width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
};`
)) as { width: number, height: number };

const calculatedViewportSize = await promiseOf(this.browser.executeScript(
`return {
width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
};`
)) as { width: number, height: number };
if (calculatedViewportSize.width > 0 && calculatedViewportSize.height > 0) {
return calculatedViewportSize;
}

// Chrome headless hard-codes window.innerWidth and window.innerHeight to 0
return promiseOf(browser.manage().window().getSize());
});
}

if (calculatedViewportSize.width > 0 && calculatedViewportSize.height > 0) {
return calculatedViewportSize;
async setViewportSize(size: { width: number, height: number }): Promise<void> {
return this.switchToAndPerform(async browser => {
const desiredWindowSize: { width: number, height: number } = await promiseOf(browser.executeScript(`
var currentViewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
var currentViewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
return {
width: Math.max(window.outerWidth - currentViewportWidth + ${ size.width }, ${ size.width }),
height: Math.max(window.outerHeight - currentViewportHeight + ${ size.height }, ${ size.height }),
};
`));

return promiseOf(browser.manage().window().setSize(desiredWindowSize.width, desiredWindowSize.height));
});
}

async close(): Promise<void> {
return this.switchToAndPerform(browser => promiseOf(browser.close()));
}

async closeOthers(): Promise<void> {
const windowHandles = await this.browser.getAllWindowHandles();

for (const handle of windowHandles) {
if (handle !== this.handle) {
await this.browser.switchTo().window(handle);
await this.browser.close();
}
}

// Chrome headless hard-codes window.innerWidth and window.innerHeight to 0
return promiseOf(this.browser.manage().window().getSize());
await this.browser.switchTo().window(this.handle);
}

async setViewportSize(size: { width: number, height: number }): Promise<void> {
const desiredWindowSize: { width: number, height: number } = await promiseOf(this.browser.executeScript(`
var currentViewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
var currentViewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
return {
width: Math.max(window.outerWidth - currentViewportWidth + ${ size.width }, ${ size.width }),
height: Math.max(window.outerHeight - currentViewportHeight + ${ size.height }, ${ size.height }),
};
`));

return promiseOf(this.browser.manage().window().setSize(desiredWindowSize.width, desiredWindowSize.height));
async isPresent(): Promise<boolean> {
const currentPageHandle = await this.browser.getWindowHandle();
const desiredPageHandle = this.handle;

const isOpen = await this.browser.switchTo().window(desiredPageHandle).then(() => true, _error => false);

await this.browser.switchTo().window(currentPageHandle);

return isOpen;
}

async switchTo(): Promise<void> {
await this.browser.switchTo().window(this.handle);
}

private async switchToAndPerform<T>(action: (browser: ProtractorBrowser) => Promise<T> | T): Promise<T> {
const originalPageHandle = await this.browser.getWindowHandle();
const desiredPageHandle = this.handle;
const shouldSwitch = originalPageHandle !== desiredPageHandle;

if (shouldSwitch) {
await this.browser.switchTo().window(desiredPageHandle);
}

const result = await action(this.browser);

if (shouldSwitch) {
await this.browser.switchTo().window(originalPageHandle);
}

return result;
}
}
25 changes: 20 additions & 5 deletions packages/web/src/screenplay/abilities/BrowseTheWeb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,38 @@ export abstract class BrowseTheWeb implements Ability {

abstract takeScreenshot(): Promise<string>;

/**
* @desc
* Returns a {@link Page} representing the currently active top-level browsing context.
*
* @returns {Promise<Page>}
*/
abstract currentPage(): Promise<Page>;
abstract pageCalled(nameOrHandleOrIndex: string | number): Promise<Page>;

/**
* @desc
* Returns an array of {@link Page} objects representing all the available
* top-level browsing context, e.g. all the open browser tabs.
*
* @returns {Promise<Array<Page>>}
*/
abstract allPages(): Promise<Array<Page>>;

abstract cookie(name: string): Promise<Cookie>;
abstract deleteAllCookies(): Promise<void>;

abstract modalDialog(): Promise<ModalDialog>;

// todo: remove
abstract switchToFrame(targetOrIndex: PageElement | number | string): Promise<void>;
abstract switchToParentFrame(): Promise<void>;
abstract switchToDefaultContent(): Promise<void>;
abstract switchToWindow(nameOrHandleOrIndex: string | number): Promise<void>;
abstract switchToOriginalWindow(): Promise<void>;
abstract getCurrentWindowHandle(): Promise<string>;
abstract getAllWindowHandles(): Promise<string[]>;

abstract closeCurrentWindow(): Promise<void>;

// todo: remove
abstract switchToFrame(targetOrIndex: PageElement | number | string): Promise<void>;
abstract switchToParentFrame(): Promise<void>;
abstract switchToDefaultContent(): Promise<void>;
}

117 changes: 106 additions & 11 deletions packages/web/src/screenplay/models/Page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Adapter, Answerable, Question } from '@serenity-js/core';
import { Adapter, Expectation, ExpectationMet, ExpectationOutcome, LogicError, Question } from '@serenity-js/core';
import { URL } from 'url';

import { BrowseTheWeb } from '../abilities';
Expand All @@ -10,15 +10,61 @@ export abstract class Page {
});
}

static called(windowNameOrHandle: Answerable<string>): Question<Promise<Page>> & Adapter<Page> {
return Question.about<Promise<Page>>(`page called "${ windowNameOrHandle }"`, async actor => {
const nameOrHandle = await actor.answer(windowNameOrHandle);
return BrowseTheWeb.as(actor).pageCalled(nameOrHandle)
static whichName(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
return Question.about(`page which name does ${ expectation }`, async actor => {
const pages = await BrowseTheWeb.as(actor).allPages();
const matcher = await actor.answer(expectation);

return Page.findMatchingPage(
`name does ${ expectation }`,
pages,
page => page.name().then(matcher)
);
});
}

static whichTitle(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
return Question.about(`page which title does ${ expectation }`, async actor => {
const pages = await BrowseTheWeb.as(actor).allPages();
const matcher = await actor.answer(expectation);

return Page.findMatchingPage(
`title does ${ expectation }`,
pages,
page => page.title().then(title => {
return matcher(title);
})
);
});
}

static whichUrl(expectation: Expectation<any, string>): Question<Promise<Page>> & Adapter<Page> {
return Question.about(`page which URL does ${ expectation }`, async actor => {
const pages = await BrowseTheWeb.as(actor).allPages();
const matcher = await actor.answer(expectation);

return Page.findMatchingPage(
`url does ${ expectation }`,
pages,
page => page.url().then(url => matcher(url.toString()))
);
});
}

private static async findMatchingPage(expectationDescription: string, pages: Page[], matcher: (page: Page) => Promise<ExpectationOutcome<any, any>>): Promise<Page> {
for (const page of pages) {
const outcome = await matcher(page);

if (outcome instanceof ExpectationMet) {
return page;
}
}

throw new LogicError(`Couldn't find a page which ${ expectationDescription }`);
}

constructor(
public readonly handle: string,
protected readonly handle: string,
) {
}

Expand All @@ -38,13 +84,62 @@ export abstract class Page {
*/
abstract url(): Promise<URL>;

/**
* @desc
* Retrieves the name of the current top-level browsing context.
*
* @returns {Promise<string>}
*/
abstract name(): Promise<string>;

/**
* @desc
* Checks if a given window / tab / page is open and can be switched to.
*
* @returns {Promise<string>}
*/
abstract isPresent(): Promise<boolean>;

/**
* @desc
* Returns the actual viewport size available for the given page,
* excluding any scrollbars.
*
* @returns {Promise<{ width: number, height: number }>}
*/
abstract viewportSize(): Promise<{ width: number, height: number }>;

/**
*
* @param size
*/
abstract setViewportSize(size: { width: number, height: number }): Promise<void>;

// close(): Promise<void>;
/**
* @desc
* Switches the current top-level browsing context to the given page
*
* @returns {Promise<void>}
*/
abstract switchTo(): Promise<void>;

/**
* @desc
* Closes the given page.
*
* @returns {Promise<void>}
*/
abstract close(): Promise<void>;

/**
* @desc
* Closes any open pages, except for this one.
*
* @returns {Promise<void>}
*/
abstract closeOthers(): Promise<void>;

// toString() {
//
// return `page handle ${ this.handle }`;
// }
toString(): string {
return `page (handle=${ this.handle })`;
}
}
Loading

0 comments on commit a73a635

Please sign in to comment.