From 882b572fd8b9ad7e7382af3482e11a0066fb8d64 Mon Sep 17 00:00:00 2001 From: Kumar Shashank Date: Wed, 6 Dec 2023 23:40:39 +0530 Subject: [PATCH] added support for selenium webdriver --- capture/backstopToolsSelenium.js | 103 ++++ .../selenium/clickAndHoverHelper.js | 44 ++ .../engine_scripts/selenium/loadCookies.js | 34 ++ capture/engine_scripts/selenium/onBefore.js | 3 + capture/engine_scripts/selenium/onReady.js | 6 + core/util/createBitmaps.js | 3 + core/util/runSelenium.js | 444 ++++++++++++++++++ package-lock.json | 114 ++++- package.json | 3 +- 9 files changed, 741 insertions(+), 13 deletions(-) create mode 100644 capture/backstopToolsSelenium.js create mode 100644 capture/engine_scripts/selenium/clickAndHoverHelper.js create mode 100644 capture/engine_scripts/selenium/loadCookies.js create mode 100644 capture/engine_scripts/selenium/onBefore.js create mode 100644 capture/engine_scripts/selenium/onReady.js create mode 100644 core/util/runSelenium.js diff --git a/capture/backstopToolsSelenium.js b/capture/backstopToolsSelenium.js new file mode 100644 index 000000000..69d4c8ab9 --- /dev/null +++ b/capture/backstopToolsSelenium.js @@ -0,0 +1,103 @@ +'use strict'; +module.exports = (driver) => { + return driver.executeScript(() => { + if (window._backstopTools) { + return false; + } + + window._backstopTools = { + hasLogged: function (str) { + return new RegExp(str).test(window._backstopTools._consoleLogger); + }, + startConsoleLogger: function () { + if (typeof window._backstopTools._consoleLogger !== 'string') { + window._backstopTools._consoleLogger = ''; + } + const log = window.console.log.bind(console); + window.console.log = function () { + window._backstopTools._consoleLogger += Array.from(arguments).join('\n'); + log.apply(this, arguments); + }; + }, + /** + * Take an array of selector names and return and array of *all* matching selectors. + * For each selector name, If more than 1 selector is matched, proceeding matches are + * tagged with an additional `__n` class. + * + * @return {[string]} [array of expanded selectors] + * @param selectors + */ + expandSelectors: function (selectors) { + if (!Array.isArray(selectors)) { + selectors = selectors.split(','); + } + return selectors.reduce(function (acc, selector) { + if (selector === 'body' || selector === 'viewport') { + return acc.concat([selector]); + } + if (selector === 'document') { + return acc.concat(['document']); + } + const qResult = document.querySelectorAll(selector); + + // pass-through any selectors that don't match any DOM elements + if (!qResult.length) { + return acc.concat(selector); + } + + const expandedSelector = [].slice.call(qResult) + .map(function (element, expandedIndex) { + if (element.classList.contains('__86d')) { + return ''; + } + if (!expandedIndex) { + // only first element is used for screenshots -- even if multiple instances exist. + // therefore index 0 does not need extended qualification. + return selector; + } + // create index partial + const indexPartial = '__n' + expandedIndex; + // update all matching selectors with additional indexPartial class + element.classList.add(indexPartial); + // return array of fully-qualified classnames + return selector + '.' + indexPartial; + }); + // concat arrays of fully-qualified classnames + return acc.concat(expandedSelector); + }, []).filter(function (selector) { + return selector !== ''; + }); + }, + /** + * is the selector element visible? + * @param {[type]} selector [a css selector str] + * @return {Boolean} [is it visible? true or false] + */ + isVisible: function (selector) { + if (selector === 'body' || selector === 'document' || selector === 'viewport') { + return true; + } else if (window._backstopTools.exists(selector)) { + const element = document.querySelector(selector); + const style = window.getComputedStyle(element); + return (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'); + } + return false; + }, + /** + * does the selector element exist? + * @param {[type]} selector [a css selector str] + * @return {[type]} [returns count of found matches -- 0 for no matches] + */ + exists: function (selector) { + if (selector === 'body' || selector === 'document' || selector === 'viewport') { + return 1; + } + return document.querySelectorAll(selector).length; + } + }; + + window._backstopTools.startConsoleLogger(); + console.info('BackstopTools have been installed.'); + return true; + }); +}; diff --git a/capture/engine_scripts/selenium/clickAndHoverHelper.js b/capture/engine_scripts/selenium/clickAndHoverHelper.js new file mode 100644 index 000000000..6d1cf5a90 --- /dev/null +++ b/capture/engine_scripts/selenium/clickAndHoverHelper.js @@ -0,0 +1,44 @@ +const { until, By, Actions } = require('selenium-webdriver'); + +module.exports = async (driver, scenario) => { + const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector; + const clickSelector = scenario.clickSelectors || scenario.clickSelector; + const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector; + const scrollToSelector = scenario.scrollToSelector; + const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int] + + if (keyPressSelector) { + for (const keyPressSelectorItem of [].concat(keyPressSelector)) { + await driver.wait(until.elementLocated(By.css(keyPressSelectorItem.selector))); + const element = await driver.findElement(By.css(keyPressSelectorItem.selector)); + await element.sendKeys(keyPressSelectorItem.keyPress); + } + } + + if (hoverSelector) { + for (const hoverSelectorIndex of [].concat(hoverSelector)) { + await driver.wait(until.elementLocated(By.css(hoverSelectorIndex))); + const hoverElement = await driver.findElement(By.css(hoverSelectorIndex)); + await new Actions(driver).move({ origin: hoverElement }).perform(); + } + } + + if (clickSelector) { + for (const clickSelectorIndex of [].concat(clickSelector)) { + await driver.wait(until.elementLocated(By.css(clickSelectorIndex))); + const clickElement = await driver.findElement(By.css(clickSelectorIndex)); + await clickElement.click(); + } + } + + if (postInteractionWait) { + await driver.sleep(postInteractionWait); + } + + if (scrollToSelector) { + await driver.wait(until.elementLocated(By.css(scrollToSelector))); + await driver.executeScript(scrollToSelector => { + document.querySelector(scrollToSelector).scrollIntoView(); + }, scrollToSelector); + } +}; diff --git a/capture/engine_scripts/selenium/loadCookies.js b/capture/engine_scripts/selenium/loadCookies.js new file mode 100644 index 000000000..57928538e --- /dev/null +++ b/capture/engine_scripts/selenium/loadCookies.js @@ -0,0 +1,34 @@ +const fs = require('fs'); + +module.exports = async (driver, scenario) => { + let cookies = []; + const cookiePath = scenario.cookiePath; + + // READ COOKIES FROM FILE IF EXISTS + if (fs.existsSync(cookiePath)) { + cookies = JSON.parse(fs.readFileSync(cookiePath)); + } + + // MUNGE COOKIE DOMAIN + cookies = cookies.map(cookie => { + if (cookie.domain.startsWith('http://') || cookie.domain.startsWith('https://')) { + cookie.url = cookie.domain; + } else { + cookie.url = 'https://' + cookie.domain; + } + delete cookie.domain; + return cookie; + }); + + // SET COOKIES + const setCookies = async () => { + return Promise.all( + cookies.map(async (cookie) => { + await driver.manage().addCookie(cookie); + }) + ); + }; + await setCookies(); + await driver.navigate().refresh(); + console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2)); +}; diff --git a/capture/engine_scripts/selenium/onBefore.js b/capture/engine_scripts/selenium/onBefore.js new file mode 100644 index 000000000..0b8672de4 --- /dev/null +++ b/capture/engine_scripts/selenium/onBefore.js @@ -0,0 +1,3 @@ +module.exports = async (driver, scenario, vp) => { + await require('./loadCookies')(driver, scenario); +}; diff --git a/capture/engine_scripts/selenium/onReady.js b/capture/engine_scripts/selenium/onReady.js new file mode 100644 index 000000000..a4c10582e --- /dev/null +++ b/capture/engine_scripts/selenium/onReady.js @@ -0,0 +1,6 @@ +module.exports = async (driver, scenario, vp) => { + console.log('SCENARIO > ' + scenario.label); + await require('./clickAndHoverHelper')(driver, scenario); + + // add more ready handlers here... +}; diff --git a/core/util/createBitmaps.js b/core/util/createBitmaps.js index 73c295f6c..6d8f92cff 100644 --- a/core/util/createBitmaps.js +++ b/core/util/createBitmaps.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const pMap = require('p-map'); const runPuppet = require('./runPuppet'); +const runSelenium = require('./runSelenium'); const { createPlaywrightBrowser, runPlaywright, disposePlaywrightBrowser } = require('./runPlaywright'); const ensureDirectoryPath = require('./ensureDirectoryPath'); @@ -147,6 +148,8 @@ function delegateScenarios (config) { }); }, e => reject(e)); }); + } else if (config.engine === 'selenium') { + return pMap(scenarioViews, runSelenium, { concurrency: asyncCaptureLimit }); } else if (/chrom./i.test(config.engine)) { logger.error('Chromy is no longer supported in version 5+. Please use version 4.x.x for chromy support.'); } else { diff --git a/core/util/runSelenium.js b/core/util/runSelenium.js new file mode 100644 index 000000000..6b9e78d5e --- /dev/null +++ b/core/util/runSelenium.js @@ -0,0 +1,444 @@ +const fs = require('./fs'); +const _ = require('lodash'); +const path = require('path'); +const chalk = require('chalk'); +const { Builder, until, By } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); +const engineTools = require('./engineTools'); +const injectBackstopTools = require('../../capture/backstopToolsSelenium.js'); +const ensureDirectoryPath = require('./ensureDirectoryPath'); + +const TEST_TIMEOUT = 120000; +const DEFAULT_FILENAME_TEMPLATE = '{configId}_{scenarioLabel}_{selectorIndex}_{selectorLabel}_{viewportIndex}_{viewportLabel}'; +const DEFAULT_BITMAPS_TEST_DIR = 'bitmaps_test'; +const DEFAULT_BITMAPS_REFERENCE_DIR = 'bitmaps_reference'; +const SELECTOR_NOT_FOUND_PATH = '/capture/resources/notFound.png'; +const HIDDEN_SELECTOR_PATH = '/capture/resources/notVisible.png'; +const ERROR_SELECTOR_PATH = '/capture/resources/unexpectedErrorSm.png'; +const DOCUMENT_SELECTOR = 'document'; +const BODY_SELECTOR = 'body'; +const VIEWPORT_SELECTOR = 'viewport'; + +module.exports = function (args) { + const scenario = args.scenario; + const viewport = args.viewport; + const config = args.config; + const scenarioLabelSafe = engineTools.makeSafe(scenario.label); + const variantOrScenarioLabelSafe = scenario._parent ? engineTools.makeSafe(scenario._parent.label) : scenarioLabelSafe; + + config._bitmapsTestPath = config.paths.bitmaps_test || DEFAULT_BITMAPS_TEST_DIR; + config._bitmapsReferencePath = config.paths.bitmaps_reference || DEFAULT_BITMAPS_REFERENCE_DIR; + config._fileNameTemplate = config.fileNameTemplate || DEFAULT_FILENAME_TEMPLATE; + config._outputFileFormatSuffix = '.' + ((config.outputFormat && config.outputFormat.match(/jpg|jpeg/)) || 'png'); + config._configId = config.id || engineTools.genHash(config.backstopConfigFileName); + + const logger = { + logged: [] + }; + Object.assign(logger, { + error: loggerAction.bind(logger, 'error'), + warn: loggerAction.bind(logger, 'warn'), + log: loggerAction.bind(logger, 'log'), + info: loggerAction.bind(logger, 'info') + }); + + return processScenarioView(scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config, logger); +}; + +function loggerAction (action, color, message, ...rest) { + this.logged.push([action, color, message.toString(), JSON.stringify(rest)]); + console[action](chalk[color](message), ...rest); +} + +async function captureScreenshot (driver, selector, selectorMap, config, selectors, viewport, logger) { + let filePath, logFilePath; + if (selector) { + filePath = selectorMap[selector].filePath; + logFilePath = selectorMap[selector].logFilePath; + ensureDirectoryPath(filePath); // logs in same dir + + try { + const screenshot = await driver.takeScreenshot(); + fs.writeFile(filePath, screenshot, 'base64'); + await writeScenarioLogs(config, logFilePath, logger); + } catch (e) { + logger.log('red', 'Error capturing..', e); + await writeScenarioLogs(config, logFilePath, logger); + return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath); + } + } else { + // OTHER-SELECTOR screenshot + const selectorShot = async (s, path, logFilePath) => { + const el = await driver.findElement(By.css(s)); + if (el) { + const box = await el.getRect(); + if (box) { + // Resize the viewport to screenshot elements outside of the viewport + if (config.useBoundingBoxViewportForSelectors !== false) { + const bodyHandle = await driver.findElement(By.css('body')); + const boundingBox = await bodyHandle.getRect(); + + await driver.manage().window().setRect({ + width: Math.max(viewport.width, Math.ceil(boundingBox.width)), + height: Math.max(viewport.height, Math.ceil(boundingBox.height)) + }); + } + + const type = config.puppeteerOffscreenCaptureFix ? driver : el; + const screenshot = await type.takeScreenshot(); + fs.writeFile(filePath, screenshot, 'base64'); + await writeScenarioLogs(config, logFilePath, logger); + } else { + logger.log('yellow', `Element not visible for capturing: ${s}`); + await writeScenarioLogs(config, logFilePath, logger); + return fs.copy(config.env.backstop + HIDDEN_SELECTOR_PATH, path); + } + } else { + logger.log('magenta', `Element not found for capturing: ${s}`); + await writeScenarioLogs(config, logFilePath, logger); + return fs.copy(config.env.backstop + SELECTOR_NOT_FOUND_PATH, path); + } + }; + + const selectorsShot = async () => { + for (let i = 0; i < selectors.length; i++) { + const selector = selectors[i]; + filePath = selectorMap[selector].filePath; + logFilePath = selectorMap[selector].logFilePath; + ensureDirectoryPath(filePath); + try { + await selectorShot(selector, filePath, logFilePath); + } catch (e) { + logger.log('red', `Error capturing Element ${selector}`, e); + await writeScenarioLogs(config, logFilePath, logger); + return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath); + } + } + }; + await selectorsShot(); + } +} + +async function delegateSelectors ( + driver, + scenario, + viewport, + variantOrScenarioLabelSafe, + scenarioLabelSafe, + config, + selectors, + selectorMap, + logger +) { + const compareConfig = { testPairs: [] }; + let captureDocument = false; + let captureViewport = false; + const captureList = []; + const captureJobs = []; + + selectors.forEach(function (selector, selectorIndex) { + const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, selectorIndex, selector); + const filePath = config.isReference ? testPair.reference : testPair.test; + const logFilePath = config.isReference ? testPair.referenceLog : testPair.testLog; + + if (!config.isReference) { + compareConfig.testPairs.push(testPair); + } + + selectorMap[selector].filePath = filePath; + selectorMap[selector].logFilePath = logFilePath; + if (selector === BODY_SELECTOR || selector === DOCUMENT_SELECTOR) { + captureDocument = selector; + } else if (selector === VIEWPORT_SELECTOR) { + captureViewport = selector; + } else { + captureList.push(selector); + } + }); + + if (captureDocument) { + captureJobs.push(function () { return captureScreenshot(driver, captureDocument, selectorMap, config, [], viewport, logger); }); + } + // TODO: push captureViewport into captureList (instead of calling captureScreenshot()) to improve perf. + if (captureViewport) { + captureJobs.push(function () { return captureScreenshot(driver, captureViewport, selectorMap, config, [], viewport, logger); }); + } + if (captureList.length) { + captureJobs.push(function () { return captureScreenshot(driver, null, selectorMap, config, captureList, viewport, logger); }); + } + + return new Promise(function (resolve, reject) { + let job = null; + const errors = []; + const next = function () { + if (captureJobs.length === 0) { + if (errors.length === 0) { + resolve(); + } else { + reject(errors); + } + return; + } + job = captureJobs.shift(); + job().catch(function (e) { + logger.log('reset', e); + errors.push(e); + }).then(function () { + next(); + }); + }; + next(); + }).then(async () => { + logger.log('green', 'x Close Browser'); + await driver.quit(); + }).catch(async (err) => { + logger.log('red', err); + await driver.quit(); + }).then(_ => compareConfig); +} + +async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config, logger) { + if (!config.paths) { + config.paths = {}; + } + + if (typeof viewport.label !== 'string') { + viewport.label = viewport.name || ''; + } + + const engineScriptsPath = config.env.engine_scripts || config.env.engine_scripts_default; + const isReference = config.isReference; + + const VP_W = viewport.width || viewport.viewport.width; + const VP_H = viewport.height || viewport.viewport.height; + + const chromeOptions = new chrome.Options(); + config.engineOptions?.args?.forEach((arg) => { + chromeOptions.addArguments(arg); + }); + chromeOptions.windowSize({ width: parseInt(VP_W), height: parseInt(VP_H) }); + + const driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(chromeOptions) + .usingServer('http://localhost:4444/wd/hub') + .build(); + + await driver.manage().setTimeouts({ pageLoad: engineTools.getEngineOption(config, 'waitTimeout', TEST_TIMEOUT) }); + + if (isReference) { + logger.log('blue', 'CREATING NEW REFERENCE FILE'); + } + + let result; + const seleniumCommands = async () => { + // --- OPEN URL --- + let url = scenario.url; + if (isReference && scenario.referenceUrl) { + url = scenario.referenceUrl; + } + await driver.get(translateUrl(url, logger)); + + // --- BEFORE SCRIPT --- + const onBeforeScript = scenario.onBeforeScript || config.onBeforeScript; + if (onBeforeScript) { + const beforeScriptPath = path.resolve(engineScriptsPath, onBeforeScript); + if (fs.existsSync(beforeScriptPath)) { + await require(beforeScriptPath)(driver, scenario, viewport, isReference, config); + } else { + logger.warn('reset', 'WARNING: script not found: ' + beforeScriptPath); + } + } + + // --- set up console output and ready event --- + const readyEvent = scenario.readyEvent || config.readyEvent; + const readyTimeout = scenario.readyTimeout || config.readyTimeout || 30000; + let readyResolve, readyPromise, readyTimeoutTimer; + + if (readyEvent) { + readyPromise = new Promise(resolve => { + readyResolve = resolve; + // fire the ready event after the readyTimeout + readyTimeoutTimer = setTimeout(() => { + logger.error('red', `ReadyEvent not detected within readyTimeout limit. (${readyTimeout} ms)`, scenario.url); + resolve(); + }, readyTimeout); + }); + } + + const logs = await driver.manage().logs().get('browser'); + logs.forEach((entry, i) => { + logger.log('reset', `Browser Console Log ${i}: ${entry.message}`); + if (readyEvent && new RegExp(readyEvent).test(entry.message)) { + readyResolve(); + } + }); + + await injectBackstopTools(driver); + + // --- WAIT FOR READY EVENT --- + if (readyEvent) { + await driver.executeScript(`window._readyEvent = '${readyEvent}'`); + + await readyPromise; + + clearTimeout(readyTimeoutTimer); + + // can't use logger here -- this executes on the page + await driver.executeScript(_ => console.info('readyEvent ok')); + } + + // --- WAIT FOR SELECTOR --- + if (scenario.readySelector) { + await driver.wait(until.elementLocated(By.css(scenario.readySelector)), readyTimeout); + } + // + + // --- DELAY --- + if (scenario.delay > 0) { + await driver.sleep(scenario.delay); + } + + // --- REMOVE SELECTORS --- + if (_.has(scenario, 'removeSelectors')) { + const removeSelectors = async () => { + return Promise.all( + scenario.removeSelectors.map(async (selector) => { + await driver + .executeScript((sel) => { + document.querySelectorAll(sel).forEach(s => { + s.style.cssText = 'display: none !important;'; + s.classList.add('__86d'); + }); + }, selector); + }) + ); + }; + + await removeSelectors(); + } + + // --- ON READY SCRIPT --- + const onReadyScript = scenario.onReadyScript || config.onReadyScript; + if (onReadyScript) { + const readyScriptPath = path.resolve(engineScriptsPath, onReadyScript); + if (fs.existsSync(readyScriptPath)) { + await require(readyScriptPath)(driver, scenario, viewport, isReference, config); + } else { + logger.warn('reset', 'WARNING: script not found: ' + readyScriptPath); + } + } + + // reinstall tools in case onReadyScript has loaded a new URL. + await injectBackstopTools(driver); + + // --- HIDE SELECTORS --- + if (_.has(scenario, 'hideSelectors')) { + const hideSelectors = async () => { + return Promise.all( + scenario.hideSelectors.map(async (selector) => { + await driver + .executeScript((sel) => { + document.querySelectorAll(sel).forEach(s => { + s.style.visibility = 'hidden'; + }); + }, selector); + }) + ); + }; + await hideSelectors(); + } + + // --- HANDLE NO-SELECTORS --- + if (!_.has(scenario, 'selectors') || !scenario.selectors.length) { + scenario.selectors = [DOCUMENT_SELECTOR]; + } + + await driver.executeScript(`window._selectorExpansion = '${scenario.selectorExpansion}'`); + await driver.executeScript(`window._backstopSelectors = '${scenario.selectors}'`); + result = await driver.executeScript(() => { + if (window._selectorExpansion.toString() === 'true') { + window._backstopSelectorsExp = window._backstopTools.expandSelectors(window._backstopSelectors); + } else { + window._backstopSelectorsExp = window._backstopSelectors; + } + if (!Array.isArray(window._backstopSelectorsExp)) { + window._backstopSelectorsExp = window._backstopSelectorsExp.split(','); + } + window._backstopSelectorsExpMap = window._backstopSelectorsExp.reduce((acc, selector) => { + acc[selector] = { + exists: window._backstopTools.exists(selector), + isVisible: window._backstopTools.isVisible(selector) + }; + return acc; + }, {}); + return { + backstopSelectorsExp: window._backstopSelectorsExp, + backstopSelectorsExpMap: window._backstopSelectorsExpMap + }; + }); + }; + + let error; + await seleniumCommands().catch(e => { + logger.log('red', `Selenium encountered an error while running scenario "${scenario.label}"`); + logger.log('red', e); + error = e; + }); + + let compareConfig; + if (!error) { + try { + compareConfig = await delegateSelectors( + driver, + scenario, + viewport, + variantOrScenarioLabelSafe, + scenarioLabelSafe, + config, + result.backstopSelectorsExp, + result.backstopSelectorsExpMap, + logger + ); + } catch (e) { + error = e; + } + } else { + await driver.quit(); + } + + if (error) { + const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, 0, `${scenario.selectors.join('__')}`); + const filePath = config.isReference ? testPair.reference : testPair.test; + const logFilePath = config.isReference ? testPair.referenceLog : testPair.testLog; + testPair.engineErrorMsg = error.message; + + compareConfig = { + testPairs: [testPair] + }; + await writeScenarioLogs(config, logFilePath, logger); + await fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath); + } + + return Promise.resolve(compareConfig); +} + +// handle relative file name +function translateUrl (url, logger) { + const RE = /^[./]/; + if (RE.test(url)) { + const fileUrl = 'file://' + path.join(process.cwd(), url); + logger.log('reset', 'Relative filename detected -- translating to ' + fileUrl); + return fileUrl; + } else { + return url; + } +} + +function writeScenarioLogs (config, logFilePath, logger) { + if (config.scenarioLogsInReports) { + return fs.writeFile(logFilePath, JSON.stringify(logger.logged)); + } else { + return Promise.resolve(true); + } +} diff --git a/package-lock.json b/package-lock.json index a202afb2d..160f7cac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "jump.js": "^1.0.2", "junit-report-builder": "^3.0.0", "lodash": "^4.17.21", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "object-hash": "3.0.0", "opn": "^5.5.0", "os": "^0.1.2", @@ -25,6 +25,7 @@ "playwright": "^1.32.1", "portfinder": "^1.0.32", "puppeteer": "^19.7.0", + "selenium-webdriver": "^4.15.0", "super-simple-web-server": "^1.1.3", "temp": "^0.9.4" }, @@ -3449,8 +3450,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/cosmiconfig": { "version": "8.1.3", @@ -5816,6 +5816,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6282,8 +6287,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "node_modules/isexe": { "version": "2.0.0", @@ -6420,6 +6424,39 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jump.js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/jump.js/-/jump.js-1.0.2.tgz", @@ -6454,6 +6491,14 @@ "node": ">=0.10.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", @@ -7937,6 +7982,11 @@ "node": ">=4" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8490,8 +8540,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -9170,7 +9219,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -9216,8 +9264,7 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -9257,6 +9304,39 @@ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", "dev": true }, + "node_modules/selenium-webdriver": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.15.0.tgz", + "integrity": "sha512-BNG1bq+KWiBGHcJ/wULi0eKY0yaDqFIbEmtbsYJmfaEghdCkXBsx1akgOorhNwjBipOr0uwpvNXqT6/nzl+zjg==", + "dependencies": { + "jszip": "^3.10.1", + "tmp": "^0.2.1", + "ws": ">=8.14.2" + }, + "engines": { + "node": ">= 14.20.0" + } + }, + "node_modules/selenium-webdriver/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/selfsigned": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", @@ -9391,8 +9471,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -10052,6 +10131,17 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, "node_modules/to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", diff --git a/package.json b/package.json index 5d35087e4..cb33f56b3 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "jump.js": "^1.0.2", "junit-report-builder": "^3.0.0", "lodash": "^4.17.21", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "object-hash": "3.0.0", "opn": "^5.5.0", "os": "^0.1.2", @@ -106,6 +106,7 @@ "playwright": "^1.32.1", "portfinder": "^1.0.32", "puppeteer": "^19.7.0", + "selenium-webdriver": "^4.15.0", "super-simple-web-server": "^1.1.3", "temp": "^0.9.4" }