een-api-toolkit 0.3.54 → 0.3.60

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.
@@ -0,0 +1,424 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+
3
+ /**
4
+ * E2E tests for the Camera Settings Modal
5
+ *
6
+ * Tests the "Settings" button on camera cards that opens a modal
7
+ * displaying the camera settings JSON response without navigating away.
8
+ *
9
+ * Required environment variables:
10
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
11
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
12
+ * - TEST_USER: Test user email
13
+ * - TEST_PASSWORD: Test user password
14
+ */
15
+
16
+ const TIMEOUTS = {
17
+ OAUTH_REDIRECT: 30000,
18
+ ELEMENT_VISIBLE: 15000,
19
+ PASSWORD_VISIBLE: 10000,
20
+ AUTH_COMPLETE: 30000,
21
+ UI_UPDATE: 10000,
22
+ PROXY_CHECK: 5000,
23
+ MODAL_CONTENT: 15000
24
+ } as const
25
+
26
+ const TEST_USER = process.env.TEST_USER
27
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
28
+ const PROXY_URL = process.env.VITE_PROXY_URL
29
+
30
+ async function isProxyAccessible(): Promise<boolean> {
31
+ if (!PROXY_URL) return false
32
+ const controller = new AbortController()
33
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
34
+
35
+ try {
36
+ const response = await fetch(PROXY_URL, {
37
+ method: 'HEAD',
38
+ signal: controller.signal
39
+ })
40
+ return response.ok || response.status === 404
41
+ } catch {
42
+ return false
43
+ } finally {
44
+ clearTimeout(timeoutId)
45
+ }
46
+ }
47
+
48
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
49
+ await page.goto('/')
50
+
51
+ await page.click('[data-testid="login-button"]')
52
+ await page.waitForURL('/login')
53
+
54
+ await Promise.all([
55
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
56
+ page.click('button:has-text("Login with Eagle Eye Networks")')
57
+ ])
58
+
59
+ const emailInput = page.locator('#authentication--input__email')
60
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
61
+ await emailInput.fill(username)
62
+
63
+ await page.getByRole('button', { name: 'Next' }).click()
64
+
65
+ const passwordInput = page.locator('#authentication--input__password')
66
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
67
+ await passwordInput.fill(password)
68
+
69
+ await page.locator('#next, button:has-text("Sign in")').first().click()
70
+
71
+ await page.waitForURL('**/cameras', { timeout: TIMEOUTS.AUTH_COMPLETE })
72
+ }
73
+
74
+ async function clearAuthState(page: Page): Promise<void> {
75
+ try {
76
+ const url = page.url()
77
+ if (url && url.startsWith('http')) {
78
+ await page.evaluate(() => {
79
+ try {
80
+ localStorage.clear()
81
+ sessionStorage.clear()
82
+ } catch {
83
+ // Ignore
84
+ }
85
+ })
86
+ }
87
+ } catch {
88
+ // Ignore
89
+ }
90
+ }
91
+
92
+ async function loginAndNavigateToCameras(page: Page): Promise<void> {
93
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
94
+ await expect(page.locator('[data-testid="nav-cameras"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
95
+
96
+ // Wait for camera grid or no-cameras message to appear
97
+ await expect(page.locator('.camera-grid, .no-cameras')).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
98
+ }
99
+
100
+ test.describe('Camera Settings Modal', () => {
101
+ let proxyAccessible = false
102
+
103
+ function skipIfNotReady() {
104
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
105
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
106
+ }
107
+
108
+ test.beforeAll(async () => {
109
+ proxyAccessible = await isProxyAccessible()
110
+ if (!proxyAccessible) {
111
+ console.log('OAuth proxy not accessible - camera settings tests will be skipped')
112
+ }
113
+ })
114
+
115
+ test.afterEach(async ({ page }) => {
116
+ await clearAuthState(page)
117
+ })
118
+
119
+ test('each camera card has a Settings button', async ({ page }) => {
120
+ skipIfNotReady()
121
+
122
+ await loginAndNavigateToCameras(page)
123
+
124
+ const cameraCards = page.locator('.camera-card')
125
+ const cardCount = await cameraCards.count()
126
+ test.skip(cardCount === 0, 'No cameras available to test')
127
+
128
+ // Every card should have a Settings button
129
+ const settingsButtons = page.locator('[data-testid="settings-btn"]')
130
+ await expect(settingsButtons).toHaveCount(cardCount)
131
+
132
+ // All buttons should display "Settings" text
133
+ for (let i = 0; i < cardCount; i++) {
134
+ await expect(settingsButtons.nth(i)).toHaveText('Settings')
135
+ }
136
+ })
137
+
138
+ test('clicking Settings opens modal with JSON content', async ({ page }) => {
139
+ skipIfNotReady()
140
+
141
+ await loginAndNavigateToCameras(page)
142
+
143
+ const cameraCards = page.locator('.camera-card')
144
+ const cardCount = await cameraCards.count()
145
+ test.skip(cardCount === 0, 'No cameras available to test')
146
+
147
+ // Settings modal should not be visible initially
148
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
149
+
150
+ // Click the first Settings button
151
+ await page.locator('[data-testid="settings-btn"]').first().click()
152
+
153
+ // Modal should appear
154
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
155
+ await expect(page.locator('[data-testid="settings-modal-content"]')).toBeVisible()
156
+
157
+ // Wait for loading to finish and JSON to appear
158
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
159
+
160
+ // JSON should contain camera settings data (data field is always present)
161
+ const jsonText = await page.locator('[data-testid="settings-modal-json"]').textContent()
162
+ expect(jsonText).toBeTruthy()
163
+ const parsed = JSON.parse(jsonText!)
164
+ expect(parsed).toHaveProperty('data')
165
+ })
166
+
167
+ test('clicking Settings does not navigate away from cameras page', async ({ page }) => {
168
+ skipIfNotReady()
169
+
170
+ await loginAndNavigateToCameras(page)
171
+
172
+ const cardCount = await page.locator('.camera-card').count()
173
+ test.skip(cardCount === 0, 'No cameras available to test')
174
+
175
+ // Record the URL before clicking
176
+ const urlBefore = page.url()
177
+
178
+ // Click Settings button
179
+ await page.locator('[data-testid="settings-btn"]').first().click()
180
+
181
+ // Wait for modal
182
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
183
+
184
+ // URL should remain the same (no navigation)
185
+ expect(page.url()).toBe(urlBefore)
186
+ })
187
+
188
+ test('modal close button (X) closes the modal', async ({ page }) => {
189
+ skipIfNotReady()
190
+
191
+ await loginAndNavigateToCameras(page)
192
+
193
+ const cardCount = await page.locator('.camera-card').count()
194
+ test.skip(cardCount === 0, 'No cameras available to test')
195
+
196
+ // Open modal
197
+ await page.locator('[data-testid="settings-btn"]').first().click()
198
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
199
+
200
+ // Wait for content to load
201
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
202
+
203
+ // Click the X close button
204
+ await page.locator('[data-testid="settings-modal-close-x"]').click()
205
+
206
+ // Modal should be gone
207
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
208
+ })
209
+
210
+ test('modal footer Close button closes the modal', async ({ page }) => {
211
+ skipIfNotReady()
212
+
213
+ await loginAndNavigateToCameras(page)
214
+
215
+ const cardCount = await page.locator('.camera-card').count()
216
+ test.skip(cardCount === 0, 'No cameras available to test')
217
+
218
+ // Open modal
219
+ await page.locator('[data-testid="settings-btn"]').first().click()
220
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
221
+
222
+ // Wait for content to load
223
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
224
+
225
+ // Click the footer Close button
226
+ await page.locator('[data-testid="settings-modal-close-btn"]').click()
227
+
228
+ // Modal should be gone
229
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
230
+ })
231
+
232
+ test('clicking overlay background closes the modal', async ({ page }) => {
233
+ skipIfNotReady()
234
+
235
+ await loginAndNavigateToCameras(page)
236
+
237
+ const cardCount = await page.locator('.camera-card').count()
238
+ test.skip(cardCount === 0, 'No cameras available to test')
239
+
240
+ // Open modal
241
+ await page.locator('[data-testid="settings-btn"]').first().click()
242
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
243
+
244
+ // Wait for content to load
245
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
246
+
247
+ // Click on the overlay (outside the modal content) - top-left corner
248
+ await page.locator('[data-testid="settings-modal-overlay"]').click({ position: { x: 5, y: 5 } })
249
+
250
+ // Modal should be gone
251
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
252
+ })
253
+
254
+ test('modal JSON includes schema when requested', async ({ page }) => {
255
+ skipIfNotReady()
256
+
257
+ await loginAndNavigateToCameras(page)
258
+
259
+ const cardCount = await page.locator('.camera-card').count()
260
+ test.skip(cardCount === 0, 'No cameras available to test')
261
+
262
+ // Open modal
263
+ await page.locator('[data-testid="settings-btn"]').first().click()
264
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
265
+
266
+ // Parse the JSON and check for schema
267
+ const jsonText = await page.locator('[data-testid="settings-modal-json"]').textContent()
268
+ const parsed = JSON.parse(jsonText!)
269
+
270
+ // Schema should be present since the component requests it via include
271
+ expect(parsed).toHaveProperty('schema')
272
+ })
273
+
274
+ test('modal JSON includes data, schema, and proposedValues', async ({ page }) => {
275
+ skipIfNotReady()
276
+
277
+ await loginAndNavigateToCameras(page)
278
+
279
+ const cardCount = await page.locator('.camera-card').count()
280
+ test.skip(cardCount === 0, 'No cameras available to test')
281
+
282
+ // Open settings modal
283
+ await page.locator('[data-testid="settings-btn"]').first().click()
284
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
285
+
286
+ // Parse the JSON
287
+ const jsonText = await page.locator('[data-testid="settings-modal-json"]').textContent()
288
+ const parsed = JSON.parse(jsonText!)
289
+
290
+ // The settings response must always have a data property with actual settings
291
+ expect(parsed).toHaveProperty('data')
292
+ expect(parsed.data).toBeTruthy()
293
+ expect(typeof parsed.data).toBe('object')
294
+
295
+ // Both schema and proposedValues should be present since the
296
+ // component requests include: ['schema', 'proposedValues']
297
+ expect(parsed).toHaveProperty('schema')
298
+ expect(parsed).toHaveProperty('proposedValues')
299
+
300
+ // Verify the modal includes line confirms both include params
301
+ const includesText = await page.locator('[data-testid="settings-modal-includes"]').textContent()
302
+ expect(includesText).toContain('schema')
303
+ expect(includesText).toContain('proposedValues')
304
+ })
305
+
306
+ test('modal displays header with title and includes', async ({ page }) => {
307
+ skipIfNotReady()
308
+
309
+ await loginAndNavigateToCameras(page)
310
+
311
+ const cardCount = await page.locator('.camera-card').count()
312
+ test.skip(cardCount === 0, 'No cameras available to test')
313
+
314
+ // Open modal
315
+ await page.locator('[data-testid="settings-btn"]').first().click()
316
+ await expect(page.locator('[data-testid="settings-modal-content"]')).toBeVisible()
317
+
318
+ // Check modal header
319
+ await expect(page.locator('[data-testid="settings-modal-content"] h3')).toHaveText('Camera Settings')
320
+
321
+ // Check include strings are displayed
322
+ const includes = page.locator('[data-testid="settings-modal-includes"]')
323
+ await expect(includes).toBeVisible()
324
+ await expect(includes).toContainText('schema')
325
+ await expect(includes).toContainText('proposedValues')
326
+ })
327
+
328
+ test('can close settings modal and navigate home', async ({ page }) => {
329
+ skipIfNotReady()
330
+
331
+ await loginAndNavigateToCameras(page)
332
+
333
+ const cardCount = await page.locator('.camera-card').count()
334
+ test.skip(cardCount === 0, 'No cameras available to test')
335
+
336
+ // Open the modal
337
+ await page.locator('[data-testid="settings-btn"]').first().click()
338
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
339
+
340
+ // Close the modal
341
+ await page.locator('[data-testid="settings-modal-close-btn"]').click()
342
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
343
+
344
+ // Navigate home
345
+ await page.click('[data-testid="nav-home"]')
346
+ await page.waitForURL('/')
347
+
348
+ // Verify we're on the home page
349
+ await expect(page.locator('[data-testid="authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
350
+ await expect(page.locator('h2')).toContainText('Welcome')
351
+ })
352
+
353
+ test('can open settings for multiple cameras sequentially', async ({ page }) => {
354
+ skipIfNotReady()
355
+
356
+ await loginAndNavigateToCameras(page)
357
+
358
+ const cardCount = await page.locator('.camera-card').count()
359
+ test.skip(cardCount < 2, 'Need at least 2 cameras to test')
360
+
361
+ // Open first camera settings
362
+ await page.locator('[data-testid="settings-btn"]').nth(0).click()
363
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
364
+
365
+ const firstJson = await page.locator('[data-testid="settings-modal-json"]').textContent()
366
+ expect(firstJson).toBeTruthy()
367
+
368
+ // Close modal
369
+ await page.locator('[data-testid="settings-modal-close-btn"]').click()
370
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
371
+
372
+ // Open second camera settings
373
+ await page.locator('[data-testid="settings-btn"]').nth(1).click()
374
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
375
+
376
+ const secondJson = await page.locator('[data-testid="settings-modal-json"]').textContent()
377
+ expect(secondJson).toBeTruthy()
378
+ })
379
+
380
+ test('full workflow: login, cameras, open settings, verify, close, go home', async ({ page }) => {
381
+ skipIfNotReady()
382
+
383
+ // 1. Start at home page (not authenticated)
384
+ await page.goto('/')
385
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
386
+
387
+ // 2. Login
388
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
389
+ await expect(page.locator('[data-testid="nav-cameras"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
390
+
391
+ // 3. Navigate to cameras
392
+ await page.click('[data-testid="nav-cameras"]')
393
+ await page.waitForURL('/cameras')
394
+ await expect(page.locator('.camera-grid, .no-cameras')).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
395
+
396
+ const cardCount = await page.locator('.camera-card').count()
397
+ test.skip(cardCount === 0, 'No cameras available to test')
398
+
399
+ // 4. Open settings modal
400
+ await page.locator('[data-testid="settings-btn"]').first().click()
401
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).toBeVisible()
402
+ await expect(page.locator('[data-testid="settings-modal-json"]')).toBeVisible({ timeout: TIMEOUTS.MODAL_CONTENT })
403
+
404
+ // 5. Verify JSON is valid and has data property
405
+ const jsonText = await page.locator('[data-testid="settings-modal-json"]').textContent()
406
+ expect(jsonText).toBeTruthy()
407
+ const parsed = JSON.parse(jsonText!)
408
+ expect(parsed).toHaveProperty('data')
409
+
410
+ // 6. Close the modal
411
+ await page.locator('[data-testid="settings-modal-close-btn"]').click()
412
+ await expect(page.locator('[data-testid="settings-modal-overlay"]')).not.toBeVisible()
413
+
414
+ // 7. Verify still on cameras page
415
+ await expect(page).toHaveURL(/\/cameras$/)
416
+ await expect(page.locator('.camera-grid')).toBeVisible()
417
+
418
+ // 8. Navigate home
419
+ await page.click('[data-testid="nav-home"]')
420
+ await page.waitForURL('/')
421
+ await expect(page.locator('[data-testid="authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
422
+ await expect(page.locator('h2')).toContainText('Welcome')
423
+ })
424
+ })
@@ -194,6 +194,12 @@ watch(
194
194
  <dt v-if="camera.devicePosition.latitude !== undefined">Coordinates</dt>
195
195
  <dd v-if="camera.devicePosition.latitude !== undefined">
196
196
  {{ camera.devicePosition.latitude }}, {{ camera.devicePosition.longitude }}
197
+ <a
198
+ :href="`https://www.google.com/maps/search/?api=1&query=${camera.devicePosition.latitude},${camera.devicePosition.longitude}`"
199
+ target="_blank"
200
+ rel="noopener noreferrer"
201
+ class="map-link"
202
+ >View on Google Maps</a>
197
203
  </dd>
198
204
 
199
205
  <dt v-if="camera.devicePosition.floor !== undefined">Floor</dt>
@@ -338,6 +344,17 @@ dd {
338
344
  color: #333;
339
345
  }
340
346
 
347
+ .map-link {
348
+ margin-left: 8px;
349
+ color: #42b883;
350
+ text-decoration: none;
351
+ font-size: 0.85rem;
352
+ }
353
+
354
+ .map-link:hover {
355
+ text-decoration: underline;
356
+ }
357
+
341
358
  .tag {
342
359
  display: inline-block;
343
360
  background: #e0e0e0;