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
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ Vitest's [`getByText`](https://vitest.dev/guide/browser/locators.html#getbytext)
await preview.getByText("Hello Vite!");
```

##### `locator`

Vitest's [`locator`](https://vitest.dev/guide/browser/locators.html) of the preview window.

```ts
await preview.locator.hover();
```

#### `webcontainer`

##### `mount`
Expand All @@ -90,6 +98,15 @@ Accepts a path that is relative to the [project root](https://vitest.dev/config/

```ts
await webcontainer.mount("/path/to/project");

await webcontainer.mount({
"package.json": { file: { contents: '{ "name": "example-project" }' } },
src: {
directory: {
"index.ts": { file: { contents: "export default 'Hello!';" } },
},
},
});
```

##### `runCommand`
Expand All @@ -102,5 +119,53 @@ await webcontainer.runCommand("npm", ["install"]);
const files = await webcontainer.runCommand("ls", ["-l"]);
```

##### `readFile`

WebContainer's [`readFile`](https://webcontainers.io/guides/working-with-the-file-system#readfile) method.

```ts
const content = await webcontainer.readFile("/package.json");
```

##### `writeFile`

WebContainer's [`writeFile`](https://webcontainers.io/guides/working-with-the-file-system#writefile) method.

```ts
await webcontainer.writeFile("/main.ts", "console.log('Hello world!')");
```

##### `rename`

WebContainer's [`rename`](https://webcontainers.io/guides/working-with-the-file-system#rename) method.

```ts
await webcontainer.rename("/before.ts", "/after.ts");
```

##### `mkdir`

WebContainer's [`mkdir`](https://webcontainers.io/guides/working-with-the-file-system#mkdir) method.

```ts
await webcontainer.mkdir("/src/components");
```

##### `readdir`

WebContainer's [`readdir`](https://webcontainers.io/guides/working-with-the-file-system#readdir) method.

```ts
const contents = await webcontainer.readdir("/src");
```

##### `rm`

WebContainer's [`rm`](https://webcontainers.io/guides/working-with-the-file-system#rm) method.

```ts
await webcontainer.rm("/node_modules");
```

