# Testing

> The unit and e2e suites, the environment variables they honour, and how to run everything against lit main.

Run the two suites, and point them at in-development Lit when you need to. Read [Development](/contributing/development/) first if you have not installed the workspace yet.

```sh
pnpm test            # unit + e2e
pnpm run test:unit   # node-only unit tests
pnpm run test:e2e    # spawns vite dev servers + system Chrome
```

`test:unit` is the fast loop: plain Node, no browser. `test:e2e` builds the plugin first, then drives a real dev server with playwright-core and the system Chrome through `channel: 'chrome'`. It runs one dev server and one browser per test file, sequentially, to avoid port and file contention.

## Environment variables

* `HMR_E2E_HEADED=1` watches the browser while the tests run.
* `HMR_E2E_EXECUTABLE=/path/to/chrome` uses a specific browser binary. Reach for it after `npx playwright install chromium` when you have no system Chrome.

By default the tests run against the published npm versions of `lit`, `@lit/context`, `@lit/task`, `@lit-labs/signals`, and `@lit-labs/virtualizer`.

## Writing an e2e test

Start from `startFixture()` in `src/test/e2e/utils.ts`. It copies the playground into a fresh directory and serves that, so a test can edit files to trigger HMR without touching the real playground or another test’s copy.

* **Fixtures live under `.e2e-tmp/`**, inside the package, not the OS temp directory. Bare imports in a fixture resolve by walking up to the repo’s `node_modules`, and they would not find it from `/tmp`. Use `tmpRoot(prefix)` for any other throwaway directory a test needs. `.e2e-tmp/` is gitignored.
* **`HOME` points at a throwaway directory** while a fixture runs, because devframe keeps its global settings store under the home directory. Panel settings a test writes never reach your own. Seed them with the `seedSettings` option instead of writing to `~/.vite`.
* **Importing a bare package a fixture has never loaded** can make Vite’s dep optimizer reload the page mid-test. Load the module once and wait for the network to settle before the part of the test that matters.

The plugin’s internal virtual modules use ids that start with `\0lit-plugin:` (`VIRTUAL_PREFIX` in `src/lib/transform.ts`). The `\0` is Rollup’s marker for “not a file”: other plugins skip these ids, and this plugin’s own transforms skip them too. The one public virtual module, `virtual:lit-plugin/timeline`, resolves to `\0virtual:lit-plugin/timeline`. When a test looks for a module in Vite’s module graph or a build chunk, match on these ids, not on a file path.

## Canary runs against lit `main`

The [lit monorepo](https://github.com/lit/lit) is checked out as a submodule. It is a read-only reference to the real lit source, not the compiled npm output, and an opt-in way to test this plugin against in-development lit changes.

```sh
git submodule update --init   # once
pnpm build:lit-canary         # installs + builds lit inside ./lit
LIT_CANARY=1 pnpm test        # resolves lit imports from ./lit
```

With `LIT_CANARY=1`, every bare `lit`, `@lit/*`, and `@lit-labs/*` import in the test fixtures and the unit tests is aliased into the built submodule output. The whole suite then runs against lit `main`.

The `Lit canary` workflow does the same every Monday and on demand from the Actions tab. It moves the submodule to the tip of lit’s default branch first, so a red run means something changed upstream, or in the plugin’s reading of it.

What the canary is for

In-place patching depends on internals of how Lit evaluates classes and templates. The canary suite is the early warning when a change on lit `main` breaks that, which is the part of this package most worth protecting.

## Related

[Development](/contributing/development/)Install, build, playground, and the docs site.

[Limitations](/reference/limitations/)The behaviours the e2e suite pins down.
