From 6d790babaa33bd2e096d7c7b21e631414a42b1da Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sat, 5 Oct 2024 13:59:18 -0400 Subject: [PATCH 1/4] feat(qwik-testing-library): add documentation for the new Qwik Testing Library framework --- docs/qwik-testing-library/api.mdx | 197 ++++++++++++++++++++++++++ docs/qwik-testing-library/example.mdx | 101 +++++++++++++ docs/qwik-testing-library/faq.mdx | 34 +++++ docs/qwik-testing-library/intro.mdx | 41 ++++++ docs/qwik-testing-library/setup.mdx | 159 +++++++++++++++++++++ sidebars.js | 9 ++ 6 files changed, 541 insertions(+) create mode 100644 docs/qwik-testing-library/api.mdx create mode 100644 docs/qwik-testing-library/example.mdx create mode 100644 docs/qwik-testing-library/faq.mdx create mode 100644 docs/qwik-testing-library/intro.mdx create mode 100644 docs/qwik-testing-library/setup.mdx diff --git a/docs/qwik-testing-library/api.mdx b/docs/qwik-testing-library/api.mdx new file mode 100644 index 000000000..43b655333 --- /dev/null +++ b/docs/qwik-testing-library/api.mdx @@ -0,0 +1,197 @@ +--- +id: api +title: API +sidebar_label: API +--- + +`@noma.to/qwik-testing-library` re-exports everything from +[`@testing-library/dom`][@testing-library/dom], as well as: + +- [`render`](#render) +- [`cleanup`](#cleanup) + +[@testing-library/dom]: ../dom-testing-library/api.mdx + +## `render` + +Render your component to the DOM with Qwik. By default, the component will be +rendered into a `` appended to `document.body`. + +```tsx +import {render} from '@noma.to/qwik-testing-library' +import {MyComponent} from './MyComponent' + +const result = await render(, renderOptions) +``` + +### Render Options + +You may also customize how Qwik Testing Library renders your component. Most of +the time, you shouldn't need to modify these options. + +| Option | Description | Default | +| ------------- | --------------------------------------------------- | -------------------------------- | +| `container` | The container in which the component is rendered. | `document.createElement('host')` | +| `baseElement` | The base element for queries and [`debug`](#debug). | `document.body` | +| `queries` | [Custom Queries][custom-queries]. | N/A | +| `wrapper` | The wrapper to provide a context to the component. | N/A | + +[custom-queries]: ../dom-testing-library/api-custom-queries.mdx + +#### `wrapper` + +You can wrap your component into a wrapper to provide a context and other +functionalities needed by the component under test. + +```tsx +import {render} from '@noma.to/qwik-testing-library' +import {QwikCityMockProvider} from '@builder.io/qwik-city' + +await render(, {wrapper: QwikCityMockProvider}) +``` + +### Render Results + +| Result | Description | +| ----------------------------- | ---------------------------------------------------------- | +| [`baseElement`](#baseelement) | The base DOM element used for queries. | +| [`container`](#container) | The DOM element the component is mounted to. | +| [`asFragment`](#asFragment) | Convert the DOM element to a `DocumentFragment`. | +| [`debug`](#debug) | Log elements using [`prettyDOM`][pretty-dom]. | +| [`unmount`](#unmount) | Unmount and destroy the component. | +| [`...queries`](#queries) | [Query functions][query-functions] bound to `baseElement`. | + +[pretty-dom]: ../dom-testing-library/api-debugging.mdx#prettydom +[query-functions]: ../queries/about.mdx + +#### `baseElement` + +The base DOM element that queries are bound to. Corresponds to +`renderOptions.baseElement`. If you do not use `renderOptions.baseElement`, this +will be `document.body`. + +#### `container` + +The DOM element the component is mounted in. Corresponds to +`renderOptions.container`. If you do not use `renderOptions.container`, this +will be `document.createElement('host')`. In general, avoid using `container` +directly to query for elements; use [testing-library queries][query-functions] +instead. + +#### `asFragment` + +Returns a `DocumentFragment` of your rendered component. This can be useful if +you need to avoid live bindings and see how your component reacts to events. + +```tsx +import {component$} from '@builder.io/qwik'; +import {render} from '@testing-library/react'; +import {userEvent} from "@testing-library/user-event"; + +const TestComponent = component$(() => { + const count = useSignal(0); + + return ( + + ) +}); + +const {getByText, asFragment} = await render() +const firstRender = asFragment() + +userEvent.click(getByText(/Click to increase/)) + +// This will snapshot only the difference between the first render, and the +// state of the DOM after the click event. +// See https://github.com/jest-community/snapshot-diff +expect(firstRender).toMatchDiffSnapshot(asFragment()) +``` + +#### `debug` + +Log the `baseElement` or a given element using [`prettyDOM`][pretty-dom]. + +:::tip + +If your `baseElement` is the default `document.body`, we recommend using +[`screen.debug`][screen-debug]. + +::: + +```tsx +import {render, screen} from '@noma.to/qwik-testing-library' + +const {debug} = await render() + +const button = screen.getByRole('button') + +// log `document.body` +screen.debug() + +// log your custom the `baseElement` +debug() + +// log a specific element +screen.debug(button) +debug(button) +``` + +[screen-debug]: ../dom-testing-library/api-debugging.mdx#screendebug + +#### `unmount` + +Unmount and destroy the Qwik component. + +```tsx +const {unmount} = await render() + +unmount() +``` + +#### Queries + +[Query functions][query-functions] bound to the [`baseElement`](#baseelement). +If you passed [custom queries][custom-queries] to `render`, those will be +available instead of the default queries. + +:::tip + +If your [`baseElement`](#baseelement) is the default `document.body`, we +recommend using [`screen`][screen] rather than bound queries. + +::: + +```tsx +import {render, screen} from '@noma.to/qwik-testing-library' + +const {getByRole} = await render() + +// query `document.body` +const button = screen.getByRole('button') + +// query using a custom `target` or `baseElement` +const button = getByRole('button') +``` + +[screen]: ../queries/about.mdx#screen + +## `cleanup` + +Destroy all components and remove any elements added to `document.body` or +`renderOptions.baseElement`. + +```tsx +import {render, cleanup} from '@noma.to/qwik-testing-library' + +// Default: runs after each test +afterEach(() => { + cleanup() +}) + +await render() + +// Called manually for more control +cleanup() +``` diff --git a/docs/qwik-testing-library/example.mdx b/docs/qwik-testing-library/example.mdx new file mode 100644 index 000000000..685e413ac --- /dev/null +++ b/docs/qwik-testing-library/example.mdx @@ -0,0 +1,101 @@ +--- +id: example +title: Example +sidebar_label: Example +--- + +Below are some examples of how to use the Qwik Testing Library to tests your +Qwik components. + +You can also learn more about the [**queries**][tl-queries-docs] and [**user +events**][tl-user-events-docs] to help you write your tests. + +[tl-queries-docs]: ../queries/about.mdx +[tl-user-events-docs]: ../user-event/intro.mdx + +## Qwikstart + +This is a minimal setup to get you started, with line-by-line explanations. + +```tsx title="counter.spec.tsx" +// import qwik-testing methods +import {screen, render, waitFor} from '@noma.to/qwik-testing-library' +// import the userEvent methods to interact with the DOM +import {userEvent} from '@testing-library/user-event' +// import the component to be tested +import {Counter} from './counter' + +// describe the test suite +describe('', () => { + // describe the test case + it('should increment the counter', async () => { + // render the component into the DOM + await render() + + // retrieve the 'increment count' button + const incrementBtn = screen.getByRole('button', {name: /increment count/}) + // click the button twice + await userEvent.click(incrementBtn) + await userEvent.click(incrementBtn) + + // assert that the counter is now 2 + await waitFor(() => expect(screen.getByText(/count:2/)).toBeInTheDocument()) + }) +}) +``` + +## Qwik City - `server$` calls + +If one of your Qwik components uses `server$` calls, your tests might fail with +a rather cryptic message (e.g. +`QWIK ERROR __vite_ssr_import_0__.myServerFunctionQrl is not a function` or +`QWIK ERROR Failed to parse URL from ?qfunc=DNpotUma33o`). + +We're happy to discuss it on [Discord][discord], but we consider this failure to +be a good thing: your components should be tested in isolation, so you will be +forced to mock your server functions. + +Here is an example of how to test a component that uses `server$` calls: + +```ts title="~/server/blog-post.ts" +import {server$} from '@builder.io/qwik-city' +import {BlogPost} from '~/lib/blog-post' + +export const getLatestPosts$ = server$(function (): Promise { + // get the latest posts + return Promise.resolve([]) +}) +``` + +```tsx title="~/components/latest-post-list.tsx" +import {render, screen, waitFor} from '@noma.to/qwik-testing-library' +import {LatestPostList} from './latest-post-list' + +vi.mock('~/server/blog-posts', () => ({ + // the mocked function should end with `Qrl` instead of `$` + getLatestPostsQrl: () => { + return Promise.resolve([ + {id: 'post-1', title: 'Post 1'}, + {id: 'post-2', title: 'Post 2'}, + ]) + }, +})) + +describe('', () => { + it('should render the latest posts', async () => { + await render() + + await waitFor(() => + expect(screen.queryAllByRole('listitem')).toHaveLength(2), + ) + }) +}) +``` + +Notice how the mocked function is ending with `Qrl` instead of `$`, despite +being named as `getLatestPosts$`. This is caused by the Qwik optimizer renaming +it to `Qrl`. So, we need to mock the `Qrl` function instead of the original `$` +one. + +If your `server$` function doesn't end with `$`, the Qwik optimizer might not +rename it to `Qrl`. diff --git a/docs/qwik-testing-library/faq.mdx b/docs/qwik-testing-library/faq.mdx new file mode 100644 index 000000000..5f380e025 --- /dev/null +++ b/docs/qwik-testing-library/faq.mdx @@ -0,0 +1,34 @@ +--- +id: faq +title: FAQ +sidebar_label: FAQ +--- + +- [How do I test file upload?](#how-do-i-test-file-upload) + +--- + +## How do I test file upload? + +Use the [upload][] utility from `@testing-library/user-event`. It works well in +both [jsdom][] and [happy-dom][]. + +```tsx +test('upload file', async () => { + const user = userEvent.setup() + + await render() + const file = new File(['hello'], 'hello.png', {type: 'image/png'}) + const input = screen.getByLabelText(/upload file/i) + + await user.upload(input, file) + + expect(input.files[0]).toBe(file) + expect(input.files.item(0)).toBe(file) + expect(input.files).toHaveLength(1) +}) +``` + +[upload]: ../user-event/api-utility.mdx#upload +[jsdom]: https://github.com/jsdom/jsdom +[happy-dom]: https://github.com/capricorn86/happy-dom diff --git a/docs/qwik-testing-library/intro.mdx b/docs/qwik-testing-library/intro.mdx new file mode 100644 index 000000000..f370c744e --- /dev/null +++ b/docs/qwik-testing-library/intro.mdx @@ -0,0 +1,41 @@ +--- +id: intro +title: Intro +sidebar_label: Introduction +--- + +[Qwik Testing Library on GitHub][gh] + +[gh]: https://github.com/ianlet/qwik-testing-library + +```bash npm2yarn +npm run qwik add testing-library +``` + +> This library is built on top of [`dom-testing-library`][dom-testing-library] +> which is where most of the logic behind the queries is. + +[dom-testing-library]: ../dom-testing-library/intro.mdx + +## The Problem + +You want to write maintainable tests for your [Qwik][qwik] components. + +[qwik]: https://qwik.dev/ + +## This Solution + +The Qwik Testing Library is a lightweight library for testing Qwik components. +It provides functions on top of `qwik` and `@testing-library/dom` so you can +mount Qwik components and query their rendered output in the DOM. Its primary +guiding principle is: + +> [The more your tests resemble the way your software is used, the more +> confidence they can give you.][guiding-principle] + +[guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 + +**What this library is not**: + +1. A test runner or framework. +2. Specific to a testing framework. diff --git a/docs/qwik-testing-library/setup.mdx b/docs/qwik-testing-library/setup.mdx new file mode 100644 index 000000000..e1499089d --- /dev/null +++ b/docs/qwik-testing-library/setup.mdx @@ -0,0 +1,159 @@ +--- +id: setup +title: Setup +sidebar_label: Setup +--- + +The easiest way to get started with Qwik Testing Library is to use the Qwik CLI +that comes with your Qwik project to automatically set it up for you: + +```bash npm2yarn +npm run qwik add testing-library +``` + +If you prefer to set it up manually or want to understand what the setup script +does, read along... + +## Manual Setup + +This module is distributed via [npm][npm] which is bundled with [node][node] and +should be installed as one of your project's `devDependencies`: + +```bash npm2yarn +npm install --save-dev @noma.to/qwik-testing-library +``` + +This library supports `qwik` versions `1.7.2` and above. + +You may also be interested in installing `@testing-library/jest-dom` and +`@testing-library/user-event` so you can use [the custom jest +matchers][jest-dom] and [the user event library][user-event] to test +interactions with the DOM. + +```bash npm2yarn +npm install --save-dev @testing-library/jest-dom @testing-library/user-event +``` + +Finally, we need a DOM environment to run the tests in. This library was tested +(for now) only with `jsdom` so we recommend using it: + +```bash npm2yarn +npm install --save-dev jsdom +``` + +[npm]: https://www.npmjs.com/ +[node]: https://nodejs.org +[jest-dom]: https://github.com/testing-library/jest-dom +[user-event]: https://github.com/testing-library/user-event + +## Vitest Configuration + +We recommend using `@noma.to/qwik-testing-library` with [Vitest][] as your test +runner. + +If you haven't done so already, add vitest to your project using Qwik CLI: + +```bash npm2yarn +npm run qwik add vitest +``` + +After that, we need to configure Vitest so it can run your tests. For this, +create a _separate_ `vitest.config.ts` so you don't have to modify your +project's `vite.config.ts`: + +```ts title="vitest.config.ts" +import {defineConfig, mergeConfig} from 'vitest/config' +import viteConfig from './vite.config' + +export default defineConfig(configEnv => + mergeConfig( + viteConfig(configEnv), + defineConfig({ + // qwik-testing-library needs to consider your project as a Qwik lib + // if it's already a Qwik lib, you can remove this section + build: { + target: 'es2020', + lib: { + entry: './src/index.ts', + formats: ['es', 'cjs'], + fileName: (format, entryName) => + `${entryName}.qwik.${format === 'es' ? 'mjs' : 'cjs'}`, + }, + }, + // configure your test environment + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + globals: true, + }, + }), + ), +) +``` + +For now, `qwik-testing-library` needs to consider your project as a lib. Hence, +the `build.lib` section in the config above. + +As the build will try to use `./src/index.ts` as the entry point, we need to +create it: + +```ts title="src/index.ts" +/** + * DO NOT DELETE THIS FILE + * + * This entrypoint is needed by @noma.to/qwik-testing-library to run your tests + */ +``` + +Then, create the `vitest.setup.ts` file: + +```ts title="vitest.setup.ts" +import {afterEach} from 'vitest' +import '@testing-library/jest-dom/vitest' + +// This has to run before qdev.ts loads. `beforeAll` is too late +globalThis.qTest = false // Forces Qwik to run as if it was in a Browser +globalThis.qRuntimeQrl = true +globalThis.qDev = true +globalThis.qInspector = false + +afterEach(async () => { + const {cleanup} = await import('@noma.to/qwik-testing-library') + cleanup() +}) +``` + +This setup will make sure that Qwik is properly configured and that everything +gets cleaned after each test. + +Additionally, it loads `@testing-library/jest-dom/vitest` in your test runner so +you can use matchers like `expect(...).toBeInTheDocument()`. + +Now, edit your `tsconfig.json` to declare the following global types: + +```diff title="tsconfig.json" +{ + "compilerOptions": { + "types": [ ++ "vitest/globals", ++ "@testing-library/jest-dom/vitest" + ] + }, + "include": ["src"] +} +``` + +[vitest]: https://vitest.dev/ + +Finally, you can add test scripts to your `package.json` to run the tests with +Vitest + +```json title="package.json" +{ + "scripts": { + "test": "vitest run", + "test:ui": "vitest --ui", + "test:watch": "vitest" + } +} +``` diff --git a/sidebars.js b/sidebars.js index b5aa8b8cc..fc824899a 100755 --- a/sidebars.js +++ b/sidebars.js @@ -171,6 +171,15 @@ module.exports = { 'solid-testing-library/api', ], }, + { + 'Qwik Testing Library': [ + 'qwik-testing-library/intro', + 'qwik-testing-library/setup', + 'qwik-testing-library/example', + 'qwik-testing-library/api', + 'qwik-testing-library/faq', + ], + }, 'cypress-testing-library/intro', 'pptr-testing-library/intro', 'testcafe-testing-library/intro', From e888514b44e122d45690f888795787e9e305647f Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sun, 27 Oct 2024 11:49:20 -0400 Subject: [PATCH 2/4] chore: apply review suggestions --- docs/qwik-testing-library/api.mdx | 12 +++++++--- docs/qwik-testing-library/example.mdx | 17 +++++++------- docs/qwik-testing-library/setup.mdx | 34 ++++++++++++++++----------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/docs/qwik-testing-library/api.mdx b/docs/qwik-testing-library/api.mdx index 43b655333..09edcce59 100644 --- a/docs/qwik-testing-library/api.mdx +++ b/docs/qwik-testing-library/api.mdx @@ -14,14 +14,20 @@ sidebar_label: API ## `render` -Render your component to the DOM with Qwik. By default, the component will be -rendered into a `` appended to `document.body`. +Render your component to the DOM with Qwik. By default, when no options are +provided, the component will be rendered into a `` appended to +`document.body`. ```tsx import {render} from '@noma.to/qwik-testing-library' +import {MockProvider} from './MockProvider' import {MyComponent} from './MyComponent' -const result = await render(, renderOptions) +const result = await render(, { + baseElement: document.body, + container: document.createElement('host'), + wrapper: MockProvider, +}) ``` ### Render Options diff --git a/docs/qwik-testing-library/example.mdx b/docs/qwik-testing-library/example.mdx index 685e413ac..385cd0324 100644 --- a/docs/qwik-testing-library/example.mdx +++ b/docs/qwik-testing-library/example.mdx @@ -35,11 +35,12 @@ describe('', () => { // retrieve the 'increment count' button const incrementBtn = screen.getByRole('button', {name: /increment count/}) // click the button twice - await userEvent.click(incrementBtn) - await userEvent.click(incrementBtn) + const user = userEvent.setup() + await user.click(incrementBtn) + await user.click(incrementBtn) // assert that the counter is now 2 - await waitFor(() => expect(screen.getByText(/count:2/)).toBeInTheDocument()) + expect(await screen.findByText(/count:2/)).toBeInTheDocument() }) }) ``` @@ -55,6 +56,8 @@ We're happy to discuss it on [Discord][discord], but we consider this failure to be a good thing: your components should be tested in isolation, so you will be forced to mock your server functions. +[discord]: https://qwik.dev/chat + Here is an example of how to test a component that uses `server$` calls: ```ts title="~/server/blog-post.ts" @@ -85,9 +88,7 @@ describe('', () => { it('should render the latest posts', async () => { await render() - await waitFor(() => - expect(screen.queryAllByRole('listitem')).toHaveLength(2), - ) + expect(await screen.findAllByRole('listitem')).toHaveLength(2) }) }) ``` @@ -97,5 +98,5 @@ being named as `getLatestPosts$`. This is caused by the Qwik optimizer renaming it to `Qrl`. So, we need to mock the `Qrl` function instead of the original `$` one. -If your `server$` function doesn't end with `$`, the Qwik optimizer might not -rename it to `Qrl`. +If your function doesn't end with `$`, the Qwik optimizer will not rename it to +`Qrl`. diff --git a/docs/qwik-testing-library/setup.mdx b/docs/qwik-testing-library/setup.mdx index e1499089d..cc1a536a8 100644 --- a/docs/qwik-testing-library/setup.mdx +++ b/docs/qwik-testing-library/setup.mdx @@ -20,10 +20,11 @@ This module is distributed via [npm][npm] which is bundled with [node][node] and should be installed as one of your project's `devDependencies`: ```bash npm2yarn -npm install --save-dev @noma.to/qwik-testing-library +npm install --save-dev @noma.to/qwik-testing-library @testing-library/dom ``` -This library supports `qwik` versions `1.7.2` and above. +This library supports `qwik` versions `1.7.2` and above and +`@testing-library/dom` versions `10.1.0` and above. You may also be interested in installing `@testing-library/jest-dom` and `@testing-library/user-event` so you can use [the custom jest @@ -48,8 +49,8 @@ npm install --save-dev jsdom ## Vitest Configuration -We recommend using `@noma.to/qwik-testing-library` with [Vitest][] as your test -runner. +We recommend using `@noma.to/qwik-testing-library` with [Vitest][vitest] as your +test runner. If you haven't done so already, add vitest to your project using Qwik CLI: @@ -108,7 +109,7 @@ create it: Then, create the `vitest.setup.ts` file: ```ts title="vitest.setup.ts" -import {afterEach} from 'vitest' +// Configure DOM matchers to work in Vitest import '@testing-library/jest-dom/vitest' // This has to run before qdev.ts loads. `beforeAll` is too late @@ -116,18 +117,23 @@ globalThis.qTest = false // Forces Qwik to run as if it was in a Browser globalThis.qRuntimeQrl = true globalThis.qDev = true globalThis.qInspector = false - -afterEach(async () => { - const {cleanup} = await import('@noma.to/qwik-testing-library') - cleanup() -}) ``` -This setup will make sure that Qwik is properly configured and that everything -gets cleaned after each test. +This setup will make sure that Qwik is properly configured. It also loads +`@testing-library/jest-dom/vitest` in your test runner so you can use matchers +like `expect(...).toBeInTheDocument()`. + +By default, Qwik Testing Library cleans everything up automatically for you. You +can opt out of this by setting the environment variable `QTL_SKIP_AUTO_CLEANUP` +to `true`. Then in your tests, you can call the `cleanup` function when needed. +For example: -Additionally, it loads `@testing-library/jest-dom/vitest` in your test runner so -you can use matchers like `expect(...).toBeInTheDocument()`. +```ts +import {cleanup} from '@noma.to/qwik-testing-library' +import {afterEach} from 'vitest' + +afterEach(cleanup) +``` Now, edit your `tsconfig.json` to declare the following global types: From 3522eedbe8b5b473ee9cee9c3679666525b8b118 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sun, 27 Oct 2024 11:58:21 -0400 Subject: [PATCH 3/4] chore: adjust user event setup --- docs/qwik-testing-library/example.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/qwik-testing-library/example.mdx b/docs/qwik-testing-library/example.mdx index 385cd0324..1a31e91de 100644 --- a/docs/qwik-testing-library/example.mdx +++ b/docs/qwik-testing-library/example.mdx @@ -29,13 +29,14 @@ import {Counter} from './counter' describe('', () => { // describe the test case it('should increment the counter', async () => { + // setup user event + const user = userEvent.setup() // render the component into the DOM await render() // retrieve the 'increment count' button const incrementBtn = screen.getByRole('button', {name: /increment count/}) // click the button twice - const user = userEvent.setup() await user.click(incrementBtn) await user.click(incrementBtn) From 98b3fab7e78e0b42003e746d49157217be12ef4a Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:14:06 +0100 Subject: [PATCH 4/4] Update docs/qwik-testing-library/example.mdx --- docs/qwik-testing-library/example.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/qwik-testing-library/example.mdx b/docs/qwik-testing-library/example.mdx index 1a31e91de..55e1b5f11 100644 --- a/docs/qwik-testing-library/example.mdx +++ b/docs/qwik-testing-library/example.mdx @@ -4,7 +4,7 @@ title: Example sidebar_label: Example --- -Below are some examples of how to use the Qwik Testing Library to tests your +Below are some examples of how to use the Qwik Testing Library to test your Qwik components. You can also learn more about the [**queries**][tl-queries-docs] and [**user