een-api-toolkit 0.3.47 → 0.3.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude/agents/een-jobs-agent.md +676 -0
  2. package/CHANGELOG.md +7 -8
  3. package/dist/index.cjs +3 -3
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +1172 -28
  6. package/dist/index.js +796 -333
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +22 -1
  9. package/docs/ai-reference/AI-AUTH.md +1 -1
  10. package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
  11. package/docs/ai-reference/AI-DEVICES.md +1 -1
  12. package/docs/ai-reference/AI-EVENTS.md +1 -1
  13. package/docs/ai-reference/AI-GROUPING.md +1 -1
  14. package/docs/ai-reference/AI-JOBS.md +1084 -0
  15. package/docs/ai-reference/AI-MEDIA.md +1 -1
  16. package/docs/ai-reference/AI-SETUP.md +1 -1
  17. package/docs/ai-reference/AI-USERS.md +1 -1
  18. package/examples/vue-jobs/.env.example +11 -0
  19. package/examples/vue-jobs/README.md +245 -0
  20. package/examples/vue-jobs/e2e/app.spec.ts +79 -0
  21. package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
  22. package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
  23. package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
  24. package/examples/vue-jobs/index.html +13 -0
  25. package/examples/vue-jobs/package-lock.json +1722 -0
  26. package/examples/vue-jobs/package.json +28 -0
  27. package/examples/vue-jobs/playwright.config.ts +47 -0
  28. package/examples/vue-jobs/src/App.vue +154 -0
  29. package/examples/vue-jobs/src/main.ts +25 -0
  30. package/examples/vue-jobs/src/router/index.ts +82 -0
  31. package/examples/vue-jobs/src/views/Callback.vue +76 -0
  32. package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
  33. package/examples/vue-jobs/src/views/Files.vue +424 -0
  34. package/examples/vue-jobs/src/views/Home.vue +195 -0
  35. package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
  36. package/examples/vue-jobs/src/views/Jobs.vue +297 -0
  37. package/examples/vue-jobs/src/views/Login.vue +33 -0
  38. package/examples/vue-jobs/src/views/Logout.vue +59 -0
  39. package/examples/vue-jobs/src/vite-env.d.ts +1 -0
  40. package/examples/vue-jobs/tsconfig.json +25 -0
  41. package/examples/vue-jobs/vite.config.ts +12 -0
  42. package/package.json +1 -1
