@api-client/ui 0.5.0 → 0.5.1
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/build/src/elements/highlight/MarkedHighlight.d.ts.map +1 -1
- package/build/src/elements/highlight/MarkedHighlight.js +2 -1
- package/build/src/elements/highlight/MarkedHighlight.js.map +1 -1
- package/build/src/md/button/internals/base.js +1 -1
- package/build/src/md/button/internals/base.js.map +1 -1
- package/build/src/md/dialog/internals/Dialog.d.ts +18 -0
- package/build/src/md/dialog/internals/Dialog.d.ts.map +1 -1
- package/build/src/md/dialog/internals/Dialog.js +60 -2
- package/build/src/md/dialog/internals/Dialog.js.map +1 -1
- package/demo/md/dialog/dialog.ts +135 -1
- package/package.json +2 -2
- package/src/elements/highlight/MarkedHighlight.ts +2 -1
- package/src/md/button/internals/base.ts +1 -1
- package/src/md/dialog/internals/Dialog.ts +54 -1
- package/test/README.md +372 -0
- package/test/dom-assertions.test.ts +182 -0
- package/test/helpers/TestUtils.ts +243 -0
- package/test/helpers/UiMock.ts +83 -13
- package/test/md/dialog/UiDialog.test.ts +169 -0
- package/test/setup.test.ts +217 -0
- package/test/setup.ts +117 -0
|
@@ -230,5 +230,174 @@ describe('md', () => {
|
|
|
230
230
|
assert.isTrue(container.classList.contains('with-buttons'), 'has the with-buttons class')
|
|
231
231
|
})
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
describe('form handling', () => {
|
|
235
|
+
async function formWrappedDialogFixture(): Promise<{ form: HTMLFormElement; dialog: UiDialog }> {
|
|
236
|
+
const container = await fixture(html`
|
|
237
|
+
<form>
|
|
238
|
+
<ui-dialog submitClose>
|
|
239
|
+
<span slot="title">Form Dialog</span>
|
|
240
|
+
<input type="text" name="username" required />
|
|
241
|
+
<ui-button color="text" slot="button" value="dismiss">Cancel</ui-button>
|
|
242
|
+
<ui-button color="text" slot="button" value="confirm" type="submit">Submit</ui-button>
|
|
243
|
+
</ui-dialog>
|
|
244
|
+
</form>
|
|
245
|
+
`)
|
|
246
|
+
const form = container as HTMLFormElement
|
|
247
|
+
const dialog = form.querySelector('ui-dialog') as UiDialog
|
|
248
|
+
return { form, dialog }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function formWithoutSubmitCloseFixture(): Promise<{ form: HTMLFormElement; dialog: UiDialog }> {
|
|
252
|
+
const container = await fixture(html`
|
|
253
|
+
<form>
|
|
254
|
+
<ui-dialog>
|
|
255
|
+
<span slot="title">Form Dialog</span>
|
|
256
|
+
<input type="text" name="username" required />
|
|
257
|
+
<ui-button color="text" slot="button" value="dismiss">Cancel</ui-button>
|
|
258
|
+
<ui-button color="text" slot="button" value="confirm" type="submit">Submit</ui-button>
|
|
259
|
+
</ui-dialog>
|
|
260
|
+
</form>
|
|
261
|
+
`)
|
|
262
|
+
const form = container as HTMLFormElement
|
|
263
|
+
const dialog = form.querySelector('ui-dialog') as UiDialog
|
|
264
|
+
return { form, dialog }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
describe('dialog wrapped in form', () => {
|
|
268
|
+
it('should detect parent form when connected', async () => {
|
|
269
|
+
const { dialog } = await formWrappedDialogFixture()
|
|
270
|
+
await dialog.updateComplete
|
|
271
|
+
|
|
272
|
+
// The form should be detected during connectedCallback
|
|
273
|
+
// We can't directly access private fields, so we test the behavior instead
|
|
274
|
+
assert.ok(dialog.submitClose, 'dialog should be configured for form handling')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should close dialog when form is submitted and submitClose is true', async () => {
|
|
278
|
+
const { form, dialog } = await formWrappedDialogFixture()
|
|
279
|
+
dialog.open = true
|
|
280
|
+
await dialog.updateComplete
|
|
281
|
+
|
|
282
|
+
const spy = sinon.spy()
|
|
283
|
+
dialog.addEventListener('close', spy)
|
|
284
|
+
|
|
285
|
+
// Simulate form submission
|
|
286
|
+
const submitEvent = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
287
|
+
form.dispatchEvent(submitEvent)
|
|
288
|
+
|
|
289
|
+
assert.isFalse(dialog.open, 'dialog should be closed')
|
|
290
|
+
assert.isTrue(spy.calledOnce, 'close event should be dispatched')
|
|
291
|
+
const event = spy.args[0][0] as CustomEvent
|
|
292
|
+
assert.isFalse(event.detail.cancelled, 'dialog should be confirmed, not cancelled')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('should not close dialog when form is submitted and submitClose is false', async () => {
|
|
296
|
+
const { form, dialog } = await formWithoutSubmitCloseFixture()
|
|
297
|
+
dialog.open = true
|
|
298
|
+
await dialog.updateComplete
|
|
299
|
+
|
|
300
|
+
const spy = sinon.spy()
|
|
301
|
+
dialog.addEventListener('close', spy)
|
|
302
|
+
|
|
303
|
+
// Simulate form submission
|
|
304
|
+
const submitEvent = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
305
|
+
form.dispatchEvent(submitEvent)
|
|
306
|
+
|
|
307
|
+
assert.isTrue(dialog.open, 'dialog should remain open')
|
|
308
|
+
assert.isFalse(spy.called, 'close event should not be dispatched')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should not handle form submit when submit button is clicked directly', async () => {
|
|
312
|
+
const { dialog } = await formWrappedDialogFixture()
|
|
313
|
+
dialog.open = true
|
|
314
|
+
await dialog.updateComplete
|
|
315
|
+
|
|
316
|
+
const submitButton = dialog.querySelector('ui-button[type="submit"]') as UiButton
|
|
317
|
+
const spy = sinon.spy()
|
|
318
|
+
dialog.addEventListener('close', spy)
|
|
319
|
+
|
|
320
|
+
// Click the submit button - this should not close the dialog immediately
|
|
321
|
+
// because we yield control to the form
|
|
322
|
+
submitButton.click()
|
|
323
|
+
|
|
324
|
+
// The dialog should still be open because the form hasn't been submitted yet
|
|
325
|
+
assert.isTrue(dialog.open, 'dialog should remain open when submit button is clicked')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should remove form event listener when disconnected', async () => {
|
|
329
|
+
const { form, dialog } = await formWrappedDialogFixture()
|
|
330
|
+
const removeEventListenerSpy = sinon.spy(form, 'removeEventListener')
|
|
331
|
+
|
|
332
|
+
dialog.remove()
|
|
333
|
+
|
|
334
|
+
// We can't test the private method directly, but we can verify the spy was called
|
|
335
|
+
// The actual method name is not accessible, so we test the behavior instead
|
|
336
|
+
assert.isTrue(removeEventListenerSpy.called, 'removeEventListener should be called on form')
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('edge cases', () => {
|
|
341
|
+
it('should handle form submission when dialog is not open', async () => {
|
|
342
|
+
const { form, dialog } = await formWrappedDialogFixture()
|
|
343
|
+
// Dialog is closed by default
|
|
344
|
+
assert.isFalse(dialog.open, 'dialog should be closed initially')
|
|
345
|
+
|
|
346
|
+
const spy = sinon.spy()
|
|
347
|
+
dialog.addEventListener('close', spy)
|
|
348
|
+
|
|
349
|
+
// Submit form when dialog is closed
|
|
350
|
+
const submitEvent = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
351
|
+
form.dispatchEvent(submitEvent)
|
|
352
|
+
|
|
353
|
+
// Should still close the dialog (set open to false even though it's already false)
|
|
354
|
+
assert.isFalse(dialog.open, 'dialog should remain closed')
|
|
355
|
+
assert.isTrue(spy.calledOnce, 'close event should still be dispatched')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('should handle multiple form submissions', async () => {
|
|
359
|
+
const { form, dialog } = await formWrappedDialogFixture()
|
|
360
|
+
dialog.open = true
|
|
361
|
+
await dialog.updateComplete
|
|
362
|
+
|
|
363
|
+
const spy = sinon.spy()
|
|
364
|
+
dialog.addEventListener('close', spy)
|
|
365
|
+
|
|
366
|
+
// Submit form multiple times
|
|
367
|
+
const submitEvent1 = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
368
|
+
const submitEvent2 = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
369
|
+
|
|
370
|
+
form.dispatchEvent(submitEvent1)
|
|
371
|
+
form.dispatchEvent(submitEvent2)
|
|
372
|
+
|
|
373
|
+
assert.equal(spy.callCount, 2, 'close event should be dispatched for each submission')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should maintain form reference across re-connections', async () => {
|
|
377
|
+
const { form, dialog } = await formWrappedDialogFixture()
|
|
378
|
+
|
|
379
|
+
// Remove and re-add dialog
|
|
380
|
+
const parent = form
|
|
381
|
+
dialog.remove()
|
|
382
|
+
await dialog.updateComplete
|
|
383
|
+
|
|
384
|
+
parent.appendChild(dialog)
|
|
385
|
+
await dialog.updateComplete
|
|
386
|
+
|
|
387
|
+
// Test the behavior instead of accessing private fields
|
|
388
|
+
// If form handling is working, submitClose should still work
|
|
389
|
+
dialog.open = true
|
|
390
|
+
await dialog.updateComplete
|
|
391
|
+
|
|
392
|
+
const spy = sinon.spy()
|
|
393
|
+
dialog.addEventListener('close', spy)
|
|
394
|
+
|
|
395
|
+
const submitEvent = new SubmitEvent('submit', { bubbles: true, cancelable: true })
|
|
396
|
+
form.dispatchEvent(submitEvent)
|
|
397
|
+
|
|
398
|
+
assert.isTrue(spy.called, 'form handling should work after reconnection')
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
})
|
|
233
402
|
})
|
|
234
403
|
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example test file demonstrating the test setup and utilities.
|
|
3
|
+
* This shows how to use the test infrastructure for UI components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { assert } from '@open-wc/testing'
|
|
7
|
+
import { testFactory, asyncHelpers, customMatchers } from './helpers/TestUtils.js'
|
|
8
|
+
import { UiMock } from './helpers/UiMock.js'
|
|
9
|
+
|
|
10
|
+
// Import the setup to ensure it runs
|
|
11
|
+
import './setup.js'
|
|
12
|
+
|
|
13
|
+
describe('Test Setup Example', () => {
|
|
14
|
+
describe('Basic Test Infrastructure', () => {
|
|
15
|
+
it('should have test utilities available globally', () => {
|
|
16
|
+
assert.exists(window.testUtils)
|
|
17
|
+
assert.isFunction(window.testUtils.waitForElement)
|
|
18
|
+
assert.isFunction(window.testUtils.waitForCondition)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should have custom matchers available', () => {
|
|
22
|
+
assert.exists(customMatchers.toHaveClasses)
|
|
23
|
+
assert.exists(customMatchers.toBeVisible)
|
|
24
|
+
assert.exists(customMatchers.toHaveAttribute)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should have async helpers available', () => {
|
|
28
|
+
assert.exists(asyncHelpers.waitForAll)
|
|
29
|
+
assert.exists(asyncHelpers.waitForAny)
|
|
30
|
+
assert.exists(asyncHelpers.retry)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should have UiMock utilities available', () => {
|
|
34
|
+
assert.exists(UiMock.keyDown)
|
|
35
|
+
assert.exists(UiMock.keyUp)
|
|
36
|
+
assert.exists(UiMock.keyPress)
|
|
37
|
+
assert.exists(UiMock.getMiddleOfElement)
|
|
38
|
+
assert.exists(UiMock.moveMouse)
|
|
39
|
+
assert.exists(UiMock.dragAndDrop)
|
|
40
|
+
assert.exists(UiMock.getFileDragEvent)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Element Testing Example', () => {
|
|
45
|
+
let element: HTMLElement
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
// Clean setup is handled by global setup
|
|
49
|
+
element = document.createElement('div')
|
|
50
|
+
element.id = 'test-element'
|
|
51
|
+
element.className = 'test-class'
|
|
52
|
+
element.setAttribute('data-test', 'value')
|
|
53
|
+
document.body.appendChild(element)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
// Cleanup is handled by global setup
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should test element classes using custom matcher', () => {
|
|
61
|
+
const result = customMatchers.toHaveClasses(element, 'test-class')
|
|
62
|
+
assert.isTrue(result.pass)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should test element visibility using custom matcher', () => {
|
|
66
|
+
const result = customMatchers.toBeVisible(element)
|
|
67
|
+
assert.isTrue(result.pass)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should test element attributes using custom matcher', () => {
|
|
71
|
+
const result = customMatchers.toHaveAttribute(element, 'data-test', 'value')
|
|
72
|
+
assert.isTrue(result.pass)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should wait for element to appear', async () => {
|
|
76
|
+
// Remove element temporarily
|
|
77
|
+
element.remove()
|
|
78
|
+
|
|
79
|
+
// Add it back after a delay
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
document.body.appendChild(element)
|
|
82
|
+
}, 10)
|
|
83
|
+
|
|
84
|
+
// Wait for it to appear
|
|
85
|
+
const foundElement = await window.testUtils.waitForElement('#test-element')
|
|
86
|
+
assert.dom.equal(foundElement, element)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should wait for condition to be true', async () => {
|
|
90
|
+
let conditionMet = false
|
|
91
|
+
|
|
92
|
+
// Set condition to true after delay
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
conditionMet = true
|
|
95
|
+
}, 100)
|
|
96
|
+
|
|
97
|
+
// Wait for condition
|
|
98
|
+
await window.testUtils.waitForCondition(() => conditionMet)
|
|
99
|
+
assert.isTrue(conditionMet)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('Async Helpers Example', () => {
|
|
104
|
+
it('should wait for all conditions', async () => {
|
|
105
|
+
let condition1 = false
|
|
106
|
+
let condition2 = false
|
|
107
|
+
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
condition1 = true
|
|
110
|
+
}, 50)
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
condition2 = true
|
|
113
|
+
}, 100)
|
|
114
|
+
|
|
115
|
+
await asyncHelpers.waitForAll([() => condition1, () => condition2])
|
|
116
|
+
|
|
117
|
+
assert.isTrue(condition1)
|
|
118
|
+
assert.isTrue(condition2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should wait for any condition', async () => {
|
|
122
|
+
let condition1 = false
|
|
123
|
+
let condition2 = false
|
|
124
|
+
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
condition1 = true
|
|
127
|
+
}, 50)
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
condition2 = true
|
|
130
|
+
}, 100)
|
|
131
|
+
|
|
132
|
+
const index = await asyncHelpers.waitForAny([() => condition1, () => condition2])
|
|
133
|
+
|
|
134
|
+
assert.equal(index, 0) // First condition should be met first
|
|
135
|
+
assert.isTrue(condition1)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should retry failed operations', async () => {
|
|
139
|
+
let attempts = 0
|
|
140
|
+
|
|
141
|
+
const operation = async () => {
|
|
142
|
+
attempts++
|
|
143
|
+
if (attempts < 3) {
|
|
144
|
+
throw new Error('Not ready yet')
|
|
145
|
+
}
|
|
146
|
+
return 'success'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = await asyncHelpers.retry(operation, 5, 10)
|
|
150
|
+
assert.equal(result, 'success')
|
|
151
|
+
assert.equal(attempts, 3)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('Test Factory Example', () => {
|
|
156
|
+
it('should create data factory', () => {
|
|
157
|
+
const userFactory = testFactory.createDataFactory({
|
|
158
|
+
id: 1,
|
|
159
|
+
name: 'Test User',
|
|
160
|
+
email: 'test@example.com',
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const user1 = userFactory()
|
|
164
|
+
assert.equal(user1.name, 'Test User')
|
|
165
|
+
|
|
166
|
+
const user2 = userFactory({ name: 'Custom User' })
|
|
167
|
+
assert.equal(user2.name, 'Custom User')
|
|
168
|
+
assert.equal(user2.email, 'test@example.com') // Keeps default
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('UI Interaction Example', () => {
|
|
173
|
+
let button: HTMLButtonElement
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
button = document.createElement('button')
|
|
177
|
+
button.textContent = 'Click me'
|
|
178
|
+
button.style.width = '100px'
|
|
179
|
+
button.style.height = '40px'
|
|
180
|
+
button.style.position = 'absolute'
|
|
181
|
+
button.style.top = '100px'
|
|
182
|
+
button.style.left = '100px'
|
|
183
|
+
document.body.appendChild(button)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should simulate keyboard interaction', async () => {
|
|
187
|
+
let keyPressed = false
|
|
188
|
+
|
|
189
|
+
button.addEventListener('keydown', (e) => {
|
|
190
|
+
if (e.code === 'Enter') {
|
|
191
|
+
keyPressed = true
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
await UiMock.keyPress(button, 'Enter')
|
|
196
|
+
assert.isTrue(keyPressed)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should get element center position', () => {
|
|
200
|
+
const center = UiMock.getMiddleOfElement(button)
|
|
201
|
+
assert.isNumber(center.x)
|
|
202
|
+
assert.isNumber(center.y)
|
|
203
|
+
assert.isAbove(center.x, 0)
|
|
204
|
+
assert.isAbove(center.y, 0)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should create file drag event', () => {
|
|
208
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' })
|
|
209
|
+
const dragEvent = UiMock.getFileDragEvent('drop', { file })
|
|
210
|
+
|
|
211
|
+
assert.equal(dragEvent.type, 'drop')
|
|
212
|
+
assert.exists(dragEvent.dataTransfer)
|
|
213
|
+
assert.equal(dragEvent.dataTransfer!.files.length, 1)
|
|
214
|
+
assert.equal(dragEvent.dataTransfer!.files[0].name, 'test.txt')
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|
package/test/setup.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test setup configuration for the UI component library.
|
|
3
|
+
* This file is imported by all test files to ensure consistent test environment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// // Global test setup
|
|
7
|
+
// before(() => {
|
|
8
|
+
// // Set up global test configuration
|
|
9
|
+
// // eslint-disable-next-line no-console
|
|
10
|
+
// console.log('Setting up test environment...')
|
|
11
|
+
// })
|
|
12
|
+
|
|
13
|
+
// beforeEach(() => {
|
|
14
|
+
// // Clean up DOM before each test
|
|
15
|
+
// document.body.innerHTML = ''
|
|
16
|
+
|
|
17
|
+
// // Reset any global state
|
|
18
|
+
// if (window.localStorage) {
|
|
19
|
+
// window.localStorage.clear()
|
|
20
|
+
// }
|
|
21
|
+
// if (window.sessionStorage) {
|
|
22
|
+
// window.sessionStorage.clear()
|
|
23
|
+
// }
|
|
24
|
+
// })
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
// Clean up after each test
|
|
28
|
+
// document.body.innerHTML = ''
|
|
29
|
+
|
|
30
|
+
// Clear any pending timers
|
|
31
|
+
const highestTimeoutId = window.setTimeout(() => {}, 0)
|
|
32
|
+
for (let i = 0; i < highestTimeoutId; i++) {
|
|
33
|
+
window.clearTimeout(i)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Clear any pending intervals
|
|
37
|
+
const highestIntervalId = window.setInterval(() => {}, 9999)
|
|
38
|
+
for (let i = 0; i < highestIntervalId; i++) {
|
|
39
|
+
window.clearInterval(i)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// after(() => {
|
|
44
|
+
// // eslint-disable-next-line no-console
|
|
45
|
+
// console.log('Test environment cleanup complete.')
|
|
46
|
+
// })
|
|
47
|
+
|
|
48
|
+
// Global test utilities
|
|
49
|
+
declare global {
|
|
50
|
+
interface Window {
|
|
51
|
+
testUtils: {
|
|
52
|
+
waitForElement: (selector: string, timeout?: number) => Promise<Element>
|
|
53
|
+
waitForCondition: (condition: () => boolean, timeout?: number) => Promise<void>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add test utilities to global scope
|
|
59
|
+
window.testUtils = {
|
|
60
|
+
/**
|
|
61
|
+
* Wait for an element to appear in the DOM
|
|
62
|
+
*/
|
|
63
|
+
async waitForElement(selector: string, timeout = 5000): Promise<Element> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const startTime = Date.now()
|
|
66
|
+
|
|
67
|
+
function checkElement() {
|
|
68
|
+
const element = document.querySelector(selector)
|
|
69
|
+
if (element) {
|
|
70
|
+
resolve(element)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Date.now() - startTime > timeout) {
|
|
75
|
+
reject(new Error(`Element with selector "${selector}" not found within ${timeout}ms`))
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setTimeout(checkElement, 50)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
checkElement()
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Wait for a condition to be true
|
|
88
|
+
*/
|
|
89
|
+
async waitForCondition(condition: () => boolean, timeout = 5000): Promise<void> {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const startTime = Date.now()
|
|
92
|
+
|
|
93
|
+
function checkCondition() {
|
|
94
|
+
try {
|
|
95
|
+
if (condition()) {
|
|
96
|
+
resolve()
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Condition threw an error, continue waiting
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Date.now() - startTime > timeout) {
|
|
104
|
+
reject(new Error(`Condition not met within ${timeout}ms`))
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setTimeout(checkCondition, 50)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
checkCondition()
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Export setup for explicit imports
|
|
117
|
+
export {}
|