diff --git a/src/assets/vitest.svg b/src/assets/vitest.svg
new file mode 100644
index 00000000..f5baed5d
--- /dev/null
+++ b/src/assets/vitest.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/src/components/build-with-friends/build-with-friends.js b/src/components/build-with-friends/build-with-friends.js
index c03a37cf..d64a7bcf 100644
--- a/src/components/build-with-friends/build-with-friends.js
+++ b/src/components/build-with-friends/build-with-friends.js
@@ -5,6 +5,7 @@ import openPropsIcon from "../../assets/open-props.svg?type=raw";
import storybookIcon from "../../assets/storybook.svg?type=raw";
import tailwindIcon from "../../assets/tailwind-logo.svg?type=raw";
import typescriptIcon from "../../assets/typescript.svg?type=raw";
+import vitestIcon from "../../assets/vitest.svg?type=raw";
import wccIcon from "../../assets/wcc.svg?type=raw";
import wtrIcon from "../../assets/modern-web.svg?type=raw";
@@ -34,14 +35,19 @@ export default class BuildWithFriends extends HTMLElement {
${wtrIcon}
- Web Test Runner
+ Web Test Runner
- ${wccIcon}
+ ${wccIcon}
+ WC Compiler
${openPropsIcon}
- Open Props
+ Open Props
+
+
+ ${vitestIcon}
+ Vitest
diff --git a/src/components/build-with-friends/build-with-friends.module.css b/src/components/build-with-friends/build-with-friends.module.css
index 1c876e00..a8d8706b 100644
--- a/src/components/build-with-friends/build-with-friends.module.css
+++ b/src/components/build-with-friends/build-with-friends.module.css
@@ -35,6 +35,10 @@
display: inline-block;
}
+.iconLabel {
+ padding-left: var(--size-1);
+}
+
@media (min-width: 600px) {
.icon {
display: inline-block;
diff --git a/src/pages/guides/ecosystem/storybook.md b/src/pages/guides/ecosystem/storybook.md
index 4d301a7d..ade796ca 100644
--- a/src/pages/guides/ecosystem/storybook.md
+++ b/src/pages/guides/ecosystem/storybook.md
@@ -47,6 +47,8 @@ We were not able to detect the right builder for your project. Please select one
Webpack 5
```
+> See our Vitest docs for additional configuration examples to support [Import Attributes](/guides/ecosystem/vitest/#import-attributes) and [Greenwood resource plugins](/guides/ecosystem/vitest/#resource-plugins) usage in your components. For that guide, you'll be updating a _vite.config.js_ file instead.
+
## Usage
You should now be good to start writing your first story! 📚
@@ -145,147 +147,6 @@ You'll want to create a CommonJS version with the following name, depending on w
-## Import Attributes
-
-As [Vite does not support Import Attributes](https://github.com/vitejs/vite/issues/14674), we will need to create a _vite.config.js_ and write a [custom plugin](https://vitejs.dev/guide/api-plugin) to work around this.
-
-In this example we are handling for CSS Module scripts:
-
-
-
-
-
- ```js
- import { defineConfig } from "vite";
- import fs from "node:fs/promises";
- import path from "node:path";
- // 1) import the greenwood plugin and lifecycle helpers
- import { greenwoodPluginStandardCss } from "@greenwood/cli/src/plugins/resource/plugin-standard-css.js";
- import { readAndMergeConfig } from "@greenwood/cli/src/lifecycles/config.js";
- import { initContext } from "@greenwood/cli/src/lifecycles/context.js";
-
- // 2) initialize Greenwood lifecycles
- const config = await readAndMergeConfig();
- const context = await initContext({ config });
- const compilation = { context, config };
-
- // 3) initialize the plugin
- const standardCssResource = greenwoodPluginStandardCss.provider(compilation);
-
- // 4) customize Vite
- function transformConstructableStylesheetsPlugin() {
- return {
- name: "transform-constructable-stylesheets",
- enforce: "pre",
- resolveId: (id, importer) => {
- if (
- // you'll need to configure this `importer` line to the location of your own components
- importer?.indexOf("/src/components/") >= 0 &&
- id.endsWith(".css") &&
- !id.endsWith(".module.css")
- ) {
- // append .type to the end of Constructable Stylesheet file paths so that they are not automatically precessed by Vite's default pipeline
- return path.join(path.dirname(importer), `${id}.type`);
- }
- },
- load: async (id) => {
- if (id.endsWith(".css.type")) {
- const filename = id.slice(0, -5);
- const contents = await fs.readFile(filename, "utf-8");
- const url = new URL(`file://${id.replace(".type", "")}`);
- // "coerce" native constructable stylesheets into inline JS so Vite / Rollup do not complain
- const request = new Request(url, {
- headers: {
- Accept: "text/javascript",
- },
- });
- const response = await standardCssResource.intercept(url, request, new Response(contents));
- const body = await response.text();
-
- return body;
- }
- },
- };
- }
-
- export default defineConfig({
- // 5) add it the plugins option
- plugins: [transformConstructableStylesheetsPlugin()],
- });
- ```
-
-
-
-
-
-Phew, should be all set now.
-
-## Resource Plugins
-
-If you're using one of Greenwood's [resource plugins](/docs/plugins/), you'll need a _vite.config.js_ so we can create a custom transformation plugin that can leverage Greenwood's plugins to automatically handle custom transformations.
-
-For example, if you're using Greenwood's [Raw Plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-raw), you'll need to create a wrapping Vite plugin to handle this transformation.
-
-
-
-
-
- ```js
- import { defineConfig } from "vite";
- import fs from "node:fs/promises";
- import path from 'node:path';
- // 1) import the greenwood plugin and lifecycle helpers
- import { greenwoodPluginImportRaw } from "@greenwood/plugin-import-raw";
- import { readAndMergeConfig } from "@greenwood/cli/src/lifecycles/config.js";
- import { initContext } from "@greenwood/cli/src/lifecycles/context.js";
-
- // 2) initialize Greenwood lifecycles
- const config = await readAndMergeConfig();
- const context = await initContext({ config });
- const compilation = { context, config };
-
- // 3) initialize the plugin
- const rawResource = greenwoodPluginImportRaw()[0].provider(compilation);
-
- // 4) customize Vite
- function transformRawImports() {
- const hint = "?type=raw";
-
- return {
- name: "transform-raw-imports",
- enforce: "pre",
- resolveId: (id, importer) => {
-
- if (
- id.endsWith(hint)
- ) {
- // append .type to the end of .css file paths so they are not automatically precessed by Vite's default CSS pipeline
- return path.join(path.dirname(importer), `${id.slice(0, id.indexOf(hint))}.type${hint}`);
- }
- },
- load: async (id) => {
- if (id.endsWith(hint)) {
- const filename = id.slice(0, id.indexOf(`.type${hint}`));
- const contents = await fs.readFile(filename, "utf-8");
- const response = await rawResource.intercept(new URL(`file://${filename}`), null, new Response(contents));
- const body = await response.text();
-
- return body;
- }
- },
- };
- }
-
- export default defineConfig({
- // 5) add it the plugins option
- plugins: [transformRawImports()],
- });
- ```
-
-
-
-
-
## Content as Data
If you are using any of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), you'll want to configure Storybook to mock the HTTP calls Greenwood's data client makes, and provide the desired response needed based on the API being called.
diff --git a/src/pages/guides/ecosystem/vitest.md b/src/pages/guides/ecosystem/vitest.md
new file mode 100644
index 00000000..4518b83b
--- /dev/null
+++ b/src/pages/guides/ecosystem/vitest.md
@@ -0,0 +1,433 @@
+---
+layout: guides
+order: 6
+tocHeading: 2
+---
+
+# Vitest
+
+[**Vitest**](https://vitest.dev/) is a test runner based on [**Vite**](https://vite.dev/). This guide will give a high level overview of setting up Vitest to test your Web Components and how to integrate any Greenwood plugins you are using as Vite plugins.
+
+> You can see an example [here](https://github.com/ProjectEvergreen/wcc) in the docs/ folder.
+
+## Setup
+
+> At time of writing, this guide was based on Vitest v4.x and Vite v7.x.
+
+First, install Vite and Vitest:
+
+
+
+
+
+ ```shell
+ npm i -D vite vitest
+ ```
+
+ ```shell
+ yarn add vite vitest --save-dev
+ ```
+
+ ```shell
+ pnpm add -D vite vitest
+ ```
+
+
+
+
+
+Next, let's create a _vitest.config.js_ file and configure the location of our test cases:
+
+
+
+
+
+ ```js
+ import { defineConfig } from 'vitest/config';
+
+ export default defineConfig({
+ test: {
+ include: ['./src/**/*.test.ts']
+ },
+ });
+ ```
+
+
+
+
+
+Lastly, let's create some NPM scripts to run your tests. By default, Vitest will run in watch mode which is great for TDD (Test Driven Development).
+
+Below is an example of how to setup NPM scripts for testing:
+
+
+
+
+
+ ```json
+ {
+ "test:docs": "vitest run --coverage",
+ "test:docs:tdd": "vitest"
+ }
+ ```
+
+
+
+
+
+## Browser Testing
+
+The best way to test Web Components is in a browser. For this guide, we will use [**Playwright**](https://playwright.dev/) as a headless browser to run our tests in.
+
+
+
+
+
+ ```shell
+ npm i -D playwright @vitest/browser-playwright
+ ```
+
+ ```shell
+ yarn add playwright @vitest/browser-playwright --save-dev
+ ```
+
+ ```shell
+ pnpm add -D playwright @vitest/browser-playwright
+ ```
+
+
+
+
+
+Then install Playwright:
+
+
+
+
+
+ ```shell
+ $ npx playwright install
+ ```
+
+
+
+
+
+Then in our _vitest.config.js_ file, let's add configuration for Playwright:
+
+
+
+
+
+ ```js
+ import { defineConfig } from 'vitest/config';
+ import { playwright } from '@vitest/browser-playwright';
+
+ export default defineConfig({
+ test: {
+ include: ['./src/**/*.test.ts'],
+ browser: {
+ provider: playwright(),
+ enabled: true,
+ headless: true,
+ instances: [{ browser: 'chromium' }],
+ screenshotFailures: false,
+ },
+ },
+ });
+ ```
+
+
+
+
+
+> Note: For CI environments like GitHub Actions, you'll want to add a step for installing Playwright, including the [`--with-deps` flag](https://playwright.dev/docs/ci):
+>
+> ```shell
+> npx playwright install --with-deps
+> ```
+
+## Usage
+
+You should now be good to start writing your first test! âš¡
+
+
+
+
+
+ ```js
+ export default class Footer extends HTMLElement {
+ connectedCallback() {
+ this.innerHTML = `
+
+ `;
+ }
+ }
+
+ customElements.define("app-footer", Footer);
+ ```
+
+
+
+
+
+
+
+
+
+ ```js
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+ import './footer.tsx';
+
+ describe('Components/Footer', () => {
+ let footer;
+
+ describe('Default Behavior', () => {
+ beforeEach(() => {
+ footer = document.createElement('app-footer');
+
+ document.body.appendChild(footer);
+ });
+
+ it('should not be undefined', () => {
+ expect(footer).not.equal(undefined);
+ expect(footer.querySelectorAll('footer').length).equal(1);
+ });
+
+ it('should have a link for to the home page', () => {
+ const heading = footer.querySelectorAll('h4');
+
+ expect(heading.length).equal(1);
+ expect(heading[0].textContent).equal('Greenwood');
+ });
+
+ it('should have the Greenwood logo image', () => {
+ const logo = footer.querySelectorAll('img');
+
+ expect(logo.length).equal(1);
+ expect(logo[0].getAttribute('src')).equal('/assets/greenwood-logo.webp');
+ });
+ });
+
+ afterEach(() => {
+ footer.remove();
+ footer = undefined;
+ });
+ });
+ ```
+
+
+
+
+
+## Import Attributes
+
+As [Vite does not support Import Attributes](https://github.com/vitejs/vite/issues/14674), you will need to update your _vitest.config.js_ file and write a [custom plugin](https://vitejs.dev/guide/api-plugin) to work around this.
+
+In this example we are handling for CSS Module scripts:
+
+
+
+
+
+ ```js
+ import { defineConfig } from 'vitest/config';
+ import fs from "node:fs/promises";
+ import path from "node:path";
+ // 1) import the greenwood plugin and lifecycle helpers
+ import { greenwoodPluginStandardCss } from "@greenwood/cli/src/plugins/resource/plugin-standard-css.js";
+ import { readAndMergeConfig } from "@greenwood/cli/src/lifecycles/config.js";
+ import { initContext } from "@greenwood/cli/src/lifecycles/context.js";
+
+ // 2) initialize Greenwood lifecycles
+ const config = await readAndMergeConfig();
+ const context = await initContext({ config });
+ const compilation = { context, config };
+
+ // 3) initialize the plugin
+ const standardCssResource = greenwoodPluginStandardCss.provider(compilation);
+
+ // 4) customize Vite
+ function transformConstructableStylesheetsPlugin() {
+ return {
+ name: "transform-constructable-stylesheets",
+ enforce: "pre",
+ resolveId: (id, importer) => {
+ if (
+ // you'll need to configure this `importer` line to the location of your own components
+ importer?.indexOf("/src/components/") >= 0 &&
+ id.endsWith(".css") &&
+ !id.endsWith(".module.css")
+ ) {
+ // append .type to the end of Constructable Stylesheet file paths so that they are not automatically precessed by Vite's default pipeline
+ return path.join(path.dirname(importer), `${id}.type`);
+ }
+ },
+ load: async (id) => {
+ if (id.endsWith(".css.type")) {
+ const filename = id.slice(0, -5);
+ const contents = await fs.readFile(filename, "utf-8");
+ const url = new URL(`file://${id.replace(".type", "")}`);
+ // "coerce" native constructable stylesheets into inline JS so Vite / Rollup do not complain
+ const request = new Request(url, {
+ headers: {
+ Accept: "text/javascript",
+ },
+ });
+ const response = await standardCssResource.intercept(url, request, new Response(contents));
+ const body = await response.text();
+
+ return body;
+ }
+ },
+ };
+ }
+
+ export default defineConfig({
+ test: { /* ... */ },
+
+ // 5) add it the plugins option
+ plugins: [transformConstructableStylesheetsPlugin()],
+ });
+ ```
+
+
+
+
+
+Phew, should be all set now.
+
+## Resource Plugins
+
+If you're using one of Greenwood's [resource plugins](/docs/plugins/), you'll want to update the _vitest.config.js_ file with a plugin that can leverage Greenwood's plugins to automatically handle custom transformations.
+
+For example, if you're using Greenwood's [Raw Plugin](https://github.com/ProjectEvergreen/greenwood/tree/master/packages/plugin-import-raw), you'll need to create a wrapping Vite plugin to handle this transformation.
+
+
+
+
+
+ ```js
+ import { defineConfig } from "vitest/config";
+ import fs from "node:fs/promises";
+ import path from 'node:path';
+ // 1) import the greenwood plugin and lifecycle helpers
+ import { greenwoodPluginImportRaw } from "@greenwood/plugin-import-raw";
+ import { readAndMergeConfig } from "@greenwood/cli/src/lifecycles/config.js";
+ import { initContext } from "@greenwood/cli/src/lifecycles/context.js";
+
+ // 2) initialize Greenwood lifecycles
+ const config = await readAndMergeConfig();
+ const context = await initContext({ config });
+ const compilation = { context, config };
+
+ // 3) initialize the plugin
+ const rawResource = greenwoodPluginImportRaw()[0].provider(compilation);
+
+ // 4) customize Vite
+ function transformRawImports() {
+ const hint = "?type=raw";
+
+ return {
+ name: "transform-raw-imports",
+ enforce: "pre",
+ resolveId: (id, importer) => {
+ if (
+ id.endsWith(hint)
+ ) {
+ // append .type to the end of .css file paths so they are not automatically precessed by Vite's default CSS pipeline
+ return path.join(path.dirname(importer), `${id.slice(0, id.indexOf(hint))}.type${hint}`);
+ }
+ },
+ load: async (id) => {
+ if (id.endsWith(hint)) {
+ const filename = id.slice(0, id.indexOf(`.type${hint}`));
+ const contents = await fs.readFile(filename, "utf-8");
+ const response = await rawResource.intercept(new URL(`file://${filename}`), null, new Response(contents));
+ const body = await response.text();
+
+ return body;
+ }
+ },
+ };
+ }
+
+ export default defineConfig({
+ test: {/* ... */},
+
+ // 5) add it the plugins option
+ plugins: [transformRawImports()],
+ });
+ ```
+
+
+
+
+
+## Content as Data
+
+If you are using any of Greenwood's Content as Data [Client APIs](/docs/content-as-data/data-client/), you'll want to configure Vitest to mock the HTTP calls Greenwood's data client makes, and provide the desired response needed based on the API being called.
+
+We'll also need to use Vitest's [`waitUntil` utility](https://vitest.dev/api/vi.html#vi-waituntil) to handle any usage of `async connectedCallback` in your components.
+
+
+
+
+
+ ```js
+ import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from 'vitest';
+ import pages from '../../../.greenwood/graph.json' with { type: 'json' };
+ import './footer.js';
+
+ describe('Components/Footer', () => {
+ let footer;
+
+ beforeAll(() => {
+ // mock fetch
+ window.fetch = vi.fn(() => {
+ return new Promise((resolve) => {
+ resolve(
+ new Response(JSON.stringify(pages.filter((page) => page.data.collection === 'nav'))),
+ );
+ });
+ });
+ });
+
+ beforeEach(async () => {
+ footer = document.createElement('app-footer');
+
+ document.body.appendChild(footer);
+
+ // to support async connected callback usage by waiting for a particular element to appear in the DOM
+ await vi.waitUntil(() => footer.querySelector('footer'));
+ });
+
+ describe('Default Behavior', () => {
+ it('should not be null', () => {
+ expect(footer).not.equal(undefined);
+ expect(footer.querySelectorAll('footer').length).equal(1);
+ });
+
+ // ...
+ });
+
+ afterEach(() => {
+ footer.remove();
+ footer = undefined;
+ });
+
+ afterAll(() => {
+ vi.clearAllMocks();
+ vi.resetAllMocks();
+ });
+ });
+ ```
+
+
+
+
+
+> To quickly get a "mock" graph to use in your stories, you can run `greenwood build` and copy the _graph.json_ file from the build output directory.