chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,93 +1,93 @@
|
||||
import { BrowserWindow, nativeTheme } from "electron/main";
|
||||
import type { BrowserWindow as ElectronBrowserWindow, Session } from "electron/main";
|
||||
import type { Browser as PlaywrightBrowser, Page as PlaywrightPage } from "playwright-core";
|
||||
import type { BrowserWindow as ElectronBrowserWindow, Session } from 'electron/main'
|
||||
import { BrowserWindow, nativeTheme } from 'electron/main'
|
||||
import type { Browser as PlaywrightBrowser, Page as PlaywrightPage } from 'playwright-core'
|
||||
|
||||
import {
|
||||
isReclaimableHiddenPlaywrightTarget,
|
||||
type CDPTargetDescriptor,
|
||||
} from "./playwright-cdp-targets";
|
||||
import { STANDARD_USER_AGENT } from "./user-agent";
|
||||
} from './playwright-cdp-targets'
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
|
||||
const hiddenPlaywrightIdleTTLms = 5 * 60_000;
|
||||
const hiddenPlaywrightReaperIntervalMs = 60_000;
|
||||
const defaultConnectTimeoutMs = 10_000;
|
||||
const defaultPageTimeoutMs = 45_000;
|
||||
const hiddenBootstrapMetaName = "geo-hidden-playwright-token";
|
||||
const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10);
|
||||
const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`;
|
||||
const cdpTargetsListURL = `${cdpEndpointURL}/json/list`;
|
||||
const hiddenPlaywrightIdleTTLms = 5 * 60_000
|
||||
const hiddenPlaywrightReaperIntervalMs = 60_000
|
||||
const defaultConnectTimeoutMs = 10_000
|
||||
const defaultPageTimeoutMs = 45_000
|
||||
const hiddenBootstrapMetaName = 'geo-hidden-playwright-token'
|
||||
const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? '9339', 10)
|
||||
const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`
|
||||
const cdpTargetsListURL = `${cdpEndpointURL}/json/list`
|
||||
|
||||
interface HiddenPlaywrightRecord {
|
||||
key: string;
|
||||
accountId: string;
|
||||
title: string;
|
||||
targetURL: string;
|
||||
window: ElectronBrowserWindow;
|
||||
browser: PlaywrightBrowser;
|
||||
page: PlaywrightPage;
|
||||
retainCount: number;
|
||||
activatedAt: number;
|
||||
lastReleasedAt: number | null;
|
||||
key: string
|
||||
accountId: string
|
||||
title: string
|
||||
targetURL: string
|
||||
window: ElectronBrowserWindow
|
||||
browser: PlaywrightBrowser
|
||||
page: PlaywrightPage
|
||||
retainCount: number
|
||||
activatedAt: number
|
||||
lastReleasedAt: number | null
|
||||
}
|
||||
|
||||
export interface HiddenPlaywrightLease {
|
||||
accountId: string;
|
||||
browser: PlaywrightBrowser;
|
||||
page: PlaywrightPage;
|
||||
release(): Promise<void>;
|
||||
accountId: string
|
||||
browser: PlaywrightBrowser
|
||||
page: PlaywrightPage
|
||||
release(): Promise<void>
|
||||
}
|
||||
|
||||
const records = new Map<string, HiddenPlaywrightRecord>();
|
||||
let reaperHandle: ReturnType<typeof setInterval> | null = null;
|
||||
let connectPromise: Promise<PlaywrightBrowser> | null = null;
|
||||
let connectedBrowser: PlaywrightBrowser | null = null;
|
||||
const records = new Map<string, HiddenPlaywrightRecord>()
|
||||
let reaperHandle: ReturnType<typeof setInterval> | null = null
|
||||
let connectPromise: Promise<PlaywrightBrowser> | null = null
|
||||
let connectedBrowser: PlaywrightBrowser | null = null
|
||||
|
||||
export function getPlaywrightCDPPort(): number {
|
||||
return cdpPort;
|
||||
return cdpPort
|
||||
}
|
||||
|
||||
export function startHiddenPlaywrightReaper(): void {
|
||||
if (reaperHandle) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
reaperHandle = setInterval(() => {
|
||||
reapIdleHiddenPlaywrightPages();
|
||||
}, hiddenPlaywrightReaperIntervalMs);
|
||||
reaperHandle.unref?.();
|
||||
reapIdleHiddenPlaywrightPages()
|
||||
}, hiddenPlaywrightReaperIntervalMs)
|
||||
reaperHandle.unref?.()
|
||||
}
|
||||
|
||||
export async function retainHiddenPlaywrightPage(options: {
|
||||
accountId: string;
|
||||
session: Session;
|
||||
targetURL: string;
|
||||
title: string;
|
||||
accountId: string
|
||||
session: Session
|
||||
targetURL: string
|
||||
title: string
|
||||
}): Promise<HiddenPlaywrightLease> {
|
||||
startHiddenPlaywrightReaper();
|
||||
startHiddenPlaywrightReaper()
|
||||
|
||||
const key = hiddenPlaywrightKey(options.accountId, options.targetURL);
|
||||
const existing = records.get(key);
|
||||
const key = hiddenPlaywrightKey(options.accountId, options.targetURL)
|
||||
const existing = records.get(key)
|
||||
if (existing && !existing.window.isDestroyed() && !existing.page.isClosed()) {
|
||||
await ensureRecordTarget(existing, options.targetURL);
|
||||
existing.retainCount += 1;
|
||||
existing.activatedAt = Date.now();
|
||||
existing.lastReleasedAt = null;
|
||||
return createLease(existing);
|
||||
await ensureRecordTarget(existing, options.targetURL)
|
||||
existing.retainCount += 1
|
||||
existing.activatedAt = Date.now()
|
||||
existing.lastReleasedAt = null
|
||||
return createLease(existing)
|
||||
}
|
||||
|
||||
const browser = await connectBrowserOverCDP();
|
||||
const knownPages = new Set(defaultContext(browser).pages());
|
||||
const bootstrapToken = createHiddenBootstrapToken();
|
||||
const bootstrapURL = buildHiddenWindowBootstrapURL(options.title, bootstrapToken);
|
||||
const window = createHiddenWindow(options.title, options.session, bootstrapURL);
|
||||
const page = await waitForNewPage(browser, knownPages, bootstrapToken);
|
||||
const browser = await connectBrowserOverCDP()
|
||||
const knownPages = new Set(defaultContext(browser).pages())
|
||||
const bootstrapToken = createHiddenBootstrapToken()
|
||||
const bootstrapURL = buildHiddenWindowBootstrapURL(options.title, bootstrapToken)
|
||||
const window = createHiddenWindow(options.title, options.session, bootstrapURL)
|
||||
const page = await waitForNewPage(browser, knownPages, bootstrapToken)
|
||||
|
||||
page.setDefaultTimeout(defaultPageTimeoutMs);
|
||||
page.setDefaultNavigationTimeout(defaultPageTimeoutMs);
|
||||
page.setDefaultTimeout(defaultPageTimeoutMs)
|
||||
page.setDefaultNavigationTimeout(defaultPageTimeoutMs)
|
||||
await page.goto(options.targetURL, {
|
||||
timeout: defaultPageTimeoutMs,
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
waitUntil: 'domcontentloaded',
|
||||
})
|
||||
|
||||
const record: HiddenPlaywrightRecord = {
|
||||
key,
|
||||
@@ -100,31 +100,34 @@ export async function retainHiddenPlaywrightPage(options: {
|
||||
retainCount: 1,
|
||||
activatedAt: Date.now(),
|
||||
lastReleasedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
window.once("closed", () => {
|
||||
records.delete(key);
|
||||
});
|
||||
page.once("close", () => {
|
||||
records.delete(key);
|
||||
});
|
||||
window.once('closed', () => {
|
||||
records.delete(key)
|
||||
})
|
||||
page.once('close', () => {
|
||||
records.delete(key)
|
||||
})
|
||||
|
||||
records.set(key, record);
|
||||
return createLease(record);
|
||||
records.set(key, record)
|
||||
return createLease(record)
|
||||
}
|
||||
|
||||
export async function releaseHiddenPlaywrightPage(accountId: string, targetURL: string): Promise<void> {
|
||||
const key = hiddenPlaywrightKey(accountId, targetURL);
|
||||
const record = records.get(key);
|
||||
export async function releaseHiddenPlaywrightPage(
|
||||
accountId: string,
|
||||
targetURL: string,
|
||||
): Promise<void> {
|
||||
const key = hiddenPlaywrightKey(accountId, targetURL)
|
||||
const record = records.get(key)
|
||||
if (!record) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (record.retainCount > 0) {
|
||||
record.retainCount -= 1;
|
||||
record.retainCount -= 1
|
||||
}
|
||||
if (record.retainCount === 0) {
|
||||
record.lastReleasedAt = Date.now();
|
||||
record.lastReleasedAt = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,24 +135,24 @@ export function reapIdleHiddenPlaywrightPages(
|
||||
now = Date.now(),
|
||||
idleTTLms = hiddenPlaywrightIdleTTLms,
|
||||
): string[] {
|
||||
const reclaimed: string[] = [];
|
||||
const reclaimed: string[] = []
|
||||
|
||||
for (const [key, record] of records.entries()) {
|
||||
if (record.retainCount > 0) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
const idleSince = record.lastReleasedAt ?? record.activatedAt;
|
||||
const idleSince = record.lastReleasedAt ?? record.activatedAt
|
||||
if (now - idleSince < idleTTLms) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
closeRecord(record);
|
||||
records.delete(key);
|
||||
reclaimed.push(key);
|
||||
closeRecord(record)
|
||||
records.delete(key)
|
||||
reclaimed.push(key)
|
||||
}
|
||||
|
||||
return reclaimed;
|
||||
return reclaimed
|
||||
}
|
||||
|
||||
export function getHiddenPlaywrightSnapshot() {
|
||||
@@ -169,7 +172,7 @@ export function getHiddenPlaywrightSnapshot() {
|
||||
windowDestroyed: record.window.isDestroyed(),
|
||||
pageClosed: record.page.isClosed(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createLease(record: HiddenPlaywrightRecord): HiddenPlaywrightLease {
|
||||
@@ -178,7 +181,7 @@ function createLease(record: HiddenPlaywrightRecord): HiddenPlaywrightLease {
|
||||
browser: record.browser,
|
||||
page: record.page,
|
||||
release: () => releaseHiddenPlaywrightPage(record.accountId, record.targetURL),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createHiddenWindow(
|
||||
@@ -194,7 +197,7 @@ function createHiddenWindow(
|
||||
focusable: false,
|
||||
hiddenInMissionControl: true,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#15191a' : '#f4f1ea',
|
||||
title,
|
||||
webPreferences: {
|
||||
session,
|
||||
@@ -203,156 +206,159 @@ function createHiddenWindow(
|
||||
nodeIntegration: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
window.webContents.setUserAgent(STANDARD_USER_AGENT);
|
||||
void window.loadURL(bootstrapURL);
|
||||
return window;
|
||||
window.webContents.setUserAgent(STANDARD_USER_AGENT)
|
||||
void window.loadURL(bootstrapURL)
|
||||
return window
|
||||
}
|
||||
|
||||
function createHiddenBootstrapToken(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
return globalThis.crypto.randomUUID();
|
||||
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
||||
return globalThis.crypto.randomUUID()
|
||||
}
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("\"", """)
|
||||
.replaceAll("'", "'");
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function buildHiddenWindowBootstrapURL(title: string, token: string): string {
|
||||
const html = [
|
||||
"<!doctype html>",
|
||||
"<html lang=\"en\">",
|
||||
"<head>",
|
||||
" <meta charset=\"utf-8\">",
|
||||
'<!doctype html>',
|
||||
'<html lang="en">',
|
||||
'<head>',
|
||||
' <meta charset="utf-8">',
|
||||
` <meta name=\"${hiddenBootstrapMetaName}\" content=\"${escapeHtml(token)}\">`,
|
||||
` <title>${escapeHtml(title)} · hidden-playwright</title>`,
|
||||
"</head>",
|
||||
'</head>',
|
||||
` <body data-${hiddenBootstrapMetaName}=\"${escapeHtml(token)}\"></body>`,
|
||||
"</html>",
|
||||
].join("");
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
'</html>',
|
||||
].join('')
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
|
||||
}
|
||||
|
||||
async function ensureRecordTarget(record: HiddenPlaywrightRecord, targetURL: string): Promise<void> {
|
||||
record.targetURL = targetURL;
|
||||
async function ensureRecordTarget(
|
||||
record: HiddenPlaywrightRecord,
|
||||
targetURL: string,
|
||||
): Promise<void> {
|
||||
record.targetURL = targetURL
|
||||
|
||||
const currentURL = safePageURL(record.page);
|
||||
const currentURL = safePageURL(record.page)
|
||||
if (currentURL === targetURL) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
await record.page.goto(targetURL, {
|
||||
timeout: defaultPageTimeoutMs,
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
waitUntil: 'domcontentloaded',
|
||||
})
|
||||
}
|
||||
|
||||
async function connectBrowserOverCDP(): Promise<PlaywrightBrowser> {
|
||||
if (connectedBrowser?.isConnected()) {
|
||||
return connectedBrowser;
|
||||
return connectedBrowser
|
||||
}
|
||||
if (connectPromise) {
|
||||
return connectPromise;
|
||||
return connectPromise
|
||||
}
|
||||
|
||||
connectPromise = (async () => {
|
||||
await waitForCDPEndpoint();
|
||||
const { chromium } = await import("playwright-core");
|
||||
await pruneStaleCDPTargets();
|
||||
await waitForCDPEndpoint()
|
||||
const { chromium } = await import('playwright-core')
|
||||
await pruneStaleCDPTargets()
|
||||
|
||||
try {
|
||||
return await openBrowserOverCDP(chromium);
|
||||
return await openBrowserOverCDP(chromium)
|
||||
} catch (error) {
|
||||
if (!isConnectOverCDPTimeout(error)) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
const prunedTargetCount = await pruneStaleCDPTargets();
|
||||
const prunedTargetCount = await pruneStaleCDPTargets()
|
||||
if (prunedTargetCount <= 0) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
return openBrowserOverCDP(chromium);
|
||||
return openBrowserOverCDP(chromium)
|
||||
}
|
||||
})();
|
||||
})()
|
||||
|
||||
try {
|
||||
return await connectPromise;
|
||||
return await connectPromise
|
||||
} finally {
|
||||
if (!connectedBrowser?.isConnected()) {
|
||||
connectPromise = null;
|
||||
connectPromise = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForCDPEndpoint(timeoutMs = defaultConnectTimeoutMs): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(`${cdpEndpointURL}/json/version`);
|
||||
const response = await fetch(`${cdpEndpointURL}/json/version`)
|
||||
if (response.ok) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Ignore and continue polling.
|
||||
}
|
||||
|
||||
await wait(200);
|
||||
await wait(200)
|
||||
}
|
||||
|
||||
throw new Error("playwright_cdp_endpoint_unavailable");
|
||||
throw new Error('playwright_cdp_endpoint_unavailable')
|
||||
}
|
||||
|
||||
async function openBrowserOverCDP(
|
||||
chromium: typeof import("playwright-core").chromium,
|
||||
chromium: typeof import('playwright-core').chromium,
|
||||
): Promise<PlaywrightBrowser> {
|
||||
const browser = await chromium.connectOverCDP(cdpEndpointURL, {
|
||||
timeout: defaultConnectTimeoutMs,
|
||||
});
|
||||
})
|
||||
|
||||
browser.on("disconnected", () => {
|
||||
connectedBrowser = null;
|
||||
connectPromise = null;
|
||||
});
|
||||
browser.on('disconnected', () => {
|
||||
connectedBrowser = null
|
||||
connectPromise = null
|
||||
})
|
||||
|
||||
connectedBrowser = browser;
|
||||
return browser;
|
||||
connectedBrowser = browser
|
||||
return browser
|
||||
}
|
||||
|
||||
async function pruneStaleCDPTargets(): Promise<number> {
|
||||
let targets: CDPTargetDescriptor[] = [];
|
||||
let targets: CDPTargetDescriptor[] = []
|
||||
try {
|
||||
const response = await fetch(cdpTargetsListURL);
|
||||
const response = await fetch(cdpTargetsListURL)
|
||||
if (!response.ok) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
const payload = await response.json();
|
||||
const payload = await response.json()
|
||||
if (Array.isArray(payload)) {
|
||||
targets = payload as CDPTargetDescriptor[];
|
||||
targets = payload as CDPTargetDescriptor[]
|
||||
}
|
||||
} catch {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
|
||||
let closedCount = 0;
|
||||
let closedCount = 0
|
||||
for (const target of targets) {
|
||||
if (!isReclaimableHiddenPlaywrightTarget(target)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const closeURL = `${cdpEndpointURL}/json/close/${encodeURIComponent(target.id ?? "")}`;
|
||||
const response = await fetch(closeURL);
|
||||
const closeURL = `${cdpEndpointURL}/json/close/${encodeURIComponent(target.id ?? '')}`
|
||||
const response = await fetch(closeURL)
|
||||
if (response.ok) {
|
||||
closedCount += 1;
|
||||
closedCount += 1
|
||||
}
|
||||
} catch {
|
||||
// Ignore best-effort stale target cleanup failures.
|
||||
@@ -360,15 +366,17 @@ async function pruneStaleCDPTargets(): Promise<number> {
|
||||
}
|
||||
|
||||
if (closedCount > 0) {
|
||||
await wait(150);
|
||||
await wait(150)
|
||||
}
|
||||
return closedCount;
|
||||
return closedCount
|
||||
}
|
||||
|
||||
function isConnectOverCDPTimeout(error: unknown): boolean {
|
||||
return error instanceof Error
|
||||
&& error.name === "TimeoutError"
|
||||
&& error.message.includes("connectOverCDP");
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.name === 'TimeoutError' &&
|
||||
error.message.includes('connectOverCDP')
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForNewPage(
|
||||
@@ -376,87 +384,87 @@ async function waitForNewPage(
|
||||
knownPages: Set<PlaywrightPage>,
|
||||
bootstrapToken: string,
|
||||
): Promise<PlaywrightPage> {
|
||||
const context = defaultContext(browser);
|
||||
const startedAt = Date.now();
|
||||
const context = defaultContext(browser)
|
||||
const startedAt = Date.now()
|
||||
|
||||
while (Date.now() - startedAt < defaultPageTimeoutMs) {
|
||||
const nextPages = context.pages().filter((page) => !knownPages.has(page));
|
||||
const nextPages = context.pages().filter((page) => !knownPages.has(page))
|
||||
for (const nextPage of nextPages) {
|
||||
if (isDevtoolsPage(nextPage)) {
|
||||
knownPages.add(nextPage);
|
||||
continue;
|
||||
knownPages.add(nextPage)
|
||||
continue
|
||||
}
|
||||
if (await matchesHiddenBootstrapPage(nextPage, bootstrapToken)) {
|
||||
return nextPage;
|
||||
return nextPage
|
||||
}
|
||||
}
|
||||
await wait(100);
|
||||
await wait(100)
|
||||
}
|
||||
|
||||
throw new Error("playwright_hidden_page_unavailable");
|
||||
throw new Error('playwright_hidden_page_unavailable')
|
||||
}
|
||||
|
||||
async function matchesHiddenBootstrapPage(
|
||||
page: PlaywrightPage,
|
||||
bootstrapToken: string,
|
||||
): Promise<boolean> {
|
||||
const url = safePageURL(page);
|
||||
const url = safePageURL(page)
|
||||
if (url.includes(bootstrapToken) || url.includes(encodeURIComponent(bootstrapToken))) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
return await page.evaluate(
|
||||
({ token, metaName }) => {
|
||||
const metaToken =
|
||||
document.querySelector(`meta[name="${metaName}"]`)?.getAttribute("content")
|
||||
?? document.body?.getAttribute(`data-${metaName}`)
|
||||
?? null;
|
||||
return metaToken === token;
|
||||
document.querySelector(`meta[name="${metaName}"]`)?.getAttribute('content') ??
|
||||
document.body?.getAttribute(`data-${metaName}`) ??
|
||||
null
|
||||
return metaToken === token
|
||||
},
|
||||
{
|
||||
token: bootstrapToken,
|
||||
metaName: hiddenBootstrapMetaName,
|
||||
},
|
||||
);
|
||||
)
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function defaultContext(browser: PlaywrightBrowser) {
|
||||
const context = browser.contexts()[0];
|
||||
const context = browser.contexts()[0]
|
||||
if (!context) {
|
||||
throw new Error("playwright_default_context_unavailable");
|
||||
throw new Error('playwright_default_context_unavailable')
|
||||
}
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
function hiddenPlaywrightKey(accountId: string, targetURL: string): string {
|
||||
return `${accountId}::${targetURL}`;
|
||||
return `${accountId}::${targetURL}`
|
||||
}
|
||||
|
||||
function safePageURL(page: PlaywrightPage): string {
|
||||
try {
|
||||
return page.url();
|
||||
return page.url()
|
||||
} catch {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function isDevtoolsPage(page: PlaywrightPage): boolean {
|
||||
const url = safePageURL(page);
|
||||
return url.startsWith("devtools://");
|
||||
const url = safePageURL(page)
|
||||
return url.startsWith('devtools://')
|
||||
}
|
||||
|
||||
function closeRecord(record: HiddenPlaywrightRecord): void {
|
||||
if (!record.window.isDestroyed()) {
|
||||
record.window.close();
|
||||
record.window.close()
|
||||
}
|
||||
}
|
||||
|
||||
function wait(timeMs: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, timeMs);
|
||||
});
|
||||
setTimeout(resolve, timeMs)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user