Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export default defineConfig({

## API

Webcontainer utilities are exposed as [test fixtures](https://vitest.dev/guide/test-context.html#test-extend).
Webcontainer utilities are exposed as [test fixtures](https://vitest.dev/guide/test-context.html#test-extend):

- [`preview`](#preview)
- [`webcontainer`](#webcontainer)
- [`setup`](#setup)

```ts
import { test } from "@webcontainer/test";
Expand All @@ -70,7 +74,7 @@ import { test, type TestContext } from "@webcontainer/test";
import { beforeEach } from "vitest";

// Mount project before each test
beforeEach<TextContext>(({ webcontainer }) => {
beforeEach<TestContext>(({ webcontainer }) => {
await webcontainer.mount("projects/example");
});
```
Expand Down Expand Up @@ -214,5 +218,34 @@ WebContainer's [`rm`](https://webcontainers.io/guides/working-with-the-file-syst
await webcontainer.rm("/node_modules");
```

### `setup`

If you have repetitive steps that are needed by multiple test cases, you can improve test performance by using `setup`.

It calls the given function once, saves WebContainer state in a snapshot, and restores that snapshot before each test.

```ts
import { test, type TestContext } from "@webcontainer/test";
import { beforeEach, expect, onTestFinished } from "vitest";

beforeEach<TestContext>(async ({ webcontainer, setup }) => {
// This is run once and cached for each next run
await setup(async () => {
await webcontainer.mount("./svelte-project");
await webcontainer.runCommand("npm", ["install"]);
});
});

// No need to re-mount file system or re-run install in test cases
test("user can build project", async ({ webcontainer }) => {
await webcontainer.runCommand("npm", ["run", "build"]);
});

test("user can start project", async ({ webcontainer, preview }) => {
void webcontainer.runCommand("npm", ["run", "dev"]);
await preview.getByRole("heading", { name: "Welcome to SvelteKit" });
});
```

[version-badge]: https://img.shields.io/npm/v/@webcontainer/test
[npm-url]: https://www.npmjs.com/package/@webcontainer/test
10 changes: 10 additions & 0 deletions src/fixtures/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,14 @@ export class FileSystem {
async rm(path: string) {
return this._instance.fs.rm(path);
}

/** @internal */
async export() {
return await this._instance.export("./", { format: "binary" });
}

/** @internal */
async restore(snapshot: Uint8Array) {
return await this._instance.mount(new Uint8Array(snapshot));
}
}
26 changes: 25 additions & 1 deletion src/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { WebContainer } from "./webcontainer";
export interface TestContext {
preview: Preview;
webcontainer: WebContainer;
setup: (callback: () => Promise<void>) => Promise<void>;

/** @internal */
_internalState: { current: Uint8Array | undefined };
}

/**
Expand All @@ -29,7 +33,10 @@ export interface TestContext {
* });
* ```
*/
export const test = base.extend<TestContext>({
export const test = base.extend<Omit<TestContext, "_internalState">>({
// @ts-ignore -- intentionally untyped, excluded from public API
_internalState: { current: undefined },

preview: async ({ webcontainer }, use) => {
await webcontainer.wait();

Expand Down Expand Up @@ -60,4 +67,21 @@ export const test = base.extend<TestContext>({

await webcontainer.teardown();
},

// @ts-ignore -- intentionally untyped, excluded from public API
setup: async ({ webcontainer, _internalState }, use) => {
const internalState = _internalState as TestContext["_internalState"];

await use(async (callback) => {
if (internalState.current) {
await webcontainer.restore(internalState.current);
return;
}

await callback();

// save current state in fixture
internalState.current = await webcontainer.export();
});
},
});
23 changes: 23 additions & 0 deletions test/setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { beforeEach, expect } from "vitest";
import { test, type TestContext } from "../src";

const counts = { setup: 0, beforeEach: 0 };

beforeEach<TestContext>(async ({ setup, webcontainer }) => {
await setup(async () => {
await webcontainer.writeFile("./example", "Hello world");
counts.setup++;
});

counts.beforeEach++;
});

test.for([1, 2, 3])("state is restored %d", async (count, { webcontainer }) => {
await expect(webcontainer.readFile("example")).resolves.toBe("Hello world");

// setup should always be called just once as it's cached
expect(counts.setup).toBe(1);

// hook itself is called each time
expect(counts.beforeEach).toBe(count);
});