een-api-toolkit 0.3.85 → 0.3.97
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/api-coverage-agent.md +71 -29
- package/.claude/agents/een-devices-agent.md +21 -0
- package/.claude/agents/een-ptz-agent.md +245 -0
- package/CHANGELOG.md +60 -40
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +412 -0
- package/dist/index.js +1079 -951
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +3 -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 +12 -1
- package/docs/ai-reference/AI-EVENT-DATA-SCHEMAS.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -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-PTZ.md +174 -0
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-ptz/.env.example +4 -0
- package/examples/vue-ptz/README.md +221 -0
- package/examples/vue-ptz/e2e/app.spec.ts +58 -0
- package/examples/vue-ptz/e2e/auth.spec.ts +296 -0
- package/examples/vue-ptz/index.html +13 -0
- package/examples/vue-ptz/package-lock.json +1729 -0
- package/examples/vue-ptz/package.json +29 -0
- package/examples/vue-ptz/playwright.config.ts +49 -0
- package/examples/vue-ptz/screenshot-ptz.png +0 -0
- package/examples/vue-ptz/src/App.vue +154 -0
- package/examples/vue-ptz/src/components/ApiLog.vue +387 -0
- package/examples/vue-ptz/src/components/CameraSelector.vue +155 -0
- package/examples/vue-ptz/src/components/DirectionPad.vue +350 -0
- package/examples/vue-ptz/src/components/LiveVideoPlayer.vue +248 -0
- package/examples/vue-ptz/src/components/PositionDisplay.vue +206 -0
- package/examples/vue-ptz/src/components/PositionInput.vue +190 -0
- package/examples/vue-ptz/src/components/PresetManager.vue +538 -0
- package/examples/vue-ptz/src/composables/useApiLog.ts +89 -0
- package/examples/vue-ptz/src/main.ts +22 -0
- package/examples/vue-ptz/src/router/index.ts +61 -0
- package/examples/vue-ptz/src/views/Callback.vue +76 -0
- package/examples/vue-ptz/src/views/Home.vue +199 -0
- package/examples/vue-ptz/src/views/Login.vue +32 -0
- package/examples/vue-ptz/src/views/Logout.vue +59 -0
- package/examples/vue-ptz/src/views/PtzControl.vue +173 -0
- package/examples/vue-ptz/src/vite-env.d.ts +12 -0
- package/examples/vue-ptz/tsconfig.json +21 -0
- package/examples/vue-ptz/tsconfig.node.json +11 -0
- package/examples/vue-ptz/vite.config.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for the Vue PTZ Example - OAuth Login Flow
|
|
3
|
+
*
|
|
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 PTZ control 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.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { test, expect, Page } from '@playwright/test'
|
|
23
|
+
import { baseURL } from '../playwright.config'
|
|
24
|
+
|
|
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
|
+
PTZ_LOAD: 30000
|
|
33
|
+
} as const
|
|
34
|
+
|
|
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
|
|
38
|
+
|
|
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
|
+
}
|
|
63
|
+
|
|
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('/')
|
|
70
|
+
|
|
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')
|
|
74
|
+
|
|
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
|
+
])
|
|
80
|
+
|
|
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)
|
|
86
|
+
|
|
87
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
88
|
+
|
|
89
|
+
const passwordInput = page.locator('#authentication--input__password')
|
|
90
|
+
await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
|
|
91
|
+
await passwordInput.fill(password)
|
|
92
|
+
|
|
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()
|
|
95
|
+
|
|
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
|
+
}
|
|
100
|
+
|
|
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
|
+
}
|
|
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
|
+
}
|
|
124
|
+
|
|
125
|
+
test.describe('Vue PTZ 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')
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test.afterEach(async ({ page }) => {
|
|
144
|
+
await clearAuthState(page)
|
|
145
|
+
})
|
|
146
|
+
|
|
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()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('ptz page redirects to login when not authenticated', async ({ page }) => {
|
|
154
|
+
await page.goto('/ptz')
|
|
155
|
+
await expect(page).toHaveURL('/login')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('login button redirects to OAuth page', async ({ page }) => {
|
|
159
|
+
skipIfNoProxy()
|
|
160
|
+
skipIfNoCredentials()
|
|
161
|
+
|
|
162
|
+
await page.goto('/')
|
|
163
|
+
await expect(page.getByTestId('login-button')).toBeVisible()
|
|
164
|
+
|
|
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')
|
|
168
|
+
|
|
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
|
+
])
|
|
174
|
+
|
|
175
|
+
// EEN OAuth page selector
|
|
176
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
177
|
+
await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('complete OAuth login flow and view PTZ controls', async ({ page }) => {
|
|
181
|
+
skipIfNoProxy()
|
|
182
|
+
skipIfNoCredentials()
|
|
183
|
+
|
|
184
|
+
await page.goto('/')
|
|
185
|
+
await expect(page.getByTestId('not-authenticated')).toBeVisible()
|
|
186
|
+
|
|
187
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
188
|
+
|
|
189
|
+
await expect(page.getByTestId('not-authenticated')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
190
|
+
await expect(page.getByTestId('nav-ptz')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
191
|
+
await expect(page.getByTestId('nav-logout')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('can view PTZ controls after login', async ({ page }) => {
|
|
195
|
+
skipIfNoProxy()
|
|
196
|
+
skipIfNoCredentials()
|
|
197
|
+
|
|
198
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
199
|
+
await expect(page.getByTestId('nav-ptz')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
200
|
+
|
|
201
|
+
await page.click('[data-testid="nav-ptz"]')
|
|
202
|
+
await page.waitForURL('/ptz')
|
|
203
|
+
|
|
204
|
+
await expect(page.getByRole('heading', { name: 'PTZ Camera Control' })).toBeVisible()
|
|
205
|
+
|
|
206
|
+
// Wait for camera selector or no-cameras message
|
|
207
|
+
await page.waitForSelector('[data-testid="ptz-camera-select"], [data-testid="no-ptz-cameras"]', {
|
|
208
|
+
timeout: TIMEOUTS.PTZ_LOAD
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('PTZ page shows direction pad and controls', async ({ page }) => {
|
|
213
|
+
skipIfNoProxy()
|
|
214
|
+
skipIfNoCredentials()
|
|
215
|
+
|
|
216
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
217
|
+
await expect(page.getByTestId('nav-ptz')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
218
|
+
|
|
219
|
+
await page.click('[data-testid="nav-ptz"]')
|
|
220
|
+
await page.waitForURL('/ptz')
|
|
221
|
+
|
|
222
|
+
// PTZ layout should be visible
|
|
223
|
+
await expect(page.getByTestId('ptz-layout')).toBeVisible()
|
|
224
|
+
|
|
225
|
+
// Direction pad should be visible
|
|
226
|
+
await expect(page.getByTestId('direction-pad')).toBeVisible()
|
|
227
|
+
|
|
228
|
+
// Position display should be visible
|
|
229
|
+
await expect(page.getByTestId('position-display')).toBeVisible()
|
|
230
|
+
|
|
231
|
+
// Preset manager should be visible
|
|
232
|
+
await expect(page.getByTestId('preset-manager')).toBeVisible()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('exercises PTZ API calls when PTZ camera is available', async ({ page }) => {
|
|
236
|
+
skipIfNoProxy()
|
|
237
|
+
skipIfNoCredentials()
|
|
238
|
+
|
|
239
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
240
|
+
await expect(page.getByTestId('nav-ptz')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
241
|
+
|
|
242
|
+
await page.click('[data-testid="nav-ptz"]')
|
|
243
|
+
await page.waitForURL('/ptz')
|
|
244
|
+
|
|
245
|
+
// Wait for camera loading to finish
|
|
246
|
+
await page.waitForSelector('[data-testid="ptz-camera-select"], [data-testid="no-ptz-cameras"]', {
|
|
247
|
+
timeout: TIMEOUTS.PTZ_LOAD
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// If no PTZ cameras available, skip gracefully
|
|
251
|
+
const noPtzCameras = await page.getByTestId('no-ptz-cameras').isVisible().catch(() => false)
|
|
252
|
+
if (noPtzCameras) {
|
|
253
|
+
console.log('No PTZ cameras available — skipping PTZ API tests')
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// PTZ camera is available — exercise API calls
|
|
258
|
+
|
|
259
|
+
// 1. Verify position display shows numeric values (confirms getPtzPosition succeeded)
|
|
260
|
+
const positionX = page.getByTestId('position-x')
|
|
261
|
+
await expect(positionX).toBeVisible({ timeout: TIMEOUTS.PTZ_LOAD })
|
|
262
|
+
await expect(positionX).not.toHaveText('--', { timeout: TIMEOUTS.PTZ_LOAD })
|
|
263
|
+
|
|
264
|
+
const positionY = page.getByTestId('position-y')
|
|
265
|
+
await expect(positionY).toBeVisible()
|
|
266
|
+
await expect(positionY).not.toHaveText('--')
|
|
267
|
+
|
|
268
|
+
const positionZ = page.getByTestId('position-z')
|
|
269
|
+
await expect(positionZ).toBeVisible()
|
|
270
|
+
await expect(positionZ).not.toHaveText('--')
|
|
271
|
+
|
|
272
|
+
// 2. Click a direction button and verify position still reads numeric (confirms movePtz round-trip)
|
|
273
|
+
await page.click('[data-testid="btn-up"]')
|
|
274
|
+
await page.waitForTimeout(2000)
|
|
275
|
+
await page.click('[data-testid="refresh-position"]')
|
|
276
|
+
await expect(positionX).not.toHaveText('--', { timeout: TIMEOUTS.UI_UPDATE })
|
|
277
|
+
await expect(positionY).not.toHaveText('--', { timeout: TIMEOUTS.UI_UPDATE })
|
|
278
|
+
|
|
279
|
+
// 3. Verify preset manager loaded (confirms getPtzSettings succeeded)
|
|
280
|
+
await expect(page.getByTestId('preset-manager')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('can logout after login', async ({ page }) => {
|
|
284
|
+
skipIfNoProxy()
|
|
285
|
+
skipIfNoCredentials()
|
|
286
|
+
|
|
287
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
288
|
+
await expect(page.getByTestId('nav-logout')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
289
|
+
|
|
290
|
+
await page.click('[data-testid="nav-logout"]')
|
|
291
|
+
|
|
292
|
+
await page.waitForURL('**/')
|
|
293
|
+
await expect(page.getByTestId('not-authenticated')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
294
|
+
await expect(page.getByTestId('nav-login')).toBeVisible()
|
|
295
|
+
})
|
|
296
|
+
})
|
|
@@ -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 PTZ Control Example</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|