een-api-toolkit 0.3.38 → 0.3.46

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 (55) hide show
  1. package/.claude/agents/docs-accuracy-reviewer.md +35 -5
  2. package/.claude/agents/een-auth-agent.md +28 -0
  3. package/.claude/agents/een-automations-agent.md +264 -0
  4. package/.claude/agents/een-devices-agent.md +5 -7
  5. package/.claude/agents/een-events-agent.md +40 -18
  6. package/.claude/agents/een-media-agent.md +12 -15
  7. package/.claude/agents/een-setup-agent.md +32 -0
  8. package/.claude/agents/een-users-agent.md +2 -2
  9. package/CHANGELOG.md +9 -75
  10. package/dist/index.cjs +3 -3
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +815 -0
  13. package/dist/index.js +986 -719
  14. package/dist/index.js.map +1 -1
  15. package/docs/AI-CONTEXT.md +17 -1
  16. package/docs/ai-reference/AI-AUTH.md +42 -1
  17. package/docs/ai-reference/AI-AUTOMATIONS.md +833 -0
  18. package/docs/ai-reference/AI-DEVICES.md +1 -1
  19. package/docs/ai-reference/AI-EVENTS.md +2 -2
  20. package/docs/ai-reference/AI-GROUPING.md +128 -66
  21. package/docs/ai-reference/AI-MEDIA.md +1 -1
  22. package/docs/ai-reference/AI-SETUP.md +1 -1
  23. package/docs/ai-reference/AI-USERS.md +1 -1
  24. package/examples/vue-alerts-metrics/src/App.vue +7 -1
  25. package/examples/vue-automations/.env.example +11 -0
  26. package/examples/vue-automations/README.md +205 -0
  27. package/examples/vue-automations/e2e/app.spec.ts +83 -0
  28. package/examples/vue-automations/e2e/auth.spec.ts +468 -0
  29. package/examples/vue-automations/index.html +13 -0
  30. package/examples/vue-automations/package-lock.json +1722 -0
  31. package/examples/vue-automations/package.json +29 -0
  32. package/examples/vue-automations/playwright.config.ts +46 -0
  33. package/examples/vue-automations/src/App.vue +122 -0
  34. package/examples/vue-automations/src/main.ts +23 -0
  35. package/examples/vue-automations/src/router/index.ts +61 -0
  36. package/examples/vue-automations/src/views/Automations.vue +692 -0
  37. package/examples/vue-automations/src/views/Callback.vue +76 -0
  38. package/examples/vue-automations/src/views/Home.vue +172 -0
  39. package/examples/vue-automations/src/views/Login.vue +33 -0
  40. package/examples/vue-automations/src/views/Logout.vue +66 -0
  41. package/examples/vue-automations/src/vite-env.d.ts +1 -0
  42. package/examples/vue-automations/tsconfig.json +21 -0
  43. package/examples/vue-automations/tsconfig.node.json +10 -0
  44. package/examples/vue-automations/vite.config.ts +12 -0
  45. package/examples/vue-bridges/e2e/auth.spec.ts +97 -0
  46. package/examples/vue-bridges/src/App.vue +7 -1
  47. package/examples/vue-cameras/src/App.vue +7 -1
  48. package/examples/vue-event-subscriptions/src/App.vue +7 -1
  49. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +1 -1
  50. package/examples/vue-events/src/App.vue +7 -1
  51. package/examples/vue-layouts/src/App.vue +7 -1
  52. package/examples/vue-users/package-lock.json +2 -2
  53. package/examples/vue-users/package.json +1 -1
  54. package/examples/vue-users/src/App.vue +7 -1
  55. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  # Cameras & Bridges API - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.38
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete reference for camera and bridge management.
6
6
  > Load this document when working with devices.
@@ -1,6 +1,6 @@
1
1
  # Events, Alerts & Real-Time Streaming - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.38
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete reference for events, alerts, metrics, and SSE subscriptions.
6
6
  > Load this document when implementing event-driven features.
