import { chromium } from 'playwright' import fs from 'node:fs/promises' import path from 'node:path' const origin = process.env.PLUGIN_BROWSER_ORIGIN || 'http://127.0.0.1:18090' const entry = `${origin}/admin/` const outputDir = path.resolve(process.env.PLUGIN_SCREENSHOT_DIR || '.playwright-cli/plugin-admin') const email = process.env.PLUGIN_TEST_EMAIL || 'admin@example.com' const password = process.env.PLUGIN_TEST_PASSWORD || 'password' await fs.mkdir(outputDir, { recursive: true }) const launchOptions = { headless: true } if (process.env.PLAYWRIGHT_EXECUTABLE_PATH) launchOptions.executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH const browser = await chromium.launch(launchOptions) try { for (const width of [425, 900, 1440]) { const page = await browser.newPage({ viewport: { width, height: 900 }, deviceScaleFactor: 1 }) const responseLeaks = [] page.on('response', async (response) => { if (!response.headers()['content-type']?.includes('application/json')) return try { const body = await response.text() if (/access_token|refresh_token|admin[_-]?api[_-]?key|password|client_secret/i.test(body)) responseLeaks.push(response.url()) } catch (_) {} }) if (process.env.PLUGIN_EXPECT_CAPTCHA === 'turnstile') { // The fixture uses a deterministic SDK shim; production pages load the // provider SDK from the CSP allowlist. This keeps the browser contract // test network-independent while exercising the proof handoff. await page.addInitScript(() => { window.turnstile = { render(container, options) { const marker = document.createElement('span') marker.dataset.testid = 'turnstile-fixture' marker.textContent = 'fixture captcha' container.appendChild(marker) queueMicrotask(() => options.callback('MOCK-TURNSTILE-TOKEN')) return 'fixture-widget' }, reset() {}, remove() {}, } }) } await page.goto(entry, { waitUntil: 'networkidle' }) await page.locator('input[autocomplete="username"]').fill(email) await page.locator('input[autocomplete="current-password"]').fill(password) if (process.env.PLUGIN_EXPECT_CAPTCHA === 'turnstile') { await page.locator('[data-testid="turnstile-fixture"]').waitFor({ state: 'visible' }) } await page.getByRole('button', { name: '登录' }).click() await page.getByRole('heading', { name: '概览' }).waitFor() const assertViewport = async (label) => { const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth) if (overflow) throw new Error(`horizontal overflow at ${width}px (${label})`) const controls = await page.locator('button, .file-button, input[type="file"]').evaluateAll((items) => items.filter((item) => !item.classList.contains('visually-hidden') && item.getClientRects().length > 0 && getComputedStyle(item).visibility !== 'hidden').every((item) => item.getBoundingClientRect().height >= 28 && item.getBoundingClientRect().width >= 28)) if (!controls) throw new Error(`control collapsed at ${width}px (${label})`) } const visitView = async (view, heading) => { await page.evaluate((hash) => { window.location.hash = hash }, `#/${view}`) await page.getByRole('heading', { name: heading }).waitFor() if (!page.url().includes(`#/${view}`)) throw new Error(`hash route missing for ${view}: ${page.url()}`) await assertViewport(view) } await visitView('plugins', '已安装插件') await visitView('marketplace', '插件市场') await visitView('operations', '操作记录') // The optional subscription module is served by the same Shell. When the // fixture has it enabled, verify the deep link remains inside the unified // hash router instead of opening a second service/login surface. if (process.env.PLUGIN_EXPECT_SUBSCRIPTION === 'true') { await visitView('modules/subscription/overview', '订阅管理') if (!page.url().includes('#/modules/subscription/overview')) throw new Error(`subscription route escaped unified shell: ${page.url()}`) } // When the backend has installed plugins, exercise a detail route and its secondary tab. await visitView('plugins', '已安装插件') const detailTrigger = page.getByRole('button', { name: '查看详情' }).first() if (await detailTrigger.count()) { await detailTrigger.click() await page.getByRole('heading', { name: '插件详情' }).waitFor().catch(() => {}) const secondary = page.getByRole('tab', { name: '版本与升级' }) if (await secondary.count()) await secondary.click() await assertViewport('detail') } await page.reload({ waitUntil: 'networkidle' }) if (!page.url().includes('#')) throw new Error(`hash route lost after refresh: ${page.url()}`) await assertViewport('refresh') await page.waitForTimeout(50) if (responseLeaks.length) throw new Error(`sensitive response field exposed at ${width}px: ${responseLeaks.join(', ')}`) await page.screenshot({ path: path.join(outputDir, `plugin-admin-${width}.png`), fullPage: true }) await page.close() } } finally { await browser.close() }