Skip to content

Commit a1cf7dd

Browse files
committed
feat(admin): add targeted announcements
1 parent 66640be commit a1cf7dd

18 files changed

Lines changed: 1683 additions & 4 deletions

‎e2e/admin-announcements.spec.ts‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { expect, test } from '@playwright/test';
2+
import { stubSupabaseSession } from './helpers/auth';
3+
4+
const ANNOUNCEMENT_ID = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
5+
const TARGET_USER_ID = '11111111-2222-4333-8444-555555555555';
6+
7+
test.beforeEach(async ({ page }) => {
8+
await stubSupabaseSession(page, { userId: '99999999-9999-4999-8999-999999999999' });
9+
await page.addInitScript(() => {
10+
window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({
11+
state: { hasCompletedOnboarding: true, isTourRunning: false },
12+
version: 0,
13+
}));
14+
});
15+
});
16+
17+
test('user sees and permanently dismisses an active announcement', async ({ page }) => {
18+
let dismissBody: Record<string, unknown> | null = null;
19+
await page.route('**/functions/v1/announcements*', async (route) => {
20+
const request = route.request();
21+
const url = new URL(request.url());
22+
if (request.method() === 'POST' && url.searchParams.get('action') === 'dismiss') {
23+
dismissBody = request.postDataJSON();
24+
await route.fulfill({ json: { success: true } });
25+
return;
26+
}
27+
await route.fulfill({
28+
json: {
29+
announcements: [{
30+
id: ANNOUNCEMENT_ID,
31+
title: 'Scheduled maintenance',
32+
body: 'DebateLab will be unavailable for ten minutes.',
33+
starts_at: '2026-08-31T12:00:00.000Z',
34+
expires_at: null,
35+
created_at: '2026-08-31T11:00:00.000Z',
36+
}],
37+
},
38+
});
39+
});
40+
41+
await page.goto('/app');
42+
await expect(page.getByRole('heading', { name: 'Scheduled maintenance' })).toBeVisible();
43+
await expect(page.getByText('DebateLab will be unavailable for ten minutes.')).toBeVisible();
44+
45+
await page.getByRole('button', { name: 'Dismiss announcement: Scheduled maintenance' }).click();
46+
47+
await expect(page.getByRole('heading', { name: 'Scheduled maintenance' })).toHaveCount(0);
48+
expect(dismissBody).toEqual({ announcementId: ANNOUNCEMENT_ID });
49+
});
50+
51+
test('admin creates a targeted announcement and deletes it from history', async ({ page }) => {
52+
let createBody: Record<string, unknown> | null = null;
53+
let deleteBody: Record<string, unknown> | null = null;
54+
55+
await page.route('**/functions/v1/announcements*', (route) => route.fulfill({ json: { announcements: [] } }));
56+
await page.route('**/functions/v1/admin*', async (route) => {
57+
const request = route.request();
58+
const action = new URL(request.url()).searchParams.get('action');
59+
60+
if (action === 'stats') {
61+
await route.fulfill({ json: { totals: { requests: 0, cost: 0, successRate: 100 }, daily: [], byFeature: {}, byUser: [] } });
62+
return;
63+
}
64+
if (action === 'users') {
65+
await route.fulfill({ json: { users: [{ id: TARGET_USER_ID, email: 'target@example.com', createdAt: '2026-08-01T00:00:00.000Z', lastSignIn: '2026-08-31T00:00:00.000Z', requestCount: 2, role: 'user' }] } });
66+
return;
67+
}
68+
if (action === 'promo-codes') {
69+
await route.fulfill({ json: { codes: [] } });
70+
return;
71+
}
72+
if (action === 'subscribers') {
73+
await route.fulfill({ json: { subscribers: [] } });
74+
return;
75+
}
76+
if (action === 'announcements') {
77+
await route.fulfill({ json: { announcements: [] } });
78+
return;
79+
}
80+
if (action === 'create-announcement') {
81+
createBody = request.postDataJSON();
82+
await route.fulfill({
83+
status: 201,
84+
json: {
85+
announcement: {
86+
id: ANNOUNCEMENT_ID,
87+
title: 'Targeted note',
88+
body: 'This message is only for the selected user.',
89+
target_type: 'users',
90+
target_user_ids: [TARGET_USER_ID],
91+
starts_at: '2026-08-31T12:00:00.000Z',
92+
expires_at: null,
93+
created_by: '99999999-9999-4999-8999-999999999999',
94+
created_at: '2026-08-31T12:00:00.000Z',
95+
},
96+
},
97+
});
98+
return;
99+
}
100+
if (action === 'delete-announcement') {
101+
deleteBody = request.postDataJSON();
102+
await route.fulfill({ json: { success: true } });
103+
return;
104+
}
105+
await route.fulfill({ status: 400, json: { error: 'Unexpected action' } });
106+
});
107+
108+
await page.goto('/app/admin');
109+
await expect(page.getByRole('heading', { name: 'Admin Dashboard' })).toBeVisible();
110+
await page.getByRole('tab', { name: 'Announcements' }).click();
111+
await expect(page.getByText('No announcements yet')).toBeVisible();
112+
113+
await page.getByLabel('Title').fill('Targeted note');
114+
await page.getByLabel('Message').fill('This message is only for the selected user.');
115+
await page.getByLabel('Audience').click();
116+
await page.getByRole('option', { name: 'Selected users' }).click();
117+
await page.getByRole('checkbox', { name: 'Select target@example.com' }).check();
118+
await page.getByRole('button', { name: 'Create announcement' }).click();
119+
120+
await expect(page.getByRole('heading', { name: 'Targeted note' })).toBeVisible();
121+
await expect.poll(() => createBody).toMatchObject({
122+
title: 'Targeted note',
123+
body: 'This message is only for the selected user.',
124+
targetType: 'users',
125+
targetUserIds: [TARGET_USER_ID],
126+
});
127+
128+
await page.getByRole('button', { name: 'Delete' }).click();
129+
await page.getByRole('button', { name: 'Delete announcement' }).click();
130+
131+
await expect(page.getByRole('heading', { name: 'Targeted note' })).toHaveCount(0);
132+
await expect.poll(() => deleteBody).toEqual({ announcementId: ANNOUNCEMENT_ID });
133+
});

0 commit comments

Comments
 (0)