From 2de06101f951e650960271111b6bdee24676c18b Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Nov 2024 08:44:51 +0100 Subject: [PATCH] docs: Updated contribution guide (#2169) Extend contribution guide with more details and pages for - testing, - styling, - and writing stories. Added example code-files. Closes #2062 --- apps/docs/src/.vitepress/config.ts | 9 +- apps/docs/src/development/index.md | 2 +- apps/docs/src/development/packages/nuxt.md | 2 +- apps/docs/src/development/theming.md | 2 +- apps/docs/src/principles/contributing.md | 203 ------------------ .../contributing/example-matrix.png | Bin 0 -> 29454 bytes .../docs/src/principles/contributing/index.md | 127 +++++++++++ .../playwright-test-for-vs-code.png | Bin .../contributing/stories-example.ts | 57 +++++ .../src/principles/contributing/stories.md | 66 ++++++ .../contributing/styling-example.vue | 36 ++++ .../src/principles/contributing/styling.md | 28 +++ .../contributing/testing-example.ct.tsx | 37 ++++ .../src/principles/contributing/testing.md | 65 ++++++ apps/docs/tsconfig.json | 1 + 15 files changed, 428 insertions(+), 207 deletions(-) delete mode 100644 apps/docs/src/principles/contributing.md create mode 100644 apps/docs/src/principles/contributing/example-matrix.png create mode 100644 apps/docs/src/principles/contributing/index.md rename apps/docs/src/principles/{ => contributing}/playwright-test-for-vs-code.png (100%) create mode 100644 apps/docs/src/principles/contributing/stories-example.ts create mode 100644 apps/docs/src/principles/contributing/stories.md create mode 100644 apps/docs/src/principles/contributing/styling-example.vue create mode 100644 apps/docs/src/principles/contributing/styling.md create mode 100644 apps/docs/src/principles/contributing/testing-example.ct.tsx create mode 100644 apps/docs/src/principles/contributing/testing.md diff --git a/apps/docs/src/.vitepress/config.ts b/apps/docs/src/.vitepress/config.ts index cbcebee2ea..80bfa2df63 100644 --- a/apps/docs/src/.vitepress/config.ts +++ b/apps/docs/src/.vitepress/config.ts @@ -177,7 +177,14 @@ export const CONFIG = { }, { text: "Contribution guide", - link: "/contributing", + link: "/", + base: "/principles/contributing", + collapsed: true, + items: [ + { text: "Stories", link: "/stories" }, + { text: "Styling", link: "/styling" }, + { text: "Testing", link: "/testing" }, + ], }, { text: "Technical vision & guidelines", diff --git a/apps/docs/src/development/index.md b/apps/docs/src/development/index.md index 7275d408f4..c48f0e099a 100644 --- a/apps/docs/src/development/index.md +++ b/apps/docs/src/development/index.md @@ -160,7 +160,7 @@ Be careful when overriding styles globally since it will affect **EVERY** compon import BrowsersList from "../.vitepress/components/BrowsersList.vue" -Onyx works best with the following browser versions. Older versions might also work but we can't guarantee full compatibility. +onyx works best with the following browser versions. Older versions might also work but we can't guarantee full compatibility. diff --git a/apps/docs/src/development/packages/nuxt.md b/apps/docs/src/development/packages/nuxt.md index 3e16098796..f839da5689 100644 --- a/apps/docs/src/development/packages/nuxt.md +++ b/apps/docs/src/development/packages/nuxt.md @@ -55,7 +55,7 @@ For all supported themes see: [Theming](/development/theming#themes) ## Integration with @nuxtjs/i18n -Onyx features built in translations and the nuxt module extends on that by offering an out of the box integration with [@nuxtjs/i18n](https://i18n.nuxtjs.org/). +onyx features built in translations and the nuxt module extends on that by offering an out of the box integration with [@nuxtjs/i18n](https://i18n.nuxtjs.org/). If your Nuxt project uses both modules, onyx will automatically detect it and use @nuxtjs/i18n to handle all the translations. This gives you all the bells and whistles of vue-i18n like lazy loading of translations. ### Setup diff --git a/apps/docs/src/development/theming.md b/apps/docs/src/development/theming.md index c7661147db..e0f19ab046 100644 --- a/apps/docs/src/development/theming.md +++ b/apps/docs/src/development/theming.md @@ -4,7 +4,7 @@ import { data } from './theming.data'; -Onyx supports a dark and a light theme as well as multiple built-in color themes. The options how to set up the theme for your application are described on this page. +onyx supports a dark and a light theme as well as multiple built-in color themes. The options how to set up the theme for your application are described on this page. To learn more about the theming concept of onyx, take a look at our [colors documentation](/basics/colors.html) diff --git a/apps/docs/src/principles/contributing.md b/apps/docs/src/principles/contributing.md deleted file mode 100644 index 371970f007..0000000000 --- a/apps/docs/src/principles/contributing.md +++ /dev/null @@ -1,203 +0,0 @@ - - -# Contribution Guide - -When contributing to onyx, please respect the [Schwarz IT Code of Conduct](https://github.com/SchwarzIT/.github/blob/main/CODE_OF_CONDUCT.md) and our [technical vision/guidelines](/principles/technical-vision). - -::: info Target audience -This document is directed at people that are developing **for** onyx. -It gives tips and guidelines on what should or must be considered when working with onyx. -::: - -## Prerequisites / Technical setup - -1. Install [Node.js](https://nodejs.org/en) version **{{ nodeVersion }}**.
- We recommend using [fnm](https://github.com/Schniz/fnm) for managing your node versions which will automatically use the correct node version when working in the onyx repo. - -2. Install the [pnpm](https://pnpm.io/) package manager with a compatible version to `^{{ packageManager.replace("pnpm@", "") }}` - -## Recommended IDE Setup - -We follow the official Vue recommendation for IDE setup which is [VSCode](https://code.visualstudio.com) with the [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) extension. - -## Set up the monorepo - -In order to work in the onyx monorepo, you need to install the dependencies by running: - -```sh -pnpm install -``` - -## Package scripts - -Depending on which package you want to contribute, there are different scripts available. A full list can be found in the `package.json` file of the corresponding package. -Here is a list of the most commonly used scripts. - -::: code-group - -```sh [Monorepo root] -pnpm install # install all dependencies -pnpm lint:fix:all # lint and fix all packages -pnpm format:all # format all files -``` - -```sh [packages/sit-onyx] -pnpm dev # run Storybook in dev mode when developing components -pnpm build # build all onyx components -pnpm test # run unit tests -pnpm test:components # run Playwright component tests -``` - -```sh [apps/docs] -pnpm dev # run docs in dev mode -``` - -::: - -## Creating a Pull Request - -Pull Requests are very welcome! -Please consider our [technical guidelines](/principles/technical-vision) when contributing to onyx. - -1. [Create a fork](https://github.com/SchwarzIT/onyx/fork) to commit and push your changes to -2. When your changes affect the user and need to be released, [add a changeset](https://github.com/SchwarzIT/onyx/blob/main/.changeset/README.md). -3. Then [create a PR](https://github.com/SchwarzIT/onyx/compare) to merge your changes back into our repository. - -When your PR gets approved, you can expect a pre-release immediately after it is merged. Production releases are planned to be published every 2 weeks after the release of version 1.0.0. - -::: tip Screenshot tests -Component screenshot tests using Playwright will only be performed in our [GitHub workflows](https://github.com/SchwarzIT/onyx/actions) to ensure consistency of the resulting images which vary on different operating systems. - -If you made visual changes to components, you can use [this Workflow](https://github.com/SchwarzIT/onyx/actions/workflows/playwright-screenshots.yml) to update the screenshots on your branch. -::: - -## Developing Components - -Below is the basic code for a new onyx component. -For more information about the density feature of onyx, visit our [density docs](/development/density). - -::: code-group - -```vue [OnyxMyComponent.vue] - - - - - -``` - -```ts [types.ts] -import type { DensityProp } from "../../styles/density"; - -export type OnyxMyComponentProps = DensityProp & { - // component props... -}; -``` - -::: - -### Include CSS Layers - -As you can see in the code snippet above, we make use of [Cascade Layers](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers) to simplify how different style sources cascade. -By putting all our styles into layers they can also be easily overwritten by users. - -Therefore, you must make use of the `@include layers.component()` mixin as shown above. -This will also normalize styles for this class and it's children. The scoped rules will be applied in the `onyx.component` layer. - -### Custom density styles - -For most cases, the [CSS variables for densities](/variables/spacings) will already support that the component adjusts based on the current density. -In exceptional cases it might be required to apply special styles for the densities which can not be covered with this. - -You can use our density mixin in this case: - -```vue - -``` - -### Testing - -We require every component to be thoroughly tested. -This project uses [Playwright](https://playwright.dev/) and [Vitest](https://vitest.dev/) for testing. - -#### Component (UI) tests - -Generally [playwright component tests](https://playwright.dev/docs/test-components) (kept in `.ct.tsx`-files) suffice to test a component. - -Component tests must include screenshot tests to ensure that any style changes happen intentionally and can be approved by our UX. -To easily generate and test screenshots for all main component states the `executeMatrixScreenshotTest` (in file `/packages/sit-onyx/src/playwright/screenshots.tsx`) utility is to be used. - -In our monorepo playwright/component tests are run non-interactively using the `pnpm test:components` script. - -To use Playwright interactively run `pnpm dlx playwright test --ui` (add the `--headed` flag to open the test browsers) in the package directory. - -#### CI - -To investigate failing playwright tests from the CI locally: - -1. Download the `html-report--attempt-x` artifact **_after_** the pipeline has finished. -2. Unzip the archive -3. `cd` into the package you want to see the report for -4. Run `pnpm dlx playwright show-report` - -##### VSCode - -We recommend the [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) extension for running component tests in development. -It allows to build and run specific tests interactively out of the IDE (see annotation `3` in screenshot beneath). -If you encounter any issues please make sure - -- to run the `pnpm build` script at least once for the pacakge -- to only select the playwright config file for the current package your are testing -- to run `Run global teardown`, `Close All Browsers` and `Clear Cache` - -You find the playwright VSCode extension settings (see annotation `2` in screenshot beneath) in the `Testing` section of VSCode (annotation `1`). - -![Playwright for VSCode overview](playwright-test-for-vs-code.png) - -#### Unit tests - -For self-contained logic, excluding Vue components, unit tests (kept in `.spec.ts`-files) can be written using [Vitest](https://vitest.dev/). - -In our monorepo unit tests are run using the `pnpm test` script. diff --git a/apps/docs/src/principles/contributing/example-matrix.png b/apps/docs/src/principles/contributing/example-matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..37731d65ce6959695ba60802cee974a6e0615c41 GIT binary patch literal 29454 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 +import { packageManager } from "../../../../../package.json"; +import nodeVersion from "../../../../../.node-version?raw"; + + +# Contribution Guide + +When contributing to onyx, please respect the [Schwarz IT Code of Conduct](https://github.com/SchwarzIT/.github/blob/main/CODE_OF_CONDUCT.md) and our [technical vision/guidelines](/principles/technical-vision). + +::: info Target audience +This document is directed at people that are developing **for** onyx. +It gives tips and guidelines on what should or must be considered when working with onyx. +::: + +## Prerequisites / Technical setup + +1. Install [Node.js](https://nodejs.org/en) version **{{ nodeVersion }}**.
+ We recommend using [fnm](https://github.com/Schniz/fnm) for managing your node versions which will automatically use the correct node version when working in the onyx repo. + +2. Install the [pnpm](https://pnpm.io/) package manager with a compatible version to `^{{ packageManager.replace("pnpm@", "") }}` + +## Recommended IDE Setup + +We follow the official Vue recommendation for IDE setup which is [VSCode](https://code.visualstudio.com) with the [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) extension. + +## Set up the monorepo + +In order to work in the onyx monorepo, you need to install the dependencies by running: + +```sh +pnpm install +``` + +## Package scripts + +Depending on which package you want to contribute, there are different scripts available. A full list can be found in the `package.json` file of the corresponding package. +Here is a list of the most commonly used scripts. + +::: code-group + +```sh [Monorepo root] +pnpm install # install all dependencies +pnpm lint:fix:all # lint and fix all packages +pnpm format:all # format all files +``` + +```sh [packages/sit-onyx] +pnpm dev # run Storybook in dev mode when developing components +pnpm build # build all onyx components +pnpm test # run unit tests +pnpm test:components # run Playwright component tests +``` + +```sh [apps/docs] +pnpm dev # run docs in dev mode +``` + +::: + +## Creating a Pull Request + +Pull Requests are very welcome! +Please consider our [technical guidelines](/principles/technical-vision) when contributing to onyx. + +1. [Create a fork](https://github.com/SchwarzIT/onyx/fork) to commit and push your changes to +2. When your changes affect the user and need to be released, [add a changeset](https://github.com/SchwarzIT/onyx/blob/main/.changeset/README.md). +3. Then [create a PR](https://github.com/SchwarzIT/onyx/compare) to merge your changes back into our repository. + +When your PR gets approved, you can expect a pre-release immediately after it is merged. Production releases are planned to be published every 2 weeks after the release of version 1.0.0. + +::: tip Screenshot tests +Component screenshot tests using Playwright will only be performed in our [GitHub workflows](https://github.com/SchwarzIT/onyx/actions) to ensure consistency of the resulting images which vary on different operating systems. + +If you made visual changes to components, you can use [this Workflow](https://github.com/SchwarzIT/onyx/actions/workflows/playwright-screenshots.yml) to update the screenshots on your branch. +::: + +## Developing Components + +Below is the basic code for a new onyx component. + +Find more details about: + +- 📚 [Writing stories](./stories.md) +- 🎨 [Creating styles](./styling.md) +- 🎭 [Implementing tests](./testing.md) + +::: code-group + +```vue [OnyxComponent.vue] + + + + + +``` + +```ts [types.ts] +import type { DensityProp } from "../../styles/density"; + +export type OnyxComponentProps = DensityProp & { + // component props... +}; +``` + +<<< ./testing-example.ct.tsx [OnyxComponent.ct.tsx] + +<<< ./stories-example.ts [OnyxComponent.stories.ts] +::: diff --git a/apps/docs/src/principles/playwright-test-for-vs-code.png b/apps/docs/src/principles/contributing/playwright-test-for-vs-code.png similarity index 100% rename from apps/docs/src/principles/playwright-test-for-vs-code.png rename to apps/docs/src/principles/contributing/playwright-test-for-vs-code.png diff --git a/apps/docs/src/principles/contributing/stories-example.ts b/apps/docs/src/principles/contributing/stories-example.ts new file mode 100644 index 0000000000..451c9b69e6 --- /dev/null +++ b/apps/docs/src/principles/contributing/stories-example.ts @@ -0,0 +1,57 @@ +import { withNativeEventLogging } from "@sit-onyx/storybook-utils"; +import type { Meta, StoryObj } from "@storybook/vue3"; +import { h } from "vue"; +import OnyxComponent from "./OnyxComponent.vue"; +import OnyxOtherComponent from "./OnyxOtherComponent.vue"; + +// #region meta +/** + * TODO: Add a general description for this component + */ +const meta: Meta = { + title: "Category/Subcategory/Component", + component: OnyxComponent, + argTypes: { + propName: { control: { type: "text" } }, // TODO: remove or add custom overwrites for properties + ...withNativeEventLogging(["onInput", "onChange", "onFocusin", "onFocusout"]), + }, +}; +export default meta; +// #endregion meta + +// #region story +type Story = StoryObj; + +/** + * TODO: Add a description for this Story + */ +export const Default = { + args: { + propName: "Value", + }, +} satisfies Story; + +/** + * TODO: Add a description for this Story + */ +export const Other = { + args: { + ...Default.args, + otherPropName: true, + }, +} satisfies Story; +// #endregion story + +// #region slot +export const WithSlotContent = { + args: { + ...Default.args, + slotName: () => [ + // The `h` function takes the following form: + // h(componentOrHtmlTag, propertiesAndAttributes, innerTextOrSlotContent) + h(OnyxOtherComponent, { label: "Item 1", href: "/" }), + h("a", { target: "_blank", href: "https://onyx.schwarz" }, "onyx"), + ], + }, +} satisfies Story; +// #endregion slot diff --git a/apps/docs/src/principles/contributing/stories.md b/apps/docs/src/principles/contributing/stories.md new file mode 100644 index 0000000000..2b134a6f06 --- /dev/null +++ b/apps/docs/src/principles/contributing/stories.md @@ -0,0 +1,66 @@ +# Storybook + +For development and API documentation we make use of [Storybook](https://storybook.js.org/). + +A **Story** is a specific scenario or state of a component that showcases its functionality, appearance, or behavior. It helps developers and designers visualize and test different variations of a component in isolation. + +All stories for a single component are kept in a neighboring `.stories.ts` file. + +::: info +The next sections describe the content of a `*.stories.ts` file in more detail. +Feel free to [skip to the end](#tl-dr) to see the complete example and a _TL;DR_. +::: + +## Meta section + +First off we have the [`Meta`](https://storybook.js.org/docs/writing-stories/typescript#typing-stories-with-meta-and-storyobj) object. +Here the component is described and documented. +Using tsdoc, a general description is added above the `meta` constant. + +- The `title` attribute defines where in the storybook the component is placed and what its title is. Categories and the title are separated by slashes ("`/`"). +- `component` sets the Component that will be used for all Stories in this file. The documentation for all properties will be extracted from its types and their [tsdoc](https://tsdoc.org/) block. +- With the [`argTypes`](https://storybook.js.org/docs/api/arg-types#manually-specifying-argtypes) attribute the components property documentation can be extended and overwritten. + Usually this is not necessary as Storybook infers the properties, their types and description from the component types. + However as shown in the example below, it can be useful to overwrite Storybooks input element using the `control` property. +- To highlight useful native DOM events, the `withNativeEventLogging` utility can be used. It enables logging and documentation for the defined DOM events. + +Make sure to export `meta` as default so Storybook can find the Story. + +<<< ./stories-example.ts#meta + +## Actual Stories + +Every non-default export represents a `Story`, where the export name is also the name of the story. +The first story is usually called `Default`. + +- The description of the story object is done via [tsdoc](https://tsdoc.org/). +- We use `StoryObj` to enable typed code completion. +- A story must define `args` which represents the props that are passed to component. + +To add further stories, add more named exports. +Consider reusing the `Default` stories args. + +<<< ./stories-example.ts#story + +## Slots + +Storybook treats Slots like props, therefore they are also configured via the `args`. +A slot and its content are defined using [Render Functions](https://vuejs.org/guide/extras/render-function.html). +A Render Functions must return a [VNode](https://vuejs.org/api/render-function.html#h) or an array of VNodes. + +<<< ./stories-example.ts#slot + +## TL;DR + +::: tip TL;DR + +- Descriptions and types are extracted by Storybook from the components types and [tsdoc](https://tsdoc.org/) +- Meta details are configured via a default export with type `Meta` +- Stories are defined as Named Exports with type `StoryObj` +- Props and slot content for a Story are passed with the `args` property +- Slots are defined using [Render Functions](https://vuejs.org/guide/extras/render-function.html) + ::: + +Complete example: + +<<< ./stories-example.ts diff --git a/apps/docs/src/principles/contributing/styling-example.vue b/apps/docs/src/principles/contributing/styling-example.vue new file mode 100644 index 0000000000..ff318cf247 --- /dev/null +++ b/apps/docs/src/principles/contributing/styling-example.vue @@ -0,0 +1,36 @@ + diff --git a/apps/docs/src/principles/contributing/styling.md b/apps/docs/src/principles/contributing/styling.md new file mode 100644 index 0000000000..d552d48af2 --- /dev/null +++ b/apps/docs/src/principles/contributing/styling.md @@ -0,0 +1,28 @@ +# Styling + +We use [SCSS](https://sass-lang.com/documentation/syntax/#scss) as CSS preprocessor. +Any valid CSS is also valid SCSS, that should make it easy to get started. + +All our design variables are provided as [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). +An overview can be found [here](/variables/introduction.html). + +For a general overview of our (S)CSS guidelines refer to our [CSS Principles](/principles/technical-vision.html#css). + +## CSS Layers + +We make use of [Cascade Layers](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers) to simplify how different style sources are combined. +By putting all our styles into layers they can also be easily overwritten by users. + +Therefore, the `@include layers.component()` mixin must be used. +It will put the contained rules into the `onyx.component` layer and normalize stylings. + +<<< ./styling-example.vue#screenshot-testing {scss} + +## Custom density styles + +For most cases, the [CSS variables for densities](/variables/spacings) will already support that the component adjusts its spacings based on the current density. +In exceptional cases it might be necessary to apply special style rules for the densities, for which the default spacings do not work. + +You can use our density mixins in this case: + +<<< ./styling-example.vue#densities {scss} diff --git a/apps/docs/src/principles/contributing/testing-example.ct.tsx b/apps/docs/src/principles/contributing/testing-example.ct.tsx new file mode 100644 index 0000000000..1821944b2c --- /dev/null +++ b/apps/docs/src/principles/contributing/testing-example.ct.tsx @@ -0,0 +1,37 @@ +import { expect, test } from "../../playwright/a11y"; +import { executeMatrixScreenshotTest } from "../../playwright/screenshots"; +import OnyxComponent from "./OnyxComponent.vue"; + +// #region executeMatrixScreenshotTest +test.describe("Screenshot tests", () => { + executeMatrixScreenshotTest({ + name: "OnyxComponent (densities)", + rows: ["default", "hover", "active", "focus-visible", "skeleton"], + columns: ["compact", "default", "cozy"], + beforeScreenshot: async (component, page, column, row) => { + /** + * TODO: Prepare the component before the screenshot + * e.g.: + */ + if (row === "hover") await component.hover(); + if (row === "focus-visible") await page.keyboard.press("Tab"); + if (row === "active") await page.mouse.down(); + }, + component: (column, row) => ( + // TODO: Set the props based on the given row and column + + ), + }); +}); +// #endregion executeMatrixScreenshotTest + +// #region toHaveScreenshot +test("should show hover effect", async ({ mount }) => { + // ARRANGE + const component = await mount(); + await component.hover(); + + // ASSERT + await expect(component).toHaveScreenshot("hover-effect.png"); +}); +// #endregion toHaveScreenshot diff --git a/apps/docs/src/principles/contributing/testing.md b/apps/docs/src/principles/contributing/testing.md new file mode 100644 index 0000000000..c12cbf5c57 --- /dev/null +++ b/apps/docs/src/principles/contributing/testing.md @@ -0,0 +1,65 @@ +# Testing + +We require every component to be thoroughly tested. +**onyx** uses [Playwright](https://playwright.dev/) and [Vitest](https://vitest.dev/) for testing. + +## Component tests + +Generally [playwright component tests](https://playwright.dev/docs/test-components) (kept in `.ct.tsx`-files) suffice to test a component. + +Component tests must include screenshot tests to ensure that any style changes happen intentionally and can be approved by our UX. +To easily generate and test screenshots for all main states the `executeMatrixScreenshotTest` utility is to be used. + +<<< ./testing-example.ct.tsx#executeMatrixScreenshotTest + +The utility creates a screenshot for every combination of `rows` and `columns`. +These are then combined in into grid and saves a single screenshot of it. +Here is an example for the OnyxButton: + +![Example of a screenshot matrix for the OnyxButton](./example-matrix.png) + +::: warning +Choose the columns and rows carefully, as the number of combination grows quadratically. It might be preferable to create a new screenshot matrix instead of adding more columns/rows. +::: + +Accessibility tests (using [axe-core](https://github.com/dequelabs/axe-core)) are also run for every combination. + +For standalone tests or more complicated setups, [`toHaveScreenshot`](https://playwright.dev/docs/test-snapshots) can be used directly: + +<<< ./testing-example.ct.tsx#toHaveScreenshot + +### Development + +In our monorepo component tests are run non-interactively using the `pnpm test:components` script. + +To use Playwright interactively run `pnpm exec playwright test --ui` (add the `--headed` flag to open the see the- browsers) in the package directory. + +### CI + +To investigate failing playwright tests from the CI locally: +You can run `pnpm gh:show-report` in the root, which will + +1. Download the `html-report--attempt-x` artifact **_after_** the pipeline has finished. +2. Unzip the archive +3. `cd` into the package you want to see the report for +4. Run `pnpm dlx playwright show-report` + +### VSCode + +We highly recommend to use the [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) extension for running component tests in development. +It allows to build and run specific tests interactively, directly from the IDE (see annotation `3` in screenshot beneath). +If you encounter any issues please make sure + +- to run the `pnpm build` script at least once for the package +- to only select the playwright config file for the current package your are testing +- to run `Run global teardown`, `Close All Browsers` and `Clear Cache` + +You find the playwright VSCode extension settings (see annotation `2` in screenshot beneath) in the `Testing` section of VSCode (annotation `1`). + +![Playwright for VSCode overview](./playwright-test-for-vs-code.png) + +## Unit tests + +For self-contained logic, excluding Vue components, unit tests (kept in `.spec.ts`-files) can be written using [Vitest](https://vitest.dev/). + +In our monorepo unit tests are run using the `pnpm test` script. diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index b7622dd119..447c4c8fe6 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "tests/**/*", "src/.vitepress/**/*", "vite.config.ts"], + "exclude": ["**/*-example.*"], "compilerOptions": { "moduleResolution": "Bundler", "baseUrl": "."