CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-microsoft--fast-element

A library for constructing Web Components

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

testing-utilities.mddocs/

Testing Utilities

Testing utilities for creating component fixtures, managing async operations, and integration testing with comprehensive support for FAST Element components.

Capabilities

Test Fixtures

Utilities for creating isolated test environments for components with proper setup and cleanup.

/**
 * Creates a test fixture for a component or template
 * @param nameOrMarkupOrTemplate - Component name, HTML markup, or ViewTemplate
 * @param options - Fixture configuration options
 * @returns Promise resolving to a fixture instance
 */
function fixture<T extends HTMLElement = HTMLElement>(
  templateNameOrType: ViewTemplate | string | Constructable<T>,
  options?: FixtureOptions
): Promise<Fixture<T>>;

/**
 * Configuration options for test fixtures
 */
interface FixtureOptions {
  /** Document to create the fixture in */
  document?: Document;
  
  /** Parent element to attach the fixture to */
  parent?: HTMLElement;
  
  /** Data source to bind the HTML to */
  source?: any;
}

/**
 * Test fixture interface providing utilities for component testing
 */
interface Fixture<T extends HTMLElement = HTMLElement> {
  /** The document containing the fixture */
  document: Document;
  
  /** The template used to create the fixture */
  template: ViewTemplate;
  
  /** The view created from the template */
  view: HTMLView;
  
  /** The parent element containing the fixture */
  parent: HTMLElement;
  
  /** The first element in the view */
  element: T;
  
  /**
   * Connects the fixture to the DOM and waits for component initialization
   */
  connect(): Promise<void>;
  
  /**
   * Disconnects the fixture from the DOM and cleans up resources
   */
  disconnect(): Promise<void>;
}

Usage Examples:

import { html, FASTElement, customElement, attr } from "@microsoft/fast-element";
import { fixture } from "@microsoft/fast-element/testing.js";

// Component for testing
@customElement("test-button")
export class TestButton extends FASTElement {
  @attr disabled: boolean = false;
  @attr label: string = "Click me";
  @attr variant: "primary" | "secondary" = "primary";
  
  clickCount: number = 0;
  
  handleClick() {
    if (!this.disabled) {
      this.clickCount++;
      this.$emit("button-clicked", { count: this.clickCount });
    }
  }
  
  static template = html<TestButton>`
    <button 
      class="btn ${x => x.variant}"
      ?disabled="${x => x.disabled}"
      @click="${x => x.handleClick()}">
      ${x => x.label}
    </button>
  `;
}

// Basic fixture tests
describe("TestButton Component", () => {
  let element: TestButton;
  let fixtureInstance: Fixture<TestButton>;
  
  beforeEach(async () => {
    // Create fixture from element name
    fixtureInstance = await fixture<TestButton>("test-button");
    element = fixtureInstance.element;
  });
  
  afterEach(async () => {
    // Clean up fixture
    await fixtureInstance.disconnect();
  });
  
  it("should create component with default properties", () => {
    expect(element.disabled).toBe(false);
    expect(element.label).toBe("Click me");
    expect(element.variant).toBe("primary");
    expect(element.clickCount).toBe(0);
  });
  
  it("should render button with correct attributes", async () => {
    await fixtureInstance.connect();
    
    const button = element.shadowRoot?.querySelector("button");
    expect(button).toBeTruthy();
    expect(button?.textContent?.trim()).toBe("Click me");
    expect(button?.className).toContain("primary");
    expect(button?.disabled).toBe(false);
  });
  
  it("should handle click events", async () => {
    await fixtureInstance.connect();
    
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    let clickEventData: any = null;
    
    element.addEventListener("button-clicked", (e: any) => {
      clickEventData = e.detail;
    });
    
    // Simulate click
    button.click();
    await fixtureInstance.detectChanges();
    
    expect(element.clickCount).toBe(1);
    expect(clickEventData).toEqual({ count: 1 });
  });
  
  it("should not handle clicks when disabled", async () => {
    element.disabled = true;
    await fixtureInstance.connect();
    
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    button.click();
    await fixtureInstance.detectChanges();
    
    expect(element.clickCount).toBe(0);
  });
});

