diff --git a/docs/qwik-testing-library/api.mdx b/docs/qwik-testing-library/api.mdx new file mode 100644 index 000000000..09edcce59 --- /dev/null +++ b/docs/qwik-testing-library/api.mdx @@ -0,0 +1,203 @@ +--- +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, 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(, { + baseElement: document.body, + container: document.createElement('host'), + wrapper: MockProvider, +}) +``` + +### 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..55e1b5f11 --- /dev/null +++ b/docs/qwik-testing-library/example.mdx @@ -0,0 +1,103 @@ +--- +id: example +title: Example +sidebar_label: Example +--- + +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 +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 () => { + // 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 + await user.click(incrementBtn) + await user.click(incrementBtn) + + // assert that the counter is now 2 + expect(await screen.findByText(/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. + +[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" +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() + + expect(await screen.findAllByRole('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 function doesn't end with `$`, the Qwik optimizer will 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..cc1a536a8 --- /dev/null +++ b/docs/qwik-testing-library/setup.mdx @@ -0,0 +1,165 @@ +--- +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 @testing-library/dom +``` + +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 +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][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" +// 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 +globalThis.qTest = false // Forces Qwik to run as if it was in a Browser +globalThis.qRuntimeQrl = true +globalThis.qDev = true +globalThis.qInspector = false +``` + +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: + +```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: + +```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',