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.