een-api-toolkit 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +10 -40
  2. package/README.md +1 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +825 -0
  6. package/dist/index.js +489 -254
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +314 -2
  9. package/examples/vue-alerts-metrics/README.md +136 -0
  10. package/examples/vue-alerts-metrics/e2e/app.spec.ts +74 -0
  11. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +561 -0
  12. package/examples/vue-alerts-metrics/index.html +13 -0
  13. package/examples/vue-alerts-metrics/package-lock.json +1756 -0
  14. package/examples/vue-alerts-metrics/package.json +31 -0
  15. package/examples/vue-alerts-metrics/playwright.config.ts +46 -0
  16. package/examples/vue-alerts-metrics/src/App.vue +108 -0
  17. package/examples/vue-alerts-metrics/src/components/AlertsList.vue +881 -0
  18. package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +106 -0
  19. package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +336 -0
  20. package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +825 -0
  21. package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +259 -0
  22. package/examples/vue-alerts-metrics/src/composables/useHlsPlayer.ts +285 -0
  23. package/examples/vue-alerts-metrics/src/main.ts +23 -0
  24. package/examples/vue-alerts-metrics/src/router/index.ts +61 -0
  25. package/examples/vue-alerts-metrics/src/views/Callback.vue +76 -0
  26. package/examples/vue-alerts-metrics/src/views/Dashboard.vue +174 -0
  27. package/examples/vue-alerts-metrics/src/views/Home.vue +216 -0
  28. package/examples/vue-alerts-metrics/src/views/Login.vue +33 -0
  29. package/examples/vue-alerts-metrics/src/views/Logout.vue +66 -0
  30. package/examples/vue-alerts-metrics/src/vite-env.d.ts +12 -0
  31. package/examples/vue-alerts-metrics/tsconfig.json +21 -0
  32. package/examples/vue-alerts-metrics/tsconfig.node.json +10 -0
  33. package/examples/vue-alerts-metrics/vite.config.ts +12 -0
  34. package/package.json +1 -1