[version-badge]: https://img.shields.io/npm/v/@webcontainer/test
[npm-url]: https://www.npmjs.com/package/@webcontainer/test
2 changes: 1 addition & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const readDirectory: BrowserCommand<[directory: string]> = async (

if (!resolved.startsWith(root)) {
throw new Error(
`[vitest:webcontainers] Cannot read files outside project root: \n${JSON.stringify(
`[vitest:webcontainers] Cannot read files outside project root:\n${JSON.stringify(
{ directory, resolved },
null,
2,
Expand Down
20 changes: 20 additions & 0 deletions src/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ import { test as base } from "vitest";
import { Preview } from "./preview";
import { WebContainer } from "./webcontainer";

/**
* Pre-defined [`test()` function](https://vitest.dev/guide/test-context.html#extend-test-context) with WebContainer fixtures.
*
* @example
* ```ts
* import { test } from "@webcontainer/test";
*
* test("run development server inside webcontainer", async ({
* webcontainer,
* preview,
* }) => {
* await webcontainer.mount("path/to/project");
*
* await webcontainer.runCommand("npm", ["install"]);
* webcontainer.runCommand("npm", ["run", "dev"]);
*
* await preview.getByRole("heading", { level: 1, name: "Hello Vite!" });
* });
* ```
*/
export const test = base.extend<{
preview: Preview;
webcontainer: WebContainer;
Expand Down
18 changes: 18 additions & 0 deletions src/fixtures/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { type Locator, page } from "@vitest/browser/context";
const TEST_ID = "webcontainers-iframe";

export class Preview {
/** @internal */
private _preview: Locator;

/** @internal */
private _iframe?: HTMLIFrameElement;

constructor() {
this._preview = page.getByTestId(TEST_ID);
}

/** @internal */
async setup(url: string) {
const iframe = document.createElement("iframe");
iframe.setAttribute("src", url);
Expand All @@ -21,17 +25,31 @@ export class Preview {
this._iframe = iframe;
}

/** @internal */
async teardown() {
if (this._iframe) {
document.body.removeChild(this._iframe);
}
}

/**
* Vitest's [`getByRole`](https://vitest.dev/guide/browser/locators.html#getbyrole) that's scoped to the preview window.
*/
async getByRole(...options: Parameters<typeof page.getByRole>) {
return this._preview.getByRole(...options);
}

/**
* Vitest's [`getByText`](https://vitest.dev/guide/browser/locators.html#getbytext) that's scoped to the preview window.
*/
async getByText(...options: Parameters<typeof page.getByText>) {
return this._preview.getByText(...options);
}

/**
* Vitest's [`locator`](https://vitest.dev/guide/browser/locators.html) of the preview window.
*/
get locator() {
return this._preview;
}
}
32 changes: 31 additions & 1 deletion src/fixtures/webcontainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
} from "@webcontainer/api";

export class WebContainer {
/** @internal */
private _instancePromise?: WebContainerApi;

/** @internal */
private _isReady: Promise<void>;

/** @internal */
private _onExit: (() => Promise<unknown>)[] = [];

constructor() {
Expand All @@ -26,18 +31,20 @@ export class WebContainer {
return this._instancePromise;
}

/** @internal */
async wait() {
await this._isReady;
}

/** @internal */
onServerReady(callback: (options: { port: number; url: string }) => void) {
this._instance.on("server-ready", (port, url) => {
callback({ port, url });
});
}

/**
* Mount file directory into webcontainer.
* Mount file directory into WebContainer.
* `string` arguments are considered paths that are relative to [`root`](https://vitest.dev/config/#root)
*/
async mount(filesOrPath: string | FileSystemTree) {
Expand All @@ -48,6 +55,7 @@ export class WebContainer {
return await this._instance.mount(filesOrPath as FileSystemTree);
}

/** @internal */
async teardown() {
await Promise.all(this._onExit.map((fn) => fn()));

Expand All @@ -58,6 +66,10 @@ export class WebContainer {
this._instancePromise = undefined;
}

/**
* Run command inside WebContainer.
* Returns the output of the command.
*/
async runCommand(command: string, args: string[] = []) {
let output = "";

Expand Down Expand Up @@ -86,26 +98,44 @@ export class WebContainer {
return output.trim();
}

/**
* WebContainer's [`readFile`](https://webcontainers.io/guides/working-with-the-file-system#readfile) method.
*/
async readFile(path: string, encoding: BufferEncoding = "utf8") {
return this._instance.fs.readFile(path, encoding);
}

/**
* WebContainer's [`writeFile`](https://webcontainers.io/guides/working-with-the-file-system#writefile) method.
*/
async writeFile(path: string, data: string, encoding = "utf8") {
return this._instance.fs.writeFile(path, data, { encoding });
}

/**
* WebContainer's [`rename`](https://webcontainers.io/guides/working-with-the-file-system#rename) method.
*/
async rename(oldPath: string, newPath: string) {
return this._instance.fs.rename(oldPath, newPath);
}

/**
* WebContainer's [`mkdir`](https://webcontainers.io/guides/working-with-the-file-system#mkdir) method.
*/
async mkdir(path: string) {
return this._instance.fs.mkdir(path);
}

/**
* WebContainer's [`readdir`](https://webcontainers.io/guides/working-with-the-file-system#readdir) method.
*/
async readdir(path: string) {
return this._instance.fs.readdir(path);
}

/**
* WebContainer's [`rm`](https://webcontainers.io/guides/working-with-the-file-system#rm) method.
*/
async rm(path: string) {
return this._instance.fs.rm(path);
}
Expand Down
1 change: 0 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Vite } from "vitest/node";
import { readDirectory } from "./commands";
import "./types.d.ts";

const COEP = "Cross-Origin-Embedder-Policy";
const COOP = "Cross-Origin-Opener-Policy";
Expand Down
41 changes: 38 additions & 3 deletions test/mount.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { expect } from "vitest";
import { test } from "../src";

test("user can mount directories to webcontainer", async ({ webcontainer }) => {
test("user can mount directories from file-system to webcontainer", async ({
webcontainer,
}) => {
await webcontainer.mount("test/fixtures/mount-example");

const ls = await webcontainer.runCommand("ls");
Expand All @@ -21,12 +23,45 @@ test("user can mount directories to webcontainer", async ({ webcontainer }) => {
);
});

test("user can mount inlined FileSystemTree to webcontainer", async ({
webcontainer,
}) => {
await webcontainer.mount({
"file-1.ts": {
file: { contents: 'export default "Hello world";' },
},
nested: {
directory: {
"file-2.ts": {
file: { contents: 'export default "Hello from nested file";' },
},
},
},
});

const ls = await webcontainer.runCommand("ls");
expect(ls).toMatchInlineSnapshot(`"file-1.ts nested"`);

const lsNested = await webcontainer.runCommand("ls", ["nested"]);
expect(lsNested).toMatchInlineSnapshot(`"file-2.ts"`);

const catFile = await webcontainer.runCommand("cat", ["file-1.ts"]);
expect(catFile).toMatchInlineSnapshot(`"export default "Hello world";"`);

const catNestedFile = await webcontainer.runCommand("cat", [
"nested/file-2.ts",
]);
expect(catNestedFile).toMatchInlineSnapshot(
`"export default "Hello from nested file";"`,
);
});

test("user should see error when attemping to mount files outside project root", async ({
webcontainer,
}) => {
await expect(() => webcontainer.mount("/home/non-existing")).rejects
.toThrowErrorMatchingInlineSnapshot(`
[Error: [vitest:webcontainers] Cannot read files outside project root:
[Error: [vitest:webcontainers] Cannot read files outside project root:
{
"directory": "/home/non-existing",
"resolved": "/home/non-existing"
Expand All @@ -35,7 +70,7 @@ test("user should see error when attemping to mount files outside project root",

await expect(() => webcontainer.mount("/../../non-existing")).rejects
.toThrowErrorMatchingInlineSnapshot(`
[Error: [vitest:webcontainers] Cannot read files outside project root:
[Error: [vitest:webcontainers] Cannot read files outside project root:
{
"directory": "/../../non-existing",
"resolved": "/non-existing"
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "bundler",
"skipLibCheck": true
"skipLibCheck": true,
"stripInternal": true
},
"include": ["src", "test"]
}