or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

attributes.mdcontext-system.mdcss-styling.mddata-binding.mddependency-injection.mdhtml-templates.mdindex.mdobservable-system.mdssr-hydration.mdstate-management.mdtemplate-directives.mdtesting-utilities.mdutilities.mdweb-components.md

testing-utilities.mddocs/

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

```