// Fixture with custom options
describe("TestButton with Custom Options", () => {
  it("should create fixture with custom attributes", async () => {
    const fixtureInstance = await fixture<TestButton>("test-button", {
      attributes: {
        label: "Custom Label",
        variant: "secondary",
        disabled: "true"
      }
    });
    
    const element = fixtureInstance.element;
    
    expect(element.label).toBe("Custom Label");
    expect(element.variant).toBe("secondary");
    expect(element.disabled).toBe(true);
    
    await fixtureInstance.disconnect();
  });
  
  it("should create fixture with custom properties", async () => {
    const fixtureInstance = await fixture<TestButton>("test-button", {
      properties: {
        label: "Property Label",
        clickCount: 5
      }
    });
    
    const element = fixtureInstance.element;
    
    expect(element.label).toBe("Property Label");
    expect(element.clickCount).toBe(5);
    
    await fixtureInstance.disconnect();
  });
});

// Template-based fixtures
describe("Template Fixtures", () => {
  it("should create fixture from template", async () => {
    const template = html`
      <div class="test-container">
        <test-button label="Template Button"></test-button>
        <p>Additional content</p>
      </div>
    `;
    
    const fixtureInstance = await fixture(template);
    const container = fixtureInstance.element as HTMLDivElement;
    
    expect(container.className).toBe("test-container");
    
    const button = container.querySelector("test-button") as TestButton;
    expect(button).toBeTruthy();
    expect(button.label).toBe("Template Button");
    
    await fixtureInstance.disconnect();
  });
  
  it("should create fixture with complex template", async () => {
    const items = ["Item 1", "Item 2", "Item 3"];
    
    const template = html`
      <div class="test-list">
        ${items.map(item => html`
          <test-button label="${item}"></test-button>
        `)}
      </div>
    `;
    
    const fixtureInstance = await fixture(template);
    const container = fixtureInstance.element as HTMLDivElement;
    
    const buttons = container.querySelectorAll("test-button");
    expect(buttons.length).toBe(3);
    
    buttons.forEach((button, index) => {
      expect((button as TestButton).label).toBe(items[index]);
    });
    
    await fixtureInstance.disconnect();
  });
});

// Form testing example
@customElement("test-form")
export class TestForm extends FASTElement {
  @attr name: string = "";
  @attr email: string = "";
  @attr message: string = "";
  
  errors: Record<string, string> = {};
  
  validate(): boolean {
    this.errors = {};
    
    if (!this.name.trim()) {
      this.errors.name = "Name is required";
    }
    
    if (!this.email.includes("@")) {
      this.errors.email = "Valid email is required";
    }
    
    if (this.message.length < 10) {
      this.errors.message = "Message must be at least 10 characters";
    }
    
    return Object.keys(this.errors).length === 0;
  }
  
  submit() {
    if (this.validate()) {
      this.$emit("form-submitted", {
        name: this.name,
        email: this.email,
        message: this.message
      });
    }
  }
  
  static template = html<TestForm>`
    <form @submit="${x => x.submit()}">
      <div class="field">
        <input type="text" 
               .value="${x => x.name}" 
               @input="${(x, e) => x.name = (e.target as HTMLInputElement).value}"
               placeholder="Name">
        <div class="error">${x => x.errors.name || ''}</div>
      </div>
      
      <div class="field">
        <input type="email" 
               .value="${x => x.email}"
               @input="${(x, e) => x.email = (e.target as HTMLInputElement).value}"
               placeholder="Email">
        <div class="error">${x => x.errors.email || ''}</div>
      </div>
      
      <div class="field">
        <textarea .value="${x => x.message}"
                  @input="${(x, e) => x.message = (e.target as HTMLTextAreaElement).value}"
                  placeholder="Message"></textarea>
        <div class="error">${x => x.errors.message || ''}</div>
      </div>
      
      <button type="submit">Submit</button>
    </form>
  `;
}

