{"id":36712496,"url":"https://github.com/stenciljs/vitest","last_synced_at":"2026-06-13T02:04:45.151Z","repository":{"id":330275631,"uuid":"1122207839","full_name":"stenciljs/vitest","owner":"stenciljs","description":"First-class testing utilities for Stencil components with Vitest","archived":false,"fork":false,"pushed_at":"2026-02-13T16:18:17.000Z","size":342,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-13T20:39:19.162Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stenciljs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["johnjenkins"]}},"created_at":"2025-12-24T09:30:35.000Z","updated_at":"2026-02-13T16:18:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/stenciljs/vitest","commit_stats":null,"previous_names":["stenciljs/vitest"],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/stenciljs/vitest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenciljs%2Fvitest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenciljs%2Fvitest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenciljs%2Fvitest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenciljs%2Fvitest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stenciljs","download_url":"https://codeload.github.com/stenciljs/vitest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenciljs%2Fvitest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29489383,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-15T19:29:10.908Z","status":"ssl_error","status_checked_at":"2026-02-15T19:29:10.419Z","response_time":118,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-12T11:50:11.602Z","updated_at":"2026-06-13T02:04:45.137Z","avatar_url":"https://github.com/stenciljs.png","language":"TypeScript","funding_links":["https://github.com/sponsors/johnjenkins"],"categories":[],"sub_categories":[],"readme":"# @stencil/vitest\n\nFirst-class testing utilities for Stencil components, powered by Vitest.\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n  - [1. Install](#1-install)\n  - [2. Create vitest.config.ts](#2-create-vitestconfigts)\n  - [3. Load your components](#3-load-your-components)\n  - [4. Write Tests](#4-write-tests)\n  - [5. Run tests](#5-run-tests)\n- [API](#api)\n  - [Rendering](#rendering)\n  - [Available matchers](#available-matchers)\n  - [Spying and Mocking](#spying-and-mocking)\n  - [Event Testing](#event-testing)\n- [Stencil Vitest Plugin (Experimental)](#stencil-vitest-plugin)\n  - [Setup](#setup)\n  - [Mocking component dependencies](#mocking-component-dependencies)\n  - [Limitations](#limitations)\n- [Snapshots](#snapshots)\n- [Screenshot Testing](#screenshot-testing)\n- [Utils](#utils)\n  - [serializeHtml](#serializehtmlelement-options)\n  - [prettifyHtml](#prettifyhtmlhtml)\n  - [waitForStable](#waitforstableelementorselector-timeout)\n  - [waitForExist](#waitforexistselector-timeout)\n- [CLI](#cli)\n  - [Usage](#usage)\n  - [Flags](#flags)\n  - [Global Variables](#global-variables)\n- [Limitations / Gotchas](#limitations--gotchas)\n- [License](#license)\n- [Contributing](#contributing)\n\n## Quick Start\n\n### 1. Install\n\n```bash\nnpm i --save-dev @stencil/vitest vitest\n```\n\nFor browser testing, also install:\n\n```bash\nnpm i -D @vitest/browser-playwright\n# or\nnpm i -D @vitest/browser-webdriverio\n```\n\n### 2. Create `vitest.config.ts`\n\n```typescript\nimport { defineVitestConfig } from '@stencil/vitest/config';\nimport { playwright } from '@vitest/browser-playwright';\n\nexport default defineVitestConfig({\n  stencilConfig: './stencil.config.ts',\n  test: {\n    projects: [\n      // Unit tests - node environment for functions / logic\n      {\n        test: {\n          name: 'unit',\n          include: ['src/**/*.unit.{ts,tsx}'],\n          environment: 'node',\n        },\n      },\n      // Spec tests - via a node DOM of your choice\n      {\n        test: {\n          name: 'spec',\n          include: ['src/**/*.spec.{ts,tsx}'],\n          environment: 'stencil',\n          setupFiles: ['./vitest-setup.ts'],\n\n          // Optional environment options\n\n          // environmentOptions: {\n          //   stencil: {\n          //     domEnvironment: 'happy-dom' | 'jsdom' | 'mock-doc' (default)\n          //                      ^^ Make sure to install relevant packages\n          //   },\n          // },\n        },\n      },\n      // Browser tests\n      {\n        test: {\n          name: 'browser',\n          include: ['src/**/*.test.{ts,tsx}'],\n          setupFiles: ['./vitest-setup.ts'],\n          browser: {\n            enabled: true,\n            provider: playwright(),\n            headless: true,\n            instances: [{ browser: 'chromium' }],\n          },\n        },\n      },\n    ],\n  },\n});\n```\n\n[refer to Vitest's documentation for all configuration options](https://vitest.dev/config/)\n\n### 3. Load your components\n\n```typescript\n// vitest-setup.ts\n\n// Load Stencil components.\n// Adjust according to your build output of choice *\nawait import('./dist/test-components/test-components.esm.js');\n\nexport {};\n// * Bear in mind, you may need `buildDist: true` (in your stencil.config)\n// or `--prod` to use an output other than the browser lazy-loader\n```\n\n### 4. Write Tests\n\n```tsx\n// src/components/my-button/my-button.spec.tsx\n\nimport { render, h, describe, it, expect } from '@stencil/vitest';\n\ndescribe('my-button', () =\u003e {\n  it('renders with text', async () =\u003e {\n    const { root, waitForChanges } = await render(\u003cmy-button label=\"Click me\" /\u003e);\n    root.click();\n    await waitForChanges();\n    await expect(root).toEqualHtml(`\n      \u003cmy-button class=\"hydrated\"\u003e\n        \u003cmock:shadow-root\u003e\n          \u003cbutton class=\"button button--secondary button--small\" type=\"button\"\u003e\n            \u003cslot\u003e\u003c/slot\u003e\n          \u003c/button\u003e\n        \u003c/mock:shadow-root\u003e\n        Small\n      \u003c/my-button\u003e\n    `);\n  });\n});\n```\n\n### 5. Run tests\n\n```json\n// package.json\n{\n  \"scripts\": {\n    \"test\": \"stencil-test\",\n    \"test:watch\": \"stencil-test --watch\",\n    \"test:e2e\": \"stencil-test --project browser\",\n    \"test:spec\": \"stencil-test --project spec\"\n  }\n}\n```\n\n## API\n\n### Rendering\n\n#### `render(VNode)`\n\nRender a component for testing.\n\n```tsx\nimport { render, h } from '@stencil/vitest';\n\nconst { root, waitForChanges, setProps, unmount } = await render(\u003cmy-component name=\"World\" /\u003e);\n\n// Access the element\nexpect(root.textContent).toContain('World');\n\n// Update props\nroot.name = 'Stencil';\nawait waitForChanges();\n// or\nawait setProps({ name: 'Stencil' });\n\n// Unmount component\nunmount();\n```\n\n#### `waitForReady` Option\n\nBy default, `render()` waits for components to be fully hydrated before returning. It detects when Stencil applies the hydrated flag (class or attribute) to your component, respecting your `stencil.config` settings.\n\n```tsx\n// Default behaviour - waits for hydration\nconst { root } = await render(\u003cmy-component /\u003e);\n\n// Skip hydration wait (useful for testing loading states)\nconst { root } = await render(\u003cmy-component /\u003e, { waitForReady: false });\n```\n\n### Available matchers:\n\n```typescript\n// DOM assertions\nexpect(element).toHaveClass('active');\nexpect(element).toHaveClasses(['active', 'primary']); // Contains all / partial match\nexpect(element).toMatchClasses(['active']); // Exact match\nexpect(element).toHaveAttribute('aria-label', 'Close');\nexpect(element).toEqualAttribute('type', 'button');\nexpect(element).toEqualAttributes({ type: 'button', disabled: true });\nexpect(element).toHaveProperty('value', 'test');\nexpect(element).toHaveTextContent('Hello World'); // includes shadow DOM text\nexpect(element).toHaveLightTextContent('Hello World'); // light DOM only\nexpect(element).toEqualText('Exact text match'); // includes shadow DOM text\nexpect(element).toEqualLightText('Exact text match'); // light DOM only\n\n// Shadow DOM\nexpect(element).toHaveShadowRoot();\nawait expect(element).toEqualHtml('\u003cdiv\u003eExpected HTML\u003c/div\u003e');\nawait expect(element).toEqualLightHtml('\u003cdiv\u003eLight DOM only\u003c/div\u003e');\n```\n\n### Spying and Mocking\n\nSpy on component methods, props, and lifecycle hooks to verify behaviour without modifying your component code.\n\n\u003e **Setup requirement:** Load your components in a `beforeAll` block (typically in your setup file). The spy system patches `customElements.define`, so components must be registered after the test framework initializes.\n\u003e\n\u003e ```diff\n\u003e // vitest-setup.ts\n\u003e - await import('./dist/test-components/test-components.esm.js');\n\u003e\n\u003e + import { beforeAll } from 'vitest';\n\u003e + beforeAll(async () =\u003e {\n\u003e +   await import('./dist/test-components/test-components.esm.js');\n\u003e + });\n\u003e ```\n\n#### Method Spying\n\nSpy on methods while still calling the original implementation:\n\n```tsx\nconst { root, spies } = await render(\u003cmy-button\u003eClick me\u003c/my-button\u003e, {\n  spyOn: {\n    methods: ['handleClick'],\n  },\n});\n\n// Trigger the method\nroot.shadowRoot?.querySelector('button')?.click();\n\n// Assert the method was called\nexpect(spies?.methods.handleClick).toHaveBeenCalledTimes(1);\nexpect(spies?.methods.handleClick).toHaveBeenCalledWith(expect.objectContaining({ type: 'click' }));\n\n// Reset call history\nspies?.methods.handleClick.mockClear();\n```\n\n#### Method Mocking\n\nReplace methods with pre-configured mocks:\n\n```tsx\n// Create mock with desired return value *before* render\nconst fetchUserMock = vi.fn().mockResolvedValue({\n  id: '123',\n  name: 'Test User',\n  email: 'test@example.com',\n});\n\n// Mock is applied before initialisation\nconst { root, spies, waitForChanges } = await render(\u003cuser-profile userId=\"123\" /\u003e, {\n  spyOn: {\n    mocks: { fetchUserData: fetchUserMock },\n  },\n});\nawait waitForChanges();\n\nexpect(fetchUserMock).toHaveBeenCalledWith('123');\nexpect(root.shadowRoot?.querySelector('.name')?.textContent).toBe('Test User');\n```\n\nAccess the original implementation to augment rather than fully replace:\n\n```tsx\nconst fetchMock = vi.fn();\nconst { spies } = await render(\u003cmy-component /\u003e, {\n  spyOn: { mocks: { fetchData: fetchMock } },\n});\n\n// Wrap the original to add logging or modify behaviour\nfetchMock.mockImplementation(async (...args) =\u003e {\n  console.log('Fetching data with args:', args);\n  const result = await spies?.mocks.fetchData.original?.(...args);\n  console.log('Got result:', result);\n  return result;\n});\n```\n\n#### Prop Spying\n\nTrack when props are changed:\n\n```tsx\nconst { spies, setProps, waitForChanges } = await render(\u003cmy-button variant=\"primary\"\u003eClick me\u003c/my-button\u003e, {\n  spyOn: {\n    props: ['variant', 'disabled'],\n  },\n});\n\nawait setProps({ variant: 'danger' });\nawait waitForChanges();\n\nexpect(spies?.props.variant).toHaveBeenCalledWith('danger');\nexpect(spies?.props.variant).toHaveBeenCalledTimes(1);\n```\n\n#### Lifecycle Spying\n\nSpy on lifecycle methods. Methods that don't exist on the component are auto-stubbed:\n\n```tsx\nconst { spies, setProps, waitForChanges } = await render(\u003cmy-button\u003eClick me\u003c/my-button\u003e, {\n  spyOn: {\n    lifecycle: ['componentWillLoad', 'componentDidLoad', 'componentWillRender', 'componentDidRender'],\n  },\n});\n\n// Lifecycle methods are called during initial render\nexpect(spies?.lifecycle.componentWillLoad).toHaveBeenCalledTimes(1);\nexpect(spies?.lifecycle.componentDidRender).toHaveBeenCalledTimes(1);\n\n// Trigger a re-render\nawait setProps({ variant: 'danger' });\nawait waitForChanges();\n\n// Re-render lifecycle methods called again\nexpect(spies?.lifecycle.componentWillRender).toHaveBeenCalledTimes(2);\nexpect(spies?.lifecycle.componentDidRender).toHaveBeenCalledTimes(2);\n```\n\n#### Resetting Spies\n\nReset all spies at once using `resetAll()`. This clears call histories AND resets mock implementations:\n\n```tsx\nconst fetchMock = vi.fn().mockReturnValue('mocked');\nconst { root, spies, setProps, waitForChanges } = await render(\u003cmy-button variant=\"primary\"\u003eClick me\u003c/my-button\u003e, {\n  spyOn: {\n    methods: ['handleClick'],\n    mocks: { fetchData: fetchMock },\n    props: ['variant'],\n  },\n});\n\n// Trigger some calls\nroot.shadowRoot?.querySelector('button')?.click();\nawait setProps({ variant: 'danger' });\n\n// Reset everything\nspies?.resetAll();\n\n// Call histories cleared\nexpect(spies?.methods.handleClick).toHaveBeenCalledTimes(0);\nexpect(spies?.props.variant).toHaveBeenCalledTimes(0);\n\n// Mock implementations reset to default (returns undefined)\nexpect(fetchMock()).toBeUndefined();\n```\n\n#### Nested Components\n\nWhen the root element is not a custom element, or when you have multiple custom elements, use `getComponentSpies()` to retrieve spies for specific elements:\n\n```tsx\nimport { render, getComponentSpies, h } from '@stencil/vitest';\n\n// Root is a div, not a custom element\nconst { root } = await render(\n  \u003cdiv\u003e\n    \u003cmy-button\u003eClick me\u003c/my-button\u003e\n  \u003c/div\u003e,\n  {\n    spyOn: { methods: ['handleClick'] },\n  },\n);\n\n// Query the nested custom element\nconst button = root.querySelector('my-button') as HTMLElement;\n\n// Get spies for the nested element\nconst buttonSpies = getComponentSpies(button);\nexpect(buttonSpies?.methods.handleClick).toBeDefined();\n\n// Multiple instances have independent spies\nconst { root: container } = await render(\n  \u003cdiv\u003e\n    \u003cmy-button class=\"a\"\u003eA\u003c/my-button\u003e\n    \u003cmy-button class=\"b\"\u003eB\u003c/my-button\u003e\n  \u003c/div\u003e,\n  { spyOn: { methods: ['handleClick'] } },\n);\n\nconst spiesA = getComponentSpies(container.querySelector('.a') as HTMLElement);\nconst spiesB = getComponentSpies(container.querySelector('.b') as HTMLElement);\n\n// Each has its own spy instance\ncontainer.querySelector('.a')?.shadowRoot?.querySelector('button')?.click();\nexpect(spiesA?.methods.handleClick).toHaveBeenCalledTimes(1);\nexpect(spiesB?.methods.handleClick).toHaveBeenCalledTimes(0);\n```\n\n#### Per-Component Configurations\n\nWhen rendering multiple component types, use the `components` property for tag-specific spy configs:\n\n```tsx\nimport { render, getComponentSpies, h } from '@stencil/vitest';\n\nconst { root } = await render(\n  \u003cmy-card cardTitle=\"Test\"\u003e\n    \u003cmy-button slot=\"footer\"\u003eClick me\u003c/my-button\u003e\n  \u003c/my-card\u003e,\n  {\n    spyOn: {\n      lifecycle: ['componentDidLoad'], // base - applies to all\n      components: {\n        'my-card': { props: ['cardTitle'] },\n        'my-button': { methods: ['handleClick'] },\n      },\n    },\n  },\n);\n\nconst cardSpies = getComponentSpies(root);\nconst buttonSpies = getComponentSpies(root.querySelector('my-button') as HTMLElement);\n\n// Both get base lifecycle spy + their specific config\nexpect(cardSpies?.lifecycle.componentDidLoad).toHaveBeenCalled();\nexpect(cardSpies?.props.cardTitle).toBeDefined();\n\nexpect(buttonSpies?.lifecycle.componentDidLoad).toHaveBeenCalled();\nexpect(buttonSpies?.methods.handleClick).toBeDefined();\n```\n\n### Event Testing\n\nTest custom events emitted by your components:\n\n```tsx\nconst { root, spyOnEvent, waitForChanges } = await render(\u003cmy-button /\u003e);\n\n// Spy on events\nconst clickSpy = spyOnEvent('buttonClick');\nconst changeSpy = spyOnEvent('valueChange');\n\n// Trigger events\nroot.click();\nawait waitForChanges();\n\n// Assert events were emitted\nexpect(clickSpy).toHaveReceivedEvent();\nexpect(clickSpy).toHaveReceivedEventTimes(1);\nexpect(clickSpy).toHaveReceivedEventDetail({ buttonId: 'my-button' });\n\n// Access event data\nexpect(clickSpy.events).toHaveLength(1);\nexpect(clickSpy.firstEvent?.detail).toEqual({ buttonId: 'my-button' });\nexpect(clickSpy.lastEvent?.detail).toEqual({ buttonId: 'my-button' });\n```\n\n## Stencil Vitest Plugin\n\nAll examples so far have mentioned setting up tests against **pre-built dist outputs**; Stencil compiles your components once and tests run against those bundles. Whilst this method is fast and reliable, it does mean Vitest never sees individual component source files as discrete modules and so does have 2 key limitations:\n\n1. `vi.mock()` cannot intercept imports made by your components, because the dependency is already bundled away before Vitest gets involved.\n2. Coverage reports will not work out-of-the-box without additional configuration (`sourceMap: true` / [3rd party tools](https://github.com/cenfun/vitest-monocart-coverage)) and even then, may not be accurate.\n\nThe experimental `stencilVitestPlugin` solves this by hooking into Vite's transform pipeline: Stencil files are compiled on-the-fly before Vitest imports them; each component file becomes its own entry in Vitest's module graph - and its imports are independently resolvable and mockable.\n\n### Setup\n\n```typescript\n// vitest.config.ts\nimport { defineVitestConfig } from '@stencil/vitest/config';\nimport { stencilVitestPlugin } from '@stencil/vitest/plugin';\n\nexport default defineVitestConfig({\n  stencilConfig: './stencil.config.ts',\n  test: {\n    projects: [\n      {\n        plugins: [stencilVitestPlugin()],\n        test: {\n          name: 'plugin',\n          include: ['src/**/*.plugin.spec.{ts,tsx}'],\n          // No dist setup file needed - each component source file registers\n\n          environment: 'stencil',\n          // ^^ you can use the plugin with any setup - even browser tests!\n        },\n      },\n    ],\n  },\n});\n```\n\n### Mocking component dependencies\n\nWith the plugin active, import the component source directly in your test. The plugin compiles it on-the-fly and the `customElements.define()` call at the end of the transformed output registers the element immediately.\n\nGiven an example component:\n\n```tsx\nimport { Component, Prop, h } from '@stencil/core';\nimport { capitalize } from '../../utils/index.js';\n\n@Component( ... )\nexport class MyLabel {\n  @Prop() value: string = '';\n\n  render() {\n    return \u003cspan class=\"label\"\u003e{capitalize(this.value)}\u003c/span\u003e;\n  }\n}\n```\n\nIt can then be imported and tested with mocked dependencies:\n\n```tsx\n// my-label.plugin.spec.tsx\nimport { describe, it, expect, vi } from 'vitest';\nimport { render, h } from '@stencil/vitest';\n\n// vi.mock() is hoisted - the mock is in place before any imports resolve\nvi.mock('../utils/index.js', () =\u003e ({\n  capitalize: vi.fn((s: string) =\u003e `[mocked:${s}]`),\n}));\n\n// Importing the source file triggers the on-the-fly compile + define\nimport './my-label.tsx';\nimport { capitalize } from '../utils/index.js';\n\nit('renders using the mocked utility', async () =\u003e {\n  vi.mocked(capitalize).mockReturnValue('Intercepted');\n\n  const { root } = await render(\u003cmy-label value=\"hello\" /\u003e);\n\n  expect(root.shadowRoot!.querySelector('span')?.textContent).toBe('Intercepted');\n  expect(capitalize).toHaveBeenCalledWith('hello');\n});\n```\n\n### Limitations\n\n#### Class inheritance\n\nIn Stencil v4 `transpile()` (used within the plugin) is a single-file compiler. When a component class `extends` a base class that lives in a separate file, `transpile()` cannot follow the import to merge the parent's metadata and will throw an error.\n\n```tsx\n// ❌ Will fail - base class is in a separate file\nimport { FormBase } from './form-base.js';\n\n@Component({ tag: 'my-input', shadow: true })\nexport class MyInput extends FormBase { ... }\n```\n\n\u003e This limitation is specific to v4. Stencil v5's `transpile()` can resolve multi-file inheritance chains.\n\n## Snapshots\n\nThe package includes a custom snapshot serializer for Stencil components that properly handles shadow DOM:\n\n```tsx\nimport { render, h } from '@stencil/vitest';\n...\nconst { root } = await render(\u003cmy-component /\u003e);\nexpect(root).toMatchSnapshot();\n```\n\n**Snapshot output example:**\n\n```html\n\u003cmy-component\u003e\n  \u003cmock:shadow-root\u003e\n    \u003cbutton class=\"primary\"\u003e\n      \u003cslot /\u003e\n    \u003c/button\u003e\n  \u003c/mock:shadow-root\u003e\n  Click me\n\u003c/my-component\u003e\n```\n\n## Screenshot Testing\n\nBrowser tests can include screenshot comparisons using Vitest's screenshot capabilities:\n\n```tsx\nimport { render, h } from '@stencil/vitest';\n...\nconst { root } = await render(\u003cmy-button variant=\"primary\"\u003ePrimary Button\u003c/my-button\u003e);\nawait expect(root).toMatchScreenshot();\n```\n\nRefer to Vitest's [screenshot testing documentation](https://vitest.dev/guide/snapshot.html#visual-snapshots) for more details.\n\n## Utils\n\n### `serializeHtml(element, options?)`\n\nSerializes an HTML element to a string, including shadow DOM content. Useful for debugging or creating custom assertions.\n\n```tsx\nimport { serializeHtml } from '@stencil/vitest';\n\nconst html = serializeHtml(element, {\n  serializeShadowRoot: true, // Include shadow DOM (default: true)\n  pretty: true, // Prettify output (default: true)\n  excludeStyles: true, // Exclude \u003cstyle\u003e tags (default: true)\n});\n```\n\n### `prettifyHtml(html)`\n\nFormats HTML string with indentation for readability.\n\n```tsx\nimport { prettifyHtml } from '@stencil/vitest';\n\nconst formatted = prettifyHtml('\u003cdiv\u003e\u003cspan\u003eHello\u003c/span\u003e\u003c/div\u003e');\n// Returns:\n// \u003cdiv\u003e\n//   \u003cspan\u003e\n//     Hello\n//   \u003c/span\u003e\n// \u003c/div\u003e\n```\n\n### `waitForStable(elementOrSelector, timeout?)`\n\nWaits for an element to be rendered and visible in the DOM. Only works in real browser environments (not jsdom/happy-dom).\n\nAccepts either an `Element` or a CSS selector string. When a selector is provided, it polls until the element appears in the DOM.\n\n```tsx\nimport { render, waitForStable, h } from '@stencil/vitest';\n\n// Wait for a rendered element to be stable / visible\nconst { root } = await render(\u003cmy-component /\u003e);\nawait waitForStable(root);\n\n// Wait for an element using a selector (useful when element isn't in DOM yet)\nawait waitForStable('my-component .inner-element');\n\n// Custom timeout (default: 5000ms)\nawait waitForStable('my-component', 10000);\n```\n\n\u003e **Note:** In non-browser environments, `waitForStable` logs a warning and returns immediately.\n\n### `waitForExist(selector, timeout?)`\n\nWaits for an element matching the selector to exist in the DOM. Unlike `waitForStable`, this works in both real browsers and mock DOM environments (jsdom/happy-dom).\n\nReturns the element if found, or `null` if timeout is reached.\n\n```tsx\nimport { waitForExist } from '@stencil/vitest';\n\n// Wait for an element to appear in the DOM\nconst element = await waitForExist('my-component .lazy-loaded');\n\n// Custom timeout (default: 5000ms)\nconst element = await waitForExist('#dynamic-content', 10000);\n```\n\n## CLI\n\nThe `stencil-test` CLI wraps both Stencil builds with Vitest testing.\n\n### Usage\n\n```bash\n# Build once, test once\nstencil-test\n\n# Watch mode (rebuilds on component changes, interactive Vitest)\nstencil-test --watch\n\n# Watch mode with dev server\nstencil-test --watch --serve\n\n# Production build before testing\nstencil-test --prod\n\n# Pass arguments to Vitest\nstencil-test --watch --coverage\n\n# Test specific files\nstencil-test button.spec.ts\n\n# Test specific project\nstencil-test --project browser\n```\n\n### Flags\n\nThe `stencil-test` CLI supports most of Stencil's CLI flags and all of Vitest CLI flags\n\n- For full Stencil CLI flags, see [Stencil CLI docs](https://stenciljs.com/docs/cli).\n- For full Vitest CLI flags, see [Vitest CLI docs](https://vitest.dev/guide/cli.html).\n\nNote: unlike a normal `stencil build` `stencil-vitest` runs in development mode by default for faster builds. Use `--prod` to test against a production build.\n\n### Global Variables\n\nThe `stencil-test` CLI exposes global variables that can be accessed in your tests to check which CLI flags were used:\n\n| Global              | Type      | Description                            |\n| ------------------- | --------- | -------------------------------------- |\n| `__STENCIL_PROD__`  | `boolean` | `true` when `--prod` flag is passed    |\n| `__STENCIL_SERVE__` | `boolean` | `true` when `--serve` flag is passed   |\n| `__STENCIL_PORT__`  | `string`  | Port number when `--port` is specified |\n\n```tsx\nif (__STENCIL_PROD__) {\n  console.log('Running tests against production build');\n}\n\nif (__STENCIL_SERVE__) {\n  const baseUrl = `http://localhost:${__STENCIL_PORT__ || '3333'}`;\n}\n```\n\n#### TypeScript Support\n\nAdd to your `tsconfig.json` for type definitions:\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"@stencil/vitest/globals\"]\n  }\n}\n```\n\n## Limitations / Gotchas\n\n### `vi.mock()` doesn't work?\n\nModules can only be mocked if they are imported in a way that Vitest can intercept. If your components are importing dependencies that you want to mock, you must use the `stencilVitestPlugin` to compile components on-the-fly and allow Vitest to mock their imports. See the [Stencil Vitest Plugin section](#stencil-vitest-plugin) for details.\n\n### Coverage reports are empty or inaccurate?\n\nWhen testing against pre-built dist outputs, source maps (`sourceMap: true` in `stencil.config.ts`) and [3rd party tools](https://github.com/cenfun/vitest-monocart-coverage) are required for coverage reports. Alternatively, consider using the `stencilVitestPlugin` which compiles components on-the-fly and provides better coverage support.\n\n## License\n\nMIT\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and guidelines.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstenciljs%2Fvitest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstenciljs%2Fvitest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstenciljs%2Fvitest/lists"}