@@ -0,0 +1,361 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+ import { baseURL } from '../playwright.config'
3
+
4
+ /**
5
+ * E2E tests for TimeLapse export functionality
6
+ *
7
+ * Tests the timeLapse export flow which requires playbackMultiplier:
8
+ * 1. Login and navigate to Create Export
9
+ * 2. Select timeLapse type and verify playbackMultiplier field appears
10
+ * 3. Create a timeLapse export job
11
+ * 4. Verify job is created and tracked
12
+ *
13
+ * Required environment variables:
14
+ * - VITE_PROXY_URL: OAuth proxy URL
15
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
16
+ * - TEST_USER: Test user email
17
+ * - TEST_PASSWORD: Test user password
18
+ */
19
+
20
+ const TIMEOUTS = {
21
+ OAUTH_REDIRECT: 30000,
22
+ ELEMENT_VISIBLE: 15000,
23
+ PASSWORD_VISIBLE: 10000,
24
+ AUTH_COMPLETE: 30000,
25
+ UI_UPDATE: 10000,
26
+ PROXY_CHECK: 5000,
27
+ JOB_CREATION: 30000
28
+ } as const
29
+
30
+ const TEST_USER = process.env.TEST_USER
31
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
32
+ const PROXY_URL = process.env.VITE_PROXY_URL
33
+
34
+ async function isProxyAccessible(): Promise<boolean> {
35
+ if (!PROXY_URL) return false
36
+ const controller = new AbortController()
37
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
38
+
39
+ try {
40
+ const response = await fetch(PROXY_URL, {
41
+ method: 'HEAD',
42
+ signal: controller.signal
43
+ })
44
+ return response.ok || response.status === 404
45
+ } catch {
46
+ return false
47
+ } finally {
48
+ clearTimeout(timeoutId)
49
+ }
50
+ }
51
+
52
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
53
+ await page.goto('/')
54
+
55
+ await Promise.all([
56
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
57
+ page.click('[data-testid="login-button"]')
58
+ ])
59
+
60
+ const emailInput = page.locator('#authentication--input__email')
61
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
62
+ await emailInput.fill(username)
63
+
64
+ await page.getByRole('button', { name: 'Next' }).click()
65
+
66
+ const passwordInput = page.locator('#authentication--input__password')
67
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
68
+ await passwordInput.fill(password)
69
+
70
+ await page.locator('#next, button:has-text("Sign in")').first().click()
71
+
72
+ const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
73
+ await page.waitForURL(baseURLPattern, { timeout: TIMEOUTS.AUTH_COMPLETE })
74
+ }
75
+
76
+ test.describe('TimeLapse Export', () => {
77
+ test.beforeAll(async () => {
78
+ // Check prerequisites
79
+ if (!TEST_USER || !TEST_PASSWORD) {
80
+ console.log('Skipping: TEST_USER and TEST_PASSWORD required')
81
+ test.skip()
82
+ }
83
+
84
+ const proxyAccessible = await isProxyAccessible()
85
+ if (!proxyAccessible) {
86
+ console.log('Skipping: OAuth proxy not accessible at', PROXY_URL)
87
+ test.skip()
88
+ }
89
+ })
90
+
91
+ test('playbackMultiplier field appears when timeLapse is selected', async ({ page }) => {
92
+ if (!TEST_USER || !TEST_PASSWORD) {
93
+ test.skip()
94
+ return
95
+ }
96
+
97
+ // Login
98
+ await performLogin(page, TEST_USER, TEST_PASSWORD)
99
+
100
+ // Wait for authenticated state
101
+ await page.waitForSelector('.authenticated, .user-info', { timeout: TIMEOUTS.UI_UPDATE })
102
+
103
+ // Navigate to Create Export page
104
+ await page.click('a[href="/create-export"], button:has-text("Create Export")')
105
+ await page.waitForURL(/.*create-export.*/, { timeout: TIMEOUTS.UI_UPDATE })
106
+
107
+ // Wait for cameras to load - check that camera select has options
108
+ const cameraSelect = page.locator('select#camera')
109
+ await expect(cameraSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
110
+
111
+ // Wait for at least one camera option to be available
112
+ const cameraOptions = cameraSelect.locator('option:not([value=""])')
113
+ await expect(cameraOptions.first()).toBeAttached({ timeout: TIMEOUTS.UI_UPDATE })
114
+
115
+ // Verify playback multiplier is NOT visible for video type (default)
116
+ const playbackMultiplierSelect = page.locator('select#playbackMultiplier')
117
+ await expect(playbackMultiplierSelect).not.toBeVisible()
118
+
119
+ // Select timeLapse export type
120
+ await page.selectOption('select#exportType', 'timeLapse')
121
+
122
+ // Verify playback multiplier IS visible for timeLapse
123
+ await expect(playbackMultiplierSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
124
+
125
+ // Verify it has options
126
+ const options = await playbackMultiplierSelect.locator('option').count()
127
+ expect(options).toBeGreaterThan(0)
128
+ console.log('TimeLapse playback multiplier field has', options, 'options')
129
+ })
130
+
131
+ test('playbackMultiplier field appears when bundle is selected', async ({ page }) => {
132
+ if (!TEST_USER || !TEST_PASSWORD) {
133
+ test.skip()
134
+ return
135
+ }
136
+
137
+ // Login
138
+ await performLogin(page, TEST_USER, TEST_PASSWORD)
139
+ await page.waitForSelector('.authenticated, .user-info', { timeout: TIMEOUTS.UI_UPDATE })
140
+
141
+ // Navigate to Create Export page
142
+ await page.click('a[href="/create-export"], button:has-text("Create Export")')
143
+ await page.waitForURL(/.*create-export.*/, { timeout: TIMEOUTS.UI_UPDATE })
144
+
145
+ // Wait for cameras to load
146
+ const cameraSelect = page.locator('select#camera')
147
+ await expect(cameraSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
148
+ const cameraOptions = cameraSelect.locator('option:not([value=""])')
149
+ await expect(cameraOptions.first()).toBeAttached({ timeout: TIMEOUTS.UI_UPDATE })
150
+
151
+ // Select bundle export type
152
+ await page.selectOption('select#exportType', 'bundle')
153
+
154
+ // Verify playback multiplier IS visible for bundle
155
+ const playbackMultiplierSelect = page.locator('select#playbackMultiplier')
156
+ await expect(playbackMultiplierSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
157
+ })
158
+
159
+ test('can create timeLapse export job', async ({ page }) => {
160
+ if (!TEST_USER || !TEST_PASSWORD) {
161
+ test.skip()
162
+ return
163
+ }
164
+
165
+ // Login
166
+ await performLogin(page, TEST_USER, TEST_PASSWORD)
167
+ await page.waitForSelector('.authenticated, .user-info', { timeout: TIMEOUTS.UI_UPDATE })
168
+
169
+ // Navigate to Create Export page
170
+ await page.click('a[href="/create-export"], button:has-text("Create Export")')
171
+ await page.waitForURL(/.*create-export.*/, { timeout: TIMEOUTS.UI_UPDATE })
172
+
173
+ // Wait for cameras to load
174
+ const cameraSelect = page.locator('select#camera')
175
+ await expect(cameraSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
176
+
177
+ // Wait a moment for options to populate
178
+ await page.waitForTimeout(1000)
179
+
180
+ const cameraOptions = cameraSelect.locator('option:not([value=""])')
181
+ const cameraCount = await cameraOptions.count()
182
+
183
+ if (cameraCount === 0) {
184
+ console.log('No cameras available - skipping timeLapse export test')
185
+ test.skip()
186
+ return
187
+ }
188
+
189
+ // Select first camera
190
+ const firstCameraValue = await cameraOptions.first().getAttribute('value')
191
+ if (firstCameraValue) {
192
+ await page.selectOption('select#camera', firstCameraValue)
193
+ }
194
+
195
+ // Select timeLapse export type
196
+ await page.selectOption('select#exportType', 'timeLapse')
197
+
198
+ // Verify playback multiplier appears
199
+ const playbackMultiplierSelect = page.locator('select#playbackMultiplier')
200
+ await expect(playbackMultiplierSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
201
+
202
+ // Select a playback multiplier (10x)
203
+ await page.selectOption('select#playbackMultiplier', '10')
204
+
205
+ // Select shortest duration (5 minutes)
206
+ await page.selectOption('select#duration', '5')
207
+
208
+ // Enter export name
209
+ await page.fill('input#name', 'E2E TimeLapse Test Export')
210
+
211
+ // Submit the form
212
+ await page.click('button[type="submit"]')
213
+
214
+ // Wait for either success or error
215
+ const resultSelector = '.success, .error'
216
+ await page.waitForSelector(resultSelector, { timeout: TIMEOUTS.JOB_CREATION })
217
+
218
+ // Check result
219
+ const successElement = page.locator('.success')
220
+ const errorElement = page.locator('.error')
221
+
222
+ if (await successElement.isVisible()) {
223
+ const successText = await successElement.textContent()
224
+ console.log('TimeLapse export job created:', successText)
225
+ expect(successText).toContain('Export job created successfully')
226
+
227
+ // Should redirect to job detail page
228
+ await page.waitForURL(/.*jobs\/.*/, { timeout: TIMEOUTS.UI_UPDATE })
229
+ console.log('Redirected to job detail page:', page.url())
230
+ } else if (await errorElement.isVisible()) {
231
+ const errorText = await errorElement.textContent()
232
+ console.log('TimeLapse export creation failed:', errorText)
233
+
234
+ // Some errors are acceptable (no video in time range, etc.)
235
+ // Just log and don't fail the test for expected errors
236
+ if (errorText?.includes('no video') || errorText?.includes('No video')) {
237
+ console.log('Expected error: No video available in time range')
238
+ } else {
239
+ // Unexpected error - fail with details
240
+ expect.soft(errorText).not.toContain('playbackMultiplier')
241
+ }
242
+ }
243
+ })
244
+
245
+ test('can create bundle export job', async ({ page }) => {
246
+ if (!TEST_USER || !TEST_PASSWORD) {
247
+ test.skip()
248
+ return
249
+ }
250
+
251
+ // Login
252
+ await performLogin(page, TEST_USER, TEST_PASSWORD)
253
+ await page.waitForSelector('.authenticated, .user-info', { timeout: TIMEOUTS.UI_UPDATE })
254
+
255
+ // Navigate to Create Export page
256
+ await page.click('a[href="/create-export"], button:has-text("Create Export")')
257
+ await page.waitForURL(/.*create-export.*/, { timeout: TIMEOUTS.UI_UPDATE })
258
+
259
+ // Wait for cameras to load
260
+ const cameraSelect = page.locator('select#camera')
261
+ await expect(cameraSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
262
+
263
+ // Wait a moment for options to populate
264
+ await page.waitForTimeout(1000)
265
+
266
+ const cameraOptions = cameraSelect.locator('option:not([value=""])')
267
+ const cameraCount = await cameraOptions.count()
268
+
269
+ if (cameraCount === 0) {
270
+ console.log('No cameras available - skipping bundle export test')
271
+ test.skip()
272
+ return
273
+ }
274
+
275
+ // Select first camera
276
+ const firstCameraValue = await cameraOptions.first().getAttribute('value')
277
+ if (firstCameraValue) {
278
+ await page.selectOption('select#camera', firstCameraValue)
279
+ }
280
+
281
+ // Select bundle export type
282
+ await page.selectOption('select#exportType', 'bundle')
283
+
284
+ // Verify playback multiplier appears (required for bundle)
285
+ const playbackMultiplierSelect = page.locator('select#playbackMultiplier')
286
+ await expect(playbackMultiplierSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
287
+
288
+ // Select a playback multiplier (5x)
289
+ await page.selectOption('select#playbackMultiplier', '5')
290
+
291
+ // Select shortest duration (5 minutes)
292
+ await page.selectOption('select#duration', '5')
293
+
294
+ // Enter export name
295
+ await page.fill('input#name', 'E2E Bundle Test Export')
296
+
297
+ // Submit the form
298
+ await page.click('button[type="submit"]')
299
+
300
+ // Wait for either success or error
301
+ const resultSelector = '.success, .error'
302
+ await page.waitForSelector(resultSelector, { timeout: TIMEOUTS.JOB_CREATION })
303
+
304
+ // Check result
305
+ const successElement = page.locator('.success')
306
+ const errorElement = page.locator('.error')
307
+
308
+ if (await successElement.isVisible()) {
309
+ const successText = await successElement.textContent()
310
+ console.log('Bundle export job created:', successText)
311
+ expect(successText).toContain('Export job created successfully')
312
+
313
+ // Should redirect to job detail page
314
+ await page.waitForURL(/.*jobs\/.*/, { timeout: TIMEOUTS.UI_UPDATE })
315
+ console.log('Redirected to job detail page:', page.url())
316
+ } else if (await errorElement.isVisible()) {
317
+ const errorText = await errorElement.textContent()
318
+ console.log('Bundle export creation failed:', errorText)
319
+
320
+ // Some errors are acceptable (no video in time range, etc.)
321
+ if (errorText?.includes('no video') || errorText?.includes('No video')) {
322
+ console.log('Expected error: No video available in time range')
323
+ } else {
324
+ // Unexpected error - fail with details
325
+ expect.soft(errorText).not.toContain('playbackMultiplier')
326
+ }
327
+ }
328
+ })
329
+
330
+ test('video export does not show playbackMultiplier field', async ({ page }) => {
331
+ if (!TEST_USER || !TEST_PASSWORD) {
332
+ test.skip()
333
+ return
334
+ }
335
+
336
+ // Login
337
+ await performLogin(page, TEST_USER, TEST_PASSWORD)
338
+ await page.waitForSelector('.authenticated, .user-info', { timeout: TIMEOUTS.UI_UPDATE })
339
+
340
+ // Navigate to Create Export page
341
+ await page.click('a[href="/create-export"], button:has-text("Create Export")')
342
+ await page.waitForURL(/.*create-export.*/, { timeout: TIMEOUTS.UI_UPDATE })
343
+
344
+ // Wait for cameras to load
345
+ const cameraSelect = page.locator('select#camera')
346
+ await expect(cameraSelect).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
347
+ const cameraOptions = cameraSelect.locator('option:not([value=""])')
348
+ await expect(cameraOptions.first()).toBeAttached({ timeout: TIMEOUTS.UI_UPDATE })
349
+
350
+ // Video is default, verify playback multiplier is NOT visible
351
+ const playbackMultiplierSelect = page.locator('select#playbackMultiplier')
352
+ await expect(playbackMultiplierSelect).not.toBeVisible()
353
+
354
+ // Explicitly select video type
355
+ await page.selectOption('select#exportType', 'video')
356
+
357
+ // Still should not be visible
358
+ await expect(playbackMultiplierSelect).not.toBeVisible()
359
+ console.log('Verified: playbackMultiplier not shown for video export type')
360
+ })
361
+ })
@@ -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 Jobs Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>