describe("TestForm Component", () => {
  let element: TestForm;
  let fixtureInstance: Fixture<TestForm>;
  
  beforeEach(async () => {
    fixtureInstance = await fixture<TestForm>("test-form");
    element = fixtureInstance.element;
    await fixtureInstance.connect();
  });
  
  afterEach(async () => {
    await fixtureInstance.disconnect();
  });
  
  it("should validate form fields", async () => {
    // Test empty form
    const isValid = element.validate();
    expect(isValid).toBe(false);
    expect(element.errors.name).toBe("Name is required");
    expect(element.errors.email).toBe("Valid email is required");
    expect(element.errors.message).toBe("Message must be at least 10 characters");
    
    // Fill form with valid data
    element.name = "John Doe";
    element.email = "john@example.com";
    element.message = "This is a test message";
    
    const isValidNow = element.validate();
    expect(isValidNow).toBe(true);
    expect(Object.keys(element.errors).length).toBe(0);
  });
  
  it("should emit form submission event", async () => {
    let submittedData: any = null;
    
    element.addEventListener("form-submitted", (e: any) => {
      submittedData = e.detail;
    });
    
    // Fill form
    element.name = "Jane Doe";
    element.email = "jane@example.com";
    element.message = "Test submission message";
    
    // Submit form
    element.submit();
    
    expect(submittedData).toEqual({
      name: "Jane Doe",
      email: "jane@example.com",
      message: "Test submission message"
    });
  });
  
  it("should update input values", async () => {
    const nameInput = element.shadowRoot?.querySelector('input[type="text"]') as HTMLInputElement;
    const emailInput = element.shadowRoot?.querySelector('input[type="email"]') as HTMLInputElement;
    const messageTextarea = element.shadowRoot?.querySelector('textarea') as HTMLTextAreaElement;
    
    // Simulate user input
    nameInput.value = "Test Name";
    nameInput.dispatchEvent(new Event("input"));
    
    emailInput.value = "test@example.com";
    emailInput.dispatchEvent(new Event("input"));
    
    messageTextarea.value = "Test message content";
    messageTextarea.dispatchEvent(new Event("input"));
    
    await fixtureInstance.detectChanges();
    
    expect(element.name).toBe("Test Name");
    expect(element.email).toBe("test@example.com");
    expect(element.message).toBe("Test message content");
  });
});

Async Testing Utilities

Utilities for handling asynchronous operations in tests, including timeouts and waiting for updates.

/**
 * Creates a timeout promise for testing async operations
 * @param duration - Duration in milliseconds
 * @returns Promise that resolves after the specified duration
 */
function timeout(duration: number): Promise<void>;

/**
 * Waits for a condition to become true
 * @param condition - Function that returns true when condition is met
 * @param timeoutMs - Maximum time to wait in milliseconds
 * @param intervalMs - Check interval in milliseconds
 * @returns Promise that resolves when condition is true
 */
function waitFor(
  condition: () => boolean | Promise<boolean>,
  timeoutMs?: number,
  intervalMs?: number
): Promise<void>;

/**
 * Waits for the next animation frame
 * @returns Promise that resolves on next animation frame
 */
function nextFrame(): Promise<void>;

/**
 * Waits for multiple animation frames
 * @param count - Number of frames to wait
 * @returns Promise that resolves after specified frames
 */
function nextFrames(count: number): Promise<void>;

/**
 * Waits for the next microtask
 * @returns Promise that resolves on next microtask
 */
function nextMicrotask(): Promise<void>;

Usage Examples:

import { 
  fixture, 
  timeout, 
  waitFor, 
  nextFrame, 
  nextFrames, 
  nextMicrotask,
  FASTElement,
  customElement,
  attr,
  html
} from "@microsoft/fast-element";

// Component with async operations
@customElement("async-component")
export class AsyncComponent extends FASTElement {
  @attr loading: boolean = false;
  @attr data: string = "";
  @attr error: string = "";
  
  async loadData(): Promise<void> {
    this.loading = true;
    this.error = "";
    
    try {
      // Simulate async operation
      await this.fetchData();
      this.data = "Loaded data";
    } catch (err) {
      this.error = "Failed to load data";
    } finally {
      this.loading = false;
    }
  }
  
  private async fetchData(): Promise<void> {
    // Simulate network request
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (Math.random() > 0.8) {
          reject(new Error("Network error"));
        } else {
          resolve();
        }
      }, 100);
    });
  }
  
  static template = html<AsyncComponent>`
    <div class="async-component">
      <button @click="${x => x.loadData()}" ?disabled="${x => x.loading}">
        ${x => x.loading ? "Loading..." : "Load Data"}
      </button>
      
      <div class="content">
        ${x => x.data ? html`<p class="data">${x => x.data}</p>` : ''}
        ${x => x.error ? html`<p class="error">${x => x.error}</p>` : ''}
      </div>
    </div>
  `;
}

