439 lines
14 KiB
TypeScript
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();
|
|
});
|