feat: complete unified plugin admin v1.1.0
This commit is contained in:
@@ -9,7 +9,9 @@ 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 browser = await chromium.launch({ headless: 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 })
|
||||
@@ -21,15 +23,72 @@ try {
|
||||
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.getByLabel('邮箱').fill(email)
|
||||
await page.getByLabel('密码').fill(password)
|
||||
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 overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth)
|
||||
if (overflow) throw new Error(`horizontal overflow at ${width}px`)
|
||||
const buttons = await page.locator('button, .file-button').evaluateAll((items) => items.filter((item) => item.getClientRects().length > 0 && getComputedStyle(item).visibility !== 'hidden').every((item) => item.getBoundingClientRect().height >= 28 && item.getBoundingClientRect().width >= 28))
|
||||
if (!buttons) throw new Error(`control collapsed at ${width}px`)
|
||||
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 })
|
||||
|
||||
Reference in New Issue
Block a user