describe("Async Testing", () => {
  let element: AsyncComponent;
  let fixtureInstance: Fixture<AsyncComponent>;
  
  beforeEach(async () => {
    fixtureInstance = await fixture<AsyncComponent>("async-component");
    element = fixtureInstance.element;
    await fixtureInstance.connect();
  });
  
  afterEach(async () => {
    await fixtureInstance.disconnect();
  });
  
  it("should handle async data loading", async () => {
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    // Start loading
    button.click();
    await nextMicrotask(); // Wait for loading to start
    
    expect(element.loading).toBe(true);
    expect(button.textContent?.trim()).toBe("Loading...");
    expect(button.disabled).toBe(true);
    
    // Wait for loading to complete
    await waitFor(() => !element.loading, 1000);
    
    expect(element.loading).toBe(false);
    expect(button.disabled).toBe(false);
    expect(element.data).toBe("Loaded data");
    
    // Wait for DOM update
    await fixtureInstance.detectChanges();
    
    const dataElement = element.shadowRoot?.querySelector(".data");
    expect(dataElement?.textContent).toBe("Loaded data");
  });
  
  it("should handle loading timeout", async () => {
    // Mock a slow operation
    jest.spyOn(element, 'fetchData').mockImplementation(() => 
      new Promise(resolve => setTimeout(resolve, 2000))
    );
    
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    button.click();
    await nextMicrotask();
    
    expect(element.loading).toBe(true);
    
    // Wait for shorter timeout than actual operation
    await timeout(500);
    
    // Should still be loading
    expect(element.loading).toBe(true);
  });
  
  it("should handle multiple rapid clicks", async () => {
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    // Click multiple times rapidly
    button.click();
    button.click();
    button.click();
    
    await nextMicrotask();
    
    // Should only process one request
    expect(element.loading).toBe(true);
    
    await waitFor(() => !element.loading, 1000);
    
    expect(element.data).toBe("Loaded data");
  });
});

// Animation testing
@customElement("animated-component")
export class AnimatedComponent extends FASTElement {
  @attr expanded: boolean = false;
  
  private animating: boolean = false;
  
  async toggle(): Promise<void> {
    if (this.animating) return;
    
    this.animating = true;
    this.expanded = !this.expanded;
    
    // Wait for animation to complete
    await timeout(300);
    
    this.animating = false;
  }
  
  static template = html<AnimatedComponent>`
    <div class="animated-component">
      <button @click="${x => x.toggle()}" ?disabled="${x => x.animating}">
        ${x => x.expanded ? "Collapse" : "Expand"}
      </button>
      
      <div class="content ${x => x.expanded ? 'expanded' : 'collapsed'}" 
           style="transition: height 0.3s ease;">
        <p>Animated content</p>
        <p>More content here</p>
      </div>
    </div>
  `;
}

describe("Animation Testing", () => {
  let element: AnimatedComponent;
  let fixtureInstance: Fixture<AnimatedComponent>;
  
  beforeEach(async () => {
    fixtureInstance = await fixture<AnimatedComponent>("animated-component");
    element = fixtureInstance.element;
    await fixtureInstance.connect();
  });
  
  afterEach(async () => {
    await fixtureInstance.disconnect();
  });
  
  it("should handle animated transitions", async () => {
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    const content = element.shadowRoot?.querySelector(".content") as HTMLElement;
    
    expect(element.expanded).toBe(false);
    expect(content.className).toContain("collapsed");
    
    // Start animation
    button.click();
    await nextFrame(); // Wait for first frame
    
    expect(element.expanded).toBe(true);
    expect(element.animating).toBe(true);
    expect(button.disabled).toBe(true);
    
    // Wait for animation to complete
    await waitFor(() => !element.animating, 1000);
    
    expect(element.animating).toBe(false);
    expect(button.disabled).toBe(false);
    expect(content.className).toContain("expanded");
  });
  
  it("should prevent multiple animations", async () => {
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    // Start first animation
    button.click();
    await nextFrame();
    
    expect(element.animating).toBe(true);
    
    // Try to start second animation
    const initialExpanded = element.expanded;
    button.click();
    await nextFrame();
    
    // Should not change state
    expect(element.expanded).toBe(initialExpanded);
    
    // Wait for first animation to complete
    await waitFor(() => !element.animating, 1000);
  });
  
  it("should work with multiple frames", async () => {
    const button = element.shadowRoot?.querySelector("button") as HTMLButtonElement;
    
    button.click();
    
    // Wait for several animation frames
    await nextFrames(5);
    
    expect(element.expanded).toBe(true);
    expect(element.animating).toBe(true);
  });
});

