UNPKG

web-snaps

Version:

Browser automation with automatic snapshotting.

132 lines (131 loc) 5.84 kB
import { mkdir } from 'node:fs/promises'; import { chromium } from 'rebrowser-playwright'; /** * Initialize a browser and browser context with scripts inserted for handling elements with closed * Shadow DOMs. * * @category Internal */ export async function initBrowser({ userDataDirPath, storeKey, options = {}, }) { await mkdir(userDataDirPath, { recursive: true }); /** WebKit is typically faster but `rebrowser-playwright` seems to only work with Chromium. */ const browserContext = await chromium.launchPersistentContext(userDataDirPath, options); try { browserContext.setDefaultTimeout(10_000); await browserContext.addInitScript((storeKey) => { /** https://github.com/evanw/esbuild/issues/2605#issuecomment-2146054255 */ globalThis.__name = (func) => func; const dataStore = { closedShadows: new Map(), queryThroughShadow, }; function getShadowRoot(node) { return node instanceof HTMLElement && node.shadowRoot ? node.shadowRoot : dataStore.closedShadows.get(node); } /** * Perform * [`.querySelector()`](https://developer.mozilla.org/docs/Web/API/Document/querySelector) * on the given element with support for elements that contain an open Shadow Root. * * @category Web : Elements * @category Package : @augment-vir/web * @package [`@augment-vir/web`](https://www.npmjs.com/package/@augment-vir/web) */ function queryThroughShadow(element, rawQuery, options = {}) { if (!rawQuery) { if (element instanceof Element) { return element; } else { return element.host; } } const query = typeof rawQuery === 'string' ? rawQuery : rawQuery.tagName; const splitQuery = query.split(' ').filter((value) => !!value); const shadowRoot = getShadowRoot(element); if (splitQuery.length > 1) { return handleNestedQueries(element, query, options, splitQuery); } else if (shadowRoot) { return queryThroughShadow(shadowRoot, query, options); } const shadowRootChildren = getShadowRootChildren(element); if (options.all) { const outerResults = Array.from(element.querySelectorAll(query)); const nestedResults = shadowRootChildren.flatMap((shadowRootChild) => { return queryThroughShadow(shadowRootChild, query, options); }); return [ ...outerResults, ...nestedResults, ]; } else { const basicResult = element.querySelector(query); if (basicResult) { return basicResult; } else { for (const shadowRootChild of shadowRootChildren) { const nestedResult = queryThroughShadow(shadowRootChild, query, options); if (nestedResult) { return nestedResult; } } return undefined; } } } function getShadowRootChildren(element) { return Array.from(element.querySelectorAll('*')) .map((child) => getShadowRoot(child)) .filter((value) => !!value); } function handleNestedQueries(element, originalQuery, options, queries) { const firstQuery = queries[0]; /** * No way to intentionally trigger this edge case, we're just catching it here for * type purposes. */ /* node:coverage ignore next 7 */ if (!firstQuery) { throw new Error(`Somehow the first query was empty in '[${queries.join(',')}]' for query '${JSON.stringify(originalQuery)}'`); } const results = queryThroughShadow(element, firstQuery, options); if (queries.length <= 1) { return results; } if (Array.isArray(results)) { return results .flatMap((result) => { return handleNestedQueries(result, originalQuery, options, queries.slice(1)); }) .filter((value) => !!value); } else if (results) { return handleNestedQueries(results, originalQuery, options, queries.slice(1)); } else { return undefined; } } globalThis[storeKey] = dataStore; // eslint-disable-next-line @typescript-eslint/unbound-method const original = Element.prototype.attachShadow; Element.prototype.attachShadow = function (init) { const shadow = original.call(this, init); if (init.mode === 'closed') { dataStore.closedShadows.set(this, shadow); } return shadow; }; }, storeKey); return { browserContext }; } catch (error) { await browserContext.close(); throw error; } }