een-api-toolkit 0.3.16 → 0.3.20

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 (30) hide show
  1. package/CHANGELOG.md +43 -10
  2. package/README.md +1 -0
  3. package/dist/index.cjs +3 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +561 -0
  6. package/dist/index.js +483 -260
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +12 -1
  9. package/examples/vue-event-subscriptions/.env.example +15 -0
  10. package/examples/vue-event-subscriptions/README.md +103 -0
  11. package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
  12. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
  13. package/examples/vue-event-subscriptions/index.html +13 -0
  14. package/examples/vue-event-subscriptions/package-lock.json +1719 -0
  15. package/examples/vue-event-subscriptions/package.json +28 -0
  16. package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
  17. package/examples/vue-event-subscriptions/src/App.vue +233 -0
  18. package/examples/vue-event-subscriptions/src/main.ts +25 -0
  19. package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
  20. package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
  21. package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
  22. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +640 -0
  23. package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
  24. package/examples/vue-event-subscriptions/src/views/Logout.vue +59 -0
  25. package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +402 -0
  26. package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
  27. package/examples/vue-event-subscriptions/tsconfig.json +21 -0
  28. package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
  29. package/examples/vue-event-subscriptions/vite.config.ts +12 -0
  30. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  # EEN API Toolkit - AI Reference
2
2
 
3
- > **Version:** 0.3.16
3
+ > **Version:** 0.3.20
4
4
  >
5
5
  > This file is optimized for AI assistants. It contains all API signatures,
6
6
  > types, and usage patterns in a single, parseable document.
@@ -214,6 +214,7 @@ Complete Vue 3 applications demonstrating toolkit features:
214
214
  | [vue-feeds](../examples/vue-feeds/) | Live video streaming (preview and main) | `src/views/Feeds.vue` |
215
215
  | [vue-events](../examples/vue-events/) | Events with bounding box overlays | `src/components/EventsModal.vue` |
216
216
  | [vue-alerts-metrics](../examples/vue-alerts-metrics/) | Event metrics, alerts, and notifications | `src/components/MetricsChart.vue`, `AlertsList.vue` |
217
+ | [vue-event-subscriptions](../examples/vue-event-subscriptions/) | Real-time event streaming with SSE | `src/views/Subscriptions.vue`, `LiveEvents.vue` |
217
218
 
218
219
  ### Configuration
219
220
 
@@ -293,6 +294,16 @@ Complete Vue 3 applications demonstrating toolkit features:
293
294
  | `listNotifications(params?)` | List notifications with filters | `Result<PaginatedResult<Notification>>` |
294
295
  | `getNotification(id)` | Get a specific notification by ID | `Result<Notification>` |
295
296
 
297
+ ### EventSubscriptions Functions
298
+
299
+ | Function | Purpose | Returns |
300
+ |----------|---------|---------|
301
+ | `listEventSubscriptions(params?)` | List all event subscriptions | `Result<PaginatedResult<EventSubscription>>` |
302
+ | `getEventSubscription(id)` | Get a specific subscription by ID | `Result<EventSubscription>` |
303
+ | `createEventSubscription(params)` | Create a new event subscription | `Result<EventSubscription>` |
304
+ | `deleteEventSubscription(id)` | Delete an event subscription | `Result<void>` |
305
+ | `connectToEventSubscription(sseUrl, options)` | Connect to SSE stream for real-time events | `Result<SSEConnection>` |
306
+
296
307
  ### Utility Functions
297
308
 
298
309
  | Function | Purpose | Returns |
