een-api-toolkit 0.1.13 → 0.2.0
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 +45 -8
- package/README.md +38 -0
- package/docs/AI-CONTEXT.md +1 -1
- package/examples/vue-bridges/README.md +126 -0
- package/examples/vue-bridges/bridges-screenshot.png +0 -0
- package/examples/vue-bridges/package-lock.json +130 -42
- package/examples/vue-bridges/package.json +2 -2
- package/examples/vue-cameras/README.md +142 -0
- package/examples/vue-cameras/cameras-screenshot.png +0 -0
- package/examples/vue-cameras/package-lock.json +130 -42
- package/examples/vue-cameras/package.json +2 -2
- package/examples/vue-feeds/README.md +162 -0
- package/examples/vue-feeds/e2e/auth.spec.ts +182 -444
- package/examples/vue-feeds/feeds-screenshot.png +0 -0
- package/examples/vue-feeds/package-lock.json +130 -42
- package/examples/vue-feeds/package.json +2 -2
- package/examples/vue-feeds/playwright.config.ts +28 -7
- package/examples/vue-media/README.md +187 -0
- package/examples/vue-media/e2e/auth.spec.ts +218 -298
- package/examples/vue-media/media-screenshot.png +0 -0
- package/examples/vue-media/package-lock.json +130 -42
- package/examples/vue-media/package.json +2 -2
- package/examples/vue-media/playwright.config.ts +28 -7
- package/examples/vue-users/README.md +58 -15
- package/examples/vue-users/package-lock.json +132 -44
- package/examples/vue-users/package.json +3 -3
- package/examples/vue-users/users-screenshot.png +0 -0
- package/package.json +12 -11
|
@@ -1,373 +1,293 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* E2E tests for the Vue Media Example - OAuth Login Flow
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Tests the OAuth login flow through the UI:
|
|
5
|
+
* 1. Click login button in the example app
|
|
6
|
+
* 2. Enter credentials on EEN OAuth page
|
|
7
|
+
* 3. Complete the OAuth callback
|
|
8
|
+
* 4. Verify authenticated state and media functionality
|
|
9
|
+
*
|
|
10
|
+
* Required environment variables:
|
|
11
|
+
* - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
|
|
12
|
+
* - VITE_EEN_CLIENT_ID: EEN OAuth client ID
|
|
13
|
+
* - TEST_USER: Test user email
|
|
14
|
+
* - TEST_PASSWORD: Test user password
|
|
15
|
+
*
|
|
16
|
+
* Note: Helper functions (isProxyAccessible, performLogin, clearAuthState) are
|
|
17
|
+
* intentionally duplicated in each example's auth.spec.ts to avoid Playwright's
|
|
18
|
+
* "Requiring @playwright/test second time" error that occurs when importing
|
|
19
|
+
* from a shared file outside the example directory.
|
|
8
20
|
*/
|
|
9
21
|
|
|
10
|
-
import { test, expect } from '@playwright/test'
|
|
11
|
-
import {
|
|
22
|
+
import { test, expect, Page } from '@playwright/test'
|
|
23
|
+
import { baseURL } from '../playwright.config'
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
const TIMEOUTS = {
|
|
26
|
+
OAUTH_REDIRECT: 30000,
|
|
27
|
+
ELEMENT_VISIBLE: 15000,
|
|
28
|
+
PASSWORD_VISIBLE: 10000,
|
|
29
|
+
AUTH_COMPLETE: 30000,
|
|
30
|
+
UI_UPDATE: 10000,
|
|
31
|
+
PROXY_CHECK: 5000,
|
|
32
|
+
MEDIA_LOAD: 30000
|
|
33
|
+
} as const
|
|
15
34
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
})
|
|
35
|
+
const TEST_USER = process.env.TEST_USER
|
|
36
|
+
const TEST_PASSWORD = process.env.TEST_PASSWORD
|
|
37
|
+
const PROXY_URL = process.env.VITE_PROXY_URL
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Checks if the OAuth proxy server is accessible.
|
|
41
|
+
* Returns false if proxy is unavailable, allowing tests to be skipped gracefully.
|
|
42
|
+
*/
|
|
43
|
+
async function isProxyAccessible(): Promise<boolean> {
|
|
44
|
+
if (!PROXY_URL) return false
|
|
45
|
+
const controller = new AbortController()
|
|
46
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(PROXY_URL, {
|
|
50
|
+
method: 'HEAD',
|
|
51
|
+
signal: controller.signal
|
|
52
|
+
})
|
|
53
|
+
return response.ok || response.status === 404
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!process.env.CI) {
|
|
56
|
+
console.log('Proxy check failed:', error instanceof Error ? error.message : error)
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timeoutId)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
26
63
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Performs OAuth login flow through the EEN authentication page.
|
|
66
|
+
* Handles two-step navigation: app login page -> EEN OAuth -> callback
|
|
67
|
+
*/
|
|
68
|
+
async function performLogin(page: Page, username: string, password: string): Promise<void> {
|
|
69
|
+
await page.goto('/')
|
|
30
70
|
|
|
31
|
-
|
|
32
|
-
|
|
71
|
+
// Click login button on home page to go to login page
|
|
72
|
+
await page.click('[data-testid="login-button"]')
|
|
73
|
+
await page.waitForURL('/login')
|
|
33
74
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
75
|
+
// Click login button on login page to trigger OAuth
|
|
76
|
+
await Promise.all([
|
|
77
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
78
|
+
page.getByRole('button', { name: 'Login with Eagle Eye Networks' }).click()
|
|
79
|
+
])
|
|
38
80
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
81
|
+
// EEN OAuth page selectors - these depend on EEN's login UI and may need
|
|
82
|
+
// updates if EEN changes their authentication page structure
|
|
83
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
84
|
+
await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
85
|
+
await emailInput.fill(username)
|
|
42
86
|
|
|
43
|
-
|
|
44
|
-
const hasNoCameras = await noCameras.isVisible().catch(() => false)
|
|
87
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
45
88
|
|
|
46
|
-
|
|
89
|
+
const passwordInput = page.locator('#authentication--input__password')
|
|
90
|
+
await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
|
|
91
|
+
await passwordInput.fill(password)
|
|
47
92
|
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
// EEN uses either #next or "Sign in" button depending on login flow
|
|
94
|
+
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
50
95
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
96
|
+
// Wait for redirect back to the app using configured baseURL
|
|
97
|
+
const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
98
|
+
await page.waitForURL(baseURLPattern, { timeout: TIMEOUTS.AUTH_COMPLETE })
|
|
99
|
+
}
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Clears authentication state from browser storage.
|
|
103
|
+
* Used in afterEach to ensure test isolation.
|
|
104
|
+
*/
|
|
105
|
+
async function clearAuthState(page: Page): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
const url = page.url()
|
|
108
|
+
if (url && url.startsWith('http')) {
|
|
109
|
+
await page.evaluate(() => {
|
|
110
|
+
try {
|
|
111
|
+
localStorage.clear()
|
|
112
|
+
sessionStorage.clear()
|
|
113
|
+
} catch {
|
|
114
|
+
// Storage access may fail in certain contexts
|
|
115
|
+
}
|
|
58
116
|
})
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!process.env.CI) {
|
|
120
|
+
console.log('Clear auth state failed:', error instanceof Error ? error.message : error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
59
124
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
125
|
+
test.describe('Vue Media Example - Auth', () => {
|
|
126
|
+
let proxyAccessible = false
|
|
127
|
+
|
|
128
|
+
function skipIfNoProxy() {
|
|
129
|
+
test.skip(!proxyAccessible, 'OAuth proxy not accessible')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function skipIfNoCredentials() {
|
|
133
|
+
test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test.beforeAll(async () => {
|
|
137
|
+
proxyAccessible = await isProxyAccessible()
|
|
138
|
+
if (!proxyAccessible) {
|
|
139
|
+
console.log('OAuth proxy not accessible - OAuth tests will be skipped')
|
|
73
140
|
}
|
|
74
141
|
})
|
|
75
142
|
|
|
76
|
-
test(
|
|
77
|
-
|
|
78
|
-
|
|
143
|
+
test.afterEach(async ({ page }) => {
|
|
144
|
+
await clearAuthState(page)
|
|
145
|
+
})
|
|
79
146
|
|
|
80
|
-
|
|
81
|
-
await
|
|
82
|
-
await expect(page.getByTestId('
|
|
83
|
-
await expect(page.
|
|
147
|
+
test('shows login button when not authenticated', async ({ page }) => {
|
|
148
|
+
await page.goto('/')
|
|
149
|
+
await expect(page.getByTestId('not-authenticated')).toBeVisible()
|
|
150
|
+
await expect(page.getByTestId('nav-login')).toBeVisible()
|
|
84
151
|
})
|
|
85
152
|
|
|
86
|
-
test('
|
|
87
|
-
// Navigate to live page (auth already injected by beforeEach)
|
|
153
|
+
test('live page redirects to login when not authenticated', async ({ page }) => {
|
|
88
154
|
await page.goto('/live')
|
|
155
|
+
await expect(page).toHaveURL('/login')
|
|
156
|
+
})
|
|
89
157
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
|
|
158
|
+
test('login button redirects to OAuth page', async ({ page }) => {
|
|
159
|
+
skipIfNoProxy()
|
|
160
|
+
skipIfNoCredentials()
|
|
96
161
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
await page.waitForSelector('[data-testid="live-image"], .no-image', {
|
|
100
|
-
timeout: 30000
|
|
101
|
-
})
|
|
162
|
+
await page.goto('/')
|
|
163
|
+
await expect(page.getByTestId('login-button')).toBeVisible()
|
|
102
164
|
|
|
103
|
-
|
|
104
|
-
|
|
165
|
+
// Click login button on home page to go to login page
|
|
166
|
+
await page.click('[data-testid="login-button"]')
|
|
167
|
+
await page.waitForURL('/login')
|
|
105
168
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
169
|
+
// Click login button on login page to trigger OAuth
|
|
170
|
+
await Promise.all([
|
|
171
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
172
|
+
page.getByRole('button', { name: 'Login with Eagle Eye Networks' }).click()
|
|
173
|
+
])
|
|
109
174
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
175
|
+
// EEN OAuth page selector
|
|
176
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
177
|
+
await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
114
178
|
})
|
|
115
179
|
|
|
116
|
-
test('
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Should show authenticated state with both buttons
|
|
121
|
-
await expect(page.getByTestId('authenticated')).toBeVisible()
|
|
122
|
-
await expect(page.getByTestId('view-live-button')).toBeVisible()
|
|
123
|
-
await expect(page.getByTestId('view-recorded-button')).toBeVisible()
|
|
124
|
-
})
|
|
180
|
+
test('complete OAuth login flow and view media', async ({ page }) => {
|
|
181
|
+
skipIfNoProxy()
|
|
182
|
+
skipIfNoCredentials()
|
|
125
183
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await page.goto('/recorded')
|
|
184
|
+
await page.goto('/')
|
|
185
|
+
await expect(page.getByTestId('not-authenticated')).toBeVisible()
|
|
129
186
|
|
|
130
|
-
|
|
131
|
-
await expect(page.getByRole('heading', { name: 'Recorded Images' })).toBeVisible()
|
|
187
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
132
188
|
|
|
133
|
-
|
|
134
|
-
await page.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
|
|
189
|
+
await expect(page.getByTestId('not-authenticated')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
190
|
+
await expect(page.getByTestId('nav-live')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
191
|
+
await expect(page.getByTestId('nav-logout')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
192
|
+
})
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
194
|
+
test('can view live camera after login', async ({ page }) => {
|
|
195
|
+
skipIfNoProxy()
|
|
196
|
+
skipIfNoCredentials()
|
|
142
197
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await expect(page.getByTestId('go-button')).toBeVisible()
|
|
146
|
-
await expect(page.getByTestId('prev-button')).toBeVisible()
|
|
147
|
-
await expect(page.getByTestId('next-button')).toBeVisible()
|
|
198
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
199
|
+
await expect(page.getByTestId('nav-live')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
148
200
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const nextDisabled = await page.getByTestId('next-button').isDisabled()
|
|
152
|
-
console.log(`Navigation buttons: prev=${prevDisabled ? 'disabled' : 'enabled'}, next=${nextDisabled ? 'disabled' : 'enabled'}`)
|
|
153
|
-
} else {
|
|
154
|
-
console.log('No cameras in account')
|
|
155
|
-
}
|
|
156
|
-
})
|
|
201
|
+
await page.click('[data-testid="nav-live"]')
|
|
202
|
+
await page.waitForURL('/live')
|
|
157
203
|
|
|
158
|
-
|
|
159
|
-
// Navigate to recorded page (auth already injected by beforeEach)
|
|
160
|
-
await page.goto('/recorded')
|
|
204
|
+
await expect(page.getByRole('heading', { name: 'Live Camera View' })).toBeVisible()
|
|
161
205
|
|
|
162
206
|
// Wait for cameras to load
|
|
163
207
|
await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
|
|
164
|
-
timeout:
|
|
208
|
+
timeout: TIMEOUTS.MEDIA_LOAD
|
|
165
209
|
})
|
|
210
|
+
})
|
|
166
211
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Wait for image to load (auto-loads on page mount with default time of 1 hour ago)
|
|
171
|
-
await page.waitForSelector('[data-testid="recorded-image"], .no-image, .error', {
|
|
172
|
-
timeout: 45000
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const hasImage = await page.getByTestId('recorded-image').isVisible().catch(() => false)
|
|
212
|
+
test('live page shows camera controls when cameras available', async ({ page }) => {
|
|
213
|
+
skipIfNoProxy()
|
|
214
|
+
skipIfNoCredentials()
|
|
176
215
|
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
217
|
+
await expect(page.getByTestId('nav-live')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
179
218
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await expect(timestamp).toBeVisible()
|
|
219
|
+
await page.click('[data-testid="nav-live"]')
|
|
220
|
+
await page.waitForURL('/live')
|
|
183
221
|
|
|
184
|
-
// Time picker should be updated with the image timestamp
|
|
185
|
-
const datetimeInput = page.getByTestId('datetime-input')
|
|
186
|
-
const inputValue = await datetimeInput.inputValue()
|
|
187
|
-
expect(inputValue).toBeTruthy()
|
|
188
|
-
console.log(`Time picker value: ${inputValue}`)
|
|
189
|
-
} else {
|
|
190
|
-
console.log('No recorded image available (no recordings in time range)')
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
console.log('No cameras to test with')
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
test('recorded image navigation buttons work', async ({ page }) => {
|
|
198
|
-
// Navigate to recorded page (auth already injected by beforeEach)
|
|
199
|
-
await page.goto('/recorded')
|
|
200
|
-
|
|
201
|
-
// Wait for cameras to load
|
|
202
222
|
await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
|
|
203
|
-
timeout:
|
|
223
|
+
timeout: TIMEOUTS.MEDIA_LOAD
|
|
204
224
|
})
|
|
205
225
|
|
|
206
226
|
const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
|
|
207
|
-
|
|
208
227
|
if (hasCameras) {
|
|
209
|
-
|
|
210
|
-
await page.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
await expect(page.getByTestId('refresh-button')).toBeVisible()
|
|
229
|
+
await expect(page.getByTestId('auto-refresh-button')).toBeVisible()
|
|
230
|
+
console.log('Camera controls visible')
|
|
231
|
+
} else {
|
|
232
|
+
console.log('No cameras in account - skipping camera-specific checks')
|
|
233
|
+
}
|
|
234
|
+
})
|
|
215
235
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const isNextEnabled = !(await nextButton.isDisabled())
|
|
236
|
+
test('can view recorded images after login', async ({ page }) => {
|
|
237
|
+
skipIfNoProxy()
|
|
238
|
+
skipIfNoCredentials()
|
|
220
239
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const initialTimestamp = await page.getByTestId('timestamp').textContent()
|
|
240
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
241
|
+
await expect(page.getByTestId('nav-recorded')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
224
242
|
|
|
225
|
-
|
|
226
|
-
|
|
243
|
+
await page.click('[data-testid="nav-recorded"]')
|
|
244
|
+
await page.waitForURL('/recorded')
|
|
227
245
|
|
|
228
|
-
|
|
229
|
-
await page.waitForTimeout(2000)
|
|
246
|
+
await expect(page.getByRole('heading', { name: 'Recorded Images' })).toBeVisible()
|
|
230
247
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
248
|
+
// Wait for cameras to load
|
|
249
|
+
await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
|
|
250
|
+
timeout: TIMEOUTS.MEDIA_LOAD
|
|
251
|
+
})
|
|
252
|
+
})
|
|
234
253
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
test('recorded page shows navigation controls when cameras available', async ({ page }) => {
|
|
255
|
+
skipIfNoProxy()
|
|
256
|
+
skipIfNoCredentials()
|
|
238
257
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
await prevButton.click()
|
|
242
|
-
await page.waitForTimeout(2000)
|
|
243
|
-
console.log('Previous navigation successful')
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
console.log('Next button disabled - only one image available')
|
|
247
|
-
}
|
|
248
|
-
} else {
|
|
249
|
-
console.log('No recorded image to navigate from')
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
console.log('No cameras to test navigation with')
|
|
253
|
-
}
|
|
254
|
-
})
|
|
258
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
259
|
+
await expect(page.getByTestId('nav-recorded')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
255
260
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await page.goto('/recorded')
|
|
261
|
+
await page.click('[data-testid="nav-recorded"]')
|
|
262
|
+
await page.waitForURL('/recorded')
|
|
259
263
|
|
|
260
|
-
// Wait for cameras to load
|
|
261
264
|
await page.waitForSelector('[data-testid="camera-select"], .no-cameras', {
|
|
262
|
-
timeout:
|
|
265
|
+
timeout: TIMEOUTS.MEDIA_LOAD
|
|
263
266
|
})
|
|
264
267
|
|
|
265
268
|
const hasCameras = await page.getByTestId('camera-select').isVisible().catch(() => false)
|
|
266
|
-
|
|
267
269
|
if (hasCameras) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const hours = String(yesterday.getHours()).padStart(2, '0')
|
|
274
|
-
const minutes = String(yesterday.getMinutes()).padStart(2, '0')
|
|
275
|
-
const dateTimeValue = `${year}-${month}-${day}T${hours}:${minutes}`
|
|
276
|
-
|
|
277
|
-
// Fill in the datetime input with yesterday's date
|
|
278
|
-
await page.getByTestId('datetime-input').fill(dateTimeValue)
|
|
279
|
-
|
|
280
|
-
// Click Go button to load image from that time
|
|
281
|
-
await page.getByTestId('go-button').click()
|
|
282
|
-
|
|
283
|
-
// Wait for the image to load or error to appear
|
|
284
|
-
await page.waitForSelector('[data-testid="recorded-image"], .error, .no-image', {
|
|
285
|
-
timeout: 45000
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// Check results
|
|
289
|
-
const hasImage = await page.getByTestId('recorded-image').isVisible().catch(() => false)
|
|
290
|
-
const hasError = await page.locator('.error').isVisible().catch(() => false)
|
|
291
|
-
|
|
292
|
-
if (hasImage) {
|
|
293
|
-
console.log('Image loaded successfully with past date')
|
|
294
|
-
// Check timestamp is shown
|
|
295
|
-
const timestamp = page.getByTestId('timestamp')
|
|
296
|
-
await expect(timestamp).toBeVisible()
|
|
297
|
-
} else if (hasError) {
|
|
298
|
-
const errorText = await page.locator('.error').textContent()
|
|
299
|
-
console.log('Error loading image with past date:', errorText)
|
|
300
|
-
// A 404 "Not found" is acceptable if no recordings exist for that time
|
|
301
|
-
expect(errorText).toMatch(/Not found|No recordings|Unknown error/)
|
|
302
|
-
} else {
|
|
303
|
-
console.log('No image or error visible - no recordings for past date')
|
|
304
|
-
}
|
|
270
|
+
await expect(page.getByTestId('datetime-input')).toBeVisible()
|
|
271
|
+
await expect(page.getByTestId('go-button')).toBeVisible()
|
|
272
|
+
await expect(page.getByTestId('prev-button')).toBeVisible()
|
|
273
|
+
await expect(page.getByTestId('next-button')).toBeVisible()
|
|
274
|
+
console.log('Recorded image controls visible')
|
|
305
275
|
} else {
|
|
306
|
-
console.log('No cameras
|
|
276
|
+
console.log('No cameras in account - skipping camera-specific checks')
|
|
307
277
|
}
|
|
308
278
|
})
|
|
309
279
|
|
|
310
|
-
test('
|
|
311
|
-
|
|
312
|
-
|
|
280
|
+
test('can logout after login', async ({ page }) => {
|
|
281
|
+
skipIfNoProxy()
|
|
282
|
+
skipIfNoCredentials()
|
|
313
283
|
|
|
314
|
-
|
|
315
|
-
await page.
|
|
316
|
-
timeout: 30000
|
|
317
|
-
})
|
|
284
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
285
|
+
await expect(page.getByTestId('nav-logout')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
318
286
|
|
|
319
|
-
|
|
320
|
-
const hasCameras = await cameraSelect.isVisible().catch(() => false)
|
|
287
|
+
await page.click('[data-testid="nav-logout"]')
|
|
321
288
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (options.length >= 2) {
|
|
327
|
-
// Get the second camera's value
|
|
328
|
-
const secondCameraValue = await options[1].getAttribute('value')
|
|
329
|
-
console.log(`Selecting camera: ${secondCameraValue}`)
|
|
330
|
-
|
|
331
|
-
// Select the second camera
|
|
332
|
-
await cameraSelect.selectOption(secondCameraValue!)
|
|
333
|
-
|
|
334
|
-
// Wait for selection to be applied
|
|
335
|
-
await page.waitForTimeout(1000)
|
|
336
|
-
|
|
337
|
-
// Navigate to recorded page
|
|
338
|
-
await page.goto('/recorded')
|
|
339
|
-
|
|
340
|
-
// Wait for cameras to load on recorded page
|
|
341
|
-
await page.waitForSelector('[data-testid="camera-select"]', {
|
|
342
|
-
timeout: 30000
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
// Check that the same camera is selected
|
|
346
|
-
const recordedCameraSelect = page.getByTestId('camera-select')
|
|
347
|
-
const selectedValue = await recordedCameraSelect.inputValue()
|
|
348
|
-
|
|
349
|
-
expect(selectedValue).toBe(secondCameraValue)
|
|
350
|
-
console.log(`Camera selection persisted: ${selectedValue}`)
|
|
351
|
-
|
|
352
|
-
// Navigate back to live page
|
|
353
|
-
await page.goto('/live')
|
|
354
|
-
|
|
355
|
-
// Wait for cameras to load
|
|
356
|
-
await page.waitForSelector('[data-testid="camera-select"]', {
|
|
357
|
-
timeout: 30000
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// Verify selection still persists
|
|
361
|
-
const liveCameraSelect = page.getByTestId('camera-select')
|
|
362
|
-
const liveSelectedValue = await liveCameraSelect.inputValue()
|
|
363
|
-
|
|
364
|
-
expect(liveSelectedValue).toBe(secondCameraValue)
|
|
365
|
-
console.log('Camera selection persisted across page navigations')
|
|
366
|
-
} else {
|
|
367
|
-
console.log('Only one camera available - cannot test persistence')
|
|
368
|
-
}
|
|
369
|
-
} else {
|
|
370
|
-
console.log('No cameras to test with')
|
|
371
|
-
}
|
|
289
|
+
await page.waitForURL('**/')
|
|
290
|
+
await expect(page.getByTestId('not-authenticated')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
291
|
+
await expect(page.getByTestId('nav-login')).toBeVisible()
|
|
372
292
|
})
|
|
373
293
|
})
|
|
Binary file
|