@@ -0,0 +1,561 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+
3
+ /**
4
+ * E2E tests for the Vue Alerts & Metrics Example - OAuth Login Flow
5
+ *
6
+ * Tests the OAuth login flow through the UI:
7
+ * 1. Click login button in the example app
8
+ * 2. Enter credentials on EEN OAuth page
9
+ * 3. Complete the OAuth callback
10
+ * 4. Verify authenticated state and landing URL
11
+ *
12
+ * Required environment variables:
13
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
14
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
15
+ * - TEST_USER: Test user email
16
+ * - TEST_PASSWORD: Test user password
17
+ */
18
+
19
+ const TIMEOUTS = {
20
+ OAUTH_REDIRECT: 30000,
21
+ ELEMENT_VISIBLE: 15000,
22
+ PASSWORD_VISIBLE: 10000,
23
+ AUTH_COMPLETE: 30000,
24
+ UI_UPDATE: 10000,
25
+ PROXY_CHECK: 5000
26
+ } as const
27
+
28
+ const TEST_USER = process.env.TEST_USER
29
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
30
+ const PROXY_URL = process.env.VITE_PROXY_URL
31
+
32
+ async function isProxyAccessible(): Promise<boolean> {
33
+ if (!PROXY_URL) return false
34
+ const controller = new AbortController()
35
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
36
+
37
+ try {
38
+ const response = await fetch(PROXY_URL, {
39
+ method: 'HEAD',
40
+ signal: controller.signal
41
+ })
42
+ return response.ok || response.status === 404
43
+ } catch {
44
+ return false
45
+ } finally {
46
+ clearTimeout(timeoutId)
47
+ }
48
+ }
49
+
50
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
51
+ await page.goto('/')
52
+
53
+ // Click login button on home page to go to login page
54
+ await page.click('[data-testid="login-button"]')
55
+ await page.waitForURL('/login')
56
+
57
+ // Click the OAuth login button on the login page
58
+ await Promise.all([
59
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
60
+ page.click('button:has-text("Login with Eagle Eye Networks")')
61
+ ])
62
+
63
+ const emailInput = page.locator('#authentication--input__email')
64
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
65
+ await emailInput.fill(username)
66
+
67
+ await page.getByRole('button', { name: 'Next' }).click()
68
+
69
+ const passwordInput = page.locator('#authentication--input__password')
70
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
71
+ await passwordInput.fill(password)
72
+
73
+ await page.locator('#next, button:has-text("Sign in")').first().click()
74
+
75
+ // After login, the alerts-metrics app redirects to /dashboard
76
+ await page.waitForURL('**/dashboard', { timeout: TIMEOUTS.AUTH_COMPLETE })
77
+ }
78
+
79
+ async function clearAuthState(page: Page): Promise<void> {
80
+ try {
81
+ const url = page.url()
82
+ if (url && url.startsWith('http')) {
83
+ await page.evaluate(() => {
84
+ try {
85
+ localStorage.clear()
86
+ sessionStorage.clear()
87
+ } catch {
88
+ // Ignore
89
+ }
90
+ })
91
+ }
92
+ } catch {
93
+ // Ignore
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Wait for cameras to finish loading (either cameras appear or error/empty state)
99
+ */
100
+ async function waitForCamerasLoaded(page: Page): Promise<void> {
101
+ // Wait until either camera options appear, an error shows, or the select is enabled (empty case)
102
+ await Promise.race([
103
+ page.locator('[data-testid="camera-option"]').first().waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE }),
104
+ page.locator('[data-testid="camera-selector-error"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE }),
105
+ page.locator('[data-testid="camera-select"]:not([disabled])').waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE })
106
+ ]).catch(() => {
107
+ // At least one should resolve - if all fail, that's ok, we'll handle in the test
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Wait for event types to finish loading
113
+ */
114
+ async function waitForEventTypesLoaded(page: Page): Promise<void> {
115
+ await Promise.race([
116
+ page.locator('[data-testid="event-type-option"]').first().waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE }),
117
+ page.locator('[data-testid="event-type-select"]:not([disabled])').waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE })
118
+ ]).catch(() => {
119
+ // Handle gracefully
120
+ })
121
+ }
122
+
123
+ /**
124
+ * Wait for metrics to finish loading (data, error, or no-data state)
125
+ */
126
+ async function waitForMetricsLoaded(page: Page): Promise<void> {
127
+ await Promise.race([
128
+ page.locator('.chart-container canvas').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE }),
129
+ page.locator('[data-testid="metrics-error"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE }),
130
+ page.locator('[data-testid="metrics-no-data"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE })
131
+ ]).catch(() => {
132
+ // Handle gracefully
133
+ })
134
+ }
135
+
136
+ /**
137
+ * Wait for alerts to finish loading
138
+ */
139
+ async function waitForAlertsLoaded(page: Page): Promise<void> {
140
+ await Promise.race([
141
+ page.locator('[data-testid="alert-item"]').first().waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE }),
142
+ page.locator('[data-testid="alerts-error"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE }),
143
+ page.locator('[data-testid="alerts-no-data"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE })
144
+ ]).catch(() => {
145
+ // Handle gracefully
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Wait for notifications to finish loading
151
+ */
152
+ async function waitForNotificationsLoaded(page: Page): Promise<void> {
153
+ await Promise.race([
154
+ page.locator('[data-testid="notification-item"]').first().waitFor({ state: 'attached', timeout: TIMEOUTS.UI_UPDATE }),
155
+ page.locator('[data-testid="notifications-error"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE }),
156
+ page.locator('[data-testid="notifications-no-data"]').waitFor({ state: 'visible', timeout: TIMEOUTS.UI_UPDATE })
157
+ ]).catch(() => {
158
+ // Handle gracefully
159
+ })
160
+ }
161
+
162
+ test.describe('Vue Alerts & Metrics Example - Auth', () => {
163
+ let proxyAccessible = false
164
+
165
+ function skipIfNoProxy() {
166
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
167
+ }
168
+
169
+ function skipIfNoCredentials() {
170
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
171
+ }
172
+
173
+ test.beforeAll(async () => {
174
+ proxyAccessible = await isProxyAccessible()
175
+ if (!proxyAccessible) {
176
+ console.log('OAuth proxy not accessible - OAuth tests will be skipped')
177
+ }
178
+ })
179
+
180
+ test.afterEach(async ({ page }) => {
181
+ await clearAuthState(page)
182
+ })
183
+
184
+ test('shows login button when not authenticated', async ({ page }) => {
185
+ await page.goto('/')
186
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
187
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
188
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
189
+ await expect(page.locator('[data-testid="nav-dashboard"]')).not.toBeVisible()
190
+ })
191
+
192
+ test('dashboard page redirects to login when not authenticated', async ({ page }) => {
193
+ await page.goto('/dashboard')
194
+ // Should redirect to login page
195
+ await expect(page.locator('h2')).toContainText('Login')
196
+ })
197
+
198
+ test('login button redirects to OAuth page', async ({ page }) => {
199
+ skipIfNoProxy()
200
+ skipIfNoCredentials()
201
+
202
+ await page.goto('/')
203
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
204
+
205
+ // Click login button to go to login page
206
+ await page.click('[data-testid="login-button"]')
207
+ await page.waitForURL('/login')
208
+
209
+ // Click the OAuth login button
210
+ await Promise.all([
211
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
212
+ page.click('button:has-text("Login with Eagle Eye Networks")')
213
+ ])
214
+
215
+ const emailInput = page.locator('#authentication--input__email')
216
+ await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
217
+ })
218
+
219
+ test('complete OAuth login flow and verify landing URL', async ({ page }) => {
220
+ skipIfNoProxy()
221
+ skipIfNoCredentials()
222
+
223
+ // Verify initially not authenticated
224
+ await page.goto('/')
225
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
226
+
227
+ // Perform login
228
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
229
+
230
+ // Verify landing URL is the dashboard page
231
+ await expect(page).toHaveURL('http://127.0.0.1:3333/dashboard')
232
+
233
+ // Verify authenticated state - we're on dashboard page so check nav elements
234
+ await expect(page.locator('[data-testid="nav-dashboard"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
235
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible()
236
+ await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible()
237
+ })
238
+
239
+ test('can view dashboard after login', async ({ page }) => {
240
+ skipIfNoProxy()
241
+ skipIfNoCredentials()
242
+
243
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
244
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
245
+
246
+ // Verify dashboard elements are present
247
+ await expect(page.locator('h2')).toContainText('Alerts & Metrics Dashboard')
248
+ await expect(page.locator('[data-testid="camera-selector"]')).toBeVisible()
249
+ await expect(page.locator('[data-testid="time-range-selector"]')).toBeVisible()
250
+ })
251
+
252
+ test('camera selector loads cameras', async ({ page }) => {
253
+ skipIfNoProxy()
254
+ skipIfNoCredentials()
255
+
256
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
257
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
258
+
259
+ // Wait for camera selector to load
260
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
261
+ await expect(cameraSelect).toBeVisible()
262
+
263
+ // Wait for cameras to load using condition-based wait
264
+ await waitForCamerasLoaded(page)
265
+
266
+ // Check if cameras loaded (should have options)
267
+ const options = page.locator('[data-testid="camera-option"]')
268
+ const optionCount = await options.count()
269
+ console.log(`Found ${optionCount} cameras`)
270
+
271
+ // Should have at least some cameras (or show an error)
272
+ const hasError = await page.locator('[data-testid="camera-selector-error"]').isVisible()
273
+ if (!hasError) {
274
+ expect(optionCount).toBeGreaterThanOrEqual(0)
275
+ }
276
+ })
277
+
278
+ test('time range selector changes selection', async ({ page }) => {
279
+ skipIfNoProxy()
280
+ skipIfNoCredentials()
281
+
282
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
283
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
284
+
285
+ // Test time range buttons
286
+ const buttonNone = page.locator('[data-testid="time-range-none"]')
287
+ const button1h = page.locator('[data-testid="time-range-1h"]')
288
+ const button24h = page.locator('[data-testid="time-range-24h"]')
289
+
290
+ await expect(buttonNone).toBeVisible()
291
+ await expect(button1h).toBeVisible()
292
+ await expect(button24h).toBeVisible()
293
+
294
+ // 'None' should be active by default
295
+ await expect(buttonNone).toHaveClass(/active/)
296
+
297
+ // Click 24h and verify it becomes active
298
+ await button24h.click()
299
+ await expect(button24h).toHaveClass(/active/)
300
+ await expect(buttonNone).not.toHaveClass(/active/)
301
+
302
+ // Click 1h and verify it becomes active
303
+ await button1h.click()
304
+ await expect(button1h).toHaveClass(/active/)
305
+ await expect(button24h).not.toHaveClass(/active/)
306
+ })
307
+
308
+ test('metrics chart displays after selecting camera and event type', async ({ page }) => {
309
+ skipIfNoProxy()
310
+ skipIfNoCredentials()
311
+
312
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
313
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
314
+
315
+ // Wait for cameras to load using condition-based wait
316
+ await waitForCamerasLoaded(page)
317
+
318
+ // Select first camera if available
319
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
320
+ const cameraOptions = page.locator('[data-testid="camera-option"]')
321
+ const cameraCount = await cameraOptions.count()
322
+
323
+ if (cameraCount > 0) {
324
+ // Get first camera value and select it
325
+ const firstCameraValue = await cameraOptions.first().getAttribute('value')
326
+ if (firstCameraValue) {
327
+ await cameraSelect.selectOption(firstCameraValue)
328
+
329
+ // Wait for event types to load using condition-based wait
330
+ await waitForEventTypesLoaded(page)
331
+
332
+ // Check metrics chart container is displayed
333
+ const metricsChart = page.locator('[data-testid="metrics-chart"]')
334
+ await expect(metricsChart).toBeVisible()
335
+
336
+ // Check event type selector is visible
337
+ const eventTypeSelect = page.locator('[data-testid="event-type-select"]')
338
+ await expect(eventTypeSelect).toBeVisible()
339
+
340
+ // Select first event type if available
341
+ const eventTypeOptions = page.locator('[data-testid="event-type-option"]')
342
+ const eventTypeCount = await eventTypeOptions.count()
343
+
344
+ if (eventTypeCount > 0) {
345
+ const firstEventType = await eventTypeOptions.first().getAttribute('value')
346
+ if (firstEventType) {
347
+ await eventTypeSelect.selectOption(firstEventType)
348
+
349
+ // Wait for metrics to load using condition-based wait
350
+ await waitForMetricsLoaded(page)
351
+
352
+ // Should show either data, loading, error, or no-data
353
+ const hasData = await page.locator('.chart-container canvas').isVisible()
354
+ const isLoading = await page.locator('[data-testid="metrics-loading"]').isVisible()
355
+ const hasError = await page.locator('[data-testid="metrics-error"]').isVisible()
356
+ const noData = await page.locator('[data-testid="metrics-no-data"]').isVisible()
357
+
358
+ expect(hasData || isLoading || hasError || noData).toBe(true)
359
+ }
360
+ } else {
361
+ console.log('No event types available for this camera')
362
+ // Should show "no selection" message
363
+ const noSelection = await page.locator('[data-testid="metrics-no-selection"]').isVisible()
364
+ expect(noSelection).toBe(true)
365
+ }
366
+ }
367
+ } else {
368
+ console.log('No cameras available to test metrics chart')
369
+ }
370
+ })
371
+
372
+ test('alerts list loads after selecting camera', async ({ page }) => {
373
+ skipIfNoProxy()
374
+ skipIfNoCredentials()
375
+
376
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
377
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
378
+
379
+ // Wait for cameras to load using condition-based wait
380
+ await waitForCamerasLoaded(page)
381
+
382
+ // Select first camera if available
383
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
384
+ const options = page.locator('[data-testid="camera-option"]')
385
+ const optionCount = await options.count()
386
+
387
+ if (optionCount > 0) {
388
+ const firstCameraValue = await options.first().getAttribute('value')
389
+ if (firstCameraValue) {
390
+ await cameraSelect.selectOption(firstCameraValue)
391
+
392
+ // Wait for alerts to load using condition-based wait
393
+ await waitForAlertsLoaded(page)
394
+
395
+ // Check alerts list is displayed
396
+ const alertsList = page.locator('[data-testid="alerts-list"]')
397
+ await expect(alertsList).toBeVisible()
398
+
399
+ // Should show either items, loading, error, or no-data
400
+ const hasItems = await page.locator('[data-testid="alert-item"]').first().isVisible().catch(() => false)
401
+ const isLoading = await page.locator('[data-testid="alerts-loading"]').isVisible()
402
+ const hasError = await page.locator('[data-testid="alerts-error"]').isVisible()
403
+ const noData = await page.locator('[data-testid="alerts-no-data"]').isVisible()
404
+
405
+ expect(hasItems || isLoading || hasError || noData).toBe(true)
406
+ }
407
+ } else {
408
+ console.log('No cameras available to test alerts list')
409
+ }
410
+ })
411
+
412
+ test('notifications list loads after selecting camera', async ({ page }) => {
413
+ skipIfNoProxy()
414
+ skipIfNoCredentials()
415
+
416
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
417
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
418
+
419
+ // Wait for cameras to load using condition-based wait
420
+ await waitForCamerasLoaded(page)
421
+
422
+ // Select first camera if available
423
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
424
+ const options = page.locator('[data-testid="camera-option"]')
425
+ const optionCount = await options.count()
426
+
427
+ if (optionCount > 0) {
428
+ const firstCameraValue = await options.first().getAttribute('value')
429
+ if (firstCameraValue) {
430
+ await cameraSelect.selectOption(firstCameraValue)
431
+
432
+ // Wait for notifications to load using condition-based wait
433
+ await waitForNotificationsLoaded(page)
434
+
435
+ // Check notifications list is displayed
436
+ const notificationsList = page.locator('[data-testid="notifications-list"]')
437
+ await expect(notificationsList).toBeVisible()
438
+
439
+ // Should show either items, loading, error, or no-data
440
+ const hasItems = await page.locator('[data-testid="notification-item"]').first().isVisible().catch(() => false)
441
+ const isLoading = await page.locator('[data-testid="notifications-loading"]').isVisible()
442
+ const hasError = await page.locator('[data-testid="notifications-error"]').isVisible()
443
+ const noData = await page.locator('[data-testid="notifications-no-data"]').isVisible()
444
+
445
+ expect(hasItems || isLoading || hasError || noData).toBe(true)
446
+ }
447
+ } else {
448
+ console.log('No cameras available to test notifications list')
449
+ }
450
+ })
451
+
452
+ test('can logout after login', async ({ page }) => {
453
+ skipIfNoProxy()
454
+ skipIfNoCredentials()
455
+
456
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
457
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
458
+
459
+ await page.click('[data-testid="nav-logout"]')
460
+
461
+ await page.waitForURL('**/')
462
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
463
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
464
+ })
465
+
466
+ test('pagination works for alerts', async ({ page }) => {
467
+ skipIfNoProxy()
468
+ skipIfNoCredentials()
469
+
470
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
471
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
472
+
473
+ // Wait for cameras to load using condition-based wait
474
+ await waitForCamerasLoaded(page)
475
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
476
+ const options = page.locator('[data-testid="camera-option"]')
477
+ const optionCount = await options.count()
478
+
479
+ if (optionCount > 0) {
480
+ const firstCameraValue = await options.first().getAttribute('value')
481
+ if (firstCameraValue) {
482
+ await cameraSelect.selectOption(firstCameraValue)
483
+
484
+ // Use 7 day range for more alerts
485
+ await page.locator('[data-testid="time-range-7d"]').click()
486
+
487
+ // Wait for alerts to load using condition-based wait
488
+ await waitForAlertsLoaded(page)
489
+
490
+ // Check if load more button exists
491
+ const loadMoreButton = page.locator('[data-testid="alerts-load-more"]')
492
+ const hasLoadMore = await loadMoreButton.isVisible()
493
+
494
+ if (hasLoadMore) {
495
+ // Get current alert count
496
+ const alertsBefore = await page.locator('[data-testid="alert-item"]').count()
497
+
498
+ // Click load more and wait for more items to appear
499
+ await loadMoreButton.click()
500
+ await waitForAlertsLoaded(page)
501
+
502
+ // Verify more alerts loaded
503
+ const alertsAfter = await page.locator('[data-testid="alert-item"]').count()
504
+ expect(alertsAfter).toBeGreaterThanOrEqual(alertsBefore)
505
+ } else {
506
+ console.log('No Load More button for alerts (not enough alerts)')
507
+ }
508
+ }
509
+ } else {
510
+ console.log('No cameras available to test pagination')
511
+ }
512
+ })
513
+
514
+ test('pagination works for notifications', async ({ page }) => {
515
+ skipIfNoProxy()
516
+ skipIfNoCredentials()
517
+
518
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
519
+ await expect(page.locator('[data-testid="dashboard-container"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
520
+
521
+ // Wait for cameras to load using condition-based wait
522
+ await waitForCamerasLoaded(page)
523
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
524
+ const options = page.locator('[data-testid="camera-option"]')
525
+ const optionCount = await options.count()
526
+
527
+ if (optionCount > 0) {
528
+ const firstCameraValue = await options.first().getAttribute('value')
529
+ if (firstCameraValue) {
530
+ await cameraSelect.selectOption(firstCameraValue)
531
+
532
+ // Use 7 day range for more notifications
533
+ await page.locator('[data-testid="time-range-7d"]').click()
534
+
535
+ // Wait for notifications to load using condition-based wait
536
+ await waitForNotificationsLoaded(page)
537
+
538
+ // Check if load more button exists
539
+ const loadMoreButton = page.locator('[data-testid="notifications-load-more"]')
540
+ const hasLoadMore = await loadMoreButton.isVisible()
541
+
542
+ if (hasLoadMore) {
543
+ // Get current notification count
544
+ const notificationsBefore = await page.locator('[data-testid="notification-item"]').count()
545
+
546
+ // Click load more and wait for more items to appear
547
+ await loadMoreButton.click()
548
+ await waitForNotificationsLoaded(page)
549
+
550
+ // Verify more notifications loaded
551
+ const notificationsAfter = await page.locator('[data-testid="notification-item"]').count()
552
+ expect(notificationsAfter).toBeGreaterThanOrEqual(notificationsBefore)
553
+ } else {
554
+ console.log('No Load More button for notifications (not enough notifications)')
555
+ }
556
+ }
557
+ } else {
558
+ console.log('No cameras available to test pagination')
559
+ }
560
+ })
561
+ })
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>EEN Alerts & Metrics Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>