feat(admin): E2E mock-сьюты inbox/reports/reviews + testids. Refs EventHub/EventHubFrontAdmin#33 #36
This commit is contained in:
+192
-7
@@ -1,4 +1,5 @@
|
||||
import type { Page, Route } from '@playwright/test';
|
||||
import { createModerationSeed, type ModerationState } from './moderationSeed';
|
||||
|
||||
export type MockAdmin = {
|
||||
id: string;
|
||||
@@ -27,13 +28,21 @@ async function json(route: Route, status: number, body: unknown, headers: Record
|
||||
});
|
||||
}
|
||||
|
||||
function listSlice<T>(items: T[], url: URL): { rows: T[]; total: number } {
|
||||
const limit = Number(url.searchParams.get('limit') || 50);
|
||||
const offset = Number(url.searchParams.get('offset') || 0);
|
||||
return { rows: items.slice(offset, offset + limit), total: items.length };
|
||||
}
|
||||
|
||||
type MockOptions = {
|
||||
user?: MockAdmin;
|
||||
moderation?: boolean;
|
||||
};
|
||||
|
||||
/** Default admin API mocks for Control Center shell / login. */
|
||||
/** Default admin API mocks for Control Center shell / login / moderation. */
|
||||
export async function installAdminApiMocks(page: Page, options: MockOptions = {}) {
|
||||
let currentUser: MockAdmin = { ...MOCK_ADMIN, ...options.user };
|
||||
const moderation: ModerationState | null = options.moderation === false ? null : createModerationSeed();
|
||||
|
||||
await page.route('**/v1/admin/**', async (route) => {
|
||||
const req = route.request();
|
||||
@@ -82,13 +91,13 @@ export async function installAdminApiMocks(page: Page, options: MockOptions = {}
|
||||
await json(route, 200, {
|
||||
users_total: 10,
|
||||
events_total: 5,
|
||||
reviews_total: 2,
|
||||
reviews_total: moderation?.reviews.length ?? 0,
|
||||
calendars_total: 3,
|
||||
tickets_total: 1,
|
||||
tickets_open: 1,
|
||||
reports_total: 2,
|
||||
reports_pending: 1,
|
||||
reports_reviewed: 1,
|
||||
tickets_total: moderation?.tickets.length ?? 0,
|
||||
tickets_open: moderation?.tickets.filter((t) => t.status === 'open').length ?? 0,
|
||||
reports_total: moderation?.reports.length ?? 0,
|
||||
reports_pending: moderation?.reports.filter((r) => r.status === 'pending').length ?? 0,
|
||||
reports_reviewed: moderation?.reports.filter((r) => r.status === 'reviewed').length ?? 0,
|
||||
avg_report_resolution_h: 1.5,
|
||||
avg_ticket_resolution_h: 2,
|
||||
events_by_day: [],
|
||||
@@ -106,6 +115,180 @@ export async function installAdminApiMocks(page: Page, options: MockOptions = {}
|
||||
return;
|
||||
}
|
||||
|
||||
if (moderation) {
|
||||
// ---- reports ----
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/reports/stats')) {
|
||||
await json(route, 200, {
|
||||
total_reports: moderation.reports.length,
|
||||
reports_by_target_type: {},
|
||||
reports_by_status: {
|
||||
pending: moderation.reports.filter((r) => r.status === 'pending').length,
|
||||
reviewed: moderation.reports.filter((r) => r.status === 'reviewed').length,
|
||||
dismissed: moderation.reports.filter((r) => r.status === 'dismissed').length,
|
||||
},
|
||||
top_targets_by_reports: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/reports')) {
|
||||
let rows = [...moderation.reports];
|
||||
const status = url.searchParams.get('status');
|
||||
if (status) rows = rows.filter((r) => r.status === status);
|
||||
const pageRows = listSlice(rows, url);
|
||||
await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) });
|
||||
return;
|
||||
}
|
||||
|
||||
const reportMatch = path.match(/\/v1\/admin\/reports\/([^/]+)$/);
|
||||
if (reportMatch) {
|
||||
const id = decodeURIComponent(reportMatch[1]);
|
||||
const idx = moderation.reports.findIndex((r) => r.id === id);
|
||||
if (idx < 0) {
|
||||
await json(route, 404, { error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
if (method === 'GET') {
|
||||
await json(route, 200, moderation.reports[idx]);
|
||||
return;
|
||||
}
|
||||
if (method === 'PUT') {
|
||||
const payload = (req.postDataJSON() || {}) as { status?: MockReportStatus };
|
||||
moderation.reports[idx] = {
|
||||
...moderation.reports[idx],
|
||||
...payload,
|
||||
resolved_at: payload.status ? '2026-07-14T13:00:00Z' : moderation.reports[idx].resolved_at,
|
||||
resolved_by: payload.status ? currentUser.id : moderation.reports[idx].resolved_by,
|
||||
};
|
||||
await json(route, 200, moderation.reports[idx]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- tickets ----
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/tickets/stats')) {
|
||||
await json(route, 200, {
|
||||
total_tickets: moderation.tickets.length,
|
||||
open: moderation.tickets.filter((t) => t.status === 'open').length,
|
||||
in_progress: moderation.tickets.filter((t) => t.status === 'in_progress').length,
|
||||
resolved: moderation.tickets.filter((t) => t.status === 'resolved').length,
|
||||
closed: moderation.tickets.filter((t) => t.status === 'closed').length,
|
||||
total_errors: moderation.tickets.reduce((s, t) => s + t.count, 0),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/tickets')) {
|
||||
const pageRows = listSlice(moderation.tickets, url);
|
||||
await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) });
|
||||
return;
|
||||
}
|
||||
|
||||
const ticketMatch = path.match(/\/v1\/admin\/tickets\/([^/]+)$/);
|
||||
if (ticketMatch) {
|
||||
const id = decodeURIComponent(ticketMatch[1]);
|
||||
const idx = moderation.tickets.findIndex((t) => t.id === id);
|
||||
if (idx < 0) {
|
||||
await json(route, 404, { error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
if (method === 'GET') {
|
||||
await json(route, 200, moderation.tickets[idx]);
|
||||
return;
|
||||
}
|
||||
if (method === 'PUT') {
|
||||
const payload = (req.postDataJSON() || {}) as Partial<(typeof moderation.tickets)[0]>;
|
||||
moderation.tickets[idx] = { ...moderation.tickets[idx], ...payload };
|
||||
await json(route, 200, moderation.tickets[idx]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- reviews ----
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/reviews/stats')) {
|
||||
await json(route, 200, {
|
||||
total_reviews: moderation.reviews.length,
|
||||
reviews_by_target_type: {},
|
||||
reviews_by_status: {
|
||||
visible: moderation.reviews.filter((r) => r.status === 'visible').length,
|
||||
hidden: moderation.reviews.filter((r) => r.status === 'hidden').length,
|
||||
deleted: moderation.reviews.filter((r) => r.status === 'deleted').length,
|
||||
},
|
||||
top_targets_by_reviews: [],
|
||||
top_targets_by_positive_reviews: [],
|
||||
top_targets_by_negative_reviews: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && path.endsWith('/v1/admin/reviews')) {
|
||||
const pageRows = listSlice(moderation.reviews, url);
|
||||
await json(route, 200, pageRows.rows, { 'X-Total-Count': String(pageRows.total) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'PATCH' && path.endsWith('/v1/admin/reviews')) {
|
||||
const updates = (req.postDataJSON() || []) as Array<{ id: string; status: string; reason?: string }>;
|
||||
for (const u of updates) {
|
||||
const idx = moderation.reviews.findIndex((r) => r.id === u.id);
|
||||
if (idx >= 0) {
|
||||
moderation.reviews[idx] = {
|
||||
...moderation.reviews[idx],
|
||||
status: u.status as (typeof moderation.reviews)[0]['status'],
|
||||
reason: u.reason ?? moderation.reviews[idx].reason,
|
||||
updated_at: '2026-07-14T13:00:00Z',
|
||||
};
|
||||
}
|
||||
}
|
||||
await json(route, 200, updates.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const reviewMatch = path.match(/\/v1\/admin\/reviews\/([^/]+)$/);
|
||||
if (reviewMatch) {
|
||||
const id = decodeURIComponent(reviewMatch[1]);
|
||||
const idx = moderation.reviews.findIndex((r) => r.id === id);
|
||||
if (idx < 0) {
|
||||
await json(route, 404, { error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
if (method === 'GET') {
|
||||
await json(route, 200, moderation.reviews[idx]);
|
||||
return;
|
||||
}
|
||||
if (method === 'PUT') {
|
||||
const payload = (req.postDataJSON() || {}) as Partial<(typeof moderation.reviews)[0]>;
|
||||
moderation.reviews[idx] = {
|
||||
...moderation.reviews[idx],
|
||||
...payload,
|
||||
updated_at: '2026-07-14T13:00:00Z',
|
||||
};
|
||||
await json(route, 200, moderation.reviews[idx]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// soft stubs for linked entities in inbox
|
||||
if (method === 'GET' && /\/v1\/admin\/users\/[^/]+$/.test(path)) {
|
||||
await json(route, 200, {
|
||||
id: 'user-1',
|
||||
email: 'user1@eventhub.local',
|
||||
nickname: 'User One',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (method === 'GET' && /\/v1\/admin\/events\/[^/]+$/.test(path)) {
|
||||
await json(route, 200, {
|
||||
id: 'evt-1',
|
||||
title: 'E2E Event',
|
||||
status: 'active',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'GET') {
|
||||
await json(route, 200, [], { 'X-Total-Count': '0' });
|
||||
return;
|
||||
@@ -114,3 +297,5 @@ export async function installAdminApiMocks(page: Page, options: MockOptions = {}
|
||||
await json(route, 200, { ok: true });
|
||||
});
|
||||
}
|
||||
|
||||
type MockReportStatus = 'pending' | 'reviewed' | 'dismissed';
|
||||
|
||||
Reference in New Issue
Block a user