een-api-toolkit 0.3.30 → 0.3.38

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 (69) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +15 -3
  2. package/.claude/agents/een-auth-agent.md +131 -0
  3. package/.claude/agents/een-devices-agent.md +10 -7
  4. package/.claude/agents/een-events-agent.md +98 -0
  5. package/.claude/agents/een-grouping-agent.md +394 -0
  6. package/.claude/agents/een-media-agent.md +25 -5
  7. package/CHANGELOG.md +77 -6
  8. package/README.md +5 -3
  9. package/dist/index.cjs +3 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +561 -0
  12. package/dist/index.js +388 -218
  13. package/dist/index.js.map +1 -1
  14. package/docs/AI-CONTEXT.md +13 -1
  15. package/docs/ai-reference/AI-AUTH.md +1 -1
  16. package/docs/ai-reference/AI-DEVICES.md +1 -1
  17. package/docs/ai-reference/AI-EVENTS.md +1 -1
  18. package/docs/ai-reference/AI-GROUPING.md +411 -0
  19. package/docs/ai-reference/AI-MEDIA.md +1 -1
  20. package/docs/ai-reference/AI-SETUP.md +1 -1
  21. package/docs/ai-reference/AI-USERS.md +1 -1
  22. package/examples/vue-alerts-metrics/README.md +2 -0
  23. package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
  24. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
  25. package/examples/vue-alerts-metrics/package-lock.json +17 -14
  26. package/examples/vue-alerts-metrics/package.json +1 -1
  27. package/examples/vue-bridges/package-lock.json +21 -15
  28. package/examples/vue-bridges/package.json +1 -1
  29. package/examples/vue-cameras/package-lock.json +21 -15
  30. package/examples/vue-cameras/package.json +1 -1
  31. package/examples/vue-event-subscriptions/README.md +2 -0
  32. package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
  33. package/examples/vue-event-subscriptions/package-lock.json +17 -14
  34. package/examples/vue-event-subscriptions/package.json +1 -1
  35. package/examples/vue-events/events-screenshot.png +0 -0
  36. package/examples/vue-events/package-lock.json +17 -14
  37. package/examples/vue-events/package.json +1 -1
  38. package/examples/vue-feeds/package-lock.json +21 -15
  39. package/examples/vue-feeds/package.json +1 -1
  40. package/examples/vue-layouts/.env.example +12 -0
  41. package/examples/vue-layouts/README.md +320 -0
  42. package/examples/vue-layouts/e2e/app.spec.ts +76 -0
  43. package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
  44. package/examples/vue-layouts/index.html +13 -0
  45. package/examples/vue-layouts/layouts-screenshot.png +0 -0
  46. package/examples/vue-layouts/package-lock.json +1722 -0
  47. package/examples/vue-layouts/package.json +28 -0
  48. package/examples/vue-layouts/playwright.config.ts +47 -0
  49. package/examples/vue-layouts/src/App.vue +124 -0
  50. package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
  51. package/examples/vue-layouts/src/main.ts +25 -0
  52. package/examples/vue-layouts/src/router/index.ts +62 -0
  53. package/examples/vue-layouts/src/views/Callback.vue +76 -0
  54. package/examples/vue-layouts/src/views/Home.vue +188 -0
  55. package/examples/vue-layouts/src/views/Layouts.vue +355 -0
  56. package/examples/vue-layouts/src/views/Login.vue +33 -0
  57. package/examples/vue-layouts/src/views/Logout.vue +59 -0
  58. package/examples/vue-layouts/src/vite-env.d.ts +12 -0
  59. package/examples/vue-layouts/tsconfig.json +21 -0
  60. package/examples/vue-layouts/tsconfig.node.json +10 -0
  61. package/examples/vue-layouts/vite.config.ts +12 -0
  62. package/examples/vue-media/e2e/auth.spec.ts +35 -1
  63. package/examples/vue-media/media-screenshot.png +0 -0
  64. package/examples/vue-media/package-lock.json +19 -14
  65. package/examples/vue-media/package.json +1 -1
  66. package/examples/vue-users/package-lock.json +21 -16
  67. package/examples/vue-users/package.json +2 -2
  68. package/package.json +2 -2
  69. package/scripts/setup-agents.ts +9 -7
