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.
- package/CHANGELOG.md +10 -40
- package/README.md +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +825 -0
- package/dist/index.js +489 -254
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +314 -2
- package/examples/vue-alerts-metrics/README.md +136 -0
- package/examples/vue-alerts-metrics/e2e/app.spec.ts +74 -0
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +561 -0
- package/examples/vue-alerts-metrics/index.html +13 -0
- package/examples/vue-alerts-metrics/package-lock.json +1756 -0
- package/examples/vue-alerts-metrics/package.json +31 -0
- package/examples/vue-alerts-metrics/playwright.config.ts +46 -0
- package/examples/vue-alerts-metrics/src/App.vue +108 -0
- package/examples/vue-alerts-metrics/src/components/AlertsList.vue +881 -0
- package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +106 -0
- package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +336 -0
- package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +825 -0
- package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +259 -0
- package/examples/vue-alerts-metrics/src/composables/useHlsPlayer.ts +285 -0
- package/examples/vue-alerts-metrics/src/main.ts +23 -0
- package/examples/vue-alerts-metrics/src/router/index.ts +61 -0
- package/examples/vue-alerts-metrics/src/views/Callback.vue +76 -0
- package/examples/vue-alerts-metrics/src/views/Dashboard.vue +174 -0
- package/examples/vue-alerts-metrics/src/views/Home.vue +216 -0
- package/examples/vue-alerts-metrics/src/views/Login.vue +33 -0
- package/examples/vue-alerts-metrics/src/views/Logout.vue +66 -0
- package/examples/vue-alerts-metrics/src/vite-env.d.ts +12 -0
- package/examples/vue-alerts-metrics/tsconfig.json +21 -0
- package/examples/vue-alerts-metrics/tsconfig.node.json +10 -0
- package/examples/vue-alerts-metrics/vite.config.ts +12 -0
- 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>
|