een-api-toolkit 0.3.47 → 0.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/een-jobs-agent.md +676 -0
- package/CHANGELOG.md +7 -8
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1172 -28
- package/dist/index.js +796 -333
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +22 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1084 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-jobs/.env.example +11 -0
- package/examples/vue-jobs/README.md +245 -0
- package/examples/vue-jobs/e2e/app.spec.ts +79 -0
- package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
- package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
- package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
- package/examples/vue-jobs/index.html +13 -0
- package/examples/vue-jobs/package-lock.json +1722 -0
- package/examples/vue-jobs/package.json +28 -0
- package/examples/vue-jobs/playwright.config.ts +47 -0
- package/examples/vue-jobs/src/App.vue +154 -0
- package/examples/vue-jobs/src/main.ts +25 -0
- package/examples/vue-jobs/src/router/index.ts +82 -0
- package/examples/vue-jobs/src/views/Callback.vue +76 -0
- package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
- package/examples/vue-jobs/src/views/Files.vue +424 -0
- package/examples/vue-jobs/src/views/Home.vue +195 -0
- package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
- package/examples/vue-jobs/src/views/Jobs.vue +297 -0
- package/examples/vue-jobs/src/views/Login.vue +33 -0
- package/examples/vue-jobs/src/views/Logout.vue +59 -0
- package/examples/vue-jobs/src/vite-env.d.ts +1 -0
- package/examples/vue-jobs/tsconfig.json +25 -0
- package/examples/vue-jobs/vite.config.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test'
|
|
2
|
+
import { baseURL } from '../playwright.config'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* E2E tests for delete features in Jobs and Files pages
|
|
6
|
+
*
|
|
7
|
+
* Tests the delete buttons and confirmation dialogs:
|
|
8
|
+
* - Delete buttons are visible in Actions column
|
|
9
|
+
* - Clicking delete shows confirmation dialog
|
|
10
|
+
* - Canceling dialog does not delete the item
|
|
11
|
+
* - Tables use at least 80% of viewport width
|
|
12
|
+
*
|
|
13
|
+
* Note: These tests do NOT actually delete data to preserve test account state.
|
|
14
|
+
* They only verify the UI behavior and confirm dialog functionality.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Timeout constants
|
|
18
|
+
const TIMEOUTS = {
|
|
19
|
+
OAUTH_REDIRECT: 30000,
|
|
20
|
+
ELEMENT_VISIBLE: 15000,
|
|
21
|
+
PASSWORD_VISIBLE: 10000,
|
|
22
|
+
AUTH_COMPLETE: 30000,
|
|
23
|
+
UI_UPDATE: 10000,
|
|
24
|
+
PROXY_CHECK: 5000,
|
|
25
|
+
DIALOG_VISIBLE: 5000
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
const TEST_USER = process.env.TEST_USER
|
|
29
|
+
const TEST_PASSWORD = process.env.TEST_PASSWORD
|
|
30
|
+
const PROXY_URL = process.env.VITE_PROXY_URL
|
|
31
|
+
|
|
32
|
+
async function isProxyAccessible(): Promise<boolean> {
|
|
33
|
+
if (!PROXY_URL) return false
|
|
34
|
+
const controller = new AbortController()
|
|
35
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(PROXY_URL, {
|
|
39
|
+
method: 'HEAD',
|
|
40
|
+
signal: controller.signal
|
|
41
|
+
})
|
|
42
|
+
return response.ok || response.status === 404
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timeoutId)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function performLogin(page: Page, username: string, password: string): Promise<void> {
|
|
51
|
+
await page.goto('/')
|
|
52
|
+
|
|
53
|
+
await Promise.all([
|
|
54
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
55
|
+
page.click('[data-testid="login-button"]')
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
59
|
+
await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
60
|
+
await emailInput.fill(username)
|
|
61
|
+
|
|
62
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
63
|
+
|
|
64
|
+
const passwordInput = page.locator('#authentication--input__password')
|
|
65
|
+
await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
|
|
66
|
+
await passwordInput.fill(password)
|
|
67
|
+
|
|
68
|
+
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
69
|
+
|
|
70
|
+
const baseURLPattern = new RegExp(baseURL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
71
|
+
await page.waitForURL(baseURLPattern, { timeout: TIMEOUTS.AUTH_COMPLETE })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function clearAuthState(page: Page): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const url = page.url()
|
|
77
|
+
if (url && url.startsWith('http')) {
|
|
78
|
+
await page.evaluate(() => {
|
|
79
|
+
try {
|
|
80
|
+
localStorage.clear()
|
|
81
|
+
sessionStorage.clear()
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore errors
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore errors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test.describe('Delete Features - Jobs and Files Pages', () => {
|
|
93
|
+
let proxyAccessible = false
|
|
94
|
+
|
|
95
|
+
function skipIfNoProxy() {
|
|
96
|
+
test.skip(!proxyAccessible, 'OAuth proxy not accessible')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function skipIfNoCredentials() {
|
|
100
|
+
test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test.beforeAll(async () => {
|
|
104
|
+
proxyAccessible = await isProxyAccessible()
|
|
105
|
+
if (!proxyAccessible) {
|
|
106
|
+
console.log('OAuth proxy not accessible - delete feature tests will be skipped')
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test.afterEach(async ({ page }) => {
|
|
111
|
+
await clearAuthState(page)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test.describe('Jobs Page Delete Feature', () => {
|
|
115
|
+
test('jobs table has delete buttons in actions column', async ({ page }) => {
|
|
116
|
+
skipIfNoProxy()
|
|
117
|
+
skipIfNoCredentials()
|
|
118
|
+
|
|
119
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
120
|
+
await page.click('[data-testid="nav-jobs"]')
|
|
121
|
+
await page.waitForURL('/jobs')
|
|
122
|
+
|
|
123
|
+
// Wait for loading to complete
|
|
124
|
+
await page.waitForFunction(
|
|
125
|
+
() => !document.querySelector('.jobs .loading'),
|
|
126
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
127
|
+
).catch(() => {})
|
|
128
|
+
|
|
129
|
+
// Check if table exists
|
|
130
|
+
const table = page.locator('.jobs table')
|
|
131
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
132
|
+
|
|
133
|
+
if (!hasTable) {
|
|
134
|
+
const noJobs = page.locator('.jobs p:has-text("No jobs found")')
|
|
135
|
+
const hasNoJobs = await noJobs.isVisible({ timeout: 2000 }).catch(() => false)
|
|
136
|
+
if (hasNoJobs) {
|
|
137
|
+
console.log('No jobs found - skipping delete button test')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
throw new Error('Neither jobs table nor "No jobs found" message visible')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for delete buttons in actions column
|
|
144
|
+
const deleteButtons = table.locator('tbody tr td.actions button.btn-danger')
|
|
145
|
+
const buttonCount = await deleteButtons.count()
|
|
146
|
+
console.log(`Found ${buttonCount} delete buttons in jobs table`)
|
|
147
|
+
|
|
148
|
+
expect(buttonCount).toBeGreaterThan(0)
|
|
149
|
+
|
|
150
|
+
// Verify first delete button has correct text
|
|
151
|
+
const firstDeleteButton = deleteButtons.first()
|
|
152
|
+
await expect(firstDeleteButton).toHaveText('Delete')
|
|
153
|
+
await expect(firstDeleteButton).toBeEnabled()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('jobs delete button shows confirmation dialog', async ({ page }) => {
|
|
157
|
+
skipIfNoProxy()
|
|
158
|
+
skipIfNoCredentials()
|
|
159
|
+
|
|
160
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
161
|
+
await page.click('[data-testid="nav-jobs"]')
|
|
162
|
+
await page.waitForURL('/jobs')
|
|
163
|
+
|
|
164
|
+
await page.waitForFunction(
|
|
165
|
+
() => !document.querySelector('.jobs .loading'),
|
|
166
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
167
|
+
).catch(() => {})
|
|
168
|
+
|
|
169
|
+
const table = page.locator('.jobs table')
|
|
170
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
171
|
+
|
|
172
|
+
if (!hasTable) {
|
|
173
|
+
console.log('No jobs table visible - skipping confirmation dialog test')
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const deleteButtons = table.locator('tbody tr td.actions button.btn-danger')
|
|
178
|
+
const buttonCount = await deleteButtons.count()
|
|
179
|
+
|
|
180
|
+
if (buttonCount === 0) {
|
|
181
|
+
console.log('No delete buttons found - skipping confirmation dialog test')
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Set up dialog handler to capture and dismiss
|
|
186
|
+
let dialogMessage = ''
|
|
187
|
+
page.on('dialog', async dialog => {
|
|
188
|
+
dialogMessage = dialog.message()
|
|
189
|
+
console.log('Dialog message:', dialogMessage)
|
|
190
|
+
await dialog.dismiss() // Cancel the delete
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Click the first delete button
|
|
194
|
+
await deleteButtons.first().click()
|
|
195
|
+
|
|
196
|
+
// Wait a moment for dialog
|
|
197
|
+
await page.waitForTimeout(500)
|
|
198
|
+
|
|
199
|
+
// Verify dialog was shown with confirmation message
|
|
200
|
+
expect(dialogMessage).toContain('Are you sure')
|
|
201
|
+
expect(dialogMessage).toContain('delete')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('canceling jobs delete dialog does not remove the job', async ({ page }) => {
|
|
205
|
+
skipIfNoProxy()
|
|
206
|
+
skipIfNoCredentials()
|
|
207
|
+
|
|
208
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
209
|
+
await page.click('[data-testid="nav-jobs"]')
|
|
210
|
+
await page.waitForURL('/jobs')
|
|
211
|
+
|
|
212
|
+
await page.waitForFunction(
|
|
213
|
+
() => !document.querySelector('.jobs .loading'),
|
|
214
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
215
|
+
).catch(() => {})
|
|
216
|
+
|
|
217
|
+
const table = page.locator('.jobs table')
|
|
218
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
219
|
+
|
|
220
|
+
if (!hasTable) {
|
|
221
|
+
console.log('No jobs table visible - skipping cancel test')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Count rows before
|
|
226
|
+
const rows = table.locator('tbody tr')
|
|
227
|
+
const rowCountBefore = await rows.count()
|
|
228
|
+
console.log(`Jobs before cancel: ${rowCountBefore}`)
|
|
229
|
+
|
|
230
|
+
if (rowCountBefore === 0) {
|
|
231
|
+
console.log('No jobs found - skipping cancel test')
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get the first job's name for verification
|
|
236
|
+
const firstJobName = await rows.first().locator('td').first().textContent()
|
|
237
|
+
|
|
238
|
+
// Set up dialog handler to dismiss (cancel)
|
|
239
|
+
page.on('dialog', async dialog => {
|
|
240
|
+
await dialog.dismiss()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// Click delete button
|
|
244
|
+
const deleteButton = table.locator('tbody tr td.actions button.btn-danger').first()
|
|
245
|
+
await deleteButton.click()
|
|
246
|
+
|
|
247
|
+
// Wait a moment
|
|
248
|
+
await page.waitForTimeout(500)
|
|
249
|
+
|
|
250
|
+
// Count rows after - should be the same
|
|
251
|
+
const rowCountAfter = await rows.count()
|
|
252
|
+
console.log(`Jobs after cancel: ${rowCountAfter}`)
|
|
253
|
+
|
|
254
|
+
expect(rowCountAfter).toBe(rowCountBefore)
|
|
255
|
+
|
|
256
|
+
// Verify first job is still there
|
|
257
|
+
const firstJobNameAfter = await rows.first().locator('td').first().textContent()
|
|
258
|
+
expect(firstJobNameAfter).toBe(firstJobName)
|
|
259
|
+
console.log('Job was NOT deleted after canceling dialog')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('jobs table uses at least 80% of viewport width', async ({ page }) => {
|
|
263
|
+
skipIfNoProxy()
|
|
264
|
+
skipIfNoCredentials()
|
|
265
|
+
|
|
266
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
267
|
+
await page.click('[data-testid="nav-jobs"]')
|
|
268
|
+
await page.waitForURL('/jobs')
|
|
269
|
+
|
|
270
|
+
await page.waitForFunction(
|
|
271
|
+
() => !document.querySelector('.jobs .loading'),
|
|
272
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
273
|
+
).catch(() => {})
|
|
274
|
+
|
|
275
|
+
// Get the jobs container width
|
|
276
|
+
const jobsContainer = page.locator('.jobs')
|
|
277
|
+
const isVisible = await jobsContainer.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
278
|
+
|
|
279
|
+
if (!isVisible) {
|
|
280
|
+
console.log('Jobs container not visible - skipping width test')
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const viewportSize = page.viewportSize()
|
|
285
|
+
if (!viewportSize) {
|
|
286
|
+
console.log('Could not get viewport size - skipping width test')
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const containerBox = await jobsContainer.boundingBox()
|
|
291
|
+
if (!containerBox) {
|
|
292
|
+
console.log('Could not get container bounding box - skipping width test')
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const widthPercentage = (containerBox.width / viewportSize.width) * 100
|
|
297
|
+
console.log(`Jobs container width: ${containerBox.width}px (${widthPercentage.toFixed(1)}% of viewport)`)
|
|
298
|
+
|
|
299
|
+
// Should be at least 80% of viewport width
|
|
300
|
+
expect(widthPercentage).toBeGreaterThanOrEqual(80)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test.describe('Files Page Delete Feature', () => {
|
|
305
|
+
test('files table has delete buttons in actions column', async ({ page }) => {
|
|
306
|
+
skipIfNoProxy()
|
|
307
|
+
skipIfNoCredentials()
|
|
308
|
+
|
|
309
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
310
|
+
await page.click('[data-testid="nav-files"]')
|
|
311
|
+
await page.waitForURL('/files')
|
|
312
|
+
|
|
313
|
+
await page.waitForFunction(
|
|
314
|
+
() => !document.querySelector('.files .loading'),
|
|
315
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
316
|
+
).catch(() => {})
|
|
317
|
+
|
|
318
|
+
const table = page.locator('.files table')
|
|
319
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
320
|
+
|
|
321
|
+
if (!hasTable) {
|
|
322
|
+
const noFiles = page.locator('.files p:has-text("No files found")')
|
|
323
|
+
const hasNoFiles = await noFiles.isVisible({ timeout: 2000 }).catch(() => false)
|
|
324
|
+
if (hasNoFiles) {
|
|
325
|
+
console.log('No files found - skipping delete button test')
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
throw new Error('Neither files table nor "No files found" message visible')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check for delete buttons in actions column
|
|
332
|
+
const deleteButtons = table.locator('tbody tr td.actions button.btn-danger')
|
|
333
|
+
const buttonCount = await deleteButtons.count()
|
|
334
|
+
console.log(`Found ${buttonCount} delete buttons in files table`)
|
|
335
|
+
|
|
336
|
+
expect(buttonCount).toBeGreaterThan(0)
|
|
337
|
+
|
|
338
|
+
// Verify first delete button has correct text
|
|
339
|
+
const firstDeleteButton = deleteButtons.first()
|
|
340
|
+
await expect(firstDeleteButton).toHaveText('Delete')
|
|
341
|
+
await expect(firstDeleteButton).toBeEnabled()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('files delete button shows confirmation dialog', async ({ page }) => {
|
|
345
|
+
skipIfNoProxy()
|
|
346
|
+
skipIfNoCredentials()
|
|
347
|
+
|
|
348
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
349
|
+
await page.click('[data-testid="nav-files"]')
|
|
350
|
+
await page.waitForURL('/files')
|
|
351
|
+
|
|
352
|
+
await page.waitForFunction(
|
|
353
|
+
() => !document.querySelector('.files .loading'),
|
|
354
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
355
|
+
).catch(() => {})
|
|
356
|
+
|
|
357
|
+
const table = page.locator('.files table')
|
|
358
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
359
|
+
|
|
360
|
+
if (!hasTable) {
|
|
361
|
+
console.log('No files table visible - skipping confirmation dialog test')
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const deleteButtons = table.locator('tbody tr td.actions button.btn-danger')
|
|
366
|
+
const buttonCount = await deleteButtons.count()
|
|
367
|
+
|
|
368
|
+
if (buttonCount === 0) {
|
|
369
|
+
console.log('No delete buttons found - skipping confirmation dialog test')
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Set up dialog handler to capture and dismiss
|
|
374
|
+
let dialogMessage = ''
|
|
375
|
+
page.on('dialog', async dialog => {
|
|
376
|
+
dialogMessage = dialog.message()
|
|
377
|
+
console.log('Dialog message:', dialogMessage)
|
|
378
|
+
await dialog.dismiss() // Cancel the delete
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// Click the first delete button
|
|
382
|
+
await deleteButtons.first().click()
|
|
383
|
+
|
|
384
|
+
// Wait a moment for dialog
|
|
385
|
+
await page.waitForTimeout(500)
|
|
386
|
+
|
|
387
|
+
// Verify dialog was shown with confirmation message
|
|
388
|
+
expect(dialogMessage).toContain('Are you sure')
|
|
389
|
+
expect(dialogMessage).toContain('delete')
|
|
390
|
+
expect(dialogMessage).toContain('recycle bin')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('canceling files delete dialog does not remove the file', async ({ page }) => {
|
|
394
|
+
skipIfNoProxy()
|
|
395
|
+
skipIfNoCredentials()
|
|
396
|
+
|
|
397
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
398
|
+
await page.click('[data-testid="nav-files"]')
|
|
399
|
+
await page.waitForURL('/files')
|
|
400
|
+
|
|
401
|
+
await page.waitForFunction(
|
|
402
|
+
() => !document.querySelector('.files .loading'),
|
|
403
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
404
|
+
).catch(() => {})
|
|
405
|
+
|
|
406
|
+
const table = page.locator('.files table')
|
|
407
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
408
|
+
|
|
409
|
+
if (!hasTable) {
|
|
410
|
+
console.log('No files table visible - skipping cancel test')
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Count rows before
|
|
415
|
+
const rows = table.locator('tbody tr')
|
|
416
|
+
const rowCountBefore = await rows.count()
|
|
417
|
+
console.log(`Files before cancel: ${rowCountBefore}`)
|
|
418
|
+
|
|
419
|
+
if (rowCountBefore === 0) {
|
|
420
|
+
console.log('No files found - skipping cancel test')
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Get the first file's name for verification
|
|
425
|
+
const firstFileName = await rows.first().locator('td').first().textContent()
|
|
426
|
+
|
|
427
|
+
// Set up dialog handler to dismiss (cancel)
|
|
428
|
+
page.on('dialog', async dialog => {
|
|
429
|
+
await dialog.dismiss()
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// Click delete button
|
|
433
|
+
const deleteButton = table.locator('tbody tr td.actions button.btn-danger').first()
|
|
434
|
+
await deleteButton.click()
|
|
435
|
+
|
|
436
|
+
// Wait a moment
|
|
437
|
+
await page.waitForTimeout(500)
|
|
438
|
+
|
|
439
|
+
// Count rows after - should be the same
|
|
440
|
+
const rowCountAfter = await rows.count()
|
|
441
|
+
console.log(`Files after cancel: ${rowCountAfter}`)
|
|
442
|
+
|
|
443
|
+
expect(rowCountAfter).toBe(rowCountBefore)
|
|
444
|
+
|
|
445
|
+
// Verify first file is still there
|
|
446
|
+
const firstFileNameAfter = await rows.first().locator('td').first().textContent()
|
|
447
|
+
expect(firstFileNameAfter).toBe(firstFileName)
|
|
448
|
+
console.log('File was NOT deleted after canceling dialog')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test('files table uses at least 80% of viewport width', async ({ page }) => {
|
|
452
|
+
skipIfNoProxy()
|
|
453
|
+
skipIfNoCredentials()
|
|
454
|
+
|
|
455
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
456
|
+
await page.click('[data-testid="nav-files"]')
|
|
457
|
+
await page.waitForURL('/files')
|
|
458
|
+
|
|
459
|
+
await page.waitForFunction(
|
|
460
|
+
() => !document.querySelector('.files .loading'),
|
|
461
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
462
|
+
).catch(() => {})
|
|
463
|
+
|
|
464
|
+
// Get the files container width
|
|
465
|
+
const filesContainer = page.locator('.files')
|
|
466
|
+
const isVisible = await filesContainer.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
467
|
+
|
|
468
|
+
if (!isVisible) {
|
|
469
|
+
console.log('Files container not visible - skipping width test')
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const viewportSize = page.viewportSize()
|
|
474
|
+
if (!viewportSize) {
|
|
475
|
+
console.log('Could not get viewport size - skipping width test')
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const containerBox = await filesContainer.boundingBox()
|
|
480
|
+
if (!containerBox) {
|
|
481
|
+
console.log('Could not get container bounding box - skipping width test')
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const widthPercentage = (containerBox.width / viewportSize.width) * 100
|
|
486
|
+
console.log(`Files container width: ${containerBox.width}px (${widthPercentage.toFixed(1)}% of viewport)`)
|
|
487
|
+
|
|
488
|
+
// Should be at least 80% of viewport width
|
|
489
|
+
expect(widthPercentage).toBeGreaterThanOrEqual(80)
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
test.describe('Actions Column Layout', () => {
|
|
494
|
+
test('jobs actions column has view and delete buttons side by side', async ({ page }) => {
|
|
495
|
+
skipIfNoProxy()
|
|
496
|
+
skipIfNoCredentials()
|
|
497
|
+
|
|
498
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
499
|
+
await page.click('[data-testid="nav-jobs"]')
|
|
500
|
+
await page.waitForURL('/jobs')
|
|
501
|
+
|
|
502
|
+
await page.waitForFunction(
|
|
503
|
+
() => !document.querySelector('.jobs .loading'),
|
|
504
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
505
|
+
).catch(() => {})
|
|
506
|
+
|
|
507
|
+
const table = page.locator('.jobs table')
|
|
508
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
509
|
+
|
|
510
|
+
if (!hasTable) {
|
|
511
|
+
console.log('No jobs table visible - skipping layout test')
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Check first row's actions cell
|
|
516
|
+
const actionsCell = table.locator('tbody tr').first().locator('td.actions')
|
|
517
|
+
const buttons = actionsCell.locator('button')
|
|
518
|
+
const buttonCount = await buttons.count()
|
|
519
|
+
|
|
520
|
+
expect(buttonCount).toBe(2) // View and Delete
|
|
521
|
+
|
|
522
|
+
// Verify button labels
|
|
523
|
+
await expect(buttons.nth(0)).toHaveText('View')
|
|
524
|
+
await expect(buttons.nth(1)).toHaveText('Delete')
|
|
525
|
+
|
|
526
|
+
console.log('Jobs actions column has View and Delete buttons')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
test('files actions column has download and delete buttons side by side', async ({ page }) => {
|
|
530
|
+
skipIfNoProxy()
|
|
531
|
+
skipIfNoCredentials()
|
|
532
|
+
|
|
533
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
534
|
+
await page.click('[data-testid="nav-files"]')
|
|
535
|
+
await page.waitForURL('/files')
|
|
536
|
+
|
|
537
|
+
await page.waitForFunction(
|
|
538
|
+
() => !document.querySelector('.files .loading'),
|
|
539
|
+
{ timeout: TIMEOUTS.ELEMENT_VISIBLE }
|
|
540
|
+
).catch(() => {})
|
|
541
|
+
|
|
542
|
+
const table = page.locator('.files table')
|
|
543
|
+
const hasTable = await table.isVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE }).catch(() => false)
|
|
544
|
+
|
|
545
|
+
if (!hasTable) {
|
|
546
|
+
console.log('No files table visible - skipping layout test')
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Check first row's actions cell
|
|
551
|
+
const actionsCell = table.locator('tbody tr').first().locator('td.actions')
|
|
552
|
+
const buttons = actionsCell.locator('button')
|
|
553
|
+
const buttonCount = await buttons.count()
|
|
554
|
+
|
|
555
|
+
expect(buttonCount).toBe(2) // Download and Delete
|
|
556
|
+
|
|
557
|
+
// Verify button labels
|
|
558
|
+
await expect(buttons.nth(0)).toHaveText('Download')
|
|
559
|
+
await expect(buttons.nth(1)).toHaveText('Delete')
|
|
560
|
+
|
|
561
|
+
console.log('Files actions column has Download and Delete buttons')
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
})
|