@@ -0,0 +1,320 @@
1
+ # EEN API Toolkit - Vue Layouts Example
2
+
3
+ A complete example showing how to use the Layouts API with een-api-toolkit in a Vue 3 application.
4
+
5
+ ![Layouts Screenshot](layouts-screenshot.png)
6
+
7
+ ## Storage Strategy: Memory
8
+
9
+ This example uses the `memory` storage strategy for maximum security. This means:
10
+
11
+ - **Tokens are never written to disk** - immune to localStorage/sessionStorage XSS attacks
12
+ - **Page refresh requires re-authentication** - tokens exist only in memory
13
+ - **Each tab is independent** - opening a new tab requires separate login
14
+
15
+ This is the recommended strategy for high-security deployments where protecting against XSS token theft is critical.
16
+
17
+ ## Features Demonstrated
18
+
19
+ - OAuth authentication flow (login, callback, logout)
20
+ - Protected routes with navigation guards
21
+ - `getLayouts()` function with pagination
22
+ - `createLayout()` function for creating new layouts
23
+ - `updateLayout()` function for modifying layouts
24
+ - `deleteLayout()` function for removing layouts
25
+ - Layout modal for create/edit operations
26
+ - Camera pane management within layouts
27
+ - Error handling with Result pattern
28
+ - Reactive authentication state
29
+
30
+ ## APIs Used
31
+
32
+ - `getLayouts()` - List layouts with pagination and filtering
33
+ - `getLayout()` - Get a specific layout by ID
34
+ - `createLayout()` - Create a new layout
35
+ - `updateLayout()` - Update an existing layout
36
+ - `deleteLayout()` - Delete a layout
37
+ - `getCameras()` - Get cameras for pane selection
38
+ - `getCurrentUser()` - Get current user profile
39
+ - `useAuthStore()` - Authentication state management
40
+ - `getAuthUrl()` - Generate OAuth login URL
41
+ - `handleAuthCallback()` - Process OAuth callback
42
+ - `initEenToolkit()` - Toolkit initialization
43
+
44
+ ## Setup
45
+
46
+ ### Prerequisites
47
+
48
+ 1. **Start the OAuth proxy** (required for authentication):
49
+
50
+ The OAuth proxy is a separate project that handles token management securely.
51
+ Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
52
+
53
+ ```bash
54
+ # In a separate terminal, from the een-oauth-proxy directory
55
+ npm install
56
+ npm run dev
57
+ ```
58
+
59
+ The proxy should be running at `http://localhost:8787`.
60
+
61
+ ### Example Setup
62
+
63
+ All commands below should be run from this example directory (`examples/vue-layouts/`):
64
+
65
+ 2. Copy the environment file:
66
+ ```bash
67
+ # From examples/vue-layouts/
68
+ cp .env.example .env
69
+ ```
70
+
71
+ 3. Edit `.env` with your EEN credentials:
72
+ ```env
73
+ VITE_EEN_CLIENT_ID=your-client-id
74
+ VITE_PROXY_URL=http://localhost:8787
75
+ # DO NOT change the redirect URI - EEN IDP only permits this URL
76
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
77
+ ```
78
+
79
+ 4. Install dependencies and start:
80
+ ```bash
81
+ # From examples/vue-layouts/
82
+ npm install
83
+ npm run dev
84
+ ```
85
+
86
+ 5. Open http://127.0.0.1:3333 in your browser.
87
+
88
+ **Important:** The EEN Identity Provider only permits `http://127.0.0.1:3333` as the OAuth redirect URI. Do not use `localhost` or other ports.
89
+
90
+ **Note:** Development and testing was done on macOS. The `npm run stop` command uses `lsof`, which is not available on Windows. Windows users should manually stop any process on port 3333 or use `npx kill-port 3333` instead.
91
+
92
+ ## Project Structure
93
+
94
+ ```
95
+ src/
96
+ ├── main.ts # App entry, toolkit initialization
97
+ ├── App.vue # Root component with navigation
98
+ ├── router/
99
+ │ └── index.ts # Vue Router with auth guards
100
+ ├── views/
101
+ │ ├── Home.vue # Home page with user profile
102
+ │ ├── Login.vue # OAuth login redirect
103
+ │ ├── Callback.vue # OAuth callback handler
104
+ │ ├── Layouts.vue # Layout list with CRUD operations
105
+ │ └── Logout.vue # Logout handler
106
+ └── components/
107
+ └── LayoutModal.vue # Modal for create/edit layouts
108
+ ```
109
+
110
+ ## Key Code Examples
111
+
112
+ ### Initializing the Toolkit (main.ts)
113
+
114
+ ```typescript
115
+ import { initEenToolkit } from 'een-api-toolkit'
116
+
117
+ initEenToolkit({
118
+ proxyUrl: import.meta.env.VITE_PROXY_URL,
119
+ clientId: import.meta.env.VITE_EEN_CLIENT_ID,
120
+ storageStrategy: 'memory', // Maximum security - tokens lost on refresh
121
+ debug: true
122
+ })
123
+ ```
124
+
125
+ ### Fetching Layouts with Pagination (Layouts.vue)
126
+
127
+ ```typescript
128
+ import { ref } from 'vue'
129
+ import { getLayouts, type Layout } from 'een-api-toolkit'
130
+
131
+ const layouts = ref<Layout[]>([])
132
+ const nextPageToken = ref<string | undefined>(undefined)
133
+ const loading = ref(false)
134
+
135
+ async function fetchLayouts() {
136
+ loading.value = true
137
+ const result = await getLayouts({
138
+ pageSize: 20,
139
+ include: ['effectivePermissions', 'resourceCounts']
140
+ })
141
+
142
+ if (result.error) {
143
+ console.error('Failed to fetch layouts:', result.error.message)
144
+ } else {
145
+ layouts.value = result.data.results
146
+ nextPageToken.value = result.data.nextPageToken
147
+ }
148
+ loading.value = false
149
+ }
150
+ ```
151
+
152
+ ### Creating a Layout
153
+
154
+ ```typescript
155
+ import { createLayout, type CreateLayoutParams } from 'een-api-toolkit'
156
+
157
+ async function handleCreate(params: CreateLayoutParams) {
158
+ const result = await createLayout({
159
+ name: 'My New Layout',
160
+ settings: {
161
+ paneColumns: 2,
162
+ cameraAspectRatio: '16x9',
163
+ showCameraBorder: true,
164
+ showCameraName: true
165
+ },
166
+ panes: []
167
+ })
168
+
169
+ if (result.error) {
170
+ console.error('Failed to create layout:', result.error.message)
171
+ } else {
172
+ console.log('Created layout:', result.data.id)
173
+ await fetchLayouts() // Refresh the list
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Updating a Layout
179
+
180
+ ```typescript
181
+ import { updateLayout, type UpdateLayoutParams } from 'een-api-toolkit'
182
+
183
+ async function handleUpdate(layoutId: string, params: UpdateLayoutParams) {
184
+ const result = await updateLayout(layoutId, {
185
+ name: 'Updated Layout Name',
186
+ settings: {
187
+ paneColumns: 3
188
+ }
189
+ })
190
+
191
+ if (result.error) {
192
+ console.error('Failed to update layout:', result.error.message)
193
+ } else {
194
+ await fetchLayouts() // Refresh the list
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### Deleting a Layout
200
+
201
+ ```typescript
202
+ import { deleteLayout } from 'een-api-toolkit'
203
+
204
+ async function handleDelete(layoutId: string) {
205
+ if (!confirm('Are you sure you want to delete this layout?')) return
206
+
207
+ const result = await deleteLayout(layoutId)
208
+
209
+ if (result.error) {
210
+ console.error('Failed to delete layout:', result.error.message)
211
+ } else {
212
+ await fetchLayouts() // Refresh the list
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Layout Modal Component (LayoutModal.vue)
218
+
219
+ ```typescript
220
+ import { ref, watch } from 'vue'
221
+ import { getCameras, type Layout, type Camera, type LayoutPane } from 'een-api-toolkit'
222
+
223
+ const props = defineProps<{
224
+ layout?: Layout | null
225
+ isOpen: boolean
226
+ }>()
227
+
228
+ const emit = defineEmits<{
229
+ save: [params: { name: string; settings: LayoutSettings; panes: LayoutPane[] }]
230
+ delete: [layoutId: string]
231
+ close: []
232
+ }>()
233
+
234
+ const cameras = ref<Camera[]>([])
235
+ const name = ref('')
236
+ const paneColumns = ref(2)
237
+ const panes = ref<LayoutPane[]>([])
238
+
239
+ // Fetch cameras for pane selection
240
+ async function fetchCameras() {
241
+ const result = await getCameras({ pageSize: 100, include: ['status'] })
242
+ if (!result.error) {
243
+ cameras.value = result.data.results
244
+ }
245
+ }
246
+
247
+ // Add a new pane
248
+ function addPane() {
249
+ panes.value.push({
250
+ id: panes.value.length,
251
+ name: `Pane ${panes.value.length + 1}`,
252
+ type: 'preview',
253
+ size: 1,
254
+ cameraId: ''
255
+ })
256
+ }
257
+ ```
258
+
259
+ ### Auth Guard (router/index.ts)
260
+
261
+ ```typescript
262
+ router.beforeEach((to, from, next) => {
263
+ const authStore = useAuthStore()
264
+
265
+ if (to.meta.requiresAuth && !authStore.isAuthenticated) {
266
+ next('/login')
267
+ } else {
268
+ next()
269
+ }
270
+ })
271
+ ```
272
+
273
+ ## Layout Types
274
+
275
+ ```typescript
276
+ interface Layout {
277
+ id: string
278
+ name: string
279
+ accountId: string
280
+ panes: LayoutPane[]
281
+ settings: LayoutSettings
282
+ effectivePermissions?: LayoutPermissions
283
+ resourceCounts?: { cameras?: number }
284
+ }
285
+
286
+ interface LayoutPane {
287
+ id: number
288
+ name: string
289
+ type: 'preview' | 'compositePreview'
290
+ size: 1 | 2 | 3
291
+ cameraId: string
292
+ }
293
+
294
+ interface LayoutSettings {
295
+ showCameraBorder: boolean
296
+ showCameraName: boolean
297
+ cameraAspectRatio: '16x9' | '4x3'
298
+ paneColumns: number // 1-6
299
+ }
300
+ ```
301
+
302
+ ## Running E2E Tests
303
+
304
+ The example includes Playwright E2E tests:
305
+
306
+ ```bash
307
+ # Run all E2E tests
308
+ npm run test:e2e
309
+
310
+ # Run with UI for debugging
311
+ npm run test:e2e:ui
312
+ ```
313
+
314
+ Tests cover:
315
+ - App loads correctly
316
+ - Navigation between pages
317
+ - Authentication flow
318
+ - Protected route redirection
319
+ - OAuth login flow (when proxy is available)
320
+ - Layouts page after authentication
@@ -0,0 +1,76 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ test.describe('Vue Layouts Example - App', () => {
4
+ test.beforeEach(async ({ page }) => {
5
+ // Capture console errors
6
+ page.on('console', msg => {
7
+ if (msg.type() === 'error') {
8
+ console.log('Browser console error:', msg.text())
9
+ }
10
+ })
11
+ page.on('pageerror', err => {
12
+ console.log('Page error:', err.message)
13
+ })
14
+
15
+ await page.goto('/')
16
+ // Wait for Vue app header AND router-view content to render
17
+ // The header renders first, then router-view resolves the route
18
+ await page.waitForSelector('[data-testid="app-title"]', { timeout: 10000 })
19
+ // Wait for router-view content (either authenticated or not-authenticated state)
20
+ await page.waitForSelector('.home, .login, .layouts', { timeout: 10000 })
21
+ })
22
+
23
+ test('app loads with correct title', async ({ page }) => {
24
+ await expect(page).toHaveTitle(/EEN Layouts/)
25
+ })
26
+
27
+ test('header displays app name', async ({ page }) => {
28
+ await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN Layouts Example')
29
+ })
30
+
31
+ test('navigation shows Home and Login links when not authenticated', async ({ page }) => {
32
+ // Home link should be visible
33
+ await expect(page.locator('[data-testid="nav-home"]')).toBeVisible()
34
+
35
+ // Login link should be visible (not authenticated)
36
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
37
+
38
+ // Layouts and Logout should NOT be visible (requires auth)
39
+ await expect(page.locator('[data-testid="nav-layouts"]')).not.toBeVisible()
40
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
41
+ })
42
+
43
+ test('home page shows not logged in message', async ({ page }) => {
44
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
45
+ await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
46
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
47
+ })
48
+
49
+ test('login page displays login button', async ({ page }) => {
50
+ await page.goto('/login')
51
+
52
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
53
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
54
+ })
55
+
56
+ test('protected route redirects to login', async ({ page }) => {
57
+ await page.goto('/layouts')
58
+
59
+ // Should be redirected to login page
60
+ await page.waitForURL('/login')
61
+ await expect(page).toHaveURL('/login')
62
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
63
+ })
64
+
65
+ test('navigation between pages works', async ({ page }) => {
66
+ // Click Login link
67
+ await page.click('[data-testid="nav-login"]')
68
+ await page.waitForURL('/login')
69
+ await expect(page).toHaveURL('/login')
70
+
71
+ // Click Home link
72
+ await page.click('[data-testid="nav-home"]')
73
+ await page.waitForURL('/')
74
+ await expect(page).toHaveURL('/')
75
+ })
76
+ })
@@ -0,0 +1,264 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+ import { baseURL } from '../playwright.config'
3
+
4
+ /**
5
+ * E2E tests for the Vue Layouts Example
6
+ *
7
+ * Tests the OAuth login flow through the UI:
8
+ * 1. Click login button in the example app
9
+ * 2. Enter credentials on EEN OAuth page
10
+ * 3. Complete the OAuth callback
11
+ * 4. Verify authenticated state
12
+ *
13
+ * Required environment variables:
14
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
15
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
16
+ * - TEST_USER: Test user email
17
+ * - TEST_PASSWORD: Test user password
18
+ */
19
+
20
+ // Timeout constants for consistent behavior
21
+ // Values chosen based on OAuth flow timing requirements
22
+ const TIMEOUTS = {
23
+ OAUTH_REDIRECT: 30000, // OAuth redirects can be slow on first load
24
+ ELEMENT_VISIBLE: 15000, // Wait for OAuth page elements to render
25
+ PASSWORD_VISIBLE: 10000, // Password field appears after email validation
26
+ AUTH_COMPLETE: 30000, // Full OAuth flow completion
27
+ UI_UPDATE: 10000, // UI state updates after auth changes
28
+ PROXY_CHECK: 5000 // Quick check if proxy is running
29
+ } as const
30
+
31
+ const TEST_USER = process.env.TEST_USER
32
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
33
+ const PROXY_URL = process.env.VITE_PROXY_URL
34
+
35
+ /**
36
+ * Checks if the OAuth proxy is accessible.
37
+ * Returns true if proxy responds (even with 404), false if unreachable.
38
+ */
39
+ async function isProxyAccessible(): Promise<boolean> {
40
+ if (!PROXY_URL) return false
41
+ const controller = new AbortController()
42
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
43
+
44
+ try {
45
+ const response = await fetch(PROXY_URL, {
46
+ method: 'HEAD',
47
+ signal: controller.signal
48
+ })
49
+ // 404 is ok - means proxy is running but endpoint doesn't exist
50
+ return response.ok || response.status === 404
51
+ } catch {
52
+ return false
53
+ } finally {
54
+ clearTimeout(timeoutId)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Performs OAuth login flow through the UI.
60
+ * Starts from home page and completes full OAuth authentication.
61
+ */
62
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
63
+ // Start at home page
64
+ await page.goto('/')
65
+
66
+ // Click login button and wait for OAuth redirect
67
+ await Promise.all([
68
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
69
+ page.click('[data-testid="login-button"]')
70
+ ])
71
+
72
+ // Fill email
73
+ const emailInput = page.locator('#authentication--input__email')
74
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
75
+ await emailInput.fill(username)
76
+
77
+ // Click next
78
+ await page.getByRole('button', { name: 'Next' }).click()
79
+
80
+ // Fill password
81
+ const passwordInput = page.locator('#authentication--input__password')
82
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
83
+ await passwordInput.fill(password)
84
+
85
+ // Click sign in - use OR selector for robustness
86
+ await page.locator('#next, button:has-text("Sign in")').first().click()
87
+
88
+ // Wait for redirect back to the app using configured baseURL
89
+ const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
90
+ await page.waitForURL(baseURLPattern, { timeout: TIMEOUTS.AUTH_COMPLETE })
91
+ }
92
+
93
+ /**
94
+ * Clears browser storage to reset auth state.
95
+ * Handles cases where localStorage isn't accessible (e.g., about:blank, cross-origin).
96
+ */
97
+ async function clearAuthState(page: Page): Promise<void> {
98
+ try {
99
+ // Only try to clear storage if we're on a page that allows it
100
+ const url = page.url()
101
+ if (url && url.startsWith('http')) {
102
+ await page.evaluate(() => {
103
+ try {
104
+ localStorage.clear()
105
+ sessionStorage.clear()
106
+ } catch {
107
+ // Ignore errors - storage may not be accessible
108
+ }
109
+ })
110
+ }
111
+ } catch {
112
+ // Ignore errors - page may be closed or in an inaccessible state
113
+ }
114
+ }
115
+
116
+ test.describe('Vue Layouts Example', () => {
117
+ // Check proxy accessibility once before all tests
118
+ let proxyAccessible = false
119
+
120
+ // Helper functions to skip tests when prerequisites aren't met
121
+ function skipIfNoProxy() {
122
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
123
+ }
124
+
125
+ function skipIfNoCredentials() {
126
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
127
+ }
128
+
129
+ function skipIfNoUser() {
130
+ test.skip(!TEST_USER, 'Test user not available')
131
+ }
132
+
133
+ test.beforeAll(async () => {
134
+ proxyAccessible = await isProxyAccessible()
135
+ if (!proxyAccessible) {
136
+ console.log('OAuth proxy not accessible - OAuth tests will be skipped')
137
+ }
138
+ })
139
+
140
+ test.afterEach(async ({ page }) => {
141
+ // Clear auth state after each test to prevent state pollution
142
+ await clearAuthState(page)
143
+ })
144
+
145
+ test('shows login button when not authenticated', async ({ page }) => {
146
+ await page.goto('/')
147
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
148
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
149
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
150
+ })
151
+
152
+ test('layouts page shows not-authenticated state without login', async ({ page }) => {
153
+ await page.goto('/layouts')
154
+ await expect(
155
+ page.locator('[data-testid="not-authenticated"], [data-testid="nav-login"], .error, .auth-required').first()
156
+ ).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
157
+ })
158
+
159
+ test('login button redirects to OAuth page', async ({ page }) => {
160
+ skipIfNoProxy()
161
+ skipIfNoCredentials()
162
+
163
+ await page.goto('/')
164
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
165
+ await expect(page.locator('[data-testid="login-button"]')).toBeEnabled()
166
+
167
+ // Click login and verify redirect to OAuth page
168
+ await Promise.all([
169
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
170
+ page.click('[data-testid="login-button"]')
171
+ ])
172
+
173
+ // Verify we're on the OAuth page
174
+ const emailInput = page.locator('#authentication--input__email')
175
+ await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
176
+ })
177
+
178
+ test('complete OAuth login flow', async ({ page }) => {
179
+ skipIfNoProxy()
180
+ skipIfNoCredentials()
181
+
182
+ // Verify initially not authenticated
183
+ await page.goto('/')
184
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
185
+
186
+ // Perform login
187
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
188
+
189
+ // Verify authenticated state
190
+ await expect(page.locator('[data-testid="not-authenticated"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
191
+ await expect(page.locator('[data-testid="nav-layouts"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
192
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
193
+ await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
194
+ })
195
+
196
+ test('can view layouts list after login', async ({ page }) => {
197
+ skipIfNoProxy()
198
+ skipIfNoCredentials()
199
+
200
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
201
+ await expect(page.locator('[data-testid="nav-layouts"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
202
+
203
+ // Navigate to layouts page
204
+ await page.click('[data-testid="nav-layouts"]')
205
+ await page.waitForURL('/layouts')
206
+
207
+ // Should see layouts header and create button (not error state)
208
+ await expect(page.locator('.layouts .header h2')).toHaveText('Layouts')
209
+ await expect(page.getByRole('button', { name: 'Create Layout' })).toBeVisible()
210
+ await expect(page.locator('.error')).not.toBeVisible()
211
+ })
212
+
213
+ test('can logout after login', async ({ page }) => {
214
+ skipIfNoProxy()
215
+ skipIfNoCredentials()
216
+
217
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
218
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
219
+
220
+ // Click logout
221
+ await page.click('[data-testid="nav-logout"]')
222
+
223
+ // Should show not authenticated - wait for redirect to app baseURL
224
+ const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
225
+ await page.waitForURL(baseURLPattern)
226
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
227
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
228
+ })
229
+
230
+ test('invalid password shows error on OAuth page', async ({ page }) => {
231
+ skipIfNoProxy()
232
+ skipIfNoUser()
233
+
234
+ await page.goto('/')
235
+
236
+ // Click login and wait for OAuth redirect
237
+ await Promise.all([
238
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
239
+ page.click('[data-testid="login-button"]')
240
+ ])
241
+
242
+ // Fill valid email
243
+ const emailInput = page.locator('#authentication--input__email')
244
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
245
+ await emailInput.fill(TEST_USER!)
246
+ await page.getByRole('button', { name: 'Next' }).click()
247
+
248
+ // Fill invalid password
249
+ const passwordInput = page.locator('#authentication--input__password')
250
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
251
+ await passwordInput.fill('invalid-password-12345!')
252
+
253
+ // Click sign in
254
+ await page.locator('#next, button:has-text("Sign in")').first().click()
255
+
256
+ // Should show error message on OAuth page
257
+ await expect(
258
+ page.locator('.error, [class*="error"], [data-testid*="error"], #error, .alert-danger').first()
259
+ ).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
260
+
261
+ // Should still be on OAuth page
262
+ await expect(page).toHaveURL(/eagleeyenetworks\.com/)
263
+ })
264
+ })
@@ -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 Layouts Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>