// Performance testing utilities
describe("Performance Testing", () => {
  it("should measure component creation time", async () => {
    const startTime = performance.now();
    
    const fixtureInstance = await fixture<TestButton>("test-button");
    await fixtureInstance.connect();
    
    const endTime = performance.now();
    const creationTime = endTime - startTime;
    
    console.log(`Component creation time: ${creationTime}ms`);
    
    // Assert reasonable creation time
    expect(creationTime).toBeLessThan(100);
    
    await fixtureInstance.disconnect();
  });
  
  it("should measure update performance", async () => {
    const fixtureInstance = await fixture<TestButton>("test-button");
    const element = fixtureInstance.element;
    await fixtureInstance.connect();
    
    const updateCount = 100;
    const startTime = performance.now();
    
    for (let i = 0; i < updateCount; i++) {
      element.label = `Update ${i}`;
      await fixtureInstance.detectChanges();
    }
    
    const endTime = performance.now();
    const totalTime = endTime - startTime;
    const avgTime = totalTime / updateCount;
    
    console.log(`Average update time: ${avgTime}ms`);
    
    expect(avgTime).toBeLessThan(10);
    
    await fixtureInstance.disconnect();
  });
});

Component Testing Helpers

Additional utilities for common component testing scenarios including event simulation and DOM queries.

/**
 * Generates a unique element name for testing
 * @param prefix - Optional prefix for the element name
 * @returns A unique element name
 */
function uniqueElementName(prefix?: string): string;

/**
 * Creates a spy for element events
 * @param element - The element to spy on
 * @param eventName - The event name to spy on
 * @returns Jest spy function
 */
function createEventSpy(element: Element, eventName: string): jest.SpyInstance;

/**
 * Simulates user interactions on elements
 */
const UserInteraction: {
  /**
   * Simulates a click on an element
   * @param element - The element to click
   * @param options - Click options
   */
  click(element: Element, options?: MouseEventInit): void;
  
  /**
   * Simulates keyboard input
   * @param element - The element to type into
   * @param text - The text to type
   * @param options - Keyboard options
   */
  type(element: HTMLInputElement | HTMLTextAreaElement, text: string, options?: KeyboardEventInit): Promise<void>;
  
  /**
   * Simulates focus on an element
   * @param element - The element to focus
   */
  focus(element: HTMLElement): void;
  
  /**
   * Simulates blur on an element
   * @param element - The element to blur
   */
  blur(element: HTMLElement): void;
  
  /**
   * Simulates hover over an element
   * @param element - The element to hover over
   */
  hover(element: Element): void;
};

/**
 * DOM query utilities for testing
 */
const TestQuery: {
  /**
   * Queries within shadow DOM
   * @param element - The element with shadow root
   * @param selector - The CSS selector
   */
  shadowQuery<T extends Element = Element>(element: Element, selector: string): T | null;
  
  /**
   * Queries all elements within shadow DOM
   * @param element - The element with shadow root
   * @param selector - The CSS selector
   */
  shadowQueryAll<T extends Element = Element>(element: Element, selector: string): T[];
  
  /**
   * Gets text content including shadow DOM
   * @param element - The element to get text from
   */
  getTextContent(element: Element): string;
};

Usage Examples:

import { 
  fixture,
  uniqueElementName,
  createEventSpy,
  UserInteraction,
  TestQuery,
  FASTElement,
  customElement,
  html
} from "@microsoft/fast-element";

// Component for interaction testing
@customElement("interactive-component")
export class InteractiveComponent extends FASTElement {
  value: string = "";
  focused: boolean = false;
  hovered: boolean = false;
  clickCount: number = 0;
  
  handleInput(event: Event) {
    const target = event.target as HTMLInputElement;
    this.value = target.value;
    this.$emit("value-changed", { value: this.value });
  }
  
