mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
import {mkdtemp, readFile, rm} from 'node:fs/promises'
|
|
import {tmpdir} from 'node:os'
|
|
import {join, resolve} from 'node:path'
|
|
import {chromium} from 'playwright-core'
|
|
import {resolveChromiumPath} from './resolve-chromium.mjs'
|
|
|
|
const root = resolve(import.meta.dirname, '..')
|
|
|
|
export async function extensionRequest(page, action, payload = {}) {
|
|
return page.evaluate(async ({requestAction, requestPayload}) => {
|
|
const response = await chrome.runtime.sendMessage({action: requestAction, payload: requestPayload})
|
|
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
|
|
return response.data
|
|
}, {requestAction: action, requestPayload: payload})
|
|
}
|
|
|
|
export async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
|
|
const deadline = Date.now() + timeoutMs
|
|
let value
|
|
while (Date.now() < deadline) {
|
|
value = await extensionRequest(page, action, payload)
|
|
if (predicate(value)) return value
|
|
await page.waitForTimeout(150)
|
|
}
|
|
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
|
|
}
|
|
|
|
export async function launchBrowserAgentContractHarness({
|
|
profilePrefix,
|
|
targetURL,
|
|
extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise'),
|
|
}) {
|
|
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
|
|
const executablePath = await resolveChromiumPath()
|
|
const userDataDir = await mkdtemp(join(tmpdir(), profilePrefix))
|
|
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
executablePath,
|
|
headless: true,
|
|
viewport: {width: 1280, height: 760},
|
|
args: [
|
|
`--disable-extensions-except=${extensionPath}`,
|
|
`--load-extension=${extensionPath}`,
|
|
'--no-first-run',
|
|
'--no-default-browser-check',
|
|
],
|
|
})
|
|
try {
|
|
let serviceWorker = context.serviceWorkers()[0]
|
|
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', {timeout: 15_000})
|
|
const extensionId = new URL(serviceWorker.url()).host
|
|
|
|
if (manifest.permissions?.includes('userScripts')) {
|
|
const extensionsPage = await context.newPage()
|
|
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
|
|
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
|
|
await toggle.waitFor({state: 'visible', timeout: 10_000})
|
|
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
|
|
await extensionsPage.close()
|
|
}
|
|
|
|
const targetPage = await context.newPage()
|
|
targetPage.on('dialog', (dialog) => void dialog.dismiss())
|
|
await targetPage.goto(targetURL)
|
|
|
|
const controlPage = await context.newPage()
|
|
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
|
|
const tabId = await controlPage.evaluate(async (url) => {
|
|
const tabs = await chrome.tabs.query({})
|
|
return tabs.find((tab) => tab.url === url)?.id
|
|
}, targetPage.url())
|
|
if (!tabId) throw new Error(`Could not resolve target tab ${targetPage.url()}`)
|
|
|
|
return {
|
|
context,
|
|
controlPage,
|
|
extensionId,
|
|
tabId,
|
|
targetPage,
|
|
async close() {
|
|
await context.close().catch(() => undefined)
|
|
await rm(userDataDir, {recursive: true, force: true})
|
|
},
|
|
}
|
|
} catch (error) {
|
|
await context.close().catch(() => undefined)
|
|
await rm(userDataDir, {recursive: true, force: true})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export function transformedFetchOptions(execution, originalHeaders) {
|
|
const headers = new Map(originalHeaders.map((header) => [header.name.toLowerCase(), {
|
|
name: header.name,
|
|
value: header.value,
|
|
}]))
|
|
for (const name of execution.removeHeaders || []) headers.delete(name.toLowerCase())
|
|
for (const header of execution.setHeaders || []) {
|
|
headers.set(header.name.toLowerCase(), {name: header.name, value: header.value})
|
|
}
|
|
return {
|
|
method: 'POST',
|
|
headers: Object.fromEntries([...headers.values()].map((header) => [header.name, header.value])),
|
|
body: Buffer.from(execution.bodyBase64, 'base64'),
|
|
}
|
|
}
|