@qawolf/browser
Advanced tools
Comparing version 0.3.5 to 0.4.0
@@ -1,4 +0,5 @@ | ||
import { ElementHandle, Page } from "puppeteer"; | ||
import { ScrollValue } from "@qawolf/types"; | ||
import { ElementHandle } from "puppeteer"; | ||
export declare const click: (element: ElementHandle<Element>) => Promise<void>; | ||
export declare const input: (elementHandle: ElementHandle<Element>, value?: string) => Promise<void>; | ||
export declare const scroll: (page: Page, yPosition: number, timeoutMs?: number) => Promise<void>; | ||
export declare const input: (elementHandle: ElementHandle<Element>, value?: string | null | undefined) => Promise<void>; | ||
export declare const scroll: (elementHandle: ElementHandle<Element>, value: ScrollValue, timeoutMs?: number) => Promise<void>; |
@@ -15,21 +15,25 @@ "use strict"; | ||
}); | ||
exports.input = (elementHandle, value = "") => __awaiter(void 0, void 0, void 0, function* () { | ||
exports.input = (elementHandle, value) => __awaiter(void 0, void 0, void 0, function* () { | ||
const strValue = value || ""; | ||
const handleProperty = yield elementHandle.getProperty("tagName"); | ||
const tagName = yield handleProperty.jsonValue(); | ||
if (tagName.toLowerCase() === "select") { | ||
yield elementHandle.select(value); | ||
yield elementHandle.select(strValue); | ||
} | ||
else { | ||
yield elementHandle.evaluate(element => { | ||
element.value = ""; | ||
}); | ||
yield elementHandle.type(value); | ||
yield elementHandle.focus(); | ||
const currentValue = yield elementHandle.evaluate(element => element.value); | ||
if (currentValue) { | ||
yield elementHandle.evaluate(() => document.execCommand("selectall", false, "")); | ||
yield elementHandle.press("Backspace"); | ||
} | ||
yield elementHandle.type(strValue); | ||
} | ||
}); | ||
exports.scroll = (page, yPosition, timeoutMs = 10000) => __awaiter(void 0, void 0, void 0, function* () { | ||
yield page.evaluate((yPosition, timeoutMs) => { | ||
exports.scroll = (elementHandle, value, timeoutMs = 10000) => __awaiter(void 0, void 0, void 0, function* () { | ||
yield elementHandle.evaluate((element, value, timeoutMs) => { | ||
const qawolf = window.qawolf; | ||
return qawolf.scrollTo(yPosition, timeoutMs); | ||
}, yPosition, timeoutMs); | ||
return qawolf.scroll(element, value, timeoutMs); | ||
}, value, timeoutMs); | ||
}); | ||
//# sourceMappingURL=actions.js.map |
@@ -1,4 +0,6 @@ | ||
import { BrowserStep, Size } from "@qawolf/types"; | ||
import puppeteer, { Page, ElementHandle } from "puppeteer"; | ||
import { BrowserStep, Event, Size } from "@qawolf/types"; | ||
import puppeteer, { ElementHandle } from "puppeteer"; | ||
import { DecoratedPage } from "./QAWolfPage"; | ||
export declare type BrowserCreateOptions = { | ||
record?: boolean; | ||
size?: Size; | ||
@@ -8,17 +10,20 @@ url?: string; | ||
export declare class Browser { | ||
_browser: puppeteer.Browser; | ||
private _browser; | ||
private _currentPageIndex; | ||
private _device; | ||
private _onClose; | ||
private _record; | ||
private _pages; | ||
private _requests; | ||
protected constructor(); | ||
static create(options?: BrowserCreateOptions): Promise<Browser>; | ||
close(): Promise<void>; | ||
currentPage(waitForRequests?: boolean): Promise<DecoratedPage>; | ||
readonly device: puppeteer.devices.Device; | ||
close(): Promise<void>; | ||
currentPage(): Promise<Page>; | ||
element(step: BrowserStep): Promise<ElementHandle>; | ||
goto(url: string): Promise<Page>; | ||
getPage(index?: number, waitForRequests?: boolean, timeoutMs?: number): Promise<Page>; | ||
private managePage; | ||
element(step: BrowserStep, waitForRequests?: boolean): Promise<ElementHandle>; | ||
readonly events: Event[]; | ||
goto(url: string): Promise<DecoratedPage>; | ||
getPage(index?: number, waitForRequests?: boolean, timeoutMs?: number): Promise<DecoratedPage>; | ||
readonly super: puppeteer.Browser; | ||
waitForClose(): Promise<unknown>; | ||
private managePages; | ||
} |
@@ -12,14 +12,14 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const config_1 = require("@qawolf/config"); | ||
const logger_1 = require("@qawolf/logger"); | ||
const web_1 = require("@qawolf/web"); | ||
const lodash_1 = require("lodash"); | ||
const browserUtils_1 = require("./browserUtils"); | ||
const device_1 = require("./device"); | ||
const pageUtils_1 = require("./pageUtils"); | ||
const RequestTracker_1 = require("./RequestTracker"); | ||
const QAWolfPage_1 = require("./QAWolfPage"); | ||
class Browser { | ||
constructor() { | ||
this._currentPageIndex = 0; | ||
this._onClose = []; | ||
this._pages = []; | ||
this._requests = new RequestTracker_1.RequestTracker(); | ||
} | ||
@@ -32,2 +32,3 @@ static create(options = {}) { | ||
self._browser = yield browserUtils_1.launchPuppeteerBrowser(self._device); | ||
self._record = !!options.record; | ||
yield self.managePages(); | ||
@@ -39,36 +40,27 @@ if (options.url) | ||
} | ||
get device() { | ||
return this._device; | ||
} | ||
close() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this._requests.dispose(); | ||
this._pages.forEach(page => page.dispose()); | ||
yield this._browser.close(); | ||
this._onClose.forEach(c => c()); | ||
logger_1.logger.verbose("Browser: closed"); | ||
}); | ||
} | ||
currentPage() { | ||
return this.getPage(this._currentPageIndex); | ||
currentPage(waitForRequests = true) { | ||
return this.getPage(this._currentPageIndex, waitForRequests); | ||
} | ||
element(step) { | ||
get device() { | ||
return this._device; | ||
} | ||
element(step, waitForRequests = true) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
logger_1.logger.verbose(`Browser: find element for ${JSON.stringify(step.target).substring(0, 100)}`); | ||
const page = yield this.getPage(step.pageId, true); | ||
const jsHandle = yield page.evaluateHandle((locator) => { | ||
const qawolf = window.qawolf; | ||
return qawolf.locate.waitForElement(locator); | ||
}, { | ||
action: step.action, | ||
dataAttribute: config_1.CONFIG.dataAttribute, | ||
target: step.target, | ||
timeoutMs: config_1.CONFIG.locatorTimeoutMs, | ||
value: step.value | ||
}); | ||
const handle = jsHandle.asElement(); | ||
if (!handle) { | ||
throw new Error(`No element handle found for step ${step}`); | ||
} | ||
return handle; | ||
const page = yield this.getPage(step.pageId, waitForRequests); | ||
return pageUtils_1.findElement(page, step); | ||
}); | ||
} | ||
get events() { | ||
const events = []; | ||
this._pages.forEach((page, index) => page.events.forEach(event => events.push(Object.assign(Object.assign({}, event), { pageId: index })))); | ||
return lodash_1.sortBy(events, e => e.time); | ||
} | ||
goto(url) { | ||
@@ -93,17 +85,17 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
yield page.bringToFront(); | ||
yield page.super.bringToFront(); | ||
if (waitForRequests) { | ||
yield this._requests.waitUntilComplete(page); | ||
yield page.waitForRequests(); | ||
} | ||
this._currentPageIndex = index; | ||
logger_1.logger.verbose(`Browser: getPage(${index}) activated ${page.url()}`); | ||
return page; | ||
logger_1.logger.verbose(`Browser: getPage(${index}) activated ${page.super.url()}`); | ||
return page.super; | ||
}); | ||
} | ||
managePage(page) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
yield pageUtils_1.injectWebBundle(page); | ||
yield page.emulate(this._device); | ||
this._requests.track(page); | ||
this._pages.push(page); | ||
get super() { | ||
return this._browser; | ||
} | ||
waitForClose() { | ||
return new Promise(resolve => { | ||
this._onClose.push(resolve); | ||
}); | ||
@@ -117,8 +109,15 @@ } | ||
} | ||
yield this.managePage(pages[0]); | ||
const options = { device: this._device, record: this._record }; | ||
this._pages.push(yield QAWolfPage_1.QAWolfPage.create(Object.assign(Object.assign({}, options), { page: pages[0] }))); | ||
this._browser.on("targetcreated", (target) => __awaiter(this, void 0, void 0, function* () { | ||
const page = yield target.page(); | ||
if (page) | ||
this.managePage(page); | ||
this._pages.push(yield QAWolfPage_1.QAWolfPage.create(Object.assign(Object.assign({}, options), { page }))); | ||
})); | ||
this._browser.on("targetdestroyed", () => __awaiter(this, void 0, void 0, function* () { | ||
const pages = yield this._browser.pages(); | ||
if (pages.length === 0) { | ||
yield this.close(); | ||
} | ||
})); | ||
}); | ||
@@ -125,0 +124,0 @@ } |
@@ -1,4 +0,5 @@ | ||
import { Page } from "puppeteer"; | ||
export declare const injectWebBundle: (page: Page) => Promise<void>; | ||
import { BrowserStep } from "@qawolf/types"; | ||
import { ElementHandle, Page } from "puppeteer"; | ||
export declare const $xText: (page: Page, xpath: string) => Promise<string>; | ||
export declare const findElement: (page: Page, step: BrowserStep) => Promise<ElementHandle<Element>>; | ||
export declare const retryExecutionError: (func: () => Promise<any>, times?: number) => Promise<any>; | ||
export declare const $xText: (page: Page, xpath: string) => Promise<string>; |
@@ -11,16 +11,30 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const config_1 = require("@qawolf/config"); | ||
const logger_1 = require("@qawolf/logger"); | ||
const fs_extra_1 = __importDefault(require("fs-extra")); | ||
const path_1 = __importDefault(require("path")); | ||
const webBundle = fs_extra_1.default.readFileSync(path_1.default.resolve(path_1.default.dirname(require.resolve("@qawolf/web")), "./qawolf.web.js"), "utf8"); | ||
exports.injectWebBundle = (page) => __awaiter(void 0, void 0, void 0, function* () { | ||
yield Promise.all([ | ||
page.evaluate(webBundle), | ||
page.evaluateOnNewDocument(webBundle) | ||
]); | ||
exports.$xText = (page, xpath) => __awaiter(void 0, void 0, void 0, function* () { | ||
return yield exports.retryExecutionError(() => __awaiter(void 0, void 0, void 0, function* () { | ||
const elements = yield page.$x(xpath); | ||
const text = yield page.evaluate(element => element.textContent, elements[0]); | ||
return text; | ||
})); | ||
}); | ||
exports.findElement = (page, step) => __awaiter(void 0, void 0, void 0, function* () { | ||
logger_1.logger.verbose(`findElement: ${JSON.stringify(step.target).substring(0, 100)}`); | ||
const jsHandle = yield page.evaluateHandle((locator) => { | ||
const qawolf = window.qawolf; | ||
return qawolf.locate.waitForElement(locator); | ||
}, { | ||
action: step.action, | ||
dataAttribute: config_1.CONFIG.dataAttribute, | ||
target: step.target, | ||
timeoutMs: config_1.CONFIG.locatorTimeoutMs, | ||
value: step.value | ||
}); | ||
const handle = jsHandle.asElement(); | ||
if (!handle) { | ||
throw new Error(`No element handle found for step ${step}`); | ||
} | ||
return handle; | ||
}); | ||
exports.retryExecutionError = (func, times = 3) => __awaiter(void 0, void 0, void 0, function* () { | ||
@@ -45,9 +59,2 @@ for (let i = 0; i < times; i++) { | ||
}); | ||
exports.$xText = (page, xpath) => __awaiter(void 0, void 0, void 0, function* () { | ||
return yield exports.retryExecutionError(() => __awaiter(void 0, void 0, void 0, function* () { | ||
const elements = yield page.$x(xpath); | ||
const text = yield page.evaluate(element => element.textContent, elements[0]); | ||
return text; | ||
})); | ||
}); | ||
//# sourceMappingURL=pageUtils.js.map |
import { Page } from "puppeteer"; | ||
export declare class RequestTracker { | ||
private _onComplete; | ||
private _onDispose; | ||
private _trackers; | ||
private _page; | ||
private _requests; | ||
private _timeout; | ||
constructor(timeout?: number); | ||
track(page: Page): void; | ||
waitUntilComplete(page: Page): Promise<unknown>; | ||
constructor(page: Page, timeout?: number); | ||
dispose(): void; | ||
waitUntilComplete(): Promise<unknown>; | ||
private handleRequest; | ||
private handleRemoveRequest; | ||
private on; | ||
} |
@@ -15,58 +15,50 @@ "use strict"; | ||
class RequestTracker { | ||
constructor(timeout = 10000) { | ||
constructor(page, timeout = 5000) { | ||
this._onComplete = []; | ||
this._onDispose = []; | ||
this._trackers = []; | ||
this._requests = []; | ||
this._page = page; | ||
this._timeout = timeout; | ||
this.on("request", (request) => this.handleRequest(request)); | ||
this.on("requestfailed", request => this.handleRemoveRequest(request, "requestfailed")); | ||
this.on("requestfinished", request => this.handleRemoveRequest(request, "requestfinished")); | ||
} | ||
track(page) { | ||
const tracker = { | ||
page, | ||
requests: [], | ||
onComplete: [] | ||
}; | ||
this._trackers.push(tracker); | ||
const removeRequest = (request, reason) => { | ||
const items = lodash_1.remove(tracker.requests, i => i === request); | ||
if (items.length < 1) | ||
return; | ||
logger_1.logger.debug(`RequestTracker: request-- ${reason} ${tracker.requests.length} ${page.url().substring(0, 100)}`); | ||
if (tracker.requests.length === 0) { | ||
tracker.onComplete.forEach(done => done()); | ||
tracker.onComplete = []; | ||
} | ||
}; | ||
this.on(page, "request", (request) => { | ||
tracker.requests.push(request); | ||
logger_1.logger.debug(`RequestTracker: request++ ${tracker.requests.length} ${page.url().substring(0, 100)}`); | ||
const intervalId = setTimeout(() => removeRequest(request, "timeout"), this._timeout); | ||
this._onDispose.push(() => clearInterval(intervalId)); | ||
}); | ||
this.on(page, "requestfailed", request => removeRequest(request, "requestfailed")); | ||
this.on(page, "requestfinished", request => removeRequest(request, "requestfinished")); | ||
dispose() { | ||
this._onDispose.forEach(f => f()); | ||
} | ||
waitUntilComplete(page) { | ||
waitUntilComplete() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
logger_1.logger.verbose(`RequestTracker: waitUntilComplete ${page.url()}`); | ||
logger_1.logger.verbose(`RequestTracker: waitUntilComplete ${this._page.url()}`); | ||
return new Promise(resolve => { | ||
const tracker = this._trackers.find(c => c.page === page); | ||
if (!tracker) | ||
throw new Error(`Cannot find counter. Page is not tracked`); | ||
if (tracker.requests.length === 0) { | ||
logger_1.logger.verbose(`RequestTracker: waitUntilComplete resolved immediately ${page.url()}`); | ||
if (this._requests.length === 0) { | ||
logger_1.logger.verbose(`RequestTracker: waitUntilComplete resolved immediately ${this._page.url()}`); | ||
resolve(); | ||
return; | ||
} | ||
tracker.onComplete.push(resolve); | ||
this._onComplete.push(resolve); | ||
}); | ||
}); | ||
} | ||
dispose() { | ||
this._onDispose.forEach(f => f()); | ||
handleRequest(request) { | ||
this._requests.push(request); | ||
logger_1.logger.debug(`RequestTracker: request++ ${this._requests.length} ${this._page.url().substring(0, 100)}`); | ||
const intervalId = setTimeout(() => this.handleRemoveRequest(request, "timeout"), this._timeout); | ||
this._onDispose.push(() => clearInterval(intervalId)); | ||
} | ||
on(page, eventName, handler) { | ||
page.on(eventName, handler); | ||
this._onDispose.push(() => page.removeListener(eventName, handler)); | ||
handleRemoveRequest(request, reason) { | ||
const items = lodash_1.remove(this._requests, i => i === request); | ||
if (items.length < 1) | ||
return; | ||
logger_1.logger.debug(`RequestTracker: request-- ${reason} ${this._requests.length} ${this._page.url().substring(0, 100)}`); | ||
if (this._requests.length === 0) { | ||
this._onComplete.forEach(done => done()); | ||
this._onComplete = []; | ||
} | ||
} | ||
on(eventName, handler) { | ||
this._page.on(eventName, handler); | ||
this._onDispose.push(() => this._page.removeListener(eventName, handler)); | ||
} | ||
} | ||
exports.RequestTracker = RequestTracker; | ||
//# sourceMappingURL=RequestTracker.js.map |
{ | ||
"name": "@qawolf/browser", | ||
"description": "qawolf browser manager", | ||
"version": "0.3.5", | ||
"version": "0.4.0", | ||
"license": "BSD-3.0", | ||
@@ -26,6 +26,6 @@ "main": "./lib/index.js", | ||
"dependencies": { | ||
"@qawolf/config": "^0.3.5", | ||
"@qawolf/logger": "^0.3.5", | ||
"@qawolf/types": "^0.3.0", | ||
"@qawolf/web": "^0.3.0", | ||
"@qawolf/config": "^0.4.0", | ||
"@qawolf/logger": "^0.4.0", | ||
"@qawolf/types": "^0.4.0", | ||
"@qawolf/web": "^0.4.0", | ||
"fs-extra": "^8.1.0", | ||
@@ -39,3 +39,3 @@ "lodash": "^4.17.15", | ||
}, | ||
"gitHead": "3fd370c7d522472d3a427be082d90b02250ecb30" | ||
"gitHead": "a0a2411b4e62470aea6d556082385f7df832cc80" | ||
} |
import { QAWolfWeb } from "@qawolf/web"; | ||
import { ElementHandle, Page } from "puppeteer"; | ||
import { ScrollValue } from "@qawolf/types"; | ||
import { ElementHandle } from "puppeteer"; | ||
@@ -10,4 +11,5 @@ export const click = async (element: ElementHandle): Promise<void> => { | ||
elementHandle: ElementHandle, | ||
value: string = "" | ||
value?: string | null | ||
): Promise<void> => { | ||
const strValue = value || ""; | ||
const handleProperty = await elementHandle.getProperty("tagName"); | ||
@@ -17,10 +19,20 @@ const tagName = await handleProperty.jsonValue(); | ||
if (tagName.toLowerCase() === "select") { | ||
await elementHandle.select(value); | ||
await elementHandle.select(strValue); | ||
} else { | ||
// clear current value | ||
await elementHandle.evaluate(element => { | ||
(element as HTMLInputElement).value = ""; | ||
}); | ||
await elementHandle.focus(); | ||
await elementHandle.type(value); | ||
const currentValue = await elementHandle.evaluate( | ||
element => (element as HTMLInputElement).value | ||
); | ||
if (currentValue) { | ||
// select all so we replace the text | ||
// from https://github.com/GoogleChrome/puppeteer/issues/1313#issuecomment-471732011 | ||
await elementHandle.evaluate(() => | ||
document.execCommand("selectall", false, "") | ||
); | ||
await elementHandle.press("Backspace"); | ||
} | ||
await elementHandle.type(strValue); | ||
} | ||
@@ -30,14 +42,14 @@ }; | ||
export const scroll = async ( | ||
page: Page, | ||
yPosition: number, | ||
elementHandle: ElementHandle, | ||
value: ScrollValue, | ||
timeoutMs: number = 10000 | ||
): Promise<void> => { | ||
await page.evaluate( | ||
(yPosition, timeoutMs) => { | ||
await elementHandle.evaluate( | ||
(element, value, timeoutMs) => { | ||
const qawolf: QAWolfWeb = (window as any).qawolf; | ||
return qawolf.scrollTo(yPosition, timeoutMs); | ||
return qawolf.scroll(element, value, timeoutMs); | ||
}, | ||
yPosition, | ||
value, | ||
timeoutMs | ||
); | ||
}; |
@@ -1,12 +0,13 @@ | ||
import { CONFIG } from "@qawolf/config"; | ||
import { logger } from "@qawolf/logger"; | ||
import { BrowserStep, Locator, Size } from "@qawolf/types"; | ||
import { QAWolfWeb, waitFor } from "@qawolf/web"; | ||
import puppeteer, { Page, ElementHandle, Serializable } from "puppeteer"; | ||
import { BrowserStep, Callback, Event, Size } from "@qawolf/types"; | ||
import { waitFor } from "@qawolf/web"; | ||
import { sortBy } from "lodash"; | ||
import puppeteer, { devices, ElementHandle } from "puppeteer"; | ||
import { launchPuppeteerBrowser } from "./browserUtils"; | ||
import { getDevice } from "./device"; | ||
import { injectWebBundle } from "./pageUtils"; | ||
import { RequestTracker } from "./RequestTracker"; | ||
import { findElement } from "./pageUtils"; | ||
import { DecoratedPage, QAWolfPage } from "./QAWolfPage"; | ||
export type BrowserCreateOptions = { | ||
record?: boolean; | ||
size?: Size; | ||
@@ -20,13 +21,13 @@ url?: string; | ||
*/ | ||
public _browser: puppeteer.Browser; | ||
private _browser: puppeteer.Browser; | ||
private _currentPageIndex: number = 0; | ||
private _device: puppeteer.devices.Device; | ||
private _device: devices.Device; | ||
private _onClose: Callback[] = []; | ||
private _record: boolean; | ||
// stored in order of open | ||
private _pages: Page[] = []; | ||
private _requests: RequestTracker; | ||
private _pages: QAWolfPage[] = []; | ||
// protect constructor to force using async Browser.create() | ||
protected constructor() { | ||
this._requests = new RequestTracker(); | ||
} | ||
// protect constructor to force using async create() | ||
protected constructor() {} | ||
@@ -42,2 +43,3 @@ public static async create(options: BrowserCreateOptions = {}) { | ||
self._browser = await launchPuppeteerBrowser(self._device); | ||
self._record = !!options.record; | ||
await self.managePages(); | ||
@@ -50,49 +52,36 @@ | ||
public get device() { | ||
return this._device; | ||
} | ||
public async close(): Promise<void> { | ||
this._requests.dispose(); | ||
this._pages.forEach(page => page.dispose()); | ||
await this._browser.close(); | ||
this._onClose.forEach(c => c()); | ||
logger.verbose("Browser: closed"); | ||
} | ||
public currentPage(): Promise<Page> { | ||
return this.getPage(this._currentPageIndex); | ||
public currentPage(waitForRequests: boolean = true): Promise<DecoratedPage> { | ||
return this.getPage(this._currentPageIndex, waitForRequests); | ||
} | ||
public async element(step: BrowserStep): Promise<ElementHandle> { | ||
logger.verbose( | ||
`Browser: find element for ${JSON.stringify(step.target).substring( | ||
0, | ||
100 | ||
)}` | ||
); | ||
public get device() { | ||
return this._device; | ||
} | ||
const page = await this.getPage(step.pageId, true); | ||
public async element( | ||
step: BrowserStep, | ||
waitForRequests: boolean = true | ||
): Promise<ElementHandle> { | ||
const page = await this.getPage(step.pageId, waitForRequests); | ||
return findElement(page, step); | ||
} | ||
const jsHandle = await page.evaluateHandle( | ||
(locator: Locator) => { | ||
const qawolf: QAWolfWeb = (window as any).qawolf; | ||
return qawolf.locate.waitForElement(locator); | ||
}, | ||
{ | ||
action: step.action, | ||
dataAttribute: CONFIG.dataAttribute, | ||
target: step.target, | ||
timeoutMs: CONFIG.locatorTimeoutMs, | ||
value: step.value | ||
} as Serializable | ||
public get events() { | ||
const events: Event[] = []; | ||
this._pages.forEach((page, index) => | ||
page.events.forEach(event => events.push({ ...event, pageId: index })) | ||
); | ||
const handle = jsHandle.asElement(); | ||
if (!handle) { | ||
throw new Error(`No element handle found for step ${step}`); | ||
} | ||
return handle; | ||
return sortBy(events, e => e.time); | ||
} | ||
public async goto(url: string): Promise<Page> { | ||
public async goto(url: string): Promise<DecoratedPage> { | ||
logger.verbose(`Browser: goto ${url}`); | ||
@@ -108,3 +97,3 @@ const page = await this.currentPage(); | ||
timeoutMs: number = 5000 | ||
): Promise<Page> { | ||
): Promise<DecoratedPage> { | ||
/** | ||
@@ -117,3 +106,2 @@ * Wait for the page at index to be ready and activate it. | ||
if (index >= this._pages.length) return null; | ||
return this._pages[index]; | ||
@@ -128,20 +116,24 @@ }, timeoutMs); | ||
// for the execution context to run | ||
await page.bringToFront(); | ||
await page.super.bringToFront(); | ||
if (waitForRequests) { | ||
await this._requests.waitUntilComplete(page); | ||
await page.waitForRequests(); | ||
} | ||
this._currentPageIndex = index; | ||
logger.verbose(`Browser: getPage(${index}) activated ${page.url()}`); | ||
logger.verbose(`Browser: getPage(${index}) activated ${page.super.url()}`); | ||
return page; | ||
return page.super; | ||
} | ||
private async managePage(page: Page) { | ||
await injectWebBundle(page); | ||
await page.emulate(this._device); | ||
this._requests.track(page); | ||
this._pages.push(page); | ||
public get super() { | ||
return this._browser; | ||
} | ||
public waitForClose() { | ||
return new Promise(resolve => { | ||
this._onClose.push(resolve); | ||
}); | ||
} | ||
private async managePages() { | ||
@@ -154,9 +146,20 @@ const pages = await this._browser.pages(); | ||
} | ||
await this.managePage(pages[0]); | ||
const options = { device: this._device, record: this._record }; | ||
this._pages.push(await QAWolfPage.create({ ...options, page: pages[0] })); | ||
this._browser.on("targetcreated", async target => { | ||
const page = await target.page(); | ||
if (page) this.managePage(page); | ||
if (page) this._pages.push(await QAWolfPage.create({ ...options, page })); | ||
}); | ||
this._browser.on("targetdestroyed", async () => { | ||
// close the browser all pages are closed | ||
const pages = await this._browser.pages(); | ||
if (pages.length === 0) { | ||
await this.close(); | ||
} | ||
}); | ||
} | ||
} |
@@ -0,18 +1,50 @@ | ||
import { CONFIG } from "@qawolf/config"; | ||
import { logger } from "@qawolf/logger"; | ||
import fs from "fs-extra"; | ||
import path from "path"; | ||
import { Page } from "puppeteer"; | ||
import { BrowserStep, Locator } from "@qawolf/types"; | ||
import { QAWolfWeb } from "@qawolf/web"; | ||
import { ElementHandle, Page, Serializable } from "puppeteer"; | ||
const webBundle = fs.readFileSync( | ||
path.resolve(path.dirname(require.resolve("@qawolf/web")), "./qawolf.web.js"), | ||
"utf8" | ||
); | ||
export const $xText = async (page: Page, xpath: string): Promise<string> => { | ||
return await retryExecutionError(async () => { | ||
const elements = await page.$x(xpath); | ||
export const injectWebBundle = async (page: Page) => { | ||
await Promise.all([ | ||
page.evaluate(webBundle), | ||
page.evaluateOnNewDocument(webBundle) | ||
]); | ||
const text = await page.evaluate( | ||
element => element.textContent, | ||
elements[0] | ||
); | ||
return text; | ||
}); | ||
}; | ||
export const findElement = async ( | ||
page: Page, | ||
step: BrowserStep | ||
): Promise<ElementHandle> => { | ||
logger.verbose( | ||
`findElement: ${JSON.stringify(step.target).substring(0, 100)}` | ||
); | ||
const jsHandle = await page.evaluateHandle( | ||
(locator: Locator) => { | ||
const qawolf: QAWolfWeb = (window as any).qawolf; | ||
return qawolf.locate.waitForElement(locator); | ||
}, | ||
{ | ||
action: step.action, | ||
dataAttribute: CONFIG.dataAttribute, | ||
target: step.target, | ||
timeoutMs: CONFIG.locatorTimeoutMs, | ||
value: step.value | ||
} as Serializable | ||
); | ||
const handle = jsHandle.asElement(); | ||
if (!handle) { | ||
throw new Error(`No element handle found for step ${step}`); | ||
} | ||
return handle; | ||
}; | ||
export const retryExecutionError = async ( | ||
@@ -42,14 +74,1 @@ func: () => Promise<any>, | ||
}; | ||
export const $xText = async (page: Page, xpath: string): Promise<string> => { | ||
return await retryExecutionError(async () => { | ||
const elements = await page.$x(xpath); | ||
const text = await page.evaluate( | ||
element => element.textContent, | ||
elements[0] | ||
); | ||
return text; | ||
}); | ||
}; |
import { logger } from "@qawolf/logger"; | ||
import { Callback } from "@qawolf/types"; | ||
import { remove } from "lodash"; | ||
import { Page, Request, PageEventObj } from "puppeteer"; | ||
type Callback = () => void; | ||
type Tracker = { | ||
page: Page; | ||
requests: Request[]; | ||
onComplete: Callback[]; | ||
}; | ||
export class RequestTracker { | ||
private _onComplete: Callback[] = []; | ||
private _onDispose: (() => void)[] = []; | ||
private _trackers: Tracker[] = []; | ||
private _page: Page; | ||
private _requests: Request[] = []; | ||
// how long until we ignore a request | ||
private _timeout: number; | ||
constructor(timeout: number = 10000) { | ||
constructor(page: Page, timeout: number = 5000) { | ||
this._page = page; | ||
this._timeout = timeout; | ||
} | ||
public track(page: Page) { | ||
const tracker: Tracker = { | ||
page, | ||
requests: [], | ||
onComplete: [] | ||
}; | ||
this._trackers.push(tracker); | ||
this.on("request", (request: Request) => this.handleRequest(request)); | ||
const removeRequest = (request: Request, reason: string) => { | ||
const items = remove(tracker.requests, i => i === request); | ||
// ignore since it was already removed | ||
if (items.length < 1) return; | ||
logger.debug( | ||
`RequestTracker: request-- ${reason} ${ | ||
tracker.requests.length | ||
} ${page.url().substring(0, 100)}` | ||
); | ||
if (tracker.requests.length === 0) { | ||
tracker.onComplete.forEach(done => done()); | ||
tracker.onComplete = []; | ||
} | ||
}; | ||
this.on(page, "request", (request: Request) => { | ||
tracker.requests.push(request); | ||
logger.debug( | ||
`RequestTracker: request++ ${ | ||
tracker.requests.length | ||
} ${page.url().substring(0, 100)}` | ||
); | ||
const intervalId = setTimeout( | ||
() => removeRequest(request, "timeout"), | ||
this._timeout | ||
); | ||
this._onDispose.push(() => clearInterval(intervalId)); | ||
}); | ||
this.on(page, "requestfailed", request => | ||
removeRequest(request, "requestfailed") | ||
this.on("requestfailed", request => | ||
this.handleRemoveRequest(request, "requestfailed") | ||
); | ||
this.on(page, "requestfinished", request => | ||
removeRequest(request, "requestfinished") | ||
this.on("requestfinished", request => | ||
this.handleRemoveRequest(request, "requestfinished") | ||
); | ||
} | ||
public async waitUntilComplete(page: Page) { | ||
logger.verbose(`RequestTracker: waitUntilComplete ${page.url()}`); | ||
public dispose() { | ||
this._onDispose.forEach(f => f()); | ||
} | ||
public async waitUntilComplete() { | ||
logger.verbose(`RequestTracker: waitUntilComplete ${this._page.url()}`); | ||
return new Promise(resolve => { | ||
const tracker = this._trackers.find(c => c.page === page); | ||
if (!tracker) throw new Error(`Cannot find counter. Page is not tracked`); | ||
if (tracker.requests.length === 0) { | ||
if (this._requests.length === 0) { | ||
logger.verbose( | ||
`RequestTracker: waitUntilComplete resolved immediately ${page.url()}` | ||
`RequestTracker: waitUntilComplete resolved immediately ${this._page.url()}` | ||
); | ||
@@ -90,19 +48,47 @@ resolve(); | ||
tracker.onComplete.push(resolve); | ||
this._onComplete.push(resolve); | ||
}); | ||
} | ||
public dispose() { | ||
this._onDispose.forEach(f => f()); | ||
private handleRequest(request: Request) { | ||
this._requests.push(request); | ||
logger.debug( | ||
`RequestTracker: request++ ${ | ||
this._requests.length | ||
} ${this._page.url().substring(0, 100)}` | ||
); | ||
const intervalId = setTimeout( | ||
() => this.handleRemoveRequest(request, "timeout"), | ||
this._timeout | ||
); | ||
this._onDispose.push(() => clearInterval(intervalId)); | ||
} | ||
private handleRemoveRequest(request: Request, reason: string) { | ||
const items = remove(this._requests, i => i === request); | ||
// ignore since it was already removed | ||
if (items.length < 1) return; | ||
logger.debug( | ||
`RequestTracker: request-- ${reason} ${ | ||
this._requests.length | ||
} ${this._page.url().substring(0, 100)}` | ||
); | ||
if (this._requests.length === 0) { | ||
this._onComplete.forEach(done => done()); | ||
this._onComplete = []; | ||
} | ||
} | ||
private on<K extends keyof PageEventObj>( | ||
page: Page, | ||
eventName: K, | ||
handler: (e: PageEventObj[K], ...args: any[]) => void | ||
) { | ||
page.on(eventName, handler); | ||
this._page.on(eventName, handler); | ||
this._onDispose.push(() => page.removeListener(eventName, handler)); | ||
this._onDispose.push(() => this._page.removeListener(eventName, handler)); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
60672
41
981
1
+ Added@qawolf/config@0.4.4(transitive)
+ Added@qawolf/logger@0.4.4(transitive)
+ Added@qawolf/types@0.4.4(transitive)
+ Added@qawolf/web@0.4.4(transitive)
- Removed@qawolf/config@0.3.5(transitive)
- Removed@qawolf/logger@0.3.5(transitive)
- Removed@qawolf/types@0.3.0(transitive)
- Removed@qawolf/web@0.3.0(transitive)
Updated@qawolf/config@^0.4.0
Updated@qawolf/logger@^0.4.0
Updated@qawolf/types@^0.4.0
Updated@qawolf/web@^0.4.0