diff --git a/@types/compositer/index.d.ts b/@types/compositer/index.d.ts index 89f0aefc..eb7083d3 100644 --- a/@types/compositer/index.d.ts +++ b/@types/compositer/index.d.ts @@ -1 +1,10 @@ -declare module 'compositer'; +type Class = new (...arguments: any[]) => T; + +type ComponentFunction = (...args: Array) => any; + +type Component = { [s: string]: any } | ComponentFunction; + +declare module 'compositer' { + function compose(baseClass: Class, components: Component, ...childParams: Array): Class; + export = compose; +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 939997d2..c8fb0b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +2.5.0 / 2019-07-24 +================== + +* Pages, selectPage and closePage methods to handle tabs and popups +* Browser.context returns Puppeteer's context object +* Improved typings for module compositer +* A browser won't have 2 tabs opened by default +* Puppeteer updated to 1.19.0 +* Minor improvements in internal typings +* Minor fix in InjectionScript error message +* Refactor on browser instances creation and setup + 2.4.0 / 2019-07-15 ================== diff --git a/README.md b/README.md index 43bae5cd..ee2bca10 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,9 @@ True if the browser is configured as incognito page. **cache** If the requests cache is active. +**context** +Returns Puppeteer's [BrowserContext](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-browsercontext). + #### Methods All the methods in Browser return a Promise than can easily be handled by using `async/await`. @@ -286,6 +289,26 @@ const element = await browser.elementFromPoint(500, 150); await browser.text(element); // ["My Title"] ``` +**selectPage(index: number)** +Selects the given page (a.k.a. tab) to be used by Wendigo. Keep in mind that tabs are **never** changed automatically unless explicitly selected or closed with `closePage`. + +```js +await browser.click(".btn.new-tab") +await browser.wait(100); // waits for new tab to be loaded +await browser.pages(); // length is 2 +await browser.selectPage(1); // goes to newly opened tab +``` + +> Due to some limitation, in order for Wendigo to work properly, changing page with `selectPage` will cause the given page to reload. + +**closePage(index: number)** +Closes the page with given index, if the closed page is the current active page, it will change to the new page with index 0 (reloading it in the process). If no more pages exists, the browser will close with `browser.close()` automatically. + +**pages()** +Returns all Puppeteer's [pages](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page), one per tab or popup. + +> **Warning:** All pages related methods are still under revision, and its behavour may heavily change in future releases + **addScript(scriptPath)** Executes the given script in the browser context. Useful to set helper methods and functions. This method must be called after the page is already loaded, if another page is loaded, the scripts won't be re-executed. If these scripts are required for a plugin to work, remember to execute this method on the `_afterOpen` hook. diff --git a/lib/browser/assertions/assertions_core.ts b/lib/browser/assertions/assertions_core.ts index 1570c2ac..988c2512 100644 --- a/lib/browser/assertions/assertions_core.ts +++ b/lib/browser/assertions/assertions_core.ts @@ -1,6 +1,6 @@ -import * as utils from '../../utils/utils'; +import { arrayfy, matchTextList, matchText, stringify, matchTextContainingList } from '../../utils/utils'; +import { sameMembers } from '../../utils/assert_utils'; import * as elementsAssertionUtils from './assert_elements'; -import * as assertUtils from '../../utils/assert_utils'; import { QueryError, FatalError, WendigoError, AssertionError } from '../../errors'; import { WendigoSelector } from '../../types'; @@ -67,10 +67,10 @@ export default class AssertionsCore { if ((!expected && expected !== "") || (Array.isArray(expected) && expected.length === 0)) { throw new WendigoError("assert.text", `Missing expected text for assertion.`); } - const processedExpected = utils.arrayfy(expected); + const processedExpected = arrayfy(expected); const texts = await this._browser.text(selector); for (const expectedText of processedExpected) { - if (!utils.matchTextList(texts, expectedText)) { + if (!matchTextList(texts, expectedText)) { if (!msg) { const foundText = texts.length === 0 ? "no text" : `"${texts.join(" ")}"`; msg = `Expected element "${selector}" to have text "${expectedText}", ${foundText} found.`; @@ -85,11 +85,11 @@ export default class AssertionsCore { throw new WendigoError("assert.textContains", `Missing expected text for assertion.`); } - const processedExpected = utils.arrayfy(expected); + const processedExpected = arrayfy(expected); const texts = await this._browser.text(selector); for (const expectedText of processedExpected) { - if (!utils.matchTextContainingList(texts, expectedText)) { + if (!matchTextContainingList(texts, expectedText)) { if (!msg) { const foundText = texts.length === 0 ? "no text" : `"${texts.join(" ")}"`; msg = `Expected element "${selector}" to contain text "${expectedText}", ${foundText} found.`; @@ -102,7 +102,7 @@ export default class AssertionsCore { public async title(expected: string | RegExp, msg?: string): Promise { const title = await this._browser.title(); const foundTitle = title ? `"${title}"` : "no title"; - if (!utils.matchText(title, expected)) { + if (!matchText(title, expected)) { if (!msg) msg = `Expected page title to be "${expected}", ${foundTitle} found.`; throw new AssertionError("assert.title", msg); } @@ -131,8 +131,8 @@ export default class AssertionsCore { } catch (err) { throw new FatalError("assert.url", `Can't obtain page url.${err.extraMessage || err.message}`); } - if (!utils.matchText(url, expected)) { - if (!msg) msg = `Expected url to be "${utils.stringify(expected)}", "${url}" found`; + if (!matchText(url, expected)) { + if (!msg) msg = `Expected url to be "${stringify(expected)}", "${url}" found`; throw new AssertionError("assert.url", msg, url, expected); } } @@ -190,7 +190,7 @@ export default class AssertionsCore { if (filteredAttributes.length === 0) return Promise.resolve(); } else { for (const attr of filteredAttributes) { - if (expectedValue === undefined || utils.matchText(attr, expectedValue)) { + if (expectedValue === undefined || matchText(attr, expectedValue)) { return Promise.resolve(); } } @@ -245,7 +245,7 @@ export default class AssertionsCore { return Promise.reject(error); } for (const html of found) { - if (utils.matchText(html, expected)) return Promise.resolve(); + if (matchText(html, expected)) return Promise.resolve(); } if (!msg) { @@ -264,7 +264,7 @@ export default class AssertionsCore { return Promise.reject(error); } for (const html of found) { - if (utils.matchText(html, expected)) return Promise.resolve(); + if (matchText(html, expected)) return Promise.resolve(); } if (!msg) { @@ -275,10 +275,10 @@ export default class AssertionsCore { } public async options(selector: WendigoSelector, expected: string | Array, msg?: string): Promise { - const parsedExpected = utils.arrayfy(expected); + const parsedExpected = arrayfy(expected); const options = await this._browser.options(selector); - const sameMembers = assertUtils.sameMembers(parsedExpected, options); - if (!sameMembers) { + const same = sameMembers(parsedExpected, options); + if (!same) { if (!msg) { const expectedText = parsedExpected.join(", "); const optionsText = options.join(", "); @@ -289,10 +289,10 @@ export default class AssertionsCore { } public async selectedOptions(selector: WendigoSelector, expected: string | Array, msg?: string): Promise { - const parsedExpected = utils.arrayfy(expected); + const parsedExpected = arrayfy(expected); const selectedOptions = await this._browser.selectedOptions(selector); - const sameMembers = assertUtils.sameMembers(parsedExpected, selectedOptions); - if (!sameMembers) { + const same = sameMembers(parsedExpected, selectedOptions); + if (!same) { if (!msg) { const expectedText = parsedExpected.join(", "); const optionsText = selectedOptions.join(", "); diff --git a/lib/browser/assertions/browser_not_assertions.ts b/lib/browser/assertions/browser_not_assertions.ts index 6cc28885..ce12261f 100644 --- a/lib/browser/assertions/browser_not_assertions.ts +++ b/lib/browser/assertions/browser_not_assertions.ts @@ -1,4 +1,4 @@ -import * as utils from '../../utils/utils'; +import { arrayfy, matchTextList, matchTextContainingList } from '../../utils/utils'; import { invertify } from '../../utils/assert_utils'; import { WendigoError, QueryError, AssertionError } from '../../errors'; import BrowserAssertions from '../browser_assertions'; @@ -40,11 +40,11 @@ export default class BrowserNotAssertions { if ((!expected && expected !== "") || (Array.isArray(expected) && expected.length === 0)) { throw new WendigoError("assert.not.text", `Missing expected text for assertion.`); } - const processedExpected = utils.arrayfy(expected); + const processedExpected = arrayfy(expected); const texts = await this._browser.text(selector); for (const expectedText of processedExpected) { - if (utils.matchTextList(texts, expectedText)) { + if (matchTextList(texts, expectedText)) { if (!msg) msg = `Expected element "${selector}" not to have text "${expectedText}".`; throw new AssertionError("assert.not.text", msg); } @@ -56,11 +56,11 @@ export default class BrowserNotAssertions { throw new WendigoError("assert.not.textContains", `Missing expected text for assertion.`); } - const processedExpected = utils.arrayfy(expected); + const processedExpected = arrayfy(expected); const texts = await this._browser.text(selector); for (const expectedText of processedExpected) { - if (utils.matchTextContainingList(texts, expectedText)) { + if (matchTextContainingList(texts, expectedText)) { if (!msg) { msg = `Expected element "${selector}" to not contain text "${expectedText}".`; } @@ -144,7 +144,7 @@ export default class BrowserNotAssertions { public selectedOptions(selector: WendigoSelector, expected: string | Array, msg?: string): Promise { if (!msg) { - const parsedExpected = utils.arrayfy(expected); + const parsedExpected = arrayfy(expected); const expectedText = parsedExpected.join(", "); msg = `Expected element "${selector}" not to have options "${expectedText}" selected.`; } diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 39072aef..3d0e8b00 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -1,5 +1,5 @@ +import PuppeteerContext from '../puppeteer_wrapper/puppeteer_context'; import BrowserTap from './mixins/browser_tap'; -import PuppeteerPage from './puppeteer_wrapper/puppeteer_page'; import { FinalBrowserSettings } from '../types'; // Modules @@ -10,6 +10,7 @@ import BrowserConsole from '../modules/console/browser_console'; import BrowserWebworker from '../modules/webworkers/browser_webworker'; import BrowserDialog from '../modules/dialog/browser_dialog'; import BrowserAuth from '../modules/auth/browser_auth'; +import PuppeteerPage from '../puppeteer_wrapper/puppeteer_page'; export default class Browser extends BrowserTap { public readonly cookies: BrowserCookies; @@ -20,9 +21,9 @@ export default class Browser extends BrowserTap { public readonly dialog: BrowserDialog; public readonly auth: BrowserAuth; - constructor(page: PuppeteerPage, settings: FinalBrowserSettings, components: Array = []) { + constructor(context: PuppeteerContext, page: PuppeteerPage, settings: FinalBrowserSettings, components: Array = []) { components = components.concat(["cookies", "localStorage", "requests", "console", "webworkers", "dialog"]); - super(page, settings, components); + super(context, page, settings, components); this.cookies = new BrowserCookies(this); this.localStorage = new BrowserLocalStorage(this); this.requests = new BrowserRequests(this); diff --git a/lib/browser/browser_core.ts b/lib/browser/browser_core.ts index a0eda051..87e648ca 100644 --- a/lib/browser/browser_core.ts +++ b/lib/browser/browser_core.ts @@ -1,14 +1,16 @@ import path from 'path'; import querystring from 'querystring'; -import { stringifyLogText } from './puppeteer_wrapper/puppeteer_utils'; +import { stringifyLogText } from '../puppeteer_wrapper/puppeteer_utils'; import WendigoConfig from '../../config'; import DomElement from '../models/dom_element'; import { FatalError, InjectScriptError } from '../errors'; import { FinalBrowserSettings, OpenSettings } from '../types'; -import PuppeteerPage from './puppeteer_wrapper/puppeteer_page'; -import { ViewportOptions, ConsoleMessage, Page, Response, Frame } from './puppeteer_wrapper/puppeteer_types'; +import PuppeteerPage from '../puppeteer_wrapper/puppeteer_page'; +import { ViewportOptions, ConsoleMessage, Page, Response, Frame, BrowserContext, Target } from '../puppeteer_wrapper/puppeteer_types'; import FailIfNotLoaded from '../decorators/fail_if_not_loaded'; +import PuppeteerContext from '../puppeteer_wrapper/puppeteer_context'; +import OverrideError from '../decorators/override_error'; const injectionScriptsPath = WendigoConfig.injectionScripts.path; const injectionScripts = WendigoConfig.injectionScripts.files; @@ -36,26 +38,43 @@ export default abstract class BrowserCore { public initialResponse: Response | null; protected _page: PuppeteerPage; + protected _context: PuppeteerContext; protected originalHtml?: string; protected settings: FinalBrowserSettings; private _loaded: boolean; - private disabled: boolean; - private components: Array; - private cache: boolean; + private _disabled: boolean; + private _components: Array; + private _cache: boolean; + private _openSettings: OpenSettings = defaultOpenOptions; - constructor(page: PuppeteerPage, settings: FinalBrowserSettings, components: Array = []) { + constructor(context: PuppeteerContext, page: PuppeteerPage, settings: FinalBrowserSettings, components: Array = []) { this._page = page; + this._context = context; this.settings = settings; this._loaded = false; this.initialResponse = null; - this.disabled = false; - this.cache = settings.cache !== undefined ? settings.cache : true; - this.components = components; + this._disabled = false; + this._cache = settings.cache !== undefined ? settings.cache : true; + this._components = components; if (this.settings.log) { this._page.on("console", pageLog); } + this._context.on('targetcreated', async (target: Target): Promise => { + const createdPage = await target.page(); + if (createdPage) { + const puppeteerPage = new PuppeteerPage(createdPage); + try { + await puppeteerPage.setBypassCSP(true); + if (this.settings.userAgent) + await puppeteerPage.setUserAgent(this.settings.userAgent); + } catch (err) { + // Will fail if browser is closed before finishing + } + } + }); + this._page.on('load', async (): Promise => { if (this._loaded) { try { @@ -70,8 +89,13 @@ export default abstract class BrowserCore { public get page(): Page { return this._page.page; } + + public get context(): BrowserContext { + return this._context.context; + } + public get loaded(): boolean { - return this._loaded && !this.disabled; + return this._loaded && !this._disabled; } public get incognito(): boolean { @@ -79,20 +103,21 @@ export default abstract class BrowserCore { } public get cacheEnabled(): boolean { - return this.cache; + return this._cache; } + @OverrideError() public async open(url: string, options?: OpenSettings): Promise { this._loaded = false; - options = Object.assign({}, defaultOpenOptions, options); + this._openSettings = Object.assign({}, defaultOpenOptions, options); url = this._processUrl(url); - await this.setCache(this.cache); - if (options.queryString) { - const qs = this._generateQueryString(options.queryString); + await this.setCache(this._cache); + if (this._openSettings.queryString) { + const qs = this._generateQueryString(this._openSettings.queryString); url = `${url}${qs}`; } try { - await this._beforeOpen(options); + await this._beforeOpen(this._openSettings); const response = await this._page.goto(url); this.initialResponse = response; return this._afterPageLoad(); @@ -102,6 +127,7 @@ export default abstract class BrowserCore { } } + @OverrideError() public async openFile(filepath: string, options: OpenSettings): Promise { const absolutePath = path.resolve(filepath); try { @@ -112,9 +138,9 @@ export default abstract class BrowserCore { } public async close(): Promise { - if (this.disabled) return Promise.resolve(); - const p = this._beforeClose(); - this.disabled = true; + if (this._disabled) return Promise.resolve(); + const p = this._beforeClose(); // Minor race condition with this._loaded if moved + this._disabled = true; this._loaded = false; this.initialResponse = null; this.originalHtml = undefined; @@ -136,6 +162,32 @@ export default abstract class BrowserCore { } else return rawResult.jsonValue(); } + public async pages(): Promise> { + return this._context.pages(); + } + + @OverrideError() + public async selectPage(index: number): Promise { + const page = await this._context.getPage(index); + if (!page) throw new FatalError("selectPage", `Invalid page index "${index}".`); + this._page = page; + // TODO: Avoid reload + await this.page.reload(); // Required to enable bypassCSP + await this._beforeOpen(this._openSettings); + await this._afterPageLoad(); + } + + public async closePage(index: number): Promise { + const page = await this._context.getPage(index); + if (!page) throw new FatalError("closePage", `Invalid page index "${index}".`); + await page.close(); + try { + await this.selectPage(0); + } catch (err) { + this.close(); + } + } + public setViewport(config: ViewportOptions = {}): Promise { return this._page.setViewport(config); } @@ -165,13 +217,13 @@ export default abstract class BrowserCore { path: scriptPath }); } catch (err) { - return Promise.reject(new InjectScriptError("open", err)); + return Promise.reject(new InjectScriptError("addScript", err)); } } public async setCache(value: boolean): Promise { await this._page.setCache(value); - this.cache = value; + this._cache = value; } protected async _beforeClose(): Promise { @@ -184,7 +236,6 @@ export default abstract class BrowserCore { if (this.settings.userAgent) { await this._page.setUserAgent(this.settings.userAgent); } - if (this.settings.bypassCSP) { await this._page.setBypassCSP(true); } @@ -198,7 +249,10 @@ export default abstract class BrowserCore { this.originalHtml = content; await this._addJsScripts(); } catch (err) { - if (err.message === "Evaluation failed: Event") throw new InjectScriptError("open", err.message); // CSP error + if (err.message === "Evaluation failed: Event") { + const cspWarning = "This may be caused by the page Content Security Policy. Make sure the option bypassCSP is set to true in Wendigo."; + throw new InjectScriptError("_afterPageLoad", `Error injecting scripts. ${cspWarning}`); // CSP error + } } this._loaded = true; await this._callComponentsMethod("_afterOpen"); @@ -221,7 +275,7 @@ export default abstract class BrowserCore { } private async _callComponentsMethod(method: string, options?: any): Promise { - await Promise.all(this.components.map((c) => { + await Promise.all(this._components.map((c) => { const anyThis = this as any; if (typeof anyThis[c][method] === 'function') return anyThis[c][method](options); diff --git a/lib/browser/mixins/browser_actions.ts b/lib/browser/mixins/browser_actions.ts index 9eac5fc6..f5dc1d1f 100644 --- a/lib/browser/mixins/browser_actions.ts +++ b/lib/browser/mixins/browser_actions.ts @@ -6,7 +6,7 @@ import { QueryError, WendigoError } from '../../errors'; import { WendigoSelector } from '../../types'; import DOMELement from '../../models/dom_element'; import FailIfNotLoaded from '../../decorators/fail_if_not_loaded'; -import { Base64ScreenShotOptions } from '../puppeteer_wrapper/puppeteer_types'; +import { Base64ScreenShotOptions } from '../../puppeteer_wrapper/puppeteer_types'; // Mixin with user actions export default abstract class BrowserActions extends BrowserQueries { diff --git a/lib/browser/mixins/browser_click.ts b/lib/browser/mixins/browser_click.ts index 7003a85a..13db065c 100644 --- a/lib/browser/mixins/browser_click.ts +++ b/lib/browser/mixins/browser_click.ts @@ -20,7 +20,7 @@ export default abstract class BrowserClick extends BrowserActions { else elements = await this.queryAll(selector); const indexErrorMsg = `Invalid index "${index}" for selector "${selector}", ${elements.length} elements found.`; const notFoundMsg = `No element "${selector}" found.`; - return this.clickElements(elements, index, new WendigoError("click", indexErrorMsg), new QueryError("click", notFoundMsg)); + return this._clickElements(elements, index, new WendigoError("click", indexErrorMsg), new QueryError("click", notFoundMsg)); } } @@ -35,7 +35,7 @@ export default abstract class BrowserClick extends BrowserActions { elements = await this.findByText(text, optionalText); const indexErrorMsg = `Invalid index "${index}" for text "${optionalText || text}", ${elements.length} elements found.`; const notFoundMsg = `No element with text "${optionalText || text}" found.`; - return this.clickElements(elements, index, new WendigoError("clickText", indexErrorMsg), new QueryError("clickText", notFoundMsg)); + return this._clickElements(elements, index, new WendigoError("clickText", indexErrorMsg), new QueryError("clickText", notFoundMsg)); } @FailIfNotLoaded @@ -49,16 +49,16 @@ export default abstract class BrowserClick extends BrowserActions { elements = await this.findByTextContaining(text, optionalText); const indexErrorMsg = `Invalid index "${index}" for text containing "${optionalText || text}", ${elements.length} elements found.`; const notFoundMsg = `No element with text containing "${optionalText || text}" found.`; - return this.clickElements(elements, index, new WendigoError("clickTextContaining", indexErrorMsg), new QueryError("clickTextContaining", notFoundMsg)); + return this._clickElements(elements, index, new WendigoError("clickTextContaining", indexErrorMsg), new QueryError("clickTextContaining", notFoundMsg)); } @FailIfNotLoaded - private clickElements(elements: Array, index: number | undefined, indexError: Error, notFoundError: Error): Promise { + private _clickElements(elements: Array, index: number | undefined, indexError: Error, notFoundError: Error): Promise { if (index !== undefined) { return this._validateAndClickElementByIndex(elements, index, indexError); } else { - return this._validateAndClickElements(elements, notFoundError); + return this._validateAnd_clickElements(elements, notFoundError); } } @@ -70,7 +70,7 @@ export default abstract class BrowserClick extends BrowserActions { return 1; } - private async _validateAndClickElements(elements: Array, error: Error): Promise { + private async _validateAnd_clickElements(elements: Array, error: Error): Promise { if (elements.length <= 0 || !elements[0]) { throw error; } diff --git a/lib/browser/mixins/browser_info.ts b/lib/browser/mixins/browser_info.ts index 6ed1cc18..1c16730d 100644 --- a/lib/browser/mixins/browser_info.ts +++ b/lib/browser/mixins/browser_info.ts @@ -3,7 +3,7 @@ import BrowserClick from './browser_click'; import { QueryError } from '../../errors'; import { WendigoSelector } from '../../types'; import FailIfNotLoaded from '../../decorators/fail_if_not_loaded'; -import { PDFOptions } from '../puppeteer_wrapper/puppeteer_types'; +import { PDFOptions } from '../../puppeteer_wrapper/puppeteer_types'; export default abstract class BrowserInfo extends BrowserClick { diff --git a/lib/browser/mixins/browser_queries.ts b/lib/browser/mixins/browser_queries.ts index 43f98eac..9759daa9 100644 --- a/lib/browser/mixins/browser_queries.ts +++ b/lib/browser/mixins/browser_queries.ts @@ -6,7 +6,7 @@ import { WendigoSelector } from '../../types'; import { isXPathQuery, createFindTextXPath } from '../../utils/utils'; import FailIfNotLoaded from '../../decorators/fail_if_not_loaded'; import OverrideError from '../../decorators/override_error'; -import { ElementHandle } from '../puppeteer_wrapper/puppeteer_types'; +import { ElementHandle } from '../../puppeteer_wrapper/puppeteer_types'; export default abstract class BrowserQueries extends BrowserCore { diff --git a/lib/browser/mixins/browser_tap.ts b/lib/browser/mixins/browser_tap.ts index 406ec2f2..050bfe70 100644 --- a/lib/browser/mixins/browser_tap.ts +++ b/lib/browser/mixins/browser_tap.ts @@ -10,25 +10,25 @@ export default abstract class BrowserTap extends BrowserWait { public async tap(selector: WendigoSelector | number, index?: number): Promise { if (typeof selector === 'number') { if (typeof index !== 'number') throw new WendigoError("tap", "Invalid coordinates"); - return this.tapCoordinates(selector, index); + return this._tapCoordinates(selector, index); } return this.queryAll(selector).then((elements) => { const indexErrorMsg = `invalid index "${index}" for selector "${selector}", ${elements.length} elements found.`; const notFoundMsg = `No element "${selector}" found.`; - return this.tapElements(elements, index, new WendigoError("tap", indexErrorMsg), new QueryError("tap", notFoundMsg)); + return this._tapElements(elements, index, new WendigoError("tap", indexErrorMsg), new QueryError("tap", notFoundMsg)); }); } - private tapElements(elements: Array, index: number | undefined, indexError: Error, notFoundError: Error): Promise { + private _tapElements(elements: Array, index: number | undefined, indexError: Error, notFoundError: Error): Promise { if (index !== undefined) { - return this.validateAndTapElementByIndex(elements, index, indexError); + return this._validateAndTapElementByIndex(elements, index, indexError); } else { - return this.validateAndTapElements(elements, notFoundError); + return this._validateAndTapElements(elements, notFoundError); } } - private async validateAndTapElementByIndex(elements: Array, index: number, error: Error): Promise { + private async _validateAndTapElementByIndex(elements: Array, index: number, error: Error): Promise { if (index > elements.length || index < 0 || !elements[index]) { throw error; } @@ -36,7 +36,7 @@ export default abstract class BrowserTap extends BrowserWait { return 1; } - private async validateAndTapElements(elements: Array, error: Error): Promise { + private async _validateAndTapElements(elements: Array, error: Error): Promise { if (elements.length <= 0 || !elements[0]) { return Promise.reject(error); } @@ -46,7 +46,7 @@ export default abstract class BrowserTap extends BrowserWait { return elements.length; } - private async tapCoordinates(x: number, y: number): Promise { + private async _tapCoordinates(x: number, y: number): Promise { await this._page.touchscreen.tap(x, y); return 1; } diff --git a/lib/browser/mixins/browser_wait.ts b/lib/browser/mixins/browser_wait.ts index 364548ab..1f7c496b 100644 --- a/lib/browser/mixins/browser_wait.ts +++ b/lib/browser/mixins/browser_wait.ts @@ -5,7 +5,7 @@ import { WendigoSelector } from '../../types'; import { createFindTextXPath, delay } from '../../utils/utils'; import FailIfNotLoaded from '../../decorators/fail_if_not_loaded'; import OverrideError from '../../decorators/override_error'; -import { EvaluateFn } from '../puppeteer_wrapper/puppeteer_types'; +import { EvaluateFn } from '../../puppeteer_wrapper/puppeteer_types'; export default abstract class BrowserWait extends BrowserNavigation { diff --git a/lib/browser_factory.ts b/lib/browser_factory.ts index 4e4c34f1..f9251b0b 100644 --- a/lib/browser_factory.ts +++ b/lib/browser_factory.ts @@ -6,27 +6,26 @@ import BrowserAssertion from './browser/browser_assertions'; import { FinalBrowserSettings, PluginModule } from './types'; import { FatalError } from './errors'; import BrowserInterface from './browser/browser_interface'; -import PuppeteerPage from './browser/puppeteer_wrapper/puppeteer_page'; -import { Page } from './browser/puppeteer_wrapper/puppeteer_types'; +import PuppeteerContext from './puppeteer_wrapper/puppeteer_context'; export default class BrowserFactory { - private static browserClass?: typeof Browser; + private static _browserClass?: typeof Browser; - public static createBrowser(page: Page, settings: FinalBrowserSettings, plugins: Array): BrowserInterface { - if (!this.browserClass) { - this.setupBrowserClass(plugins); + public static async createBrowser(context: PuppeteerContext, settings: FinalBrowserSettings, plugins: Array): Promise { + if (!this._browserClass) { + this._setupBrowserClass(plugins); } - if (!this.browserClass) throw new FatalError("BrowserFactory", "Error on setupBrowserClass"); + if (!this._browserClass) throw new FatalError("BrowserFactory", "Error on setupBrowserClass"); - const puppeteerPage = new PuppeteerPage(page); - return new this.browserClass(puppeteerPage, settings) as BrowserInterface; + const page = await context.getDefaultPage(); + return new this._browserClass(context, page, settings) as BrowserInterface; } public static clearCache(): void { - this.browserClass = undefined; + this._browserClass = undefined; } - private static setupBrowserClass(plugins: Array): void { + private static _setupBrowserClass(plugins: Array): void { const components: { [s: string]: any } = {}; const assertComponents: { [s: string]: any } = {}; @@ -41,7 +40,7 @@ export default class BrowserFactory { const assertionClass = compose(BrowserAssertion, assertComponents); const finalComponents = Object.assign({}, components, { assert: assertionClass }); - this.browserClass = compose(Browser, finalComponents) as typeof Browser; + this._browserClass = compose(Browser, finalComponents) as typeof Browser; } private static _setupAssertionModule(assertionPlugin: any, name: string): any { diff --git a/lib/errors.ts b/lib/errors.ts index 3ad74d04..e6b5df24 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -70,7 +70,6 @@ export class TimeoutError extends WendigoError { export class InjectScriptError extends FatalError { constructor(fn: string, message: string) { - message = `${message}. This may be caused by the page Content Security Policy. Make sure the option bypassCSP is set to true in Wendigo.`; super(fn, message); this.name = this.constructor.name; } diff --git a/lib/models/dom_element.ts b/lib/models/dom_element.ts index a9e0a9dc..0dc65920 100644 --- a/lib/models/dom_element.ts +++ b/lib/models/dom_element.ts @@ -1,4 +1,4 @@ -import { JSHandle, ElementHandle } from '../browser/puppeteer_wrapper/puppeteer_types'; +import { JSHandle, ElementHandle } from '../puppeteer_wrapper/puppeteer_types'; import { isXPathQuery } from '../utils/utils'; export default class DomElement { @@ -13,7 +13,7 @@ export default class DomElement { public async query(selector: string): Promise { let elementHandle: ElementHandle | null; if (isXPathQuery(selector)) { - selector = this.processXPath(selector); + selector = this._processXPath(selector); const results = await this.element.$x(selector); elementHandle = results[0] || null; } else elementHandle = await this.element.$(selector); @@ -23,7 +23,7 @@ export default class DomElement { public async queryAll(selector: string): Promise> { let elements: Array; if (isXPathQuery(selector)) { - selector = this.processXPath(selector); + selector = this._processXPath(selector); elements = await this.element.$x(selector); } else elements = await this.element.$$(selector); @@ -64,7 +64,7 @@ export default class DomElement { else return null; } - private processXPath(selector: string): string { + private _processXPath(selector: string): string { if (selector[0] === '/') selector = `.${selector}`; return selector; } diff --git a/lib/modules/auth/browser_auth.ts b/lib/modules/auth/browser_auth.ts index 0195ae48..d3109633 100644 --- a/lib/modules/auth/browser_auth.ts +++ b/lib/modules/auth/browser_auth.ts @@ -8,21 +8,21 @@ export default class BrowserAuth extends WendigoModule { await this.clear(); if (credentials) { const token = base64(`${credentials.user}:${credentials.password}`); - await this.setAuthorizationHeader(`Basic ${token}`); + await this._setAuthorizationHeader(`Basic ${token}`); } } public async bearer(token?: string): Promise { await this.clear(); if (token) - await this.setAuthorizationHeader(`Bearer ${token}`); + await this._setAuthorizationHeader(`Bearer ${token}`); } public clear(): Promise { - return this.setAuthorizationHeader(); + return this._setAuthorizationHeader(); } - private async setAuthorizationHeader(txt?: string): Promise { + private async _setAuthorizationHeader(txt?: string): Promise { if (!txt) txt = ""; return this._page.setExtraHTTPHeaders({ Authorization: txt diff --git a/lib/modules/console/browser_console.ts b/lib/modules/console/browser_console.ts index b3885b72..b7b8fdfd 100644 --- a/lib/modules/console/browser_console.ts +++ b/lib/modules/console/browser_console.ts @@ -1,21 +1,22 @@ import Log from './log'; import { matchText } from '../../utils/utils'; -import { stringifyLogText } from '../../browser/puppeteer_wrapper/puppeteer_utils'; +import { stringifyLogText } from '../../puppeteer_wrapper/puppeteer_utils'; import WendigoModule from '../wendigo_module'; import { LogType, ConsoleFilter } from './types'; import Browser from '../../browser/browser'; import { OpenSettings } from '../../types'; +import { ConsoleMessage } from '../../puppeteer_wrapper/puppeteer_types'; export default class BrowserConsole extends WendigoModule { - private logs: Array; + private _logs: Array; constructor(browser: Browser) { super(browser); - this.logs = []; - this._page.on("console", async (log) => { + this._logs = []; + this._page.on("console", async (log: ConsoleMessage) => { if (log) { const text = await stringifyLogText(log); - this.logs.push(new Log(log, text)); + this._logs.push(new Log(log, text)); } }); } @@ -25,11 +26,11 @@ export default class BrowserConsole extends WendigoModule { } public all(): Array { - return this.logs; + return this._logs; } public filter(filters: ConsoleFilter = {}): Array { - return this.logs.filter((l) => { + return this._logs.filter((l) => { if (filters.type && l.type !== filters.type) return false; if (filters.text && !matchText(l.text, filters.text)) return false; return true; @@ -37,7 +38,7 @@ export default class BrowserConsole extends WendigoModule { } public clear(): void { - this.logs = []; + this._logs = []; } protected async _beforeOpen(options: OpenSettings): Promise { diff --git a/lib/modules/console/log.ts b/lib/modules/console/log.ts index 2547aa4e..75b68b76 100644 --- a/lib/modules/console/log.ts +++ b/lib/modules/console/log.ts @@ -1,4 +1,4 @@ -import { ConsoleMessage, ConsoleMessageType } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { ConsoleMessage, ConsoleMessageType } from '../../puppeteer_wrapper/puppeteer_types'; export default class Log { public message: ConsoleMessage; diff --git a/lib/modules/console/types.ts b/lib/modules/console/types.ts index 09fcd2dd..a74dd2fc 100644 --- a/lib/modules/console/types.ts +++ b/lib/modules/console/types.ts @@ -1,4 +1,4 @@ -import { ConsoleMessageType } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { ConsoleMessageType } from '../../puppeteer_wrapper/puppeteer_types'; export interface ConsoleFilter { type?: ConsoleMessageType; diff --git a/lib/modules/cookies/browser_cookies.ts b/lib/modules/cookies/browser_cookies.ts index 6146cb66..d5807b09 100644 --- a/lib/modules/cookies/browser_cookies.ts +++ b/lib/modules/cookies/browser_cookies.ts @@ -1,4 +1,4 @@ -import { Cookie as CookieData, SetCookie, DeleteCookie } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Cookie as CookieData, SetCookie, DeleteCookie } from '../../puppeteer_wrapper/puppeteer_types'; import WendigoModule from '../wendigo_module'; import { WendigoError } from '../../errors'; import { arrayfy } from '../../utils/utils'; @@ -6,7 +6,7 @@ import { arrayfy } from '../../utils/utils'; export default class BrowserCookies extends WendigoModule { public async all(): Promise<{ [s: string]: string }> { const cookies = await this._page.cookies(); - return cookies.reduce((acc, cookie): { [s: string]: string } => { + return cookies.reduce((acc, cookie: CookieData): { [s: string]: string } => { acc[cookie.name] = cookie.value; return acc; }, {} as { [s: string]: string }); @@ -38,7 +38,7 @@ export default class BrowserCookies extends WendigoModule { public delete(name: string | Array | DeleteCookie): Promise { if (name === undefined || name === null) throw new WendigoError("cookies.delete", "Delete cookie name missing"); - if (this.isDeleteCookieInterface(name)) { + if (this._isDeleteCookieInterface(name)) { return this._page.deleteCookie(name); } @@ -56,7 +56,7 @@ export default class BrowserCookies extends WendigoModule { return this.delete(cookiesList); } - private isDeleteCookieInterface(data: any): data is DeleteCookie { + private _isDeleteCookieInterface(data: any): data is DeleteCookie { if (data.name) return true; else return false; } diff --git a/lib/modules/dialog/browser_dialog.ts b/lib/modules/dialog/browser_dialog.ts index 6eb2a4df..c0ffe23d 100644 --- a/lib/modules/dialog/browser_dialog.ts +++ b/lib/modules/dialog/browser_dialog.ts @@ -14,53 +14,53 @@ const defaultOptions: DialogOptions = { }; export default class BrowserDialog extends WendigoModule { - private dialogs: Array; - private options: DialogOptions; - private onDialogCB?: (d: Dialog) => void; - private lastDialog?: Dialog; + private _dialogs: Array; + private _options: DialogOptions; + private _onDialogCB?: (d: Dialog) => void; + private _lastDialog?: Dialog; constructor(browser: Browser) { super(browser); - this.dialogs = []; - this.options = Object.assign({}, defaultOptions); + this._dialogs = []; + this._options = Object.assign({}, defaultOptions); browser.page.on("dialog", (rawDialog) => { const newDialog = new Dialog(rawDialog); - this.dialogs.push(newDialog); - if (this.onDialogCB) { - this.onDialogCB(newDialog); - this.onDialogCB = undefined; - this.lastDialog = undefined; + this._dialogs.push(newDialog); + if (this._onDialogCB) { + this._onDialogCB(newDialog); + this._onDialogCB = undefined; + this._lastDialog = undefined; } else { - this.lastDialog = newDialog; + this._lastDialog = newDialog; } - if (this.options.dismissAllDialogs) newDialog.dismiss(); + if (this._options.dismissAllDialogs) newDialog.dismiss(); }); } public all(): Array { - return this.dialogs; + return this._dialogs; } public clear(): void { - this.dialogs = []; - this.lastDialog = undefined; + this._dialogs = []; + this._lastDialog = undefined; } public waitForDialog(timeout = 500): Promise { - if (this.lastDialog) { - const result = Promise.resolve(this.lastDialog); - this.lastDialog = undefined; + if (this._lastDialog) { + const result = Promise.resolve(this._lastDialog); + this._lastDialog = undefined; return result; } else { return new Promise((resolve, reject) => { const tid = setTimeout(() => { - if (this.onDialogCB) { - this.onDialogCB = undefined; + if (this._onDialogCB) { + this._onDialogCB = undefined; reject(new TimeoutError("dialog.waitForDialog", "", timeout)); } }, timeout); - this.onDialogCB = (dialog) => { + this._onDialogCB = (dialog) => { clearTimeout(tid); resolve(dialog); }; @@ -69,14 +69,14 @@ export default class BrowserDialog extends WendigoModule { } public dismissLast(): Promise { - if (this.dialogs.length === 0) return Promise.resolve(); - return this.dialogs[this.dialogs.length - 1].dismiss(); + if (this._dialogs.length === 0) return Promise.resolve(); + return this._dialogs[this._dialogs.length - 1].dismiss(); } protected async _beforeOpen(options: OpenSettings): Promise { await super._beforeOpen(options); this.clear(); - this.options = Object.assign({}, defaultOptions); - if ((options as any).dismissAllDialogs) this.options.dismissAllDialogs = true; + this._options = Object.assign({}, defaultOptions); + if ((options as any).dismissAllDialogs) this._options.dismissAllDialogs = true; } } diff --git a/lib/modules/dialog/dialog.ts b/lib/modules/dialog/dialog.ts index b4be7dbf..20deb45d 100644 --- a/lib/modules/dialog/dialog.ts +++ b/lib/modules/dialog/dialog.ts @@ -1,30 +1,30 @@ -import { Dialog as PuppeteerDialog, DialogType } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Dialog as PuppeteerDialog, DialogType } from '../../puppeteer_wrapper/puppeteer_types'; export default class Dialog { - private dialog: PuppeteerDialog; + private _dialog: PuppeteerDialog; constructor(dialog: PuppeteerDialog) { - this.dialog = dialog; + this._dialog = dialog; } public get text(): string { - return this.dialog.message(); + return this._dialog.message(); } public get type(): DialogType { - return this.dialog.type(); + return this._dialog.type(); } public get handled(): boolean { - return Boolean((this.dialog as any)._handled); + return Boolean((this._dialog as any)._handled); } public async dismiss(): Promise { if (!this.handled) - return this.dialog.dismiss(); + return this._dialog.dismiss(); } public async accept(text: string): Promise { if (!this.handled) - return this.dialog.accept(text); + return this._dialog.accept(text); } } diff --git a/lib/modules/local_storage/local_storage_not_assertions.ts b/lib/modules/local_storage/local_storage_not_assertions.ts index fb36df02..8c39e7a9 100644 --- a/lib/modules/local_storage/local_storage_not_assertions.ts +++ b/lib/modules/local_storage/local_storage_not_assertions.ts @@ -3,10 +3,10 @@ import BrowserLocalStorageAssertions from './local_storage_assertions'; import { arrayfy } from '../../utils/utils'; export default class BrowserLocalStorageNotAssertions { - private localStorageAssertions: BrowserLocalStorageAssertions; + private _localStorageAssertions: BrowserLocalStorageAssertions; constructor(localStorageAssertions: BrowserLocalStorageAssertions) { - this.localStorageAssertions = localStorageAssertions; + this._localStorageAssertions = localStorageAssertions; } public exist(key: string | Array, msg?: string): Promise { @@ -16,7 +16,7 @@ export default class BrowserLocalStorageNotAssertions { msg = `Expected ${itemText} "${keyList.join(" ")}" not to exist in localStorage.`; } return invertify(() => { - return this.localStorageAssertions.exist(keyList, ""); + return this._localStorageAssertions.exist(keyList, ""); }, "assert.localStorage.not.exist", msg); } @@ -38,7 +38,7 @@ export default class BrowserLocalStorageNotAssertions { } return invertify(() => { - return this.localStorageAssertions.value(keyVals, ""); + return this._localStorageAssertions.value(keyVals, ""); }, "assert.localStorage.not.value", msg); } @@ -48,14 +48,14 @@ export default class BrowserLocalStorageNotAssertions { msg = `Expected localStorage not to have ${expected} ${itemText}.`; } return invertify(() => { - return this.localStorageAssertions.length(expected, ""); + return this._localStorageAssertions.length(expected, ""); }, "assert.localStorage.not.length", msg); } public empty(msg?: string): Promise { if (!msg) msg = `Expected localStorage not to be empty.`; return invertify(() => { - return this.localStorageAssertions.empty(""); + return this._localStorageAssertions.empty(""); }, "assert.localStorage.not.empty", msg); } } diff --git a/lib/modules/requests/browser_requests.ts b/lib/modules/requests/browser_requests.ts index 18c6bcde..5970ddf3 100644 --- a/lib/modules/requests/browser_requests.ts +++ b/lib/modules/requests/browser_requests.ts @@ -5,7 +5,7 @@ import RequestFilter from './request_filter'; import RequestMocker from './request_mocker'; import Browser from '../../browser/browser'; import RequestMock from './request_mock'; -import { Request, Response } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Request, Response } from '../../puppeteer_wrapper/puppeteer_types'; import { RequestMockOptions } from './types'; import { TimeoutError } from '../../errors'; import { promiseOr, matchText } from '../../utils/utils'; diff --git a/lib/modules/requests/request_assertions_filter.ts b/lib/modules/requests/request_assertions_filter.ts index 0ff127e4..1623f353 100644 --- a/lib/modules/requests/request_assertions_filter.ts +++ b/lib/modules/requests/request_assertions_filter.ts @@ -2,7 +2,7 @@ import { AssertionError } from '../../errors'; import { stringify } from '../../utils/utils'; import RequestFilter from './request_filter'; import { ExpectedHeaders } from './types'; -import { ResourceType } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { ResourceType } from '../../puppeteer_wrapper/puppeteer_types'; type PromiseExecutor = (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void; diff --git a/lib/modules/requests/request_filter.ts b/lib/modules/requests/request_filter.ts index e7c1470d..c2d41a03 100644 --- a/lib/modules/requests/request_filter.ts +++ b/lib/modules/requests/request_filter.ts @@ -1,4 +1,4 @@ -import { Request, ResourceType } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Request, ResourceType } from '../../puppeteer_wrapper/puppeteer_types'; import { matchText } from '../../utils/utils'; import { ExpectedHeaders } from './types'; @@ -14,14 +14,14 @@ async function filterPromise(p: Promise>, cb: (t: T) => boolean): Pr } export default class RequestFilter { - private requestList: Promise>; + private _requestList: Promise>; constructor(requests: Promise> = Promise.resolve([])) { - this.requestList = requests; + this._requestList = requests; } get requests(): Promise> { - return this.requestList; + return this._requestList; } public url(url: string | RegExp): RequestFilter { @@ -49,7 +49,7 @@ export default class RequestFilter { public responseBody(body: string | RegExp | object): RequestFilter { // This one returns a promise const parsedBody = processBody(body); const requests = this.requests.then(async (req) => { - const reqBodyPairs = await this.getResponsesBody(req); + const reqBodyPairs = await this._getResponsesBody(req); const filteredRequests = reqBodyPairs.map(el => { if (matchText(el[1], parsedBody)) return el[0]; else return false; @@ -82,7 +82,7 @@ export default class RequestFilter { public responseHeaders(headers: ExpectedHeaders): RequestFilter { const requests = filterPromise(this.requests, el => { - return this.responseHasHeader(el, headers); + return this._responseHasHeader(el, headers); }); return new RequestFilter(requests); } @@ -112,7 +112,7 @@ export default class RequestFilter { return new RequestFilter(requests); } - private responseHasHeader(request: Request, headers: ExpectedHeaders): boolean { + private _responseHasHeader(request: Request, headers: ExpectedHeaders): boolean { const response = request.response(); if (!response) return false; const keys = Object.keys(headers); @@ -127,7 +127,7 @@ export default class RequestFilter { return true; } - private async getResponsesBody(requests: Array): Promise> { + private async _getResponsesBody(requests: Array): Promise> { type requestResponsePair = [Request, string]; const responses = await Promise.all(requests.map(async (req) => { diff --git a/lib/modules/requests/request_mock.ts b/lib/modules/requests/request_mock.ts index 8b5b1f79..a0cc7fa2 100644 --- a/lib/modules/requests/request_mock.ts +++ b/lib/modules/requests/request_mock.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events'; import { URL } from 'url'; -import { Request } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Request } from '../../puppeteer_wrapper/puppeteer_types'; import RequestFilter from './request_filter'; import { FatalError, AssertionError, TimeoutError } from '../../errors'; @@ -30,27 +30,27 @@ interface RequestMockInterface { } class RequestMockAssertions { - private mock: RequestMock; + private _mock: RequestMock; constructor(mock: RequestMock) { - this.mock = mock; + this._mock = mock; } public called(times?: number, msg?: string): Promise { if (typeof times === 'number') { - const timesCalled = this.mock.timesCalled; + const timesCalled = this._mock.timesCalled; if (times !== timesCalled) { - if (!msg) msg = `Mock of ${this.mock.url} not called ${times} times.`; + if (!msg) msg = `Mock of ${this._mock.url} not called ${times} times.`; return Promise.reject(new AssertionError("assert.called", msg, timesCalled, times)); } - } else if (!this.mock.called) { - if (!msg) msg = `Mock of ${this.mock.url} not called.`; + } else if (!this._mock.called) { + if (!msg) msg = `Mock of ${this._mock.url} not called.`; return Promise.reject(new AssertionError("assert.called", msg)); } return Promise.resolve(); } public async postBody(expected: RequestBody | RegExp, msg?: string): Promise { - const filter = new RequestFilter(Promise.resolve(this.mock.requestsReceived)).postBody(expected); + const filter = new RequestFilter(Promise.resolve(this._mock.requestsReceived)).postBody(expected); const filteredRequests = await filter.requests; if (filteredRequests.length === 0) { if (!msg) { @@ -70,20 +70,20 @@ export default class RequestMock implements RequestMockInterface { public readonly queryString?: { [s: string]: string; }; public requestsReceived: Array = []; - private events: EventEmitter; - private redirectTo?: URL; - private delay: number; + private _events: EventEmitter; + private _redirectTo?: URL; + private _delay: number; constructor(url: string | RegExp, options: RequestMockOptions) { - this.url = this.parseUrl(url); - this.response = this.processResponse(options); - this.delay = options.delay || 0; + this.url = this._parseUrl(url); + this.response = this._processResponse(options); + this._delay = options.delay || 0; this.method = options.method; if (options.queryString !== undefined) this.queryString = utils.parseQueryString(options.queryString); - else this.queryString = this.parseUrlQueryString(url); + else this.queryString = this._parseUrlQueryString(url); this.auto = options.auto !== false; - this.events = new EventEmitter(); - this.redirectTo = options.redirectTo ? new URL(options.redirectTo) : undefined; + this._events = new EventEmitter(); + this._redirectTo = options.redirectTo ? new URL(options.redirectTo) : undefined; this.assert = new RequestMockAssertions(this); } @@ -96,12 +96,12 @@ export default class RequestMock implements RequestMockInterface { } get immediate(): boolean { - return this.delay === 0; + return this._delay === 0; } public trigger(response: RequestMockResponseOptions): void { if (this.auto) throw new FatalError("trigger", `Cannot trigger auto request mock.`); - this.events.emit("respond", response); + this._events.emit("respond", response); } public async waitUntilCalled(timeout: number = 500): Promise { @@ -111,7 +111,7 @@ export default class RequestMock implements RequestMockInterface { rejected = true; reject(new TimeoutError("waitUntilCalled", `Wait until mock of "${this.url}" is called`, timeout)); }, timeout); - this.events.once("on-request", () => { + this._events.once("on-request", () => { if (!rejected) { clearTimeout(tid); resolve(); @@ -125,38 +125,38 @@ export default class RequestMock implements RequestMockInterface { this.requestsReceived.push(request); if (this.auto && this.immediate) { - return this.respondRequest(request); + return this._respondRequest(request); } else if (this.auto) { - await utils.delay(this.delay); - return this.respondRequest(request); + await utils.delay(this._delay); + return this._respondRequest(request); } else { - this.onTrigger((response) => { - return this.respondRequest(request, response); + this._onTrigger((response) => { + return this._respondRequest(request, response); }); } } - private async respondRequest(request: Request, optionalResponse?: RequestMockResponseOptions): Promise { + private async _respondRequest(request: Request, optionalResponse?: RequestMockResponseOptions): Promise { let response = this.response; if (optionalResponse) { - response = this.processResponse(optionalResponse); + response = this._processResponse(optionalResponse); } - if (this.redirectTo) { + if (this._redirectTo) { const url = new URL(request.url()); let qs = url.searchParams.toString(); if (qs) qs = `?${qs}`; await request.continue({ - url: `${this.redirectTo.origin}${this.redirectTo.pathname}${qs || ""}` + url: `${this._redirectTo.origin}${this._redirectTo.pathname}${qs || ""}` }); } else await request.respond(response); - this.events.emit("on-request"); + this._events.emit("on-request"); } - private onTrigger(cb: (r: RequestMockResponseOptions) => Promise): void { - this.events.once("respond", cb); + private _onTrigger(cb: (r: RequestMockResponseOptions) => Promise): void { + this._events.once("respond", cb); } - private processResponse(options: RequestMockOptions): MockResponse { + private _processResponse(options: RequestMockOptions): MockResponse { const body = utils.stringify(options.body) || undefined; return { status: options.status, @@ -166,7 +166,7 @@ export default class RequestMock implements RequestMockInterface { }; } - private parseUrlQueryString(url: string | RegExp): { [s: string]: string; } | undefined { + private _parseUrlQueryString(url: string | RegExp): { [s: string]: string; } | undefined { if (url instanceof RegExp) return undefined; else { const parsedUrl = new URL(url); @@ -176,7 +176,7 @@ export default class RequestMock implements RequestMockInterface { } } - private parseUrl(url: string | RegExp): string | RegExp { + private _parseUrl(url: string | RegExp): string | RegExp { if (url instanceof RegExp) { return url; } else { diff --git a/lib/modules/requests/request_mocker.ts b/lib/modules/requests/request_mocker.ts index c158b7f1..69f8e038 100644 --- a/lib/modules/requests/request_mocker.ts +++ b/lib/modules/requests/request_mocker.ts @@ -1,7 +1,7 @@ import { URL } from 'url'; import { parseQueryString, compareObjects, matchText } from '../../utils/utils'; import RequestMock from './request_mock'; -import { Request } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Request } from '../../puppeteer_wrapper/puppeteer_types'; import { RequestMockOptions } from './types'; export default class RequestMocker { diff --git a/lib/modules/webworkers/webworker.ts b/lib/modules/webworkers/webworker.ts index db88ad16..42a020f6 100644 --- a/lib/modules/webworkers/webworker.ts +++ b/lib/modules/webworkers/webworker.ts @@ -1,4 +1,4 @@ -import { Worker } from '../../browser/puppeteer_wrapper/puppeteer_types'; +import { Worker } from '../../puppeteer_wrapper/puppeteer_types'; export default class WebWoker { public readonly worker: Worker; diff --git a/lib/modules/wendigo_module.ts b/lib/modules/wendigo_module.ts index 93f9b18e..1d8d77c4 100644 --- a/lib/modules/wendigo_module.ts +++ b/lib/modules/wendigo_module.ts @@ -1,6 +1,6 @@ import Browser from '../browser/browser'; import { OpenSettings } from '../types'; -import PuppeteerPage from '../browser/puppeteer_wrapper/puppeteer_page'; +import PuppeteerPage from '../puppeteer_wrapper/puppeteer_page'; export default abstract class WendigoModule { protected _browser: Browser; diff --git a/lib/puppeteer_wrapper/puppeteer_context.ts b/lib/puppeteer_wrapper/puppeteer_context.ts new file mode 100644 index 00000000..6464b5ef --- /dev/null +++ b/lib/puppeteer_wrapper/puppeteer_context.ts @@ -0,0 +1,40 @@ +import { BrowserContext, Page, BrowserContextEventObj } from "./puppeteer_types"; +import PuppeteerPage from "./puppeteer_page"; + +export default class PuppeteerContext { + public context: BrowserContext; + + constructor(context: BrowserContext) { + this.context = context; + } + + public async getDefaultPage(): Promise { + const pages = await this.pages(); + if (pages.length > 0) return new PuppeteerPage(pages[0]); + else return this.newPage(); + } + + public async pages(): Promise> { + return await this.context.pages(); + } + + public async getPage(index: number): Promise { + const pages = await this.pages(); + const rawPage = pages[index]; + if (!rawPage) return undefined; + return new PuppeteerPage(rawPage); + } + + public async newPage(): Promise { + const page = await this.context.newPage(); + return new PuppeteerPage(page); + } + + public on(eventName: K, cb: (msg: BrowserContextEventObj[K]) => Promise): void { + this.context.on(eventName, cb); + } + + public off(eventName: K, cb: (msg: BrowserContextEventObj[K]) => Promise): void { + this.context.off(eventName, cb); + } +} diff --git a/lib/browser/puppeteer_wrapper/puppeteer_page.ts b/lib/puppeteer_wrapper/puppeteer_page.ts similarity index 96% rename from lib/browser/puppeteer_wrapper/puppeteer_page.ts rename to lib/puppeteer_wrapper/puppeteer_page.ts index 1bb671f4..60de7da0 100644 --- a/lib/browser/puppeteer_wrapper/puppeteer_page.ts +++ b/lib/puppeteer_wrapper/puppeteer_page.ts @@ -36,6 +36,14 @@ export default class PuppeteerPage { return this.page.frames(); } + public close(): Promise { + return this.page.close(); + } + + public isClosed(): boolean { + return this.page.isClosed(); + } + public setViewport(config: ViewportOptions = {}): Promise { const finalConfig = Object.assign({}, this.page.viewport(), config) as Viewport; return this.page.setViewport(finalConfig); diff --git a/lib/browser/puppeteer_wrapper/puppeteer_types.ts b/lib/puppeteer_wrapper/puppeteer_types.ts similarity index 84% rename from lib/browser/puppeteer_wrapper/puppeteer_types.ts rename to lib/puppeteer_wrapper/puppeteer_types.ts index 989d82dc..86f85fb4 100644 --- a/lib/browser/puppeteer_wrapper/puppeteer_types.ts +++ b/lib/puppeteer_wrapper/puppeteer_types.ts @@ -6,5 +6,5 @@ export { Page, Frame, Viewport, EvaluateFn, SerializableOrJSHandle, JSHandle, Response, Worker, ScriptTagOptions, Browser, Base64ScreenShotOptions, Keyboard, Mouse, NavigationOptions, WaitForSelectorOptions, ElementHandle, Touchscreen, Cookie, SetCookie, DeleteCookie, PageEventObj, Request, Timeoutable, PDFOptions, ConsoleMessage, ConsoleMessageType, - ResourceType, DialogType, Dialog, BrowserContext + ResourceType, DialogType, Dialog, BrowserContext, Target, BrowserContextEventObj } from 'puppeteer'; diff --git a/lib/browser/puppeteer_wrapper/puppeteer_utils.ts b/lib/puppeteer_wrapper/puppeteer_utils.ts similarity index 100% rename from lib/browser/puppeteer_wrapper/puppeteer_utils.ts rename to lib/puppeteer_wrapper/puppeteer_utils.ts diff --git a/lib/types.ts b/lib/types.ts index 01dc0e7d..5dba3428 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ import DomElement from './models/dom_element'; -import { ViewportOptions } from './browser/puppeteer_wrapper/puppeteer_types'; +import { ViewportOptions } from './puppeteer_wrapper/puppeteer_types'; export type WendigoSelector = string | DomElement; diff --git a/lib/wendigo.ts b/lib/wendigo.ts index ccaf9436..2e9be5ef 100644 --- a/lib/wendigo.ts +++ b/lib/wendigo.ts @@ -1,10 +1,11 @@ import process from 'process'; import puppeteer from 'puppeteer'; -import { BrowserContext, Browser } from './browser/puppeteer_wrapper/puppeteer_types'; +import { BrowserContext } from './puppeteer_wrapper/puppeteer_types'; import BrowserFactory from './browser_factory'; import * as Errors from './errors'; import { WendigoPluginInterface, BrowserSettings, FinalBrowserSettings, WendigoPluginAssertionInterface, PluginModule } from './types'; import BrowserInterface from './browser/browser_interface'; +import PuppeteerContext from './puppeteer_wrapper/puppeteer_context'; const defaultSettings: BrowserSettings = { log: false, @@ -18,30 +19,29 @@ const defaultSettings: BrowserSettings = { }; export default class Wendigo { - private customPlugins: Array; - private browsers: Array; + private _customPlugins: Array; + private _browsers: Array; constructor() { - this.customPlugins = []; - this.browsers = []; + this._customPlugins = []; + this._browsers = []; } public async createBrowser(settings: BrowserSettings = {}): Promise { const finalSettings = this._processSettings(settings); const instance = await this._createInstance(finalSettings); - const plugins = this.customPlugins; - const page = await instance.newPage(); - const b = BrowserFactory.createBrowser(page, finalSettings, plugins); - this.browsers.push(b); + const plugins = this._customPlugins; + const b = await BrowserFactory.createBrowser(instance, finalSettings, plugins); + this._browsers.push(b); return b; } public async stop(): Promise { this.clearPlugins(); - const p = Promise.all(this.browsers.map((b) => { + const p = Promise.all(this._browsers.map((b) => { return b.close(); })); - this.browsers = []; + this._browsers = []; await p; } @@ -59,7 +59,7 @@ export default class Wendigo { this._validatePlugin(finalName, plugin, assertions); BrowserFactory.clearCache(); - this.customPlugins.push({ + this._customPlugins.push({ name: finalName, plugin: plugin, assertions: assertions @@ -67,7 +67,7 @@ export default class Wendigo { } public clearPlugins(): void { - this.customPlugins = []; + this._customPlugins = []; BrowserFactory.clearCache(); } @@ -82,7 +82,7 @@ export default class Wendigo { if (!name || typeof name !== 'string') throw new Errors.FatalError("registerPlugin", `Plugin requires a name.`); let invalidNames = ["assert", "page", "not"]; const defaultModules = ["cookies", "localStorage", "requests", "console", "webworkers", "dialog"]; - const plugins = this.customPlugins; + const plugins = this._customPlugins; invalidNames = invalidNames.concat(plugins.map(p => p.name)).concat(defaultModules); const valid = !invalidNames.includes(name); if (!valid) throw new Errors.FatalError("registerPlugin", `Invalid plugin name "${name}".`); @@ -94,19 +94,22 @@ export default class Wendigo { } } - private async _createInstance(settings: FinalBrowserSettings): Promise { + private async _createInstance(settings: FinalBrowserSettings): Promise { const instance = await puppeteer.launch(settings); + let context: BrowserContext; if (settings.incognito) { - return instance.createIncognitoBrowserContext(); - } else return instance; + context = await instance.createIncognitoBrowserContext(); + } else context = await instance.defaultBrowserContext(); + + return new PuppeteerContext(context); } private _removeBrowser(browser: BrowserInterface): void { - const idx = this.browsers.indexOf(browser); + const idx = this._browsers.indexOf(browser); if (idx === -1) { throw new Errors.FatalError("onClose", "browser not found on closing."); } - this.browsers.splice(idx, 1); + this._browsers.splice(idx, 1); } private _processSettings(settings: BrowserSettings): FinalBrowserSettings { diff --git a/package.json b/package.json index ea712eff..f3aa67fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wendigo", - "version": "2.4.0", + "version": "2.5.0", "description": "A proper monster for front-end automated testing", "engines": { "node": ">=8.0.0" @@ -40,19 +40,19 @@ }, "homepage": "https://github.com/angrykoala/wendigo#readme", "dependencies": { - "compositer": "^1.3.4", + "compositer": "^1.3.5", "is-class": "0.0.8", - "puppeteer": "~1.18.1" + "puppeteer": "~1.19.0" }, "devDependencies": { "@types/mocha": "^5.2.7", - "@types/node": "^12.6.2", + "@types/node": "^12.6.8", "@types/puppeteer": "^1.12.4", "basic-auth": "^2.0.1", - "eslint": "^6.0.1", + "eslint": "^6.1.0", "express": "^4.17.1", "markdownlint-cli": "^0.17.0", - "mocha": "^6.1.4", + "mocha": "^6.2.0", "tslint": "^5.18.0", "typescript": "^3.5.3" } diff --git a/tests/browser/open.test.js b/tests/browser/open.test.js index f5c84310..4a15c603 100644 --- a/tests/browser/open.test.js +++ b/tests/browser/open.test.js @@ -59,7 +59,7 @@ describe("Open", function() { }); await utils.assertThrowsAsync(async() => { await browser2.open(configUrls.index); - }, `InjectScriptError: [open] Evaluation failed: Event. This may be caused by the page Content Security Policy. Make sure the option bypassCSP is set to true in Wendigo.`); + }, `InjectScriptError: [open] Error injecting scripts. This may be caused by the page Content Security Policy. Make sure the option bypassCSP is set to true in Wendigo.`); await browser2.close(); }); diff --git a/tests/browser/tabs.test.js b/tests/browser/tabs.test.js new file mode 100644 index 00000000..1703188b --- /dev/null +++ b/tests/browser/tabs.test.js @@ -0,0 +1,137 @@ +"use strict"; + +const assert = require('assert'); +const Wendigo = require('../..'); +const configUrls = require('../config.json').urls; +const utils = require('../test_utils'); + +describe("Multiple Pages", function() { + this.timeout(5000); + let browser; + + beforeEach(async() => { + browser = await Wendigo.createBrowser(); + await browser.open(configUrls.tabsAndPopups); + }); + + afterEach(async() => { + await browser.close(); + }); + + it("Get Browser Pages And Puppeteer Native Browser", async() => { + const pages = await browser.pages(); + assert.strictEqual(pages.length, 1); + assert.strictEqual(pages[0], browser.page); + assert.ok(browser.context); + assert.strictEqual(browser.context, browser.page.browser().defaultBrowserContext()); + }); + + it("Opening Normal Link Wont Create New Tabs", async() => { + await browser.click(".btn-link"); + await browser.waitForNavigation(); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 1); + await browser.assert.text("p", "html_test"); + await browser.assert.global("WendigoUtils"); + }); + + describe("Tabs", () => { + it("Open Tab", async() => { + const pagesBefore = await browser.pages(); + assert.strictEqual(pagesBefore.length, 1); + await browser.click(".btn-tab"); + await browser.wait(50); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + }); + + it("Switch To Tab", async() => { + await browser.click(".btn-tab"); + await browser.assert.not.text("p", "html_test"); + await browser.wait(50); + await browser.selectPage(1); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + await browser.assert.text("p", "html_test"); + await browser.assert.global("WendigoUtils"); + }); + + it("Close Tab", async() => { + await browser.click(".btn-tab"); + await browser.assert.not.text("p", "html_test"); + await browser.wait(50); + await browser.selectPage(1); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + await browser.closePage(0); + const pagesAfterClose = await browser.pages(); + assert.strictEqual(pagesAfterClose.length, 1); + }); + + it("Close Current Tab", async() => { + await browser.click(".btn-tab"); + await browser.assert.not.text("p", "html_test"); + await browser.wait(100); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + await browser.closePage(0); + const pagesAfterClose = await browser.pages(); + assert.strictEqual(pagesAfterClose.length, 1); + await browser.assert.text("p", "html_test"); + }); + + it("Close Only Tab", async() => { + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 1); + await browser.closePage(0); + const pagesAfterClose = await browser.pages(); + assert.strictEqual(pagesAfterClose.length, 0); + }); + + it("Switch To Invalid Tab", async() => { + await utils.assertThrowsAsync(async() => { + await browser.selectPage(10); + }, `FatalError: [selectPage] Invalid page index "10".`); + }); + + it("Close Invalid Tab", async() => { + await utils.assertThrowsAsync(async() => { + await browser.closePage(10); + }, `FatalError: [closePage] Invalid page index "10".`); + }); + }); + + describe("Popups", () => { + it("Open Popup", async() => { + const pagesBefore = await browser.pages(); + assert.strictEqual(pagesBefore.length, 1); + await browser.click(".btn-popup"); + await browser.wait(50); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + }); + + it("Switch To Popup", async() => { + await browser.click(".btn-popup"); + await browser.assert.not.text("p", "html_test"); + await browser.wait(50); + await browser.selectPage(1); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + await browser.assert.text("p", "html_test"); + await browser.assert.global("WendigoUtils"); + }); + + it("Close Popup", async() => { + await browser.click(".btn-popup"); + await browser.assert.not.text("p", "html_test"); + await browser.wait(50); + await browser.selectPage(1); + const pagesAfter = await browser.pages(); + assert.strictEqual(pagesAfter.length, 2); + await browser.closePage(0); + const pagesAfterClose = await browser.pages(); + assert.strictEqual(pagesAfterClose.length, 1); + }); + }); +}); diff --git a/tests/browser_modules/browser_dialog.test.js b/tests/browser_modules/browser_dialog.test.js index 5158846e..1953c8d8 100644 --- a/tests/browser_modules/browser_dialog.test.js +++ b/tests/browser_modules/browser_dialog.test.js @@ -139,13 +139,13 @@ describe("Dialog Alerts", function() { assert.strictEqual(alerts.length, 1); }); - it("dismissAllDialogs Setting Updated Properly", async() => { + it("DismissAllDialogs Setting Updated Properly", async() => { await browser.open(configUrls.dialogAlert, { dismissAllDialogs: true }); - assert.strictEqual(browser.dialog.options.dismissAllDialogs, true); + assert.strictEqual(browser.dialog._options.dismissAllDialogs, true); await browser.open(configUrls.dialogAlert); - assert.strictEqual(browser.dialog.options.dismissAllDialogs, false); + assert.strictEqual(browser.dialog._options.dismissAllDialogs, false); }); it("Dialog Handled", async() => { diff --git a/tests/config.json b/tests/config.json index 819a0e89..0a8fefea 100644 --- a/tests/config.json +++ b/tests/config.json @@ -31,6 +31,7 @@ "triggerEvent": "http://localhost:3456/trigger_event.html", "auth": "http://localhost:3456/auth.html", "weirdText": "http://localhost:3456/weird_text.html", - "nestedElements": "http://localhost:3456/nested_elements.html" + "nestedElements": "http://localhost:3456/nested_elements.html", + "tabsAndPopups": "http://localhost:3456/tabs_and_popups.html" } } diff --git a/tests/dummy_server/static/tabs_and_popups.html b/tests/dummy_server/static/tabs_and_popups.html new file mode 100644 index 00000000..8f5e8993 --- /dev/null +++ b/tests/dummy_server/static/tabs_and_popups.html @@ -0,0 +1,15 @@ + + + + + Tabs Test + + + + + link + new tab + Popup + + +