UNPKG

creevey

Version:

Cross-browser screenshot testing tool for Storybook with fancy UI Runner

683 lines 33.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InternalBrowser = void 0; const chalk_1 = __importDefault(require("chalk")); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const loglevel_1 = __importDefault(require("loglevel")); const loglevel_plugin_prefix_1 = __importDefault(require("loglevel-plugin-prefix")); const pngjs_1 = require("pngjs"); const selenium_webdriver_1 = require("selenium-webdriver"); // import { Options as IeOptions } from 'selenium-webdriver/ie'; // import { Options as EdgeOptions } from 'selenium-webdriver/edge'; // import { Options as ChromeOptions } from 'selenium-webdriver/chrome'; // import { Options as SafariOptions } from 'selenium-webdriver/safari'; // import { Options as FirefoxOptions } from 'selenium-webdriver/firefox'; const capabilities_js_1 = require("selenium-webdriver/lib/capabilities.js"); const types_js_1 = require("../../types.js"); const logger_js_1 = require("../logger.js"); const messages_js_1 = require("../messages.js"); const utils_js_1 = require("../utils.js"); const webdriver_js_1 = require("../webdriver.js"); const storybook_helpers_js_1 = require("../storybook-helpers.js"); // type UnPromise<P> = P extends Promise<infer T> ? T : never; // let context: UnPromise<ReturnType<typeof BrowsingContext>> | null = null; function getSessionData(grid, sessionId = '') { const gridUrl = new URL(grid); gridUrl.pathname = `/host/${sessionId}`; return new Promise((resolve, reject) => (gridUrl.protocol == 'https:' ? https_1.default : http_1.default).get(gridUrl.toString(), (res) => { if (res.statusCode !== 200) { reject(new Error(`Couldn't get session data for ${sessionId}. Status code: ${res.statusCode ?? 'Unknown'}`)); return; } let data = ''; res.setEncoding('utf-8'); res.on('data', (chunk) => (data += chunk)); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (error) { reject(new Error(`Couldn't get session data for ${sessionId}. ${error instanceof Error ? (error.stack ?? error.message) : error}`)); } }); })); } async function openUrlAndWaitForPageSource(browser, url, predicate) { let source = ''; await browser.get(url); do { try { source = await browser.getPageSource(); } catch { // NOTE: Firefox can raise exception "curContainer.frame.document.documentElement is null" } } while (predicate(source)); return source; } async function buildWebdriver(browser, gridUrl, config, debug) { const browserConfig = config.browsers[browser]; const { /*customizeBuilder,*/ seleniumCapabilities, browserName, connectionTimeout } = browserConfig; // Use browser-specific or global or default timeout (60 seconds) const timeout = connectionTimeout ?? config.connectionTimeout ?? 60_000; const url = new URL(gridUrl); url.username = url.username ? '********' : ''; url.password = url.password ? '********' : ''; (0, logger_js_1.logger)().debug(`Connecting to Selenium ${chalk_1.default.magenta(url.toString())}`); // TODO Define some capabilities explicitly and define typings const capabilities = new selenium_webdriver_1.Capabilities({ browserName, ...seleniumCapabilities, pageLoadStrategy: capabilities_js_1.PageLoadStrategy.EAGER, }); const prefs = new selenium_webdriver_1.logging.Preferences(); if (debug) { for (const type of Object.values(selenium_webdriver_1.logging.Type)) { prefs.setLevel(type, selenium_webdriver_1.logging.Level.ALL); } } // TODO Fetch selenium grid capabilities // TODO Validate browsers, versions, and platform // TODO Use `customizeBuilder` let webdriver; try { const maxRetries = 5; let retries = 0; do { webdriver = await Promise.race([ new Promise((resolve) => { setTimeout(() => { retries += 1; resolve(null); }, timeout); }), (async () => { if (retries > 0) { (0, logger_js_1.logger)().debug(`Trying to initialize session to Selenium Grid: retried ${retries} of ${maxRetries}`); } const retry = retries; // const ie = new IeOptions(); // const edge = new EdgeOptions(); // const chrome = new ChromeOptions(); // const safari = new SafariOptions(); // const firefox = new FirefoxOptions(); // edge.enableBidi(); // chrome.enableBidi(); // firefox.enableBidi(); const driver = await new selenium_webdriver_1.Builder() // .setIeOptions(ie) // .setEdgeOptions(edge) // .setChromeOptions(chrome) // .setSafariOptions(safari) // .setFirefoxOptions(firefox) .usingServer(gridUrl) .withCapabilities(capabilities) .setLoggingPrefs(prefs) // NOTE: Should go last .build(); // const id = await driver.getWindowHandle(); // context = await BrowsingContext(driver, { browsingContextId: id }); if (retry != retries) { void driver.quit().catch(() => { /* noop */ }); return null; } return driver; })(), ]); if (webdriver) break; } while (retries < maxRetries); if (!webdriver) throw new Error('Failed to initialize session to Selenium Grid due to many retries'); } catch (error) { (0, logger_js_1.logger)().error(`Failed to start browser:`, error); return null; } return webdriver; } class InternalBrowser { #isShuttingDown = false; #browser; #storybookGlobals; #unsubscribe = types_js_1.noop; #keepAliveInterval = null; #sessionId = ''; constructor(browser, storybookGlobals) { this.#browser = browser; this.#storybookGlobals = storybookGlobals; this.#unsubscribe = (0, messages_js_1.subscribeOn)('shutdown', () => { void this.closeBrowser(); }); } get browser() { return this.#browser; } async closeBrowser() { if (this.#isShuttingDown) return; this.#isShuttingDown = true; this.#unsubscribe(); if (this.#keepAliveInterval !== null) clearInterval(this.#keepAliveInterval); try { await this.#browser.quit(); } catch { /* noop */ } } async takeScreenshot(captureElement, ignoreElements) { let screenshot; const ignoreStyles = await this.insertIgnoreStyles(ignoreElements); if ((0, logger_js_1.logger)().getLevel() <= loglevel_1.default.levels.DEBUG) { const { innerWidth, innerHeight } = await this.#browser.executeScript(function () { return { innerWidth: window.innerWidth, innerHeight: window.innerHeight, }; }); (0, logger_js_1.logger)().debug(`Viewport size is: ${innerWidth}x${innerHeight}`); } try { if (!captureElement) { (0, logger_js_1.logger)().debug('Capturing viewport screenshot'); screenshot = await this.#browser.takeScreenshot(); (0, logger_js_1.logger)().debug('Viewport screenshot is captured'); } else { (0, logger_js_1.logger)().debug(`Checking is element ${chalk_1.default.cyan(captureElement)} fit into viewport`); const rects = await this.#browser.executeScript(function (selector) { window.scrollTo(0, 0); // eslint-disable-next-line no-var var element = document.querySelector(selector); if (!element) return; // eslint-disable-next-line no-var var elementRect = element.getBoundingClientRect(); return { elementRect: { top: elementRect.top, left: elementRect.left, width: elementRect.width, height: elementRect.height, }, // NOTE page_Offset is used only for IE9-11 windowRect: { top: Math.round(window.scrollY || window.pageYOffset), left: Math.round(window.scrollX || window.pageXOffset), width: window.innerWidth, height: window.innerHeight, }, }; }, captureElement); const { elementRect, windowRect } = rects ?? {}; if (!elementRect || !windowRect) throw new Error(`Couldn't find element with selector: '${captureElement}'`); const isFitIntoViewport = elementRect.width + elementRect.left <= windowRect.width && elementRect.height + elementRect.top <= windowRect.height; if (isFitIntoViewport) { (0, logger_js_1.logger)().debug(`Capturing ${chalk_1.default.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`); } else (0, logger_js_1.logger)().debug(`Capturing composite screenshot image of ${chalk_1.default.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`); // const element = await browser.findElement(By.css(captureElement)); // screenshot = isFitIntoViewport // ? context // ? await context.captureElementScreenshot(await element.getId()) // : await browser.findElement(By.css(captureElement)).takeScreenshot() // : await takeCompositeScreenshot(browser, windowRect, elementRect); screenshot = isFitIntoViewport ? await this.#browser.findElement(selenium_webdriver_1.By.css(captureElement)).takeScreenshot() : await this.takeCompositeScreenshot(windowRect, elementRect); (0, logger_js_1.logger)().debug(`${chalk_1.default.cyan(captureElement)} is captured`); } } finally { await this.removeIgnoreStyles(ignoreStyles); } return typeof screenshot === 'string' ? Buffer.from(screenshot, 'base64') : screenshot; } async selectStory(id) { const sessionId = await this.#browser.executeScript(() => window.__CREEVEY_SESSION_ID__); if (sessionId !== this.#sessionId) { const done = await this.initStorybook(); if (!done) return; } await this.#browser.executeScript(() => delete window.__CREEVEY_SELECT_STORY_RESULT__); await this.resetMousePosition(); (0, logger_js_1.logger)().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk_1.default.magenta(id)}`); void this.#browser.executeScript(storybook_helpers_js_1.selectStory, id).catch(() => { /* noop */ }); let isWaitingForStory = true; const result = await Promise.race([ new Promise((resolve) => { setTimeout(() => { isWaitingForStory = false; resolve({ type: 'timeout' }); }, 60000); }), (async () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (isWaitingForStory) { const [selectResult, sessionId] = await this.#browser.executeScript(() => [window.__CREEVEY_SELECT_STORY_RESULT__, window.__CREEVEY_SESSION_ID__]); if (selectResult) return { type: 'select', ...selectResult }; if (sessionId !== this.#sessionId) return { type: 'reload' }; await new Promise((resolve) => setTimeout(resolve, 100)); } return { type: 'timeout' }; })(), ]); if (result.type === 'timeout') throw new Error('Story selection timed out'); if (result.type === 'reload') { (0, logger_js_1.logger)().debug('Storybook page has been reloaded during story selection'); const done = await this.initStorybook(); if (!done) return; } if (result.type === 'select' && result.status === 'error') { throw new Error(`Failed to select story: ${result.message}`); } } async updateStoryArgs(story, updatedArgs) { await this.#browser.executeAsyncScript(function (storyId, updatedArgs, UPDATE_STORY_ARGS, STORY_RENDERED, callback) { window.__STORYBOOK_ADDONS_CHANNEL__.once(STORY_RENDERED, callback); window.__STORYBOOK_ADDONS_CHANNEL__.emit(UPDATE_STORY_ARGS, { storyId, updatedArgs, }); }, story.id, updatedArgs, types_js_1.StorybookEvents.UPDATE_STORY_ARGS, types_js_1.StorybookEvents.STORY_RENDERED); } async loadStoriesFromBrowser() { return await this.#browser.executeAsyncScript(storybook_helpers_js_1.getStories); } async afterTest() { if ((0, logger_js_1.logger)().getLevel() <= loglevel_1.default.levels.DEBUG) { const logs = await this.#browser.manage().logs().get('browser'); for (const log of logs) { (0, logger_js_1.logger)().debug(`Console message: ${new Date(log.timestamp).toISOString()} - ${log.message}`); } } } static async getBrowser(browserName, gridUrl, config, debug) { const browserConfig = config.browsers[browserName]; const { storybookUrl: address = config.storybookUrl, limit, viewport, // eslint-disable-next-line @typescript-eslint/no-deprecated _storybookGlobals, storybookGlobals = _storybookGlobals, } = browserConfig; void limit; const browser = await buildWebdriver(browserName, gridUrl, config, debug); if (!browser) return null; const internalBrowser = new InternalBrowser(browser, storybookGlobals); try { if (utils_js_1.isShuttingDown.current) return null; const done = await internalBrowser.init({ browserName, gridUrl, viewport, storybookUrl: address, }); return done ? internalBrowser : null; } catch (originalError) { void internalBrowser.closeBrowser(); const message = originalError instanceof Error ? originalError.message : (originalError ?? 'Unknown error'); const error = new Error(`Can't load storybook root page: ${message}`); if (originalError instanceof Error) error.stack = originalError.stack; (0, logger_js_1.logger)().error(error); return null; } } async init({ browserName, gridUrl, viewport, storybookUrl, }) { const sessionId = (await this.#browser.getSession()).getId(); let browserHost = ''; try { const { Name } = await getSessionData(gridUrl, sessionId); if (typeof Name == 'string') browserHost = Name; } catch { /* noop */ } loglevel_plugin_prefix_1.default.apply((0, logger_js_1.logger)(), { format(level) { const levelColor = logger_js_1.colors[level.toUpperCase()]; return `[${browserName}:${chalk_1.default.gray(process.pid)}] ${levelColor(level)} => ${chalk_1.default.gray(sessionId)}`; }, }); (0, logger_js_1.logger)().debug(`Connected successful with ${chalk_1.default.green(browserHost)}`); this.#sessionId = sessionId; return await (0, utils_js_1.runSequence)([ () => this.#browser.manage().setTimeouts({ pageLoad: 60000, script: 60000 }), () => this.openStorybookPage(storybookUrl), () => this.initStorybook(), // NOTE: Selenium draws automation toolbar with some delay after webdriver initialization // NOTE: So if we resize window right after getting webdriver instance we might get situation // NOTE: When the toolbar appears after resize and final viewport size become smaller than we set () => this.resizeViewport(viewport), () => { this.keepAlive(); }, ], () => !this.#isShuttingDown); } async initStorybook() { await this.#browser.executeScript((id) => (window.__CREEVEY_SESSION_ID__ = id), this.#sessionId); return await (0, utils_js_1.runSequence)([() => this.waitForStorybook(), () => this.loadStorybookStories(), () => this.defineGlobals()], () => !this.#isShuttingDown); } async openStorybookPage(storybookUrl) { if (!webdriver_js_1.LOCALHOST_REGEXP.test(storybookUrl)) { return this.#browser.get((0, webdriver_js_1.appendIframePath)(storybookUrl)); } try { // NOTE: getUrlChecker already calls `browser.get` so we don't need another one await (0, webdriver_js_1.resolveStorybookUrl)((0, webdriver_js_1.appendIframePath)(storybookUrl), (url) => this.checkUrl(url)); } catch (error) { (0, logger_js_1.logger)().error('Failed to resolve storybook URL', error instanceof Error ? error.message : ''); throw error; } } async checkUrl(url) { try { // NOTE: Before trying a new url, reset the current one (0, logger_js_1.logger)().debug(`Opening ${chalk_1.default.magenta('about:blank')} page`); await openUrlAndWaitForPageSource(this.#browser, 'about:blank', (source) => !source.includes('<body></body>')); (0, logger_js_1.logger)().debug(`Opening ${chalk_1.default.magenta(url)} and checking the page source`); const source = await openUrlAndWaitForPageSource(this.#browser, url, // NOTE: IE11 can return only `head` without body (source) => source.length == 0 || !/<body([^>]*>).+<\/body>/s.test(source)); // NOTE: This is the most optimal way to check if we in storybook or not // We don't use any page load strategies except `NONE` // because other add significant delay and some of them don't work in earlier chrome versions // Browsers always load page successful even it's failed // So we just check `root` element (0, logger_js_1.logger)().debug(`Checking ${chalk_1.default.cyan(`#${webdriver_js_1.storybookRootID}`)} existence on ${chalk_1.default.magenta(url)}`); return source.includes(`id="${webdriver_js_1.storybookRootID}"`); } catch { return false; } } async waitForStorybook() { (0, logger_js_1.logger)().debug('Waiting for Storybook to initiate'); void this.#browser .executeScript(function () { requestAnimationFrame(check); function check() { if (document.readyState !== 'complete' || typeof window.__STORYBOOK_PREVIEW__ === 'undefined' || typeof window.__STORYBOOK_ADDONS_CHANNEL__ === 'undefined' || (!('ready' in window.__STORYBOOK_PREVIEW__) && window.__STORYBOOK_ADDONS_CHANNEL__.last('setGlobals') === undefined)) { requestAnimationFrame(check); return; } if ('ready' in window.__STORYBOOK_PREVIEW__) { // NOTE: Storybook <= 7.x doesn't have ready() method void window.__STORYBOOK_PREVIEW__.ready().then(() => (window.__CREEVYE_STORYBOOK_READY__ = true)); } else { window.__CREEVYE_STORYBOOK_READY__ = true; } } }) .catch(() => { /* noop */ }); let isWaitingForStorybook = true; const isTimeout = await Promise.race([ new Promise((resolve) => { setTimeout(() => { isWaitingForStorybook = false; resolve(true); }, 60000); }), (async () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (isWaitingForStorybook) { if (await this.#browser.executeScript(() => window.__CREEVYE_STORYBOOK_READY__)) return false; else await new Promise((resolve) => setTimeout(resolve, 100)); } return true; })(), ]); if (isTimeout) throw new Error('Failed to wait Storybook init'); } async loadStorybookStories() { (0, logger_js_1.logger)().debug('Loading Storybook stories'); void this.#browser .executeScript(() => { void window.__STORYBOOK_PREVIEW__.extract().then((stories) => { window.__CREEVEY_STORYBOOK_STORIES__ = stories; }); }) .catch(() => { /* noop */ }); let isWaitingForStories = true; const result = await Promise.race([ new Promise((resolve) => { setTimeout(() => { isWaitingForStories = false; resolve({ type: 'timeout' }); }, 60000); }), (async () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (isWaitingForStories) { const [hasStories, sessionId] = await this.#browser.executeScript(() => [ Boolean(window.__CREEVEY_STORYBOOK_STORIES__), window.__CREEVEY_SESSION_ID__, ]); if (hasStories) return { type: 'stories' }; if (sessionId !== this.#sessionId) return { type: 'reload' }; await new Promise((resolve) => setTimeout(resolve, 100)); } return { type: 'timeout' }; })(), ]); if (result.type === 'timeout') throw new Error('Failed to load Storybook stories'); if (result.type === 'reload') await this.waitForStorybook(); } async defineGlobals() { (0, logger_js_1.logger)().debug('Defining Storybook globals'); await this.#browser.executeAsyncScript(function (userGlobals, callback) { // @ts-expect-error https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084 window.__name = (func) => func; if (userGlobals) { window.__STORYBOOK_ADDONS_CHANNEL__.once('globalsUpdated', () => { callback(); }); window.__STORYBOOK_ADDONS_CHANNEL__.emit('updateGlobals', { globals: userGlobals }); } else { callback(); } }, this.#storybookGlobals); } async resizeViewport(viewport) { if (!viewport) return; const windowRect = await this.#browser.manage().window().getRect(); const { innerWidth, innerHeight } = await this.#browser.executeScript(function () { return { innerWidth: window.innerWidth, innerHeight: window.innerHeight, }; }); (0, logger_js_1.logger)().debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`); const dWidth = windowRect.width - innerWidth; const dHeight = windowRect.height - innerHeight; await this.#browser .manage() .window() .setRect({ width: viewport.width + dWidth, height: viewport.height + dHeight, }); } async resetMousePosition() { (0, logger_js_1.logger)().debug('Resetting mouse position to the top-left corner'); const browserName = (await this.#browser.getCapabilities()).getBrowserName(); const [browserVersion] = (await this.#browser.getCapabilities()).getBrowserVersion()?.split('.') ?? (await this.#browser.getCapabilities()).get('version')?.split('.') ?? []; // NOTE Reset mouse position to support keweb selenium grid browser versions if (browserName == 'chrome' && browserVersion == '70') { const { top, left, width, height } = await this.#browser.executeScript(function () { const bodyRect = document.body.getBoundingClientRect(); return { top: bodyRect.top, left: bodyRect.left, width: bodyRect.width, height: bodyRect.height, }; }); // NOTE Bridge mode doesn't support `Origin.VIEWPORT`, move mouse relative await this.#browser .actions({ bridge: true }) .move({ origin: this.#browser.findElement(selenium_webdriver_1.By.css('body')), x: Math.ceil((-1 * width) / 2) - left, y: Math.ceil((-1 * height) / 2) - top, }) .perform(); } else if (browserName == 'firefox') { // NOTE Firefox for some reason moving by 0 x 0 move cursor in bottom left corner :sad: // NOTE In recent versions (eg 128.0) moving by 0 x 0 doesn't work at all await this.#browser.actions().move({ origin: selenium_webdriver_1.Origin.VIEWPORT, x: 0, y: 1 }).perform(); } else { // NOTE IE don't emit move events until force window focus or connect by RDP on virtual machine await this.#browser.actions().move({ origin: selenium_webdriver_1.Origin.VIEWPORT, x: 0, y: 0 }).perform(); } } async insertIgnoreStyles(ignoreElements) { const ignoreSelectors = Array.prototype.concat(ignoreElements).filter(Boolean); if (!ignoreSelectors.length) return null; (0, logger_js_1.logger)().debug('Hiding ignored elements before capturing'); return await this.#browser.executeScript(storybook_helpers_js_1.insertIgnoreStyles, ignoreSelectors); } async takeCompositeScreenshot(windowRect, elementRect) { const screens = []; const isScreenshotWithoutScrollBar = !(await this.hasScrollBar()); const scrollBarWidth = await this.getScrollBarWidth(); // NOTE Sometimes viewport has been scrolled somewhere const normalizedElementRect = { left: elementRect.left - windowRect.left, right: elementRect.left + elementRect.width - windowRect.left, top: elementRect.top - windowRect.top, bottom: elementRect.top + elementRect.height - windowRect.top, }; const isFitHorizontally = windowRect.width >= elementRect.width + normalizedElementRect.left; const isFitVertically = windowRect.height >= elementRect.height + normalizedElementRect.top; const viewportWidth = windowRect.width - (isFitVertically ? 0 : scrollBarWidth); const viewportHeight = windowRect.height - (isFitHorizontally ? 0 : scrollBarWidth); const cols = Math.ceil(elementRect.width / viewportWidth); const rows = Math.ceil(elementRect.height / viewportHeight); const xOffset = Math.round(isFitHorizontally ? normalizedElementRect.left : Math.max(0, cols * viewportWidth - elementRect.width)); const yOffset = Math.round(isFitVertically ? normalizedElementRect.top : Math.max(0, rows * viewportHeight - elementRect.height)); for (let row = 0; row < rows; row += 1) { for (let col = 0; col < cols; col += 1) { const dx = Math.min(viewportWidth * col + normalizedElementRect.left, Math.max(0, normalizedElementRect.right - viewportWidth)); const dy = Math.min(viewportHeight * row + normalizedElementRect.top, Math.max(0, normalizedElementRect.bottom - viewportHeight)); await this.#browser.executeScript(function (x, y) { window.scrollTo(x, y); }, dx, dy); screens.push(await this.#browser.takeScreenshot()); } } const images = screens.map((s) => Buffer.from(s, 'base64')).map((b) => pngjs_1.PNG.sync.read(b)); const compositeImage = new pngjs_1.PNG({ width: Math.round(elementRect.width), height: Math.round(elementRect.height) }); for (let y = 0; y < compositeImage.height; y += 1) { for (let x = 0; x < compositeImage.width; x += 1) { const col = Math.floor(x / viewportWidth); const row = Math.floor(y / viewportHeight); const isLastCol = cols - col == 1; const isLastRow = rows - row == 1; const scrollOffset = isFitVertically || isScreenshotWithoutScrollBar ? 0 : scrollBarWidth; const i = (y * compositeImage.width + x) * 4; const j = // NOTE compositeImage(x, y) => image(x, y) ((y % viewportHeight) * (viewportWidth + scrollOffset) + (x % viewportWidth)) * 4 + // NOTE Offset for last row/col image (isLastRow ? yOffset * (viewportWidth + scrollOffset) * 4 : 0) + (isLastCol ? xOffset * 4 : 0); const image = images[row * cols + col]; compositeImage.data[i + 0] = image.data[j + 0]; compositeImage.data[i + 1] = image.data[j + 1]; compositeImage.data[i + 2] = image.data[j + 2]; compositeImage.data[i + 3] = image.data[j + 3]; } } return pngjs_1.PNG.sync.write(compositeImage); } async removeIgnoreStyles(ignoreStyles) { if (ignoreStyles) { (0, logger_js_1.logger)().debug('Revert hiding ignored elements'); await this.#browser.executeScript(storybook_helpers_js_1.removeIgnoreStyles, ignoreStyles); } } // NOTE Firefox and Safari take viewport screenshot without scrollbars async hasScrollBar() { const browserName = (await this.#browser.getCapabilities()).getBrowserName(); const [browserVersion] = (await this.#browser.getCapabilities()).getBrowserVersion()?.split('.') ?? []; return (browserName != 'Safari' && // NOTE This need to work with keweb selenium grid !(browserName == 'firefox' && browserVersion == '61')); } async getScrollBarWidth() { const scrollBarWidth = await this.#browser.executeScript(function () { // eslint-disable-next-line no-var var div = document.createElement('div'); div.innerHTML = 'a'; // NOTE: In IE clientWidth is 0 if this div is empty. div.style.overflowY = 'scroll'; document.body.appendChild(div); // eslint-disable-next-line no-var var widthDiff = div.offsetWidth - div.clientWidth; document.body.removeChild(div); return widthDiff; }); return scrollBarWidth; } keepAlive() { this.#keepAliveInterval = setInterval(() => { // NOTE Simple way to keep session alive void this.#browser .getCurrentUrl() .then((url) => { (0, logger_js_1.logger)().debug('current url', chalk_1.default.magenta(url)); }) .catch((error) => { (0, logger_js_1.logger)().error(error); (0, messages_js_1.emitWorkerMessage)({ type: 'error', payload: { subtype: 'browser', error: 'Failed to ping browser' }, }); }); }, 10 * 1000); } } exports.InternalBrowser = InternalBrowser; //# sourceMappingURL=internal.js.map