@@ -0,0 +1,15 @@
1
+ # EEN API Toolkit Configuration
2
+ # Copy this file to .env and fill in your values
3
+
4
+ # OAuth Proxy URL (required)
5
+ VITE_PROXY_URL=http://localhost:8787
6
+
7
+ # EEN OAuth Client ID (required)
8
+ VITE_EEN_CLIENT_ID=your_client_id_here
9
+
10
+ # OAuth Redirect URI (required)
11
+ # Must match the redirect URI configured in your EEN OAuth client
12
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
13
+
14
+ # Enable debug logging (optional)
15
+ VITE_DEBUG=true
@@ -0,0 +1,103 @@
1
+ # Event Subscriptions Example
2
+
3
+ This example demonstrates how to use the Event Subscriptions API from the EEN API Toolkit to create SSE subscriptions and receive real-time events from cameras.
4
+
5
+ ## Features
6
+
7
+ - Create and delete event subscriptions with SSE delivery
8
+ - View active subscriptions with pagination
9
+ - Select cameras and event types for filtering
10
+ - Connect to SSE streams for live events
11
+ - Real-time event display with auto-scrolling
12
+
13
+ ## Prerequisites
14
+
15
+ 1. Node.js 20 LTS or later
16
+ 2. EEN OAuth credentials (client ID and secret)
17
+ 3. Running OAuth proxy server (from `../een-oauth-proxy`)
18
+
19
+ ## Setup
20
+
21
+ 1. Copy the environment file:
22
+ ```bash
23
+ cp .env.example .env
24
+ ```
25
+
26
+ 2. Edit `.env` with your configuration:
27
+ - `VITE_PROXY_URL`: URL of your OAuth proxy server
28
+ - `VITE_EEN_CLIENT_ID`: Your EEN OAuth client ID
29
+ - `VITE_REDIRECT_URI`: OAuth redirect URI (default: http://127.0.0.1:3333)
30
+ - `VITE_DEBUG`: Enable debug logging (optional)
31
+
32
+ 3. Install dependencies:
33
+ ```bash
34
+ npm install
35
+ ```
36
+
37
+ 4. Start the OAuth proxy (in a separate terminal):
38
+ ```bash
39
+ cd ../een-oauth-proxy
40
+ npm run dev
41
+ ```
42
+
43
+ 5. Start the example app:
44
+ ```bash
45
+ npm run dev
46
+ ```
47
+
48
+ 6. Open http://127.0.0.1:3333 in your browser
49
+
50
+ ## Usage
51
+
52
+ ### Creating a Subscription
53
+
54
+ 1. Log in with your Eagle Eye Networks account
55
+ 2. Go to "Subscriptions" page
56
+ 3. Select one or more cameras from the dropdown
57
+ 4. Select one or more event types
58
+ 5. Click "Create Subscription"
59
+
60
+ ### Viewing Live Events
61
+
62
+ 1. Go to "Live Events" page
63
+ 2. Select a subscription from the dropdown
64
+ 3. Click "Connect"
65
+ 4. Events will appear in real-time as they occur
66
+ 5. Click "Disconnect" to stop receiving events
67
+
68
+ ## API Functions Used
69
+
70
+ - `listEventSubscriptions()` - List all subscriptions
71
+ - `getEventSubscription(id)` - Get a specific subscription
72
+ - `createEventSubscription(params)` - Create a new subscription
73
+ - `deleteEventSubscription(id)` - Delete a subscription
74
+ - `connectToEventSubscription(sseUrl, options)` - Connect to SSE stream
75
+ - `getCameras()` - List cameras for filter selection
76
+ - `listEventTypes()` - List event types for filter selection
77
+
78
+ ## Event Subscription Lifecycle
79
+
80
+ SSE subscriptions are **temporary** by default:
81
+ - Automatically created with a time-to-live (TTL)
82
+ - Deleted when no client is connected after TTL expires
83
+ - Can be manually deleted at any time
84
+
85
+ ## Troubleshooting
86
+
87
+ ### No events appearing
88
+
89
+ - Ensure the camera has activity (motion, etc.)
90
+ - Check that the subscription has the correct event types
91
+ - Verify the SSE connection status shows "connected"
92
+
93
+ ### Connection errors
94
+
95
+ - Verify the OAuth proxy is running
96
+ - Check that your token is valid (re-login if needed)
97
+ - Ensure the subscription still exists (may have expired)
98
+
99
+ ### Cannot create subscription
100
+
101
+ - Ensure you have cameras in your account
102
+ - Check that event types are loaded
103
+ - Verify your account has permission to create subscriptions
@@ -0,0 +1,71 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ test.describe('Event Subscriptions Example - App', () => {
4
+ test.beforeEach(async ({ page }) => {
5
+ await page.goto('/')
6
+ })
7
+
8
+ test('app loads with correct title', async ({ page }) => {
9
+ await expect(page).toHaveTitle(/Event Subscriptions/)
10
+ })
11
+
12
+ test('header displays app name', async ({ page }) => {
13
+ await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN Event Subscriptions')
14
+ })
15
+
16
+ test('navigation shows Home and Login links when not authenticated', async ({ page }) => {
17
+ // Home link should be visible
18
+ await expect(page.locator('[data-testid="nav-home"]')).toBeVisible()
19
+
20
+ // Login link should be visible (not authenticated)
21
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
22
+
23
+ // Subscriptions, Live, and Logout should NOT be visible (requires auth)
24
+ await expect(page.locator('[data-testid="nav-subscriptions"]')).not.toBeVisible()
25
+ await expect(page.locator('[data-testid="nav-live"]')).not.toBeVisible()
26
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
27
+ })
28
+
29
+ test('home page shows not logged in message', async ({ page }) => {
30
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
31
+ await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
32
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
33
+ })
34
+
35
+ test('login page displays login button', async ({ page }) => {
36
+ await page.goto('/login')
37
+
38
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
39
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
40
+ })
41
+
42
+ test('protected route (subscriptions) redirects to login', async ({ page }) => {
43
+ await page.goto('/subscriptions')
44
+
45
+ // Should be redirected to login page
46
+ await page.waitForURL('/login')
47
+ await expect(page).toHaveURL('/login')
48
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
49
+ })
50
+
51
+ test('protected route (live) redirects to login', async ({ page }) => {
52
+ await page.goto('/live')
53
+
54
+ // Should be redirected to login page
55
+ await page.waitForURL('/login')
56
+ await expect(page).toHaveURL('/login')
57
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
58
+ })
59
+
60
+ test('navigation between pages works', async ({ page }) => {
61
+ // Click Login link
62
+ await page.click('[data-testid="nav-login"]')
63
+ await page.waitForURL('/login')
64
+ await expect(page).toHaveURL('/login')
65
+
66
+ // Click Home link
67
+ await page.click('[data-testid="nav-home"]')
68
+ await page.waitForURL('/')
69
+ await expect(page).toHaveURL('/')
70
+ })
71
+ })
@@ -0,0 +1,290 @@
1
+ import { test, expect, Page } from '@playwright/test'
2
+ import { baseURL } from '../playwright.config'
3
+
4
+ /**
5
+ * E2E tests for the Event Subscriptions Example
6
+ *
7
+ * Tests the OAuth login flow and event subscription functionality:
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
+ * 5. Test event subscription CRUD operations
13
+ * 6. Test SSE connection for live events
14
+ *
15
+ * Required environment variables:
16
+ * - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
17
+ * - VITE_EEN_CLIENT_ID: EEN OAuth client ID
18
+ * - TEST_USER: Test user email
19
+ * - TEST_PASSWORD: Test user password
20
+ */
21
+
22
+ // Timeout constants for consistent behavior
23
+ const TIMEOUTS = {
24
+ OAUTH_REDIRECT: 30000, // OAuth redirects can be slow on first load
25
+ ELEMENT_VISIBLE: 15000, // Wait for OAuth page elements to render
26
+ PASSWORD_VISIBLE: 10000, // Password field appears after email validation
27
+ AUTH_COMPLETE: 30000, // Full OAuth flow completion
28
+ UI_UPDATE: 10000, // UI state updates after auth changes
29
+ PROXY_CHECK: 5000, // Quick check if proxy is running
30
+ SSE_CONNECTION: 15000, // SSE connection establishment
31
+ DATA_LOAD: 15000 // Data loading (cameras, event types, subscriptions)
32
+ } as const
33
+
34
+ const TEST_USER = process.env.TEST_USER
35
+ const TEST_PASSWORD = process.env.TEST_PASSWORD
36
+ const PROXY_URL = process.env.VITE_PROXY_URL
37
+
38
+ /**
39
+ * Checks if the OAuth proxy is accessible.
40
+ */
41
+ async function isProxyAccessible(): Promise<boolean> {
42
+ if (!PROXY_URL) return false
43
+ const controller = new AbortController()
44
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
45
+
46
+ try {
47
+ const response = await fetch(PROXY_URL, {
48
+ method: 'HEAD',
49
+ signal: controller.signal
50
+ })
51
+ return response.ok || response.status === 404
52
+ } catch {
53
+ return false
54
+ } finally {
55
+ clearTimeout(timeoutId)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Performs OAuth login flow through the UI.
61
+ */
62
+ async function performLogin(page: Page, username: string, password: string): Promise<void> {
63
+ await page.goto('/')
64
+
65
+ // Click login button and wait for OAuth redirect
66
+ await Promise.all([
67
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
68
+ page.click('[data-testid="login-button"]')
69
+ ])
70
+
71
+ // Fill email
72
+ const emailInput = page.locator('#authentication--input__email')
73
+ await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
74
+ await emailInput.fill(username)
75
+
76
+ // Click next
77
+ await page.getByRole('button', { name: 'Next' }).click()
78
+
79
+ // Fill password
80
+ const passwordInput = page.locator('#authentication--input__password')
81
+ await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
82
+ await passwordInput.fill(password)
83
+
84
+ // Click sign in
85
+ await page.locator('#next, button:has-text("Sign in")').first().click()
86
+
87
+ // Wait for redirect back to the app
88
+ const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
89
+ await page.waitForURL(baseURLPattern, { timeout: TIMEOUTS.AUTH_COMPLETE })
90
+ }
91
+
92
+ /**
93
+ * Clears browser storage to reset auth state.
94
+ */
95
+ async function clearAuthState(page: Page): Promise<void> {
96
+ try {
97
+ const url = page.url()
98
+ if (url && url.startsWith('http')) {
99
+ await page.evaluate(() => {
100
+ try {
101
+ localStorage.clear()
102
+ sessionStorage.clear()
103
+ } catch {
104
+ // Ignore errors
105
+ }
106
+ })
107
+ }
108
+ } catch {
109
+ // Ignore errors
110
+ }
111
+ }
112
+
113
+ test.describe('Event Subscriptions Example', () => {
114
+ let proxyAccessible = false
115
+
116
+ function skipIfNoProxy() {
117
+ test.skip(!proxyAccessible, 'OAuth proxy not accessible')
118
+ }
119
+
120
+ function skipIfNoCredentials() {
121
+ test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
122
+ }
123
+
124
+ test.beforeAll(async () => {
125
+ proxyAccessible = await isProxyAccessible()
126
+ if (!proxyAccessible) {
127
+ console.log('OAuth proxy not accessible - OAuth tests will be skipped')
128
+ }
129
+ })
130
+
131
+ test.afterEach(async ({ page }) => {
132
+ await clearAuthState(page)
133
+ })
134
+
135
+ test('shows login button when not authenticated', async ({ page }) => {
136
+ await page.goto('/')
137
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
138
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
139
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
140
+ })
141
+
142
+ test('login button redirects to OAuth page', async ({ page }) => {
143
+ skipIfNoProxy()
144
+ skipIfNoCredentials()
145
+
146
+ await page.goto('/')
147
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
148
+ await expect(page.locator('[data-testid="login-button"]')).toBeEnabled()
149
+
150
+ // Click login and verify redirect to OAuth page
151
+ await Promise.all([
152
+ page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
153
+ page.click('[data-testid="login-button"]')
154
+ ])
155
+
156
+ // Verify we're on the OAuth page
157
+ const emailInput = page.locator('#authentication--input__email')
158
+ await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
159
+ })
160
+
161
+ test('complete OAuth login flow', async ({ page }) => {
162
+ skipIfNoProxy()
163
+ skipIfNoCredentials()
164
+
165
+ // Verify initially not authenticated
166
+ await page.goto('/')
167
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
168
+
169
+ // Perform login
170
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
171
+
172
+ // Verify authenticated state
173
+ await expect(page.locator('[data-testid="not-authenticated"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
174
+ await expect(page.locator('[data-testid="nav-subscriptions"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
175
+ await expect(page.locator('[data-testid="nav-live"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
176
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
177
+ await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
178
+ })
179
+
180
+ test('can view subscriptions page after login', async ({ page }) => {
181
+ skipIfNoProxy()
182
+ skipIfNoCredentials()
183
+
184
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
185
+ await expect(page.locator('[data-testid="nav-subscriptions"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
186
+
187
+ // Navigate to subscriptions page
188
+ await page.click('[data-testid="nav-subscriptions"]')
189
+ await page.waitForURL('/subscriptions')
190
+
191
+ // Should see the create form and subscriptions table (or no-data message)
192
+ await expect(page.locator('[data-testid="camera-select"]')).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
193
+ await expect(page.locator('[data-testid="event-type-select"]')).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
194
+ })
195
+
196
+ test('can view live events page 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-live"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
202
+
203
+ // Navigate to live events page
204
+ await page.click('[data-testid="nav-live"]')
205
+ await page.waitForURL('/live')
206
+
207
+ // Should see the subscription selector and connect button
208
+ await expect(page.locator('[data-testid="subscription-select"]')).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
209
+ await expect(page.locator('[data-testid="connect-button"]')).toBeVisible()
210
+ await expect(page.locator('[data-testid="connection-status"]')).toHaveText('disconnected')
211
+ })
212
+
213
+ test('can create and delete subscription', async ({ page }) => {
214
+ skipIfNoProxy()
215
+ skipIfNoCredentials()
216
+
217
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
218
+
219
+ // Navigate to subscriptions page
220
+ await page.click('[data-testid="nav-subscriptions"]')
221
+ await page.waitForURL('/subscriptions')
222
+
223
+ // Wait for cameras and event types to load
224
+ await expect(page.locator('[data-testid="camera-select"] option')).not.toHaveCount(0, { timeout: TIMEOUTS.DATA_LOAD })
225
+ await expect(page.locator('[data-testid="event-type-select"] option')).not.toHaveCount(0, { timeout: TIMEOUTS.DATA_LOAD })
226
+
227
+ // Select a camera (first one)
228
+ const cameraSelect = page.locator('[data-testid="camera-select"]')
229
+ const cameraOptions = await cameraSelect.locator('option').all()
230
+ if (cameraOptions.length > 0) {
231
+ await cameraSelect.selectOption({ index: 0 })
232
+ }
233
+
234
+ // Select an event type (first one, typically motion detection)
235
+ const eventTypeSelect = page.locator('[data-testid="event-type-select"]')
236
+ const eventTypeOptions = await eventTypeSelect.locator('option').all()
237
+ if (eventTypeOptions.length > 0) {
238
+ await eventTypeSelect.selectOption({ index: 0 })
239
+ }
240
+
241
+ // Create subscription
242
+ const createButton = page.locator('[data-testid="create-subscription-button"]')
243
+ await expect(createButton).toBeEnabled()
244
+ await createButton.click()
245
+
246
+ // Wait for success message
247
+ await expect(page.locator('.success')).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
248
+
249
+ // Verify subscription appears in table
250
+ await expect(page.locator('[data-testid="subscriptions-table"]')).toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
251
+
252
+ // Delete the first subscription
253
+ const deleteButton = page.locator('button.danger.small').first()
254
+ await expect(deleteButton).toBeVisible()
255
+
256
+ // Count subscriptions before deletion
257
+ const initialRowCount = await page.locator('[data-testid="subscriptions-table"] tbody tr').count()
258
+
259
+ // Accept the confirmation dialog
260
+ page.on('dialog', dialog => dialog.accept())
261
+ await deleteButton.click()
262
+
263
+ // Wait for deletion to complete by checking the row count decreases or delete button is no longer visible
264
+ // This is more reliable than a fixed timeout
265
+ if (initialRowCount > 1) {
266
+ // Wait for row count to decrease
267
+ await expect(page.locator('[data-testid="subscriptions-table"] tbody tr')).toHaveCount(initialRowCount - 1, { timeout: TIMEOUTS.DATA_LOAD })
268
+ } else {
269
+ // If only one subscription, wait for the table or empty state to appear
270
+ await expect(deleteButton).not.toBeVisible({ timeout: TIMEOUTS.DATA_LOAD })
271
+ }
272
+ })
273
+
274
+ test('can logout after login', async ({ page }) => {
275
+ skipIfNoProxy()
276
+ skipIfNoCredentials()
277
+
278
+ await performLogin(page, TEST_USER!, TEST_PASSWORD!)
279
+ await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
280
+
281
+ // Click logout
282
+ await page.click('[data-testid="nav-logout"]')
283
+
284
+ // Should show not authenticated - wait for redirect to app baseURL
285
+ const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
286
+ await page.waitForURL(baseURLPattern)
287
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
288
+ await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
289
+ })
290
+ })
@@ -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 Event Subscriptions Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>