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.
- package/.claude/agents/een-devices-agent.md +50 -3
- package/.claude/agents/een-events-agent.md +14 -3
- package/CHANGELOG.md +12 -5
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +306 -2
- package/dist/index.js +410 -292
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +1 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +175 -78
- package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +40 -23
- package/docs/ai-reference/AI-EVENTS.md +8 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1 -1
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-cameras/cameras-screenshot.png +0 -0
- package/examples/vue-cameras/e2e/camera-details.spec.ts +547 -0
- package/examples/vue-cameras/e2e/camera-settings.spec.ts +424 -0
- package/examples/vue-cameras/src/views/CameraDetail.vue +17 -0
- package/examples/vue-cameras/src/views/Cameras.vue +261 -115
- package/examples/vue-cameras/src/views/Home.vue +7 -6
- package/examples/vue-events/src/components/EventsModal.vue +49 -0
- package/examples/vue-media/e2e/auth.spec.ts +21 -12
- package/package.json +1 -1
|
@@ -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;
|