Files
creator-hub/web/src/AuditPage.test.jsx
T
rogee 7ecad93821 refactor(web): migrate frontend to React 19 + Refine v5 + shadcn/ui stack
技术栈切换(按 AGENTS.md 批准栈):react-admin/MUI → Refine v5 + shadcn/ui
(Tailwind CSS v4),Vite 8 + React 19 + react-router v7 hash 路由。

- 全部 9 个页面重写:AccountsPage/BrowsersPage(BrowserList/CreatePage/Detail)/
  BrowserImagesPage/GatewaysPage/NetworkExitsPage/TasksPage/TracePages/
  DraftPage/AuditPage;自建 lib/ui.jsx 组件库与 Layout(侧导航 + Outlet)
- 简单创建/编辑改 modal(账号/网关/镜像/出口),环境创建走独立页
  /browsers/new;列表按信息密度选表格或移动卡片双形态
- 图标统一 RemixIcon;领域动作保持显式动词(start/stop/upgrade/recycle
  等,不伪装成 CRUD update)
- LoginPage:Basic 凭证存 localStorage;401 清凭证并跳 #/login;
  AuthGate 改为渲染期读 localStorage(react-router v7 的 pushState 不触发
  hashchange,事件订阅会在导航后拿到过期状态导致登录后卡死)
- dataProvider 适配 Refine v5 契约:所有方法收单个参数对象
  ({resource, pagination, filters, meta, variables}),create/update/
  delete/deleteOne 语义对齐
- Modal 关闭时焦点还给触发元素;Select 保持原生 button/combobox 语义
- e2e 补登录前置;账号创建流程同步 modal 交互

验证:npm test 65/65 通过;覆盖率 71.5% stmts / 76.3% lines(门槛 65%);
vite build 成功;playwright e2e 9/9 通过。
2026-09-02 13:40:34 +08:00

63 lines
3.5 KiB
React

import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Refine } from '@refinedev/core'
import { MemoryRouter } from 'react-router'
import { AuditPage, auditCSV } from './AuditPage'
afterEach(() => { cleanup(); vi.restoreAllMocks() })
const httpQuery = () => new QueryClient({ defaultOptions: { queries: { retry: false } } })
function renderAudit(dataProvider, initialEntry = '/audit?task_id=task-a') {
return render(
<QueryClientProvider client={httpQuery()}>
<Refine dataProvider={dataProvider}>
<MemoryRouter initialEntries={[initialEntry]}><AuditPage /></MemoryRouter>
</Refine>
</QueryClientProvider>,
)
}
describe('AuditPage', () => {
it('deep-links safe correlation fields and exposes no mutation or secret details', async () => {
const dataProvider = {
getList: vi.fn().mockResolvedValue({ data: [{
id: 7, event_type: 'task_verified', account_id: 'account-a', task_id: 'task-a', attempt_id: 'attempt-a',
browser_env_alias: 'env-a', network_exit_id: 'exit-a', binding_version: 4, reason_code: 'manual_verification_recorded',
details: { token: 'must-not-render' }, created_at: '2026-08-30T00:00:00Z',
}], total: 1 }),
getOne: vi.fn(), getMany: vi.fn(), getManyReference: vi.fn(), create: vi.fn(), update: vi.fn(), updateMany: vi.fn(), delete: vi.fn(), deleteMany: vi.fn(),
}
renderAudit(dataProvider)
expect((await screen.findAllByRole('link', { name: 'task-a' })).length).toBeGreaterThan(0)
expect(screen.getAllByRole('link', { name: 'account-a' })[0].getAttribute('href')).toBe('/accounts/account-a')
expect(screen.getAllByRole('link', { name: 'attempt-a' })[0].getAttribute('href')).toBe('/attempts/attempt-a')
expect(screen.getAllByRole('link', { name: 'env-a' })[0].getAttribute('href')).toBe('/browsers/env-a')
expect(screen.getAllByRole('link', { name: 'exit-a' })[0].getAttribute('href')).toBe('/network-exits/exit-a')
expect(screen.queryByText('must-not-render')).toBeNull()
expect(screen.queryByRole('button', { name: /编辑|删除/ })).toBeNull()
})
it('exports only allowlisted correlation columns and requests the next page', async () => {
const event = {
id: 7, event_type: 'task_verified', task_id: 'task-a', attempt_id: 'attempt-a', browser_env_alias: 'env-a', network_exit_id: 'exit-a',
details: { api_key: 'api-secret', private_key: 'private-secret', credential_key: 'credential-secret', authorization_header: 'auth-secret', proxy_url: 'proxy-secret' },
created_at: '2026-08-30T00:00:00Z',
}
const csv = auditCSV([event])
expect(csv).toContain('attempt-a')
for (const secret of ['api-secret', 'private-secret', 'credential-secret', 'auth-secret', 'proxy-secret']) expect(csv).not.toContain(secret)
const getList = vi.fn().mockImplementation(() => Promise.resolve({ data: [{ ...event, id: 7 }], total: 26 }))
const dataProvider = {
getList, getOne: vi.fn(), getMany: vi.fn(), getManyReference: vi.fn(), create: vi.fn(), update: vi.fn(), updateMany: vi.fn(), delete: vi.fn(), deleteMany: vi.fn(),
}
renderAudit(dataProvider, '/audit')
fireEvent.click(await screen.findByRole('button', { name: '下一页' }))
await waitFor(() => expect(getList.mock.calls.some(([{ pagination }]) => (pagination?.currentPage || pagination?.page) === 2)).toBe(true))
})
})