diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 48d50b11b..017e042cd 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -196,6 +196,14 @@ export const configSchema = { maximum: 750, minimum: 1 }, + waitForSelector: { + type: 'string' + }, + waitForTimeout: { + type: 'integer', + minimum: 1, + maximum: 30000 + }, disableCache: { type: 'boolean' }, @@ -295,6 +303,8 @@ export const snapshotSchema = { allowedHostnames: { $ref: '/config/discovery#/properties/allowedHostnames' }, disallowedHostnames: { $ref: '/config/discovery#/properties/disallowedHostnames' }, requestHeaders: { $ref: '/config/discovery#/properties/requestHeaders' }, + waitForSelector: { $ref: '/config/discovery#/properties/waitForSelector' }, + waitForTimeout: { $ref: '/config/discovery#/properties/waitForTimeout' }, authorization: { $ref: '/config/discovery#/properties/authorization' }, disableCache: { $ref: '/config/discovery#/properties/disableCache' }, captureMockedServiceWorker: { $ref: '/config/discovery#/properties/captureMockedServiceWorker' }, diff --git a/packages/core/src/discovery.js b/packages/core/src/discovery.js index dc57ffad0..743d90e44 100644 --- a/packages/core/src/discovery.js +++ b/packages/core/src/discovery.js @@ -1,5 +1,6 @@ import logger from '@percy/logger'; import Queue from './queue.js'; +import Page from './page.js'; import { normalizeURL, hostnameMatches, @@ -9,7 +10,9 @@ import { createLogResource, yieldAll, snapshotLogName, - withRetries + waitForTimeout, + withRetries, + waitForSelectorInsideBrowser } from './utils.js'; import { sha256hash @@ -203,6 +206,18 @@ async function* captureSnapshotResources(page, snapshot, options) { yield resizePage(snapshot.widths[0]); yield page.goto(snapshot.url, { cookies }); + // wait for any specified timeout + if (snapshot.discovery.waitForTimeout && page.enableJavaScript) { + log.debug(`Wait for ${snapshot.discovery.waitForTimeout}ms timeout`); + await waitForTimeout(snapshot.discovery.waitForTimeout); + } + + // wait for any specified selector + if (snapshot.discovery.waitForSelector && page.enableJavaScript) { + log.debug(`Wait for selector: ${snapshot.discovery.waitForSelector}`); + await waitForSelectorInsideBrowser(page, snapshot.discovery.waitForSelector, Page.TIMEOUT); + } + if (snapshot.execute) { // when any execute options are provided, inject snapshot options /* istanbul ignore next: cannot detect coverage of injected code */ diff --git a/packages/core/src/snapshot.js b/packages/core/src/snapshot.js index 341a42b33..6d315a2ef 100644 --- a/packages/core/src/snapshot.js +++ b/packages/core/src/snapshot.js @@ -130,6 +130,8 @@ function getSnapshotOptions(options, { config, meta }) { allowedHostnames: config.discovery.allowedHostnames, disallowedHostnames: config.discovery.disallowedHostnames, networkIdleTimeout: config.discovery.networkIdleTimeout, + waitForTimeout: config.discovery.waitForTimeout, + waitForSelector: config.discovery.waitForSelector, devicePixelRatio: config.discovery.devicePixelRatio, requestHeaders: config.discovery.requestHeaders, authorization: config.discovery.authorization, diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index 81fc06e38..163fe7702 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -257,6 +257,15 @@ async function waitForSelector(selector, timeout) { } } +// wait for a query selector to exist within an optional timeout inside browser +export async function waitForSelectorInsideBrowser(page, selector, timeout) { + try { + return page.eval(`await waitForSelector(${JSON.stringify(selector)}, ${timeout})`); + } catch { + throw new Error(`Unable to find: ${selector}`); + } +} + // Browser-specific util to wait for an xpath selector to exist within an optional timeout. /* istanbul ignore next: tested, but coverage is stripped */ async function waitForXPath(selector, timeout) { diff --git a/packages/core/test/discovery.test.js b/packages/core/test/discovery.test.js index 318a590f8..dcc8d1144 100644 --- a/packages/core/test/discovery.test.js +++ b/packages/core/test/discovery.test.js @@ -2430,6 +2430,72 @@ describe('Discovery', () => { }); }); + describe('waitForSelector/waitForTimeout at the time of discovery =>', () => { + it('waitForTimeout and waitForSelector are called when Js is enabled', async () => { + const page = await percy.browser.page(); + spyOn(percy.browser, 'page').and.returnValue(page); + spyOn(page, 'eval').and.callThrough(); + percy.loglevel('debug'); + + await percy.snapshot({ + name: 'test discovery', + url: 'http://localhost:8000', + enableJavaScript: true, + discovery: { + waitForTimeout: 100, + waitForSelector: 'body' + } + }); + await percy.idle(); + + expect(page.eval).toHaveBeenCalledWith('await waitForSelector("body", 30000)'); + expect(logger.stderr).toEqual(jasmine.arrayContaining([ + '[percy:core:discovery] Wait for selector: body', + '[percy:core:discovery] Wait for 100ms timeout' + ])); + }); + it('waitForTimeout and waitForSelector are not called Js is disabled and domSnapshot is not present', async () => { + percy.loglevel('debug'); + + await percy.snapshot({ + name: 'test discovery', + url: 'http://localhost:8000', + cliEnableJavaScript: false, + discovery: { + waitForTimeout: 100, + waitForSelector: 'body' + } + }); + + await percy.idle(); + + expect(logger.stderr).not.toEqual(jasmine.arrayContaining([ + '[percy:core:discovery] Wait for 100ms timeout', + '[percy:core:discovery] Wait for selector: body' + ])); + }); + it('waitForTimeout and waitForSelector are not called CLI Js is enabled and domSnapshot is present', async () => { + percy.loglevel('debug'); + + await percy.snapshot({ + name: 'test discovery', + url: 'http://localhost:8000', + domSnapshot: testDOM, + discovery: { + waitForTimeout: 100, + waitForSelector: 'body' + } + }); + + await percy.idle(); + + expect(logger.stderr).not.toEqual(jasmine.arrayContaining([ + '[percy:core:discovery] Wait for 100ms timeout', + '[percy:core:discovery] Wait for selector: body' + ])); + }); + }); + describe('Capture image srcset =>', () => { it('make request call to capture resource', async () => { let responsiveDOM = dedent`