  handleFocus() {
    this.focused = true;
    this.$emit("focus");
  }
  
  handleBlur() {
    this.focused = false;
    this.$emit("blur");
  }
  
  handleMouseEnter() {
    this.hovered = true;
  }
  
  handleMouseLeave() {
    this.hovered = false;
  }
  
  handleClick() {
    this.clickCount++;
    this.$emit("clicked", { count: this.clickCount });
  }
  
  static template = html<InteractiveComponent>`
    <div class="interactive-component ${x => x.hovered ? 'hovered' : ''}"
         @mouseenter="${x => x.handleMouseEnter()}"
         @mouseleave="${x => x.handleMouseLeave()}">
      
      <input type="text"
             .value="${x => x.value}"
             @input="${x => x.handleInput}"
             @focus="${x => x.handleFocus()}"
             @blur="${x => x.handleBlur()}"
             placeholder="Type something">
      
      <button @click="${x => x.handleClick()}">
        Click me (${x => x.clickCount})
      </button>
      
      <div class="status">
        <p>Value: "${x => x.value}"</p>
        <p>Focused: ${x => x.focused}</p>
        <p>Hovered: ${x => x.hovered}</p>
      </div>
    </div>
  `;
}

describe("Interactive Component Testing", () => {
  let element: InteractiveComponent;
  let fixtureInstance: Fixture<InteractiveComponent>;
  
  beforeEach(async () => {
    const elementName = uniqueElementName("interactive-component");
    fixtureInstance = await fixture<InteractiveComponent>(elementName);
    element = fixtureInstance.element;
    await fixtureInstance.connect();
  });
  
  afterEach(async () => {
    await fixtureInstance.disconnect();
  });
  
  it("should handle text input", async () => {
    const input = TestQuery.shadowQuery<HTMLInputElement>(element, 'input[type="text"]');
    expect(input).toBeTruthy();
    
    const eventSpy = createEventSpy(element, "value-changed");
    
    // Simulate typing
    await UserInteraction.type(input!, "Hello World");
    await fixtureInstance.detectChanges();
    
    expect(element.value).toBe("Hello World");
    expect(eventSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        detail: { value: "Hello World" }
      })
    );
    
    // Check display update
    const statusText = TestQuery.getTextContent(element);
    expect(statusText).toContain('Value: "Hello World"');
  });
  
  it("should handle focus and blur events", async () => {
    const input = TestQuery.shadowQuery<HTMLInputElement>(element, 'input[type="text"]');
    const focusSpy = createEventSpy(element, "focus");
    const blurSpy = createEventSpy(element, "blur");
    
    expect(element.focused).toBe(false);
    
    // Focus input
    UserInteraction.focus(input!);
    await fixtureInstance.detectChanges();
    
    expect(element.focused).toBe(true);
    expect(focusSpy).toHaveBeenCalled();
    
    // Blur input
    UserInteraction.blur(input!);
    await fixtureInstance.detectChanges();
    
    expect(element.focused).toBe(false);
    expect(blurSpy).toHaveBeenCalled();
  });
  
  it("should handle mouse interactions", async () => {
    const container = TestQuery.shadowQuery(element, '.interactive-component');
    
    expect(element.hovered).toBe(false);
    expect(container?.className).not.toContain('hovered');
    
    // Hover over component
    UserInteraction.hover(container!);
    await fixtureInstance.detectChanges();
    
    expect(element.hovered).toBe(true);
    expect(container?.className).toContain('hovered');
  });
  
  it("should handle button clicks", async () => {
    const button = TestQuery.shadowQuery<HTMLButtonElement>(element, 'button');
    const clickSpy = createEventSpy(element, "clicked");
    
    expect(element.clickCount).toBe(0);
    
    // Click button multiple times
    UserInteraction.click(button!);
    await fixtureInstance.detectChanges();
    
    expect(element.clickCount).toBe(1);
    expect(clickSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        detail: { count: 1 }
      })
    );
    
    UserInteraction.click(button!);
    UserInteraction.click(button!);
    await fixtureInstance.detectChanges();
    
    expect(element.clickCount).toBe(3);
    expect(button?.textContent?.trim()).toBe("Click me (3)");
  });
  
  it("should query shadow DOM elements", () => {
    const input = TestQuery.shadowQuery(element, 'input[type="text"]');
    const button = TestQuery.shadowQuery(element, 'button');
    const statusParagraphs = TestQuery.shadowQueryAll(element, '.status p');
    
    expect(input).toBeTruthy();
    expect(button).toBeTruthy();
    expect(statusParagraphs).toHaveLength(3);
    
    // Test text content extraction
    const fullText = TestQuery.getTextContent(element);
    expect(fullText).toContain("Value:");
    expect(fullText).toContain("Focused:");
    expect(fullText).toContain("Hovered:");
  });
});

