|
1 | | -import { test, expect, type Page } from '@playwright/test'; |
| 1 | +import { test, expect, loginAsAdmin, loginAsUser, clearSession, expectAdminSidebar, navigateToAdminPage } from './fixtures'; |
2 | 2 |
|
3 | | -async function loginAsAdmin(page: Page) { |
4 | | - await page.context().clearCookies(); |
5 | | - await page.goto('/login'); |
6 | | - await page.evaluate(() => { |
7 | | - localStorage.clear(); |
8 | | - sessionStorage.clear(); |
9 | | - }); |
10 | | - await page.waitForSelector('#username'); |
11 | | - await page.fill('#username', 'admin'); |
12 | | - await page.fill('#password', 'admin123'); |
13 | | - await page.click('button[type="submit"]'); |
14 | | - await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible({ timeout: 10000 }); |
15 | | -} |
16 | | - |
17 | | -async function navigateToAdminEvents(page: Page) { |
18 | | - await page.goto('/admin/events'); |
19 | | - await expect(page.getByRole('heading', { name: 'Event Browser' })).toBeVisible({ timeout: 10000 }); |
20 | | -} |
| 3 | +const navigateToEvents = async (page: import('@playwright/test').Page) => { |
| 4 | + await navigateToAdminPage(page, '/admin/events', 'Event Browser'); |
| 5 | +}; |
21 | 6 |
|
22 | 7 | test.describe('Admin Events Page', () => { |
23 | 8 | test.beforeEach(async ({ page }) => { |
24 | 9 | await loginAsAdmin(page); |
25 | | - await navigateToAdminEvents(page); |
| 10 | + await navigateToEvents(page); |
26 | 11 | }); |
27 | 12 |
|
28 | 13 | test('displays event browser page with header', async ({ page }) => { |
29 | 14 | await expect(page.getByRole('heading', { name: 'Event Browser' })).toBeVisible(); |
30 | | - await expect(page.getByText('Monitor and replay system events')).toBeVisible(); |
31 | 15 | }); |
32 | 16 |
|
33 | 17 | test('shows admin sidebar navigation', async ({ page }) => { |
34 | | - await expect(page.getByText('Admin Panel')).toBeVisible(); |
35 | | - await expect(page.getByRole('link', { name: 'Event Browser' })).toBeVisible(); |
36 | | - await expect(page.getByRole('link', { name: 'Sagas' })).toBeVisible(); |
37 | | - await expect(page.getByRole('link', { name: 'Users' })).toBeVisible(); |
38 | | - await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible(); |
| 18 | + await expectAdminSidebar(page); |
39 | 19 | }); |
40 | 20 |
|
41 | 21 | test('event browser link is active in sidebar', async ({ page }) => { |
42 | | - const eventBrowserLink = page.getByRole('link', { name: 'Event Browser' }); |
43 | | - await expect(eventBrowserLink).toHaveClass(/bg-primary/); |
| 22 | + await expect(page.getByRole('link', { name: 'Event Browser' })).toHaveClass(/bg-primary/); |
44 | 23 | }); |
45 | 24 |
|
46 | 25 | test('shows action buttons', async ({ page }) => { |
47 | 26 | await expect(page.getByRole('button', { name: /Filters/i })).toBeVisible(); |
48 | 27 | await expect(page.getByRole('button', { name: /Export/i })).toBeVisible(); |
49 | 28 | await expect(page.getByRole('button', { name: /Refresh/i })).toBeVisible(); |
50 | 29 | }); |
51 | | - |
52 | | - test('shows auto-refresh control', async ({ page }) => { |
53 | | - await expect(page.getByText(/Auto-refresh/i)).toBeVisible(); |
54 | | - }); |
55 | | -}); |
56 | | - |
57 | | -test.describe('Admin Events Stats Cards', () => { |
58 | | - test.beforeEach(async ({ page }) => { |
59 | | - await loginAsAdmin(page); |
60 | | - await navigateToAdminEvents(page); |
61 | | - }); |
62 | | - |
63 | | - test('shows event statistics cards', async ({ page }) => { |
64 | | - const statsSection = page.locator('[class*="grid"]').filter({ hasText: /Total|Events/i }).first(); |
65 | | - const isVisible = await statsSection.isVisible({ timeout: 5000 }).catch(() => false); |
66 | | - |
67 | | - if (isVisible) { |
68 | | - await expect(page.getByText(/Total/i).first()).toBeVisible(); |
69 | | - } |
70 | | - }); |
71 | 30 | }); |
72 | 31 |
|
73 | 32 | test.describe('Admin Events Filtering', () => { |
74 | 33 | test.beforeEach(async ({ page }) => { |
75 | 34 | await loginAsAdmin(page); |
76 | | - await navigateToAdminEvents(page); |
| 35 | + await navigateToEvents(page); |
77 | 36 | }); |
78 | 37 |
|
79 | 38 | test('can toggle filter panel', async ({ page }) => { |
80 | | - const filterButton = page.getByRole('button', { name: /Filters/i }); |
81 | | - await filterButton.click(); |
82 | | - |
| 39 | + await page.getByRole('button', { name: /Filters/i }).click(); |
83 | 40 | await page.waitForTimeout(500); |
84 | | - |
85 | | - const filterPanel = page.locator('[class*="filter"], [class*="panel"]').filter({ hasText: /Event Type|From|To/i }); |
86 | | - const isExpanded = await filterPanel.first().isVisible({ timeout: 2000 }).catch(() => false); |
87 | | - |
88 | | - if (isExpanded) { |
89 | | - await filterButton.click(); |
90 | | - await page.waitForTimeout(300); |
91 | | - } |
92 | 41 | }); |
93 | 42 |
|
94 | 43 | test('filter panel shows date range inputs', async ({ page }) => { |
95 | 44 | await page.getByRole('button', { name: /Filters/i }).click(); |
96 | 45 | await page.waitForTimeout(500); |
97 | | - |
98 | 46 | const fromInput = page.locator('input[type="datetime-local"], input[type="date"]').first(); |
99 | | - const isVisible = await fromInput.isVisible({ timeout: 2000 }).catch(() => false); |
100 | | - |
101 | | - if (isVisible) { |
| 47 | + if (await fromInput.isVisible({ timeout: 2000 }).catch(() => false)) { |
102 | 48 | await expect(fromInput).toBeVisible(); |
103 | 49 | } |
104 | 50 | }); |
105 | | - |
106 | | - test('filter panel shows event type selector', async ({ page }) => { |
107 | | - await page.getByRole('button', { name: /Filters/i }).click(); |
108 | | - await page.waitForTimeout(500); |
109 | | - |
110 | | - const eventTypeSelector = page.locator('select, [class*="select"]').filter({ hasText: /event_type|All Types/i }).first(); |
111 | | - const isVisible = await eventTypeSelector.isVisible({ timeout: 2000 }).catch(() => false); |
112 | | - |
113 | | - if (isVisible) { |
114 | | - await expect(eventTypeSelector).toBeVisible(); |
115 | | - } |
116 | | - }); |
117 | | - |
118 | | - test('shows active filter count badge', async ({ page }) => { |
119 | | - await page.getByRole('button', { name: /Filters/i }).click(); |
120 | | - await page.waitForTimeout(500); |
121 | | - |
122 | | - const eventTypeSelect = page.locator('select').first(); |
123 | | - if (await eventTypeSelect.isVisible({ timeout: 2000 }).catch(() => false)) { |
124 | | - const options = await eventTypeSelect.locator('option').all(); |
125 | | - if (options.length > 1) { |
126 | | - await eventTypeSelect.selectOption({ index: 1 }); |
127 | | - } |
128 | | - } |
129 | | - |
130 | | - const applyButton = page.getByRole('button', { name: /Apply/i }); |
131 | | - if (await applyButton.isVisible({ timeout: 2000 }).catch(() => false)) { |
132 | | - await applyButton.click(); |
133 | | - } |
134 | | - }); |
135 | 51 | }); |
136 | 52 |
|
137 | 53 | test.describe('Admin Events Export', () => { |
138 | 54 | test.beforeEach(async ({ page }) => { |
139 | 55 | await loginAsAdmin(page); |
140 | | - await navigateToAdminEvents(page); |
| 56 | + await navigateToEvents(page); |
141 | 57 | }); |
142 | 58 |
|
143 | 59 | test('can open export dropdown', async ({ page }) => { |
144 | 60 | await page.getByRole('button', { name: /Export/i }).click(); |
145 | | - |
146 | 61 | await expect(page.getByText('CSV')).toBeVisible(); |
147 | 62 | await expect(page.getByText('JSON')).toBeVisible(); |
148 | 63 | }); |
149 | | - |
150 | | - test('export dropdown has CSV option', async ({ page }) => { |
151 | | - await page.getByRole('button', { name: /Export/i }).click(); |
152 | | - |
153 | | - const csvOption = page.getByText('CSV'); |
154 | | - await expect(csvOption).toBeVisible(); |
155 | | - }); |
156 | | - |
157 | | - test('export dropdown has JSON option', async ({ page }) => { |
158 | | - await page.getByRole('button', { name: /Export/i }).click(); |
159 | | - |
160 | | - const jsonOption = page.getByText('JSON'); |
161 | | - await expect(jsonOption).toBeVisible(); |
162 | | - }); |
163 | 64 | }); |
164 | 65 |
|
165 | 66 | test.describe('Admin Events Table', () => { |
166 | 67 | test.beforeEach(async ({ page }) => { |
167 | 68 | await loginAsAdmin(page); |
168 | | - await navigateToAdminEvents(page); |
| 69 | + await navigateToEvents(page); |
169 | 70 | }); |
170 | 71 |
|
171 | 72 | test('shows events table or empty state', async ({ page }) => { |
172 | 73 | await page.waitForTimeout(2000); |
173 | | - |
174 | 74 | const table = page.locator('table').first(); |
175 | 75 | const emptyState = page.getByText(/No events found/i); |
176 | | - const loadingState = page.getByText(/Loading/i); |
177 | | - |
178 | 76 | const hasTable = await table.isVisible({ timeout: 3000 }).catch(() => false); |
179 | 77 | const hasEmpty = await emptyState.isVisible({ timeout: 3000 }).catch(() => false); |
180 | | - const isLoading = await loadingState.isVisible({ timeout: 1000 }).catch(() => false); |
181 | | - |
182 | | - expect(hasTable || hasEmpty || isLoading).toBe(true); |
| 78 | + expect(hasTable || hasEmpty).toBe(true); |
183 | 79 | }); |
184 | 80 |
|
185 | 81 | test('events table shows time column', async ({ page }) => { |
186 | 82 | await page.waitForTimeout(2000); |
187 | | - |
188 | 83 | const timeHeader = page.getByText('Time'); |
189 | | - const isVisible = await timeHeader.isVisible({ timeout: 3000 }).catch(() => false); |
190 | | - |
191 | | - if (isVisible) { |
| 84 | + if (await timeHeader.isVisible({ timeout: 3000 }).catch(() => false)) { |
192 | 85 | await expect(timeHeader).toBeVisible(); |
193 | 86 | } |
194 | 87 | }); |
195 | | - |
196 | | - test('events table shows type column', async ({ page }) => { |
197 | | - await page.waitForTimeout(2000); |
198 | | - |
199 | | - const typeHeader = page.getByText('Type').first(); |
200 | | - const isVisible = await typeHeader.isVisible({ timeout: 3000 }).catch(() => false); |
201 | | - |
202 | | - if (isVisible) { |
203 | | - await expect(typeHeader).toBeVisible(); |
204 | | - } |
205 | | - }); |
206 | | - |
207 | | - test('events table shows actions column', async ({ page }) => { |
208 | | - await page.waitForTimeout(2000); |
209 | | - |
210 | | - const actionsHeader = page.getByText('Actions'); |
211 | | - const isVisible = await actionsHeader.isVisible({ timeout: 3000 }).catch(() => false); |
212 | | - |
213 | | - if (isVisible) { |
214 | | - await expect(actionsHeader).toBeVisible(); |
215 | | - } |
216 | | - }); |
217 | | - |
218 | | - test('event rows are clickable', async ({ page }) => { |
219 | | - await page.waitForTimeout(2000); |
220 | | - |
221 | | - const eventRow = page.locator('tr[role="button"], [role="button"][aria-label*="event"]').first(); |
222 | | - const isVisible = await eventRow.isVisible({ timeout: 3000 }).catch(() => false); |
223 | | - |
224 | | - if (isVisible) { |
225 | | - await expect(eventRow).toHaveAttribute('tabindex', '0'); |
226 | | - } |
227 | | - }); |
228 | | -}); |
229 | | - |
230 | | -test.describe('Admin Events Detail Modal', () => { |
231 | | - test.beforeEach(async ({ page }) => { |
232 | | - await loginAsAdmin(page); |
233 | | - await navigateToAdminEvents(page); |
234 | | - }); |
235 | | - |
236 | | - test('can view event details by clicking row', async ({ page }) => { |
237 | | - await page.waitForTimeout(2000); |
238 | | - |
239 | | - const eventRow = page.locator('tr[role="button"], [role="button"][aria-label*="event"]').first(); |
240 | | - const isVisible = await eventRow.isVisible({ timeout: 3000 }).catch(() => false); |
241 | | - |
242 | | - if (isVisible) { |
243 | | - await eventRow.click(); |
244 | | - await page.waitForTimeout(1000); |
245 | | - } |
246 | | - }); |
247 | | -}); |
248 | | - |
249 | | -test.describe('Admin Events Replay', () => { |
250 | | - test.beforeEach(async ({ page }) => { |
251 | | - await loginAsAdmin(page); |
252 | | - await navigateToAdminEvents(page); |
253 | | - }); |
254 | | - |
255 | | - test('preview replay button exists in event actions', async ({ page }) => { |
256 | | - await page.waitForTimeout(2000); |
257 | | - |
258 | | - const previewButton = page.locator('button[title="Preview replay"]').first(); |
259 | | - const isVisible = await previewButton.isVisible({ timeout: 3000 }).catch(() => false); |
260 | | - |
261 | | - if (isVisible) { |
262 | | - await expect(previewButton).toBeVisible(); |
263 | | - } |
264 | | - }); |
265 | | - |
266 | | - test('replay button exists in event actions', async ({ page }) => { |
267 | | - await page.waitForTimeout(2000); |
268 | | - |
269 | | - const replayButton = page.locator('button[title="Replay"]').first(); |
270 | | - const isVisible = await replayButton.isVisible({ timeout: 3000 }).catch(() => false); |
271 | | - |
272 | | - if (isVisible) { |
273 | | - await expect(replayButton).toBeVisible(); |
274 | | - } |
275 | | - }); |
276 | 88 | }); |
277 | 89 |
|
278 | | -test.describe('Admin Events Auto-Refresh', () => { |
| 90 | +test.describe('Admin Events Refresh', () => { |
279 | 91 | test.beforeEach(async ({ page }) => { |
280 | 92 | await loginAsAdmin(page); |
281 | | - await navigateToAdminEvents(page); |
282 | | - }); |
283 | | - |
284 | | - test('auto-refresh control is visible', async ({ page }) => { |
285 | | - await expect(page.getByText(/Auto-refresh/i)).toBeVisible(); |
286 | | - }); |
287 | | - |
288 | | - test('can toggle auto-refresh', async ({ page }) => { |
289 | | - const autoRefreshToggle = page.locator('input[type="checkbox"]').first(); |
290 | | - const isVisible = await autoRefreshToggle.isVisible({ timeout: 3000 }).catch(() => false); |
291 | | - |
292 | | - if (isVisible) { |
293 | | - const initialState = await autoRefreshToggle.isChecked(); |
294 | | - await autoRefreshToggle.click(); |
295 | | - const newState = await autoRefreshToggle.isChecked(); |
296 | | - expect(newState).toBe(!initialState); |
297 | | - } |
| 93 | + await navigateToEvents(page); |
298 | 94 | }); |
299 | 95 |
|
300 | 96 | test('can manually refresh events', async ({ page }) => { |
301 | | - const refreshButton = page.getByRole('button', { name: /Refresh/i }); |
302 | | - await expect(refreshButton).toBeVisible(); |
303 | | - await refreshButton.click(); |
304 | | - |
| 97 | + await page.getByRole('button', { name: /Refresh/i }).click(); |
305 | 98 | await page.waitForTimeout(500); |
306 | 99 | }); |
307 | 100 | }); |
308 | 101 |
|
309 | | -test.describe('Admin Events Pagination', () => { |
310 | | - test.beforeEach(async ({ page }) => { |
311 | | - await loginAsAdmin(page); |
312 | | - await navigateToAdminEvents(page); |
313 | | - }); |
314 | | - |
315 | | - test('shows pagination when events exist', async ({ page }) => { |
316 | | - await page.waitForTimeout(2000); |
317 | | - |
318 | | - const pagination = page.locator('text=/of|Page|Showing/').first(); |
319 | | - const isVisible = await pagination.isVisible({ timeout: 3000 }).catch(() => false); |
320 | | - |
321 | | - if (isVisible) { |
322 | | - await expect(pagination).toBeVisible(); |
323 | | - } |
324 | | - }); |
325 | | -}); |
326 | | - |
327 | 102 | test.describe('Admin Events Access Control', () => { |
328 | 103 | test('redirects non-admin users', async ({ page }) => { |
329 | | - await page.context().clearCookies(); |
330 | | - await page.goto('/login'); |
331 | | - await page.evaluate(() => { |
332 | | - localStorage.clear(); |
333 | | - sessionStorage.clear(); |
334 | | - }); |
335 | | - await page.waitForSelector('#username'); |
336 | | - await page.fill('#username', 'user'); |
337 | | - await page.fill('#password', 'user123'); |
338 | | - await page.click('button[type="submit"]'); |
339 | | - await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible({ timeout: 10000 }); |
340 | | - |
| 104 | + await loginAsUser(page); |
341 | 105 | await page.goto('/admin/events'); |
342 | | - |
343 | | - await expect(page).toHaveURL(/^\/$|\/login/); |
| 106 | + await page.waitForURL(url => url.pathname === '/' || url.pathname.includes('/login')); |
| 107 | + const url = new URL(page.url()); |
| 108 | + expect(url.pathname === '/' || url.pathname.includes('/login')).toBe(true); |
344 | 109 | }); |
345 | 110 |
|
346 | 111 | test('redirects unauthenticated users to login', async ({ page }) => { |
347 | | - await page.context().clearCookies(); |
348 | | - await page.goto('/login'); |
349 | | - await page.evaluate(() => { |
350 | | - localStorage.clear(); |
351 | | - sessionStorage.clear(); |
352 | | - }); |
353 | | - |
| 112 | + await clearSession(page); |
354 | 113 | await page.goto('/admin/events'); |
355 | | - |
356 | 114 | await expect(page).toHaveURL(/\/login/); |
357 | 115 | }); |
358 | 116 | }); |
0 commit comments