Files
cms-client/tests/e2e/admin-events.spec.ts
Noa Virellia 96453ae197
All checks were successful
Client Prod Build (NixCN CMS) TeamCity build finished
Client Check Build (NixCN CMS) TeamCity build finished
feat: add pagination and KYC detail modal to attendance page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:53:31 +08:00

439 lines
14 KiB
TypeScript

import { expect } from '@playwright/test';
import { test } from './helpers/fixtures';
import { mock } from './helpers/mock';
// ─── Shared mock data ────────────────────────────────────────────────────────
const adminEvent = {
event_id: 'adm1',
name: '管理员测试活动',
subtitle: '副标题',
type: 'party',
enable_kyc: false,
is_joined: false,
is_checked_in: false,
start_time: '2026-06-01T09:00:00Z',
end_time: '2026-06-02T18:00:00Z',
owner: '1'
};
const adminEvent2 = {
...adminEvent,
event_id: 'adm2',
name: '第二个活动',
owner: 'other-user'
};
async function overrideEventList(items: (typeof adminEvent)[]) {
await mock.override('GET', '/event/list', {
status: 200,
body: { status: 200, data: { items, total: items.length } }
});
}
async function overrideEventInfo(ev = adminEvent) {
await mock.override('GET', '/event/info', {
status: 200,
body: { status: 200, data: ev }
});
}
async function overrideEventGuide(guide = '') {
await mock.override('GET', '/event/guide', {
status: 200,
body: { status: 200, data: { attendance_guide: guide } }
});
}
// ─── Tests ───────────────────────────────────────────────────────────────────
test('permission gate blocks Lv10 user', async ({ page, loggedInUser }) => {
void loggedInUser; // Lv10 by default
await page.goto('/app/admin/events');
await page.waitForLoadState('networkidle');
await expect(page.getByText(/权限不足/)).toBeVisible();
});
test('admin list shows events for Lv30 user', async ({ page, loggedInUser }) => {
// Override /user/info to return a Lv30 user before navigating
await mock.override('GET', '/user/info', {
status: 200,
body: { status: 200, data: { ...loggedInUser, permission_level: 30 } }
});
await overrideEventList([adminEvent]);
await page.goto('/app/admin/events');
await page.waitForLoadState('networkidle');
await expect(page.getByText('管理员测试活动')).toBeVisible();
await expect(page.getByRole('link', { name: /新建活动/ })).toBeVisible();
});
test('Lv40 admin sees all events', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventList([adminEvent, adminEvent2]);
await page.goto('/app/admin/events');
await page.waitForLoadState('networkidle');
await expect(page.getByText('管理员测试活动')).toBeVisible();
await expect(page.getByText('第二个活动')).toBeVisible();
});
test('create event page renders form with required fields', async ({ page, superAdminUser }) => {
void superAdminUser;
await page.goto('/app/admin/events/new');
await page.waitForLoadState('networkidle');
// Verify the create form is rendered with all required fields
await expect(page.getByRole('heading', { name: '新建活动' })).toBeVisible();
await expect(page.locator('input[name="name"]')).toBeVisible();
await expect(page.locator('input[name="subtitle"]')).toBeVisible();
await expect(page.locator('input[name="start_time"]')).toBeVisible();
await expect(page.locator('input[name="end_time"]')).toBeVisible();
await expect(page.getByRole('button', { name: '保存' })).toBeVisible();
await expect(page.getByRole('link', { name: '← 返回列表' })).toBeVisible();
});
test('create event redirects to event list', async ({ page, superAdminUser }) => {
void superAdminUser;
await mock.override('POST', '/event/create', {
status: 200,
body: { status: 200, data: { event_id: 'new1' } }
});
await overrideEventList([]);
await page.goto('/app/admin/events/new');
await page.waitForLoadState('networkidle');
await page.locator('input[name="name"]').fill('测试活动');
await page.locator('input[name="subtitle"]').fill('副标题');
await page.locator('input[name="start_time"]').fill('2026-07-01T09:00');
await page.locator('input[name="end_time"]').fill('2026-07-01T18:00');
await page.getByRole('button', { name: '保存' }).click();
await page.waitForURL('**/admin/events');
await expect(page).toHaveURL(/\/admin\/events$/);
});
test('edit event saves successfully', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('PATCH', '/event/update', {
status: 200,
body: { status: 200 }
});
await page.goto('/app/admin/events/adm1');
await page.waitForLoadState('networkidle');
await expect(page.locator('input[name="name"]')).toHaveValue('管理员测试活动');
await page.locator('input[name="name"]').fill('已修改名称');
// After save, superforms re-runs load so event info is fetched again
await mock.override('GET', '/event/info', {
status: 200,
body: { status: 200, data: { ...adminEvent, name: '已修改名称' } }
});
await page.getByRole('button', { name: '保存' }).click();
await page.waitForLoadState('networkidle');
await expect(page.locator('.alert-error')).not.toBeVisible();
});
test('delete event redirects to list', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('DELETE', '/event/delete', {
status: 200,
body: { status: 200 }
});
await overrideEventList([]);
await page.goto('/app/admin/events/adm1');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: '删除活动' }).click();
await page.getByRole('button', { name: '确认删除' }).click();
await page.waitForURL('**/admin/events');
});
test('agenda tab lists items', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/agenda/list', {
status: 200,
body: {
status: 200,
data: [
{ agenda_id: 'ag1', name: '开幕式', status: 'pending', description: '' },
{ agenda_id: 'ag2', name: '主题演讲', status: 'pending', description: '' }
]
}
});
await page.goto('/app/admin/events/adm1/agenda');
await page.waitForLoadState('networkidle');
await expect(page.getByText('开幕式')).toBeVisible();
await expect(page.getByText('主题演讲')).toBeVisible();
});
test('approve button opens approve dialog', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/agenda/list', {
status: 200,
body: {
status: 200,
data: [{ agenda_id: 'ag1', name: '开幕式', status: 'pending', description: '' }]
}
});
await page.goto('/app/admin/events/adm1/agenda');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: '通过' }).click();
await expect(page.getByRole('heading', { name: '审核通过' })).toBeVisible();
});
test('attendance tab shows table rows', async ({ page, superAdminUser }) => {
test.setTimeout(60_000); // first hit to this route triggers Vite cold-compile
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: [
{
attendance_id: 'att1',
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: '2026-05-01T10:30:00Z',
user_info: { user_id: 'u1', username: 'alice', nickname: 'Alice', email: '' }
},
{
attendance_id: 'att2',
joined_at: '2026-05-02T09:00:00Z',
checked_in_at: null,
user_info: { user_id: 'u2', username: 'bob', nickname: 'Bob', email: '' }
}
],
total: 2
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Alice')).toBeVisible();
await expect(page.getByText('Bob')).toBeVisible();
});
test('attendance pagination bar hidden when total fits on one page', async ({
page,
superAdminUser
}) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: [
{
attendance_id: 'att1',
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
user_info: { user_id: 'u1', username: 'alice', nickname: 'Alice', email: '' }
}
],
total: 1
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await expect(page.getByText(/第.*页.*共.*页/)).not.toBeVisible();
});
test('attendance page 1 shows next button when total > pageSize', async ({
page,
superAdminUser
}) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: Array.from({ length: 50 }, (_, i) => ({
attendance_id: `att${i}`,
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
user_info: { user_id: `u${i}`, username: `user${i}`, nickname: `User ${i}`, email: '' }
})),
total: 75
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await expect(page.getByText('第 1 页 · 共 2 页')).toBeVisible();
await expect(page.getByRole('link', { name: '下一页' })).toBeVisible();
await expect(page.getByRole('link', { name: '上一页' })).not.toBeVisible();
});
test('attendance page 2 shows prev button and sends offset=50', async ({
page,
superAdminUser
}) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: Array.from({ length: 25 }, (_, i) => ({
attendance_id: `att${i + 50}`,
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
user_info: {
user_id: `u${i + 50}`,
username: `user${i + 50}`,
nickname: `User ${i + 50}`,
email: ''
}
})),
total: 75
}
}
});
await page.goto('/app/admin/events/adm1/attendance?page=2');
await page.waitForLoadState('networkidle');
await expect(page.getByText('第 2 页 · 共 2 页')).toBeVisible();
await expect(page.getByRole('link', { name: '上一页' })).toBeVisible();
await expect(page.getByRole('link', { name: '下一页' })).not.toBeVisible();
const reqs = await mock.requests({ method: 'GET', path: '/event/attendance' });
expect(reqs[0].query.offset).toBe('50');
expect(reqs[0].query.limit).toBe('50');
});
test('attendance KYC button absent when kyc_info is null', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: [
{
attendance_id: 'att1',
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
kyc_info: null,
user_info: { user_id: 'u1', username: 'alice', nickname: 'Alice', email: '' }
}
],
total: 1
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await expect(page.getByRole('button', { name: 'KYC 信息' })).not.toBeVisible();
});
test('attendance KYC passport modal shows fields', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: [
{
attendance_id: 'att1',
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
kyc_type: 'passport',
kyc_info: {
given_names: 'John',
surname: 'Doe',
nationality: 'USA',
date_of_birth: '1990-01-01',
document_type: 'PASSPORT',
document_number: 'A1234567',
expiry_date: '2030-01-01'
},
user_info: { user_id: 'u1', username: 'alice', nickname: 'Alice', email: '' }
}
],
total: 1
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'KYC 信息' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page.getByText('John')).toBeVisible();
await expect(page.getByText('Doe')).toBeVisible();
await expect(page.getByText('A1234567')).toBeVisible();
});
test('attendance KYC cnrid modal shows fields', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/attendance', {
status: 200,
body: {
status: 200,
data: {
items: [
{
attendance_id: 'att1',
joined_at: '2026-05-01T10:00:00Z',
checked_in_at: null,
kyc_type: 'cnrid',
kyc_info: {
legal_name: '张三',
resident_id: '11010119900101001X'
},
user_info: { user_id: 'u1', username: 'alice', nickname: 'Alice', email: '' }
}
],
total: 1
}
}
});
await page.goto('/app/admin/events/adm1/attendance');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'KYC 信息' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page.getByText('张三')).toBeVisible();
await expect(page.getByText('11010119900101001X')).toBeVisible();
});
test('stats tab shows four stat cards', async ({ page, superAdminUser }) => {
void superAdminUser;
await overrideEventInfo();
await overrideEventGuide();
await mock.override('GET', '/event/stats', {
status: 200,
body: {
status: 200,
data: {
join_count: 142,
kyc_pass_rate: 0.87,
checkin_count: 98,
agenda_submission_count: 24
}
}
});
await page.goto('/app/admin/events/adm1/stats');
await page.waitForLoadState('networkidle');
await expect(page.getByText('142')).toBeVisible();
await expect(page.getByText('87%')).toBeVisible();
await expect(page.getByText('98')).toBeVisible();
await expect(page.getByText('24')).toBeVisible();
});