// Integration testing example
@customElement("parent-component")
export class ParentComponent extends FASTElement {
  childData: string = "Initial data";
  
  updateChildData() {
    this.childData = `Updated at ${new Date().toLocaleTimeString()}`;
  }
  
  static template = html<ParentComponent>`
    <div class="parent-component">
      <button @click="${x => x.updateChildData()}">Update Child</button>
      <child-component data="${x => x.childData}"></child-component>
    </div>
  `;
}

@customElement("child-component")
export class ChildComponent extends FASTElement {
  @attr data: string = "";
  
  processedData: string = "";
  
  dataChanged() {
    this.processedData = `Processed: ${this.data}`;
    this.$emit("data-processed", { processed: this.processedData });
  }
  
  static template = html<ChildComponent>`
    <div class="child-component">
      <p>Original: ${x => x.data}</p>
      <p>Processed: ${x => x.processedData}</p>
    </div>
  `;
}

describe("Integration Testing", () => {
  it("should test parent-child component interaction", async () => {
    const parentFixture = await fixture<ParentComponent>("parent-component");
    const parent = parentFixture.element;
    await parentFixture.connect();
    
    const child = TestQuery.shadowQuery<ChildComponent>(parent, 'child-component');
    expect(child).toBeTruthy();
    expect(child!.data).toBe("Initial data");
    
    const childProcessSpy = createEventSpy(child!, "data-processed");
    
    // Update parent data
    const updateButton = TestQuery.shadowQuery<HTMLButtonElement>(parent, 'button');
    UserInteraction.click(updateButton!);
    
    await parentFixture.detectChanges();
    await waitFor(() => child!.data !== "Initial data", 1000);
    
    expect(child!.data).toContain("Updated at");
    expect(childProcessSpy).toHaveBeenCalled();
    
    const childText = TestQuery.getTextContent(child!);
    expect(childText).toContain("Processed:");
    
    await parentFixture.disconnect();
  });
});

Types

/**
 * Test fixture configuration
 */
interface FixtureOptions {
  /** Parent element for the fixture */
  parent?: HTMLElement;
  
  /** Document to create elements in */
  document?: Document;
  
  /** Whether to auto-connect the fixture */
  autoConnect?: boolean;
  
  /** Custom attributes to set */
  attributes?: Record<string, string>;
  
  /** Custom properties to set */
  properties?: Record<string, any>;
  
  /** Operation timeout in milliseconds */
  timeout?: number;
}

/**
 * Test fixture interface
 */
interface Fixture<T extends HTMLElement = HTMLElement> {
  /** The test document */
  readonly document: Document;
  
  /** The template used */
  readonly template: ViewTemplate;
  
  /** The fixture element */
  readonly element: T;
  
  /** The parent container */
  readonly parent: HTMLElement;
  
  /** Connect to DOM */
  connect(): Promise<void>;
  
  /** Disconnect from DOM */
  disconnect(): Promise<void>;
  
  /** Trigger change detection */
  detectChanges(): Promise<void>;
  
  /** Wait for stability */
  whenStable(): Promise<void>;
}

/**
 * User interaction event options
 */
interface UserInteractionOptions {
  /** Mouse event options */
  mouse?: MouseEventInit;
  
  /** Keyboard event options */
  keyboard?: KeyboardEventInit;
  
  /** Focus options */
  focus?: FocusOptions;
}

docs

attributes.md

context-system.md

css-styling.md

data-binding.md

dependency-injection.md

html-templates.md

index.md

observable-system.md

ssr-hydration.md

state-management.md

template-directives.md

testing-utilities.md

utilities.md

web-components.md

tile.json