diff --git a/index.js b/index.js index 9eb781b..833de7c 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,8 @@ const ENV_INFO = `${seleniumPkg.name}/${seleniumPkg.version}`; const utils = require('@percy/sdk-utils'); const { DriverMetadata } = require('./driverMetadata'); const log = utils.logger('selenium-webdriver'); +const CS_MAX_SCREENSHOT_LIMIT = 25000; +const SCROLL_DEFAULT_SLEEP_TIME = 0.45; // 450ms const getWidthsForMultiDOM = (userPassedWidths, eligibleWidths) => { // Deep copy of eligible mobile widths @@ -31,7 +33,7 @@ const getWidthsForMultiDOM = (userPassedWidths, eligibleWidths) => { async function changeWindowDimensionAndWait(driver, width, height, resizeCount) { try { const caps = await driver.getCapabilities(); - if (typeof driver?.sendDevToolsCommand === 'function' && caps.getBrowserName() === 'chrome') { + if (typeof driver?.sendDevToolsCommand === 'function' && caps.getBrowserName() === 'chrome' && process.env.PERCY_DISABLE_CDP_RESIZE !== 'true') { await driver?.sendDevToolsCommand('Emulation.setDeviceMetricsOverride', { height, width, @@ -67,17 +69,30 @@ async function captureResponsiveDOM(driver, options) { // Setup the resizeCount listener if not present /* istanbul ignore next: no instrumenting injected code */ await driver.executeScript('PercyDOM.waitForResize()'); + let height = currentHeight; + if (process.env.PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT) { + height = await driver.executeScript(`return window.outerHeight - window.innerHeight + ${utils.percy?.config?.snapshot?.minHeight}`); + } for (let width of widths) { if (lastWindowWidth !== width) { resizeCount++; - await changeWindowDimensionAndWait(driver, width, currentHeight, resizeCount); + await changeWindowDimensionAndWait(driver, width, height, resizeCount); lastWindowWidth = width; } + if (process.env.PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE) { + await driver.navigate().refresh(); + await driver.executeScript(await utils.fetchPercyDOM()); + } + if (process.env.RESPONSIVE_CAPTURE_SLEEP_TIME) { await new Promise(resolve => setTimeout(resolve, parseInt(process.env.RESPONSIVE_CAPTURE_SLEEP_TIME) * 1000)); } + if (process.env.PERCY_ENABLE_LAZY_LOADING_SCROLL) { + await module.exports.slowScrollToBottom(driver); + } + let domSnapshot = await captureSerializedDOM(driver, options); domSnapshot.width = width; domSnapshots.push(domSnapshot); @@ -252,3 +267,33 @@ module.exports.percyScreenshot = async function percyScreenshot(driver, name, op module.exports.isPercyEnabled = async function isPercyEnabled() { return await utils.isPercyEnabled(); }; + +module.exports.slowScrollToBottom = async (driver, scrollSleep = SCROLL_DEFAULT_SLEEP_TIME) => { + if (process.env.PERCY_LAZY_LOAD_SCROLL_TIME) { + scrollSleep = parseFloat(process.env.PERCY_LAZY_LOAD_SCROLL_TIME); + } + + const scrollHeightCommand = 'return Math.max(document.body.scrollHeight, document.body.clientHeight, document.body.offsetHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight, document.documentElement.offsetHeight);'; + let scrollHeight = Math.min(await driver.executeScript(scrollHeightCommand), CS_MAX_SCREENSHOT_LIMIT); + const clientHeight = await driver.executeScript('return document.documentElement.clientHeight'); + let current = 0; + + let page = 1; + // Break the loop if maximum scroll height 25000px is reached + while (scrollHeight > current && current < CS_MAX_SCREENSHOT_LIMIT) { + current = clientHeight * page; + page += 1; + await driver.executeScript(`window.scrollTo(0, ${current})`); + await new Promise(resolve => setTimeout(resolve, scrollSleep * 1000)); + + // Recalculate scroll height for dynamically loaded pages + scrollHeight = await driver.executeScript(scrollHeightCommand); + } + // Get back to top + await driver.executeScript('window.scrollTo(0, 0)'); + let sleepAfterScroll = 1; + if (process.env.PERCY_SLEEP_AFTER_LAZY_LOAD_COMPLETE) { + sleepAfterScroll = parseFloat(process.env.PERCY_SLEEP_AFTER_LAZY_LOAD_COMPLETE); + } + await new Promise(resolve => setTimeout(resolve, sleepAfterScroll * 1000)); +}; diff --git a/package.json b/package.json index 9d8feef..bf17f3f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@percy/selenium-webdriver", "description": "Selenium client library for visual testing with Percy", - "version": "2.2.1-beta.0", + "version": "2.2.2-alpha.2", "license": "MIT", "author": "Perceptual Inc.", "repository": "https://github.com/percy/percy-selenium-js", @@ -12,7 +12,7 @@ ], "publishConfig": { "access": "public", - "tag": "beta" + "tag": "alpha" }, "main": "index.js", "types": "types/index.d.ts", diff --git a/test/index.test.mjs b/test/index.test.mjs index 3d933e7..86da2e3 100644 --- a/test/index.test.mjs +++ b/test/index.test.mjs @@ -3,7 +3,7 @@ import helpers from '@percy/sdk-utils/test/helpers'; import percySnapshot from '../index.js'; import utils from '@percy/sdk-utils'; import { Cache } from '../cache.js'; -const { percyScreenshot } = percySnapshot; +const { percyScreenshot, slowScrollToBottom } = percySnapshot; describe('percySnapshot', () => { let driver; @@ -14,8 +14,9 @@ describe('percySnapshot', () => { .forBrowser('firefox').build(); mockedDriver = { - getCapabilities: jasmine.createSpy('sendDevToolsCommand').and.returnValue({ getBrowserName: () => 'chrome'}), + getCapabilities: jasmine.createSpy('sendDevToolsCommand').and.returnValue({ getBrowserName: () => 'chrome' }), sendDevToolsCommand: jasmine.createSpy('sendDevToolsCommand').and.returnValue(Promise.resolve()), + navigate: jasmine.createSpy('navigate').and.returnValue({ refresh: jasmine.createSpy('refresh') }), manage: jasmine.createSpy('manage').and.returnValue({ window: jasmine.createSpy('window').and.returnValue({ setRect: jasmine.createSpy('setRect').and.returnValue(Promise.resolve()), @@ -187,6 +188,13 @@ describe('percySnapshot', () => { expect(mockedDriver.executeScript).not.toHaveBeenCalledWith('return window.resizeCount'); }); + it('should reload page if PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE is set', async () => { + process.env.PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE = true; + await percySnapshot(mockedDriver, 'Test Snapshot', { responsiveSnapshotCapture: true }); + expect(mockedDriver.navigate().refresh).toHaveBeenCalled(); + delete process.env.PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE; + }); + it('should wait if RESPONSIVE_CAPTURE_SLEEP_TIME is set', async () => { process.env.RESPONSIVE_CAPTURE_SLEEP_TIME = 1; spyOn(global, 'setTimeout').and.callThrough(); @@ -197,6 +205,31 @@ describe('percySnapshot', () => { delete process.env.RESPONSIVE_CAPTURE_SLEEP_TIME; }); + it('should scroll if PERCY_ENABLE_LAZY_LOADING_SCROLL is set', async () => { + process.env.PERCY_ENABLE_LAZY_LOADING_SCROLL = true; + const mockedScroll = spyOn(percySnapshot, 'slowScrollToBottom').and.resolveTo(true); + await percySnapshot(mockedDriver, 'Test Snapshot', { responsiveSnapshotCapture: true }); + + expect(mockedScroll).toHaveBeenCalledWith(mockedDriver); + delete process.env.PERCY_ENABLE_LAZY_LOADING_SCROLL; + }); + + it('should use minHeight if PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is set', async () => { + process.env.PERCY_ENABLE_LAZY_LOADING_SCROLL = true; + process.env.PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT = true; + utils.percy.config = { snapshot: { minHeight: 10 } }; + + const mockedScroll = spyOn(percySnapshot, 'slowScrollToBottom').and.resolveTo(true); + + mockedDriver.executeScript.calls.reset(); + await percySnapshot(mockedDriver, 'Test Snapshot', { responsiveSnapshotCapture: true }); + + expect(mockedDriver.executeScript).toHaveBeenCalledTimes(4); + expect(mockedScroll).toHaveBeenCalledWith(mockedDriver); + delete process.env.PERCY_ENABLE_LAZY_LOADING_SCROLL; + delete process.env.PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT; + }); + it('throw error in SDK if PERCY_RAISE_ERROR is true', async () => { process.env.PERCY_RAISE_ERROR = 'true'; await helpers.test('error', '/percy/healthcheck'); @@ -229,6 +262,48 @@ describe('percySnapshot', () => { }); }); +describe('#slowScrollToBottom', () => { + let mockedDriver = { executeScript: jasmine.createSpy('executeScript') }; + beforeEach(() => { + mockedDriver.executeScript.calls.reset(); + }); + + it('should scroll to bottom and sleep after loading as set in env', async () => { + process.env.PERCY_SLEEP_AFTER_LAZY_LOAD_COMPLETE = 2; + mockedDriver.executeScript.and.returnValues(9, 5, true, 9, true, 9, true); + spyOn(global, 'setTimeout').and.callThrough(); + + await slowScrollToBottom(mockedDriver); + expect(setTimeout.calls.allArgs()).toEqual([[jasmine.any(Function), 450], [jasmine.any(Function), 450], [jasmine.any(Function), 2000]]); + expect(mockedDriver.executeScript).toHaveBeenCalledWith('window.scrollTo(0, 0)'); + expect(mockedDriver.executeScript).toHaveBeenCalledTimes(7); + delete process.env.PERCY_SLEEP_AFTER_LAZY_LOAD_COMPLETE; + }); + + it('should scroll to bottom and sleep as set in env', async () => { + process.env.PERCY_LAZY_LOAD_SCROLL_TIME = '1.2'; + mockedDriver.executeScript.and.returnValues(9, 5, true, 9, true, 9, true); + spyOn(global, 'setTimeout').and.callThrough(); + + await slowScrollToBottom(mockedDriver); + expect(setTimeout.calls.allArgs()).toEqual([[jasmine.any(Function), 1200], [jasmine.any(Function), 1200], [jasmine.any(Function), 1000]]); + expect(mockedDriver.executeScript).toHaveBeenCalledWith('window.scrollTo(0, 0)'); + expect(mockedDriver.executeScript).toHaveBeenCalledTimes(7); + delete process.env.PERCY_LAZY_LOAD_SCROLL_TIME; + }); + + it('should scroll upto 25k px and sleep as passed in function', async () => { + mockedDriver.executeScript = jasmine.createSpy('executeScript'); + mockedDriver.executeScript.and.returnValues(30000, 15000, true, 30000, true, 30000, true); + spyOn(global, 'setTimeout').and.callThrough(); + + await slowScrollToBottom(mockedDriver, 2); + expect(setTimeout.calls.allArgs()).toEqual([[jasmine.any(Function), 2000], [jasmine.any(Function), 2000], [jasmine.any(Function), 1000]]); + expect(mockedDriver.executeScript).toHaveBeenCalledWith('window.scrollTo(0, 0)'); + expect(mockedDriver.executeScript).toHaveBeenCalledTimes(7); + }); +}); + describe('percyScreenshot', () => { class Browser { // Mocking WDIO driver constructor() {