|
- import fs from 'node:fs'
- import path from 'node:path'
-
- import { expect, test as base, type Locator, type Page, type TestInfo } from '@playwright/test'
-
- export const runId = (process.env.APE2E_RUN_ID || 'manual').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 80) || 'manual'
- export const runPrefix = `APE2E-${runId.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()}`
- const screenshotDirectory = path.resolve('artifacts', 'runs', runId, 'screenshots')
-
- const safeName = (value: string) => value.normalize('NFKC')
- .replace(/[^\p{L}\p{N}._-]+/gu, '-')
- .replace(/^-+|-+$/g, '')
- .slice(0, 120)
-
- const stableTestName = (testInfo: TestInfo) => {
- const fileStem = path.basename(testInfo.file, path.extname(testInfo.file))
- return safeName(`${testInfo.project.name}--${fileStem}--${testInfo.title}`).slice(0, 110) || 'unnamed-test'
- }
-
- export async function captureScreenshot(page: Page, testInfo: TestInfo, label: string, masks: Locator[] = []) {
- fs.mkdirSync(screenshotDirectory, { recursive: true })
- const artifactName = safeName(label).slice(0, 70) || 'success'
- const screenshotPath = path.join(screenshotDirectory, `${stableTestName(testInfo)}--${artifactName}.png`)
- await page.screenshot({
- path: screenshotPath,
- fullPage: true,
- animations: 'disabled',
- caret: 'hide',
- mask: masks,
- maskColor: '#1f2937',
- })
- await testInfo.attach(`success-${artifactName}`, { path: screenshotPath, contentType: 'image/png' })
- return screenshotPath
- }
-
- export async function attachJson(testInfo: TestInfo, name: string, value: unknown) {
- const serialized = JSON.stringify(value, (_key, item) => {
- if (typeof item === 'string' && /(?:Bearer\s+|eyJ)[A-Za-z0-9._~-]+/i.test(item)) return '<redacted>'
- return item
- }, 2)
- await testInfo.attach(name, { body: Buffer.from(serialized), contentType: 'application/json' })
- }
-
- export const test = base
-
- test.beforeEach(async ({}, testInfo) => {
- fs.mkdirSync(screenshotDirectory, { recursive: true })
- const prefix = `${stableTestName(testInfo)}--`
- for (const filename of fs.readdirSync(screenshotDirectory)) {
- if (filename.startsWith(prefix) && filename.endsWith('.png')) fs.rmSync(path.join(screenshotDirectory, filename), { force: true })
- }
- })
-
- test.afterEach(async ({ page }, testInfo) => {
- if (testInfo.status === testInfo.expectedStatus || page.isClosed()) return
- fs.mkdirSync(screenshotDirectory, { recursive: true })
- const filename = `${safeName(testInfo.titlePath.join('--')) || 'failed-test'}--retry-${testInfo.retry}--${Date.now()}.png`
- const screenshotPath = path.join(screenshotDirectory, filename)
- try {
- // 失败截图同样必须遵守敏感信息约束。部分密码框支持“显示密码”,
- // 因此除了 type=password,还要覆盖 autocomplete 标记和一次性密码结果区。
- const sensitiveControls = page.locator([
- 'input[type="password"]',
- 'input[autocomplete="current-password"]',
- 'input[autocomplete="new-password"]',
- '[data-testid="generated-password"]',
- '[data-testid="password-result-secret"]',
- // 远控终端创建/轮换后的密钥由 Element Plus 警告框仅展示一次。
- // 即使测试恰好在该警告框打开时失败,失败截图也不得带出完整密钥。
- '.el-message-box:has-text("接入密钥")',
- '[role="alertdialog"]:has-text("接入密钥")',
- ].join(', '))
- await page.screenshot({
- path: screenshotPath,
- fullPage: true,
- animations: 'disabled',
- caret: 'hide',
- mask: [sensitiveControls],
- maskColor: '#1f2937',
- })
- await testInfo.attach('failure-screenshot', { path: screenshotPath, contentType: 'image/png' })
- } catch (error) {
- await testInfo.attach('failure-screenshot-error', { body: Buffer.from(String(error)), contentType: 'text/plain' })
- }
- })
-
- export { expect }
|