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.
- package/CHANGELOG.md +43 -10
- package/README.md +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +483 -260
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +12 -1
- package/examples/vue-event-subscriptions/.env.example +15 -0
- package/examples/vue-event-subscriptions/README.md +103 -0
- package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
- package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
- package/examples/vue-event-subscriptions/index.html +13 -0
- package/examples/vue-event-subscriptions/package-lock.json +1719 -0
- package/examples/vue-event-subscriptions/package.json +28 -0
- package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
- package/examples/vue-event-subscriptions/src/App.vue +233 -0
- package/examples/vue-event-subscriptions/src/main.ts +25 -0
- package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
- package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
- package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +640 -0
- package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
- package/examples/vue-event-subscriptions/src/views/Logout.vue +59 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +402 -0
- package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
- package/examples/vue-event-subscriptions/tsconfig.json +21 -0
- package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
- package/examples/vue-event-subscriptions/vite.config.ts +12 -0
- package/package.json +1 -1
package/docs/AI-CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# EEN API Toolkit - AI Reference
|
|
2
2
|
|
|
3
|
-
> **Version:** 0.3.
|
|
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>
|