// 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(/]+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(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') })