Why: Complete the authorized export entry while preserving current main project and session behavior. Evidence: SELF full frontend tests and build, 263 unit tests; deferred browser races reproduced then all 6 passed. Raw records stay outside source until Dev task closure. Impact: Export callbacks are invalidated on project, identity and unmount changes. Command readiness and current main security paths remain intact. Refs: Dev PR #488; Frontend PR #73; user takeover authorization on 2026-10-14
108 lines
6.1 KiB
TypeScript
108 lines
6.1 KiB
TypeScript
// Isolated browser component regression: actual React workspace, deliberately deferred API responses.
|
|
// This checks client request ownership, not server authorization or business acceptance.
|
|
import { test, expect, type Page } from '@playwright/test'
|
|
|
|
test.skip(!process.env.PLAYWRIGHT_COMPONENT_BASE_URL, 'requires the isolated Vite component test server')
|
|
test.use({ baseURL: process.env.PLAYWRIGHT_COMPONENT_BASE_URL })
|
|
|
|
async function mount(page: Page, delayed: string) {
|
|
const index = await page.request.get('/')
|
|
const html = (await index.text()).replace(/<script[^>]+src="\/src\/main\.tsx[^" ]*"[^>]*><\/script>/, '')
|
|
// Let Vite prepare ReactDOM before requesting its optimized module directly.
|
|
await page.request.get('/src/main.tsx')
|
|
await page.route('**/__export_test', route => route.fulfill({ contentType: 'text/html',
|
|
body: html }))
|
|
await page.goto('/__export_test')
|
|
await page.waitForFunction(() => typeof (window as any).$RefreshReg$ === 'function')
|
|
await page.evaluate(async delayed => {
|
|
const load = (path: string) => import(/* @vite-ignore */ path)
|
|
const { default: React } = await load('/node_modules/.vite/deps/react.js')
|
|
const { default: ReactDOM } = await load('/node_modules/.vite/deps/react-dom_client.js')
|
|
const { RdmAuditWorkspace } = await load('/src/components/RdmAuditWorkspace.tsx')
|
|
const { api } = await load('/src/api.ts')
|
|
const { auth } = await load('/src/auth.ts')
|
|
const { store } = await load('/src/store.ts')
|
|
const { RDM_AUDIT_COMMANDS, RDM_EXPORT_COMMANDS } = await load('/src/workspaces.ts')
|
|
const calls: { command: string; params: any }[] = []
|
|
let actor = 'alice'
|
|
const pending: (() => void)[] = []
|
|
let downloads = 0
|
|
URL.createObjectURL = () => { downloads++; return 'blob:export-test' }
|
|
URL.revokeObjectURL = () => {}
|
|
HTMLAnchorElement.prototype.click = () => {}
|
|
api.listPlugins = async () => []
|
|
api.listWorkspaces = async () => []
|
|
api.listCommands = async () => [...RDM_AUDIT_COMMANDS, ...RDM_EXPORT_COMMANDS].map(id => ({ id, enabled: true }))
|
|
api.adminZoneSections = async () => []
|
|
api.getSettings = async () => ({ user: {} })
|
|
api.rdmCommand = async (command: string, params: any = {}) => {
|
|
calls.push({ command, params })
|
|
const result = command === 'rdm.inspect.capabilities'
|
|
? { actor, crossProject: false, projects: ['A', 'B'].map(projectId => ({ projectId, name: actor + projectId, canAudit: true })) }
|
|
: command === 'rdm.inspect.summary'
|
|
? { projectId: params.projectId, counts: {}, watermark: 7 }
|
|
: command === 'rdm.inspect.export.create'
|
|
? { exportId: params.projectId + '-report', watermark: 7, sha256: 'abc', fileName: 'report.docx' }
|
|
: command === 'rdm.inspect.export.status'
|
|
? { expiresAt: '2099-01-01', watermark: 7, sha256: 'abc' }
|
|
: { fileName: 'report.docx', mime: 'application/octet-stream', dataB64: 'QQ==', sha256: 'abc' }
|
|
if (command === delayed && params.projectId === 'A') {
|
|
await new Promise<void>(resolve => pending.push(resolve))
|
|
}
|
|
return result
|
|
}
|
|
await store.refreshPlugins()
|
|
const root = ReactDOM.createRoot(document.getElementById('root')!)
|
|
root.render(React.createElement(RdmAuditWorkspace))
|
|
;(window as any).exportTest = { calls, release: () => pending.splice(0).forEach(resolve => resolve()),
|
|
downloads: () => downloads, identity: async () => { actor = 'bob'; auth.beginSession(); await store.refreshPlugins() },
|
|
unmount: () => root.unmount() }
|
|
}, delayed)
|
|
await expect(page.locator('.rdm-audit-export')).toBeVisible()
|
|
}
|
|
async function release(page: Page) {
|
|
await page.evaluate(async () => { (window as any).exportTest.release(); await new Promise(resolve => setTimeout(resolve, 0)) })
|
|
}
|
|
const generate = (page: Page) => page.getByRole('button', { name: '生成', exact: true }).click()
|
|
|
|
for (const stage of ['create', 'status']) {
|
|
test(`ignores old ${stage} response after project A -> B -> A`, async ({ page }) => {
|
|
await mount(page, 'rdm.inspect.export.' + stage)
|
|
await generate(page)
|
|
await expect.poll(() => page.evaluate(stage => (window as any).exportTest.calls
|
|
.some((c: any) => c.command === 'rdm.inspect.export.' + stage), stage)).toBe(true)
|
|
await page.getByLabel('选择项目').selectOption('B')
|
|
await expect(page.locator('.rdm-audit-export')).toBeVisible()
|
|
await page.getByLabel('选择项目').selectOption('A')
|
|
await expect(page.locator('.rdm-audit-export')).toBeVisible()
|
|
await release(page)
|
|
await expect(page.getByRole('button', { name: '下载', exact: true })).toHaveCount(0)
|
|
if (stage === 'create') expect(await page.evaluate(() => (window as any).exportTest.calls
|
|
.filter((c: any) => c.command === 'rdm.inspect.export.status').length)).toBe(0)
|
|
await expect(page.getByRole('button', { name: '生成', exact: true })).toBeEnabled()
|
|
})
|
|
}
|
|
for (const change of ['project', 'identity', 'unmount']) {
|
|
test(`suppresses delayed download after ${change} change`, async ({ page }) => {
|
|
await mount(page, 'rdm.inspect.export.download')
|
|
await generate(page)
|
|
await page.getByRole('button', { name: '下载', exact: true }).click()
|
|
if (change === 'project') {
|
|
await page.getByLabel('选择项目').selectOption('B')
|
|
await expect(page.locator('.rdm-audit-export')).toBeVisible()
|
|
} else if (change === 'identity') {
|
|
await page.evaluate(() => (window as any).exportTest.identity())
|
|
await expect(page.getByLabel('选择项目').locator('option').first()).toHaveText('bobA')
|
|
} else await page.evaluate(() => (window as any).exportTest.unmount())
|
|
await release(page)
|
|
expect(await page.evaluate(() => (window as any).exportTest.downloads())).toBe(0)
|
|
})
|
|
}
|
|
test('current identity can generate and download normally', async ({ page }) => {
|
|
await mount(page, '')
|
|
await generate(page)
|
|
await page.getByRole('button', { name: '下载', exact: true }).click()
|
|
await expect.poll(() => page.evaluate(() => (window as any).exportTest.downloads())).toBe(1)
|
|
await expect(page.locator('.rdm-audit-export-result')).toContainText('水位 7')
|
|
})
|