0
# Testing Utilities
1
2
Testing utilities for creating component fixtures, managing async operations, and integration testing with comprehensive support for FAST Element components.
3
4
## Capabilities
5
6
### Test Fixtures
7
8
Utilities for creating isolated test environments for components with proper setup and cleanup.
9
10
```typescript { .api }
11
/**
12
* Creates a test fixture for a component or template
13
* @param nameOrMarkupOrTemplate - Component name, HTML markup, or ViewTemplate
14
* @param options - Fixture configuration options
15
* @returns Promise resolving to a fixture instance
16
*/
17
function fixture<T extends HTMLElement = HTMLElement>(
18
templateNameOrType: ViewTemplate | string | Constructable<T>,
19
options?: FixtureOptions
20
): Promise<Fixture<T>>;
21
22
/**
23
* Configuration options for test fixtures
24
*/
25
interface FixtureOptions {
26
/** Document to create the fixture in */
27
document?: Document;
28
29
/** Parent element to attach the fixture to */
30
parent?: HTMLElement;
31
32
/** Data source to bind the HTML to */
33
source?: any;
34
}
35
36
/**
37
* Test fixture interface providing utilities for component testing
38
*/
39
interface Fixture<T extends HTMLElement = HTMLElement> {
40
/** The document containing the fixture */
41
document: Document;
42
43
/** The template used to create the fixture */
44
template: ViewTemplate;
45
46
/** The view created from the template */
47
view: HTMLView;
48
49
/** The parent element containing the fixture */
50
parent: HTMLElement;
51
52
/** The first element in the view */
53
element: T;
54
55
/**
56
* Connects the fixture to the DOM and waits for component initialization
57
*/
58
connect(): Promise<void>;
59
60
/**
61
* Disconnects the fixture from the DOM and cleans up resources
62
*/
63
disconnect(): Promise<void>;
64
}
65
```
66
67
**Usage Examples:**
68
69
```typescript
70
import { html, FASTElement, customElement, attr } from "@microsoft/fast-element";
71
import { fixture } from "@microsoft/fast-element/testing.js";
72
73
// Component for testing
74
@customElement("test-button")
75
export class TestButton extends FASTElement {
76
@attr disabled: boolean = false;
77
@attr label: string = "Click me";
78
@attr variant: "primary" | "secondary" = "primary";
79
80
clickCount: number = 0;
81
82
handleClick() {
83
if (!this.disabled) {
84
this.clickCount++;
85
this.$emit("button-clicked", { count: this.clickCount });
86
}
87
}
88
89
static template = html<TestButton>`
90
<button
91
class="btn ${x => x.variant}"
92
?disabled="${x => x.disabled}"
93
@click="${x => x.handleClick()}">
94
${x => x.label}
95
</button>
96
`;
97
}
98
99
// Basic fixture tests
100
describe("TestButton Component", () => {
101
let element: TestButton;
102
let fixtureInstance: Fixture<TestButton>;
103
104
beforeEach(async () => {
105
// Create fixture from element name
106
fixtureInstance = await fixture<TestButton>("test-button");
107
element = fixtureInstance.element;
108
});
109
110
afterEach(async () => {
111
// Clean up fixture
112
await fixtureInstance.disconnect();
113
});
114
115
it("should create component with default properties", () => {
116
expect(element.disabled).toBe(false);
117
expect(element.label).toBe("Click me");
118
expect(element.variant).toBe("primary");
119
expect(element.clickCount).toBe(0);
120
});
121
122
it("should render button with correct attributes", async () => {
123
await fixtureInstance.connect();
124
125
const button = element.shadowRoot?.querySelector("button");
126
expect(button).toBeTruthy();
127
expect(button?.textContent?.trim()).toBe("Click me");
128
expect(button?.className).toContain("primary");
129
expect(button?.disabled).toBe(false);
130
});
131
132
it("should handle click events", async () => {
133
await fixtureInstance.connect();
134
135
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
136
let clickEventData: any = null;
137
138
element.addEventListener("button-clicked", (e: any) => {
139
clickEventData = e.detail;
140
});
141
142
// Simulate click
143
button.click();
144
await fixtureInstance.detectChanges();
145
146
expect(element.clickCount).toBe(1);
147
expect(clickEventData).toEqual({ count: 1 });
148
});
149
150
it("should not handle clicks when disabled", async () => {
151
element.disabled = true;
152
await fixtureInstance.connect();
153
154
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
155
156
button.click();
157
await fixtureInstance.detectChanges();
158
159
expect(element.clickCount).toBe(0);
160
});
161
});
162
163
// Fixture with custom options
164
describe("TestButton with Custom Options", () => {
165
it("should create fixture with custom attributes", async () => {
166
const fixtureInstance = await fixture<TestButton>("test-button", {
167
attributes: {
168
label: "Custom Label",
169
variant: "secondary",
170
disabled: "true"
171
}
172
});
173
174
const element = fixtureInstance.element;
175
176
expect(element.label).toBe("Custom Label");
177
expect(element.variant).toBe("secondary");
178
expect(element.disabled).toBe(true);
179
180
await fixtureInstance.disconnect();
181
});
182
183
it("should create fixture with custom properties", async () => {
184
const fixtureInstance = await fixture<TestButton>("test-button", {
185
properties: {
186
label: "Property Label",
187
clickCount: 5
188
}
189
});
190
191
const element = fixtureInstance.element;
192
193
expect(element.label).toBe("Property Label");
194
expect(element.clickCount).toBe(5);
195
196
await fixtureInstance.disconnect();
197
});
198
});
199
200
// Template-based fixtures
201
describe("Template Fixtures", () => {
202
it("should create fixture from template", async () => {
203
const template = html`
204
<div class="test-container">
205
<test-button label="Template Button"></test-button>
206
<p>Additional content</p>
207
</div>
208
`;
209
210
const fixtureInstance = await fixture(template);
211
const container = fixtureInstance.element as HTMLDivElement;
212
213
expect(container.className).toBe("test-container");
214
215
const button = container.querySelector("test-button") as TestButton;
216
expect(button).toBeTruthy();
217
expect(button.label).toBe("Template Button");
218
219
await fixtureInstance.disconnect();
220
});
221
222
it("should create fixture with complex template", async () => {
223
const items = ["Item 1", "Item 2", "Item 3"];
224
225
const template = html`
226
<div class="test-list">
227
${items.map(item => html`
228
<test-button label="${item}"></test-button>
229
`)}
230
</div>
231
`;
232
233
const fixtureInstance = await fixture(template);
234
const container = fixtureInstance.element as HTMLDivElement;
235
236
const buttons = container.querySelectorAll("test-button");
237
expect(buttons.length).toBe(3);
238
239
buttons.forEach((button, index) => {
240
expect((button as TestButton).label).toBe(items[index]);
241
});
242
243
await fixtureInstance.disconnect();
244
});
245
});
246
247
// Form testing example
248
@customElement("test-form")
249
export class TestForm extends FASTElement {
250
@attr name: string = "";
251
@attr email: string = "";
252
@attr message: string = "";
253
254
errors: Record<string, string> = {};
255
256
validate(): boolean {
257
this.errors = {};
258
259
if (!this.name.trim()) {
260
this.errors.name = "Name is required";
261
}
262
263
if (!this.email.includes("@")) {
264
this.errors.email = "Valid email is required";
265
}
266
267
if (this.message.length < 10) {
268
this.errors.message = "Message must be at least 10 characters";
269
}
270
271
return Object.keys(this.errors).length === 0;
272
}
273
274
submit() {
275
if (this.validate()) {
276
this.$emit("form-submitted", {
277
name: this.name,
278
email: this.email,
279
message: this.message
280
});
281
}
282
}
283
284
static template = html<TestForm>`
285
<form @submit="${x => x.submit()}">
286
<div class="field">
287
<input type="text"
288
.value="${x => x.name}"
289
@input="${(x, e) => x.name = (e.target as HTMLInputElement).value}"
290
placeholder="Name">
291
<div class="error">${x => x.errors.name || ''}</div>
292
</div>
293
294
<div class="field">
295
<input type="email"
296
.value="${x => x.email}"
297
@input="${(x, e) => x.email = (e.target as HTMLInputElement).value}"
298
placeholder="Email">
299
<div class="error">${x => x.errors.email || ''}</div>
300
</div>
301
302
<div class="field">
303
<textarea .value="${x => x.message}"
304
@input="${(x, e) => x.message = (e.target as HTMLTextAreaElement).value}"
305
placeholder="Message"></textarea>
306
<div class="error">${x => x.errors.message || ''}</div>
307
</div>
308
309
<button type="submit">Submit</button>
310
</form>
311
`;
312
}
313
314
describe("TestForm Component", () => {
315
let element: TestForm;
316
let fixtureInstance: Fixture<TestForm>;
317
318
beforeEach(async () => {
319
fixtureInstance = await fixture<TestForm>("test-form");
320
element = fixtureInstance.element;
321
await fixtureInstance.connect();
322
});
323
324
afterEach(async () => {
325
await fixtureInstance.disconnect();
326
});
327
328
it("should validate form fields", async () => {
329
// Test empty form
330
const isValid = element.validate();
331
expect(isValid).toBe(false);
332
expect(element.errors.name).toBe("Name is required");
333
expect(element.errors.email).toBe("Valid email is required");
334
expect(element.errors.message).toBe("Message must be at least 10 characters");
335
336
// Fill form with valid data
337
element.name = "John Doe";
338
element.email = "john@example.com";
339
element.message = "This is a test message";
340
341
const isValidNow = element.validate();
342
expect(isValidNow).toBe(true);
343
expect(Object.keys(element.errors).length).toBe(0);
344
});
345
346
it("should emit form submission event", async () => {
347
let submittedData: any = null;
348
349
element.addEventListener("form-submitted", (e: any) => {
350
submittedData = e.detail;
351
});
352
353
// Fill form
354
element.name = "Jane Doe";
355
element.email = "jane@example.com";
356
element.message = "Test submission message";
357
358
// Submit form
359
element.submit();
360
361
expect(submittedData).toEqual({
362
name: "Jane Doe",
363
email: "jane@example.com",
364
message: "Test submission message"
365
});
366
});
367
368
it("should update input values", async () => {
369
const nameInput = element.shadowRoot?.querySelector('input[type="text"]') as HTMLInputElement;
370
const emailInput = element.shadowRoot?.querySelector('input[type="email"]') as HTMLInputElement;
371
const messageTextarea = element.shadowRoot?.querySelector('textarea') as HTMLTextAreaElement;
372
373
// Simulate user input
374
nameInput.value = "Test Name";
375
nameInput.dispatchEvent(new Event("input"));
376
377
emailInput.value = "test@example.com";
378
emailInput.dispatchEvent(new Event("input"));
379
380
messageTextarea.value = "Test message content";
381
messageTextarea.dispatchEvent(new Event("input"));
382
383
await fixtureInstance.detectChanges();
384
385
expect(element.name).toBe("Test Name");
386
expect(element.email).toBe("test@example.com");
387
expect(element.message).toBe("Test message content");
388
});
389
});
390
```
391
392
### Async Testing Utilities
393
394
Utilities for handling asynchronous operations in tests, including timeouts and waiting for updates.
395
396
```typescript { .api }
397
/**
398
* Creates a timeout promise for testing async operations
399
* @param duration - Duration in milliseconds
400
* @returns Promise that resolves after the specified duration
401
*/
402
function timeout(duration: number): Promise<void>;
403
404
/**
405
* Waits for a condition to become true
406
* @param condition - Function that returns true when condition is met
407
* @param timeoutMs - Maximum time to wait in milliseconds
408
* @param intervalMs - Check interval in milliseconds
409
* @returns Promise that resolves when condition is true
410
*/
411
function waitFor(
412
condition: () => boolean | Promise<boolean>,
413
timeoutMs?: number,
414
intervalMs?: number
415
): Promise<void>;
416
417
/**
418
* Waits for the next animation frame
419
* @returns Promise that resolves on next animation frame
420
*/
421
function nextFrame(): Promise<void>;
422
423
/**
424
* Waits for multiple animation frames
425
* @param count - Number of frames to wait
426
* @returns Promise that resolves after specified frames
427
*/
428
function nextFrames(count: number): Promise<void>;
429
430
/**
431
* Waits for the next microtask
432
* @returns Promise that resolves on next microtask
433
*/
434
function nextMicrotask(): Promise<void>;
435
```
436
437
**Usage Examples:**
438
439
```typescript
440
import {
441
fixture,
442
timeout,
443
waitFor,
444
nextFrame,
445
nextFrames,
446
nextMicrotask,
447
FASTElement,
448
customElement,
449
attr,
450
html
451
} from "@microsoft/fast-element";
452
453
// Component with async operations
454
@customElement("async-component")
455
export class AsyncComponent extends FASTElement {
456
@attr loading: boolean = false;
457
@attr data: string = "";
458
@attr error: string = "";
459
460
async loadData(): Promise<void> {
461
this.loading = true;
462
this.error = "";
463
464
try {
465
// Simulate async operation
466
await this.fetchData();
467
this.data = "Loaded data";
468
} catch (err) {
469
this.error = "Failed to load data";
470
} finally {
471
this.loading = false;
472
}
473
}
474
475
private async fetchData(): Promise<void> {
476
// Simulate network request
477
return new Promise((resolve, reject) => {
478
setTimeout(() => {
479
if (Math.random() > 0.8) {
480
reject(new Error("Network error"));
481
} else {
482
resolve();
483
}
484
}, 100);
485
});
486
}
487
488
static template = html<AsyncComponent>`
489
<div class="async-component">
490
<button @click="${x => x.loadData()}" ?disabled="${x => x.loading}">
491
${x => x.loading ? "Loading..." : "Load Data"}
492
</button>
493
494
<div class="content">
495
${x => x.data ? html`<p class="data">${x => x.data}</p>` : ''}
496
${x => x.error ? html`<p class="error">${x => x.error}</p>` : ''}
497
</div>
498
</div>
499
`;
500
}
501
502
describe("Async Testing", () => {
503
let element: AsyncComponent;
504
let fixtureInstance: Fixture<AsyncComponent>;
505
506
beforeEach(async () => {
507
fixtureInstance = await fixture<AsyncComponent>("async-component");
508
element = fixtureInstance.element;
509
await fixtureInstance.connect();
510
});
511
512
afterEach(async () => {
513
await fixtureInstance.disconnect();
514
});
515
516
it("should handle async data loading", async () => {
517
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
518
519
// Start loading
520
button.click();
521
await nextMicrotask(); // Wait for loading to start
522
523
expect(element.loading).toBe(true);
524
expect(button.textContent?.trim()).toBe("Loading...");
525
expect(button.disabled).toBe(true);
526
527
// Wait for loading to complete
528
await waitFor(() => !element.loading, 1000);
529
530
expect(element.loading).toBe(false);
531
expect(button.disabled).toBe(false);
532
expect(element.data).toBe("Loaded data");
533
534
// Wait for DOM update
535
await fixtureInstance.detectChanges();
536
537
const dataElement = element.shadowRoot?.querySelector(".data");
538
expect(dataElement?.textContent).toBe("Loaded data");
539
});
540
541
it("should handle loading timeout", async () => {
542
// Mock a slow operation
543
jest.spyOn(element, 'fetchData').mockImplementation(() =>
544
new Promise(resolve => setTimeout(resolve, 2000))
545
);
546
547
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
548
549
button.click();
550
await nextMicrotask();
551
552
expect(element.loading).toBe(true);
553
554
// Wait for shorter timeout than actual operation
555
await timeout(500);
556
557
// Should still be loading
558
expect(element.loading).toBe(true);
559
});
560
561
it("should handle multiple rapid clicks", async () => {
562
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
563
564
// Click multiple times rapidly
565
button.click();
566
button.click();
567
button.click();
568
569
await nextMicrotask();
570
571
// Should only process one request
572
expect(element.loading).toBe(true);
573
574
await waitFor(() => !element.loading, 1000);
575
576
expect(element.data).toBe("Loaded data");
577
});
578
});
579
580
// Animation testing
581
@customElement("animated-component")
582
export class AnimatedComponent extends FASTElement {
583
@attr expanded: boolean = false;
584
585
private animating: boolean = false;
586
587
async toggle(): Promise<void> {
588
if (this.animating) return;
589
590
this.animating = true;
591
this.expanded = !this.expanded;
592
593
// Wait for animation to complete
594
await timeout(300);
595
596
this.animating = false;
597
}
598
599
static template = html<AnimatedComponent>`
600
<div class="animated-component">
601
<button @click="${x => x.toggle()}" ?disabled="${x => x.animating}">
602
${x => x.expanded ? "Collapse" : "Expand"}
603
</button>
604
605
<div class="content ${x => x.expanded ? 'expanded' : 'collapsed'}"
606
style="transition: height 0.3s ease;">
607
<p>Animated content</p>
608
<p>More content here</p>
609
</div>
610
</div>
611
`;
612
}
613
614
describe("Animation Testing", () => {
615
let element: AnimatedComponent;
616
let fixtureInstance: Fixture<AnimatedComponent>;
617
618
beforeEach(async () => {
619
fixtureInstance = await fixture<AnimatedComponent>("animated-component");
620
element = fixtureInstance.element;
621
await fixtureInstance.connect();
622
});
623
624
afterEach(async () => {
625
await fixtureInstance.disconnect();
626
});
627
628
it("should handle animated transitions", async () => {
629
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
630
const content = element.shadowRoot?.querySelector(".content") as HTMLElement;
631
632
expect(element.expanded).toBe(false);
633
expect(content.className).toContain("collapsed");
634
635
// Start animation
636
button.click();
637
await nextFrame(); // Wait for first frame
638
639
expect(element.expanded).toBe(true);
640
expect(element.animating).toBe(true);
641
expect(button.disabled).toBe(true);
642
643
// Wait for animation to complete
644
await waitFor(() => !element.animating, 1000);
645
646
expect(element.animating).toBe(false);
647
expect(button.disabled).toBe(false);
648
expect(content.className).toContain("expanded");
649
});
650
651
it("should prevent multiple animations", async () => {
652
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
653
654
// Start first animation
655
button.click();
656
await nextFrame();
657
658
expect(element.animating).toBe(true);
659
660
// Try to start second animation
661
const initialExpanded = element.expanded;
662
button.click();
663
await nextFrame();
664
665
// Should not change state
666
expect(element.expanded).toBe(initialExpanded);
667
668
// Wait for first animation to complete
669
await waitFor(() => !element.animating, 1000);
670
});
671
672
it("should work with multiple frames", async () => {
673
const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
674
675
button.click();
676
677
// Wait for several animation frames
678
await nextFrames(5);
679
680
expect(element.expanded).toBe(true);
681
expect(element.animating).toBe(true);
682
});
683
});
684
685
// Performance testing utilities
686
describe("Performance Testing", () => {
687
it("should measure component creation time", async () => {
688
const startTime = performance.now();
689
690
const fixtureInstance = await fixture<TestButton>("test-button");
691
await fixtureInstance.connect();
692
693
const endTime = performance.now();
694
const creationTime = endTime - startTime;
695
696
console.log(`Component creation time: ${creationTime}ms`);
697
698
// Assert reasonable creation time
699
expect(creationTime).toBeLessThan(100);
700
701
await fixtureInstance.disconnect();
702
});
703
704
it("should measure update performance", async () => {
705
const fixtureInstance = await fixture<TestButton>("test-button");
706
const element = fixtureInstance.element;
707
await fixtureInstance.connect();
708
709
const updateCount = 100;
710
const startTime = performance.now();
711
712
for (let i = 0; i < updateCount; i++) {
713
element.label = `Update ${i}`;
714
await fixtureInstance.detectChanges();
715
}
716
717
const endTime = performance.now();
718
const totalTime = endTime - startTime;
719
const avgTime = totalTime / updateCount;
720
721
console.log(`Average update time: ${avgTime}ms`);
722
723
expect(avgTime).toBeLessThan(10);
724
725
await fixtureInstance.disconnect();
726
});
727
});
728
```
729
730
### Component Testing Helpers
731
732
Additional utilities for common component testing scenarios including event simulation and DOM queries.
733
734
```typescript { .api }
735
/**
736
* Generates a unique element name for testing
737
* @param prefix - Optional prefix for the element name
738
* @returns A unique element name
739
*/
740
function uniqueElementName(prefix?: string): string;
741
742
/**
743
* Creates a spy for element events
744
* @param element - The element to spy on
745
* @param eventName - The event name to spy on
746
* @returns Jest spy function
747
*/
748
function createEventSpy(element: Element, eventName: string): jest.SpyInstance;
749
750
/**
751
* Simulates user interactions on elements
752
*/
753
const UserInteraction: {
754
/**
755
* Simulates a click on an element
756
* @param element - The element to click
757
* @param options - Click options
758
*/
759
click(element: Element, options?: MouseEventInit): void;
760
761
/**
762
* Simulates keyboard input
763
* @param element - The element to type into
764
* @param text - The text to type
765
* @param options - Keyboard options
766
*/
767
type(element: HTMLInputElement | HTMLTextAreaElement, text: string, options?: KeyboardEventInit): Promise<void>;
768
769
/**
770
* Simulates focus on an element
771
* @param element - The element to focus
772
*/
773
focus(element: HTMLElement): void;
774
775
/**
776
* Simulates blur on an element
777
* @param element - The element to blur
778
*/
779
blur(element: HTMLElement): void;
780
781
/**
782
* Simulates hover over an element
783
* @param element - The element to hover over
784
*/
785
hover(element: Element): void;
786
};
787
788
/**
789
* DOM query utilities for testing
790
*/
791
const TestQuery: {
792
/**
793
* Queries within shadow DOM
794
* @param element - The element with shadow root
795
* @param selector - The CSS selector
796
*/
797
shadowQuery<T extends Element = Element>(element: Element, selector: string): T | null;
798
799
/**
800
* Queries all elements within shadow DOM
801
* @param element - The element with shadow root
802
* @param selector - The CSS selector
803
*/
804
shadowQueryAll<T extends Element = Element>(element: Element, selector: string): T[];
805
806
/**
807
* Gets text content including shadow DOM
808
* @param element - The element to get text from
809
*/
810
getTextContent(element: Element): string;
811
};
812
```
813
814
**Usage Examples:**
815
816
```typescript
817
import {
818
fixture,
819
uniqueElementName,
820
createEventSpy,
821
UserInteraction,
822
TestQuery,
823
FASTElement,
824
customElement,
825
html
826
} from "@microsoft/fast-element";
827
828
// Component for interaction testing
829
@customElement("interactive-component")
830
export class InteractiveComponent extends FASTElement {
831
value: string = "";
832
focused: boolean = false;
833
hovered: boolean = false;
834
clickCount: number = 0;
835
836
handleInput(event: Event) {
837
const target = event.target as HTMLInputElement;
838
this.value = target.value;
839
this.$emit("value-changed", { value: this.value });
840
}
841
842
handleFocus() {
843
this.focused = true;
844
this.$emit("focus");
845
}
846
847
handleBlur() {
848
this.focused = false;
849
this.$emit("blur");
850
}
851
852
handleMouseEnter() {
853
this.hovered = true;
854
}
855
856
handleMouseLeave() {
857
this.hovered = false;
858
}
859
860
handleClick() {
861
this.clickCount++;
862
this.$emit("clicked", { count: this.clickCount });
863
}
864
865
static template = html<InteractiveComponent>`
866
<div class="interactive-component ${x => x.hovered ? 'hovered' : ''}"
867
@mouseenter="${x => x.handleMouseEnter()}"
868
@mouseleave="${x => x.handleMouseLeave()}">
869
870
<input type="text"
871
.value="${x => x.value}"
872
@input="${x => x.handleInput}"
873
@focus="${x => x.handleFocus()}"
874
@blur="${x => x.handleBlur()}"
875
placeholder="Type something">
876
877
<button @click="${x => x.handleClick()}">
878
Click me (${x => x.clickCount})
879
</button>
880
881
<div class="status">
882
<p>Value: "${x => x.value}"</p>
883
<p>Focused: ${x => x.focused}</p>
884
<p>Hovered: ${x => x.hovered}</p>
885
</div>
886
</div>
887
`;
888
}
889
890
describe("Interactive Component Testing", () => {
891
let element: InteractiveComponent;
892
let fixtureInstance: Fixture<InteractiveComponent>;
893
894
beforeEach(async () => {
895
const elementName = uniqueElementName("interactive-component");
896
fixtureInstance = await fixture<InteractiveComponent>(elementName);
897
element = fixtureInstance.element;
898
await fixtureInstance.connect();
899
});
900
901
afterEach(async () => {
902
await fixtureInstance.disconnect();
903
});
904
905
it("should handle text input", async () => {
906
const input = TestQuery.shadowQuery<HTMLInputElement>(element, 'input[type="text"]');
907
expect(input).toBeTruthy();
908
909
const eventSpy = createEventSpy(element, "value-changed");
910
911
// Simulate typing
912
await UserInteraction.type(input!, "Hello World");
913
await fixtureInstance.detectChanges();
914
915
expect(element.value).toBe("Hello World");
916
expect(eventSpy).toHaveBeenCalledWith(
917
expect.objectContaining({
918
detail: { value: "Hello World" }
919
})
920
);
921
922
// Check display update
923
const statusText = TestQuery.getTextContent(element);
924
expect(statusText).toContain('Value: "Hello World"');
925
});
926
927
it("should handle focus and blur events", async () => {
928
const input = TestQuery.shadowQuery<HTMLInputElement>(element, 'input[type="text"]');
929
const focusSpy = createEventSpy(element, "focus");
930
const blurSpy = createEventSpy(element, "blur");
931
932
expect(element.focused).toBe(false);
933
934
// Focus input
935
UserInteraction.focus(input!);
936
await fixtureInstance.detectChanges();
937
938
expect(element.focused).toBe(true);
939
expect(focusSpy).toHaveBeenCalled();
940
941
// Blur input
942
UserInteraction.blur(input!);
943
await fixtureInstance.detectChanges();
944
945
expect(element.focused).toBe(false);
946
expect(blurSpy).toHaveBeenCalled();
947
});
948
949
it("should handle mouse interactions", async () => {
950
const container = TestQuery.shadowQuery(element, '.interactive-component');
951
952
expect(element.hovered).toBe(false);
953
expect(container?.className).not.toContain('hovered');
954
955
// Hover over component
956
UserInteraction.hover(container!);
957
await fixtureInstance.detectChanges();
958
959
expect(element.hovered).toBe(true);
960
expect(container?.className).toContain('hovered');
961
});
962
963
it("should handle button clicks", async () => {
964
const button = TestQuery.shadowQuery<HTMLButtonElement>(element, 'button');
965
const clickSpy = createEventSpy(element, "clicked");
966
967
expect(element.clickCount).toBe(0);
968
969
// Click button multiple times
970
UserInteraction.click(button!);
971
await fixtureInstance.detectChanges();
972
973
expect(element.clickCount).toBe(1);
974
expect(clickSpy).toHaveBeenCalledWith(
975
expect.objectContaining({
976
detail: { count: 1 }
977
})
978
);
979
980
UserInteraction.click(button!);
981
UserInteraction.click(button!);
982
await fixtureInstance.detectChanges();
983
984
expect(element.clickCount).toBe(3);
985
expect(button?.textContent?.trim()).toBe("Click me (3)");
986
});
987
988
it("should query shadow DOM elements", () => {
989
const input = TestQuery.shadowQuery(element, 'input[type="text"]');
990
const button = TestQuery.shadowQuery(element, 'button');
991
const statusParagraphs = TestQuery.shadowQueryAll(element, '.status p');
992
993
expect(input).toBeTruthy();
994
expect(button).toBeTruthy();
995
expect(statusParagraphs).toHaveLength(3);
996
997
// Test text content extraction
998
const fullText = TestQuery.getTextContent(element);
999
expect(fullText).toContain("Value:");
1000
expect(fullText).toContain("Focused:");
1001
expect(fullText).toContain("Hovered:");
1002
});
1003
});
1004
1005
// Integration testing example
1006
@customElement("parent-component")
1007
export class ParentComponent extends FASTElement {
1008
childData: string = "Initial data";
1009
1010
updateChildData() {
1011
this.childData = `Updated at ${new Date().toLocaleTimeString()}`;
1012
}
1013
1014
static template = html<ParentComponent>`
1015
<div class="parent-component">
1016
<button @click="${x => x.updateChildData()}">Update Child</button>
1017
<child-component data="${x => x.childData}"></child-component>
1018
</div>
1019
`;
1020
}
1021
1022
@customElement("child-component")
1023
export class ChildComponent extends FASTElement {
1024
@attr data: string = "";
1025
1026
processedData: string = "";
1027
1028
dataChanged() {
1029
this.processedData = `Processed: ${this.data}`;
1030
this.$emit("data-processed", { processed: this.processedData });
1031
}
1032
1033
static template = html<ChildComponent>`
1034
<div class="child-component">
1035
<p>Original: ${x => x.data}</p>
1036
<p>Processed: ${x => x.processedData}</p>
1037
</div>
1038
`;
1039
}
1040
1041
describe("Integration Testing", () => {
1042
it("should test parent-child component interaction", async () => {
1043
const parentFixture = await fixture<ParentComponent>("parent-component");
1044
const parent = parentFixture.element;
1045
await parentFixture.connect();
1046
1047
const child = TestQuery.shadowQuery<ChildComponent>(parent, 'child-component');
1048
expect(child).toBeTruthy();
1049
expect(child!.data).toBe("Initial data");
1050
1051
const childProcessSpy = createEventSpy(child!, "data-processed");
1052
1053
// Update parent data
1054
const updateButton = TestQuery.shadowQuery<HTMLButtonElement>(parent, 'button');
1055
UserInteraction.click(updateButton!);
1056
1057
await parentFixture.detectChanges();
1058
await waitFor(() => child!.data !== "Initial data", 1000);
1059
1060
expect(child!.data).toContain("Updated at");
1061
expect(childProcessSpy).toHaveBeenCalled();
1062
1063
const childText = TestQuery.getTextContent(child!);
1064
expect(childText).toContain("Processed:");
1065
1066
await parentFixture.disconnect();
1067
});
1068
});
1069
```
1070
1071
## Types
1072
1073
```typescript { .api }
1074
/**
1075
* Test fixture configuration
1076
*/
1077
interface FixtureOptions {
1078
/** Parent element for the fixture */
1079
parent?: HTMLElement;
1080
1081
/** Document to create elements in */
1082
document?: Document;
1083
1084
/** Whether to auto-connect the fixture */
1085
autoConnect?: boolean;
1086
1087
/** Custom attributes to set */
1088
attributes?: Record<string, string>;
1089
1090
/** Custom properties to set */
1091
properties?: Record<string, any>;
1092
1093
/** Operation timeout in milliseconds */
1094
timeout?: number;
1095
}
1096
1097
/**
1098
* Test fixture interface
1099
*/
1100
interface Fixture<T extends HTMLElement = HTMLElement> {
1101
/** The test document */
1102
readonly document: Document;
1103
1104
/** The template used */
1105
readonly template: ViewTemplate;
1106
1107
/** The fixture element */
1108
readonly element: T;
1109
1110
/** The parent container */
1111
readonly parent: HTMLElement;
1112
1113
/** Connect to DOM */
1114
connect(): Promise<void>;
1115
1116
/** Disconnect from DOM */
1117
disconnect(): Promise<void>;
1118
1119
/** Trigger change detection */
1120
detectChanges(): Promise<void>;
1121
1122
/** Wait for stability */
1123
whenStable(): Promise<void>;
1124
}
1125
1126
/**
1127
* User interaction event options
1128
*/
1129
interface UserInteractionOptions {
1130
/** Mouse event options */
1131
mouse?: MouseEventInit;
1132
1133
/** Keyboard event options */
1134
keyboard?: KeyboardEventInit;
1135
1136
/** Focus options */
1137
focus?: FocusOptions;
1138
}
1139
```