diff --git a/README.md b/README.md index a71cc090..00c9ad3e 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,8 @@ const chromeless = new Chromeless({ - [`click(selector: string)`](docs/api.md#api-click) - [`wait(timeout: number)`](docs/api.md#api-wait-timeout) - [`wait(selector: string)`](docs/api.md#api-wait-selector) -- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet +- [`wait(selectors: string[])`](docs/api.md#api-wait-selectors) +- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`](docs/api.md#api-wait-function) - [`clearCache()`](docs/api.md#api-clearcache) - [`focus(selector: string)`](docs/api.md#api-focus) - [`press(keyCode: number, count?: number, modifiers?: any)`](docs/api.md#api-press) diff --git a/docs/api.md b/docs/api.md index 31c00ba0..f6604f94 100644 --- a/docs/api.md +++ b/docs/api.md @@ -24,7 +24,8 @@ Chromeless provides TypeScript typings. - [`click(selector: string)`](#api-click) - [`wait(timeout: number)`](#api-wait-timeout) - [`wait(selector: string, timeout?: number)`](#api-wait-selector) -- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`] - Not implemented yet +- [`wait(selectors: string[], timeout?: number)`](#api-wait-selectors) +- [`wait(fn: (...args: any[]) => boolean, ...args: any[])`](#api-wait-function) - [`clearCache()`](docs/api.md#api-clearcache) - [`focus(selector: string)`](#api-focus) - [`press(keyCode: number, count?: number, modifiers?: any)`](#api-press) @@ -157,13 +158,30 @@ await chromeless.wait('div#loaded', 1000) --------------------------------------- - + -### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless +### wait(selectors: string[], timeout?: number): Chromeless -Not implemented yet +Wait until one of the elements appears. + +__Arguments__ +- `selectors` - Array of DOM selectors to wait for +- `timeout` - How long to wait for any of the element to appear (default is value of waitTimeout option) + +__Example__ + +```js +await chromeless.wait(['div#error', 'div#success']) +await chromeless.wait(['div#error', 'div#success'], 1000) +``` + +--------------------------------------- + + + +### wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless -Wait until a function returns. +Wait until a function returns truthy value. Note that the function is run in headless browser. __Arguments__ - `fn` - Function to wait for @@ -172,7 +190,9 @@ __Arguments__ __Example__ ```js -await chromeless.wait(() => { return console.log('@TODO: put a better example here') }) +await chromeless.wait((divId) => { + return !!document.getElementById(divId) +}, 'div#loaded') ``` --------------------------------------- diff --git a/src/__tests__/test.html b/src/__tests__/test.html index c4075a23..61ecfe1a 100644 --- a/src/__tests__/test.html +++ b/src/__tests__/test.html @@ -3,6 +3,7 @@ Title +
@@ -10,6 +11,17 @@

This is a test page for Chromeless unit tests

+
Click me!
+
+ - \ No newline at end of file + diff --git a/src/api.ts b/src/api.ts index 940d17df..7e3f65b3 100644 --- a/src/api.ts +++ b/src/api.ts @@ -92,7 +92,8 @@ export default class Chromeless implements Promise { wait(timeout: number): Chromeless wait(selector: string, timeout?: number): Chromeless - wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless + wait(selectors: string[], timeout?: number): Chromeless + wait(fn: (...args: any[]) => boolean, ...args: any[]): Chromeless wait(firstArg, ...args: any[]): Chromeless { switch (typeof firstArg) { case 'number': { @@ -100,13 +101,27 @@ export default class Chromeless implements Promise { break } case 'string': { - this.queue.enqueue({ type: 'wait', selector: firstArg, timeout: args[0] }) + this.queue.enqueue({ + type: 'wait', + selector: firstArg, + timeout: args[0], + }) break } case 'function': { this.queue.enqueue({ type: 'wait', fn: firstArg, args }) break } + case 'object': { + if (Array.isArray(firstArg) && firstArg.length) { + this.queue.enqueue({ + type: 'wait', + selectors: firstArg, + timeout: args[0], + }) + break + } + } default: throw new Error(`Invalid wait arguments: ${firstArg} ${args}`) } diff --git a/src/chrome/local-runtime.ts b/src/chrome/local-runtime.ts index 1f56b3aa..c09e9cea 100644 --- a/src/chrome/local-runtime.ts +++ b/src/chrome/local-runtime.ts @@ -11,6 +11,8 @@ import { nodeExists, wait, waitForNode, + waitForNodes, + waitFunction, click, evaluate, screenshot, @@ -58,10 +60,12 @@ export default class LocalRuntime { case 'wait': { if (command.selector) { return this.waitSelector(command.selector, command.timeout) + } else if (command.selectors) { + return this.waitSelectors(command.selectors, command.timeout) } else if (command.timeout) { return this.waitTimeout(command.timeout) } else { - throw new Error('waitFn not yet implemented') + return this.waitFunction(command.fn, command.args) } } case 'clearCache': @@ -150,13 +154,32 @@ export default class LocalRuntime { private async waitSelector( selector: string, - waitTimeout: number = this.chromelessOptions.waitTimeout + waitTimeout: number = this.chromelessOptions.waitTimeout, ): Promise { this.log(`Waiting for ${selector} ${waitTimeout}`) await waitForNode(this.client, selector, waitTimeout) this.log(`Waited for ${selector}`) } + private async waitSelectors( + selectors: string[], + waitTimeout: number = this.chromelessOptions.waitTimeout, + ): Promise { + this.log(`Waiting for ${selectors} ${waitTimeout}`) + await waitForNodes(this.client, selectors, waitTimeout) + this.log(`Waited for ${selectors}`) + } + + private async waitFunction( + fn: string, + args: any[], + waitTimeout: number = this.chromelessOptions.waitTimeout, + ): Promise { + this.log(`Waiting for function`) + await waitFunction(this.client, fn, args, waitTimeout) + this.log(`Waited for function`) + } + private async click(selector: string): Promise { if (this.chromelessOptions.implicitWait) { this.log(`click(): Waiting for ${selector}`) @@ -395,10 +418,7 @@ export default class LocalRuntime { // Returns the S3 url or local file path async returnPdf(options?: PdfOptions): Promise { - const { - filePath, - ...cdpOptions - } = options || { filePath: undefined } + const { filePath, ...cdpOptions } = options || { filePath: undefined } const data = await pdf(this.client, cdpOptions) if (isS3Configured()) { diff --git a/src/types.ts b/src/types.ts index 394fc35c..de6b0ad4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,7 @@ export type Command = type: 'wait' timeout?: number selector?: string + selectors?: string[] fn?: string args?: any[] } diff --git a/src/util.test.ts b/src/util.test.ts index e2fcd216..0065fbc3 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -9,21 +9,20 @@ const testUrl = `data:text/html,${testHtml}` const getPngMetaData = async (filePath): Promise => { const fd = fs.openSync(filePath, 'r') - return await new Promise((resolve) => { - fs.read(fd, Buffer.alloc(24), 0, 24, 0, - (err, bytesRead, buffer) => resolve({ - width: buffer.readUInt32BE(16), - height: buffer.readUInt32BE(20) - })) + return await new Promise(resolve => { + fs.read(fd, Buffer.alloc(24), 0, 24, 0, (err, bytesRead, buffer) => + resolve({ + width: buffer.readUInt32BE(16), + height: buffer.readUInt32BE(20), + }), + ) }) } // POC test('evaluate (document.title)', async t => { const chromeless = new Chromeless({ launchChrome: false }) - const title = await chromeless - .goto(testUrl) - .evaluate(() => document.title) + const title = await chromeless.goto(testUrl).evaluate(() => document.title) await chromeless.end() @@ -32,12 +31,8 @@ test('evaluate (document.title)', async t => { test('screenshot and pdf path', async t => { const chromeless = new Chromeless({ launchChrome: false }) - const screenshot = await chromeless - .goto(testUrl) - .screenshot() - const pdf = await chromeless - .goto(testUrl) - .pdf() + const screenshot = await chromeless.goto(testUrl).screenshot() + const pdf = await chromeless.goto(testUrl).pdf() await chromeless.end() @@ -48,18 +43,85 @@ test('screenshot and pdf path', async t => { }) test('screenshot by selector', async t => { - const version = await CDP.Version() - const versionMajor = parseInt(/Chrome\/(\d+)/.exec(version['User-Agent'])[1]) - // clipping will only work on chrome 61+ + const version = await CDP.Version() + const versionMajor = parseInt(/Chrome\/(\d+)/.exec(version['User-Agent'])[1]) + // clipping will only work on chrome 61+ + + const chromeless = new Chromeless({ launchChrome: false }) + const screenshot = await chromeless.goto(testUrl).screenshot('img') + + await chromeless.end() + + const png = await getPngMetaData(screenshot) + t.is(png.width, versionMajor > 60 ? 512 : 1440) + t.is(png.height, versionMajor > 60 ? 512 : 900) +}) + +test('wait for selector', async t => { + const chromeless = new Chromeless({ launchChrome: false }) + const divExists = await chromeless + .goto(testUrl) + .click('#click') + .wait('#add-div') + .exists('#add-div') + + await chromeless.end() + + t.truthy(divExists) +}) + +test('wait for time', async t => { + const chromeless = new Chromeless({ launchChrome: false }) + const divExists = await chromeless + .goto(testUrl) + .click('#click') + .wait(1200) + .exists('#add-div') + + await chromeless.end() + + t.truthy(divExists) +}) + +test('wait for function', async t => { + const chromeless = new Chromeless({ launchChrome: false }) + const divExists = await chromeless + .goto(testUrl) + .click('#click') + .wait(() => { + return !!document.getElementById('add-div') + }) + .exists('#add-div') + + await chromeless.end() + + t.truthy(divExists) +}) - const chromeless = new Chromeless({ launchChrome: false }) - const screenshot = await chromeless - .goto(testUrl) - .screenshot('img') +test('wait for function with args', async t => { + const chromeless = new Chromeless({ launchChrome: false }) + const divExists = await chromeless + .goto(testUrl) + .click('#click') + .wait(divId => { + return !!document.getElementById(divId) + }, 'add-div') + .exists('#add-div') + + await chromeless.end() + + t.truthy(divExists) +}) - await chromeless.end() +test('wait for selectors array', async t => { + const chromeless = new Chromeless({ launchChrome: false }) + const divExists = await chromeless + .goto(testUrl) + .click('#click') + .wait(['#this-wont-exist', '#add-div']) + .exists('#add-div') + + await chromeless.end() - const png = await getPngMetaData(screenshot) - t.is(png.width, versionMajor > 60 ? 512 : 1440) - t.is(png.height, versionMajor > 60 ? 512 : 900) + t.truthy(divExists) }) diff --git a/src/util.ts b/src/util.ts index d80501b1..ffd23c5e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -93,6 +93,105 @@ export async function waitForNode( } } +export async function waitForNodes( + client: Client, + selectors: string[], + waitTimeout: number, +): Promise { + const { Runtime } = client + + let argStr: string=''; + + if(selectors && selectors.length) { + const jsonArgs = JSON.stringify(selectors) + argStr = jsonArgs.substr(1, jsonArgs.length - 2) + } + const argsArrStr = `[${argStr}]` + + const getNodes = `selectors => { + return JSON.parse(selectors).some(el => { + return !!document.querySelector(el) + }); + }` + + const result = await Runtime.evaluate({ + expression: `(${getNodes})(\`${argsArrStr}\`)`, + }) + + if (!result.result.value) { + const start = new Date().getTime() + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + if (new Date().getTime() - start > waitTimeout) { + clearInterval(interval) + reject( + new Error(`wait("${argsArrStr}") timed out after ${waitTimeout}ms`), + ) + } + + const result = await Runtime.evaluate({ + expression: `(${getNodes})(\`${argsArrStr}\`)`, + }) + + if (result.result.value) { + clearInterval(interval) + resolve() + } + }, 500) + }) + } else { + return + } +} + +export async function waitFunction( + client: Client, + fn: string, + args: any[], + waitTimeout: number, +): Promise { + const { Runtime } = client + + let argStr = ''; + + if(args && args.length) { + const jsonArgs = JSON.stringify(args) + argStr = jsonArgs.substr(1, jsonArgs.length - 2) + } + + const expression = ` + (${fn})(${argStr}); + ` + const result = await Runtime.evaluate({ + expression, + }) + + if (!result.result.value) { + const start = new Date().getTime() + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + if (new Date().getTime() - start > waitTimeout) { + clearInterval(interval) + reject( + new Error(`waitFunction timed out after ${waitTimeout}ms`), + ) + } + + const result = await Runtime.evaluate({ + expression, + }) + + if (result.result.value) { + clearInterval(interval) + resolve() + } + }, 500) + }) + } else { + return + } +} + export async function wait(timeout: number): Promise { return new Promise((resolve, reject) => setTimeout(resolve, timeout)) } @@ -605,4 +704,4 @@ export async function uploadToS3(data: string, contentType: string): Promise