@@ -1632,7 +1632,7 @@ watch(selectedSubscriptionId, (newId) => {
1632
1632
  <ul class="warning-list">
1633
1633
  <li>SSE URLs are single-use. Once disconnected, the subscription cannot be reconnected.</li>
1634
1634
  <li>To receive events again after disconnecting, create a new subscription.</li>
1635
- <li>Subscriptions have a 15-minute TTL and expire if not connected.</li>
1635
+ <li>SSE subscriptions have a server-determined 15-minute TTL (not configurable) and expire if not connected.</li>
1636
1636
  </ul>
1637
1637
  </div>
1638
1638
 
@@ -1,6 +1,6 @@
1
1
  # Layouts API - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.30
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete reference for layout management (camera grouping).
6
6
  > Load this document when working with layouts.
@@ -241,16 +241,29 @@ if (error) {
241
241
 
242
242
  ---
243
243
 
244
- ## Vue Components
244
+ ## Vue Component Example
245
245
 
246
- ### Layouts.vue
247
-
248
- ```vue
249
- <script setup lang="ts">
246
+ ```typescript
250
247
  import { ref, computed, onMounted } from 'vue'
251
- import { getLayouts, createLayout, updateLayout, deleteLayout, type Layout, type EenError, type ListLayoutsParams, type LayoutSettings } from 'een-api-toolkit'
252
-
248
+ import {
249
+ getLayouts,
250
+ getCameras,
251
+ createLayout,
252
+ updateLayout,
253
+ deleteLayout,
254
+ type Layout,
255
+ type Camera,
256
+ type EenError,
257
+ type ListLayoutsParams,
258
+ type CreateLayoutParams,
259
+ type UpdateLayoutParams,
260
+ type LayoutSettings
261
+ } from 'een-api-toolkit'
262
+ import LayoutModal from '../components/LayoutModal.vue'
263
+
264
+ // Reactive state
253
265
  const layouts = ref<Layout[]>([])
266
+ const cameras = ref<Camera[]>([])
254
267
  const loading = ref(false)
255
268
  const error = ref<EenError | null>(null)
256
269
  const nextPageToken = ref<string | undefined>(undefined)
@@ -262,6 +275,20 @@ const params = ref<ListLayoutsParams>({
262
275
  include: ['resourceCounts', 'effectivePermissions']
263
276
  })
264
277
 
278
+ // Modal state
279
+ const showModal = ref(false)
280
+ const selectedLayout = ref<Layout | null>(null)
281
+ const modalLoading = ref(false)
282
+ const modalError = ref<string | null>(null)
283
+
284
+ // Default settings for new layouts
285
+ const defaultSettings: LayoutSettings = {
286
+ showCameraBorder: true,
287
+ showCameraName: true,
288
+ cameraAspectRatio: '16x9',
289
+ paneColumns: 3
290
+ }
291
+
265
292
  async function fetchLayouts(fetchParams?: ListLayoutsParams, append = false) {
266
293
  loading.value = true
267
294
  error.value = null
@@ -271,7 +298,9 @@ async function fetchLayouts(fetchParams?: ListLayoutsParams, append = false) {
271
298
 
272
299
  if (result.error) {
273
300
  error.value = result.error
274
- if (!append) layouts.value = []
301
+ if (!append) {
302
+ layouts.value = []
303
+ }
275
304
  nextPageToken.value = undefined
276
305
  } else {
277
306
  if (append) {
@@ -283,80 +312,113 @@ async function fetchLayouts(fetchParams?: ListLayoutsParams, append = false) {
283
312
  }
284
313
 
285
314
  loading.value = false
315
+ return result
316
+ }
317
+
318
+ async function fetchCameras() {
319
+ const result = await getCameras({ pageSize: 100, include: ['status'] })
320
+ if (result.data) {
321
+ cameras.value = result.data.results
322
+ }
323
+ }
324
+
325
+ function refresh() {
326
+ return fetchLayouts()
286
327
  }
287
328
 
288
329
  async function fetchNextPage() {
289
330
  if (!nextPageToken.value) return
290
- return fetchLayouts({ ...params.value, pageToken: nextPageToken.value }, true)
331
+ // Destructure to explicitly exclude any existing pageToken from params
332
+ const { pageToken: _existingToken, ...restParams } = params.value
333
+ return fetchLayouts({ ...restParams, pageToken: nextPageToken.value }, true)
291
334
  }
292
335
 
293
- async function handleCreate(name: string, settings: LayoutSettings) {
294
- const result = await createLayout({ name, settings })
295
- if (result.data) {
296
- await fetchLayouts()
297
- }
298
- return result
336
+ function openCreateModal() {
337
+ selectedLayout.value = null
338
+ modalError.value = null
339
+ showModal.value = true
340
+ }
341
+
342
+ function openEditModal(layout: Layout) {
343
+ selectedLayout.value = layout
344
+ modalError.value = null
345
+ showModal.value = true
346
+ }
347
+
348
+ function closeModal() {
349
+ showModal.value = false
350
+ selectedLayout.value = null
351
+ modalError.value = null
299
352
  }
300
353
 
301
- async function handleUpdate(layoutId: string, name: string, settings: Partial<LayoutSettings>) {
302
- const result = await updateLayout(layoutId, { name, settings })
303
- if (!result.error) {
354
+ async function handleSave(data: { name: string; settings: LayoutSettings; panes: Layout['panes'] }) {
355
+ modalLoading.value = true
356
+ modalError.value = null
357
+
358
+ try {
359
+ if (selectedLayout.value) {
360
+ // Update existing layout
361
+ const updateParams: UpdateLayoutParams = {
362
+ name: data.name,
363
+ settings: data.settings,
364
+ panes: data.panes
365
+ }
366
+
367
+ const result = await updateLayout(selectedLayout.value.id, updateParams)
368
+ if (result.error) {
369
+ modalError.value = result.error.message
370
+ return
371
+ }
372
+ } else {
373
+ // Create new layout
374
+ const createParams: CreateLayoutParams = {
375
+ name: data.name,
376
+ settings: data.settings,
377
+ panes: data.panes
378
+ }
379
+
380
+ const result = await createLayout(createParams)
381
+ if (result.error) {
382
+ modalError.value = result.error.message
383
+ return
384
+ }
385
+ }
386
+
387
+ closeModal()
304
388
  await fetchLayouts()
389
+ } catch (err) {
390
+ // Handle unexpected errors (network failures, state mutations, etc.)
391
+ modalError.value = err instanceof Error ? err.message : 'An unexpected error occurred'
392
+ console.error('handleSave error:', err)
393
+ } finally {
394
+ modalLoading.value = false
305
395
  }
306
- return result
307
396
  }
308
397
 
309
398
  async function handleDelete(layoutId: string) {
399
+ if (!confirm('Are you sure you want to delete this layout?')) {
400
+ return
401
+ }
402
+
403
+ modalLoading.value = true
404
+ modalError.value = null
405
+
310
406
  const result = await deleteLayout(layoutId)
311
- if (!result.error) {
312
- await fetchLayouts()
407
+
408
+ if (result.error) {
409
+ modalError.value = result.error.message
410
+ modalLoading.value = false
411
+ return
313
412
  }
314
- return result
413
+
414
+ closeModal()
415
+ await fetchLayouts()
416
+ modalLoading.value = false
315
417
  }
316
418
 
317
- onMounted(() => fetchLayouts())
318
- </script>
319
-
320
- <template>
321
- <div class="layouts">
322
- <div class="header">
323
- <h2>Layouts</h2>
324
- <button @click="fetchLayouts" :disabled="loading">
325
- {{ loading ? 'Loading...' : 'Refresh' }}
326
- </button>
327
- </div>
328
-
329
- <div v-if="loading && layouts.length === 0" class="loading">
330
- Loading layouts...
331
- </div>
332
-
333
- <div v-else-if="error" class="error">
334
- Error: {{ error.message }}
335
- </div>
336
-
337
- <div v-else>
338
- <div v-if="layouts.length > 0" class="layout-grid">
339
- <div v-for="layout in layouts" :key="layout.id" class="layout-card">
340
- <h3>{{ layout.name }}</h3>
341
- <p>Panes: {{ layout.panes.length }}</p>
342
- <p>Columns: {{ layout.settings.paneColumns }}</p>
343
- <div v-if="layout.effectivePermissions">
344
- <span v-if="layout.effectivePermissions.edit">Can Edit</span>
345
- <span v-if="layout.effectivePermissions.delete">Can Delete</span>
346
- </div>
347
- </div>
348
- </div>
349
-
350
- <p v-else>No layouts found.</p>
351
-
352
- <div v-if="hasNextPage" class="pagination">
353
- <button @click="fetchNextPage" :disabled="loading">
354
- {{ loading ? 'Loading...' : 'Load More' }}
355
- </button>
356
- </div>
357
- </div>
358
- </div>
359
- </template>
419
+ onMounted(async () => {
420
+ await Promise.all([fetchLayouts(), fetchCameras()])
421
+ })
360
422
  ```
361
423
 
362
424
  ---
@@ -1,6 +1,6 @@
1
1
  # Media & Live Video - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.38
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete reference for media retrieval, live streaming, and video playback.
6
6
  > Load this document when implementing video features.
@@ -1,6 +1,6 @@
1
1
  # Vue 3 Application Setup - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.38
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete guide for setting up a Vue 3 application with the een-api-toolkit.
6
6
  > Load this document when creating a new project or troubleshooting setup issues.
@@ -1,6 +1,6 @@
1
1
  # Users API - EEN API Toolkit
2
2
 
3
- > **Version:** 0.3.38
3
+ > **Version:** 0.3.46
4
4
  >
5
5
  > Complete reference for user management.
6
6
  > Load this document when working with user data.
@@ -1,9 +1,15 @@
1
1
  <script setup lang="ts">
2
+ import { onMounted, computed } from 'vue'
2
3
  import { useAuthStore } from 'een-api-toolkit'
3
- import { computed } from 'vue'
4
4
 
5
5
  const authStore = useAuthStore()
6
6
  const isAuthenticated = computed(() => authStore.isAuthenticated)
7
+
8
+ // Initialize auth store from storage on app mount
9
+ // This restores the session if a valid token exists in localStorage/sessionStorage
10
+ onMounted(() => {
11
+ authStore.initialize()
12
+ })
7
13
  </script>
8
14
 
9
15
  <template>
@@ -0,0 +1,11 @@
1
+ # EEN OAuth proxy URL
2
+ VITE_PROXY_URL=http://localhost:8787
3
+
4
+ # EEN OAuth client ID
5
+ VITE_EEN_CLIENT_ID=your-client-id-here
6
+
7
+ # OAuth redirect URI (must be exactly this for EEN IDP)
8
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
9
+
10
+ # Enable debug logging
11
+ VITE_DEBUG=true
@@ -0,0 +1,205 @@
1
+ # EEN API Toolkit - Vue Automations Example
2
+
3
+ A complete example showing how to use the een-api-toolkit automation functions in a Vue 3 application.
4
+
5
+ ## Storage Strategy: Memory
6
+
7
+ This example uses the `memory` storage strategy for maximum security. This means:
8
+
9
+ - **Tokens are never written to disk** - immune to localStorage/sessionStorage XSS attacks
10
+ - **Page refresh requires re-authentication** - tokens exist only in memory
11
+ - **Each tab is independent** - opening a new tab requires separate login
12
+
13
+ This is the recommended strategy for high-security deployments where protecting against XSS token theft is critical.
14
+
15
+ ## Features Demonstrated
16
+
17
+ - OAuth authentication flow (login, callback, logout)
18
+ - Protected routes with navigation guards
19
+ - List event alert condition rules
20
+ - List alert condition rules with actions and insights
21
+ - List alert action rules
22
+ - List alert actions (notifications, webhooks, etc.)
23
+ - Filter by enabled status
24
+ - Pagination support
25
+
26
+ ## APIs Used
27
+
28
+ - `listEventAlertConditionRules()` - List event alert condition rules
29
+ - `listAlertConditionRules()` - List alert condition rules with optional includes
30
+ - `listAlertActionRules()` - List alert action rules
31
+ - `listAlertActions()` - List alert actions
32
+ - `useAuthStore()` - Authentication state management
33
+ - `getAuthUrl()` - Generate OAuth login URL
34
+ - `handleAuthCallback()` - Process OAuth callback
35
+ - `initEenToolkit()` - Toolkit initialization
36
+
37
+ ## Setup
38
+
39
+ ### Prerequisites
40
+
41
+ 1. **Start the OAuth proxy** (required for authentication):
42
+
43
+ The OAuth proxy is a separate project that handles token management securely.
44
+ Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
45
+
46
+ ```bash
47
+ # In a separate terminal, from the een-oauth-proxy directory
48
+ npm install
49
+ npm run dev
50
+ ```
51
+
52
+ The proxy should be running at `http://localhost:8787`.
53
+
54
+ ### Example Setup
55
+
56
+ All commands below should be run from this example directory (`examples/vue-automations/`):
57
+
58
+ 2. Copy the environment file:
59
+ ```bash
60
+ # From examples/vue-automations/
61
+ cp .env.example .env
62
+ ```
63
+
64
+ 3. Edit `.env` with your EEN credentials:
65
+ ```env
66
+ VITE_EEN_CLIENT_ID=your-client-id
67
+ VITE_PROXY_URL=http://localhost:8787
68
+ # DO NOT change the redirect URI - EEN IDP only permits this URL
69
+ VITE_REDIRECT_URI=http://127.0.0.1:3333
70
+ ```
71
+
72
+ 4. Install dependencies and start:
73
+ ```bash
74
+ # From examples/vue-automations/
75
+ npm install
76
+ npm run dev
77
+ ```
78
+
79
+ 5. Open http://127.0.0.1:3333 in your browser.
80
+
81
+ **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.
82
+
83
+ **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.
84
+
85
+ ## Project Structure
86
+
87
+ ```
88
+ src/
89
+ ├── main.ts # App entry, toolkit initialization
90
+ ├── App.vue # Root component with navigation
91
+ ├── router/
92
+ │ └── index.ts # Vue Router with auth guards
93
+ └── views/
94
+ ├── Home.vue # Home page with user profile
95
+ ├── Login.vue # OAuth login redirect
96
+ ├── Callback.vue # OAuth callback handler
97
+ ├── Automations.vue # Automation rules display
98
+ └── Logout.vue # Logout handler
99
+ ```
100
+
101
+ ## Key Code Examples
102
+
103
+ ### Initializing the Toolkit (main.ts)
104
+
105
+ ```typescript
106
+ import { initEenToolkit } from 'een-api-toolkit'
107
+
108
+ initEenToolkit({
109
+ proxyUrl: import.meta.env.VITE_PROXY_URL,
110
+ clientId: import.meta.env.VITE_EEN_CLIENT_ID,
111
+ storageStrategy: 'memory', // Maximum security - tokens lost on refresh
112
+ debug: true
113
+ })
114
+ ```
115
+
116
+ ### Listing Event Alert Condition Rules
117
+
118
+ ```typescript
119
+ import { listEventAlertConditionRules, type EventAlertConditionRule } from 'een-api-toolkit'
120
+
121
+ const rules = ref<EventAlertConditionRule[]>([])
122
+
123
+ async function fetchRules() {
124
+ const result = await listEventAlertConditionRules({
125
+ pageSize: 10,
126
+ enabled: true // Only enabled rules
127
+ })
128
+
129
+ if (result.error) {
130
+ // Handle error
131
+ } else {
132
+ rules.value = result.data.results
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### Listing Alert Condition Rules with Includes
138
+
139
+ ```typescript
140
+ import { listAlertConditionRules, type AlertConditionRule } from 'een-api-toolkit'
141
+
142
+ const rules = ref<AlertConditionRule[]>([])
143
+
144
+ async function fetchRules() {
145
+ const result = await listAlertConditionRules({
146
+ pageSize: 10,
147
+ include: ['actions', 'insights'] // Include related data
148
+ })
149
+
150
+ if (result.error) {
151
+ // Handle error
152
+ } else {
153
+ rules.value = result.data.results
154
+ // Each rule now has rule.actions and rule.insights populated
155
+ }
156
+ }
157
+ ```
158
+
159
+ ### Listing Alert Actions
160
+
161
+ ```typescript
162
+ import { listAlertActions, type AutomationAlertAction } from 'een-api-toolkit'
163
+
164
+ const actions = ref<AutomationAlertAction[]>([])
165
+
166
+ async function fetchActions() {
167
+ const result = await listAlertActions({
168
+ pageSize: 10,
169
+ type__in: ['notification', 'webhook', 'slack'] // Filter by action type
170
+ })
171
+
172
+ if (result.error) {
173
+ // Handle error
174
+ } else {
175
+ actions.value = result.data.results
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### Auth Guard (router/index.ts)
181
+
182
+ ```typescript
183
+ router.beforeEach((to, from, next) => {
184
+ const authStore = useAuthStore()
185
+
186
+ if (to.meta.requiresAuth && !authStore.isAuthenticated) {
187
+ next('/login')
188
+ } else {
189
+ next()
190
+ }
191
+ })
192
+ ```
193
+
194
+ ## Running E2E Tests
195
+
196
+ ```bash
197
+ # Install Playwright browsers (first time only)
198
+ npx playwright install
199
+
200
+ # Run E2E tests
201
+ npm run test:e2e
202
+
203
+ # Run with UI
204
+ npm run test:e2e:ui
205
+ ```
@@ -0,0 +1,83 @@
1
+ import { test, expect } from '@playwright/test'
2
+
3
+ test.describe('Vue Automations 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(/EEN Automations/)
10
+ })
11
+
12
+ test('header displays app name', async ({ page }) => {
13
+ await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN Automations Example')
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
+ // Automations and Logout should NOT be visible (requires auth)
24
+ await expect(page.locator('[data-testid="nav-automations"]')).not.toBeVisible()
25
+ await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
26
+ })
27
+
28
+ test('home page shows not logged in message', async ({ page }) => {
29
+ await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
30
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
31
+ })
32
+
33
+ test('home page displays storage strategy', async ({ page }) => {
34
+ await expect(page.locator('[data-testid="storage-strategy"]')).toBeVisible()
35
+ })
36
+
37
+ test('login page displays login button', async ({ page }) => {
38
+ await page.goto('/login')
39
+
40
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
41
+ await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
42
+ })
43
+
44
+ test('protected route redirects to login', async ({ page }) => {
45
+ await page.goto('/automations')
46
+
47
+ // Should be redirected to login page
48
+ await page.waitForURL('/login')
49
+ await expect(page).toHaveURL('/login')
50
+ await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
51
+ })
52
+
53
+ test('navigation between pages works', async ({ page }) => {
54
+ // Click Login link
55
+ await page.click('[data-testid="nav-login"]')
56
+ await page.waitForURL('/login')
57
+ await expect(page).toHaveURL('/login')
58
+
59
+ // Click Home link
60
+ await page.click('[data-testid="nav-home"]')
61
+ await page.waitForURL('/')
62
+ await expect(page).toHaveURL('/')
63
+ })
64
+
65
+ test('home page shows features list', async ({ page }) => {
66
+ // Check that the features section is visible
67
+ const description = page.locator('.description')
68
+ await expect(description).toBeVisible()
69
+
70
+ // Check for features list items (6 features + 4 functions = 10 total)
71
+ await expect(description.locator('li')).toHaveCount(10)
72
+ })
73
+
74
+ test('home page shows functions used', async ({ page }) => {
75
+ // Check that the functions used section mentions the automation functions
76
+ const description = page.locator('.description')
77
+ await expect(description.locator('code')).toHaveCount(4)
78
+ await expect(description.locator('text=listEventAlertConditionRules')).toBeVisible()
79
+ await expect(description.locator('text=listAlertConditionRules')).toBeVisible()
80
+ await expect(description.locator('text=listAlertActionRules')).toBeVisible()
81
+ await expect(description.locator('text=listAlertActions')).toBeVisible()
82
+ })
83
+ })