# TWD (Test While Developing) - Full Documentation > In-browser frontend testing for React, Vue, Angular, Solid, Astro, Nuxt, HTMX and vanilla JS. Runs in your real browser via Vite, Webpack, or a CDN. This file concatenates the full TWD documentation for LLM ingestion. For a concise index, see https://twd.dev/llms.txt. # TWD Manifesto Source: https://twd.dev/twd-manifesto **Test While Developing** (TWD) is a mindset, not a checklist. It's about writing tests as you build — not afterward, not someday, not just when you're done. TWD is for developers who want faster feedback, deeper confidence, and fewer regressions — without the ceremony of traditional testing philosophies. --- ## Principles 1. **Write tests while building, not after** Testing is part of the development flow — not something you add at the end. 2. **Keep your test runner open** Let your tests run continuously while coding to get fast, meaningful feedback. 3. **Automate what you already check manually** If you're repeating a check by hand, it's a good candidate for a test. 4. **Let real use guide what you test** Focus on the behaviors and flows your users actually rely on. 5. **Use coverage to find gaps, not for validation** Don't chase numbers — use coverage tools after the fact to spot what you missed, not to prove anything. 6. **Prefer fast feedback over strict structure** Don't get stuck on test types or formats — prioritize feedback that helps you move forward. 7. **Test what you own, mock what you don't** Focus on testing your own code and logic. External services, third-party APIs, and dependencies you don't control should be mocked — not tested. 8. **Design features to be easy to validate** Write code that's naturally testable and easy to observe as you develop. --- > TWD isn't about writing *more* tests — it's about writing the *right* ones, at the right time. # Rethinking Testing - Why I Test While Developing Source: https://twd.dev/motivation These days, we're surrounded by tools that help us move faster — frameworks, CLIs, even AI that writes code for us. It's never been easier to build. And yet, despite all that speed, many teams still struggle with something fundamental: **testing**. Think about it: You build a feature. You check that it works — maybe in the browser, maybe with an API call, maybe by looking at the database. In your mind, the **feature is done**. Then someone says: > "Now let's add some tests." And suddenly, what felt complete now feels like overhead. Why write more code to prove something you just confirmed manually? ## Why Testing Still Gets Left Behind This frustration isn't rare — it's a symptom of how we've been taught to think about testing. Ask someone about testing and you'll often hear: - "We didn't have time to add tests." - "TDD just slows me down." - "I test manually and it works, why write more code?" And to be fair, they're not entirely wrong. Testing often feels like a **separate task** — something formal and rigid that comes after development. It's rarely part of the same flow. We switch mental gears, bring in new tools, write boilerplate... and all to confirm something we already verified manually. No wonder it feels like overhead. But the problem isn't testing itself — it's the **disconnect between building and testing**. ## Testing Should Be Part of Development, Not a Chore What if, instead of treating testing as a separate step, we just **automated the same checks** we're already doing during development? I think about testing the same way I think about committing code: it's not optional, it's just part of getting the job done. But here's the catch — to make that work, we need to **stop treating testing as a set of rituals**. Forget about the pyramid, about whether it's unit, integration, or E2E. Forget about TDD or BDD. Instead, ask yourself: - What am I building right now? - How do I manually check that it works? - How can I automate **that exact validation**? That's your test. That's the mindset. ## Example: Building an API Endpoint Let's say you're developing an API endpoint that inserts in a database and returns the inserted data. Typical workflow: 1. Write the endpoint code. 2. Test it manually in Postman. 3. Add the DB insert logic. 4. Check the data in your DB client. 5. Verify the final response again in Postman. Cool, it works. But now someone asks you to "add tests." You feel resistance — **why repeat all that work** when you already know it works? So maybe you write a unit test for some internal function… or you try to mock the DB in an integration test. Either way, it's an extra effort for something you've already tested manually. ## The Alternative: Test While Developing Instead of starting with tests like in strict TDD, or leaving them for later (which often means never), **I take a different route**. I build the feature as I normally would, but I replace every manual step with a test. For example: 1. Create the endpoint. 2. Instead of opening Postman, write a test that calls the endpoint the same way Postman would (Supertest in node for example). 3. Add the DB logic. 4. Instead of checking your DB with a client like DBeaver, include tools in your test to validate your database. 5. Instead of manually verifying the response, assert it with your test framework. You've just automated the exact checks you would've done manually. No extra work — **just redirected effort**. These tests stick around, and they run every time the code changes. Yes, it's a bit more work at first — and you'll probably hit some friction the first few times. But it pays off quickly. You're **not doing extra work** — you're just turning your manual checks into repeatable ones. ## It Scales and Teaches Well This approach comes with a lot of side benefits: - You get real coverage of actual business logic. - Onboarding juniors is easier — they can read working tests that mirror how features work. - You don't need to explain how to test things manually — the tests are the documentation. - Plus, you still get all the classic benefits of having tests: safer refactors, faster debugging, and fewer regressions. ## What About Frontend? For frontend development, I use **TWD** — a testing library designed specifically for this philosophy. Instead of waiting for the full app to be ready or relying on real backends, TWD runs tests directly in your browser during development. You get a beautiful sidebar that shows test results in real-time as you code. I use a built-in service worker to mock API responses, so I can develop and test features without needing a running backend. That's right — I'm not running tests in a separate terminal or waiting for CI. I'm building and testing in the same environment, with instant feedback right in my browser. Some might say "that's not how traditional testing works." But we're not here to follow tradition — **we're here to build solutions**. And TWD gives me fast feedback, solid coverage, and minimal extra effort. It's testing while developing, not testing after developing. ## What About Traditional Tests? Yes, you should still do those too — once the main work is done. Here's where **coverage tools** come in. Not as a vanity metric, but as a checklist. Once your "test while developing" tests are done, run coverage to see what's missing. Then fill in the gaps with more traditional unit, integration, or even property-based tests. ## What Changes in the AI Era Now that AI is generating a big portion of our frontend code, everything we've talked about matters even more. Speed is no longer the bottleneck — **confidence is**. AI can produce features fast, but it doesn't know your project the way you do. It doesn't know what should be mocked, how your auth works, or what "good tests" look like in your codebase. Without that context, AI-generated tests are generic, inconsistent, and sometimes useless. That's why TWD now includes an [AI Workflow](/twd-ai/setup) — a set of skills that give the AI agent your project context, your testing patterns, and your rules. The AI writes tests that actually fit your architecture, executes them in your real browser, and iterates until they pass. The philosophy hasn't changed. The principles still apply. But now, instead of just *you* testing while developing, **your AI agent tests while developing too** — with the same mindset, the same patterns, and the same confidence. ## Final Thoughts Testing isn't a separate phase of development. It's not an extra burden. It's a **different way of thinking** about the same job. Stop trying to follow someone else's test strategy. Just ask: > How do I know this works? Then automate that answer. That's it. That's my philosophy. Test while developing. # Frontend tests that run in your real browser — TWD Source: https://twd.dev/twd-js ## The problem Testing gets pushed to next week. Next week never comes. The reason isn't that you don't care — it's that testing usually runs in a different environment from where you're building. You write a feature, check it works in the browser, then context-switch to a different tool to "add tests." Suddenly what felt complete feels like overhead. twd-js fixes this by putting the tests where the feature lives: in your real browser, in your real dev server, with a sidebar that updates as you code. ## How it works You install `twd-js`, add the Vite plugin, and write tests next to your code in `*.twd.test.ts` files. When you run `npm run dev`, the TWD sidebar appears in your browser. Click play to run any test. Tests use the same DOM, routes, and state your users will — there's no separate test environment to keep in sync. Selectors come from Testing Library (`screenDom.findByRole`, `findByLabelText`, …); assertions are chainable (`twd.should(el, 'be.visible')`). Mocking is built in: `twd.mockRequest(...)` intercepts fetch/XHR via a service worker so you can develop and test features without a running backend. Nothing TWD ships to production — every `twd-js` import is dev-only (`import.meta.env.DEV`-guarded), so your prod bundle is untouched. ## Quick start ```bash npm install --save-dev twd-js ``` ```ts // vite.config.ts import { defineConfig } from 'vite' import { twd } from 'twd-js/vite-plugin' export default defineConfig({ plugins: [twd({ open: true })], }) ``` ```ts // src/App.twd.test.ts import { twd, screenDom } from 'twd-js' import { describe, it } from 'twd-js/runner' describe('App', () => { it('shows the heading', async () => { await twd.visit('/') const heading = await screenDom.findByRole('heading', { level: 1 }) twd.should(heading, 'be.visible') }) }) ``` Run `npm run dev` and open the app. The sidebar appears in your browser; click play to run the test. # Token-efficient browser testing for AI agents — TWD Source: https://twd.dev/twd-relay ## The problem AI agents write test files that look correct, then never execute them in a real browser. No one notices until production does. The other half of the problem: when agents *do* try to run browser tests, they usually reach for Playwright or Puppeteer MCP. Those tools talk back in screenshots and DOM dumps, and the payloads are huge. A single test run can burn thousands of tokens on visual diffs the agent can't really reason about. twd-relay fixes both halves. It runs in the dev server you already have open, and it streams structured pass/fail events back over a WebSocket. Text, not pixels. ## How it works `twd-relay` is a WebSocket server that routes messages between your **browser** (where TWD is loaded) and an **external client** (an AI agent, a script, or the bundled `twd-relay run` CLI). ``` ┌───────────────┐ WebSocket ┌──────────────────┐ ┌───────────────────┐ │ AI Agent │◄──────────────────►│ Relay Server │◄──────────────►│ Browser (TWD) │ │ (Claude Code,│ /__twd/ws │ (Vite plugin or │ │ Test runner + │ │ script) │ │ standalone) │ │ sidebar UI │ └───────────────┘ └──────────────────┘ └───────────────────┘ ``` The agent sends `{ type: "run", scope: "all" }`; the relay forwards it to the browser; TWD runs the tests; per-test events stream back; the relay closes with `run:complete`. The agent reads pass/fail/skip counts, opens failing test names, fixes the code, and runs again. A tight write/run/read/fix loop with no browser automation runtime, no screenshots, and a tiny token footprint per iteration. ## Quick start ```bash npm install --save-dev twd-relay ``` ```ts // vite.config.ts import { defineConfig } from 'vite' import { twd } from 'twd-js/vite-plugin' import { twdRemote } from 'twd-relay/vite' export default defineConfig({ plugins: [ twd(), // sidebar + test discovery twdRemote(), // relay endpoint + auto-injected browser client ], }) ``` Add a line to your agent's instructions file (e.g. `CLAUDE.md`): ```text To run TWD tests: npx twd-relay run Exit code 0 means all tests passed; 1 means failures or errors. ``` That's it. Start `npm run dev`, leave the tab open, and your agent can drive tests on demand. # Validate every mock against your OpenAPI spec — TWD Source: https://twd.dev/contract-testing ## The problem Frontend teams write mock responses in tests that drift from reality over time. Fields get renamed, removed, or added in the API — but mocks stay frozen. Tests pass, code ships, and the app breaks in production. Contract testing closes this gap: **test what you own, mock what you don't — then validate the mocks.** ## How it works During the test run, TWD collects every mock registered via `twd.mockRequest()`. After tests complete, `twd-cli` validates those mocks against your OpenAPI specs. Each mock either matches the spec (✓), fails (✗) with the precise field that broke, or warns (⚠) when the status code or schema isn't documented yet. You pick the mode per spec. `"error"` fails the test run (use this for stable endpoints you trust). `"warn"` reports but doesn't fail (use this while you're catching up to a moving target). When the GitHub Action runs in CI, a summary table is posted as a PR comment so the breakage is visible to the reviewer, not just to whoever scrolled the CI log. ## Quick start ```bash npm install --save-dev twd-cli ``` ```json // twd.config.json { "url": "http://localhost:5173", "contractReportPath": ".twd/contract-report.md", "contracts": [ { "source": "./contracts/users-3.0.json", "baseUrl": "/api", "mode": "error", "strict": true } ] } ``` ## What you see in CI After `npx twd-cli run` finishes, contract validation results print alongside the test output: ``` $ npx twd-cli run Running TWD tests in headless browser… ✓ Users page > shows users ✓ User detail > shows address ✓ Counter > increments on click 3 passed, 0 failed (0.4s) Validating mocks against OpenAPI specs… Source: ./contracts/users-3.0.json ERROR ✓ GET /users (200) — mock "getUsers" ✗ GET /users/{userId} (200) — mock "getUserBadAddress" → response.address.city: missing required property → response.address.country: missing required property ⚠ GET /users/{userId} (404) — mock "getUserNotFound" Status 404 not documented for GET /users/{userId} Contract report written to .twd/contract-report.md ``` With the GitHub Action, the same summary is posted as a PR comment so failed validations surface in the reviewer's queue, not just the CI log. [Full setup, options, validations, and PR reports →](/contract-testing-setup) # Getting Started Source: https://twd.dev/getting-started Welcome to TWD (Test While Developing)! This guide will help you set up TWD in your application and write your first test. TWD is a deterministic browser validation layer for your frontend boundaries. It works with any frontend that renders in the browser: SPAs (React, Vue, Angular, Solid), hydrated SSR (React Router, Nuxt), Astro islands, and no-build projects (HTMX, vanilla JS) via a CDN, on Vite, Webpack, or no bundler at all. ## Installation Install TWD using your preferred package manager: ::: code-group ```bash [npm] npm install --save-dev twd-js ``` ```bash [yarn] yarn add --dev twd-js ``` ```bash [pnpm] pnpm add -D twd-js ``` ::: ::: tip No build step? Use a CDN Plain HTML pages, static sites, and [HTMX](https://htmx.org/) projects can load TWD from a CDN with no bundler and no install, using an import map. See [Vanilla JS](/frameworks#vanilla-js-cdn-no-bundler) and [HTMX](/frameworks#htmx-cdn-no-bundler) in the framework guide. ::: ## Quick Setup ### 1. Add the Vite Plugin For Vite-based projects (React, Vue, Solid.js, and other Vite-native frameworks), add the `twd()` plugin to your `vite.config.ts`. The plugin auto-loads the sidebar and discovers test files in dev — no entry-file changes required. ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // or vue, solid, etc. import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', open: true, position: 'left', search: true, // Enable search/filter in the sidebar (default: false) pace: true, // Enable the execution speed selector (default: false) serviceWorker: true, // Enable request mocking (default: true) serviceWorkerUrl: '/mock-sw.js', // Custom service worker path (default: '/mock-sw.js') // rootSelector: '#my-app', // (Optional) Override the app root for screenDom queries }), ], }); ``` The plugin only runs in `vite dev` (`apply: 'serve'`), so production builds never include any TWD code. ::: tip Using Angular or another non-Vite tool? Skip to [Manual setup for non-Vite projects](#manual-setup-for-non-vite-projects) below. ::: ### 2. Set Up the Service Worker (Optional but recommended) If you plan to use API mocking, set up the mock service worker: ```bash npx twd-js init public ``` This copies the required `mock-sw.js` file to your public directory. ### 3. Write Your First Test Create your first test file: ```ts // src/App.twd.test.ts import { twd, userEvent, screenDom } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("App Component", () => { it("should render the main heading", async () => { await twd.visit("/"); // Use screenDom for testing library queries const heading = screenDom.getByRole("heading", { level: 1 }); twd.should(heading, "be.visible"); }); it("should handle button clicks", async () => { await twd.visit("/"); const user = userEvent.setup(); const button = screenDom.getByRole("button"); await user.click(button); // Add your assertions here const result = screenDom.getByText("Button clicked!"); twd.should(result, "be.visible"); }); }); ``` ### 4. Run Your App Start your development server as usual: ```bash npm run dev ``` You should now see the TWD sidebar in your browser automatically in development mode. Click on it to view and run your tests!

TWD Sidebar showing test execution

## Manual setup for non-Vite projects If your project doesn't use Vite (e.g. Angular CLI, Webpack, custom bundler), initialize TWD manually in your dev entry point. The `twd()` Vite plugin doesn't apply here, but the underlying `initTWD` API works the same way the plugin does internally. ```tsx // src/main.tsx (or main.ts) import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; // Only load the test sidebar and tests in development mode if (import.meta.env.DEV) { const { initTWD } = await import('twd-js/bundled'); const tests = import.meta.glob('./**/*.twd.test.ts'); initTWD(tests, { open: true, position: 'left', search: true, // Enable search/filter in the sidebar (default: false) pace: true, // Enable the execution speed selector (default: false) serviceWorker: true, // Enable request mocking (default: true) serviceWorkerUrl: '/mock-sw.js', // Custom service worker path (default: '/mock-sw.js') // rootSelector: '#my-app', // (Optional) Override the app root for screenDom queries }); } createRoot(document.getElementById('root')!).render( , ); ``` The bundled setup ships React internally, so it works with any framework — the entry-file integration is the only difference. ## File Naming Convention We recommend naming your test files using the following patterns: - `*.twd.test.ts` - `*.twd.test.tsx` - `*.twd.test.js` - `*.twd.test.jsx` You can customize this pattern in your test loader using different glob patterns. ## Development Workflow 1. **Write tests** alongside your components 2. **Run tests** using the browser sidebar 3. **See instant feedback** as you develop 4. **Mock external systems** (APIs, auth, feature flags) for deterministic results 5. **Iterate quickly** with live reloading ## Next Steps - Learn about [Writing Tests](/writing-tests) in detail - Explore [API Mocking](/api-mocking) capabilities - Follow the [Tutorial](/tutorial/) for step-by-step learning - Browse the [API Reference](/api/) for all available methods ## Troubleshooting ### Tests Not Loading Make sure you: 1. Are running in development mode (`vite dev` / `import.meta.env.DEV` is true) 2. Used a file name that matches your `testFilePattern`. The default is `'/**/*.twd.test.ts'`, which matches `.ts` only. If your tests are `.tsx` or `.jsx`, set `twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' })` or they will be skipped silently. 3. Added `twd()` to the `plugins` array in your `vite.config.ts` (Vite projects), or have the manual `initTWD` logic in your entry file (non-Vite projects) ### Service Worker Issues If API mocking isn't working: 1. Run `npx twd-js init public` to install the service worker 2. Make sure request mocking is enabled (`serviceWorker: true` is the default in both the plugin and `initTWD`) 3. Check the browser console for service worker registration errors ### Test Duplication on HMR If you're using the `twd()` Vite plugin, full-reload on test edits is handled automatically — no extra plugin needed. For manual (non-Vite) setups where you notice test entries duplicating when you edit test files during development, add the TWD HMR plugin alongside `initTWD`: ```ts // vite.config.ts import { twdHmr } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ // ... other plugins twdHmr(), ], }); ``` This plugin forces a full page reload when TWD test files change, preventing duplicate test entries. ## Getting Help - 📖 [Browse the documentation](/api/) - 🐛 [Report issues](https://github.com/BRIKEV/twd/issues) - 💬 [Join discussions](https://github.com/BRIKEV/twd/discussions) # Writing Tests Source: https://twd.dev/writing-tests Learn how to write effective tests with TWD's intuitive API and powerful features. ## Test Structure TWD uses a familiar testing structure similar to Jest, Mocha, and other popular testing frameworks: ```ts import { twd, userEvent } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; describe("User Authentication", () => { beforeEach(() => { // Reset state before each test console.log("Setting up test environment"); }); it("should login with valid credentials", async () => { // Your test logic here }); it("should show error with invalid credentials", async () => { // Your test logic here }); }); ``` ### Test Functions | Function | Purpose | Example | |----------|---------|---------| | `describe(name, fn)` | Groups related tests | `describe("Login Form", () => {...})` | | `it(name, fn)` | Defines a test case | `it("should submit form", async () => {...})` | | `describe.only(name, fn)` | Runs only this suite (and its children) — helpful when debugging a group of tests | `describe.only("Only this suite", () => {...})` | | `describe.skip(name, fn)` | Skips this suite and all its descendant tests | `describe.skip("Skipped suite", () => {...})` | | `it.only(name, fn)` | Runs only this test | `it.only("debug this test", () => {...})` | | `it.skip(name, fn)` | Skips this test | `it.skip("broken test", () => {...})` | | `beforeEach(fn)` | Runs before each test | `beforeEach(() => {...})` | | `afterEach(fn)` | Runs after each test | `afterEach(() => {...})` | ### Nested Describes You can nest `describe` blocks for better organization: ```ts describe("User Management", () => { describe("Registration", () => { it("should create new user", async () => { // Registration tests }); }); describe("Login", () => { it("should authenticate user", async () => { // Login tests }); }); }); ``` ## Element Selection TWD provides multiple ways to select DOM elements. **Prefer Testing Library's async `findBy*` queries.** They are the recommended way to select elements in TWD, ahead of `getBy*`/`queryBy*` and ahead of TWD's native `twd.get()`. ::: tip Testing a single component instead of a page? If you render a component in isolation with Testing Library's `render()`, use `screen` rather than `screenDom`. See [Component Testing](/component-testing#queries). ::: ### Recommended: `findBy*` queries > **Always reach for `findBy*` first.** > > - **`findBy*` (recommended)**: returns a promise and **waits** for the element to appear. Because UI in a real app renders asynchronously (after navigation, data fetches, state updates, or re-renders), `findBy*` is the most reliable choice and avoids flaky tests. > - **`getBy*`**: synchronous; throws immediately if the element isn't already in the DOM. Only use it when you're certain the element is already rendered. > - **`queryBy*`**: synchronous; returns `null` if not found. Use it **only** when you intentionally want to assert that an element is *absent*. > > Default to `findBy*`. Use `findBy*` over `twd.get()` too, since Testing Library's semantic queries are more accessible and resilient than CSS selectors. ```ts // ✅ Recommended: waits for the element to appear const successMessage = await screenDom.findByText("Login successful!"); const submitButton = await screenDom.findByRole("button", { name: /sign in/i }); // ⚠️ Synchronous: throws if not already in the DOM const heading = screenDom.getByRole("heading", { name: "Welcome" }); // ⚠️ Only to assert absence const error = screenDom.queryByText("Error"); expect(error).to.equal(null); ``` ### Testing Library Queries TWD supports Testing Library's query methods through two APIs: 1. **`screenDom`** - Scoped queries that exclude the TWD sidebar (recommended for most cases) 2. **`screenDomGlobal`** - Global queries for portal-rendered elements (modals, dialogs) #### Import ```ts import { screenDom, screenDomGlobal } from "twd-js"; ``` #### When to Use screenDom vs screenDomGlobal **Use `screenDom` (default):** - For regular page content within your app - Automatically excludes sidebar elements - Recommended for most queries **Use `screenDomGlobal`:** - For portal-rendered elements (modals, dialogs, tooltips) - When you need to search outside the root container - ⚠️ **Important:** Use specific selectors (e.g., `getByRole` with `name`) to avoid matching sidebar elements #### Query by Role (Recommended) ```ts // Find button by role and accessible name (waits for it to appear) const submitButton = await screenDom.findByRole("button", { name: /submit/i }); const heading = await screenDom.findByRole("heading", { name: "Welcome", level: 1 }); // Find form elements const emailInput = await screenDom.findByRole("textbox", { name: /email/i }); const checkbox = await screenDom.findByRole("checkbox", { name: /terms/i }); ``` #### Query by Label ```ts // Find inputs by their labels (most accessible) const emailInput = await screenDom.findByLabelText("Email Address:"); const searchInput = await screenDom.findByLabelText(/search/i); ``` #### Query by Text ```ts // Find elements by text content const title = await screenDom.findByText("Welcome to TWD"); const partialMatch = await screenDom.findByText(/welcome/i); ``` #### Query by Test ID ```ts // Find elements by data-testid const userCard = await screenDom.findByTestId("user-card"); ``` #### Query Methods - **findBy*** *(recommended)* - Returns a promise, **waits** for the element to appear. Use this by default. - **getBy*** - Returns element or throws immediately if not found. Use only when the element is already rendered. - **queryBy*** - Returns element or `null` if not found. Use only to assert an element is absent. - **findAllBy*** *(recommended for lists)* / **getAllBy*** / **queryAllBy*** - Returns an array of elements. ```ts // ✅ findBy waits for element to appear (recommended) const successMessage = await screenDom.findByText("Success!"); // findAllBy waits and returns an array const buttons = await screenDom.findAllByRole("button"); expect(buttons).to.have.length(3); // getBy throws if element doesn't exist (must already be rendered) const button = screenDom.getByRole("button"); // queryBy returns null if element doesn't exist (use to assert absence) const error = screenDom.queryByText("Error"); expect(error).to.equal(null); ``` #### Complete Example with screenDom ```ts import { screenDom, screenDomGlobal, userEvent, twd } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Login Form", () => { it("should submit login form", async () => { await twd.visit("/login"); // Use screenDom findBy* for semantic queries (regular page content) const emailInput = await screenDom.findByLabelText("Email:"); const passwordInput = await screenDom.findByLabelText("Password:"); const submitButton = await screenDom.findByRole("button", { name: /sign in/i }); // Use userEvent for interactions const user = userEvent.setup(); await user.type(emailInput, "user@example.com"); await user.type(passwordInput, "password123"); await user.click(submitButton); // Wait for success message const successMessage = await screenDom.findByText("Login successful!"); twd.should(successMessage, "be.visible"); }); it("should handle modal confirmation", async () => { await twd.visit("/settings"); // Use screenDom findBy* for regular button const deleteButton = await screenDom.findByRole("button", { name: /delete/i }); const user = userEvent.setup(); await user.click(deleteButton); // Use screenDomGlobal for modal (rendered via portal) // ⚠️ Use specific queries to avoid matching sidebar elements const confirmModal = await screenDomGlobal.findByRole("dialog", { name: "Confirm Deletion" }); const confirmButton = await screenDomGlobal.findByRole("button", { name: "Yes, Delete Account" }); await user.click(confirmButton); }); }); ``` > **Note:** For complete Testing Library documentation, see the [Testing Library API reference](/testing-library). ### TWD Native Selectors TWD's native selectors use CSS selectors for simple, direct element access. Prefer Testing Library's `findBy*` queries above; reach for these only when a CSS selector is genuinely the simplest option (for example, selecting by a non-semantic class or a complex structural selector). #### Single Element Selection Use `twd.get()` to select a single element: ```ts // By tag const button = await twd.get("button"); // By ID const emailInput = await twd.get("#email"); // By class const errorMessage = await twd.get(".error-message"); // By attribute const submitButton = await twd.get("button[type='submit']"); // By data attribute const userCard = await twd.get("[data-testid='user-card']"); // Complex selectors const firstListItem = await twd.get("ul > li:first-child"); ``` #### Multiple Element Selection Use `twd.getAll()` to select multiple elements: ```ts // Get all buttons const buttons = await twd.getAll("button"); // Get all list items const listItems = await twd.getAll("li"); // Access specific elements buttons[0].should("be.visible"); listItems[2].should("contain.text", "Third item"); ``` ## Assertions TWD provides a comprehensive set of assertions for testing element states and content. There are two ways to make assertions: 1. **Method style** (`element.should()`) - For elements from `twd.get()` or `twd.getAll()` 2. **Function style** (`twd.should(element, ...)`) - For any element, especially from Testing Library queries ### Using Assertions #### Method Style (TWD Elements) Elements returned from `twd.get()` and `twd.getAll()` have a `.should()` method: ```ts const element = await twd.get("h1"); element.should("have.text", "Welcome"); element.should("be.visible"); ``` #### Function Style (Any Element) Use `twd.should()` for elements from Testing Library queries or any raw DOM element: ```ts import { twd, screenDom } from "twd-js"; // With Testing Library queries const button = screenDom.getByRole("button"); twd.should(button, "be.visible"); twd.should(button, "have.text", "Submit"); // With raw DOM elements const element = document.querySelector(".my-element"); twd.should(element, "contain.text", "Hello"); ``` ### Text Content Assertions ```ts // Method style (TWD elements) const element = await twd.get("h1"); element.should("have.text", "Welcome to TWD"); element.should("contain.text", "Welcome"); element.should("be.empty"); // Function style (Testing Library or raw elements) const heading = screenDom.getByRole("heading"); twd.should(heading, "have.text", "Welcome to TWD"); twd.should(heading, "contain.text", "Welcome"); // Negated assertions element.should("not.have.text", "Goodbye"); twd.should(heading, "not.be.empty"); ``` ### Attribute Assertions ```ts // Method style const input = await twd.get("input#email"); input.should("have.attr", "type", "email"); input.should("have.value", "user@example.com"); input.should("have.class", "form-control"); // Function style const emailInput = screenDom.getByLabelText("Email:"); twd.should(emailInput, "have.attr", "type", "email"); twd.should(emailInput, "have.value", "user@example.com"); twd.should(emailInput, "have.class", "form-control"); ``` ### Element State Assertions ```ts // Method style const button = await twd.get("button"); button.should("be.disabled"); button.should("be.visible"); // Function style const submitButton = screenDom.getByRole("button", { name: /submit/i }); twd.should(submitButton, "be.enabled"); twd.should(submitButton, "be.visible"); // Checked state const checkbox = screenDom.getByRole("checkbox"); twd.should(checkbox, "be.checked"); // Selected state const option = screenDom.getByRole("option", { selected: true }); twd.should(option, "be.selected"); // Focus state const input = screenDom.getByLabelText("Username:"); input.focus(); twd.should(input, "be.focused"); ``` ### URL Assertions ```ts // Exact URL match await twd.url().should("eq", "http://localhost:3000/dashboard"); // URL contains substring await twd.url().should("contain.url", "/dashboard"); await twd.url().should("contain.url", "localhost"); // Negated URL assertions await twd.url().should("not.contain.url", "/login"); ``` ### chai expect assertions You can use the `expect` function from the `chai` library to make assertions: ```ts import { expect, twd } from "twd-js"; // Get all list items const listItems = await twd.getAll("li"); // Assert array length. These assertions are not displayed in the sidebar logs. expect(listItems).to.have.length(3); ``` ## User Interactions TWD integrates with `@testing-library/user-event` for realistic user interactions. All user event methods are available and automatically logged in the TWD sidebar: ### Click Events ```ts import { userEvent } from "twd-js"; const user = userEvent.setup(); const button = await twd.get("button"); // Single click await user.click(button.el); // Double click await user.dblClick(button.el); // Right click await user.pointer({ target: button.el, keys: '[MouseRight]' }); ``` ### Typing and Input ```ts const user = userEvent.setup(); const input = await twd.get("input#username"); // Type text await user.type(input.el, "john_doe"); // Clear and type await user.clear(input.el); await user.type(input.el, "new_username"); // Type special characters await user.keyboard(input.el, "Hello{Enter}World{Tab}"); ``` ### Form Interactions ```ts const user = userEvent.setup(); // Select dropdown options const select = await twd.get("select#country"); await user.selectOptions(select.el, "US"); // Multiple selections await user.selectOptions(select.el, ["US", "CA", "MX"]); // Checkbox interactions const checkbox = await twd.get("input[type='checkbox']"); await user.click(checkbox.el); // Toggle // Radio button selection const radio = await twd.get("input[value='premium']"); await user.click(radio.el); ``` ### File Upload ```ts const user = userEvent.setup(); const fileInput = await twd.get("input[type='file']"); // Create a mock file const file = new File(['hello'], 'hello.png', { type: 'image/png' }); // Upload file await user.upload(fileInput.el, file); // Multiple files const files = [ new File(['file1'], 'file1.txt', { type: 'text/plain' }), new File(['file2'], 'file2.txt', { type: 'text/plain' }) ]; await user.upload(fileInput.el, files); ``` ## Navigation ### Page Navigation ```ts // Navigate to different routes await twd.visit("/"); await twd.visit("/login"); await twd.visit("/dashboard"); // Navigate with query parameters await twd.visit("/search?q=testing"); // Navigate with hash await twd.visit("/docs#getting-started"); ``` ### Waiting ```ts // Wait for a specific time await twd.wait(1000); // Wait 1 second // Wait for element to appear const element = await twd.get(".loading-spinner"); element.should("be.visible"); // Wait for element to disappear await twd.wait(500); const spinner = await twd.get(".loading-spinner"); spinner.should("not.be.visible"); ``` ## State Management & Test Isolation TWD runs tests directly in the browser **without page reloads**. The `twd.visit()` command simulates SPA navigation using the History API, which means your SPA router re-renders but **in-memory application state is preserved** between tests. This is a deliberate trade-off: it keeps tests fast and deterministic, but it means state from tools like Zustand, Redux, Jotai, or plain module-level variables will **leak between tests** unless you explicitly reset it. ### What TWD resets for you TWD provides built-in reset methods for its own managed state: ```ts beforeEach(() => { twd.clearRequestMockRules(); // Clears API mock rules twd.clearComponentMocks(); // Clears component mocks }); ``` ### What you need to reset manually Any state that lives in your application's JavaScript memory persists across tests: | State type | Example | How to reset | |---|---|---| | State managers | Zustand, Redux, Jotai, Pinia | Call your store's reset method | | Browser storage | localStorage, sessionStorage | `localStorage.clear()` | | Module singletons | Caches, counters, flags | Re-assign to initial value | | Global event listeners | `window.addEventListener(...)` | Remove in `afterEach` | | Timers | `setInterval`, `setTimeout` | Clear in `afterEach` | Most state management libraries provide a way to reset stores to their initial state. Expose a reset method on your stores and call it in `beforeEach`: ```ts import { resetMyStore } from "../../store"; describe("My feature", () => { beforeEach(() => { twd.clearRequestMockRules(); twd.clearComponentMocks(); resetMyStore(); localStorage.clear(); }); it("should start with clean state", async () => { await twd.visit("/my-page"); // State is fresh for every test }); }); ``` ### Why not just reload the page? TWD's test runner, sidebar UI, mock service worker, and all test definitions live in the same browser page as your app. A full page reload (`window.location.reload()`) would destroy the test runner itself, losing all test results and state. This is the fundamental constraint of in-browser testing — and the same trade-off other in-browser tools face. ## Best Practices ### 1. Group Related Tests Organize tests logically with nested describes: ```ts describe("Shopping Cart", () => { describe("Adding Items", () => { it("should add item to cart", async () => { // Test adding items }); }); describe("Removing Items", () => { it("should remove item from cart", async () => { // Test removing items }); }); }); ``` ### 2. Clean Up After Tests Use `beforeEach` to ensure clean state. See [State Management & Test Isolation](#state-management-test-isolation) above for full details. ```ts beforeEach(() => { twd.clearRequestMockRules(); twd.clearComponentMocks(); localStorage.clear(); // Reset your app's state managers too (Zustand, Redux, etc.) }); ``` ### 3. Write Descriptive Test Names ```ts // Good ✅ it("should show validation error when email is invalid", async () => { // Test implementation }); // Bad ❌ it("should validate email", async () => { // Test implementation }); ``` ### 4. Test User Workflows Test complete user workflows rather than isolated functions: ```ts describe("User Registration Flow", () => { it("should register new user and redirect to dashboard", async () => { await twd.visit("/register"); const user = userEvent.setup(); // Fill registration form const emailInput = await twd.get("#email"); const passwordInput = await twd.get("#password"); const confirmPasswordInput = await twd.get("#confirmPassword"); await user.type(emailInput.el, "user@example.com"); await user.type(passwordInput.el, "securePassword123"); await user.type(confirmPasswordInput.el, "securePassword123"); // Submit form const submitButton = await twd.get("button[type='submit']"); await user.click(submitButton.el); // Verify redirect and welcome message await twd.url().should("contain.url", "/dashboard"); const welcome = await twd.get("h1"); welcome.should("contain.text", "Welcome"); }); }); ``` ## Next Steps - Learn about [API Mocking](/api-mocking) for testing with external APIs - Follow the [Tutorial](/tutorial/) for comprehensive testing scenarios - Check the [API Reference](/api/) for complete method documentation # API Mocking Source: https://twd.dev/api-mocking TWD provides API mocking through its own mock service worker integration, allowing you to isolate your frontend from external systems and validate UI behavior against deterministic network boundaries. ::: tip Cross-origin requests The mock service worker intercepts **all** requests made from the page, including cross-origin URLs (e.g., third-party APIs like payment providers or analytics services). You can mock any URL your frontend calls, regardless of domain. ::: ## Setup ### 1. Install the service worker First, set up the mock service worker in your project: ```bash npx twd-js init public ``` This command copies the required `mock-sw.js` file to your public directory. ### 2. Remove Service Worker from Production Builds The service worker file (`mock-sw.js`) is only needed during development for API mocking. To automatically remove it from production builds, add the `removeMockServiceWorker` Vite plugin to your configuration: ```ts // vite.config.ts import { defineConfig } from 'vite'; import { twd, removeMockServiceWorker } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ twd(), // dev: discovery + sidebar + request mocking removeMockServiceWorker(), // build: strip mock-sw.js from prod dist ], }); ``` This plugin will automatically remove `mock-sw.js` from your `dist` folder during production builds. ::: tip The plugin only runs during build time (`apply: 'build'`) and will not affect your development workflow. ::: ### 3. Initialize Mocking The `twd()` Vite plugin (recommended) initializes request mocking automatically — no entry-file changes required. Mocking is enabled by default (`serviceWorker: true`). ```ts // vite.config.ts import { defineConfig } from 'vite'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ twd({ serviceWorker: true, // default — request mocking enabled serviceWorkerUrl: '/mock-sw.js', // default }), ], }); ``` For non-Vite projects, initialize manually in your entry file. Both `initTWD` and the standard setup register the service worker for you: ```ts // Bundled (recommended for non-Vite projects) if (import.meta.env.DEV) { const { initTWD } = await import('twd-js/bundled'); const tests = import.meta.glob('./**/*.twd.test.ts'); initTWD(tests, { serviceWorker: true }); } ``` ```ts // Standard (React-only, full control) if (import.meta.env.DEV) { const testModules = import.meta.glob('./**/*.twd.test.ts'); const { initTests, twd, TWDSidebar } = await import('twd-js'); initTests(testModules, , createRoot); twd.initRequestMocking().catch(console.error); } ``` ::: tip You only need to register request mocking once (via the plugin or `initTWD`/`initRequestMocking`), not per test. ::: ## Basic Mocking ### Simple GET Request ```ts import { twd, userEvent } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("User Profile", () => { it("should load user data", async () => { // Mock the API request await twd.mockRequest("getUser", { method: "GET", url: "https://api.example.com/user/123", response: { id: 123, name: "John Doe", email: "john@example.com" } }); await twd.visit("/profile"); // Trigger the request const loadButton = await twd.get("button[data-testid='load-profile']"); await userEvent.click(loadButton.el); // Wait for the mock to be called await twd.waitForRequest("getUser"); // Verify the UI updated const userName = await twd.get("[data-testid='user-name']"); userName.should("have.text", "John Doe"); }); }); ``` ### POST Request with Body ```ts it("should create new user", async () => { await twd.mockRequest("createUser", { method: "POST", url: "https://api.example.com/users", response: { id: 456, name: "Jane Smith", email: "jane@example.com", created: true }, status: 201 }); await twd.visit("/users/new"); const user = userEvent.setup(); // Fill form await user.type(await twd.get("#name"), "Jane Smith"); await user.type(await twd.get("#email"), "jane@example.com"); // Submit form await user.click(await twd.get("button[type='submit']")); // Wait for request and verify const rule = await twd.waitForRequest("createUser"); // Check the request body expect(rule.request).to.deep.equal({ name: "Jane Smith", email: "jane@example.com" }); }); ``` ## Advanced Mocking ### URL Patterns with RegExp ```ts it("should handle dynamic user IDs", async () => { // Mock any user ID await twd.mockRequest("getUserById", { method: "GET", url: /\/api\/users\/\d+/, // Matches /api/users/123, /api/users/456, etc. response: { id: 123, name: "Dynamic User" } }); // This will match the pattern await twd.visit("/users/123"); // Trigger request and verify const loadButton = await twd.get("button[data-testid='load-user']"); await userEvent.click(loadButton.el); await twd.waitForRequest("getUserById"); }); ``` ### URL Matching with Strings When you provide a string URL (not a RegExp), TWD uses **boundary-aware matching**. The mock triggers when the request URL contains the mock URL followed by a valid boundary character: `?`, `#`, `&`, or end of string. This prevents similar URLs from accidentally matching each other: ```ts await twd.mockRequest("getUsers", { method: "GET", url: "/api/users", response: [{ id: 1, name: "John" }], }); // ✅ Matches: /api/users // ✅ Matches: /api/users?page=1&limit=10 // ❌ Does NOT match: /api/users/123 (different resource) // ❌ Does NOT match: /api/users-settings // ❌ Does NOT match: /api/username ``` This means you can safely mock similar endpoints and nested resources without conflicts: ```ts // Similar endpoint names await twd.mockRequest("getOrders", { method: "GET", url: "/api/orders", response: [{ id: 1 }], }); await twd.mockRequest("getOrdersSummary", { method: "GET", url: "/api/orders-summary", response: { total: 100 }, }); // Nested resources await twd.mockRequest("travelerDetail", { method: "GET", url: `v1/travelers/${travelerId}`, response: mockTravelerDetail, }); await twd.mockRequest("billingDetails", { method: "GET", url: `v1/travelers/${travelerId}/billing-details`, response: mockBillingDetails, }); // Each request matches only its intended mock ``` ::: tip Query String Matching Boundary checking only applies to the **path** portion of the URL. Once the match extends into the query string, substring matching is used. This lets you provide partial query strings to match any value: ```ts // Matches any search query await twd.mockRequest("search", { method: "GET", url: "https://api.example.com/search?q=", response: [{ id: 1, title: "Result" }], }); // ✅ Matches: /search?q=laptops // ✅ Matches: /search?q=phones&category=electronics ``` ::: If you need more flexible matching, use RegExp patterns instead. ### Custom Status Codes and Headers ```ts it("should handle API errors", async () => { await twd.mockRequest("getUserError", { method: "GET", url: "https://api.example.com/user/999", response: { error: "User not found", code: "USER_NOT_FOUND" }, status: 404, responseHeaders: { "Content-Type": "application/json", "X-Error-Code": "USER_NOT_FOUND" } }); await twd.visit("/user/999"); const loadButton = await twd.get("button[data-testid='load-user']"); await userEvent.click(loadButton.el); await twd.waitForRequest("getUserError"); // Verify error handling const errorMessage = await twd.get(".error-message"); errorMessage.should("contain.text", "User not found"); }); ``` ### Multiple Requests ```ts it("should handle multiple API calls", async () => { // Mock multiple endpoints await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { id: 123, name: "John Doe" } }); await twd.mockRequest("getUserPosts", { method: "GET", url: "/api/user/123/posts", response: [ { id: 1, title: "First Post" }, { id: 2, title: "Second Post" } ] }); await twd.visit("/user/123"); const loadButton = await twd.get("button[data-testid='load-all']"); await userEvent.click(loadButton.el); // Wait for both requests const rules = await twd.waitForRequests(["getUser", "getUserPosts"]); expect(rules).to.have.length(2); // Verify UI shows both user and posts const userName = await twd.get("[data-testid='user-name']"); userName.should("have.text", "John Doe"); const posts = await twd.getAll(".post-item"); expect(posts).to.have.length(2); }); ``` ## Simulating Network Delay You can simulate network latency by adding a `delay` option (in milliseconds) to any mock rule. This is useful for testing loading states, spinners, timeouts, and other UX that depends on slow responses. ### Basic Delay ```ts it("should show loading state while waiting for API", async () => { // Mock a slow API response (1 second delay) await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { id: 123, name: "John Doe" }, delay: 1000, // 1 second delay }); await twd.visit("/profile"); // Loading indicator should be visible while waiting const spinner = await twd.get("[data-testid='loading-spinner']"); spinner.should("be.visible"); // Wait for the request to complete await twd.waitForRequest("getUser"); // After the response arrives, loading should be gone await twd.notExists("[data-testid='loading-spinner']"); const userName = await twd.get("[data-testid='user-name']"); userName.should("have.text", "John Doe"); }); ``` ::: tip The `delay` only affects how long the browser waits for the HTTP response. The `EXECUTED` notification still fires immediately, so `twd.waitForRequest()` resolves right away -- it does not wait for the delay. This mirrors real network behavior: the server receives the request instantly, but the response takes time to arrive. ::: ### Delay with Error Responses ```ts it("should handle timeout-like errors", async () => { await twd.mockRequest("slowError", { method: "GET", url: "/api/data", response: { error: "Gateway Timeout" }, status: 504, delay: 3000, // Simulate a 3-second timeout }); await twd.visit("/data-page"); // Verify the app shows appropriate timeout messaging await twd.waitForRequest("slowError"); await twd.wait(3100); // Wait for the delayed response to arrive const errorMessage = await twd.get(".error-message"); errorMessage.should("contain.text", "Gateway Timeout"); }); ``` ## Asserting Request Count TWD tracks how many times each mock rule has been matched. This lets you assert that an API was called the expected number of times. ### `getRequestCount(alias)` Returns the number of times a specific mock rule was hit. ```ts it("should call the API exactly twice", async () => { await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1, name: "John" }, }); await twd.visit("/profile"); // Trigger two requests const refreshButton = await twd.get("button[data-testid='refresh']"); await userEvent.click(refreshButton.el); await twd.waitForRequest("getUser"); // Re-register to reset executed flag for second wait await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1, name: "John" }, }); await userEvent.click(refreshButton.el); await twd.waitForRequest("getUser"); // Assert the count expect(twd.getRequestCount("getUser")).to.equal(2); }); ``` ### `getRequestCounts()` Returns a snapshot of all mock rule hit counts as an object. ```ts it("should track counts for multiple endpoints", async () => { await twd.mockRequest("getUsers", { method: "GET", url: "/api/users", response: [], }); await twd.mockRequest("getSettings", { method: "GET", url: "/api/settings", response: { theme: "dark" }, }); await twd.visit("/dashboard"); await twd.waitForRequests(["getUsers", "getSettings"]); const counts = twd.getRequestCounts(); expect(counts).to.deep.equal({ getUsers: 1, getSettings: 1, }); }); ``` ### Counter Reset Counters are automatically reset when you call `twd.clearRequestMockRules()`. This happens naturally in `beforeEach` cleanup: ```ts describe("API Tests", () => { beforeEach(() => { twd.clearRequestMockRules(); // Also resets all counters }); it("starts with zero counts", () => { expect(twd.getRequestCount("anyAlias")).to.equal(0); expect(twd.getRequestCounts()).to.deep.equal({}); }); }); ``` ## Dynamic Mocking ### Updating Mocks Mid-Test ```ts it("should handle changing API responses", async () => { // Initial mock await twd.mockRequest("getStatus", { method: "GET", url: "/api/status", response: { status: "loading" } }); await twd.visit("/dashboard"); const refreshButton = await twd.get("button[data-testid='refresh']"); await userEvent.click(refreshButton.el); await twd.waitForRequest("getStatus"); let statusText = await twd.get("[data-testid='status']"); statusText.should("have.text", "loading"); // Update the mock await twd.mockRequest("getStatus", { method: "GET", url: "/api/status", response: { status: "completed" } }); // Trigger another request await userEvent.click(refreshButton.el); await twd.waitForRequest("getStatus"); statusText = await twd.get("[data-testid='status']"); statusText.should("have.text", "completed"); }); ``` ### Conditional Responses ```ts it("should handle authentication states", async () => { // Mock unauthorized response first await twd.mockRequest("getProfile", { method: "GET", url: "/api/profile", response: { error: "Unauthorized" }, status: 401 }); await twd.visit("/profile"); // Should redirect to login await twd.wait(100); await twd.url().should("contain.url", "/login"); // Now mock successful login await twd.mockRequest("login", { method: "POST", url: "/api/login", response: { token: "abc123", user: { id: 1, name: "John Doe" } } }); // Mock authorized profile request await twd.mockRequest("getProfile", { method: "GET", url: "/api/profile", response: { id: 1, name: "John Doe", email: "john@example.com" } }); // Login and verify const user = userEvent.setup(); await user.type(await twd.get("#username"), "john"); await user.type(await twd.get("#password"), "password"); await user.click(await twd.get("button[type='submit']")); await twd.waitForRequest("login"); // Should now access profile await twd.visit("/profile"); await twd.waitForRequest("getProfile"); const profileName = await twd.get("[data-testid='profile-name']"); profileName.should("have.text", "John Doe"); }); ``` ## Request Inspection ### Verifying Request Data ```ts it("should send correct form data", async () => { await twd.mockRequest("submitForm", { method: "POST", url: "/api/contact", response: { success: true } }); await twd.visit("/contact"); const user = userEvent.setup(); // Fill form await user.type(await twd.get("#email"), "test@example.com"); await user.type(await twd.get("#message"), "Hello world"); await user.click(await twd.get("#newsletter")); // Submit await user.click(await twd.get("button[type='submit']")); // Verify request data const rule = await twd.waitForRequest("submitForm"); expect(rule.request).to.deep.equal({ email: "test@example.com", message: "Hello world", newsletter: true }); }); ``` ### Verifying Request Body ```ts it("should send the correct request body", async () => { await twd.mockRequest("authenticatedRequest", { method: "POST", url: "/api/protected", response: { data: "secret data" } }); await twd.visit("/protected"); const loadButton = await twd.get("button[data-testid='load-protected']"); await userEvent.click(loadButton.el); const rule = await twd.waitForRequest("authenticatedRequest"); // rule.request IS the parsed body directly (not rule.request.body) expect(rule.request).to.deep.equal({ token: "bearer-token-123" }); }); ``` ::: warning `rule.request` contains the parsed request body directly — **not** a request object with a `.body` property. Use `rule.request.fieldName`, not `rule.request.body.fieldName`. ::: ## Mock Management ### Clearing Mocks ::: tip You can also clear all mocks (both API request mocks and component mocks) at any time by clicking the **Clear mocks** button in the TWD sidebar header, next to the "Run All" button. This is useful for quick manual resets during development without modifying test code. ::: ```ts describe("User Management", () => { // Clear mocks before each test beforeEach(() => { twd.clearRequestMockRules(); }); it("should handle user creation", async () => { // This test starts with clean mocks await twd.mockRequest("createUser", { method: "POST", url: "/api/users", response: { id: 1, created: true } }); // Test implementation... }); // Mocks are automatically cleared before the next test }); ``` ### Inspecting Active Mocks ```ts it("should have correct mocks configured", async () => { await twd.mockRequest("getUsers", { method: "GET", url: "/api/users", response: [] }); await twd.mockRequest("createUser", { method: "POST", url: "/api/users", response: { id: 1 } }); // Check active mocks const activeMocks = twd.getRequestMockRules(); expect(activeMocks).to.have.length(2); const getUsersMock = activeMocks.find(mock => mock.alias === "getUsers"); expect(getUsersMock?.method).to.equal("GET"); }); ``` ## Common Patterns ### Error Handling ```ts it("should display error message on API failure", async () => { await twd.mockRequest("failedRequest", { method: "GET", url: "/api/data", response: { error: "Server error", message: "Something went wrong" }, status: 500 }); await twd.visit("/data-page"); const loadButton = await twd.get("button[data-testid='load-data']"); await userEvent.click(loadButton.el); await twd.waitForRequest("failedRequest"); // Verify error display const errorMessage = await twd.get(".error-message"); errorMessage.should("be.visible"); errorMessage.should("contain.text", "Something went wrong"); // Verify retry button appears const retryButton = await twd.get("button[data-testid='retry']"); retryButton.should("be.visible"); }); ``` ### Pagination ```ts it("should handle paginated results", async () => { // Mock first page await twd.mockRequest("getPage1", { method: "GET", url: "/api/users?page=1", response: { users: [ { id: 1, name: "User 1" }, { id: 2, name: "User 2" } ], pagination: { page: 1, hasNext: true, total: 3 } } }); // Mock second page await twd.mockRequest("getPage2", { method: "GET", url: "/api/users?page=2", response: { users: [ { id: 3, name: "User 3" }, ], pagination: { page: 2, hasNext: false, total: 3 } } }); await twd.visit("/users"); // Load first page await twd.waitForRequest("getPage1"); let users = await twd.getAll(".user-item"); expect(users).to.have.length(2); // Load next page const nextButton = await twd.get("button[data-testid='next-page']"); await userEvent.click(nextButton.el); await twd.waitForRequest("getPage2"); // Should now have 4 users total users = await twd.getAll(".user-item"); expect(users).to.have.length(1); }); ``` ## Best Practices ### 1. Use Descriptive Aliases ```ts // Good ✅ await twd.mockRequest("getUserProfile", { /* ... */ }); await twd.mockRequest("updateUserSettings", { /* ... */ }); // Bad ❌ twd.mockRequest("req1", { /* ... */ }); twd.mockRequest("api2", { /* ... */ }); ``` ### 2. Mock Realistic Data ```ts // Good ✅ - Realistic user data await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { id: 123, name: "John Doe", email: "john.doe@example.com", avatar: "https://example.com/avatar.jpg", createdAt: "2024-01-15T10:30:00Z", role: "user" } }); // Bad ❌ - Minimal/unrealistic data twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { name: "test" } }); ``` ### 3. Mock before request event is fired ```ts // Good ✅ - mock before request event is fired await twd.mockRequest("saveUser", { method: "POST", url: "/api/users", response: { id: 1, created: true } }); await twd.visit("/users/new"); const user = userEvent.setup(); await user.type(await twd.get("#name"), "John Doe"); await user.type(await twd.get("#email"), "john.doe@example.com"); await user.click(await twd.get("button[type='submit']")); await twd.waitForRequest("saveUser"); const userName = await twd.get("[data-testid='user-name']"); userName.should("have.text", "John Doe"); ``` ```ts // Bad ❌ - mock after request event is fired await twd.visit("/users/new"); const user = userEvent.setup(); await user.type(await twd.get("#name"), "John Doe"); await user.type(await twd.get("#email"), "john.doe@example.com"); await user.click(await twd.get("button[type='submit']")); // Bad ❌ - mock after request event is fired twd.mockRequest("saveUser", { method: "POST", url: "/api/users", response: { id: 1 } }); await twd.waitForRequest("saveUser"); ``` ### 3. Clean Up Mocks ```ts describe("API Tests", () => { // Always clean up after each test afterEach(() => { twd.clearRequestMockRules(); }); // Or clean up before each test beforeEach(() => { twd.clearRequestMockRules(); }); }); ``` ### 4. Test Error Scenarios ```ts describe("Error Handling", () => { it("should handle network errors", async () => { twd.mockRequest("networkError", { method: "GET", url: "/api/data", response: { error: "Network error" }, status: 0 // Simulate network failure }); // Test your error handling... }); it("should handle server errors", async () => { twd.mockRequest("serverError", { method: "GET", url: "/api/data", response: { error: "Internal server error" }, status: 500 }); // Test your error handling... }); }); ``` ## Troubleshooting ### Mock Not Triggering 1. **Check in console mock-sw.js version** - Should log `[TWD] Mock Service Worker loaded - version x.x.x` the same version as your `twd-js` package 2. **Ensure mocking is initialized** - Call `twd.initRequestMocking()` 3. **Check the URL pattern** - Make sure it matches exactly 4. **Verify the HTTP method** - GET, POST, PUT, DELETE must match 5. **Check browser console** - Look for service worker errors ### Request Not Matching ```ts // Use RegExp for flexible matching await twd.mockRequest("flexibleMatch", { method: "GET", url: /\/api\/users\/\d+/, // Matches any user ID response: { /* ... */ } }); ``` ### Service Worker Issues 1. **Reinstall service worker**: Run `npx twd-js init public` again 2. **Check public directory**: Ensure `mock-sw.js` exists 3. **Browser cache**: Try hard refresh (Ctrl+Shift+R) ## Next Steps - Learn about [User Events in the Tutorial](/tutorial/first-test) for form interactions - Check the [API Reference](/api/twd-commands) for all mocking methods # Component Testing Source: https://twd.dev/component-testing Call Testing Library's `render()` inside a TWD test and the component mounts into your running app, in your real browser. Same Testing Library API you already use, no simulated DOM underneath. ::: tip Not to be confused with Component Mocking This page is about **rendering a component in isolation and testing it**. If you want to **replace** a child component with a stub, see [Component Mocking](/component-mocking). ::: ## Setup ### 1. Install Testing Library TWD does not ship Testing Library's framework renderers. Install the one for your framework as a dev dependency: ```bash npm install --save-dev @testing-library/react ``` ### 2. Make the test pattern match `.tsx` Component tests are `.tsx`. The `twd()` plugin's default `testFilePattern` is `'/**/*.twd.test.ts'`, which matches `.ts` only, so `.tsx` files are skipped **silently**: no error, no warning, and nothing appears in the sidebar. ```ts // vite.config.ts import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ // ... your other plugins twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' }), ], }); ``` ### 3. Exclude TWD tests from Vitest If the same repo also runs Vitest, it matches `*.test.tsx` by default, collects your TWD files, finds no `describe` it recognises, and fails the run with `No test suite found in file`. Here is the complete config combining both steps: ```ts // vite.config.ts (complete) import { defineConfig, configDefaults } from 'vitest/config'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ // ... your other plugins twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' }), ], test: { exclude: [...configDefaults.exclude, '**/*.twd.test.*'], }, }); ``` ### 4. Add the component host `render()` appends its container to `document.body`, which puts your component after the app root, a full viewport below the layout. The app's own DOM is also still on the page, so `screen` matches its elements as well as the ones your test rendered. One helper solves both, and your app does not change: ```ts // twd-tests/support/componentHost.ts const HOST_ID = 'twd-component-host'; const APP_ROOT_ID = 'root'; // 'app' in a default Vue app let appRoot: HTMLElement | null = null; let placeholder: Comment | null = null; /** The element component tests render into: a blank div on an empty page. */ export function componentHost(): HTMLElement { detachApp(); let host = document.getElementById(HOST_ID); if (!host) { host = document.createElement('div'); host.id = HOST_ID; } if (!host.isConnected) { document.body.prepend(host); } host.innerHTML = ''; return host; } /** Removes the host and puts the app back. Call it in afterEach. */ export function restorePage(): void { document.getElementById(HOST_ID)?.remove(); attachApp(); } function detachApp(): void { if (placeholder) return; const root = document.getElementById(APP_ROOT_ID); if (!root) return; appRoot = root; placeholder = document.createComment(' app detached by twd component test '); root.replaceWith(placeholder); } function attachApp(): void { if (!placeholder || !appRoot) return; placeholder.replaceWith(appRoot); placeholder = null; appRoot = null; } ``` Two details in there matter. **Detaching the app root is not the same as emptying it.** `root.innerHTML = ''` pulls the DOM out from under your framework while it still holds references to those nodes, and the app does not come back. Moving the node out and putting it back leaves those references intact, so `restorePage()` returns a live app. **The host is prepended, not appended.** That puts the component at the top of the page, where you can watch it run without scrolling, and keeps it in normal flow so it sits inside the offset TWD applies for its sidebar. The payoff is that `screen` behaves exactly as it does in jsdom: the only thing in the document is what your test rendered. That also covers content your component sends through a portal or a `Teleport`, which lands on `document.body` rather than inside the host. ::: tip This replaces the blank-route approach An earlier version of this page recommended declaring an empty route and visiting it with `twd.visit()`. The host does the same job without a route, without `twd.visit()`, and without touching your app. ::: ### 5. Clean up between tests `render()` appends to the document and removes nothing on its own. In jsdom the environment is torn down for you between files. In a real browser it is not, so renders stack up and queries start finding duplicates. ```tsx import { cleanup } from "@testing-library/react"; import { afterEach } from "twd-js/runner"; import { restorePage } from "./support/componentHost"; afterEach(() => { cleanup(); restorePage(); }); ``` Use `afterEach`, not `beforeEach`. TWD runs after-hooks in a `finally`, so this still runs when a test fails, and it puts the app back before your flow tests need it. If you also mock requests, `twd.clearRequestMockRules()` belongs in `beforeEach` as usual. That is the whole setup. ## Your first component test ```tsx import { render, screen, cleanup } from "@testing-library/react"; import { describe, it, afterEach } from "twd-js/runner"; import { twd } from "twd-js"; import { AppProvider } from "@/context/AppContext"; import { Add } from "../Add"; import { componentHost, restorePage } from "./support/componentHost"; describe("Add Component", () => { afterEach(() => { cleanup(); restorePage(); }); it("renders the Add component", () => { render(, { container: componentHost() }); twd.should(screen.getByText("Add Item"), "be.visible"); }); }); ``` `AppProvider` here is the real provider, not a test double. `Add` uses a hook that reads from context and posts to an API, and in a real browser both of those work. `twd.should` accepts any element you hand it, whether a query found it in your app or in a component you just rendered. ## Queries: use `screen`, not `screenDom` {#queries} ::: warning `screenDom` will not find your rendered component `render()` mounts outside the app root that `screenDom` scopes to, and while a component test runs that root is not even in the document. Queries will fail. ::: Every other page in these docs steers you to `screenDom`, because for flow tests it is the right default: it excludes the TWD sidebar. Component tests are the exception. Use one of these instead: - **Testing Library's own `screen`** (recommended). It queries `document.body`, which is where `render()` put your component. - **TWD's `screenDomGlobal`**, which also queries the whole document. If you pick this one, keep queries specific, because it can also match elements inside the TWD sidebar. ```ts // Works: render() mounted into document.body screen.getByText("Add Item"); screenDomGlobal.getByRole("button", { name: "Add Item" }); // Does not work: scoped to the app root, which is not where render() mounted screenDom.getByText("Add Item"); ``` ## Mocking `twd.mockRequest` still works and is still the right tool. It replaces the network, which is a boundary you do not own, and everything on your side of it still runs: component, hook, provider, router. ```ts await twd.mockRequest("createCar", { url: "/api/cars", method: "POST", status: 201, response: { id: "test-1", model: "Golf", year: "2023" }, }); ``` The rule this follows: **mock at boundaries you do not own, and nowhere else.** The network, the clock, third-party services. When you stub a hook, a context, or a component out of your own `src/`, the stub sits between the assertion and the behaviour you meant to check. ## Choosing between component and flow tests {#choosing-between-component-and-flow-tests} Rendering a component in isolation is the right move when the component is the subject: a form's validation states, a dialog that opens and closes, a table that sorts. You skip the navigation and the fixtures, and the test says exactly what it is about. Flow tests stay the right move for anything that crosses a boundary: routing, data loading, a sequence of screens, state that survives a navigation. Rendering a component in isolation to test those means rebuilding the app around it. Neither replaces the other. The useful change is that choosing between them is a decision about scope, made per test. ## One run, one coverage report Component tests and flow tests are files in the same project, running in the same browser, in the same session, against the same instrumented bundle. One command runs both: ``` $ npx twd-cli run Running 14 test(s)... Code coverage data written to .nyc_output/out.json --- Run complete --- Passed: 14 | Failed: 0 | Skipped: 0 Duration: 6.9s ``` One coverage file comes out, covering both styles. There is nothing to merge, because there was only ever one run. ## Other frameworks The examples above use `@testing-library/react`, which is what has been verified. `@testing-library/vue` and `@testing-library/solid` take the same `container` option and default to `document.body` the same way, so the helper transfers with one change: `APP_ROOT_ID` is `'app'` in a default Vue app rather than `'root'`. Neither is verified end to end yet. If you try one, [open an issue](https://github.com/BRIKEV/twd/issues) and tell us how it went. `@testing-library/angular` is a different shape. It mounts through `TestBed` and does not accept a `container`, so the helper does not apply as written. Not tried. ## Troubleshooting **My tests do not appear in the sidebar.** Your `testFilePattern` probably ends in `.ts`. Component tests are `.tsx`. See [step 2](#setup). **Queries find two of everything.** You are rendering on top of the app. Pass `componentHost()` as the `container`, and call `cleanup()` and `restorePage()` in `afterEach`. See [step 4](#setup). **My app is blank after the component tests run.** `restorePage()` is missing from `afterEach`, so the app root was never put back. See [step 5](#setup). **`screenDom` cannot find my component.** Expected. Use `screen` or `screenDomGlobal`. See [Queries](#queries). **Vitest fails with `No test suite found in file`.** Vitest is collecting your TWD tests. Add the `exclude` entry from [step 3](#setup). ## Why this works Testing Library was never tied to jsdom. `@testing-library/react` renders a component into a DOM node, and `@testing-library/dom` queries it. jsdom is simply the DOM most people hand it. TWD already runs inside your app in the browser, so the real DOM is right there and `render()` uses it. The component is laid out on a screen, at a real size and a real position, with your CSS applied to it. It also means the providers and the network around your component are the real ones, so there is often nothing left to stand in for. ## Further reading - [No More Fake DOM: Testing Library Unit Tests in the Real Browser](https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77) - [frontend-challenge](https://github.com/kevinccbsg/frontend-challenge), which tests the same component in jsdom and in the browser side by side ## Next Steps - [Testing Library](/testing-library) for the full query and `userEvent` reference - [API Mocking](/api-mocking) for the service worker setup - [Coverage](/coverage) for collecting a report across both test styles # Component Mocking Source: https://twd.dev/component-mocking TWD provides powerful component mocking capabilities, allowing you to replace React components with mock implementations during testing. This is especially useful for isolating components, testing edge cases, or replacing complex dependencies with simpler test doubles. ::: tip Looking to test a component in isolation? This page is about **replacing** a component with a stub. To **render** a single component and test it in a real browser, see [Component Testing](/component-testing). ::: ## Overview Component mocking in TWD allows you to: - Replace components with mock implementations for isolated testing - Test how parent components handle different component behaviors - Simplify complex component dependencies during testing - Verify component prop passing and interaction ## Setup ### 1. Wrap Components with MockedComponent To make a component mockable, wrap it with the `MockedComponent` component and provide a unique name: ```tsx import { MockedComponent } from "twd-js/ui"; interface ButtonProps { onClick: (count: number) => void; count: number; } const Button = ({ onClick, count }: ButtonProps) => { return ( ); }; export default function CounterPage() { const [count, setCount] = useState(0); return (

Count: {count}

); } ``` ::: tip The `name` prop must be unique and match the name used in your test when calling `twd.mockComponent()`. ::: ### 2. Mock Components in Tests Use `twd.mockComponent()` to replace the component with a mock implementation: ```ts import { twd, userEvent } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; interface ButtonProps { onClick: (count: number) => void; count: number; } const Button = ({ onClick, count }: ButtonProps) => { return ( ); }; describe("Component Mocking", () => { beforeEach(() => { twd.clearComponentMocks(); }); it("should mock a component", async () => { // Mock the Button component to increment by 2 instead of 1 twd.mockComponent("Button", ({ onClick, count }: ButtonProps) => ( )); await twd.visit("/counter"); const button = await twd.get("button"); await userEvent.click(button.el); // Verify the mock behavior const countText = await twd.get("p"); countText.should("have.text", "Count: 10"); }); ``` ### Component Without Mock When a component is not mocked, it uses its original implementation: ```ts it("should use original component when not mocked", async () => { // Don't call twd.mockComponent() - component uses original behavior await twd.visit("/counter"); let button = await twd.get("button"); button.should("have.text", "Click me 0"); await userEvent.click(button.el); button = await twd.get("button"); button.should("have.text", "Click me 1"); const countText = await twd.get("p"); countText.should("have.text", "Count: 1"); }); ``` ## Advanced Usage ### Conditional Mocking You can create mocks that behave differently based on props: ```ts it("should handle conditional mocking", async () => { twd.mockComponent("UserCard", ({ user, onEdit }: UserCardProps) => { if (user.role === "admin") { return (

{user.name} (Admin)

); } return (

{user.name}

); }); await twd.visit("/users/123"); const adminCard = await twd.get("[data-testid='admin-card']"); adminCard.should("be.visible"); }); ``` ### Mocking with Different Rendering You can completely change what the component renders: ```ts it("should render completely different content", async () => { twd.mockComponent("ComplexChart", () => (

Chart data would be displayed here

)); await twd.visit("/dashboard"); const mockChart = await twd.get("[data-testid='mock-chart']"); mockChart.should("be.visible"); mockChart.should("contain.text", "Chart data would be displayed here"); }); ``` ### Testing Error States Mock components to simulate error scenarios: ```ts it("should handle component errors", async () => { twd.mockComponent("DataFetcher", ({ onError }: DataFetcherProps) => { // Simulate an error state return (

Failed to load data

); }); await twd.visit("/data-page"); const errorState = await twd.get("[data-testid='error-state']"); errorState.should("be.visible"); errorState.should("contain.text", "Failed to load data"); }); ``` ### Mocking Multiple Components You can mock multiple components in the same test: ```ts it("should mock multiple components", async () => { // Mock first component twd.mockComponent("Header", () => (
Mock Header
)); // Mock second component twd.mockComponent("Footer", () => (
Mock Footer
)); await twd.visit("/page"); const header = await twd.get("[data-testid='mock-header']"); const footer = await twd.get("[data-testid='mock-footer']"); header.should("be.visible"); footer.should("be.visible"); }); ``` ## Mock Management ### Clearing Mocks ::: tip You can also clear all component mocks (and API request mocks) at once by clicking the **Clear mocks** button in the TWD sidebar header, next to the "Run All" button. ::: Always clear component mocks between tests to ensure isolation: ```ts describe("Component Tests", () => { beforeEach(() => { // Clear all component mocks before each test twd.clearComponentMocks(); }); it("should test with mock", async () => { twd.mockComponent("Button", () => ); // Test implementation... }); it("should test without mock", async () => { // This test runs with clean state - no mocks active // Test implementation... }); }); ``` ## Best Practices ### 1. Use Descriptive Component Names ```ts // Good ✅ // Bad ❌ ``` ### 2. Clear Mocks Between Tests ```ts describe("Component Tests", () => { beforeEach(() => { twd.clearComponentMocks(); }); // Your tests... }); ``` ### 3. Keep Mock Implementations Simple ```ts // Good ✅ - Simple, focused mock twd.mockComponent("Button", ({ onClick }: ButtonProps) => ( )); // Bad ❌ - Overly complex mock that's hard to understand twd.mockComponent("Button", ({ onClick, count, disabled, className, ...rest }: ButtonProps) => { const [internalState, setInternalState] = useState(0); // Complex logic... }); ``` ### 4. Preserve Component Interface When mocking, try to maintain the same props interface: ```ts // Good ✅ - Maintains the same props twd.mockComponent("Button", ({ onClick, count }: ButtonProps) => ( )); // Bad ❌ - Changes the interface, might break parent component twd.mockComponent("Button", () => (
Completely different component
)); ``` ## Troubleshooting ### Mock Not Applied 1. **Check component name** - Ensure the name in `MockedComponent` matches the name in `twd.mockComponent()` 2. **Verify MockedComponent wrapper** - The component must be wrapped with `MockedComponent` 3. **Check mock timing** - Call `twd.mockComponent()` before visiting the page or before the component renders ```ts // Good ✅ - Mock before visiting twd.mockComponent("Button", () => ); await twd.visit("/page"); // Bad ❌ - Mock after component already rendered await twd.visit("/page"); twd.mockComponent("Button", () => ); ``` ## Next Steps - Learn about [API Mocking](/api-mocking) for testing network requests - Check the [Writing Tests Guide](/writing-tests) for more testing patterns - Review the [API Reference](/api/twd-commands) for complete method documentation # Module Mocking Source: https://twd.dev/module-mocking Authentication is one of the trickiest features to test in modern apps. Tools like Auth0 help a lot, especially with React, providing hooks like `useAuth0` to manage authentication. However, automating these tests is tricky. - With Cypress, testing across domains requires special configuration. - With Vitest, you need to mock data, but your tests still only return a simple pass/fail in the terminal. - You also need real users created and validated TWD (Testing While Developing) takes a different approach: it encourages testing **behavior**, not infrastructure. That means you don't need to test the auth provider itself — you just need to simulate the authentication behavior your app depends on. For React apps using Auth0 with PKCE flow, it's a bit more complicated, because we rely on the hook rather than simple requests. Let's see how we can fully control authentication in our tests. --- ## Tool for Spying and Mocking We'll use **Sinon**, a classic library for spies, stubs, and mocks. Why Sinon? Because it works directly in the browser — which is essential for TWD. ## Understand the Auth0 Hook Here's the basic usage in a React component: ```ts import { useAuth0 } from "@auth0/auth0-react"; // get info of user in the app const { user, getAccessTokenSilently, loginWithRedirect, logout } = useAuth0(); ``` These methods allow your app to: - Log in and log out users - Get user info - Get tokens to send to your API ## Move Auth Logic to a Separate File To stub the hook in TWD tests, we create a small wrapper: ```ts import { useAuth0 } from "@auth0/auth0-react"; const useAuth = () => useAuth0(); export default { useAuth }; ``` **Important**: we export as **default** and as an **object**. Why? Because ESModules are immutable by default. Named exports like `export const useAuth = ...` **cannot be mocked at runtime**, but an object property can. This small tradeoff gives us complete control during tests. ## Stub the Hook in Your Tests Now, in your TWD test, you can use Sinon to control authentication behavior: ```ts import { beforeEach, describe, it, afterEach } from 'twd-js/runner'; import { twd, screenDom, userEvent, expect } from 'twd-js'; import authSession from '../hooks/useAuth'; import Sinon from 'sinon'; import userMock from './userMock.json'; import type { Auth0ContextInterface } from '@auth0/auth0-react'; describe('App tests', () => { beforeEach(() => { Sinon.resetHistory(); Sinon.restore(); twd.clearRequestMockRules(); }); afterEach(() => { twd.clearRequestMockRules(); }); it('should render home page for authenticated user', async () => { Sinon.stub(authSession, 'useAuth').returns({ isAuthenticated: true, isLoading: false, user: userMock, getAccessTokenSilently: Sinon.stub().resolves('fake-token'), loginWithRedirect: Sinon.stub().resolves(), logout: Sinon.stub().resolves(), } as unknown as Auth0ContextInterface); await twd.visit('/'); const welcomeText = await screenDom.findByRole('heading', { name: 'Authenticated area', level: 1 }); twd.should(welcomeText, 'be.visible'); const infoText = await screenDom.findByText('You are signed in with Auth0. Manage your profile and jot down quick notes below.'); twd.should(infoText, 'be.visible'); }); }); ``` With this setup, you can test any scenario: - User is logged in or logged out - User has different roles or data - Authentication errors --- ## Conclusion Exporting hooks as mutable objects and stubbing them with Sinon lets you run **browser tests** in TWD with full control over authentication behavior. This allows you to focus on **testing your app's behavior**, without worrying about the complexities of the auth provider itself. ## Examples - Full example React app with this flow: [twd-auth0-pkce](https://github.com/BRIKEV/twd-auth0-pkce) - Auth flow with backend sessions example: [twd-auth0](https://github.com/BRIKEV/twd-auth0) This approach isn't just for Auth0 — it works for any module you need to control in TWD browser tests. # Framework Integration Source: https://twd.dev/frameworks TWD runs your tests in the real browser, so it works with any frontend that renders there: SPAs like React, Vue, Angular, and Solid; hydrated SSR like React Router and Nuxt; Astro islands; and no-build projects like HTMX and vanilla JS, via a CDN. On Vite, Webpack, or no bundler at all. ## React TWD works seamlessly with any Vite-based React application. **We recommend using the `twd()` Vite plugin** — it auto-loads the sidebar and discovers test files in dev with no entry-file changes. Manual setup is available for projects that need full control. **[View React Examples](https://github.com/BRIKEV/twd/tree/main/examples)** - Multiple React examples available in the repository. ### Recommended: Vite Plugin Add the `twd()` plugin to your `vite.config.ts`: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', open: true, position: 'left', serviceWorker: true, // Enable request mocking (default: true) serviceWorkerUrl: '/mock-sw.js', // Custom service worker path (default: '/mock-sw.js') }), ], }); ``` The plugin only runs in `vite dev` (`apply: 'serve'`) — it's a no-op in production builds. #### twd() Plugin Options The `twd()` plugin accepts the following options: - **`testFilePattern`** (`string`, optional) - Glob pattern for discovering test files. Default: `'/**/*.twd.test.ts'`. This matches `.ts` only. For `.tsx` test files (including component tests) use `'/**/*.twd.test.{ts,tsx}'`. - **`open`** (`boolean`, optional) - Whether the sidebar is open by default. Default: `true` - **`position`** (`"left" | "right"`, optional) - Sidebar position. Default: `"left"` - **`serviceWorker`** (`boolean`, optional) - Whether to initialize request mocking. Default: `true` - **`serviceWorkerUrl`** (`string`, optional) - Custom path to the service worker file. Default: `'/mock-sw.js'` - **`theme`** (`Partial`, optional) - Custom theme configuration. See [Theming](/theming) for details. - **`search`** (`boolean`, optional) - Whether to show the search/filter input in the sidebar. Default: `false` - **`pace`** (`boolean`, optional) - Whether to show the execution speed selector in the sidebar, which slows a run down so you can watch it. Default: `false` **Examples:** ```ts // Minimal setup — uses all defaults twd(); // Custom sidebar configuration twd({ open: false, position: 'right' }); // Custom test file pattern twd({ testFilePattern: '/**/*.spec.{ts,tsx}' }); // Disable request mocking twd({ serviceWorker: false }); // Enable test filtering in the sidebar twd({ search: true }); // Enable the execution speed selector, to watch a run step by step twd({ pace: true }); // All options together twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', open: true, position: 'right', serviceWorker: true, serviceWorkerUrl: '/my-mock-sw.js', theme: { primary: '#2563eb', background: '#ffffff' }, }); ``` ::: tip Theming Learn more about customizing the TWD sidebar appearance in the [Theming](/theming) guide. ::: ### Alternative: Manual Bundled Setup If you need full control, you can call `initTWD` directly in your entry file instead of using the plugin. This is the same code the plugin runs internally: ```tsx // src/main.tsx if (import.meta.env.DEV) { const { initTWD } = await import('twd-js/bundled'); const tests = import.meta.glob('./**/*.twd.test.ts'); initTWD(tests, { open: true, position: 'left', serviceWorker: true, serviceWorkerUrl: '/mock-sw.js', }); } ``` `initTWD` accepts the same options as the plugin (minus `testFilePattern`, which is handled by `import.meta.glob` here). Use this approach when you need conditional init, custom test discovery, or any logic the plugin doesn't expose. ### Alternative: Standard Setup (React Only) The standard setup gives full control over the React root and request-mocking lifecycle. React-only. ```tsx // src/main.tsx if (import.meta.env.DEV) { const testModules = import.meta.glob("./**/*.twd.test.ts"); const { initTests, twd, TWDSidebar } = await import('twd-js'); // You need to pass the test modules, the sidebar component, and createRoot function initTests(testModules, , createRoot); // Initialize request mocking (optional) twd.initRequestMocking().catch(console.error); } ``` ::: tip For Vue, Solid.js, and other Vite-based frameworks, use the `twd()` plugin (recommended) or the manual bundled setup. The standard setup above is React-only. ::: ## Testing shadcn Components If you're using shadcn/ui components in your React application, we've created a comprehensive guide with TWD patterns specifically for testing shadcn components: **[shadcn Testing Guide](https://brikev.github.io/twd-shadcn/)** - Patterns and best practices for testing shadcn/ui components with TWD. ## Vue For Vue applications, use the `twd()` Vite plugin. The plugin is framework-agnostic and the bundled version it uses internally ships React separately, so it doesn't conflict with your Vue runtime. **[Vue Example Repository](https://github.com/BRIKEV/twd-vue-example)** - Complete working example with advanced scenarios. ```ts // vite.config.ts import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ vue(), twd({ testFilePattern: '/**/*.twd.test.ts', open: true, position: 'left', }), ], }); ``` Your `src/main.ts` stays untouched — no `initTWD` import needed: ```ts // src/main.ts import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app'); ``` ### Alternative: Manual Bundled Setup (Vue) If you can't use the Vite plugin, fall back to manual `initTWD` in `src/main.ts`: ```ts import { createApp } from 'vue'; import App from './App.vue'; if (import.meta.env.DEV) { const { initTWD } = await import('twd-js/bundled'); const tests = import.meta.glob('./**/*.twd.test.ts'); initTWD(tests, { open: true, position: 'left' }); } createApp(App).mount('#app'); ``` ## Solid For Solid.js applications, use the `twd()` Vite plugin. The bundled version handles its React runtime internally and doesn't conflict with your Solid runtime. **[Solid Example Repository](https://github.com/BRIKEV/twd-solid-example)** - Complete Solid.js integration example. ```ts // vite.config.ts import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ solid(), twd({ testFilePattern: '/**/*.twd.test.ts', open: true, position: 'left', }), ], }); ``` Your `src/main.tsx` stays untouched: ```tsx // src/main.tsx /* @refresh reload */ import { render } from 'solid-js/web'; import App from './App'; const root = document.getElementById('root'); if (!(root instanceof HTMLElement)) { throw new Error('Root element not found.'); } render(() => , root); ``` ### Notes for Solid - This setup works with **Solid + Vite** applications. - Solid Start compatibility has not been tested yet, but may work with similar configuration. ## Angular Angular CLI uses esbuild, not vanilla Vite, so the `twd()` Vite plugin doesn't apply. Angular projects use the manual bundled setup with `initTWD`. You'll typically need to build the `tests` object explicitly since Angular's build tooling doesn't support `import.meta.glob` the same way. **[Angular Example Repository](https://github.com/BRIKEV/twd-angular-example)** - Working Angular integration example. ```ts // src/main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { App } from './app/app'; // Replaced at build time by the `define` option in angular.json (see below). // Declared as possibly undefined so a missing `define` cannot throw at module scope. declare const TWD_ENABLED: boolean | undefined; if (typeof TWD_ENABLED !== 'undefined' && TWD_ENABLED) { const { initTWD } = await import('twd-js/bundled'); // Define your test files manually or use a compatible glob importer const tests = { './twd-tests/helloWorld.twd.test.ts': () => import('./twd-tests/helloWorld.twd.test'), './twd-tests/todoList.twd.test.ts': () => import('./twd-tests/todoList.twd.test'), }; // Initialize TWD - request mocking is automatically initialized by default initTWD(tests, { open: true, position: 'left' }); } else if (typeof TWD_ENABLED === 'undefined') { console.warn('[TWD] TWD_ENABLED is not defined — add the `define` option to angular.json.'); } bootstrapApplication(App, appConfig) .catch((err) => console.error(err)); ``` Then declare `TWD_ENABLED` in `angular.json` — **off by default**, opted into under `development` only: ```jsonc "build": { "options": { "define": { "TWD_ENABLED": "false" } }, "configurations": { "development": { "define": { "TWD_ENABLED": "true" } } } } ``` ::: warning Don't use `isDevMode()` as the guard `isDevMode()` works at runtime, but it is a *function call* — esbuild cannot prove the branch is dead, so it keeps the branch and emits every `await import()` inside it as a lazy chunk. In the Angular example that is **+580 K in `dist/` (332 K → 912 K, 2 chunks → 10)**, including a copy of React the app never executes. The chunks are never fetched at runtime, so it is dead weight in the deployed artifact rather than a performance bug — and it is easy to miss, because the *initial* bundle only grows by ~1 kB, so budgets and Lighthouse stay quiet. A `define` constant is a literal by the time esbuild sees it, so the whole block is dead-code eliminated. ::: ::: danger Keep the `typeof` check A bare `if (TWD_ENABLED)` throws `TWD_ENABLED is not defined` at module scope if the `define` is missing or a new build configuration forgets it — that happens *before* `bootstrapApplication`, so the page renders nothing at all. `typeof TWD_ENABLED !== 'undefined' && TWD_ENABLED` boots normally and warns instead. ::: ### Angular + twd-relay On Vite, `twdRemote()` attaches the relay WebSocket to the dev server and injects the browser client for you. Angular CLI has no Vite plugin, so nothing serves `/__twd/ws` on the app's port (4200) — you run the relay standalone and point the client at it explicitly. ```ts // src/main.ts — inside the TWD_ENABLED block, after initTWD(...) const { createBrowserClient } = await import('twd-relay/browser'); createBrowserClient({ url: 'ws://localhost:9876/__twd/ws' }).connect(); ``` ```jsonc // package.json "scripts": { "relay": "npx twd-relay run --port 9876", "relay:serve": "npx twd-relay serve --port 9876" } ``` - **Pass `--port 9876` to both commands.** `twd-relay serve` listens on `9876` by default, but `twd-relay run` defaults to `5173` (Vite's port). Left mismatched, `run` connects to nothing. - **Use the explicit `ws://localhost:9876/__twd/ws` URL**, not `` `${window.location.origin}/__twd/ws` `` — on Angular the app's own origin does not serve the relay. - **A browser tab must be open on the app** for a run to do anything. If none is connected, `twd-relay run` waits until it times out with `Timeout: no run:complete received within 180s`. - The tab title is prefixed **`[TWD]`** when the client is connected — the fastest way to confirm it attached. ## Create React App (CRA) Create React App uses Webpack instead of Vite, so there is no `twd()` plugin — you initialize TWD manually with `initTWD` from `twd-js/bundled` and discover test files with Webpack's `require.context`. **[CRA Example](https://github.com/BRIKEV/twd-create-react-app)** - Complete Create React App integration with react-router loaders/actions, json-server, CI execution, and contract validation. First, install the mock service worker into your public directory: ```bash npx twd-js init public ``` Then initialize TWD in your entry file: ```js // src/index.js (or src/index.tsx) if (process.env.NODE_ENV === "development") { // Use Webpack's context feature to load all test files const context = require.context("./", true, /\.twd\.test\.ts$/); // Build a Vite-like object of async importers const testModules = {}; context.keys().forEach((key) => { testModules[key] = async () => { // Webpack requires modules synchronously, so wrap in Promise.resolve return Promise.resolve(context(key)); }; }); const { initTWD } = await import('twd-js/bundled'); initTWD(testModules, { open: true, search: true, serviceWorker: true, serviceWorkerUrl: '/mock-sw.js', }); } ``` `initTWD` accepts the same options as the Vite plugin (minus `testFilePattern`, which is handled by the `require.context` regex here). ### Notes for CRA - **TypeScript test files work in a JavaScript project.** CRA's Babel pipeline strips types from `.twd.test.ts` files even without the `typescript` package installed — you just don't get type checking on them. - **Exclude TWD tests from Jest.** CRA's Jest test matcher also picks up `*.twd.test.ts` files, but they only run in the browser. Exclude them in your `test` script: ```json "test": "react-scripts test --testPathIgnorePatterns=src/twd-tests" ``` - **Recommended: relax Jest/Testing Library ESLint rules for TWD tests.** CRA's `react-app/jest` ESLint preset treats TWD test files as Jest tests and flags chai-style assertions like `expect(...).to.deep.equal(...)` with `jest/valid-expect`, which breaks dev-server compilation. Add an override to `eslintConfig` in `package.json`: ```json "eslintConfig": { "extends": ["react-app", "react-app/jest"], "overrides": [ { "files": ["src/twd-tests/**/*", "**/*.twd.test.*"], "settings": { "testing-library/utils-module": "off", "testing-library/custom-renders": "off", "testing-library/custom-queries": "off" }, "rules": { "jest/valid-expect": "off", "jest/valid-expect-in-promise": "off" } } ] } ``` - This approach also works for other Webpack-based React setups. ## Astro Astro uses Vite under the hood, so you can register the `twd()` plugin via Astro's `vite.plugins` config block. **[Astro Example](https://github.com/BRIKEV/twd/tree/main/examples/astro-example)** - Astro + React integration example. ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ integrations: [react()], vite: { plugins: [ twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', open: true, position: 'left', }), ], }, }); ``` The plugin handles test discovery, sidebar mounting, and HMR full-reload — no per-page component or `useEffect` boilerplate required. ## React Router (Framework Mode) TWD works with React Router v7 in framework mode (including SSR), but the setup has one quirk that other Vite-based SPAs don't. The `twd()` and `twdRemote()` plugins inject their bootstrap scripts via Vite's `transformIndexHtml` hook — and that hook never fires for SSR-framework HTML, because React Router's dev middleware renders `app/root.tsx` itself instead of serving a Vite-controlled `index.html`. Plugin auto-injection silently no-ops. The fix is to point at the virtual modules manually from `app/root.tsx`. The rest of the testing model is the same as any TWD project: use `createRoutesStub` to mount a route with stubbed loaders/actions, and let your backend tests cover the real loader/action code as plain async functions. ::: info TWD focuses on **client-side UI behavior**. Server-side loaders and actions are tested separately as pure functions — they're plain async code that returns data. ::: **[React Router + TWD Example](https://github.com/BRIKEV/twd-react-router)** — complete working repo, including CI with `twd-cli`. ### Setup 1. **Add the plugins to `vite.config.ts`**: ```ts // vite.config.ts import { defineConfig, type PluginOption } from 'vite'; import { reactRouter } from '@react-router/dev/vite'; import { twd } from 'twd-js/vite-plugin'; import { twdRemote } from 'twd-relay/vite'; export default defineConfig({ plugins: [ reactRouter(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', serviceWorker: false, // unless you actually use API mocking }), twdRemote() as PluginOption, // optional, for twd-relay / AI agents ], server: { warmup: { clientFiles: [ './app/twd-tests/**/*.twd.test.{ts,tsx}', './app/root.tsx', ], }, }, optimizeDeps: { include: ['twd-js/bundled', 'twd-relay/browser'], }, }); ``` The `server.warmup` and `optimizeDeps` blocks aren't cosmetic — they're a workaround for SSR mode. Vite's dep scanner doesn't walk virtual modules, so it discovers their transitive deps lazily on the first browser request, optimizes them, and triggers an auto-reload to serve the optimized bundle. Locally that's a quick refresh; in headless CI (twd-cli + Puppeteer with a 10s timeout) it's a hard failure — the bootstrap restarts mid-load and `waitForSelector('#twd-sidebar-root')` times out. Warming the client files and pre-bundling the two entries means no first-request optimization fires, so the sidebar mounts on first hit both locally and in CI. 2. **Inject the bootstrap scripts in `app/root.tsx`**, dev-only: ```tsx // app/root.tsx — inside 's {import.meta.env.DEV && ( <> ``` The plugin still registers the virtual module and Vite still serves it at `/@id/...` — you're just pointing the SSR'd template at it. Note the `/_nuxt/` prefix: that's Nuxt's Vite `base`. The `import.meta.dev` guard keeps the tag out of production. Run `npm run dev` and the sidebar mounts next to your app. ::: tip Testing against a real backend Nuxt runs `useFetch` and `$fetch` as real browser requests on client navigation, so you can test pages against your real Nitro routes and database instead of mocking them. A dev-only reset endpoint (guarded by `import.meta.dev`) gives each test a clean starting point. The [example repo](https://github.com/BRIKEV/twd-nuxt-example) shows the full pattern. ::: ## Vanilla JS (CDN, no bundler) If your project has no build step at all (a plain HTML page, a static site, or anything served as-is), you can load TWD straight from a CDN. No `npm install`, no bundler. TWD's bundled entry is self-contained, so an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) is all you need. **[Vanilla JS Example Repository](https://github.com/BRIKEV/twd-vanillajs)** - Complete no-build example with a counter page, a todo list, API mocking, and CI. Add an import map and boot TWD from your entry HTML: ```html
``` Your test files import from the same bare specifiers the import map defines: ```js // tests/helloWorld.twd.js import { twd, userEvent, screenDom } from 'twd-js'; import { describe, it } from 'twd-js/runner'; describe('Counter', () => { it('increments on click', async () => { const button = screenDom.getByRole('button', { name: /clicks:/i }); await userEvent.setup().click(button); twd.should(button, 'have.text', 'Clicks: 1'); }); }); ``` ### Notes for Vanilla JS - **Use esm.sh.** It resolves TWD's internal React dependency automatically, so the import map stays to three lines. Pin the version (`twd-js@1.8.2`) so loads stay deterministic. - **The service worker is served from your own origin.** Browsers only register same-origin service workers, so the library loads from the CDN but `mock-sw.js` must sit on your domain. Run `npx twd-js init public` (or download it from the CDN) and point `serviceWorkerUrl` at it. Request mocking then works exactly like a bundled setup. - **Give TWD an app root.** With no framework root element, wrap your app in a known root such as `
` (or pass `rootSelector` to `initTWD`) so `screenDom` queries stay scoped. - **Tests survive a migration.** These are plain Testing Library queries and TWD commands. If you later move this app to React, Vue, or Solid, the same test files keep working, only the setup changes. ## HTMX (CDN, no bundler) [HTMX](https://htmx.org/) apps have no build step either, so TWD loads from a CDN the same way as [Vanilla JS](#vanilla-js-cdn-no-bundler) above. Load HTMX, then add the TWD import map alongside it. **[HTMX Example Repository](https://github.com/BRIKEV/twd-htmx)** - Complete no-build HTMX example with a counter, a todo list, a small HTML backend, and CI. ```html
    ``` The TWD service worker intercepts HTMX's requests (service workers see XHR and `fetch` alike), so the CDN, service-worker, and app-root notes from [Vanilla JS](#notes-for-vanilla-js) apply here too. ### Notes for HTMX - **HTMX swaps HTML, so test against a real backend.** HTMX expects endpoints to return HTML fragments, not JSON. The cleanest way to test that is to run against your real HTML-returning backend and reset it between tests, the same pattern the [Nuxt example](#nuxt) uses. A dev-only reset endpoint gives each test a clean starting point. - **API mocking is JSON-oriented today.** `twd.mockRequest` serializes responses as JSON, which fits JSON APIs. First-class HTML-fragment mocking for hypermedia frameworks is on the roadmap; until then, prefer real-backend testing for HTMX's HTML endpoints. ## Framework Support Philosophy TWD is designed for **deterministic frontend boundary validation**. It focuses on frameworks that provide: - **Explicit execution** - Clear control over when and how components render - **Deterministic behavior** - Predictable rendering and state management - **Fast feedback loops** - Quick test execution and hot module replacement TWD works with any frontend that renders in the browser and exposes an explicit, testable boundary. This includes SPAs, hydrated SSR frameworks like **React Router** and **Nuxt** where loaders and data fetching are explicit, Astro islands, and no-build projects served from a CDN. The one setup TWD does not target is where the server owns rendering and data-loading together (**React Server Components**, as in the **Next.js App Router**), since there is no explicit browser boundary to test there yet. TWD officially supports: - **React (SPA)** - Standard Vite-based React applications - **React Router (Framework Mode)** - Including SSR mode, with explicit loaders and `createRoutesStub` - **Nuxt (SSR)** - Nuxt 4 with client-side `useFetch`/`$fetch`, testable against the real backend - **Vue, Angular, Solid.js** - Other SPA frameworks - **Astro** - When used with client-driven components - **Vanilla JS and HTMX** - Any no-build project, loaded from a CDN with an import map ## Other Frameworks We're actively working on adding more framework recipes and integrations. If you're using a framework not listed here: 1. **Check if it's Vite-based** - If so, the standard Vite setup should work 2. **Check if it uses Webpack** - Adapt the CRA setup above 3. **Browse our [examples directory](https://github.com/BRIKEV/twd/tree/main/examples)** - See working examples for multiple frameworks 4. **Share your setup** - We'd love to hear about your integration! [Open an issue](https://github.com/BRIKEV/twd/issues) or [start a discussion](https://github.com/BRIKEV/twd/discussions) ## Framework Support Roadmap We plan to add official support and documentation for: - **Svelte** - Framework support in development ## Getting Help If you're having trouble integrating TWD with your framework: - 📖 Check the [Getting Started Guide](/getting-started) for the standard setup - 📚 Review the [Installation Tutorial](/tutorial/installation) for step-by-step instructions - 🐛 [Report issues](https://github.com/BRIKEV/twd/issues) if you encounter problems - 💬 [Join discussions](https://github.com/BRIKEV/twd/discussions) to share your setup or ask questions # Testing Library Source: https://twd.dev/testing-library TWD fully supports Testing Library's query methods and user event utilities, giving you access to the same powerful APIs used in traditional testing frameworks. ## Overview TWD provides two ways to select elements: 1. **TWD's native selectors** (`twd.get()`, `twd.getAll()`) - Simple CSS selector-based queries 2. **Testing Library** (`screenDom`, `userEvent`) - Accessible, semantic queries that follow testing best practices Both approaches work seamlessly together, and you can choose the one that best fits your needs. ## Rendering components directly Everything on this page assumes you are querying your running app. You can also call Testing Library's `render()` inside a TWD test to mount a single component in isolation, in the same real browser. One important difference: `render()` mounts into a fresh `div` on `document.body`, which is outside the app root, so **`screenDom` will not find it**. Use Testing Library's own `screen` or TWD's `screenDomGlobal` for rendered components. See [Component Testing](/component-testing) for the full setup. ## Screen Queries TWD provides two screen query APIs that give you access to all query methods from `@testing-library/dom`: 1. **`screenDom`** - Scoped queries that exclude the TWD sidebar (recommended for most use cases) 2. **`screenDomGlobal`** - Global queries that search the entire document.body (for portals/modals) ### Import ```ts import { screenDom, screenDomGlobal } from "twd-js"; ``` ## screenDom (Scoped Queries) `screenDom` searches only within the main app container (typically `#root`), automatically excluding the TWD sidebar. This is the recommended option for most queries. **Use `screenDom` when:** - Querying elements within your main application - You want to avoid accidentally matching sidebar elements - Working with regular page content **Note:** `screenDom` will NOT find portal-rendered elements (modals, dialogs) that are rendered outside the root container. For portals, use `screenDomGlobal` instead. ### How screenDom Works `screenDom` resolves the app's root container in this priority order: 1. **Configured selector** — if you pass `rootSelector` (to the `twd()` Vite plugin or to `initTWD`), that selector is tried first. 2. **Known framework roots** — `#root` (Vite / CRA / Solid), `#app` (Vue), then `app-root` (Angular). Most apps fall into this bucket and need no configuration. 3. **Heuristic fallback** — the first direct child of `` that isn't the TWD sidebar, isn't an excluded tag, and isn't empty. 4. **Last resort** — `document.body`. If resolution reaches step 3 or 4 without a configured `rootSelector`, `screenDom` logs a one-time console warning pointing you to the `rootSelector` option. **Excluded elements:** The following tags are ignored during the heuristic fallback (they're never app content): - `script`, `style`, `svg`, `path`, `noscript`, `link`, `iframe`, `template`, `meta` ### Configuring a custom root If your app mounts into a non-standard element, pass `rootSelector` to the `twd()` plugin (Vite projects) or to `initTWD` (manual setup): ```ts // vite.config.ts (Vite projects) import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [twd({ rootSelector: '#my-app' })], }); ``` ```ts // Manual setup (Angular / Webpack / non-Vite) initTWD(tests, { rootSelector: '#my-app', }); ``` This is the simplest way to handle apps whose root isn't `#root`, `#app`, or `app-root`. ### Troubleshooting: When screenDom Can't Find Your Container If `screenDom` queries fail unexpectedly: 1. **Your app uses a non-standard root selector.** Pass it via `twd({ rootSelector: '#your-root' })` (Vite plugin) or `initTWD({ rootSelector: '#your-root' })` (manual setup). 2. **Your app root exists but hasn't mounted yet.** `screenDom` queries inside a `twd.visit()` flow should run after the route mounts — make sure you `await twd.visit(...)` before querying. 3. **You need portal-rendered elements (modals, tooltips).** Use `screenDomGlobal` for those; `screenDom` only searches inside the resolved root container. **When to use `screenDomGlobal` instead:** - You need to query portal-rendered elements (modals, dialogs) ⚠️ **Remember:** When using `screenDomGlobal`, make your queries specific (e.g., `getByRole('button', { name: 'Submit' })`) to avoid accidentally matching elements in the TWD sidebar. ### screenDomGlobal (Global Queries) `screenDomGlobal` searches all elements in `document.body`, including portal-rendered elements (modals, dialogs, tooltips, etc.). **Use `screenDomGlobal` when:** - Querying portal-rendered elements (modals, dialogs, tooltips) - You need to search outside the root container - Working with elements rendered via React portals or similar mechanisms ⚠️ **WARNING:** `screenDomGlobal` may also match elements inside the TWD sidebar if your selectors are not specific enough. Always use specific queries (e.g., `getByRole` with `name` option) to avoid matching sidebar elements. **Example:** ```ts // ✅ Good - Specific query that won't match sidebar const modal = screenDomGlobal.getByRole('dialog', { name: 'Confirm Action' }); const modalTitle = screenDomGlobal.getByText('Are you sure?'); // ❌ Avoid - Too generic, might match sidebar elements const button = screenDomGlobal.getByRole('button'); // Could match sidebar buttons! const text = screenDomGlobal.getByText('Submit'); // Could match sidebar text! ``` ### Query Methods All Testing Library query methods are available for both `screenDom` and `screenDomGlobal`: #### getBy* Methods (Throws if not found) ```ts // Get by role (recommended) const button = screenDom.getByRole("button", { name: /submit/i }); const heading = screenDom.getByRole("heading", { name: "Welcome", level: 1 }); // Get by text const title = screenDom.getByText("Welcome to TWD"); const partialText = screenDom.getByText(/welcome/i); // Get by label const emailInput = screenDom.getByLabelText("Email Address:"); const searchInput = screenDom.getByLabelText(/search/i); // Get by placeholder const input = screenDom.getByPlaceholderText("Enter your email"); // Get by test ID const card = screenDom.getByTestId("user-card"); // Get by alt text const logo = screenDom.getByAltText("Company Logo"); ``` #### queryBy* Methods (Returns null if not found) ```ts // Use queryBy when element might not exist const errorMessage = screenDom.queryByText("Error occurred"); if (errorMessage) { // Handle error message } // Check for absence const modal = screenDom.queryByRole("dialog"); expect(modal).toBeNull(); // Modal should not be present ``` #### findBy* Methods (Async, waits for element) TWD configures the async utility timeout at **3000ms** (instead of RTL's default 1000ms) to accommodate CI rendering delays after network requests. ```ts // Wait for element to appear (waits up to 3s) const successMessage = await screenDom.findByText("Success!"); const loadingSpinner = await screenDom.findByRole("status"); ``` #### getAllBy* / queryAllBy* / findAllBy* Methods ```ts // Get multiple elements const buttons = screenDom.getAllByRole("button"); const links = screenDom.queryAllByRole("link"); const items = await screenDom.findAllByTestId("list-item"); // Check count expect(buttons).to.have.length(3); twd.should(buttons[0], "be.visible"); ``` ### Complete Example ```ts import { screenDom, screenDomGlobal, userEvent, twd } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("User Profile", () => { it("should display user information", async () => { await twd.visit("/profile"); // Query by role (most accessible) - using screenDom for regular content const heading = screenDom.getByRole("heading", { name: "User Profile" }); expect(heading).to.exist; // Query by label const emailInput = screenDom.getByLabelText("Email:"); twd.should(emailInput, "have.value", "user@example.com"); // Query by text const saveButton = screenDom.getByRole("button", { name: /save/i }); // Interact with userEvent const user = userEvent.setup(); await user.click(saveButton); }); it("should handle conditional elements", async () => { await twd.visit("/dashboard"); // Use queryBy for optional elements const adminPanel = screenDom.queryByTestId("admin-panel"); // Check if element exists if (adminPanel) { twd.should(adminPanel, "be.visible"); } else { // User is not admin expect(adminPanel).toBeNull(); } }); it("should wait for async content", async () => { await twd.visit("/posts"); // Wait for content to load const firstPost = await screenDom.findByTestId("post-1"); twd.should(firstPost, "be.visible"); // Get all posts once loaded const posts = screenDom.getAllByTestId(/^post-/); expect(posts.length).to.be.greaterThan(0); }); it("should interact with modal dialog", async () => { await twd.visit("/settings"); // Open modal using screenDom (regular button in app) const deleteButton = screenDom.getByRole("button", { name: /delete account/i }); const user = userEvent.setup(); await user.click(deleteButton); // Query modal using screenDomGlobal (modal is rendered via portal) const confirmModal = await screenDomGlobal.findByRole("dialog", { name: "Confirm Deletion" }); twd.should(confirmModal, "be.visible"); // Use specific queries to avoid matching sidebar const confirmButton = screenDomGlobal.getByRole("button", { name: "Yes, Delete" }); await user.click(confirmButton); }); }); ``` ## User Event (userEvent) TWD integrates with `@testing-library/user-event` for realistic user interactions. All user event methods are available and logged in the TWD sidebar. ### Import ```ts import { userEvent } from "twd-js"; ``` ### Setup ```ts const user = userEvent.setup(); ``` ### Common Interactions ```ts import { screenDom, userEvent } from "twd-js"; describe("Form Interactions", () => { it("should handle form submission", async () => { const user = userEvent.setup(); // Find elements using screenDom const emailInput = screenDom.getByLabelText("Email:"); const passwordInput = screenDom.getByLabelText("Password:"); const submitButton = screenDom.getByRole("button", { name: /submit/i }); // Type into inputs await user.type(emailInput, "user@example.com"); await user.type(passwordInput, "password123"); // Click button await user.click(submitButton); // Wait for success message const successMessage = await screenDom.findByText("Login successful!"); twd.should(successMessage, "be.visible"); }); it("should handle dropdown selection", async () => { const user = userEvent.setup(); const countrySelect = screenDom.getByLabelText("Country:"); await user.selectOptions(countrySelect, "US"); const selectedOption = screenDom.getByRole("option", { name: "United States", selected: true }); expect(selectedOption).to.exist; }); it("should handle keyboard navigation", async () => { const user = userEvent.setup(); const firstInput = screenDom.getByLabelText("First Name:"); await user.type(firstInput, "John"); // Tab to next input await user.tab(); const secondInput = screenDom.getByLabelText("Last Name:"); twd.should(secondInput, "be.focused"); await user.type(secondInput, "Doe"); }); }); ``` ### Available User Event Methods All `@testing-library/user-event` methods are supported: - `click()` - Click an element - `dblClick()` - Double click - `type()` - Type text into an input - `clear()` - Clear input value - `selectOptions()` - Select dropdown options - `upload()` - Upload files - `tab()` - Navigate with Tab key - `keyboard()` - Send keyboard events - `hover()` - Hover over element - `unhover()` - Remove hover - And more... ## Combining TWD and Testing Library You can mix and match TWD's native selectors with Testing Library queries: ```ts import { twd, screenDom, userEvent } from "twd-js"; describe("Mixed Approach", () => { it("should use both selector types", async () => { await twd.visit("/dashboard"); // Use TWD for simple CSS selectors const container = await twd.get(".dashboard-container"); container.should("be.visible"); // Use screenDom for semantic queries const heading = screenDom.getByRole("heading", { name: "Dashboard" }); expect(heading).to.exist; // Use screenDom for form elements const user = userEvent.setup(); const searchInput = screenDom.getByLabelText("Search:"); await user.type(searchInput, "query"); // Use TWD for complex selectors const results = await twd.getAll(".search-result"); expect(results.length).to.be.greaterThan(0); }); }); ``` ## When to Use Each Approach ### Use TWD Selectors (`twd.get`, `twd.getAll`) When: - You need complex CSS selectors - You're selecting by data attributes - You want simple, direct element access - You prefer CSS selector syntax ```ts // Complex selectors const item = await twd.get("ul > li:nth-child(3) button"); const cards = await twd.getAll("[data-testid='product-card']"); ``` ### Use Testing Library (`screenDom` or `screenDomGlobal`) When: - You want accessible, semantic queries - You're following Testing Library best practices - You want queries that match how users interact with your app - You need role-based queries (recommended for accessibility) ```ts // Semantic, accessible queries - use screenDom for regular content const button = screenDom.getByRole("button", { name: /submit/i }); const form = screenDom.getByLabelText("Email:"); // Use screenDomGlobal for portal-rendered elements (modals, dialogs) const modal = screenDomGlobal.getByRole("dialog", { name: "Confirm" }); const tooltip = screenDomGlobal.getByRole("tooltip"); ``` ## Best Practices ### 1. Prefer Role-Based Queries ```ts // ✅ Good - Accessible and semantic const button = screenDom.getByRole("button", { name: "Submit" }); // ❌ Avoid - Less accessible const button = screenDom.getByText("Submit"); ``` ### 2. Use Appropriate Query Types ```ts // Use getBy when element must exist const heading = screenDom.getByRole("heading"); // Use queryBy when element might not exist const error = screenDom.queryByText("Error"); if (error) { // Handle error } // Use findBy when waiting for async content const data = await screenDom.findByTestId("async-data"); ``` ### 3. Combine with TWD Assertions ```ts // Use screenDom for queries const input = screenDom.getByLabelText("Email:"); // Use TWD's should() function for assertions (not method on element) twd.should(input, "have.value", "user@example.com"); twd.should(input, "be.visible"); ``` ## Logging All Testing Library queries and user events are automatically logged in the TWD sidebar, making it easy to see what your tests are doing: - Query operations show as `query: getByRole("button")` - User events show as `Event fired: Clicked element` - All operations are visible in the test sidebar ## Next Steps - Read [Component Testing](/component-testing) to render components in isolation with `render()` - Learn about [TWD Commands](/api/twd-commands) for native selectors - Explore [User Interactions](/writing-tests#user-interactions) in detail - Check the [Testing Library docs](https://testing-library.com/docs/testing-library/intro/) for more query options # CI Execution Source: https://twd.dev/ci-execution Use the `twd-cli` package to run TWD tests in headless CI environments. It wraps Puppeteer, waits for your app, executes all tests, and reports coverage. It exits with a non-zero status code when a test fails, so it integrates directly into any CI/CD pipeline. You can find the source code, release notes, and issue tracker at [github.com/BRIKEV/twd-cli](https://github.com/BRIKEV/twd-cli). ### Install ```bash npm install twd-cli ``` or run it directly: ```bash npx twd-cli run ``` ### How It Works Puppeteer is **not** used as a testing framework — it simply provides a headless browser to load your application. Once the page loads, all test execution happens inside the real browser context through the TWD runner. 1. Launches a headless browser via Puppeteer 2. Navigates to your dev server URL 3. Waits for the app and TWD sidebar to be ready 4. TWD's in-browser test runner executes all tests against the real DOM 5. Collects and reports test results 6. Validates collected mocks against OpenAPI contracts (if [configured](/contract-testing)) 7. Optionally collects code coverage data 8. Exits with appropriate code (0 for success, 1 for failures) ### Configure (optional) Create `twd.config.json` in your repo to customize the runner: ```json { "url": "http://localhost:5173", "timeout": 10000, "coverage": true, "coverageDir": "./coverage", "nycOutputDir": "./.nyc_output", "headless": true, "puppeteerArgs": ["--no-sandbox", "--disable-setuid-sandbox"], "retryCount": 2, "protocolTimeout": 300000, "maxFailures": 10, "chunkSize": 10, "contracts": [], "contractReportPath": ".twd/contract-report.md" } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `url` | string | `"http://localhost:5173"` | Dev server URL to open before running tests | | `timeout` | number | `10000` | Milliseconds to wait for the page/sidebar | | `coverage` | boolean | `true` | Toggle code coverage collection | | `coverageDir` | string | `"./coverage"` | Output folder for coverage reports | | `nycOutputDir` | string | `"./.nyc_output"` | NYC temp folder | | `headless` | boolean | `true` | Run Chrome in headless mode | | `puppeteerArgs` | string[] | `["--no-sandbox", "--disable-setuid-sandbox"]` | Extra arguments for Puppeteer | | `retryCount` | number | `2` | Number of times to attempt each test before reporting failure. Default is 2 (one normal attempt + one retry). Set to 1 to disable retries. | | `protocolTimeout` | number | `300000` | Puppeteer CDP `protocolTimeout` in ms (5 min). Tests run in chunks, so this bounds a **single chunk's browser call**, not the entire run. Raise it (e.g. `600000`) for slow CI or if individual chunks hang. `0` means no timeout. | | `maxFailures` | number | `10` | Stop the run once this many tests have failed in total. The CLI prints the results gathered so far and exits non-zero. Set `0` to disable and always run every test. Note this limit is **per shard** when [sharding](/sharding). | | `chunkSize` | number | `10` | How many tests run per browser call. Smaller values make the failure limit and timeouts more granular (less work lost if one chunk hangs), larger values reduce overhead. `0` runs everything in one call. | | `contracts` | object[] | `[]` | OpenAPI contract validation specs. See [Contract Testing](/contract-testing) | | `contractReportPath` | string | — | Path to write a markdown report for CI/PR integration | | `record` | object | see [Recording Runs](/recording) | Video recording settings | ## Filtering tests Run only a subset of tests with the repeatable `--test` flag. Matching is **case-insensitive** and matches a **substring** of each test's full `"Suite > test name"` path: ```bash # Every test whose name contains "shows error" npx twd-cli run --test "shows error" # Because matching uses the full "suite > test" path, passing a describe name # runs every test inside that describe block: npx twd-cli run --test "Login" # Multiple --test flags are combined with OR (a test runs if it matches any): npx twd-cli run --test "Login" --test "Signup" ``` Two things to know: - If no test matches any filter, the run exits with code `1` and prints `No tests matched filter(s): ...`, so a typo will not silently look like a pass. - Code coverage collection is skipped while a `--test` filter is active, since a filtered run is a partial (debug) run. `--test` and `--shard` compose. Filters resolve first, then the filtered list is sharded. See [Sharding](/sharding). ::: tip `twd-relay` accepts the same `--test` flag for driving a run from an AI agent. See [AI Remote Testing](/ai-remote-testing). ::: ## GitHub Action (Recommended) The easiest way to run TWD tests in CI. The composite action handles Puppeteer caching, Chrome installation, and optional contract report posting in a single step: ```yaml name: TWD Tests on: push: branches: [main] pull_request: branches: [main] permissions: pull-requests: write # only needed if using contract-report jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 24 cache: npm - name: Install dependencies run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Start dev server run: | nohup npm run dev > /dev/null 2>&1 & npx wait-on http://localhost:5173 - name: Run TWD tests uses: BRIKEV/twd-cli/.github/actions/run@main with: contract-report: 'true' ``` ### Action Inputs | Input | Default | Description | |-------|---------|-------------| | `working-directory` | `.` | Directory where `twd.config.json` lives | | `contract-report` | `false` | Post contract validation summary as a PR comment | | `shard` | (empty) | Run one shard of the suite, as `/` (e.g. `2/4`). Leave empty to run everything in one job. See [Sharding](/sharding) | | `report-dir` | `.twd/run` | Where the shard report is written. Only used when `shard` is set | | `upload-report` | `true` | Upload the shard report as an artifact named `twd-run-`, the layout `twd-cli merge` expects. Only used when `shard` is set | ### With code coverage The action runs in the same job, so coverage data is available for subsequent steps: ```yaml - name: Run TWD tests uses: BRIKEV/twd-cli/.github/actions/run@main - name: Display coverage run: npm run collect:coverage:text ``` ## Custom Setup (Without the Action) If you prefer full control over each CI step, or your CI isn't GitHub Actions, set up each step manually. Puppeteer 24+ no longer auto-downloads Chrome, so you need to install it explicitly: ```yaml - name: Install dependencies run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Cache Puppeteer browsers uses: actions/cache@v4 with: path: ~/.cache/puppeteer key: ${{ runner.os }}-puppeteer-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-puppeteer- - name: Install Chrome for Puppeteer run: npx puppeteer browsers install chrome - name: Run TWD tests run: npx twd-cli run ``` > **Tip:** Puppeteer 24+ no longer downloads Chrome automatically. Either run `npx puppeteer browsers install chrome` in CI or cache `~/.cache/puppeteer` between runs to avoid repeated downloads. ## Custom Runner Options If you're building your own CI script instead of using `twd-cli`, you can pass options to the `TestRunner` constructor to handle flaky CI environments: ```ts const runner = new TestRunner({ onStart: () => {}, onPass: (test, retryAttempt) => { const suffix = retryAttempt ? ` (retry ${retryAttempt}/2)` : ''; testStatus.push({ id: test.id, status: "pass" }); console.log(`✓ ${test.name}${suffix}`); }, onFail: (test, err) => { testStatus.push({ id: test.id, status: "fail", error: err.message }); }, onSkip: (test) => { testStatus.push({ id: test.id, status: "skip" }); }, }, { retryCount: 2 }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `retryCount` | number | `1` | Total number of attempts per test. `1` means no retry. `2` means one original attempt + one retry on failure. | The `onPass` callback receives an optional second parameter `retryAttempt` — it is `undefined` when the test passes on the first attempt, or the attempt number (2+) when it passes on a retry. This lets you log which tests are flaky so you can fix them later. > **Note:** The retry mechanism re-runs the full test cycle (beforeEach hooks → test → afterEach hooks) on each attempt, ensuring clean state between retries. ## Cross-browser testing (experimental) ::: warning Experimental For the vast majority of projects, **`twd-cli` is the recommended runner and covers ~90% of use cases** — it's faster, collects coverage, validates contracts, and is battle-tested. `twd-runner` is an experimental complement; reach for it only when you specifically need to validate other browser engines. ::: `twd-cli` runs your tests in headless **Chromium** (via Puppeteer). If you also want to catch **Firefox** and **WebKit (Safari)** engine differences, [`twd-runner`](https://github.com/BRIKEV/twd-runner) runs the same TWD tests across engines using Playwright. It's a complement to `twd-cli`, not a replacement: keep `twd-cli` as your primary runner (coverage + contracts), and add `twd-runner` as an extra cross-browser check. ```bash npm install -D twd-runner npx playwright install # downloads the browser binaries (npm install does not) npx twd-runner run ``` It reads the same `twd.config.json`. The keys that matter most here: | Option | Default | Description | |--------|---------|-------------| | `browsers` | `["chromium","firefox","webkit"]` | Engines to run, in parallel within a single job | | `waitForServiceWorker` | `false` | Set `true` for apps that mock via a service worker. Firefox/WebKit can be slow to take control of the page, and mocks registered before then are silently dropped. Enabling it also auto-warms the dev server first (a cold dev server otherwise races SW registration). | A single job runs the engines in parallel, so the same two steps work on any CI — no GitHub-specific matrix required: ```yaml - name: Start dev server run: | nohup npm run dev > /dev/null 2>&1 & npx wait-on http://localhost:5173 - name: Run cross-browser tests run: npx twd-runner run ``` ::: tip Recommended split Run your **full suite on Chromium with `twd-cli`** (coverage, contracts, retries), and use `twd-runner` only for a cross-browser pass on the engines Puppeteer can't reach — typically `"browsers": ["firefox", "webkit"]`. `twd-runner` does not collect coverage or run contract validation, and is slower than `twd-cli`. ::: ## Next Steps - [Sharding](/sharding): Split a long run across parallel CI jobs and merge the reports (beta) - [Recording Runs](/recording): Record a run to video, paced so it is watchable in a pull request - [Contract Testing](/contract-testing): Validate your API mocks against OpenAPI specs - [Code Coverage](/coverage): Learn how to collect and report code coverage with TWD - [Writing Tests](/writing-tests): Create testable components - [API Mocking](/api-mocking): Test with network requests - [API Reference](/api/): Complete function documentation # Sharding Source: https://twd.dev/sharding ::: warning Beta Sharding is new and marked beta on purpose. It is strictly additive: a run without `--shard` behaves exactly as it did before, writes the same files, and exits the same way, so enabling it cannot affect your existing pipeline. What may still change is **how tests are assigned to shards**. Today each shard takes every nth test from the discovered list, and a future release is likely to group by top-level `describe` instead, so a suite always stays in one shard. Do not build anything that depends on *which* tests land in a given shard. Everything else (the flags, the report files, `merge`'s output and exit code) is stable. ::: A single run walks the whole suite in one browser. Sharding splits it across parallel CI jobs instead, then joins the results back into one report. ```bash npx twd-cli run --shard 2/4 # "I am job 2 of 4" npx twd-cli merge .twd/shards # join the reports, decide the exit code ``` Requires `twd-cli` 1.5.0 or newer. The `4` is how many jobs you are running, **not** how many tests exist. You never need to know the test count. Each shard boots its own browser, discovers the whole suite exactly as a normal run does, and keeps every 4th test. Add tests and the same 4 jobs just split more of them. Each shard writes `run.json` and `coverage.json` to `./.twd/run` (change it with `--report-dir`). `merge` reads the downloaded shard directories, combines test results, coverage and contract validation, prints one summary, and exits non-zero if anything failed anywhere. ## A complete workflow This runs as-is. The bundled action installs Chrome, runs the shard, and uploads its report under the name `merge` expects. ```yaml name: TWD tests (sharded) on: pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: # Without this, the first red shard cancels its siblings and the merge job # sees gaps it cannot tell apart from a shard that crashed. fail-fast: false matrix: shard: [1, 2, 3, 4] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 24 cache: npm - run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Start the dev server run: | nohup npm run dev > dev.log 2>&1 & npx wait-on http://localhost:5173 --timeout 60000 - name: Run this shard uses: BRIKEV/twd-cli/.github/actions/run@main with: shard: ${{ matrix.shard }}/4 merge: runs-on: ubuntu-latest needs: [test] # Runs even though a shard job may have exited 1. Without this a red shard # short-circuits the workflow and the merged summary never prints. if: ${{ !cancelled() }} steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 24 cache: npm - run: npm ci - uses: actions/download-artifact@v4 with: pattern: twd-run-* path: .twd/shards - name: Merge the shard reports run: npx twd-cli merge .twd/shards ``` `merge` owns the final exit code. It fails if any test failed in any shard, if a contract was violated in `error` mode, or if a shard report is missing entirely. ## Without the bundled action If you drive the CLI directly, you own the two steps the action was doing for you: installing Chrome, and uploading the report with `if: always()`. ```yaml - run: npx puppeteer browsers install chrome - run: npx twd-cli run --shard ${{ matrix.shard }}/4 - uses: actions/upload-artifact@v4 # A red shard must still upload, or merge cannot tell "this shard failed" # from "this shard never ran". if: always() with: name: twd-run-${{ matrix.shard }} path: .twd/run if-no-files-found: error ``` ## Commands and flags | Flag | Command | Default | Description | |------|---------|---------|-------------| | `--shard /` | `run` | off | Run shard `i` of `n`. A malformed spec throws rather than silently running zero tests | | `--report-dir ` | `run` | `.twd/run` | Where this shard writes `run.json` and `coverage.json` | | `--out ` | `merge` | `.twd/merged-run.json` | Where the merged report is written | `twd-cli merge ` takes the directory holding the downloaded shard reports as its first positional argument. ## The three conditions that matter Each of these breaks a sharded run in a different way, and all three are easy to leave out: | Condition | Where | What breaks without it | |---|---|---| | `fail-fast: false` | the shard matrix | the first red shard cancels its siblings, and `merge` reports their reports as missing | | `if: always()` | the shard's artifact upload | a red shard uploads nothing, so `merge` cannot distinguish failure from a crash | | if: ${{ !cancelled() }} | the merge job | a red shard short-circuits the workflow and the merged summary never prints | ## When sharding pays Sharding buys wall clock with compute. Every shard repeats the per-job setup (install, browser, dev server), and the merge job runs after all of them, so it only wins once test time dominates that fixed cost. Measured on a real suite of 256 browser tests: ``` Shards Wall clock Runner time 1 █████████████████████████ 12.6 min baseline 2 ████████████████ 8.1 min +15% 4 █████████████ 6.5 min +47% ├──────┤ ~4 min floor, no matter how far you shard ``` The takeaways: - **Most of the win is in the first split.** 1 to 2 shards saved 4.5 minutes. 2 to 4 saved only 1.6 more. - **There is a floor you cannot get under.** Setup runs in every shard and the merge job runs after them all, so past four shards you pay a lot for seconds. - **Total compute goes up.** Runner time grew 15% at two shards and 47% at four. If you are billed for runner minutes, or your runner concurrency is contended, pick the smallest number of shards that gets you under your target. - **Short suites get slower.** The `twd-cli` project's own 71-test suite goes from 25s in one job to 41s across two plus a merge. Under a couple of minutes, do not shard. ## Caveats - **Coverage.** Each shard writes its own `coverage.json`, and `merge` combines them into `.nyc_output/out.json`, but only when the whole run is green. That matches how a single run behaves. `merge` reports how many shards contributed. See [Code Coverage](/coverage) for reporting on the merged output. - **Missing shards are an error.** If a shard job dies before uploading, `merge` refuses and names the gap rather than silently reporting 3 of 4 shards as a complete green run. - **Tests must register identically in every job.** Each shard fingerprints the ordered list of `"suite > test"` paths it discovered and `merge` verifies they match. Registering tests conditionally, behind a feature flag, a date, or `Math.random()`, makes the fingerprints diverge and `merge` will say so. It compares paths rather than internal test ids because `twd-js` assigns those at registration time and they differ on every page load, so each shard's browser sees its own. - **`maxFailures` is per shard.** Four shards at the default of 10 can reach 40 failures between them before all four bail. - **`--test` and `--shard` compose.** Filters resolve first, then the filtered list is sharded. As with any filtered run, coverage is skipped. - **The contract report is written by `merge`, not per shard.** Each shard would otherwise overwrite the others with a fraction of the mocks, so the PR comment step belongs in the merge job. See [Contract Testing Setup](/contract-testing-setup#pr-reports). - **Recording** produces one clip per shard. They are not concatenated. - **A missing shard leaves no merged report on disk.** `merge` throws before it writes `.twd/merged-run.json`, so a CI step that uploads that path with `if: always()` will find nothing when a shard is missing. The error message on stderr is the diagnosis in that case. - **`record.filename` collides under sharding.** Only the *derived* recording filename is per-shard. If `record.filename` is set explicitly in `twd.config.json`, every shard writes to the same video path. Use the derived name, or a per-shard `--record-dir`, when recording a sharded run. See [Recording Runs](/recording). - **Assignment may change.** See the beta note at the top. Which tests land in which shard is not part of the stable contract yet. ## Next Steps - [CI Execution](/ci-execution): the single-job setup, config options, and the bundled action - [Recording Runs](/recording): capture a run to video for a pull request - [Contract Testing](/contract-testing): validate your API mocks against OpenAPI specs - [Code Coverage](/coverage): collect and report coverage # Layout Snapshots Source: https://twd.dev/layout-snapshots `twd.matchLayout` watches the **geometry** of your page and fails when it moves. No SaaS account, no Docker image, no headless browser service. The reference is a text file you commit. ::: warning Beta The capture pipeline is validated, but the API and the `.snap` format may still change. See [Beta limits](#beta-limits) for what is not built yet. ::: Two things decide whether this is useful to you, so they come first. **It watches geometry, not appearance.** It sees a block that grows, shrinks or moves, a list that appears or disappears, a container that overflows or wraps, a flex or grid that collapses. It does not see a string that changed, a colour that shifted slightly, or white text on a white background. That is deliberate. Content is already covered by TWD's DOM assertions, and the two are meant to be used together: ```ts twd.should(counter, 'have.text', 'Count is 1'); // content await twd.matchLayout(checkout, 'checkout'); // geometry ``` It is called `matchLayout` and not `matchSnapshot` for the same reason. With `matchSnapshot` people expect Percy, and then report a changed string as a bug. **The verdict comes from `twd-cli`, not the sidebar.** In the browser sidebar layout snapshots are skipped. The sidebar resizes the page, and your dev viewport is whatever size your window happens to be right now, which differs from your colleague's and changes when you drag the edge. A reference created there would fail for everyone else. There is a debug mode for inspecting a failure locally, covered below, but it is for looking, not for deciding. ## Install `matchLayout` needs the `twdSnapshot` Vite plugin, because the browser cannot write files and the dev server can. ```ts // vite.config.ts import { defineConfig } from 'vite'; import { twd, twdSnapshot } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [twd(), twdSnapshot()], }); ``` Then ignore the failure captures. The `.snap` files are committed; the PNGs are not. ``` # .gitignore __twd_snapshots__/*.png ``` ## Write a test ```ts import { twd, screenDom } from 'twd-js'; import { describe, it } from 'twd-js/runner'; describe('Landing', () => { it('keeps its layout', async () => { await twd.visit('/'); const landing = await screenDom.findByTestId('landing'); await twd.matchLayout(landing, 'landing'); }); }); ``` There is no `expect` to write. `matchLayout` throws when the layout moved, the same way `twd.should` throws when an assertion fails. The first run has no reference, so it writes `__twd_snapshots__/landing.snap` and passes, the way Jest snapshots do. Commit that file. Every run after that compares against it. ## What a reference looks like ``` hash 8003000003c007f0... size 1192x2451 viewport 1280x800 rows 33 cols 16 grid 1800000000000000... # preview (# = filled, . = empty) # # . . . . . . . . . . . . . # # # . . . . . . # # # # . . . . . . ``` It is text on purpose, so a layout change is reviewable in a pull request without opening an image. The preview is drawn from the same bits that decide the verdict, which makes `git diff` on a `.snap` a readable diff of what moved. Only the `.snap` is committed. A reference PNG would not survive a clean checkout, so it would never exist in CI anyway. ## When it fails The error names what moved: ``` Layout snapshot "landing" changed - the block height changed, 577x512 -> 577x532 Reference: __twd_snapshots__/landing.snap Capture: __twd_snapshots__/landing.failed.png Accept: npx twd-cli --update-snapshots ``` Next to the reference you get `landing.failed.png`: your current page, with the rows that diverged boxed in red and a ribbon across the top when the block resized. The reference itself is never touched by a failure. If the change was intended, accept it with `npx twd-cli --update-snapshots`. Accepting is never silent: updated snapshots are reported separately from passing ones, because a flag left on by accident rewrites every reference and then nothing ever fails again. ## Viewports A reference records the viewport it was taken at. When the current viewport is different, the snapshot is skipped rather than failed: ``` Layout snapshot "landing" skipped - viewport mismatch (reference 1280x800, current 1512x945). Run twd-cli to validate layout snapshots. ``` This is what keeps the feature from being flaky, and it is why a resized window never produces a red test you did not cause. Under `twd-cli` the viewport is fixed, so it always matches. Nothing in this feature sets the viewport itself: a snapshot simply records whatever the browser reports at the time it was taken. That is why the reference has to be created by `twd-cli`, where the size is fixed and reproducible. ## Debugging in the sidebar To run snapshots in the browser while you investigate a failure: ```ts twdSnapshot({ debug: true }) ``` Leave it off in normal development and in CI. A reference created at your window size is not one anybody else can reproduce. ## Beta limits Not built yet: - no tolerance option, so a snapshot either matches or it does not - no update button in the sidebar - an element with its own scrollbar is captured only as far as it is visible - images and background images loaded from an external URL do not render into the capture, and `::before` / `::after` are not cloned There is also a ceiling worth understanding. When something near the top of the page changes height, everything below it shifts. Rows are matched by content rather than by position, so a shift on its own is not reported as a change, but a height change high up still limits how precisely a smaller change further down can be located. You are told that the page diverged and where the divergence starts, not everything that moved after it. # Code Coverage Source: https://twd.dev/coverage Coverage in TWD is a feedback tool, not a vanity metric. It helps you discover untested paths and missing assertions so you can strengthen your tests where it matters. The goal isn’t “100%”, it’s to reveal gaps while you develop. ## Instrumenting the Vite App To collect browser-based coverage, your app must be instrumented. Instrumentation inserts counters into your built code so the browser can record which lines/branches run. This is framework-specific; for Vite apps we recommend `vite-plugin-istanbul`. Install the plugin: ```bash npm i --save-dev vite-plugin-istanbul ``` Then add it to your `vite.config.ts` alongside the `twd()` plugin: ```ts /// import path from "path" import tailwindcss from "@tailwindcss/vite" import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // add plugins for code coverage and TWD import istanbul from 'vite-plugin-istanbul'; import { twd } from 'twd-js/vite-plugin'; // https://vite.dev/config/ export default defineConfig({ plugins: [ react(), tailwindcss(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', }), // configure istanbul plugin istanbul({ include: 'src/**/*', exclude: ['node_modules', 'tests/'], extension: ['.ts', '.tsx'], requireEnv: !process.env.CI }), ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, server: { watch: { ignored: ["**/data/data.json", "**data/routes.json"], }, }, }) ``` This plugin automatically adds coverage data to `window.__coverage__`. When you run tests with `twd-cli`, the CLI extracts and stores this data so you can generate reports. ### Configure (optional) You can configure the output folders used by the CLI in a `twd.config.json` file: ```json { "url": "http://localhost:5173", "timeout": 10000, "coverage": true, "coverageDir": "./coverage", "nycOutputDir": "./.nyc_output", "headless": true, "puppeteerArgs": ["--no-sandbox", "--disable-setuid-sandbox"], "retryCount": 2, "contracts": [], "contractReportPath": ".twd/contract-report.md" } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `url` | string | `"http://localhost:5173"` | Dev server URL to open before running tests | | `timeout` | number | `10000` | Milliseconds to wait for the page/sidebar | | `coverage` | boolean | `true` | Toggle code coverage collection | | `coverageDir` | string | `"./coverage"` | Output folder for coverage reports | | `nycOutputDir` | string | `"./.nyc_output"` | NYC temp folder | | `headless` | boolean | `true` | Run Chrome in headless mode | | `puppeteerArgs` | string[] | `["--no-sandbox", "--disable-setuid-sandbox"]` | Extra arguments for Puppeteer | | `retryCount` | number | `2` | Number of times to attempt each test before reporting failure. Default is 2 (one normal attempt + one retry). Set to 1 to disable retries. | | `contracts` | object[] | `[]` | OpenAPI contract validation specs. See [Contract Testing](/contract-testing) | | `contractReportPath` | string | — | Path to write a markdown report for CI/PR integration | ## Updating package.json Scripts Install a coverage reporter to transform the raw `.nyc_output` into reports: ```bash npm i --save-dev nyc ``` Add helpful scripts: ```jsonc { "scripts": { // ... "dev": "vite", "dev:ci": "CI=true vite", "test:ci": "twd-cli run", "collect:coverage:html": "npx nyc report --reporter=html --report-dir=coverage", "collect:coverage:lcov": "npx nyc report --reporter=lcov --report-dir=coverage", "collect:coverage:text": "npx nyc report --reporter=text --report-dir=coverage" } } ``` Now, run these in two terminals: ```bash npm run dev:ci ``` And in another terminal: ```bash npm run test:ci ``` Once the tests complete, you can generate coverage reports in different formats: ```bash npm run collect:coverage:html npm run collect:coverage:lcov npm run collect:coverage:text ``` You’ll see outputs like: ![coverage html reporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ss03qfi57r43ehwgf0fn.png) ![coverage text reporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1dtemuclax3jwbmlju5x.png) ## Adding Coverage to GitHub Actions ### Using the GitHub Action (recommended) The [composite GitHub Action](/ci-execution#github-action-recommended) handles Puppeteer setup, test execution, and optional contract reporting — so you only need to add the coverage step after it: ```yaml - name: Start Vite dev server run: | nohup npm run dev > /dev/null 2>&1 & npx wait-on http://localhost:5173 env: CI: true - name: Run TWD tests uses: BRIKEV/twd-cli/.github/actions/run@main - name: Display coverage run: npm run collect:coverage:text ``` See the [CI Execution](/ci-execution#github-action-recommended) page for the full workflow. ### Custom setup If you prefer full control over each CI step, set up the workflow manually: ```yml name: CI - twd tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 24 cache: ‘npm’ - name: Install dependencies run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Start Vite dev server run: | nohup npm run dev > vite.log 2>&1 & npx wait-on http://localhost:5173 env: CI: true - name: Cache Puppeteer browsers uses: actions/cache@v4 with: path: ~/.cache/puppeteer key: ${{ runner.os }}-puppeteer-${{ hashFiles(‘package-lock.json’) }} restore-keys: | ${{ runner.os }}-puppeteer- - name: Install Chrome for Puppeteer run: npx puppeteer browsers install chrome - name: Run TWD tests run: npx twd-cli run - name: Display coverage run: | npm run collect:coverage:text ``` With the `Display coverage` step you’ll see the coverage summary directly in your GitHub Action logs: ![Github action coverage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0cznd4txgvtpxz5tn10o.png) Having this basic configuration, it’s entirely up to you how you want to handle the coverage results. You can publish them to a service like Codecov or Coveralls, display them as badges in your README, or even use them in your CI pipeline to fail a build if coverage drops below a threshold. What matters is that TWD gives you the flexibility to collect and track coverage directly from your browser tests, without relying on a separate test runner. # AI Integration Source: https://twd.dev/ai-overview TWD produces structured, deterministic output that AI agents can parse and act on autonomously. Every test run returns the same pass/fail signals for the same inputs — no flakiness, no ambiguity. Whether you want your AI assistant to write better tests, generate tests from browser interactions, or run tests autonomously, TWD has you covered. ## Watch the loop An AI agent writes a test, runs it in your real browser through TWD, reads the failure, fixes it, and re-runs until green — no screenshots, no separate browser. ## Claude Code Plugin The fastest way to get AI-powered TWD testing is with the [Claude Code plugin](https://github.com/BRIKEV/twd-ai). It gives Claude a full set of testing skills. ```bash claude plugin marketplace add BRIKEV/twd-ai claude plugin install twd@twd-ai ``` | Command / Skill | What it does | |---------|-------------| | `/twd:setup` | Analyzes your project, asks configuration questions, and generates `.claude/twd-patterns.md` | | `twd` skill | Autonomous agent — writes tests, runs them via twd-relay, fixes failures, and re-runs until green | | `/twd:ci-setup` | Installs `twd-cli`, configures coverage, and generates a GitHub Actions workflow | | `/twd:test-gaps` | Scans routes, finds untested pages, and classifies risk (HIGH/MEDIUM/LOW) | | `/twd:test-quality` | Grades test files on journey coverage, interaction depth, assertion quality, and edge cases | | `/twd:test-flow-gallery` | Generates Mermaid flowcharts and plain-language summaries from test files | The agent works in a forked context — your main conversation stays clean while tests are written, run, and fixed. If a test still fails after 3 fix attempts, it's marked as `it.skip` so it doesn't block the rest. **[Read the full Claude Code Plugin guide](/claude-plugin)** ### Other AI Tools For Cursor, Copilot, Windsurf, and other AI tools, use the [Agent Skills CLI](https://github.com/vercel-labs/skills): ```bash npx skills add BRIKEV/twd-ai ``` This copies TWD context into your AI tool's configuration file (`.cursorrules`, `.github/copilot-instructions.md`, etc.). --- ## Features at a Glance ### 1. AI Context Teach your AI assistant (Claude, Cursor, Copilot, Windsurf) how to write correct TWD tests by providing a comprehensive prompt with API reference, patterns, and common pitfalls. **Best for:** Getting AI to write correct TWD tests on the first try. [Read the AI Context & Prompts guide](/agents) --- ### 2. AI Remote Testing (twd-relay) A WebSocket bridge that lets AI agents trigger test runs and stream results back, without launching a browser automation tool. Your Vite dev server is already running with TWD loaded -- the relay just connects to it. **Best for:** AI agents that need to run tests, read failures, and iterate. [Read the AI Remote Testing guide](/ai-remote-testing) --- ### 3. Claude Code Plugin — Autonomous Testing When you install the TWD plugin, Claude Code can automatically invoke the testing agent when it detects the task is relevant. For example: - You ask: _"Add a search filter to the orders page"_ - Claude implements the feature - Claude sees the `twd` skill and spawns it as a sub-agent - The agent writes tests, runs them via `npx twd-relay run`, reads failures, fixes, and re-runs until green - Claude continues with your task You can also set up your project interactively with `/twd:setup`. **[Read the full Claude Code Plugin guide](/claude-plugin)** --- ## How They Work Together You can use these features independently or combine them: ``` AI Context & Prompts → AI writes better tests (any AI tool) AI Remote Testing → AI runs tests and reads results (any AI tool) Claude Code Plugin → AI writes, runs, and fixes tests autonomously ``` A typical workflow: 1. **Plugin / Skills** install TWD context into your AI agent 2. The **TWD agent** writes tests, runs them via twd-relay, and fixes failures 3. The autonomous validation loop continues until all tests pass ::: info MCP Integration TWD also provides an experimental MCP server that works with Playwright MCP to generate test code from browser automation data. This is an early feature — if you're interested, check the [TWD MCP package](https://github.com/BRIKEV/twd-mcp) for details. ::: # Contract Testing Setup Source: https://twd.dev/contract-testing-setup This page covers the reference details for configuring contract testing in TWD. For the overview, audience, and pitch, see [/contract-testing](/contract-testing). ## Setup ### 1. Add your OpenAPI specs Place your OpenAPI 3.0 or 3.1 spec files (JSON format) somewhere in your project: ``` contracts/ users-3.0.json posts-3.1.json ``` ### 2. Configure contracts in `twd.config.json` ```json { "url": "http://localhost:5173", "contractReportPath": ".twd/contract-report.md", "contracts": [ { "source": "./contracts/users-3.0.json", "baseUrl": "/api", "mode": "error", "strict": true }, { "source": "./contracts/posts-3.1.json", "baseUrl": "/api", "mode": "warn", "strict": true } ] } ``` ### Contract Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `source` | string | — | Path to the OpenAPI spec file (JSON) | | `baseUrl` | string | `"/"` | Base URL prefix to strip when matching mock URLs to spec paths | | `mode` | `"error"` \| `"warn"` | `"warn"` | `"error"` fails the test run; `"warn"` reports but doesn't fail | | `strict` | boolean | `true` | When true, rejects unexpected properties not defined in the spec | ## Example Output When a mock response doesn't match the spec, you'll see detailed errors: ``` Source: ./contracts/users-3.0.json ERROR ✓ GET /users (200) — mock "getUsers" ✗ GET /users/{userId} (200) — mock "getUserBadAddress" → response.address.city: missing required property → response.address.country: missing required property ⚠ GET /users/{userId} (404) — mock "getUserNotFound" Status 404 not documented for GET /users/{userId} ``` - **✓** Mock matches the spec - **✗** Mock has validation errors (fields that fail against the spec) - **⚠** Warning — the status code or schema isn't documented (mock isn't wrong, but it's not contract-tested either) ## Supported Validations The validator checks all standard OpenAPI/JSON Schema constraints: - **Types**: `string`, `number`, `integer`, `boolean`, `array`, `object` - **String**: `minLength`, `maxLength`, `pattern`, `format` (date, date-time, email, uuid, uri, hostname, ipv4, ipv6) - **Number/Integer**: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` - **Array**: `minItems`, `maxItems`, `uniqueItems` - **Object**: `required`, `additionalProperties` - **Composition**: `oneOf`, `anyOf`, `allOf` - **Enum**: validates against allowed values - **Nullable**: supports both OpenAPI 3.0 (`nullable: true`) and 3.1 (`type: ["string", "null"]`) ::: tip Strict mode and allOf Strict mode (`additionalProperties: false`) can conflict with `allOf` schemas. When `allOf` branches define different properties, each branch rejects the other's properties as "additional." Use `{ strict: false }` for endpoints that use `allOf` composition, or define `additionalProperties` explicitly in your spec. ::: ## PR Reports When `contractReportPath` is set and you use the [GitHub Action](/ci-execution#github-action-recommended) with `contract-report: 'true'`, a summary table is posted as a PR comment: | Spec | Passed | Failed | Warnings | Mode | |------|--------|--------|----------|------| | `users-3.0.json` | 2 | 3 | 1 | `error` | | `posts-3.1.json` | 2 | 2 | 0 | `warn` | Failed validations are included in a collapsible details section with a link to the full CI log. ```yaml - name: Run TWD tests uses: BRIKEV/twd-cli/.github/actions/run@main with: contract-report: 'true' ``` See [CI Execution](/ci-execution#github-action-recommended) for the full workflow setup. ### With a sharded run A [sharded run](/sharding) deliberately writes no contract markdown per shard, because each would overwrite the others with a fraction of the mocks. `twd-cli merge` writes it instead, so the PR comment step belongs in the merge job rather than in the shard jobs: ```yaml merge: permissions: contents: read pull-requests: write steps: # ...checkout, npm ci, download the shard artifacts, then: - name: Merge the shard reports run: npx twd-cli merge .twd/shards - name: Post contract report to PR if: github.event_name == 'pull_request' && hashFiles('.twd/contract-report.md') != '' env: GH_TOKEN: ${{ github.token }} run: gh pr comment "${{ github.event.pull_request.number }}" --body-file .twd/contract-report.md ``` ## Next Steps - Run contract tests in CI with the [GitHub Action](/ci-execution#github-action-recommended) - Learn how to create mocks with [API Mocking](/api-mocking) - Collect [Code Coverage](/coverage) alongside contract validation - Split a long run across parallel jobs with [Sharding](/sharding) # Theming Source: https://twd.dev/theming TWD allows you to customize the appearance of the test sidebar to match your preferences or your application's design system. You can personalize colors, spacing, typography, and more through a simple theme configuration. ## Overview TWD uses CSS variables for theming, which means you can customize the entire UI without modifying any source code. The theme system is designed to be: - **Non-intrusive**: Uses CSS variables that won't conflict with your application - **Flexible**: Override any or all theme properties - **Type-safe**: Full TypeScript support with autocomplete ## Basic Usage Here's a quick preview of the default theme: ![Default TWD Sidebar](/images/twd_side_bar_success.png) With the `twd()` Vite plugin, pass a `theme` option in your `vite.config.ts`: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd } from 'twd-js/vite-plugin'; const customTheme = { primary: '#2563eb', background: '#ffffff', // ... more theme properties }; export default defineConfig({ plugins: [ react(), twd({ open: true, theme: customTheme, }), ], }); ``` ## Theme Properties The theme object accepts the following properties (all optional): ### Colors | Property | Description | Default | |----------|-------------|----------| | `primary` | Primary brand color | `#1A6EF4` | | `background` | Main sidebar background | `#f9fafb` | | `backgroundSecondary` | Secondary background areas | `#f3f4f6` | | `border` | Border color | `#e5e7eb` | | `borderLight` | Lighter border color | `#d1d5db` | | `text` | Primary text color | `#374151` | | `textSecondary` | Secondary text color | `#6b7280` | | `textMuted` | Muted text color | `#475569` | ### Describe Blocks | Property | Description | Default | |----------|-------------|----------| | `describeBg` | Background for describe/suite blocks | `#0f172a` | | `describeText` | Text color for describe/suite headers | `#94a3b8` | | `describeBorder` | Left border accent for describe blocks | `#334155` | ### Status Colors | Property | Description | Default | |----------|-------------|----------| | `success` | Color for passed tests | `#00c951` | | `successBg` | Background for passed tests | `#dcfce7` | | `error` | Color for failed tests | `#fb2c36` | | `errorBg` | Background for failed tests | `#fee2e2` | | `warning` | Color for running tests | `#fef9c3` | | `warningBg` | Background for running tests | `#fef9c3` | | `skip` | Color for skipped tests | `#f3f4f6` | | `skipBg` | Background for skipped tests | `#f3f4f6` | ### Interactive Elements | Property | Description | Default | |----------|-------------|----------| | `buttonPrimary` | Primary button background | `#1A6EF4` | | `buttonPrimaryText` | Primary button text | `#ffffff` | | `buttonSecondary` | Secondary button background | `#f8fafc` | | `buttonSecondaryText` | Secondary button text | `#475569` | | `buttonBorder` | Button border color | `#cbd5e1` | ### Spacing | Property | Description | Default | |----------|-------------|----------| | `spacingXs` | Extra small spacing | `4px` | | `spacingSm` | Small spacing | `6px` | | `spacingMd` | Medium spacing | `8px` | | `spacingLg` | Large spacing | `12px` | | `spacingXl` | Extra large spacing | `14px` | ### Typography | Property | Description | Default | |----------|-------------|----------| | `fontSizeXs` | Extra small font size | `10px` | | `fontSizeSm` | Small font size | `12px` | | `fontSizeMd` | Medium font size | `14px` | | `fontSizeLg` | Large font size | `16px` | | `fontWeightNormal` | Normal font weight | `400` | | `fontWeightMedium` | Medium font weight | `500` | | `fontWeightBold` | Bold font weight | `700` | ### Layout | Property | Description | Default | |----------|-------------|----------| | `sidebarWidth` | Sidebar width | `280px` | | `borderRadius` | Border radius | `4px` | | `borderRadiusLg` | Large border radius | `6px` | ### Effects | Property | Description | Default | |----------|-------------|----------| | `shadow` | Main shadow | `2px 0 6px rgba(0,0,0,0.1)` | | `shadowSm` | Small shadow | `0 1px 2px rgba(0, 0, 0, 0.05)` | ### Other | Property | Description | Default | |----------|-------------|----------| | `zIndexSidebar` | Sidebar z-index | `1000` | | `zIndexSticky` | Sticky header z-index | `1000` | | `animationDuration` | Animation duration | `0.2s` | | `iconColor` | Icon color | `#000000` | | `iconColorSecondary` | Secondary icon color | `#364153` | ## Example Themes ### Dark Theme A complete dark theme for those who prefer dark mode: ![Dark Theme Preview](/images/dark_theme.png) ```ts // vite.config.ts import { defineConfig } from 'vite'; import { twd } from 'twd-js/vite-plugin'; const darkTheme = { primary: '#2dd4bf', buttonPrimary: '#2dd4bf', buttonPrimaryText: '#042f2e', background: '#0b0f14', backgroundSecondary: '#111827', skipBg: '#111827', border: 'rgba(255, 255, 255, 0.08)', borderLight: 'rgba(255, 255, 255, 0.12)', buttonBorder: 'rgba(255, 255, 255, 0.12)', text: '#e5e7eb', textSecondary: '#9ca3af', textMuted: '#6b7280', describeBg: '#0f172a', describeText: '#94a3b8', describeBorder: '#334155', success: '#22c55e', successBg: 'rgba(34, 197, 94, 0.15)', error: '#f87171', errorBg: 'rgba(248, 113, 113, 0.15)', warning: '#facc15', warningBg: 'rgba(250, 204, 21, 0.15)', skip: '#6b7280', buttonSecondary: '#111827', buttonSecondaryText: '#e5e7eb', sidebarWidth: '320px', borderRadius: '10px', shadow: '0 0 0 1px rgba(255,255,255,0.05), 0 8px 24px rgba(0,0,0,0.6)', shadowSm: '0 1px 2px rgba(0,0,0,0.4)', iconColor: '#e5e7eb', iconColorSecondary: '#9ca3af', }; export default defineConfig({ plugins: [twd({ open: true, theme: darkTheme })], }); ``` ### Minimal Theme A clean, minimal theme with subtle colors and increased spacing: ![Minimal Theme Preview](/images/theme_minimal.png) ```ts // vite.config.ts import { defineConfig } from 'vite'; import { twd } from 'twd-js/vite-plugin'; const minimalTheme = { primary: '#6366f1', background: '#ffffff', backgroundSecondary: '#f8fafc', border: '#e2e8f0', borderLight: '#cbd5e1', text: '#1e293b', textSecondary: '#64748b', textMuted: '#94a3b8', describeBg: '#f8fafc', describeText: '#64748b', describeBorder: '#cbd5e1', success: '#10b981', successBg: '#ecfdf5', error: '#f43f5e', errorBg: '#fff1f2', warning: '#f59e0b', warningBg: '#fffbeb', skip: '#f1f5f9', skipBg: '#f8fafc', buttonPrimary: '#6366f1', buttonPrimaryText: '#ffffff', buttonSecondary: '#f1f5f9', buttonSecondaryText: '#475569', buttonBorder: '#e2e8f0', spacingXs: '6px', spacingSm: '8px', spacingMd: '12px', spacingLg: '16px', spacingXl: '20px', sidebarWidth: '300px', borderRadius: '12px', borderRadiusLg: '16px', shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', shadowSm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', }; export default defineConfig({ plugins: [twd({ open: true, theme: minimalTheme })], }); ``` ## CSS Variable Override Alternatively, you can override theme variables directly in your CSS without using the JavaScript API. This is useful if you want to use CSS preprocessors or keep all styling in CSS files: ```css /* In your global CSS file */ :root { --twd-primary: #2563eb; --twd-background: #1e293b; --twd-text: #f1f5f9; --twd-success: #22c55e; --twd-error: #ff5252; --twd-background-secondary: #182130; --twd-describe-bg: #0f172a; --twd-describe-text: #94a3b8; --twd-describe-border: #334155; /* ... other variables */ } ``` All theme properties are automatically converted to CSS variables with the `--twd-` prefix. For example: - `primary` → `--twd-primary` - `backgroundSecondary` → `--twd-background-secondary` - `buttonPrimary` → `--twd-button-primary` ## TypeScript Support For full TypeScript support and autocomplete, import the `TWDTheme` type: ```tsx import type { TWDTheme } from 'twd-js/bundled'; const myTheme: Partial = { primary: '#2563eb', background: '#ffffff', // TypeScript will autocomplete all available properties }; ``` ## Best Practices 1. **Start with defaults**: Only override the properties you need to change 2. **Maintain contrast**: Ensure text colors have sufficient contrast against backgrounds for accessibility 3. **Test status colors**: Make sure success, error, and warning colors are clearly distinguishable 4. **Consistent spacing**: Use the spacing scale consistently throughout your theme 5. **Consider dark mode**: If your app supports dark mode, create a matching dark theme for TWD ## Complete Example Here's a complete example combining theme customization with other options: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), twd({ open: true, position: 'right', search: true, serviceWorker: true, serviceWorkerUrl: '/mock-sw.js', theme: { primary: '#8b5cf6', background: '#faf5ff', text: '#4c1d95', sidebarWidth: '320px', borderRadius: '8px', }, }), ], }); ``` # Project Setup Source: https://twd.dev/twd-ai/setup Now that AI is generating a big portion of our frontend code, speed is no longer the main bottleneck. **Confidence is.** We've seen this before: when teams wanted to move fast, the real enabler wasn't "more code" — it was **having a solid testing strategy** that allowed safe refactoring and iteration. That hasn't changed. If anything, it's **more important now**. > Test what you own. Mock what you don't. With this mindset, **TWD (Test While Developing)** lets you create **deterministic UI tests**, where you fully control the environment, simulate any scenario, and avoid flaky behavior. ## The Problem: AI Doesn't Know Your Project AI can write tests... but not *your* tests. It doesn't know: - How your project is structured - What should be mocked - How your auth works - What "good tests" look like in your codebase So even if it generates tests, they often: - Don't follow your patterns - Mock the wrong things - Miss important flows ## The Solution: TWD + Skills To solve this, we introduced TWD skills inside a [Claude Code plugin](https://github.com/BRIKEV/twd-ai). These skills give the AI: - **Context** about your project - **Rules** to follow - **Patterns** to reuse So instead of generic tests, you get tests that actually fit your codebase. ## `/twd:setup` — The Most Important First Step Everything starts here: ```plaintext /twd:setup ``` This is an interactive setup that analyzes your project and creates a project-specific testing configuration file: ``` .claude/twd-patterns.md ``` This file becomes the **source of truth** for how tests should be written in your project. ## What It Does - Detects your framework, Vite config, entry points, CSS setup - Asks about: - Auth & permissions - API structure - Third-party dependencies - State management - Generates a project-specific testing config - Optionally installs and configures TWD for you ## How It Looks in Practice You just run `/twd:setup` and the agent starts understanding your project: ![TWD Setup - First questions](/images/tutorial/twd-setup-first-question.png) It will ask the right questions to understand how your app works and detect your frontend configuration — what should be mocked, how auth behaves, where your services live: ![TWD Setup - Second questions](/images/tutorial/twd-setup-second-question.png) Then it reviews your answers before generating the config: ![TWD Setup - Review answers](/images/tutorial/twd-setup-review-answers.png) Once completed, it generates your config file `.claude/twd-patterns.md`: ![TWD Setup - Generated patterns file](/images/tutorial/twd-setup-twd-pattern.png) It also installs TWD and creates a default test file to verify everything works: ![TWD Setup - Installation](/images/tutorial/twd-setup-installation.png) ![TWD Setup - Default test file](/images/tutorial/twd-setup-default-test.png) ![TWD Setup - Default test executed successfully](/images/tutorial/twd-setup-default-test-executed.png) This is **critical** — every future test the AI writes uses this config as context. And when it's done, it guides you to the next step: ![TWD Setup - Complete](/images/tutorial/twd-setup-completed.png) ## Why This Matters Without this step, AI-generated tests are: - Generic - Inconsistent - Sometimes useless With `/twd:setup`, tests become **aligned with your architecture**. ## What's Next Now that your project is configured, it's time to write your first tests with the AI agent.
    [Writing Tests →](./writing-tests)
    # Writing Tests Source: https://twd.dev/twd-ai/writing-tests Once your project is configured with `/twd:setup`, you can start writing tests using the `/twd` skill. This is the core of the TWD + AI workflow — the agent writes tests, executes them in your real browser, and iterates until they pass. ## How It Works The `/twd` skill doesn't just generate test code — it also **runs** the tests using the [twd-relay](https://github.com/BRIKEV/twd-relay) package. The relay connects the AI agent to your browser via WebSocket, so the agent can: 1. Write a test based on your project patterns (from `.claude/twd-patterns.md`) 2. Execute it in the browser through the relay 3. Read the results (pass/fail with error details) 4. Fix any failures and re-run The token usage is remarkably low — the relay executes commands in the terminal that interact with the browser and send text-based results back to the agent. No screenshots or heavy payloads. ## Test-First Approach We recommend running `/twd` **before** implementing a feature. Write the tests first, then build the implementation to make them pass. This gives you: - A clear specification of what the feature should do - Immediate feedback as you implement - Confidence that the feature works when the tests go green ## See It in Action Here's a video showing the full workflow — from writing tests to executing them in the browser: ## What's Next With tests in place, the next step is setting up CI so your tests run automatically on every push.
    [CI Setup →](./ci-setup)
    # CI Setup Source: https://twd.dev/twd-ai/ci-setup Setting up CI early is one of the best things you can do for your project. The `/twd:ci-setup` skill automates the entire process — from detecting your project configuration to generating GitHub Actions workflows. ## Why Set Up CI Early The longer you wait to add CI, the harder it gets. Tests that pass locally but fail in CI often reveal: - Missing environment variables - Hardcoded ports or paths - Dependencies that aren't properly declared Setting up CI right after your first tests means you catch these issues before they compound. ## Running the Skill ```plaintext /twd:ci-setup ``` Just like `/twd:setup`, this skill starts with a discovery phase. It detects your project configuration and asks whether you want to include code coverage: ![TWD CI Setup - Questions](/images/tutorial/twd-ci-setup-questions.png) It will ask about the dependencies to install, then generate all CI workflow files for you: ![TWD CI Setup - Install dependencies](/images/tutorial/twd-ci-setup-install-dependencies.png) ![TWD CI Setup - Done](/images/tutorial/twd-ci-setup-done.png) ## What It Generates - A GitHub Actions workflow file for running TWD tests - Optional coverage collection and reporting - Proper dependency installation and build steps - Configuration based on your detected dev server port and base path ## What's Next With CI running, you now have a safety net. Next, let's find out what you're missing with test gap analysis.
    [Test Gap Analysis →](./test-gaps)
    # Test Gap Analysis Source: https://twd.dev/twd-ai/test-gaps You've written some tests, CI is running — but how do you know if you've covered enough? The `/twd:test-gaps` skill scans your project to find untested and partially-tested routes, then classifies them by risk. ## Running the Skill ```plaintext /twd:test-gaps ``` The skill cross-references your routes against your existing TWD test files and produces a prioritized report: ![TWD Gap Analysis - Results](/images/tutorial/twd-gap-analysis.png) ![TWD Gap Analysis - Detailed breakdown](/images/tutorial/twd-gap-analysis-2.png) ## How It Works 1. **Route discovery** — Detects all routes in your app (from framework router configs, page component patterns, or test URLs) 2. **Coverage classification** — Each route is classified as: - **Tested** — Has both `twd.visit()` and `userEvent` interactions - **Partially tested** — Has visits but missing interaction or mutation mock coverage - **Untested** — No matching test files at all 3. **Risk assessment** — Reads component code to assign risk (HIGH/MEDIUM/LOW) based on mutations, financial handling, permissions, and UI complexity ## Filling the Gaps Once you know what's missing, you can run the `/twd` skill again to write tests for the high-priority gaps. Here's a video showing the full flow — from identifying gaps to writing and running the missing tests: ## What's Next Now that you've filled your gaps, let's check how good your tests actually are.
    [Test Quality →](./test-quality)
    # Test Quality Source: https://twd.dev/twd-ai/test-quality Having tests isn't enough — they need to be *good* tests. The `/twd:test-quality` skill analyzes your test files and grades them across four dimensions, giving you actionable feedback on where to improve. ## Running the Skill ```plaintext /twd:test-quality ``` The skill reads each of your TWD test files and evaluates them against four weighted criteria: - **Journey Coverage (35%)** — Do tests cover complete user workflows, not just visibility checks? - **Interaction Depth (20%)** — Is there variety in the user actions being tested? - **Assertion Quality (25%)** — Are assertions verifying real outcomes (payload checks, state) vs. loose checks? - **Error & Edge Cases (20%)** — Are failure scenarios and boundary conditions covered? Each file gets a letter grade (A through D) with a weighted overall score: ![TWD Quality Grades - Overview](/images/tutorial/twd-quality-grades.png) ![TWD Quality Grades - Detailed breakdown](/images/tutorial/twd-quality-grades-2.png) ## Improving Your Tests The skill provides 2-3 actionable suggestions per file that scores below A, referencing the actual test content. You can then run `/twd` again to implement the improvements: ## What's Next Your tests are written, running in CI, covering your routes, and scoring well. The final step: generate a visual gallery so everyone on the team can see what's being tested.
    [Test Flow Gallery →](./flow-gallery)
    # Test Flow Gallery Source: https://twd.dev/twd-ai/flow-gallery The `/twd:test-flow-gallery` skill turns your TWD test files into visual flowcharts with business-friendly summaries. It's a great tool for onboarding new team members, giving product teams visibility into what's being tested, and helping QA understand test coverage at a glance. ## Running the Skill ```plaintext /twd:test-flow-gallery ``` The skill reads each of your TWD test files and generates Mermaid flowcharts — one per test case — with 2-4 sentence summaries written in business language rather than technical jargon. ![TWD Flow Gallery - Overview](/images/tutorial/twd-flow-gallery.png) ![TWD Flow Gallery - Detailed flow](/images/tutorial/twd-flow-gallery-2.png) ![TWD Flow Gallery - Summary view](/images/tutorial/twd-flow-gallery-3.png) ## What It Generates For each test file, the skill creates a colocated `.flows.md` file with: - **Mermaid flowcharts** for each test case — user actions shown as blue rectangles, assertions as green hexagons, API calls as separate subgraphs - **Business-friendly summaries** — describing what the test verifies in plain language - **A root-level index** for quick navigation across all test suites ## Who Benefits - **New developers** — Understand existing test coverage without reading test code - **Product teams** — See which user journeys are covered - **QA** — Identify what's tested and what's not, in visual form ## That's the Full TWD + AI Workflow You've now seen the complete journey: 1. **[Project Setup](./setup)** — Configure your project with `/twd:setup` 2. **[Writing Tests](./writing-tests)** — Write and run tests with `/twd` 3. **[CI Setup](./ci-setup)** — Automate test runs with `/twd:ci-setup` 4. **[Test Gap Analysis](./test-gaps)** — Find what's missing with `/twd:test-gaps` 5. **[Test Quality](./test-quality)** — Grade your tests with `/twd:test-quality` 6. **[Test Flow Gallery](./flow-gallery)** — Visualize your tests with `/twd:test-flow-gallery` Each skill builds on the previous one. Start with setup, and you'll have a complete, AI-driven testing workflow in no time. # Claude Code Plugin Source: https://twd.dev/claude-plugin The [TWD plugin for Claude Code](https://github.com/BRIKEV/twd-ai) gives Claude a set of skills: project setup, autonomous test writing, CI configuration, test quality analysis, gap detection, and visual test documentation. ## Installation First, add it from the Claude Code marketplace: ```bash claude plugin marketplace add BRIKEV/twd-ai ``` Then install the plugin skills: ```bash claude plugin install twd@twd-ai # or update claude plugin update twd@twd-ai ``` That's it. You now have access to all TWD skills and commands. ## `/twd:setup` — Interactive Project Setup Run this once per project. Claude analyzes your codebase and asks a series of questions to configure TWD for your stack. ``` /twd:setup ``` ### What It Auto-Detects Before asking you anything, setup scans your project for: - **Framework** — React, Vue, Angular, or Solid.js - **Vite config** — `vite.config.ts` / `vite.config.js` location - **Entry point** — your main app file (e.g., `main.tsx`, `main.ts`) - **CSS libraries** — Tailwind, Bootstrap, Material UI, etc. - **API folder** — common patterns like `src/api/`, `src/services/` ### Interactive Questions Setup then confirms or asks about: | Question | Why it matters | |----------|---------------| | Framework confirmation | Determines import style and bundled vs. standard setup | | Base path | For `twd.visit()` calls if your app doesn't serve from `/` | | Public folder | Where to install the mock service worker script | | Dev server port | So the relay connects to the right Vite server | | Entry point file | Where to add TWD initialization code | | API folder path | So the agent knows where your API calls live for mocking | | CSS / component library | So tests use the right selectors for your UI kit | | Auth middleware | So the agent knows to stub auth in tests | | Third-party modules to mock | Any modules that need the Sinon stub pattern | ### What It Generates The primary output is **`.claude/twd-patterns.md`** — a project-specific context file that tells Claude (and the `twd` skill) exactly how to write tests for your app. See [below](#claude-twd-patterns-md) for details. ### Optional Automated Setup After generating the patterns file, setup can also: - Install `twd-js` and `twd-relay` packages - Run `npx twd-js init public` to install the service worker - Add the `twd()` plugin to your `vite.config.ts` (or fall back to manual `initTWD` in your entry point for non-Vite projects) - Add `twdRemote()` to your Vite config - Create a first test file to validate the setup You can accept or skip each step. --- ## `twd` Skill — Autonomous Testing Agent The `twd` skill is an autonomous agent that writes tests, runs them, and fixes failures. You can invoke it explicitly or Claude can spawn it as a sub-agent when it detects testing is relevant. ### Examples ``` Write TWD tests for the Login page ``` ``` Add tests for the checkout flow — mock the payment API ``` ``` Test the user profile component with auth mocking ``` ### The 5-Phase Workflow When the agent runs, it follows this sequence: #### 1. Detect Reads your project structure, `.claude/twd-patterns.md`, and existing test files to understand your app. #### 2. Setup Checks that `twd-js` and `twd-relay` are installed and that the relay is configured. If anything is missing, it tells you what to set up. #### 3. Write Writes test files following your project's patterns — correct imports, selectors, mock patterns, and file naming conventions. #### 4. Run & Fix Runs tests via `npx twd-relay run` and reads the structured output. If tests fail: - **Isolates** the failing test with `--test "name"` to reduce noise (e.g. `npx twd-relay run --test "should show error"`) - **Reads the error** and fixes the test code - **Re-runs** the isolated test - **Repeats** up to 3 attempts per failing test - If still failing after 3 attempts, marks the test as `it.skip` with a comment explaining why #### 5. Report Outputs a summary of what was tested, what passed, and what was skipped (if any). ### Auto-Invocation When the plugin is installed, Claude Code can automatically invoke the `twd` skill as a sub-agent. For example: 1. You ask: _"Add a search filter to the orders page"_ 2. Claude implements the feature 3. Claude sees the `twd` skill and spawns it 4. The agent writes tests, runs them, fixes failures 5. Claude continues with your original task Your main conversation stays clean — the testing work happens in a forked context. --- ## `/twd:ci-setup` — CI/CD Configuration Sets up CI/CD for TWD tests — installs `twd-cli`, optionally configures code coverage, and generates a GitHub Actions workflow. ``` /twd:ci-setup ``` ### What It Does - Detects your project setup (framework, Vite config, existing workflows) - Installs `twd-cli` for headless test running - Optionally sets up code coverage with `vite-plugin-istanbul` + `nyc` (requires Vite) - Generates `.github/workflows/twd-tests.yml` --- ## `/twd:test-flow-gallery` — Visual Test Documentation Reads TWD test files and generates visual Mermaid flowcharts with plain-language summaries — living documentation auto-generated from real tests. ``` /twd:test-flow-gallery ``` ### What It Does - Reads all `*.twd.test.{ts,js}` files in the project - Generates a `.flows.md` file next to each test file with Mermaid diagrams - Creates a `test-flow-gallery.md` index at the project root - Each `it()` block gets a plain-language summary and color-coded flowchart (purple visit nodes, blue actions, green assertions, orange API subgraphs) ### Example Output ``` ## Items Page **What this tests:** A user navigates to the items page and sees a list of all available items. The page loads data from the API and displays each item as a list entry. ``` --- ## `/twd:test-gaps` — Untested Pages Report Scans project routes and cross-references against TWD test files to find untested pages, classify risk, and generate a prioritized gap report. ``` /twd:test-gaps ``` ### What It Does - Detects your framework and reads the route config (React Router, Vue Router, Angular, Next.js, Nuxt, SolidJS) - Discovers all TWD test files and extracts tested routes - Identifies untested and partially tested pages - Classifies risk: **HIGH** (mutations, payments), **MEDIUM** (auth, complex loading), **LOW** (static, read-only) - Outputs a prioritized "Start Here" top-5 list ### Example Output ``` ## Summary - Pages/routes discovered: 22 - Pages with tests: 15 - Pages partially tested: 2 - Pages untested: 5 ## UNTESTED Pages — HIGH Risk | Page | Path | Why High Risk | |---------------|-------------------|----------------------------------| | Checkout | /checkout | Handles payments — direct impact | | User Settings | /settings/account | Has delete account flow | ``` If no TWD tests are found, the skill detects other test frameworks (Playwright, Jest, etc.) and explains how TWD complements them. --- ## `/twd:test-quality` — Test Quality Grading Reads TWD test files, evaluates quality across four dimensions, assigns letter grades (A/B/C/D), and generates an improvement report. ``` /twd:test-quality ``` ### What It Does - Grades each test file across 4 dimensions: - **Journey Coverage** (35%) — complete user flows vs visibility-only checks - **Interaction Depth** (20%) — variety of `userEvent` types (click, type, keyboard, etc.) - **Assertion Quality** (25%) — `be.visible` (weak) to `deep.equal` on API payloads (strong) - **Error & Edge Cases** (20%) — error states, empty states, cancel flows - Assigns a final letter grade per file and an overall project grade - Generates specific grade-up suggestions per file - Ranks improvements by impact ### Example Output ``` # TWD Test Quality Report Files analyzed: 8 Overall grade: C ## Grade Distribution | Grade | Count | Files | |-------|-------|----------------------------------------------------| | A | 2 | user-list.twd.test.ts, order-list.twd.test.ts | | D | 2 | payment-list.twd.test.ts, item-create.twd.test.ts | ### payment-list.twd.test.ts — Grade D | Dimension | Grade | Notes | |--------------------|-------|------------------------------------| | Journey Coverage | D | 1 test, visibility check only | | Interaction Depth | D | No userEvent calls | | Assertion Quality | D | Only be.visible and greaterThan(1) | | Error & Edge Cases | D | Only happy path | **To reach grade C:** Add a search test with userEvent.type + URL assertion. Add column header assertions with have.text. ``` Works best alongside `/twd:test-gaps` — gaps tells you **what's untested**, quality tells you **if existing tests are any good**. --- ## `.claude/twd-patterns.md` This file is the bridge between your project and the AI. It contains: - **Project config** — framework, base path, entry point, Vite config location - **Import patterns** — exact imports for your setup (bundled vs. standard) - **Selector strategy** — which Testing Library queries to prefer for your UI kit - **Mock patterns** — how to mock your API endpoints (URLs, methods, response shapes) - **Auth patterns** — how to stub authentication for test isolation - **Component mock patterns** — which components to wrap with `MockedComponent` - **File conventions** — where test files go, naming pattern (`*.twd.test.ts`) The `twd` skill reads this file before writing any test. If your project changes significantly (new API patterns, different auth system, etc.), re-run `/twd:setup` to regenerate it. --- ## Updating the Plugin To update to the latest version, remove and reinstall: ```bash # Remove and reinstall (the marketplace stays, only the plugin needs reinstalling) claude plugin remove twd@twd-ai claude plugin install twd@twd-ai # or remove and reinstall claude plugin remove twd@twd-ai claude plugin add /path/to/twd-ai ``` --- ## Supported Frameworks The plugin works with any framework that TWD supports: | Framework | Setup Type | |-----------|-----------| | React | Standard or Bundled | | Vue | Bundled (recommended) | | Angular | Bundled (recommended) | | Solid.js | Bundled (recommended) | All frameworks require **Vite** as the build tool. The plugin detects your framework during `/twd:setup` and adjusts the generated patterns accordingly. --- ::: tip Other AI Tools The Claude Code plugin provides the richest experience, but you can use TWD with any AI tool. See the [AI Context & Prompts](/agents) page for Cursor, Copilot, Windsurf, and other agents. ::: # AI Context & Prompts Source: https://twd.dev/agents This page covers the TWD prompts and context files that help any AI coding assistant (Claude, Cursor, Copilot, Windsurf, etc.) generate correct TWD test code. These prompts work with all AI tools. ::: tip Claude Code users For the full autonomous experience — where Claude writes tests, runs them, and fixes failures automatically — see the [Claude Code Plugin](/claude-plugin) page. ::: ## Quick Start: Agent Skills (Recommended) The easiest way to give your AI assistant TWD context — no copy-pasting needed: ```bash npx skills add BRIKEV/twd-ai ``` This installs TWD skills directly into your AI agent. Works with Cursor, Codex, and [35+ more agents](https://github.com/vercel-labs/skills#available-agents). For **Claude Code**, use the plugin instead for the full autonomous agent experience: ```bash claude plugin install BRIKEV/twd-ai ``` See the [AI Integration overview](/ai-overview) for details. ## Alternative: Manual Setup If you prefer manual configuration, copy the comprehensive prompt from: **[`ai-guides/TWD_PROMPT.md`](https://github.com/brikev/twd/blob/main/ai-guides/TWD_PROMPT.md)** This file contains everything your AI needs to write TWD tests correctly. ### How to Use | Tool | Instructions | |------|-------------| | **Claude Code** | Add to your project's `CLAUDE.md` file | | **Cursor** | Paste into Settings > Rules for AI or `.cursorrules` | | **GitHub Copilot** | Add to `.github/copilot-instructions.md` | | **Windsurf** | Add to your AI context configuration | --- ## Compact Prompt For quick copy-paste, here's a condensed version with the essentials: ```text # TWD (Test While Developing) Context ## Overview TWD is an in-browser test runner. Tests run in the browser (not Node.js). Syntax is similar to Jest/Cypress. ## Core Rules 1. Imports — use in every TWD test file: import { twd, userEvent, screenDom, expect } from "twd-js"; import { describe, it, beforeEach, afterEach } from "twd-js/runner"; 2. File naming: *.twd.test.ts or *.twd.test.tsx The plugin default pattern matches .ts ONLY. For .tsx tests the project needs twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' }) or they are skipped silently. 3. Async/Await: - twd.get() and twd.getAll() are async. Always await them. - userEvent methods (click, type) are async. Always await them. - Test functions passed to it() should be async. 4. Assertions: - Use .should(assertion, value) on elements from twd.get(). - Common: "have.text", "contain.text", "be.visible", "have.value", "have.class", "be.disabled", "have.attr". - Use Chai expect for non-element assertions. ## Common Patterns ### Basic test structure import { twd, userEvent, screenDom, expect } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; describe("Feature Name", () => { beforeEach(() => { twd.clearRequestMockRules(); twd.clearComponentMocks(); }); it("should perform an action", async () => { /* test logic */ }); }); ### Selecting elements Preferred: screenDom (Testing Library) const heading = screenDom.getByRole("heading", { name: "Welcome" }); const submitBtn = screenDom.getByRole("button", { name: "Submit" }); const emailInput = screenDom.getByLabelText("Email Address"); // For modals/portals: use screenDomGlobal instead Fallback: twd.get() with CSS selectors const container = await twd.get(".custom-container"); ### Interactions (userEvent) const user = userEvent.setup(); await user.click(btn); await user.type(input, "text"); // With twd.get(): use .el for raw DOM — await user.click(rawBtn.el); ### Navigation await twd.visit("/path"); ### Assertions message.should("have.text", "Success"); message.should("contain.text", "saved"); message.should("be.visible"); message.should("not.be.visible"); input.should("have.value", "test@example.com"); message.should("have.class", "success-alert"); button.should("be.disabled"); button.should("be.enabled"); checkbox.should("be.checked"); element.should("have.attr", "type", "submit"); await twd.url().should("contain.url", "/dashboard"); ### Mocking requests Define mocks BEFORE the action that triggers the request. await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1, name: "John" }, status: 200 }); await twd.waitForRequest("getUser"); ### Component mocking // In component: wrap with // In test: twd.mockComponent("Chart", () =>
    Mocked
    ); ### Component tests (Testing Library render) Mount one component in isolation, in the same real browser. import { render, screen, cleanup } from "@testing-library/react"; import { componentHost, restorePage } from "./support/componentHost"; afterEach(() => { cleanup(); restorePage(); }); // browser DOM is not torn down between tests render(, { container: componentHost() }); twd.should(screen.getByText("Add Item"), "be.visible"); componentHost() detaches the app root and returns a blank div at the top of the page, so screen only sees what the test rendered; restorePage() puts the app back. Write that helper into the project if it is missing: see https://twd.dev/component-testing Use screen (or screenDomGlobal), NOT screenDom: render() mounts outside the app root. Use the real providers. Mock only the network, with twd.mockRequest. ### Module stubbing (Sinon) Tests run in the browser, so use Sinon for stubs/spies. ESM constraint: named exports (export const foo = ...) are IMMUTABLE and CANNOT be stubbed. Solution: wrap in an object and export as default. // hooks/useAuth.ts — CORRECT (stubbable) import { useAuth0 } from "@auth0/auth0-react"; const useAuth = () => useAuth0(); export default { useAuth }; // hooks/useAuth.ts — WRONG (not stubbable) export const useAuth = () => useAuth0(); // In test: import authSession from '../hooks/useAuth'; import Sinon from 'sinon'; Sinon.stub(authSession, 'useAuth').returns({ isAuthenticated: true, ... }); // Clean up in beforeEach: Sinon.restore(); ## Do's and Don'ts DO: await twd.get(); await userEvent actions; use .el when passing twd.get() result to userEvent. DO: Clear mocks in beforeEach: twd.clearRequestMockRules(); twd.clearComponentMocks(); DO: Mock requests BEFORE twd.visit() or triggering the request. DON'T: use cy.get or cy.visit (not Cypress); use global describe/it — always import from "twd-js/runner". DON'T: assume Node.js (fs, path) is available — tests run in browser. DON'T: try to stub named exports (export const fn = ...) — ESM makes them immutable. Wrap in an object and export default. DO: use Sinon for module stubs/spies. Always call Sinon.restore() in beforeEach. ``` # AI Remote Testing Source: https://twd.dev/ai-remote-testing TWD Relay (`twd-relay`) enables AI coding agents (Claude Code, Cursor, Copilot) to **run in-browser validations and read structured results** — without launching a browser automation tool. Your app is already running with TWD loaded; the relay just opens a WebSocket bridge so external tools can trigger test runs and stream results back. ::: tip How this fits with other AI features - **[AI Context](/agents)** — Prompts so your AI writes correct TWD tests - **AI Remote Testing (this page)** — Run tests and get results via WebSocket - **[Auto-Invocation](/ai-overview#_3-auto-invocation-claude-code)** — Claude Code automatically writes, runs, and fixes tests ::: ## The Problem During development, TWD tests run in the browser and results appear in the sidebar UI. That's great when you're looking at it — but AI agents run as CLI processes. They can edit files and run shell commands, but they can't click "Run All" in your browser. The irony: the Vite dev server is already running, TWD is already loaded, and the test runner exists in memory. We just need a bridge. AI agents need structured, parseable pass/fail signals — not screenshots or DOM dumps. The relay provides exactly that: consistent results the agent can read and act on. ## How It Works The relay is a WebSocket server that routes messages between the **browser** (where TWD runs) and **external clients** (AI agents, scripts). ``` ┌───────────────┐ WebSocket ┌──────────────────┐ ┌───────────────────┐ │ AI Agent │◄──────────────────►│ Relay Server │◄──────────────►│ Browser (TWD) │ │ (Claude Code,│ /__twd/ws │ (Vite plugin or │ │ Test runner + │ │ script) │ │ standalone) │ │ sidebar UI │ └───────────────┘ └──────────────────┘ └───────────────────┘ ``` 1. Browser connects and identifies as `role: "browser"` 2. Client (AI agent) connects and identifies as `role: "client"` 3. Client sends `{ type: "run", scope: "all" }` 4. Relay forwards to browser → tests execute → events stream back 5. Client receives `run:complete` with all test results ## Requirements ::: warning Version requirements - **twd-js** `>=1.5.2` — earlier versions do not support the relay protocol - **twd-relay** `>=1.1.0` — earlier versions lack throttle-abort detection, heartbeat-based run recovery, and the tab indicator. `>=1.0.0` still works but without those safety features. ::: ## Installation ```bash npm install --save-dev twd-relay ``` ## Setup ### Option A: Vite Plugin (recommended) If you already use Vite, attach the relay to your dev server alongside the `twd()` plugin — the WebSocket runs on the same host/port: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd } from 'twd-js/vite-plugin'; import { twdRemote } from 'twd-relay/vite'; export default defineConfig({ plugins: [ react(), twd(), // sidebar + test discovery twdRemote(), // adds /__twd/ws to your dev server ], }); ``` Then connect the browser client in your app entry: ```ts // main.ts import { createBrowserClient } from 'twd-relay/browser'; if (import.meta.env.DEV) { const client = createBrowserClient(); client.connect(); } ``` When using the Vite plugin the URL is auto-detected — no configuration needed. ### Option B: Standalone Server For non-Vite projects, run the relay as a separate process: ```bash npx twd-relay --port 9876 ``` Then connect the browser client with an explicit URL: ```ts import { createBrowserClient } from 'twd-relay/browser'; const client = createBrowserClient({ url: 'ws://localhost:9876/__twd/ws', }); client.connect(); ``` ## Tab Identifier Once the browser client connects, the tab's favicon turns blue and its title gains a `[TWD]` prefix. The prefix reflects the current state so you can spot the active TWD tab at a glance, especially when multiple tabs are open to the same origin (a common dev scenario). | Favicon | Title prefix | State | |---|---|---| | Blue | `[TWD]` | Connected, idle | | Orange | `[TWD ...]` | Tests running | | Green | `[TWD ✓]` | Last run passed | | Red | `[TWD ✗]` | Last run failed or was aborted | On disconnect or eviction (another tab taking over the relay slot), the original favicon and title are restored automatically. The indicator requires no configuration — it's on by default when the browser client is connected. ## Triggering a Test Run Once the relay is running and the browser is connected, use the `twd-relay run` CLI to trigger tests: ```bash # If using the Vite plugin (default port 5173) npx twd-relay run # If using the standalone relay on a custom port npx twd-relay run --port 9876 # With a custom timeout (default: 180s) npx twd-relay run --timeout 30000 # Run specific tests by name (substring match, case-insensitive) npx twd-relay run --test "should show error" # Run multiple specific tests npx twd-relay run --test "login" --test "signup" ``` The `--test` flag filters tests by name using substring matching. When used and no tests match, the CLI prints the available test names so you can correct the filter. This is especially useful for AI agents to run only the tests they're working on without modifying the test file. The command connects to the relay, sends a run command, streams test output to the terminal, and exits with code 0 (all passed) or 1 (failures or timeout). Example output: ``` Connecting to ws://localhost:5173/__twd/ws... Browser connected, triggering test run... Running 3 test(s)... RUN: Counter > increments when button is clicked PASS: Counter > increments when button is clicked (42ms) RUN: Counter > resets to zero PASS: Counter > resets to zero (18ms) RUN: Counter > displays initial value PASS: Counter > displays initial value (5ms) --- Run complete --- Passed: 3 | Failed: 0 | Skipped: 0 Duration: 0.1s ``` ### From an AI agent Add this to your agent's instructions (e.g. `CLAUDE.md`): ```text To run TWD tests: npx twd-relay run To run specific tests: npx twd-relay run --test "test name" Exit code 0 means all tests passed; 1 means failures or errors. ``` The agent can then run tests, read failures, fix code, and re-run — all in a tight loop without needing Playwright or Puppeteer. This is the core AI iteration loop: write, run, read, fix, repeat. ## Visibility Fallback When an AI agent triggers tests via the relay, the browser tab is typically in the background (you're in your editor or terminal). This causes `@testing-library/user-event` methods like `type()` to behave incorrectly because the element doesn't truly have focus. TWD handles this automatically. The `userEvent` proxy detects when the document is hidden or unfocused and falls back to a programmatic approach: - Sets the value using the native input setter (required for React controlled inputs) - Dispatches `input` and `change` events so frameworks pick up the change This happens transparently — your tests don't need to change. It currently applies to `userEvent.type()`. Other methods like `click()` work normally even when the tab is in the background. ## Handling Throttled or Stuck Runs Chrome aggressively throttles timers in backgrounded tabs. A test run that normally finishes in ~1 second can stretch to 20+ seconds when the TWD tab isn't focused. The relay and browser client cooperate on two recovery mechanisms so AI agents and CI scripts never hang silently waiting on a frozen or throttled tab. ### Throttle-abort The browser client monitors wall-clock time for each test. If any single test exceeds **10 seconds** (default, configurable), it aborts the run and emits a new `run:aborted` event. The CLI prints a clear multi-line error and exits with code 1: ``` Run aborted: test "App interactions > test button" ran for 12.4s — threshold exceeded. The TWD browser tab is likely backgrounded and throttled by the browser. Foreground the TWD tab (identified by the "[TWD …]" title prefix) and keep it active, then retry. For unattended runs, prefer `twd-cli` which drives a headless browser with no tab throttling. ``` The 10 s default sits above Testing Library's default `findBy*` timeout (3 s) so a legitimately failing test with one or two missed selectors does not false-abort. Throttled runs — where tests typically cluster in the 10–30 s band — still trip the abort reliably. Tune the threshold when a test legitimately needs longer: ```bash # Raise the threshold to 20 seconds npx twd-relay run --max-test-duration 20000 # Disable abort detection entirely npx twd-relay run --max-test-duration 0 ``` Or per-project via the browser client option: ```ts createBrowserClient({ maxTestDurationMs: 20000 }); ``` Detection fires on two triggers for reliability: the 3-second heartbeat tick (for in-flight tests) and on every test completion (for tests that finish just over threshold between heartbeat ticks). ::: tip Prefer `twd-cli` for CI and unattended agents `npx twd-cli run` drives a headless Chrome where the page is always foregrounded, so tab throttling never applies. No abort threshold needed. Use the relay for interactive dev, `twd-cli` for automated runs. ::: ### Frozen-tab recovery While a run is in progress the browser client sends a heartbeat every 3 seconds. If the relay stops receiving heartbeats for 120 seconds (e.g. the tab was closed, crashed, or Chrome froze it completely), the relay marks the run as abandoned and broadcasts a `run:abandoned` event. The CLI surfaces this with a clear error and exits 1: ``` Run abandoned — browser tab appears frozen. Refresh the browser tab and retry. ``` The run lock is cleared automatically so the next run can start without a manual reset. ### Improved `RUN_IN_PROGRESS` error If a second `run` command arrives while one is already active, the relay now returns a detailed recovery message instead of the old bare sentence: ``` [RUN_IN_PROGRESS] A test run is already in progress. If the previous run appears stuck, the browser tab may be backgrounded and throttled — foreground the TWD tab (identified by the "[TWD …]" title prefix) or reload it. The relay also auto-clears the lock after 120s of heartbeat silence. ``` The error `code` is unchanged (`RUN_IN_PROGRESS`) — only the human-readable `message` is richer. Existing code that dispatches on `code` continues to work. ## Protocol Reference All messages are JSON over WebSocket. The `twd-relay run` CLI handles this protocol for you, but if you want to build a custom client: ### Client → Relay | Message | Description | |---------|-------------| | `{ type: "hello", role: "client" }` | Identify as an external client | | `{ type: "run", scope: "all" }` | Run all tests | | `{ type: "run", scope: "all", testNames: ["..."] }` | Run tests matching names (substring, case-insensitive) | | `{ type: "run", scope: "all", maxTestDurationMs: 15000 }` | Run all tests with a custom per-test abort threshold (ms). `0` disables detection. Omit to use the browser client's default (5000). | ### Browser → Relay | Message | Description | |---------|-------------| | `{ type: "hello", role: "browser" }` | Identify as the browser | | `{ type: "heartbeat" }` | Sent every 3 seconds during a run. Not forwarded to clients; drives the relay's 120-second `run:abandoned` timeout. | ### Browser → Relay → Client | Message | Description | |---------|-------------| | `{ type: "connected", browser: true }` | Browser is connected and ready | | `{ type: "test:start", testId, name }` | A test started running | | `{ type: "test:pass", testId, name }` | A test passed | | `{ type: "test:fail", testId, name, error }` | A test failed | | `{ type: "test:skip", testId, name }` | A test was skipped | | `{ type: "run:complete", passed, failed, skipped, duration }` | All tests finished (also emitted after `run:aborted`, so the lock clears) | | `{ type: "run:aborted", reason: "throttled", durationMs, testName }` | Browser aborted because a test exceeded the threshold. Followed immediately by `run:complete`. | | `{ type: "run:abandoned", reason: "heartbeat_timeout" }` | Relay declared the run abandoned after 120 s of heartbeat silence. | ## Sidebar Integration When the relay triggers a test run, the TWD sidebar updates in real time. The browser client dispatches a `twd:state-change` event after each status update, and the sidebar listens for it to re-render with the latest results. You don't need to configure anything — if the relay is connected, the sidebar reflects relay-triggered runs automatically. A relay-triggered run executes at full speed, which makes it hard to see what an agent's new test actually did. Enable the sidebar speed selector with `twd({ pace: true })` and pick `Slow (300ms)`: the pace applies to relay-triggered runs too, so you can watch the flow the agent wrote before deciding to keep it. See [Recording Runs](/recording#watching-a-run-without-recording-it). # API Reference Source: https://twd.dev/api/ Complete reference documentation for all TWD functions, methods, and types. ## Quick Navigation | Section | Description | |---------|-------------| | [Initialization](/api/#initialization) | `twd()` Vite plugin (recommended) and `initTWD()` (manual) | | [Test Functions](/api/test-functions) | `describe`, `it`, `beforeEach`, `it.only`, `it.skip`, `afterEach` | | [TWD Commands](/api/twd-commands) | `twd.get()`, `twd.visit()`, `twd.matchLayout()`, `twd.mockRequest()`, etc. | | [Assertions](/api/assertions) | All available assertions and their usage | ## Import Reference ```ts // Main imports import { describe, it, beforeEach, afterEach, twd, userEvent, screenDom, screenDomGlobal, expect } from "twd-js"; // Vite Plugin (Recommended - works with all Vite-based frameworks) import { twd } from "twd-js/vite-plugin"; // Bundled Setup (manual API; the plugin uses this internally) import { initTWD } from "twd-js/bundled"; // UI Component (for React apps using standard setup) import { TWDSidebar } from "twd-js"; // Production-build Vite plugin (cleanup) import { removeMockServiceWorker } from "twd-js/vite-plugin"; // CI Integration (for test execution) import { reportResults } from "twd-js"; // twd() plugin options: // testFilePattern?: string // Glob for discovering test files (default: '/**/*.twd.test.ts') // open?: boolean // Whether the sidebar is open by default (default: true) // position?: "left" | "right" // Sidebar position (default: "left") // search?: boolean // Show search input to filter tests (default: false) // serviceWorker?: boolean // Enable request mocking (default: true) // serviceWorkerUrl?: string // Custom service worker path (default: '/mock-sw.js') // theme?: Partial // Custom theme // TWDSidebar props (for standard setup): // open?: boolean // Whether the sidebar is open by default (default: true) // position?: "left" | "right" // Sidebar position (default: "left") // search?: boolean // Show search input to filter tests (default: false) ``` ## Initialization TWD provides two ways to initialize the in-browser sidebar and discover tests: 1. **`twd()` Vite plugin** (recommended) — declarative, lives in `vite.config.ts`, no entry-file changes. 2. **`initTWD()` manual API** — for non-Vite projects (Angular, Webpack/CRA) or when you need direct control. ### twd(options?) — Vite plugin The recommended setup for any Vite-based project (React, Vue, Solid.js, Astro, etc.). Auto-injects test discovery and sidebar mounting in `vite dev`. No-op in production builds. #### Syntax ```ts import { twd } from "twd-js/vite-plugin"; twd(options?: TwdPluginOptions): VitePlugin ``` #### TwdPluginOptions ```ts interface TwdPluginOptions { testFilePattern?: string; // Glob pattern for discovering test files (default: '/**/*.twd.test.ts') open?: boolean; // Whether the sidebar is open by default (default: true) position?: "left" | "right"; // Sidebar position (default: "left") search?: boolean; // Show search input to filter tests (default: false) serviceWorker?: boolean; // Enable request mocking (default: true) serviceWorkerUrl?: string; // Custom service worker path (default: '/mock-sw.js') theme?: Partial; // Custom theme — see /theming rootSelector?: string; // Override the app root for screenDom queries } ``` #### Examples ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // or vue, solid import { twd } from 'twd-js/vite-plugin'; // Minimal — all defaults export default defineConfig({ plugins: [react(), twd()], }); // Custom sidebar configuration export default defineConfig({ plugins: [react(), twd({ open: false, position: 'right' })], }); // Different test file pattern export default defineConfig({ plugins: [react(), twd({ testFilePattern: '/**/*.spec.{ts,tsx}' })], }); // Disable request mocking export default defineConfig({ plugins: [react(), twd({ serviceWorker: false })], }); ``` #### Notes - The plugin uses `apply: 'serve'`, so it's a no-op in `vite build`. Production bundles never include TWD code. - Full-reload on test-file edits is handled automatically. - Internally the plugin imports from `twd-js/bundled` and calls `initTWD` for you. Same runtime, less boilerplate. ### initTWD(files, options?) — manual API The lower-level API. Use this for non-Vite projects (Angular, Webpack/CRA) or when you need conditional initialization the plugin doesn't expose. #### Syntax ```ts import { initTWD } from "twd-js/bundled"; initTWD(files: TestModule, options?: InitTWDOptions): void ``` #### Parameters - **files** (`TestModule`) - Object mapping test file paths to async import functions (typically from `import.meta.glob()` in Vite, or built manually in non-Vite environments) - **options** (`InitTWDOptions`, optional) - Configuration options (same shape as `TwdPluginOptions` above, minus `testFilePattern` since you supply `files` directly) #### InitTWDOptions ```ts interface InitTWDOptions { open?: boolean; // Whether the sidebar is open by default (default: true) position?: "left" | "right"; // Sidebar position (default: "left") search?: boolean; // Show search input to filter tests (default: false) serviceWorker?: boolean; // Enable request mocking (default: true) serviceWorkerUrl?: string; // Custom service worker path (default: '/mock-sw.js') theme?: Partial; // Custom theme rootSelector?: string; // Override the app root for screenDom queries } ``` #### Returns `void` #### Examples ```ts // Minimal setup - uses all defaults const tests = import.meta.glob("./**/*.twd.test.ts"); initTWD(tests); // Custom sidebar configuration initTWD(tests, { open: false, position: 'right' }); // Enable search filter for tests initTWD(tests, { search: true }); // Disable request mocking initTWD(tests, { serviceWorker: false }); // Custom service worker path initTWD(tests, { serviceWorkerUrl: '/custom-path/mock-sw.js' }); // All options together initTWD(tests, { open: true, position: 'right', serviceWorker: true, serviceWorkerUrl: '/my-mock-sw.js' }); ``` #### Framework Examples (manual init) **Angular** (no Vite — manual init required): ```ts // TWD_ENABLED comes from the `define` option in angular.json — see the Angular // section in Framework Setup. Do not use isDevMode(): it is a function call, so // esbuild keeps the branch and ships ~580 K of unused chunks in production. declare const TWD_ENABLED: boolean | undefined; if (typeof TWD_ENABLED !== 'undefined' && TWD_ENABLED) { const { initTWD } = await import('twd-js/bundled'); const tests = { './tests/example.twd.test.ts': () => import('./tests/example.twd.test'), }; initTWD(tests); } ``` **Webpack / CRA** (manual init): ```ts if (process.env.NODE_ENV === "development") { const context = require.context("./", true, /\.twd\.test\.ts$/); const tests = {}; context.keys().forEach((key) => { tests[key] = async () => Promise.resolve(context(key)); }); const { initTWD } = await import('twd-js/bundled'); initTWD(tests); } ``` For Vite-based React/Vue/Solid examples, prefer the `twd()` plugin shown above. #### Notes - The bundled setup automatically handles React dependencies internally - Request mocking is initialized automatically by default (`serviceWorker: true`) - Works with all supported frameworks (React, Vue, Angular, Solid.js) - Test files are excluded from production builds when the guard folds to a constant at build time: `import.meta.env.DEV` (Vite), `process.env.NODE_ENV` (Webpack), or a `define`d `TWD_ENABLED` (Angular). A guard the bundler can only evaluate at *runtime* — such as Angular's `isDevMode()` — keeps the branch and its lazy chunks in the output --- ## Type Definitions ### Element API ```ts interface TWDElemAPI { el: Element; // The raw DOM element should: ShouldFn; // Assertion function } ``` ### Assertion Types ```ts type AssertionName = | "have.text" | "contain.text" | "be.empty" | "have.attr" | "have.value" | "be.disabled" | "be.enabled" | "be.checked" | "be.selected" | "be.focused" | "be.visible" | "have.class"; type AnyAssertion = AssertionName | `not.${AssertionName}`; ``` ### Mock Request Types ```ts interface Options { method: string; url: string | RegExp; response: unknown; status?: number; headers?: Record; } interface Rule { method: string; url: string | RegExp; response: unknown; alias: string; executed?: boolean; request?: unknown; status?: number; headers?: Record; } ``` ## Core Concepts ### Test Structure TWD follows familiar testing patterns: ```ts describe("Test Suite", () => { beforeEach(() => { // Setup before each test }); it("should do something", async () => { // Test implementation }); it.only("focused test", async () => { // Only this test runs }); it.skip("skipped test", async () => { // This test is skipped }); }); ``` ### Element Selection and Interaction ```ts // TWD native selectors const element = await twd.get("selector"); const elements = await twd.getAll("selector"); // Testing Library queries (also available) // screenDom - for regular content (excludes sidebar) const button = screenDom.getByRole("button", { name: /submit/i }); const input = screenDom.getByLabelText("Email:"); // screenDomGlobal - for portal-rendered elements (modals, dialogs) // ⚠️ Use specific queries to avoid matching sidebar elements const modal = screenDomGlobal.getByRole("dialog", { name: "Confirm" }); // Make assertions element.should("assertion", ...args); // For twd.get() elements twd.should(button, "be.visible"); // For screenDom elements // User interactions const user = userEvent.setup(); await user.click(element.el); await user.type(input, "text"); ``` ### API Mocking ```ts // Mock requests await twd.mockRequest("alias", { method: "GET", url: "/api/endpoint", response: { data: "value" } }); // Wait for requests const rule = await twd.waitForRequest("alias"); const rules = await twd.waitForRequests(["alias1", "alias2"]); // Clean up twd.clearRequestMockRules(); ``` ### Best Practices 1. **Use data attributes** for reliable element selection 2. **Clean up mocks** after each test 3. **Wait appropriately** for async operations 4. **Be specific** with assertions 5. **Test user workflows** rather than implementation details ## Browser Compatibility TWD works in all modern browsers that support: - ES2020+ features - Service Workers (for API mocking) - DOM APIs - Async/await ### Supported Browsers - Chrome 80+ - Firefox 72+ - Safari 13.1+ - Edge 80+ ## Performance Considerations - Element queries use `document.querySelector` internally - Service Worker mocking adds minimal overhead - Tests run in the main thread (no web workers) - Tests are not included in the bundle, so they are not included in the bundle size. ## Debugging ### Browser DevTools - Use browser DevTools to inspect elements - Check Network tab for mocked requests - Console logs show TWD operations - Check sidebar for logs and mock rules ### Common Debug Patterns ```ts // Log element details const element = await twd.get("selector"); console.log("Element:", element.el); console.log("Text content:", element.el.textContent); // Log mock rules console.log("Active mocks:", twd.getRequestMockRules()); // Add debug waits await twd.wait(1000); // Pause to inspect state ``` ## Migration Guide ### From Other Testing Libraries #### From Cypress ```ts // Cypress cy.get('[data-testid="button"]').click(); cy.get('[data-testid="message"]').should('contain', 'Success'); // TWD const button = await twd.get('[data-testid="button"]'); await userEvent.click(button.el); const message = await twd.get('[data-testid="message"]'); message.should('contain.text', 'Success'); ``` #### From Testing Library TWD now supports Testing Library queries directly! You can use the same `screenDom` and `screenDomGlobal` APIs: ```ts // Testing Library const button = screen.getByTestId('button'); fireEvent.click(button); expect(screen.getByTestId('message')).toHaveTextContent('Success'); // TWD - Same API! import { screenDom, screenDomGlobal, userEvent, twd } from 'twd-js'; // screenDom - for regular content (excludes sidebar) const button = screenDom.getByTestId('button'); await userEvent.click(button); const message = screenDom.getByTestId('message'); twd.should(message, 'contain.text', 'Success'); // screenDomGlobal - for portal-rendered elements (modals, dialogs) // ⚠️ Use specific queries to avoid matching sidebar elements const modal = screenDomGlobal.getByRole('dialog', { name: 'Confirm' }); // Or use TWD's native selectors const button = await twd.get('[data-testid="button"]'); await userEvent.click(button.el); const message = await twd.get('[data-testid="message"]'); message.should('contain.text', 'Success'); ``` ## Vite Plugin ### removeMockServiceWorker() Vite plugin that removes the mock service worker file from production builds. This ensures your production bundle doesn't include testing infrastructure. #### Syntax ```ts import { removeMockServiceWorker } from "twd-js"; ``` #### Usage ```ts // vite.config.ts import { defineConfig } from 'vite'; import { removeMockServiceWorker } from 'twd-js'; export default defineConfig({ plugins: [ // ... other plugins removeMockServiceWorker() ] }); ``` #### What it does - **Build-time cleanup**: Automatically removes `mock-sw.js` from the `dist` folder - **Production-safe**: Only runs during build (`apply: 'build'`) - **Zero configuration**: Works out of the box with standard Vite setups - **Logging**: Provides feedback about file removal #### Example Output ```bash # During build 🧹 Removed mock-sw.js from build # If no mock file found 🧹 No mock-sw.js found in build ``` --- ## CI Integration ### reportResults(handlers, testStatus) Formats and displays test results in a readable format with colored output. #### Syntax ```ts reportResults(handlers: Handler[], testStatus: TestResult[]): void ``` #### Parameters - **handlers** (`Handler[]`) - Test handlers from `executeTests()` - **testStatus** (`TestResult[]`) - Test results from `executeTests()` #### Usage ```ts import { executeTests, reportResults } from "twd-js"; // Complete CI workflow const { handlers, testStatus } = await executeTests(); reportResults(handlers, testStatus); // Exit with appropriate code const hasFailures = testStatus.some(t => t.status === 'fail'); process.exit(hasFailures ? 1 : 0); ``` #### Example Output ```bash User Authentication ✓ should login with valid credentials ✓ should logout successfully ✗ should handle invalid credentials - Error: Expected element to contain text "Invalid credentials" Shopping Cart ✓ should add items to cart ○ should remove items from cart (skipped) ``` #### Output Format - **✓** Green checkmark for passed tests - **✗** Red X for failed tests - **○** Yellow circle for skipped tests - **Error details** shown below failed tests - **Hierarchical structure** matches your test organization --- ## Contributing - 📖 [View source code](https://github.com/BRIKEV/twd) - 🐛 [Report issues](https://github.com/BRIKEV/twd/issues) - 💡 [Request features](https://github.com/BRIKEV/twd/discussions) - 🔄 [Submit pull requests](https://github.com/BRIKEV/twd/pulls) # Test Functions Source: https://twd.dev/api/test-functions Core functions for structuring and organizing your TWD tests. ## describe(name, fn) Groups related tests together for better organization and readability. ### Syntax ```ts describe(name: string, fn: () => void): void ``` ### Parameters - **name** (`string`) - Descriptive name for the test group - **fn** (`function`) - Function containing the test group's tests and setup ### Examples ```ts describe("User Authentication", () => { it("should login with valid credentials", async () => { // Test implementation }); it("should reject invalid credentials", async () => { // Test implementation }); }); ``` ### Nested Groups ```ts describe("Shopping Cart", () => { describe("Adding Items", () => { it("should add product to cart", async () => { // Test implementation }); }); describe("Removing Items", () => { it("should remove product from cart", async () => { // Test implementation }); }); }); ``` ## describe.only(name, fn) Run only this `describe` block (and its nested describes/tests). When used, other suites and tests that are not within the `.only` tree will be skipped — this is useful for focusing on a group of related tests while debugging. ### Syntax ```ts describe.only(name: string, fn: () => void): void ``` ### Example ```ts describe.only("Payments", () => { it("should process card payment", async () => { // This test runs (others outside this describe are skipped) }); }); ``` > Tip: Remember to remove `describe.only` before merging or running full CI, as it will skip other tests. ## describe.skip(name, fn) Skips this `describe` block and all its descendant tests. Useful for temporarily disabling an entire suite while you work on other areas. ### Syntax ```ts describe.skip(name: string, fn: () => void): void ``` ### Example ```ts describe.skip("Experimental feature", () => { it("should do something", async () => { // This won't run }); }); ``` Use `describe.skip` for larger blocks that aren't ready or are flaky in certain environments. --- ## it(name, fn) Defines an individual test case. ### Syntax ```ts it(name: string, fn: () => Promise | void): void ``` ### Parameters - **name** (`string`) - Descriptive name for the test - **fn** (`function`) - Test implementation (can be async) ### Examples ```ts it("should display welcome message", async () => { await twd.visit("/"); const heading = await twd.get("h1"); heading.should("contain.text", "Welcome"); }); it("should handle button clicks", async () => { const button = await twd.get("button"); const user = userEvent.setup(); await user.click(button.el); // Assertions... }); ``` ### Async Tests ```ts it("should load user data", async () => { await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { name: "John Doe" } }); await twd.visit("/profile"); await twd.waitForRequest("getUser"); const userName = await twd.get(".user-name"); userName.should("have.text", "John Doe"); }); ``` --- ## itOnly(name, fn) Runs only this test, skipping all others in the suite. Useful for debugging specific tests. ### Syntax ```ts itOnly(name: string, fn: () => Promise | void): void ``` ### Parameters - **name** (`string`) - Descriptive name for the test - **fn** (`function`) - Test implementation (can be async) ### Examples ```ts describe("User Management", () => { itOnly("should create new user", async () => { // Only this test will run await twd.visit("/users/new"); // Test implementation... }); it("should delete user", async () => { // This test will be skipped }); it("should update user", async () => { // This test will also be skipped }); }); ``` ### Use Cases - **Debugging** - Focus on a failing test - **Development** - Work on one test at a time - **Troubleshooting** - Isolate problematic tests ::: warning Remember to remove `itOnly` before committing code, as it will skip other tests in CI/CD. ::: --- ## itSkip(name, fn) Skips this test. Useful for temporarily disabling broken or incomplete tests. ### Syntax ```ts itSkip(name: string, fn: () => Promise | void): void ``` ### Parameters - **name** (`string`) - Descriptive name for the test - **fn** (`function`) - Test implementation (will not be executed) ### Examples ```ts describe("Payment Processing", () => { it("should process credit card payment", async () => { // This test runs normally }); itSkip("should process PayPal payment", async () => { // This test is skipped - maybe PayPal integration isn't ready throw new Error("This won't run"); }); it("should handle payment errors", async () => { // This test runs normally }); }); ``` ### Use Cases - **Incomplete features** - Skip tests for unimplemented functionality - **Known issues** - Temporarily skip failing tests while fixing - **Environment-specific** - Skip tests that don't work in certain environments --- ## beforeEach(fn) Runs a setup function before each test in the current `describe` block. ### Syntax ```ts beforeEach(fn: () => Promise | void): void ``` ### Parameters - **fn** (`function`) - Setup function to run before each test (can be async) ### Examples ```ts describe("User Dashboard", () => { beforeEach(() => { // Reset state before each test localStorage.clear(); twd.clearRequestMockRules(); }); it("should show user profile", async () => { // Clean state guaranteed }); it("should display recent orders", async () => { // Clean state guaranteed }); }); ``` ### Async Setup ```ts describe("API Integration", () => { beforeEach(async () => { // Set up common mocks await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1, name: "Test User" } }); }); it("should load user data", async () => { // Mocks are already set up }); }); ``` ### Nested beforeEach ```ts describe("E-commerce Tests", () => { beforeEach(() => { // Runs before all tests in this describe localStorage.clear(); }); describe("Shopping Cart", () => { beforeEach(() => { // Runs before cart tests (after parent beforeEach) await twd.visit("/cart"); }); it("should add items to cart", async () => { // Both beforeEach functions have run }); }); }); ``` --- ## Best Practices ### 1. Descriptive Test Names ```ts // ✅ Good - Describes what and why it("should show validation error when email is invalid", async () => { // Test implementation }); // ❌ Bad - Too vague it("should validate email", async () => { // Test implementation }); ``` ### 2. Logical Test Grouping ```ts // ✅ Good - Logical grouping describe("User Registration Form", () => { describe("Validation", () => { it("should require email field", async () => {}); it("should require password field", async () => {}); }); describe("Submission", () => { it("should create user on valid input", async () => {}); it("should show success message", async () => {}); }); }); ``` ### 3. Clean State Management ```ts describe("Shopping Cart", () => { beforeEach(() => { // Ensure clean state localStorage.removeItem("cart"); sessionStorage.clear(); twd.clearRequestMockRules(); }); it("should start with empty cart", async () => { // Test with guaranteed clean state }); }); ``` ### 4. Avoid Test Dependencies ```ts // ✅ Good - Each test is independent describe("User Management", () => { it("should create user", async () => { // Creates user from scratch }); it("should delete user", async () => { // Creates user first, then deletes }); }); // ❌ Bad - Tests depend on each other describe("User Management", () => { it("should create user", async () => { // Creates user }); it("should delete user", async () => { // Assumes user from previous test exists }); }); ``` ### 5. Use beforeEach for Common Setup ```ts describe("Authentication Flow", () => { beforeEach(async () => { // Common setup for all auth tests await twd.visit("/login"); }); it("should login successfully", async () => { // No need to repeat visit and mock setup }); it("should handle login errors", async () => { // No need to repeat visit and mock setup }); }); ``` ## Common Patterns ### Test Suite Organization ```ts describe("E-commerce Application", () => { describe("Authentication", () => { beforeEach(() => { await twd.visit("/login"); }); it("should login with valid credentials", async () => {}); it("should reject invalid credentials", async () => {}); it("should redirect after login", async () => {}); }); describe("Product Catalog", () => { beforeEach(() => { await twd.visit("/products"); }); it("should display product list", async () => {}); it("should filter products", async () => {}); it("should search products", async () => {}); }); describe("Shopping Cart", () => { beforeEach(() => { localStorage.setItem("user", JSON.stringify({ id: 1 })); await twd.visit("/cart"); }); it("should add products to cart", async () => {}); it("should remove products from cart", async () => {}); it("should calculate total price", async () => {}); }); }); ``` ### Conditional Test Execution ```ts describe("Feature Tests", () => { const isFeatureEnabled = localStorage.getItem("newFeature") === "true"; if (isFeatureEnabled) { it("should show new feature", async () => { // Test new feature }); } else { itSkip("should show new feature", async () => { // Feature not enabled, skip test }); } }); ``` ### Debug-Focused Testing ```ts describe("Debug Session", () => { // Focus on the failing test itOnly("should handle complex user workflow", async () => { await twd.visit("/complex-page"); // Add debug logging console.log("Starting complex workflow test"); const user = userEvent.setup(); // ... complex test steps console.log("Workflow completed"); }); // Skip other tests to focus itSkip("should handle simple workflow", async () => { // Skip during debugging }); }); ``` ## Error Handling ### Test Function Errors ```ts describe("Error Handling", () => { it("should handle test errors gracefully", async () => { try { const element = await twd.get(".non-existent"); element.should("be.visible"); } catch (error) { // Test will fail with descriptive error console.error("Element not found:", error.message); throw error; // Re-throw to fail the test } }); }); ``` ## Next Steps - Learn about [TWD Commands](/api/twd-commands) for element interaction - Explore [Assertions](/api/assertions) for testing element states # TWD Commands Source: https://twd.dev/api/twd-commands Core commands for element selection, navigation, and API mocking in TWD tests. ## Element Selection ### twd.get(selector) Selects a single DOM element using a CSS selector. #### Syntax ```ts twd.get(selector: string): Promise ``` #### Parameters - **selector** (`string`) - CSS selector to find the element #### Returns `Promise` - Element API with assertion methods #### Examples ```ts // Basic selectors const button = await twd.get("button"); const emailInput = await twd.get("#email"); const errorMessage = await twd.get(".error-message"); // Complex selectors const submitButton = await twd.get("form button[type='submit']"); const firstItem = await twd.get("ul > li:first-child"); const dataAttribute = await twd.get("[data-testid='user-card']"); // Use the returned element button.should("be.visible"); emailInput.should("have.value", "test@example.com"); ``` --- ### twd.getAll(selector) Selects multiple DOM elements using a CSS selector. #### Syntax ```ts twd.getAll(selector: string): Promise ``` #### Parameters - **selector** (`string`) - CSS selector to find elements #### Returns `Promise` - Array of element APIs #### Examples ```ts // Get all matching elements const buttons = await twd.getAll("button"); const listItems = await twd.getAll("li"); const productCards = await twd.getAll(".product-card"); // Access specific elements buttons[0].should("be.visible"); buttons[buttons.length - 1].should("be.enabled"); // Check array length expect(productCards).to.have.length(10); // Iterate through elements for (let i = 0; i < listItems.length; i++) { listItems[i].should("be.visible"); } ``` --- ## Navigation ### twd.visit(url, reload?) Navigates to a specific URL in your single-page application. #### Syntax ```ts twd.visit(url: string, reload?: boolean): Promise ``` #### Parameters - **url** (`string`) - The URL path to navigate to - **reload** (`boolean`, optional) - If `true`, forces a reload even if already on the target URL. Defaults to `false`. #### Returns `Promise` - Resolves when navigation is complete #### Examples ```ts // Basic navigation await twd.visit("/"); await twd.visit("/products"); await twd.visit("/user/profile"); // With query parameters await twd.visit("/search?q=laptop&category=electronics"); // With hash fragments await twd.visit("/docs#getting-started"); // Force reload even if already on the page await twd.visit("/dashboard", true); ``` ::: warning Application state persists between visits `twd.visit()` uses the History API to simulate SPA navigation **without a page reload**. This means in-memory application state (Zustand, Redux, Jotai, module-level variables, localStorage) is **not reset** between calls. You must manually reset application state in `beforeEach` to ensure test isolation. See [State Management & Test Isolation](/writing-tests#state-management-test-isolation) for patterns and examples. ::: #### Use Cases ```ts describe("Navigation Tests", () => { it("should navigate between pages", async () => { await twd.visit("/"); const homeHeading = await twd.get("h1"); homeHeading.should("contain.text", "Home"); await twd.visit("/about"); const aboutHeading = await twd.get("h1"); aboutHeading.should("contain.text", "About"); }); }); ``` --- ### twd.url() Returns the URL command API for making assertions about the current URL. #### Syntax ```ts twd.url(): URLCommandAPI ``` #### Returns `URLCommandAPI` - Object with URL assertion methods #### Methods - `should(assertion, value, retries?)` - Make URL assertions (async, retries with 100ms delay between attempts, default: 5 retries) #### Examples ```ts // Exact URL matching await twd.url().should("eq", "http://localhost:3000/products"); // URL contains substring await twd.url().should("contain.url", "/products"); await twd.url().should("contain.url", "localhost"); // Negated assertions await twd.url().should("not.contain.url", "/admin"); // After navigation twd.visit("/login"); await twd.url().should("contain.url", "/login"); const loginButton = await twd.get("button[type='submit']"); await userEvent.click(loginButton.el); await twd.url().should("contain.url", "/dashboard"); ``` --- ## Input Handling ### userEvent (Recommended) The primary and recommended way to interact with form inputs. `userEvent` simulates realistic user interactions and should be used for most input scenarios. #### Import and Setup ```ts import { userEvent } from 'twd-js'; // In your test const user = userEvent.setup(); ``` #### Text Inputs ```ts describe("Form Input Tests", () => { it("should handle text input realistically", async () => { await twd.visit("/contact"); const user = userEvent.setup(); const nameInput = await twd.get("input[name='name']"); const emailInput = await twd.get("input[name='email']"); const messageTextarea = await twd.get("textarea[name='message']"); // Type text naturally (with timing and events) await user.type(nameInput.el, "John Doe"); await user.type(emailInput.el, "john@example.com"); await user.type(messageTextarea.el, "Hello, this is my message!"); // Verify values nameInput.should("have.value", "John Doe"); emailInput.should("have.value", "john@example.com"); messageTextarea.should("have.value", "Hello, this is my message!"); }); }); ``` #### Form Interactions ```ts describe("Complete Form Workflow", () => { it("should handle full form interaction", async () => { await twd.visit("/registration"); const user = userEvent.setup(); // Fill text inputs const usernameInput = await twd.get("input[name='username']"); const passwordInput = await twd.get("input[name='password']"); await user.type(usernameInput.el, "johndoe"); await user.type(passwordInput.el, "securepassword123"); // Handle checkboxes const termsCheckbox = await twd.get("input[name='terms']"); await user.click(termsCheckbox.el); termsCheckbox.should("be.checked"); // Handle select dropdowns const countrySelect = await twd.get("select[name='country']"); await user.selectOptions(countrySelect.el, "US"); countrySelect.should("have.value", "US"); // Submit form const submitButton = await twd.get("button[type='submit']"); await user.click(submitButton.el); // Verify submission const successMessage = await twd.get(".success-message"); successMessage.should("contain.text", "Registration successful"); }); }); ``` #### Advanced User Interactions ```ts describe("Advanced Input Interactions", () => { it("should handle complex user behaviors", async () => { const user = userEvent.setup(); const searchInput = await twd.get("input[name='search']"); // Type and then clear await user.type(searchInput.el, "initial search"); await user.clear(searchInput.el); searchInput.should("have.value", ""); // Type with special keys await user.type(searchInput.el, "Hello{backspace}{backspace}lo World"); searchInput.should("have.value", "Hello World"); // Tab navigation await user.tab(); const nextInput = await twd.get("input[name='email']"); nextInput.should("be.focused"); }); }); ``` #### Why Use userEvent? - **Realistic interactions** - Simulates actual user behavior - **Proper event firing** - Triggers all necessary DOM events - **Timing simulation** - Includes natural typing delays - **Focus management** - Handles focus/blur correctly - **Accessibility testing** - Works with screen readers and keyboard navigation - **Framework compatibility** - Works with React, Vue, and other frameworks --- ### twd.setInputValue(element, value) - Special Cases Only ⚠️ **Use sparingly** - Only for specific input types where `userEvent` doesn't work well. Sets the value of an input element directly and dispatches an input event. **Only recommended for range, color, time, and date inputs** where user event simulation is complex or unreliable. #### Syntax ```ts twd.setInputValue(element: Element, value: string): void ``` #### When to Use setInputValue Use `setInputValue` **only** for these specific input types: ```ts describe("Special Input Types", () => { it("should handle inputs that userEvent struggles with", async () => { await twd.visit("/settings"); // ✅ Range inputs - dragging simulation is complex const volumeSlider = await twd.get("input[type='range']"); twd.setInputValue(volumeSlider.el, "75"); volumeSlider.should("have.value", "75"); // ✅ Color inputs - color picker doesn't respond to typing const colorPicker = await twd.get("input[type='color']"); twd.setInputValue(colorPicker.el, "#ff0000"); colorPicker.should("have.value", "#ff0000"); // ✅ Time inputs - complex time format requirements const timeInput = await twd.get("input[type='time']"); twd.setInputValue(timeInput.el, "13:30"); timeInput.should("have.value", "13:30"); // ✅ Date inputs - date picker complexity const dateInput = await twd.get("input[type='date']"); twd.setInputValue(dateInput.el, "2024-12-25"); dateInput.should("have.value", "2024-12-25"); }); }); ``` #### ❌ Don't Use setInputValue For ```ts // ❌ BAD - Use userEvent instead for text inputs const nameInput = await twd.get("input[type='text']"); twd.setInputValue(nameInput.el, "John Doe"); // Don't do this // ✅ GOOD - Use userEvent for realistic interaction const user = userEvent.setup(); await user.type(nameInput.el, "John Doe"); // Do this instead // ❌ BAD - Use userEvent for checkboxes const checkbox = await twd.get("input[type='checkbox']"); twd.setInputValue(checkbox.el, "true"); // Don't do this // ✅ GOOD - Use userEvent for checkboxes await user.click(checkbox.el); // Do this instead ``` #### Best Practice Pattern ```ts describe("Mixed Input Form", () => { it("should use appropriate method for each input type", async () => { const user = userEvent.setup(); // Use userEvent for standard inputs (RECOMMENDED) const nameInput = await twd.get("input[name='name']"); const emailInput = await twd.get("input[name='email']"); await user.type(nameInput.el, "John Doe"); await user.type(emailInput.el, "john@example.com"); // Use setInputValue ONLY for special input types const birthDate = await twd.get("input[type='date']"); const favoriteColor = await twd.get("input[type='color']"); const volume = await twd.get("input[type='range']"); twd.setInputValue(birthDate.el, "1990-05-15"); twd.setInputValue(favoriteColor.el, "#3366cc"); twd.setInputValue(volume.el, "80"); // Back to userEvent for form submission const submitButton = await twd.get("button[type='submit']"); await user.click(submitButton.el); }); }); ``` --- ## Utility Functions ### twd.wait(ms) Waits for a specified amount of time. #### Syntax ```ts twd.wait(time: number): Promise ``` #### Parameters - **time** (`number`) - Time in milliseconds to wait #### Returns `Promise` - Resolves after the specified time #### Examples ```ts // Wait for animations await twd.wait(500); // Wait for API calls const loadButton = await twd.get("button[data-action='load']"); await userEvent.click(loadButton.el); await twd.wait(2000); // Wait for loading // Wait for state changes const modal = await twd.get(".modal"); modal.should("not.be.visible"); const showModalButton = await twd.get("button[data-action='show-modal']"); await userEvent.click(showModalButton.el); await twd.wait(300); // Wait for modal animation modal.should("be.visible"); ``` #### Best Practices ```ts // ✅ Use twd.wait for intentional fixed delays await twd.wait(300); // Wait for CSS exit animation to finish // ❌ Avoid - Use twd.waitFor() instead for condition-based waiting await twd.wait(1000); // Hoping the spinner is gone by now ``` ::: tip For condition-based waiting (waiting for an element to change, an event to fire, etc.), use [`twd.waitFor()`](#twd-waitfor-callback-options) instead. ::: --- ### twd.waitFor(callback, options?) Retries a callback until it stops throwing or the timeout expires. Use this instead of `twd.wait(ms)` to avoid blind delays — `waitFor` resolves as soon as your condition is met, making tests faster and more reliable. If the callback returns a value, `waitFor` resolves with that value — useful for extracting elements or data without nesting all assertions inside the callback. #### Syntax ```ts twd.waitFor( callback: () => T | Promise, options?: { timeout?: number; // Default: 2000 interval?: number; // Default: 50 message?: string; } ): Promise ``` #### Parameters - **callback** (`() => T | Promise`) - Function to retry. Should throw if the condition is not yet met. Can be sync or async. If it returns a value, `waitFor` resolves with that value. - **options** (`object`, optional): - **timeout** (`number`) - Max time to wait in milliseconds. Default: `2000` - **interval** (`number`) - Poll interval in milliseconds. Default: `50` - **message** (`string`) - Context message included in timeout errors for easier debugging #### Returns `Promise` - Resolves with the callback's return value when it succeeds. If the callback returns `void`, resolves as `Promise`. Rejects with a timeout error if the callback keeps throwing past the timeout. #### Error Format ``` // Without message: waitFor timed out after 2000ms. Last error: expected undefined to exist // With message: waitFor timed out after 2000ms waiting for: purchase event to fire. Last error: expected undefined to exist ``` #### Examples ```ts // Return an element — single expression const heading = await twd.waitFor(() => screenDom.getByRole("heading", { name: /checkout/i })); twd.should(heading, "be.visible"); // Return a value with assertions inside const event = await twd.waitFor(() => { const ev = findEvent("purchase"); expect(ev).to.exist; return ev; }, { message: "purchase event to fire" }); expect(event.customer_type).to.equal("b2c"); // Fire-and-forget (void) — works exactly as before await twd.waitFor(() => { expect(heading).to.have.attribute("data-loaded", "true"); }); // Custom timeout for slow operations await twd.waitFor(() => { const dropin = document.querySelector(".adyen-checkout__dropin"); if (!dropin) throw new Error("Adyen dropin not rendered"); }, { timeout: 5000 }); // UI state change after action const submitButton = screenDom.getByRole("button", { name: /submit/i }); await userEvent.click(submitButton); await twd.waitFor(() => { expect(submitButton.disabled).to.be.false; }, { message: "submit button to re-enable" }); ``` #### `waitFor` vs `twd.wait` | | `twd.waitFor(fn)` | `twd.wait(ms)` | |---|---|---| | **Resolves when** | Callback stops throwing | Fixed time elapses | | **Speed** | As fast as the condition is met | Always waits the full duration | | **Reliability** | Adapts to timing variations | Fails if operation is slower than the wait | | **Returns** | The callback's return value | `void` | | **Use for** | Any async condition (DOM, events, state) | Intentional delays (animations, debounce testing) | ::: warning Only use waitFor when retry logic is necessary `waitFor` is a polling utility — it retries the callback repeatedly until it passes. Don't use it as a general-purpose wrapper. If you already have the element or value and just need to assert on it, assert directly. ```ts // DON'T — no retry needed, getByRole throws synchronously if not found const heading = await twd.waitFor(() => screenDom.getByRole("heading")); // DO — use waitFor when the element might not exist yet (async rendering, delayed state) const heading = await twd.waitFor(() => screenDom.getByRole("heading", { name: /loaded/i })); ``` Use `waitFor` for: analytics events that fire asynchronously, DOM attributes that update after an async operation, elements that appear after a delay. ::: ::: tip Prefer waitFor over twd.wait Most uses of `twd.wait(ms)` can be replaced with `twd.waitFor()`. The callback should be a **pure check** — don't perform actions inside it, only assertions or reads. ::: --- ### twd.notExists(selector) Asserts that an element matching the provided selector does not exist in the DOM. Resolves when no matching element is found and rejects if the element exists. #### Syntax ```ts twd.notExists(selector: string): Promise ``` #### Parameters - **selector** (`string`) - CSS selector of the element to check #### Returns `Promise` - Resolves if the element is not present, rejects with an Error if it is found #### Examples ```ts // Assert that a specific element is not present await twd.notExists('.non-existent'); // Will reject when an element exists const el = document.createElement('div'); el.className = 'maybe'; document.body.appendChild(el); await expect(twd.notExists('.maybe')).rejects.toThrow(); ``` ### twd.should(element, assertion, ...args) Makes assertions on any DOM element. This is the standalone function version of assertions, useful for elements from Testing Library queries (`screenDom`) or any raw DOM element. #### Syntax ```ts twd.should(element: Element, assertion: string, ...args: any[]): void ``` #### Parameters - **element** (`Element`) - The DOM element to assert on - **assertion** (`string`) - The assertion name (e.g., `"be.visible"`, `"have.text"`) - **args** (`any[]`) - Additional arguments for the assertion (e.g., expected text value) #### Returns `void` - This function doesn't return a value (unlike the `.should()` method on TWD elements) #### When to Use - **Use `twd.should()`** for elements from Testing Library queries (`screenDom`) - **Use `element.should()`** for elements returned from `twd.get()` or `twd.getAll()` #### Examples ```ts import { twd, screenDom } from "twd-js"; // With Testing Library queries const button = screenDom.getByRole("button", { name: /submit/i }); twd.should(button, "be.visible"); twd.should(button, "have.text", "Submit"); // With raw DOM elements const element = document.querySelector(".my-element"); twd.should(element, "be.visible"); twd.should(element, "contain.text", "Hello"); // With TWD elements (alternative to .should() method) const input = await twd.get("input#email"); twd.should(input.el, "have.value", "user@example.com"); // Or use the method: input.should("have.value", "user@example.com"); // All assertion types are supported const heading = screenDom.getByRole("heading"); twd.should(heading, "have.text", "Welcome"); twd.should(heading, "be.visible"); twd.should(heading, "have.class", "main-title"); const checkbox = screenDom.getByRole("checkbox"); twd.should(checkbox, "be.checked"); const select = screenDom.getByRole("combobox"); const option = screenDom.getByRole("option", { selected: true }); twd.should(option, "be.selected"); ``` #### Comparison: Method vs Function ```ts // Method style (for twd.get() elements) const button = await twd.get("button"); button.should("be.visible").should("have.text", "Click me"); // Function style (for any element, especially screenDom) const button = screenDom.getByRole("button"); twd.should(button, "be.visible"); twd.should(button, "have.text", "Click me"); ``` #### Available Assertions All assertions available on `.should()` method are also available with `twd.should()`: - `"have.text"` - Exact text match - `"contain.text"` - Partial text match - `"be.empty"` - Element has no text - `"have.attr"` - Has attribute with value - `"have.value"` - Input/textarea value - `"have.class"` - Has CSS class - `"be.disabled"` / `"be.enabled"` - Form element state - `"be.checked"` - Checkbox/radio state - `"be.selected"` - Option element state - `"be.focused"` - Element has focus - `"be.visible"` - Element is visible All assertions can be negated with `"not."` prefix (e.g., `"not.be.visible"`). --- ## Layout Snapshots ### twd.matchLayout(el, name) (beta) Captures the geometry of an element and compares it against a committed `.snap` reference, throwing when the layout moved. See [Layout Snapshots](/layout-snapshots) for the full guide. **Note**: The verdict comes from `twd-cli`, not the sidebar. In the browser sidebar, layout snapshots are skipped. #### Syntax ```ts twd.matchLayout(el: HTMLElement, name: string): Promise ``` #### Parameters - **el** (`HTMLElement`) - The element to capture. - **name** (`string`) - Snapshot name, used as the file name under `__twd_snapshots__`. #### Returns `Promise` - Resolves when the layout matches the reference (or when a new reference is written). Throws when the layout moved. #### Examples ```ts const landing = await screenDom.findByTestId('landing'); await twd.matchLayout(landing, 'landing'); ``` --- ## API Mocking ### twd.mockRequest(alias, options) Mocks an HTTP request with specified response. #### Syntax ```ts await twd.mockRequest(alias: string, options: Options): void ``` #### Parameters - **alias** (`string`) - Unique identifier for the mock - **options** (`Options`) - Mock configuration #### Options Interface ```ts interface Options { method: string; // HTTP method (GET, POST, etc.) url: string | RegExp; // URL to match response: unknown; // Response body status?: number; // HTTP status code (default: 200) headers?: Record; // Response headers } ``` #### URL Matching Behavior When `url` is a **string**, TWD uses boundary-aware matching: the mock triggers when the request URL contains the mock URL followed by `?`, `#`, `&`, or end of string. This prevents `/api/orders` from accidentally matching `/api/orders-summary` and `/api/travelers/123` from matching `/api/travelers/123/billing-details`. Boundary checking only applies to the path portion — query string matching uses substring, so a rule like `/search?q=` will match `/search?q=anything`. When `url` is a **RegExp**, standard regex matching applies. #### Examples ```ts // Basic GET request await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { id: 123, name: "John Doe", email: "john@example.com" } }); // POST request with custom status await twd.mockRequest("createUser", { method: "POST", url: "/api/users", response: { id: 456, created: true }, status: 201, headers: { "Content-Type": "application/json", "Location": "/api/users/456" } }); // Using RegExp for dynamic URLs await twd.mockRequest("getUserById", { method: "GET", url: /\/api\/users\/\d+/, response: { id: 123, name: "Dynamic User" } }); // Error response await twd.mockRequest("serverError", { method: "GET", url: "/api/data", response: { error: "Internal server error" }, status: 500 }); ``` --- ### twd.waitForRequest(alias) Waits for a mocked request to be made. #### Syntax ```ts twd.waitForRequest(alias: string, retries?: number, retryDelay?: number): Promise ``` #### Parameters - **alias** (`string`) - The alias of the mock to wait for - **retries** (`number`, optional) - Maximum number of retry attempts (default: `10`) - **retryDelay** (`number`, optional) - Delay between retries in milliseconds (default: `100`) #### Returns `Promise` - The matched rule. Note: `rule.request` contains the **parsed request body directly** (e.g., `rule.request.email`), not a request object with a `.body` property. #### Examples ```ts // Wait for single request await twd.mockRequest("getProfile", { method: "GET", url: "/api/profile", response: { name: "John Doe" } }); const loadButton = await twd.get("button[data-action='load-profile']"); await userEvent.click(loadButton.el); const rule = await twd.waitForRequest("getProfile"); console.log("Request completed:", rule); // Verify request body for POST requests await twd.mockRequest("submitForm", { method: "POST", url: "/api/contact", response: { success: true } }); const submitButton = await twd.get("button[type='submit']"); await userEvent.click(submitButton.el); const submitRule = await twd.waitForRequest("submitForm"); // rule.request IS the parsed body directly (not rule.request.body) expect(submitRule.request).to.deep.equal({ name: "John Doe", email: "john@example.com" }); ``` --- ### twd.waitForRequests(aliases) Waits for multiple mocked requests to be made. #### Syntax ```ts twd.waitForRequests(aliases: string[]): Promise ``` #### Parameters - **aliases** (`string[]`) - Array of mock aliases to wait for #### Returns `Promise` - Array of matched rules #### Examples ```ts // Wait for multiple requests await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1, name: "John" } }); await twd.mockRequest("getUserPosts", { method: "GET", url: "/api/user/posts", response: [{ id: 1, title: "First Post" }] }); const loadButton = await twd.get("button[data-action='load-all']"); await userEvent.click(loadButton.el); const rules = await twd.waitForRequests(["getUser", "getUserPosts"]); expect(rules).to.have.length(2); // Process each rule rules.forEach((rule, index) => { console.log(`Request ${index + 1}:`, rule.alias); }); ``` --- ### twd.initRequestMocking() Initializes the mock service worker for request interception. #### Syntax ```ts twd.initRequestMocking(path?: string): Promise ``` #### Parameters - **path** (`string`, optional) - Service worker absolute path #### Returns `Promise` - Resolves when mocking is initialized #### Examples ```ts // In test loader (src/loadTests.ts) import { twd } from "twd-js"; twd.initRequestMocking() .then(() => { console.log("Request mocking initialized"); }) .catch((err) => { console.error("Error initializing request mocking:", err); }); // init with custom service worker path twd.initRequestMocking('/test-path/mock-sw.js') .then(() => { console.log("Request mocking initialized with custom path"); }) .catch((err) => { console.error("Error initializing request mocking with custom path:", err); }); ``` --- ### twd.clearRequestMockRules() Clears all active mock rules. ::: tip This can also be triggered from the sidebar via the **Clear mocks** button, which clears both request mock rules and component mocks. ::: #### Syntax ```ts twd.clearRequestMockRules(): void ``` #### Examples ```ts describe("API Integration", () => { beforeEach(() => { // Clear mocks before each test twd.clearRequestMockRules(); }); it("should handle user creation", async () => { await twd.mockRequest("createUser", { method: "POST", url: "/api/users", response: { id: 1, created: true } }); // Test implementation... }); // Mocks are automatically cleared before next test }); // Or clear manually when needed describe("Complex API Test", () => { it("should handle multiple scenarios", async () => { // First scenario await twd.mockRequest("scenario1", { /* ... */ }); // Test scenario 1... // Clear and set up second scenario twd.clearRequestMockRules(); await twd.mockRequest("scenario2", { /* ... */ }); // Test scenario 2... }); }); ``` --- ### twd.getRequestMockRules() Gets all currently active mock rules. #### Syntax ```ts twd.getRequestMockRules(): Rule[] ``` #### Returns `Rule[]` - Array of active mock rules #### Examples ```ts // Debug active mocks await twd.mockRequest("getUser", { method: "GET", url: "/api/user", response: { id: 1 } }); await twd.mockRequest("getPosts", { method: "GET", url: "/api/posts", response: [] }); const activeMocks = twd.getRequestMockRules(); console.log(`Active mocks: ${activeMocks.length}`); activeMocks.forEach(mock => { console.log(`Mock: ${mock.alias} - ${mock.method} ${mock.url}`); }); // Verify specific mock exists const userMock = activeMocks.find(mock => mock.alias === "getUser"); expect(userMock).to.exist; expect(userMock?.method).to.equal("GET"); ``` --- ## Component Mocking ### twd.mockComponent(name, component) Mocks a React component with a custom implementation. #### Syntax ```ts twd.mockComponent(name: string, component: React.ComponentType): void ``` #### Parameters - **name** (`string`) - Unique identifier matching the `name` prop in `MockedComponent` - **component** (`React.ComponentType`) - The mock component implementation #### Examples ```ts interface ButtonProps { onClick: (count: number) => void; count: number; } const Button = ({ onClick, count }: ButtonProps) => { return ; }; // Mock the component to change its behavior twd.mockComponent("Button", ({ onClick, count }: ButtonProps) => ( ); // Test implementation... }); it("should test without mock", async () => { // This test runs with clean state - no mocks active // Test implementation... }); }); ``` ```ts // Clear mocks manually when needed describe("Component Behavior", () => { it("should handle changing mock behavior", async () => { // First mock twd.mockComponent("StatusIndicator", () => (
    Loading...
    )); // Test with first mock... // Clear and set up second mock twd.clearComponentMocks(); twd.mockComponent("StatusIndicator", () => (
    Success!
    )); // Test with second mock... }); }); ``` --- ## Best Practices ### 1. Use Descriptive Selectors ```ts // ✅ Good - Semantic and stable const submitButton = await twd.get("button[type='submit']"); const userCard = await twd.get("[data-testid='user-card']"); // ❌ Avoid - Fragile and non-semantic const button = await twd.get("div > div:nth-child(3) button"); ``` ### 2. Clean Up Mocks ```ts // Clean up API mocks describe("API Tests", () => { beforeEach(() => { twd.clearRequestMockRules(); }); it("should handle requests", async () => { // Clean state guaranteed }); }); // Clean up component mocks describe("Component Tests", () => { beforeEach(() => { twd.clearComponentMocks(); }); it("should test components", async () => { // Clean state guaranteed }); }); ``` ### 3. Wait Appropriately ```ts // ✅ Best - Use waitFor for condition-based waiting await userEvent.click(loadButton.el); await twd.waitFor(() => { const spinner = screenDom.queryByRole("progressbar"); expect(spinner).not.to.exist; }, { message: "loading to complete" }); // ✅ OK - Use twd.wait only for intentional fixed delays await twd.wait(300); // Wait for CSS transition to finish // ❌ Avoid - Blind waits for async conditions await twd.wait(2000); // Hoping the API responded by now ``` ### 4. Use Realistic Mock Data ```ts // ✅ Good - Realistic data structure await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { id: 123, name: "John Doe", email: "john.doe@example.com", avatar: "https://example.com/avatar.jpg", createdAt: "2024-01-15T10:30:00Z", preferences: { theme: "dark", notifications: true } } }); // ❌ Too minimal await twd.mockRequest("getUser", { method: "GET", url: "/api/user/123", response: { name: "test" } }); ``` ## Next Steps - Learn about [Assertions](/api/assertions) for testing element states - Check [Test Functions](/api/test-functions) for organizing tests # Assertions Source: https://twd.dev/api/assertions Complete reference for all TWD assertion methods to verify element states, content, and behavior. ## Text Content Assertions ### have.text Verifies that an element's text content matches exactly. #### Syntax ```ts element.should("have.text", expectedText: string): TWDElemAPI ``` #### Examples ```ts const heading = await twd.get("h1"); heading.should("have.text", "Welcome to TWD"); const button = await twd.get("button"); button.should("have.text", "Submit Form"); // Case sensitive const label = await twd.get("label"); label.should("have.text", "Email Address"); // Must match exactly ``` #### Negation ```ts const element = await twd.get(".message"); element.should("not.have.text", "Error occurred"); ``` --- ### contain.text Verifies that an element's text content contains a substring. #### Syntax ```ts element.should("contain.text", substring: string): TWDElemAPI ``` #### Examples ```ts const description = await twd.get(".product-description"); description.should("contain.text", "premium quality"); const errorMessage = await twd.get(".error"); errorMessage.should("contain.text", "required"); // Partial matching const title = await twd.get("h1"); title.should("contain.text", "Product"); // Matches "Product Details" ``` #### Negation ```ts const successMessage = await twd.get(".success"); successMessage.should("not.contain.text", "error"); ``` --- ### be.empty Verifies that an element has no text content. #### Syntax ```ts element.should("be.empty"): TWDElemAPI ``` #### Examples ```ts const emptyDiv = await twd.get(".placeholder"); emptyDiv.should("be.empty"); const clearedInput = await twd.get("#search"); clearedInput.should("be.empty"); // After clearing content const user = userEvent.setup(); const textArea = await twd.get("textarea"); await user.clear(textArea.el); textArea.should("be.empty"); ``` #### Negation ```ts const contentDiv = await twd.get(".content"); contentDiv.should("not.be.empty"); ``` --- ## Attribute Assertions ### have.attr Verifies that an element has a specific attribute with a specific value. #### Syntax ```ts element.should("have.attr", attributeName: string, expectedValue: string): TWDElemAPI ``` #### Examples ```ts // Form attributes const emailInput = await twd.get("#email"); emailInput.should("have.attr", "type", "email"); emailInput.should("have.attr", "required"); emailInput.should("have.attr", "placeholder", "Enter your email"); // Link attributes const externalLink = await twd.get("a[href^='https://']"); externalLink.should("have.attr", "target", "_blank"); externalLink.should("have.attr", "rel", "noopener noreferrer"); // Custom data attributes const userCard = await twd.get(".user-card"); userCard.should("have.attr", "data-user-id", "123"); userCard.should("have.attr", "data-role", "admin"); // Boolean attributes (just check existence) const requiredField = await twd.get("#required-field"); requiredField.should("have.attr", "required"); ``` #### Negation ```ts const optionalField = await twd.get("#optional-field"); optionalField.should("not.have.attr", "required"); const internalLink = await twd.get("a[href^='/']"); internalLink.should("not.have.attr", "target", "_blank"); ``` --- ### have.value Verifies the value of form input elements. #### Syntax ```ts element.should("have.value", expectedValue: string): TWDElemAPI ``` #### Examples ```ts // Input fields const nameInput = await twd.get("#name"); nameInput.should("have.value", "John Doe"); const emailInput = await twd.get("#email"); emailInput.should("have.value", "john@example.com"); // After user input const user = userEvent.setup(); const searchInput = await twd.get("#search"); await user.type(searchInput.el, "laptop"); searchInput.should("have.value", "laptop"); // Textarea const messageArea = await twd.get("textarea#message"); messageArea.should("have.value", "Hello world!"); // Empty values const clearedInput = await twd.get("#cleared"); clearedInput.should("have.value", ""); ``` #### Negation ```ts const passwordInput = await twd.get("#password"); passwordInput.should("not.have.value", ""); passwordInput.should("not.have.value", "password123"); ``` --- ### have.class Verifies that an element has a specific CSS class. #### Syntax ```ts element.should("have.class", className: string): TWDElemAPI ``` #### Examples ```ts // Button states const primaryButton = await twd.get(".btn"); primaryButton.should("have.class", "btn-primary"); primaryButton.should("have.class", "btn"); // Status indicators const alert = await twd.get(".alert"); alert.should("have.class", "alert-success"); // Dynamic classes const activeTab = await twd.get(".tab"); activeTab.should("have.class", "active"); // Multiple class checks const element = await twd.get(".complex-element"); element.should("have.class", "component"); element.should("have.class", "visible"); element.should("have.class", "interactive"); ``` #### Negation ```ts const button = await twd.get("button"); button.should("not.have.class", "disabled"); button.should("not.have.class", "hidden"); const inactiveTab = await twd.get(".tab:not(.active)"); inactiveTab.should("not.have.class", "active"); ``` --- ## Element State Assertions ### be.disabled Verifies that a form element is disabled. #### Syntax ```ts element.should("be.disabled"): TWDElemAPI ``` #### Examples ```ts // Initially disabled submit button const submitButton = await twd.get("button[type='submit']"); submitButton.should("be.disabled"); // Disabled input field const readOnlyInput = await twd.get("#readonly"); readOnlyInput.should("be.disabled"); // Conditionally disabled elements const conditionalButton = await twd.get("#conditional-btn"); conditionalButton.should("be.disabled"); // After form validation fails const user = userEvent.setup(); const form = await twd.get("form"); await user.click(await twd.get("button[type='submit']")); submitButton.should("be.disabled"); ``` #### Negation ```ts const enabledButton = await twd.get("button"); enabledButton.should("not.be.disabled"); ``` --- ### be.enabled Verifies that a form element is enabled (not disabled). #### Syntax ```ts element.should("be.enabled"): TWDElemAPI ``` #### Examples ```ts // Active form elements const nameInput = await twd.get("#name"); nameInput.should("be.enabled"); const submitButton = await twd.get("button[type='submit']"); submitButton.should("be.enabled"); // After enabling conditionally const user = userEvent.setup(); await user.type(await twd.get("#email"), "test@example.com"); const conditionalButton = await twd.get("#conditional-btn"); conditionalButton.should("be.enabled"); ``` #### Negation ```ts const disabledField = await twd.get("#disabled-field"); disabledField.should("not.be.enabled"); ``` --- ### be.checked Verifies that a checkbox or radio button is checked. #### Syntax ```ts element.should("be.checked"): TWDElemAPI ``` #### Examples ```ts // Checked checkbox const agreeCheckbox = await twd.get("#agree-terms"); agreeCheckbox.should("be.checked"); // Selected radio button const premiumRadio = await twd.get("input[value='premium']"); premiumRadio.should("be.checked"); // After user interaction const user = userEvent.setup(); const newsletterCheckbox = await twd.get("#newsletter"); await user.click(newsletterCheckbox.el); newsletterCheckbox.should("be.checked"); // Default selections const defaultRadio = await twd.get("input[name='plan'][checked]"); defaultRadio.should("be.checked"); ``` #### Negation ```ts const uncheckedBox = await twd.get("#optional-feature"); uncheckedBox.should("not.be.checked"); const unselectedRadio = await twd.get("input[value='basic']"); unselectedRadio.should("not.be.checked"); ``` --- ### be.selected Verifies that an option element is selected. #### Syntax ```ts element.should("be.selected"): TWDElemAPI ``` #### Examples ```ts // Default selected option const defaultOption = await twd.get("select#country option[value='US']"); defaultOption.should("be.selected"); // After user selection const user = userEvent.setup(); const countrySelect = await twd.get("select#country"); await user.selectOptions(countrySelect.el, "CA"); const canadaOption = await twd.get("select#country option[value='CA']"); canadaOption.should("be.selected"); // Multiple select const multiSelect = await twd.get("select[multiple]"); await user.selectOptions(multiSelect.el, ["option1", "option3"]); const option1 = await twd.get("select[multiple] option[value='option1']"); const option3 = await twd.get("select[multiple] option[value='option3']"); option1.should("be.selected"); option3.should("be.selected"); ``` #### Negation ```ts const unselectedOption = await twd.get("select option[value='other']"); unselectedOption.should("not.be.selected"); ``` --- ### be.focused Verifies that an element currently has focus. #### Syntax ```ts element.should("be.focused"): TWDElemAPI ``` #### Examples ```ts // Initial focus const firstInput = await twd.get("#first-input"); firstInput.should("be.focused"); // After tab navigation const user = userEvent.setup(); await user.keyboard("{Tab}"); const secondInput = await twd.get("#second-input"); secondInput.should("be.focused"); // After clicking await user.click(await twd.get("#clickable-input")); const clickedInput = await twd.get("#clickable-input"); clickedInput.should("be.focused"); // Focus management in modals const modal = await twd.get(".modal"); const modalInput = await twd.get(".modal input"); modalInput.should("be.focused"); ``` #### Negation ```ts const unfocusedInput = await twd.get("#other-input"); unfocusedInput.should("not.be.focused"); ``` --- ### be.visible Verifies that an element is visible on the page. #### Syntax ```ts element.should("be.visible"): TWDElemAPI ``` #### Examples ```ts // Always visible elements const header = await twd.get("header"); header.should("be.visible"); const mainContent = await twd.get("main"); mainContent.should("be.visible"); // Conditionally visible elements const successMessage = await twd.get(".success-message"); successMessage.should("be.visible"); // After showing modal const user = userEvent.setup(); const showModalButton = await twd.get("button[data-action='show-modal']"); await user.click(showModalButton.el); const modal = await twd.get(".modal"); modal.should("be.visible"); // Dynamic content const loadedContent = await twd.get(".dynamic-content"); loadedContent.should("be.visible"); ``` #### Negation ```ts // Hidden elements const hiddenDiv = await twd.get(".hidden"); hiddenDiv.should("not.be.visible"); // Closed modal const closedModal = await twd.get(".modal"); closedModal.should("not.be.visible"); // Loading spinner after load const spinner = await twd.get(".loading-spinner"); await twd.wait(2000); spinner.should("not.be.visible"); ``` --- ### be.hidden Verifies that an element is hidden on the page. #### Syntax ```ts element.should("be.hidden"): TWDElemAPI ``` #### Examples ```ts // Hidden elements const hiddenDiv = await twd.get(".hidden"); hiddenDiv.should("be.hidden"); // After hiding modal const user = userEvent.setup(); const showModalButton = await twd.get("button[data-action='show-modal']"); await user.click(showModalButton.el); const modal = await twd.get(".modal"); modal.should("be.visible"); const closeModalButton = await twd.get("button[data-action='close-modal']"); await user.click(closeModalButton.el); modal.should("be.hidden"); ``` #### Negation ```ts // Visible elements const visibleDiv = await twd.get(".visible"); visibleDiv.should("not.be.hidden"); ``` --- ## URL Assertions ### eq (URL) Verifies that the current URL matches exactly. #### Syntax ```ts await twd.url().should("eq", expectedUrl: string, retries?: number): Promise ``` #### Examples ```ts // Exact URL matching twd.visit("/"); await twd.url().should("eq", "http://localhost:3000/"); twd.visit("/products"); await twd.url().should("eq", "http://localhost:3000/products"); // With query parameters twd.visit("/search?q=laptop&sort=price"); await twd .url() .should("eq", "http://localhost:3000/search?q=laptop&sort=price"); // After navigation const user = userEvent.setup(); const loginLink = await twd.get("a[href='/login']"); await user.click(loginLink.el); await twd.url().should("eq", "http://localhost:3000/login"); ``` #### Negation ```ts await twd.url().should("not.eq", "http://localhost:3000/wrong-page"); ``` --- ### contain.url Verifies that the current URL contains a substring. #### Syntax ```ts await twd.url().should("contain.url", substring: string, retries?: number): Promise ``` #### Examples ```ts // Path matching twd.visit("/products/category/electronics"); await twd.url().should("contain.url", "/products"); await twd.url().should("contain.url", "electronics"); await twd.url().should("contain.url", "category"); // Domain matching await twd.url().should("contain.url", "localhost"); await twd.url().should("contain.url", "3000"); // Query parameters twd.visit("/search?q=laptop"); await twd.url().should("contain.url", "q=laptop"); await twd.url().should("contain.url", "search"); // After navigation const categoryLink = await twd.get("a[href*='clothing']"); await userEvent.click(categoryLink.el); await twd.url().should("contain.url", "clothing"); ``` #### Negation ```ts await twd.url().should("not.contain.url", "/admin"); await twd.url().should("not.contain.url", "error"); ``` --- ## Assertion Chaining All assertions return the element API, allowing you to chain multiple assertions: ```ts const element = await twd.get("button"); // Chain multiple assertions element .should("be.visible") .should("be.enabled") .should("have.class", "btn-primary") .should("contain.text", "Submit") .should("not.be.disabled"); // Complex form validation const emailInput = await twd.get("#email"); emailInput .should("have.attr", "type", "email") .should("have.attr", "required") .should("be.enabled") .should("be.visible") .should("not.have.value", ""); ``` ## Practical Examples ### Form Validation ```ts describe("Form Validation", () => { it("should validate required fields", async () => { await twd.visit("/contact"); const user = userEvent.setup(); const submitButton = await twd.get("button[type='submit']"); // Submit empty form await user.click(submitButton.el); // Check validation errors const emailError = await twd.get(".error-email"); emailError .should("be.visible") .should("contain.text", "required") .should("have.class", "error-message"); const messageError = await twd.get(".error-message"); messageError.should("be.visible").should("not.be.empty"); // Fix one error const emailInput = await twd.get("#email"); await user.type(emailInput.el, "test@example.com"); emailInput.should("have.value", "test@example.com"); emailError.should("not.be.visible"); }); }); ``` ### Shopping Cart State ```ts describe("Shopping Cart", () => { it("should update cart state correctly", async () => { await twd.visit("/products"); // Initial empty state const cartCount = await twd.get(".cart-count"); cartCount.should("have.text", "0").should("be.visible"); const emptyMessage = await twd.get(".cart-empty"); emptyMessage.should("be.visible").should("contain.text", "empty"); // Add item to cart const addButton = await twd.get("button[data-product='123']"); await userEvent.click(addButton.el); // Verify cart updates cartCount.should("have.text", "1").should("not.have.text", "0"); emptyMessage.should("not.be.visible"); const cartItem = await twd.get(".cart-item"); cartItem .should("be.visible") .should("have.attr", "data-product", "123") .should("contain.text", "Product Name"); }); }); ``` ### Modal Dialog ```ts describe("Modal Dialog", () => { it("should handle modal state transitions", async () => { await twd.visit("/modal-example"); const modal = await twd.get(".modal"); const overlay = await twd.get(".modal-overlay"); const showButton = await twd.get("button[data-action='show-modal']"); // Initial hidden state modal.should("not.be.visible"); overlay.should("not.be.visible"); showButton.should("be.visible").should("be.enabled"); // Show modal await userEvent.click(showButton.el); modal.should("be.visible").should("have.class", "modal-open"); overlay.should("be.visible"); // Focus management const modalInput = await twd.get(".modal input"); modalInput.should("be.focused"); // Close modal const closeButton = await twd.get(".modal button[data-action='close']"); closeButton.should("be.visible").should("be.enabled"); await userEvent.click(closeButton.el); modal.should("not.be.visible"); overlay.should("not.be.visible"); showButton.should("be.focused"); // Focus returns }); }); ``` ## Best Practices ### 1. Use Appropriate Assertion Types ```ts // ✅ Good - Right assertion for the job element.should("contain.text", "Welcome"); // Partial match element.should("have.value", "exact@email.com"); // Exact value element.should("be.checked"); // Boolean state // ❌ Wrong assertion type element.should( "have.text", "Welcome to our amazing website with lots of features!", ); // Too specific element.should("contain.text", "exact@email.com"); // Should be exact match ``` ### 2. Chain Related Assertions ```ts // ✅ Good - Logical grouping const button = await twd.get("button"); button .should("be.visible") .should("be.enabled") .should("have.class", "btn-primary"); // ❌ Repetitive const button = await twd.get("button"); button.should("be.visible"); button.should("be.enabled"); button.should("have.class", "btn-primary"); ``` ### 3. Test User-Visible Behavior ```ts // ✅ Good - Tests what users see const errorMessage = await twd.get(".error-message"); errorMessage .should("be.visible") .should("contain.text", "Invalid email") .should("have.class", "error"); // ❌ Implementation details element.should("have.attr", "data-error-state", "true"); ``` ### 4. Use Negation Appropriately ```ts // ✅ Good - Clear intent const modal = await twd.get(".modal"); modal.should("not.be.visible"); const button = await twd.get("button"); button.should("not.be.disabled"); // ✅ Also good - Positive assertion when clearer button.should("be.enabled"); // Clearer than "not.be.disabled" ``` ## Troubleshooting ### Assertion Failures ```ts // Common issues and solutions // Issue: Text doesn't match exactly // ❌ Fails if text is "Welcome, John!" element.should("have.text", "Welcome"); // ✅ Use contain.text for partial matches element.should("contain.text", "Welcome"); // Issue: Timing problems // ❌ Element might not be ready const element = await twd.get(".async-content"); element.should("be.visible"); // ✅ Wait for content to load await twd.wait(1000); const element = await twd.get(".async-content"); element.should("be.visible"); ``` ### Element State Issues ```ts // Issue: Element appears but isn't interactive yet const button = await twd.get("button"); button.should("be.visible"); // ✅ Passes button.should("be.enabled"); // ❌ Might fail if still loading // Solution: Wait for interactive state await twd.wait(500); // Wait for initialization button.should("be.enabled"); // Or check loading states const spinner = await twd.get(".loading"); spinner.should("not.be.visible"); // Wait for loading to complete button.should("be.enabled"); ``` ## Next Steps - Explore [TWD Commands](/api/twd-commands) for element selection - Check [Test Functions](/api/test-functions) for organizing tests # Tutorial Source: https://twd.dev/tutorial/ Learn TWD step by step with practical examples and real-world scenarios. ## Tutorial Path Follow this recommended learning path to master TWD: | Step | Topic | Description | |------|-------|-------------| | 0 | [Introduction](./intro) | Welcome to TWD and what you'll learn | | 1 | [Installation](./installation) | Set up TWD in your project | | 2 | [First Test](./first-test) | Write and run your first test with selectors and assertions | | 3 | [API Mocking](./api-mocking) | Mock HTTP requests and responses | | 4 | [CI Integration](./ci-integration) | Run tests in continuous integration | | 5 | [Code Coverage](./coverage) | Collect and visualize code coverage | | 6 | [Production Builds](./production-builds) | Remove test code from production bundles | ## What You'll Learn By the end of this tutorial, you'll be able to: - Set up TWD in any React project - Write comprehensive UI tests with assertions and user interactions - Mock API calls for isolated testing - Run tests automatically in CI/CD pipelines - Collect and report code coverage - Build production-ready applications without test code ## Prerequisites - Basic knowledge of JavaScript/TypeScript - Familiarity with React - Understanding of web development concepts (DOM, HTTP requests) - Node.js and npm/yarn installed ## Getting Help If you get stuck during the tutorial: - 📖 Check the [API Reference](/api/) for detailed documentation - 📚 Review the [Writing Tests Guide](/writing-tests) for best practices - 🐛 [Report issues](https://github.com/BRIKEV/twd/issues) if you find bugs - 💬 [Join discussions](https://github.com/BRIKEV/twd/discussions) for questions Let's get started! [Introduction](./intro) # Welcome to TWD Source: https://twd.dev/tutorial/intro Welcome to the TWD (Testing While Developing) tutorial! This comprehensive guide will walk you through everything you need to know to start testing your applications with TWD. While the examples use React, TWD works with React, Vue, Angular, Solid.js, and other Vite-based frameworks. ## What is TWD? TWD is a browser validation system that makes testing a natural part of your development workflow. Unlike traditional testing tools that run in separate environments, TWD runs directly in your browser, allowing you to validate your application exactly as your users experience it. ## Why TWD? When building frontend applications, existing testing tools often don't fit the natural development workflow: - **Cypress** runs tests in a separate browser environment, breaking the natural development flow where developers want to stay in their browser with their extensions and dev tools. - **Playwright** is powerful but often too heavy for iterative local testing, feeling more like a QA stage than a development aid. - **Testing Library** runs in a virtual DOM, so it doesn't replicate real browser interactions or timing. TWD solves these problems by letting you: - Test interactivity like Cypress or Playwright — but directly in your own browser, with no extra browser instance. - Handle the DOM easily, with a feel similar to React Testing Library's user-event utilities. - Use a familiar syntax inspired by popular test runners (like Vitest, Jest, and Mocha). - Install and run tests effortlessly, without complex configuration. - Mock network requests with a simple, declarative API (similar to Cypress intercepts). - Run in CI mode and generate coverage reports automatically. - Produce structured, AI-readable output so agents can run tests, parse results, and iterate autonomously. ## What You'll Learn This tutorial series will guide you through: 1. **[Installation](./installation)** - Set up TWD in your project and see the sidebar appear 2. **[First Test](./first-test)** - Write your first test with selectors, assertions, and user interactions 3. **[API Mocking](./api-mocking)** - Mock network requests to test your frontend independently 4. **[CI Integration](./ci-integration)** - Run tests automatically in your CI/CD pipeline 5. **[Code Coverage](./coverage)** - Collect and visualize code coverage from your tests ## Prerequisites Before starting, make sure you have: - Basic knowledge of JavaScript/TypeScript - Understanding of web development concepts (DOM, HTTP requests) - Node.js and npm/yarn installed - A Vite-based project set up (React, Vue, Angular, Solid.js, etc. - we'll help you add TWD to it!) ## Getting Help If you get stuck during the tutorial: - 📖 Check the [API Reference](/api/) for detailed documentation - 📚 Review the [Writing Tests Guide](/writing-tests) for best practices - 🐛 [Report issues](https://github.com/BRIKEV/twd/issues) if you find bugs - 💬 [Join discussions](https://github.com/BRIKEV/twd/discussions) for questions ## Ready to Start? Let's begin by installing TWD in your project! [Installation →](./installation) # Installation and First Test Source: https://twd.dev/tutorial/installation Let's start our journey learning how to use TWD (Test While Developing)! In this first part, we'll work with a small finished project that includes two pages: - A Hello World page (perfect for our first test) - A Todo List page that makes requests to an API powered by JSON Server Our goal in this post is to install TWD and add the sidebar that will host all our tests. ## Setting Up the Project We'll start by setting up our base project. You can clone the repo here: ```bash git clone git@github.com:BRIKEV/twd-docs-tutorial.git cd twd-docs-tutorial git checkout 01-setup npm i ``` This project includes two routes: `/` and `/todos`. All components and pages are already in place — ready for us to test. To run the project locally: ``` npm run serve:dev ``` You should see this: ![tutorial homepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mzcpj8el0qqkc98bfp9.png) ## Getting Started with TWD Now let's install TWD: ```bash # You can use npm, yarn, or pnpm npm i --save-dev twd-js ``` Once installed, open `vite.config.ts` and add the `twd()` plugin alongside the existing plugins: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', open: true, position: 'left', }), ], }); ``` > The `twd()` plugin only runs in `vite dev` (`apply: 'serve'`) — it's a no-op in production builds, so nothing reaches your prod bundle. The `testFilePattern` option tells the plugin which files to discover as tests. The default pattern matches `*.twd.test.{ts,tsx}` files anywhere in your project. Once that's added, restart your dev server. You'll see the TWD Sidebar, where all your tests will appear: ![Tutorial homepage with twd sidebar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7xokpi55hwh5hrg58t0s.png) ## Creating Our First Test The plugin discovers any file matching the `testFilePattern` you configured. So let's create our very first test file. Create a new file at `src/twd-tests/helloWorld.twd.test.ts` and add this code: ```ts import { describe, it } from "twd-js/runner"; describe("Hello World Page", () => { it("should display the welcome title and counter button", async () => { console.log('Executed console.log'); }); }); ``` Now, your test will appear in the **TWD sidebar**. You can click the play icon next to it, or press Run All to execute all tests. You'll see the `console.log` output in your browser console. ![tutorial homepage with tests executed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0pch7m88wj5dtxwyhnxp.png) This test passes (green) because it doesn't actually test anything yet. In the next post, we'll explore assertions and selectors — the real power of TWD. --- ## Bonus: Visiting a Page Before we move on, let's add one small command: `twd.visit`. Update your test like this: ```ts import { twd } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Hello World Page", () => { it("should display the welcome title and counter button", async () => { await twd.visit('/'); }); }); ``` This command visits the page exactly like Cypress's cy.visit() — simple and familiar. --- In the [next tutorial](./first-test), we'll dive into assertions and selectors, where you'll start interacting with elements and verifying real behavior. Meanwhile, you can check out the [TWD documentation](/getting-started) for more details. # Selectors, Assertions, and User Events Source: https://twd.dev/tutorial/first-test In the [previous tutorial](./installation), we set up our basic testing configuration and used the `visit` command to load our app. Now, we're ready to make our tests a little more meaningful. In this part, we'll focus on three key features: - Selectors – how we find elements in our UI - Assertions – how we verify what's on screen - User events – how we simulate real interactions Let's dive in. ## Before You Start If you're following along from the installation tutorial, you can continue as is. But if you want to reset your repo or make sure you're on the correct branch: ``` # Repo git clone git@github.com:BRIKEV/twd-docs-tutorial.git git reset --hard git clean -d -f git checkout 02-assertions npm run serve:dev ``` --- ## Selectors TWD uses a simple and familiar approach for selectors. It provides two commands — `get` and `getAll` — which are based directly on the native DOM APIs `document.querySelector` and `document.querySelectorAll`. That means you can use any selector you'd normally use in the browser: class, id, tag, attribute, role, etc. We believe this keeps things flexible and intuitive, especially for developers already comfortable with the DOM. Let's improve our existing test file `src/twd-tests/helloWorld.twd.test.ts`. We'll test that our title displays the text "Welcome to TWD" and that our counter button updates as expected. We'll start by selecting the elements using twd.get: ```ts const title = await twd.get("[data-testid='welcome-title']"); ``` Here, we're using a `data-testid` attribute — just like in React Testing Library or Cypress. Because `get` is based on `querySelector`, you can use any CSS selector you prefer. For the counter button, we can do the same: ```ts const counterButton = await twd.get("[data-testid='counter-button']"); ``` > Note: Selectors are async because TWD automatically retries finding the element for up to two seconds. > So don't forget to use `await` before them. --- ## Assertions Once we've selected our elements, it's time to verify that our UI behaves as expected. Each selected element returned by `twd.get` includes two useful properties: - `el` – the raw DOM element - `should` – a utility for performing assertions Here's how we can assert visibility and text content: ```ts const title = await twd.get("[data-testid='welcome-title']"); title.should("be.visible").should("have.text", "Welcome to TWD"); const counterButton = await twd.get("[data-testid='counter-button']"); counterButton.should("be.visible").should("have.text", "Count is 0"); ``` The should command is chainable, so you can stack multiple conditions easily. TWD supports several built-in assertions such as: - be.visible - have.text - have.class - have.attribute - and even their `not` versions (e.g. `should("not.have.text", "Error")`) Once you run your tests, the sidebar will clearly show what was tested: ![Tests running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6cpsujkux94p9jzfw1a8.png) --- ## Interacting with the UI Now that we can find and verify elements, let's make things interactive. TWD integrates directly with [user event](https://github.com/testing-library/user-event) from React Testing Library — a well-known tool that handles realistic user interactions such as clicks, typing, and keyboard input. We chose `user-event` because it already supports most common browser interactions and mimics how a real user would use the app. TWD also provides a custom `setInputValue` helper for specific inputs that `user-event` doesn't handle perfectly. To use it, import both `twd` and `userEvent`: ```ts import { twd, userEvent } from "twd-js"; ``` Then, to simulate a click: ```ts const counterButton = await twd.get("[data-testid='counter-button']"); await userEvent.click(counterButton.el); ``` Notice we pass the element itself (`.el`) to `userEvent`, as that's what the library expects. --- ## Putting It All Together Now let's combine selectors, assertions, and events into a single test. Our `helloWorld.twd.test.ts` will look like this: ```ts import { twd, userEvent } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Hello World Page", () => { it("should display the welcome title and counter button", async () => { await twd.visit("/"); const title = await twd.get("[data-testid='welcome-title']"); title.should("be.visible").should("have.text", "Welcome to TWD"); const counterButton = await twd.get("[data-testid='counter-button']"); counterButton.should("be.visible").should("have.text", "Count is 0"); await userEvent.click(counterButton.el); counterButton.should("have.text", "Count is 1"); await userEvent.click(counterButton.el); counterButton.should("have.text", "Count is 2"); await userEvent.click(counterButton.el); counterButton.should("have.text", "Count is 3"); }); }); ``` And the test results will look like this: ![test running after three clicks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1e29i5r2cuyjsz2mz6l.png) As you can see, the UI updates in real time — and since it's your actual app, you can continue using it normally while your tests run. --- ## What's Next In the [next tutorial](./api-mocking), we'll explore one of the most powerful features of TWD: network mocking. This will let developers test their frontend independently, without relying on backend integrations — perfect for building and testing features faster. You can also learn more about selectors, assertions, and user events in our [official API documentation](/api/twd-commands). # API Mocking Tutorial Source: https://twd.dev/tutorial/api-mocking In the [previous tutorial](./first-test), we explored assertions, selectors, and user interactions in TWD. We used those tools to test the homepage, which only had a single button. Now, it's time to move to the `/todos` page — which brings more realistic functionality: - Display Todos - Create Todos - Remove Todos This page uses an API created with `json-server`. We could run our tests directly against the real API like before, but that would modify local data and require resetting the database between tests. That's not ideal — we want each test to be fully independent. That's where mocking comes in. In TWD, we recommend mocking your network requests so you can test your frontend **without backend dependencies**. This approach brings several advantages: - You can simulate any scenario: success, errors, or missing data. - You can validate the UX for those edge cases. - You can reproduce bugs easily by mocking the exact request that caused them. To achieve this, TWD provides utilities for mocking requests using Service Workers that intercept network traffic. Let's dive in. ## Before You Start If you're following along from the previous tutorial, you can continue as is. But if you want to reset your repo or make sure you're on the correct branch: ``` # Repo git clone git@github.com:BRIKEV/twd-docs-tutorial.git git reset --hard git clean -d -f git checkout 03-network-mocking npm run serve:dev ``` --- ## Installing the Service Worker We have a command to install the Service Worker that handles request interception: ``` npx twd-js init public --save ``` This command creates a `mock-sw.js` file in the folder you specify. Since we're using Vite, it will be stored inside the `public` folder. If you're using the `twd()` Vite plugin (recommended in this tutorial), you don't need to do anything else — request mocking is enabled by default. The plugin's `serviceWorker: true` option (also the default) registers the worker for you in dev. ```ts // vite.config.ts import { twd } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ twd({ // serviceWorker: true is the default }), ], }); ``` --- ## Displaying Todos Create a new file `src/twd-tests/todoList.twd.test.ts` and start with a basic test: ```ts import { twd } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Todo List Page", () => { it("should display the todo list", async () => { await twd.visit("/todos"); }); }); ``` This test simply visits the `/todos` page and loads the real API data: ![todo page without mock](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9skcvw7fut99pzo55dwa.png) Now let's mock the request so we can control the response. ```ts await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [], status: 200, }); ``` This method receives an alias and a configuration object — including the HTTP method, URL, mocked response, and status code. There are more options ([check our docs](/api-mocking) for all available fields). Now the complete test looks like this: ```ts import { twd } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Todo List Page", () => { it("should display the todo list", async () => { await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [], status: 200, }); await twd.visit("/todos"); }); }); ``` This will show an empty list — exactly what we defined: ![todo page with route mocked](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cln5fy10tc7b97uj6cfx.png) This is **extremely powerful** because it lets you simulate all possible frontend scenarios — from successful responses to errors — without relying on your backend. You can even capture real requests from bugs and replay them as tests. Now let's build a complete test with real data. --- ## Mocking with Real Data Create a mock file: `src/twd-tests/mocks/todoList.json` ```json [ { "id": "1", "title": "Learn TWD", "description": "Understand how to use TWD for testing web applications", "date": "2024-12-20" }, { "id": "2", "title": "Build Todo App", "description": "Create a todo list application to demonstrate TWD features", "date": "2024-12-25" } ] ``` And now the complete test: ```ts import { twd } from "twd-js"; import { describe, it } from "twd-js/runner"; import todoListMock from "./mocks/todoList.json"; describe("Todo List Page", () => { it("should display the todo list", async () => { await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock, status: 200, }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); const todo1Title = await twd.get("[data-testid='todo-title-1']"); todo1Title.should("have.text", "Learn TWD"); const todo2Title = await twd.get("[data-testid='todo-title-2']"); todo2Title.should("have.text", "Build Todo App"); const todo1Description = await twd.get("[data-testid='todo-description-1']"); todo1Description.should("have.text", "Understand how to use TWD for testing web applications"); const todo2Description = await twd.get("[data-testid='todo-description-2']"); todo2Description.should("have.text", "Create a todo list application to demonstrate TWD features"); const todo1Date = await twd.get("[data-testid='todo-date-1']"); todo1Date.should("have.text", "Date: 2024-12-20"); const todo2Date = await twd.get("[data-testid='todo-date-2']"); todo2Date.should("have.text", "Date: 2024-12-25"); }); }); ``` The new command here is: ```ts await twd.waitForRequest("getTodoList"); ``` It waits until the mocked request is triggered — useful when you need to ensure data is rendered before asserting UI state. > Always define `mockRequest` before triggering the request (clicks, submits, or navigation). > If you mock it too late, it might not intercept properly. ![homepage with list todos tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uc6wwevk534xxpcsrigi.png) --- ## Creating a Todo When testing the "create todo" feature, we need to verify: - The form is filled correctly - The correct data is sent - The list refreshes after submission - The new element appears in the list Let's define our request mocks first: ```ts // request of creating a todo await twd.mockRequest("createTodo", { method: "POST", url: "/api/todos", response: todoListMock[0], status: 200, }); // empty list on first load await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [], status: 200, }); ``` Then visit the page and confirm it's empty: ```ts await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); const noTodosMessage = await twd.get("[data-testid='no-todos-message']"); noTodosMessage.should("be.visible"); ``` Next, update the mock to include a new todo after creation: ```ts await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [todoListMock[0]], status: 200, }); ``` Fill and submit the form: ```ts const title = await twd.get("input[name='title']"); await userEvent.type(title.el, "Test Todo"); const description = await twd.get("input[name='description']"); await userEvent.type(description.el, "Test Description"); const date = await twd.get("input[name='date']"); await userEvent.type(date.el, "2024-12-20"); const submitButton = await twd.get("button[type='submit']"); // submit await userEvent.click(submitButton.el); // we wait for the list request to be made await twd.waitForRequest("getTodoList"); ``` Finally, we validate both the **request body** and **updated list**: ```ts import { twd, expect, userEvent } from "twd-js"; // all waits return the rule with the definition and request made const rule = await twd.waitForRequest("createTodo"); // we can validate the request sent to the backend expect(rule.request).to.deep.equal({ title: "Test Todo", description: "Test Description", date: "2024-12-20", }); const todoList = await twd.getAll("[data-testid='todo-item']"); expect(todoList).to.have.length(1); ``` > expect assertions (from Chai) only show up in the sidebar on failure — unlike .should which always displays. We use Chai as these tests execute in the browser --- ## Full Example Here's the complete file, including a best practice: use `twd.clearRequestMockRules()` in a `beforeEach()` to ensure every test runs independently. ```ts import { twd, expect, userEvent } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; import todoListMock from "./mocks/todoList.json"; describe("Todo List Page", () => { beforeEach(() => { twd.clearRequestMockRules(); }); it("should display the todo list", async () => { await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock, status: 200, }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); const todo1Title = await twd.get("[data-testid='todo-title-1']"); todo1Title.should("have.text", "Learn TWD"); const todo2Title = await twd.get("[data-testid='todo-title-2']"); todo2Title.should("have.text", "Build Todo App"); }); it("should create a todo", async () => { await twd.mockRequest("createTodo", { method: "POST", url: "/api/todos", response: todoListMock[0], status: 200, }); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [], status: 200, }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [todoListMock[0]], status: 200, }); const noTodosMessage = await twd.get("[data-testid='no-todos-message']"); noTodosMessage.should("be.visible"); const title = await twd.get("input[name='title']"); await userEvent.type(title.el, "Test Todo"); const description = await twd.get("input[name='description']"); await userEvent.type(description.el, "Test Description"); const date = await twd.get("input[name='date']"); await userEvent.type(date.el, "2024-12-20"); const submitButton = await twd.get("button[type='submit']"); await userEvent.click(submitButton.el); await twd.waitForRequest("getTodoList"); const rule = await twd.waitForRequest("createTodo"); expect(rule.request).to.deep.equal({ title: "Test Todo", description: "Test Description", date: "2024-12-20", }); const todoList = await twd.getAll("[data-testid='todo-item']"); expect(todoList).to.have.length(1); }); }); ``` ![All tests passed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qd33cy9ky9ir6f3ztio.png) ## Removing Todos For deleting todos, we reuse what we've learned — mocking, waiting, and validating. ```ts it("should delete a todo", async () => { await twd.mockRequest("deleteTodo", { method: "DELETE", url: "/api/todos/1", response: null, status: 200, }); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock, status: 200, }); await twd.visit("/todos"); const deleteButton = await twd.get("[data-testid='delete-todo-1']"); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock.filter((todo) => todo.id !== "1"), status: 200, }); await userEvent.click(deleteButton.el); await twd.waitForRequest("deleteTodo"); await twd.waitForRequest("getTodoList"); const todoList = await twd.getAll("[data-testid='todo-item']"); expect(todoList).to.have.length(1); }); ``` --- ## What's Next We've covered one of the most important parts of the TWD approach — mocking. This is where the framework truly shines. A few closing thoughts: - Use mocks to test what you're building — your frontend. Don't worry about backend or third-party services during development. - These mocks only intercept **client-side** requests (SSR is out of scope). - You'll have all possible scenarios documented as runnable tests — no setup, no servers, just `npm i`. We strongly believe this is the most efficient way to develop and test modern SPAs. It keeps your workflow fast, predictable, and resilient to backend changes. In the [next tutorial](./ci-integration), we'll take things further by adding **terminal execution for CI**. You can also learn more about API mocking in our [official documentation](/api-mocking). # CI Integration Tutorial Source: https://twd.dev/tutorial/ci-integration In the [previous tutorial](./api-mocking), we explored one of TWD's core features — **network mocking** — and completed our first full set of tests. Now, it's time to take the next step: **running those tests in the terminal** so we can integrate them into a **CI workflow**. > Looking for the quickest path? Install `twd-cli` (`npx twd-cli run`) and you’ll have a ready-made CI runner that handles Puppeteer setup, config, coverage, and exit codes for you. The rest of this guide shows how to build the same flow yourself so you can customize every step. Check the updated [CI Execution docs](/ci-execution) for the latest CLI options and ready-to-copy workflow snippets. To roll your own runner, we’ll still use Puppeteer and one of TWD's utilities, `reportResults`, to display test results directly in the console. ## Before You Start If you're following along from the previous tutorial, you can continue as is. But if you want to reset your repo or make sure you're on the correct branch: ``` # Repo git clone git@github.com:BRIKEV/twd-docs-tutorial.git git reset --hard git clean -d -f git checkout 04-ci-integration npm run serve:dev ``` --- ## Running Tests in the Terminal TWD exposes its runner on the `window` object, which means you can programmatically execute your tests from any environment — including tools like Puppeteer. Here's the basic version of that script: ```ts import { reportResults } from 'twd-js/runner-ci'; const TestRunner = window.__testRunner; const testStatus = []; const runner = new TestRunner({ onStart: () => {}, onPass: (test, retryAttempt) => { // retryAttempt is undefined on first-attempt success, or the attempt number (2+) on retry testStatus.push({ id: test.id, status: "pass" }); }, onFail: (test, err) => { testStatus.push({ id: test.id, status: "fail", error: err.message }); }, onSkip: (test) => { testStatus.push({ id: test.id, status: "skip" }); }, }); const handlers = await runner.runAll(); // report results reportResults(handlers, testStatus); ``` > **Tip:** You can pass a second argument to the `TestRunner` constructor with `{ retryCount: 2 }` to automatically retry failing tests in CI. See the [CI Execution docs](/ci-execution#custom-runner-options) for details. That's the core logic we need to run TWD tests headlessly — but we still need a way to access the `window` context. Let's do that by using Puppeteer. --- ## Step 1. Install Dependencies ```bash npm install --save-dev puppeteer ``` ## Step 2. Create a CI Script Let's create a new file: `scripts/run-tests-ci.js`: ```ts import puppeteer from "puppeteer"; import { reportResults } from 'twd-js/runner-ci'; const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); console.time('Total Test Time'); try { // Navigate to your development server console.log('Navigating to http://localhost:5173 ...'); await page.goto('http://localhost:5173'); // wait to load data-testid="twd-sidebar" await page.waitForSelector('[data-testid="twd-sidebar"]', { timeout: 10000 }); console.log('Page loaded. Starting tests...'); // reload page // Execute all tests const { handlers, testStatus } = await page.evaluate(async () => { const TestRunner = window.__testRunner; const testStatus = []; const runner = new TestRunner({ onStart: () => {}, onPass: (test) => { testStatus.push({ id: test.id, status: "pass" }); }, onFail: (test, err) => { testStatus.push({ id: test.id, status: "fail", error: err.message }); }, onSkip: (test) => { testStatus.push({ id: test.id, status: "skip" }); }, }); const handlers = await runner.runAll(); return { handlers: Array.from(handlers.values()), testStatus }; }); console.log(`Tests to report: ${testStatus.length}`); // Display results in console reportResults(handlers, testStatus); // Exit with appropriate code const hasFailures = testStatus.some(test => test.status === 'fail'); console.timeEnd('Total Test Time'); process.exit(hasFailures ? 1 : 0); } catch (error) { console.error('Error running tests:', error); process.exit(1); } finally { console.log('Closing browser...'); await browser.close(); } ``` ## Step 3. Add the Script to package.json ```jsonc { "scripts": { // ... "test:ci": "node scripts/run-tests-ci.js" } } ``` Now, with your development server running in another terminal, execute: ```bash npm run test:ci ``` And you should see something like this: ![tests loaded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfqyh4iuktkuy4qm8ttl.png) --- ## GitHub Actions Integration You now have two equally valid paths: - **Use `twd-cli`** for a batteries-included workflow (see [`/ci-execution`](/ci-execution) for full YAML). - **Use your custom script** (`npm run test:ci`) when you need bespoke orchestration. Below is an example that wires the script from this tutorial into Actions: Create a file at `.github/workflows/ci.yml`: ```yml name: CI - PR Tests on: pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 24 cache: 'npm' - name: Install dependencies run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Start Vite dev server run: | nohup npm run dev > vite.log 2>&1 & npx wait-on http://localhost:5173 env: CI: true - name: Run Puppeteer tests (test:ci) run: npm run test:ci env: CI: true ``` And that's it — your tests will now run automatically in CI. ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xny2jnyq13l6a5euy3hl.png) ## What's Next We've learned how TWD integrates smoothly into CI workflows. The [next step](./coverage) is to collect code coverage, the last missing piece of most testing setups — and that's exactly what we'll cover in the next tutorial. You can always check out [our official docs](/ci-execution) to learn more. # Code Coverage Tutorial Source: https://twd.dev/tutorial/coverage TWD still has many more advanced features — such as function mocking — but with what we've covered so far, you already have everything a solid testing tool needs. In this last step, we'll see how to collect code coverage from your TWD tests and visualize it locally or in CI. ## Before You Start If you're following along from the [CI integration tutorial](./ci-integration), you can continue as is. But if you want to reset your repo or make sure you're on the correct branch: ``` # Repo git clone git@github.com:BRIKEV/twd-docs-tutorial.git git reset --hard git clean -d -f git checkout 05-coverage npm run serve:dev ``` ## Instrumenting the Vite App We'll use the [vite-plugin-istanbul](https://www.npmjs.com/package/vite-plugin-istanbul) plugin to instrument our code and generate coverage data. ``` npm i --save-dev vite-plugin-istanbul ``` Then, open your `vite.config.ts` and add the plugin alongside the `twd()` plugin from the previous tutorial step: ```ts /// import path from "path" import tailwindcss from "@tailwindcss/vite" import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // add plugins for code coverage and TWD import istanbul from 'vite-plugin-istanbul'; import { twd } from 'twd-js/vite-plugin'; // https://vite.dev/config/ export default defineConfig({ plugins: [ react(), tailwindcss(), twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}', }), // configure istanbul plugin istanbul({ include: 'src/**/*', exclude: ['node_modules', 'tests/'], extension: ['.ts', '.tsx'], requireEnv: process.env.CI ? true : false, }), ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, server: { watch: { ignored: ["**/data/data.json", "**data/routes.json"], }, }, }) ``` This plugin automatically adds coverage data to `window.__coverage__`, which means we can later extract it from Puppeteer during our CI run. --- ## Updating the Puppeteer Script Let's update our `scripts/run-tests-ci.js` script to collect that coverage and save it locally: ```ts import fs from 'fs'; import path from 'path'; import puppeteer from "puppeteer"; import { reportResults } from 'twd-js/runner-ci'; // Determine project root let __dirname = path.resolve(); const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); console.time('Total Test Time'); try { // Navigate to your development server console.log('Navigating to http://localhost:5173 ...'); await page.goto('http://localhost:5173'); // wait to load data-testid="twd-sidebar" await page.waitForSelector('[data-testid="twd-sidebar"]', { timeout: 10000 }); console.log('Page loaded. Starting tests...'); // reload page // Execute all tests const { handlers, testStatus } = await page.evaluate(async () => { const TestRunner = window.__testRunner; const testStatus = []; // Pass { retryCount: 2 } as second arg to retry flaky tests in CI const runner = new TestRunner({ onStart: () => {}, onPass: (test, retryAttempt) => { testStatus.push({ id: test.id, status: "pass" }); }, onFail: (test, err) => { testStatus.push({ id: test.id, status: "fail", error: err.message }); }, onSkip: (test) => { testStatus.push({ id: test.id, status: "skip" }); }, }); const handlers = await runner.runAll(); return { handlers: Array.from(handlers.values()), testStatus }; }); console.log(`Tests to report: ${testStatus.length}`); // Display results in console reportResults(handlers, testStatus); // --- Collect coverage --- const coverage = await page.evaluate(() => window.__coverage__); if (coverage) { console.log('Collecting code coverage data...'); const coverageDir = path.resolve(__dirname, './coverage'); const nycDir = path.resolve(__dirname, './.nyc_output'); if (!fs.existsSync(nycDir)) { fs.mkdirSync(nycDir); } if (!fs.existsSync(coverageDir)) { fs.mkdirSync(coverageDir); } const coveragePath = path.join(nycDir, 'out.json'); fs.writeFileSync(coveragePath, JSON.stringify(coverage)); console.log(`Code coverage data written to ${coveragePath}`); } else { console.log('No code coverage data found.'); } // Exit with appropriate code const hasFailures = testStatus.some(test => test.status === 'fail'); console.timeEnd('Total Test Time'); process.exit(hasFailures ? 1 : 0); } catch (error) { console.error('Error running tests:', error); process.exit(1); } finally { console.log('Closing browser...'); await browser.close(); } ``` --- ## Updating package.json Scripts ```jsonc { "scripts": { // ... "dev:ci": "CI=true VITE_COVERAGE=true vite", "test:ci": "node scripts/run-tests-ci.js", "collect:coverage:html": "npx nyc report --reporter=html --report-dir=coverage", "collect:coverage:lcov": "npx nyc report --reporter=lcov --report-dir=coverage", "collect:coverage:text": "npx nyc report --reporter=text --report-dir=coverage" } } ``` Now, run these in two terminals: ``` npm run dev:ci ``` And in another terminal: ``` npm run test:ci ``` Once the tests complete, you can generate coverage reports in different formats: ``` npm run collect:coverage:html npm run collect:coverage:lcov npm run collect:coverage:text ``` You'll see something like this: ![coverage html reporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ss03qfi57r43ehwgf0fn.png) ![coverage text reporter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1dtemuclax3jwbmlju5x.png) ## Adding Coverage to GitHub Actions To include the coverage output in your GitHub Action, just update your existing CI workflow: ```yml name: CI - PR Tests on: pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: 24 cache: 'npm' - name: Install dependencies run: npm ci - name: Install mock service worker run: npx twd-js init public --save - name: Start Vite dev server run: | nohup npm run dev > vite.log 2>&1 & npx wait-on http://localhost:5173 env: CI: true - name: Run Puppeteer tests (test:ci) run: npm run test:ci env: CI: true - name: Display coverage run: | npm run collect:coverage:text ``` With the new `Display coverage` step, you'll see the coverage summary directly in your GitHub Action logs: ![Github action coverage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0cznd4txgvtpxz5tn10o.png) Having this basic configuration, it's entirely up to you how you want to handle the coverage results. You can publish them to a service like Codecov or Coveralls, display them as badges in your README, or even use them in your CI pipeline to fail a build if coverage drops below a threshold. What matters is that the library gives you the flexibility to collect and track coverage directly from your browser tests, without relying on a separate test runner. --- ## Conclusion And that's it — we've reached the end of the TWD tutorial series! Throughout these five parts, we've built a complete workflow from zero to production-ready testing: - Setup and first visit test - Selectors, assertions, and user events - Network mocking - CI integration with Puppeteer - Code coverage collection and reporting With this foundation, you can now test your apps as you develop them, keeping your environment close to what your users actually see — which is what TWD (**Test While Developing**) is all about. Check out the integration with Testing Library selectors in the next tutorial page [Using Testing Library Selectors](./testing-library-selectors). Thanks for following along, and happy testing! You can always explore more in our [official docs](/getting-started). # Production Builds Source: https://twd.dev/tutorial/production-builds Learn how to build production-ready applications without including test code or mock service workers. ## Why Remove Test Code from Production? When building for production, you want to: - **Reduce bundle size** - Remove test files and mock service workers - **Improve performance** - Eliminate development-only code - **Enhance security** - Don't expose test infrastructure - **Clean deployment** - Only ship what users need ## Automatic Test Exclusion TWD automatically handles most production concerns: ### Test Files Are Not Bundled ``` src/ ├── components/ │ ├── Button.tsx ✅ Included in production │ └── Header.tsx ✅ Included in production ├── tests/ │ ├── button.twd.test.ts ❌ Excluded from production │ └── header.twd.test.ts ❌ Excluded from production └── utils/ └── helpers.ts ✅ Included in production ``` Test files (`.twd.test.ts` or `.twd.test.js`) are automatically excluded from your production bundle. ## Removing the Service Worker The mock service worker file (`mock-sw.js`) needs to be manually removed from production builds. ### Using the Vite Plugin (Recommended) Add `removeMockServiceWorker()` alongside the `twd()` plugin you already configured for development: ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { twd, removeMockServiceWorker } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), twd(), // dev: sidebar + tests + service worker registration removeMockServiceWorker(), // build: strips mock-sw.js from dist ], }); ``` ### What the Plugin Does ```bash # During build process npm run build # Plugin output 🧹 Removed mock-sw.js from build # Or if no mock file found 🧹 No mock-sw.js found in build ``` The plugin: - Only runs during build (`apply: 'build'`) - Removes `dist/mock-sw.js` after build completes - Provides feedback about the removal - Fails gracefully if no mock file exists ### Manual Removal (Alternative) If you can't use the Vite plugin, remove the file manually: ```json { "scripts": { "build": "vite build && rm -f dist/mock-sw.js", "build:win": "vite build && del dist\\mock-sw.js" } } ``` ## Troubleshooting ### Service Worker Still in Build If `mock-sw.js` appears in your production build: 1. **Check Vite plugin configuration**: ```ts // Make sure plugin is added correctly import { removeMockServiceWorker } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), removeMockServiceWorker() // Must be here ] }); ``` 2. **Verify build command**: ```bash # Make sure you're running build, not dev npm run build # ✅ Correct npm run dev # ❌ Wrong for production ``` 3. **Check plugin output**: ```bash # Look for plugin messages during build npm run build # Should see: 🧹 Removed mock-sw.js from build ``` ## Best Practices ### 1. Always Use the Vite Plugin ```ts // ✅ Recommended approach import { removeMockServiceWorker } from 'twd-js/vite-plugin'; export default defineConfig({ plugins: [ react(), removeMockServiceWorker() ] }); ``` # Testing Library Selectors Source: https://twd.dev/tutorial/testing-library-selectors In this part, we’ll look at a new feature in TWD: support for Testing Library–style selectors. These selectors are widely known in the community, and while you could technically use them before (TWD is very flexible), now they’re fully integrated — including visual selection in the **sidebar**. ## Before You Start If you’re following along from Part 5, you can continue as is. But if you want to reset your repo or make sure you're on the correct branch: ``` # Repo git clone git@github.com:BRIKEV/twd-docs-tutorial.git git reset --hard git clean -d -f git checkout 06-selectors-testing-library npm run serve:dev ``` ## Let’s Begin We’re going to migrate our current `twd.get` calls (which uses querySelector and we were using data-testid in previous post) to the new Testing Library selectors. ## Using screenDom First, update your `src/twd-tests/helloWorld.twd.test.ts` file. ```ts // We will change this const title = await twd.get("[data-testid='welcome-title']"); // to this const title = await screenDom.getByText("Welcome to TWD"); ``` For assertions, we’ll use a new command: `twd.should`. It works exactly like `element.should`, but can be used with any element returned by `screenDom`. Your updated test will look like this: ```ts import { twd, userEvent, screenDom } from "twd-js"; import { describe, it } from "twd-js/runner"; describe("Hello World Page", () => { it("should display the welcome title and counter button", async () => { await twd.visit("/"); const title = await screenDom.getByText("Welcome to TWD"); twd.should(title, 'be.visible'); const counterButton = await screenDom.getByText("Count is 0"); twd.should(counterButton, 'be.visible'); await userEvent.click(counterButton); twd.should(counterButton, 'have.text', 'Count is 1'); await userEvent.click(counterButton); twd.should(counterButton, 'have.text', 'Count is 2'); await userEvent.click(counterButton); twd.should(counterButton, 'have.text', 'Count is 3'); }); }); ``` > You can mix both approaches. `twd.get` includes a built-in `.should`, while screenDom lets you use familiar Testing Library selectors, combined with `twd.should`. ## Todo list Tests Here’s how the Todo tests look after migrating to testing-library selectors: ```ts import { twd, expect, userEvent, screenDom } from "twd-js"; import { describe, it, beforeEach } from "twd-js/runner"; import todoListMock from "./mocks/todoList.json"; describe("Todo List Page", () => { beforeEach(() => { twd.clearRequestMockRules(); }); it("should display the todo list", async () => { await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock, status: 200, }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); const todo1Title = await screenDom.getByText("Learn TWD"); twd.should(todo1Title, "be.visible"); const todo2Title = await screenDom.getByText("Build Todo App"); twd.should(todo2Title, "be.visible"); const todo1Description = await screenDom.getByText("Understand how to use TWD for testing web applications"); twd.should(todo1Description, "be.visible"); const todo2Description = await screenDom.getByText("Create a todo list application to demonstrate TWD features"); twd.should(todo2Description, "be.visible"); const todo1Date = await screenDom.getByText("Date: 2024-12-20"); twd.should(todo1Date, "be.visible"); const todo2Date = await screenDom.getByText("Date: 2024-12-25"); twd.should(todo2Date, "be.visible"); }); it("should create a todo", async () => { await twd.mockRequest("createTodo", { method: "POST", url: "/api/todos", response: todoListMock[0], status: 200, }); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [], status: 200, }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); const noTodosMessage = await screenDom.getByText("No todos yet. Create one above!"); twd.should(noTodosMessage, "be.visible"); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: [ todoListMock[0] ], status: 200, }); const titleInput = await screenDom.getByLabelText("Title"); await userEvent.type(titleInput, "Test Todo"); const descriptionInput = await screenDom.getByLabelText("Description"); await userEvent.type(descriptionInput, "Test Description"); const dateInput = await screenDom.getByLabelText("Date"); await userEvent.type(dateInput, "2024-12-20"); const submitButton = await screenDom.getByRole("button", { name: "Create Todo" }); await userEvent.click(submitButton); await twd.waitForRequest("getTodoList"); const rule = await twd.waitForRequest("createTodo"); expect(rule.request).to.deep.equal({ title: "Test Todo", description: "Test Description", date: "2024-12-20", }); const todoList = await screenDom.getAllByText(/Learn TWD|Build Todo App|Test Todo/); expect(todoList).to.have.length(1); }); it("should delete a todo", async () => { await twd.mockRequest("deleteTodo", { method: "DELETE", url: "/api/todos/1", response: null, status: 200, }); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock, status: 200, }); await twd.visit("/todos"); // Find the delete button for the first todo (Learn TWD) // Since there are multiple delete buttons, we'll get all and use the first one // which corresponds to the first todo item const deleteButtons = await screenDom.getAllByRole("button", { name: "Delete" }); const deleteButton = deleteButtons[0]; await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", response: todoListMock.filter((todo) => todo.id !== "1"), status: 200, }); await userEvent.click(deleteButton); await twd.waitForRequest("deleteTodo"); await twd.waitForRequest("getTodoList"); const todoList = await screenDom.getAllByText(/Learn TWD|Build Todo App/); expect(todoList).to.have.length(1); twd.should(todoList[0], "be.visible"); }); }); ``` ## Conclusion As you can see, TWD is flexible enough to support both styles of selectors. And remember: these tests run inside your actual application, so anything your app can access—stores, utilities, helpers—can be used to set up your scenarios. TWD is all about keeping testing close to development, while staying simple, flexible, and intuitive. # Community & Examples Source: https://twd.dev/community Real-world examples, tutorials, and community content for TWD. ## Contributors People from the community who have shipped code, docs, or other improvements to TWD. If you're starting out in tech and looking for a beginner-friendly first PR, see the [open issues](https://github.com/BRIKEV/twd/issues) — reach out and I'll help with setup and walk you through it. | Contributor | Contribution | |-------------|--------------| | [Francisco Javier Rodriguez](https://github.com/DamonCaos) | Documentation — [PR #230](https://github.com/BRIKEV/twd/pull/230) | | [Guillermo Ruiz Arranz](https://github.com/codesthenos) | Code — [PR #232](https://github.com/BRIKEV/twd/pull/232) | | [Roberto Gomez Fabrega](https://github.com/Rober040992) | Documentation — [PR #239](https://github.com/BRIKEV/twd/pull/239) | ## Live Showcase See TWD testing real shadcn/ui components with live code and tests running in the browser: **[TWD + shadcn/ui Showcase](https://brikev.github.io/twd-shadcn/)** — Interactive demo with component tests you can run yourself. ## Example Repositories | Repository | Framework | What it demonstrates | |------------|-----------|----------------------| | [twd-shadcn](https://github.com/BRIKEV/twd-shadcn) | React + shadcn/ui | Testing shadcn components (forms, dialogs, tables) with [live demo](https://brikev.github.io/twd-shadcn/) | | [twd-react-router](https://github.com/BRIKEV/twd-react-router) | React Router (Framework Mode) | SSR-compatible setup with `createRoutesStub`, loader mocking | | [twd-tanstack-example](https://github.com/BRIKEV/twd-tanstack-example) | React + TanStack Router | TanStack Router integration with route + loader testing | | [twd-vue-example](https://github.com/BRIKEV/twd-vue-example) | Vue | Vue integration with bundled setup | | [twd-nuxt-example](https://github.com/BRIKEV/twd-nuxt-example) | Nuxt 4 (SSR) | Pages tested in the browser against a real SQLite backend, with a dev-only reset endpoint | | [twd-angular-example](https://github.com/BRIKEV/twd-angular-example) | Angular | Angular integration with manual test imports | | [twd-auth0](https://github.com/BRIKEV/twd-auth0) | React + Auth0 | Auth session mocking with Sinon stubs | | [twd-auth0-pkce](https://github.com/BRIKEV/twd-auth0-pkce) | React + Auth0 PKCE | PKCE flow mocking for SPAs | | [twd-docs-tutorial](https://github.com/BRIKEV/twd-docs-tutorial) | React | Step-by-step companion to the [tutorial](/tutorial/) | | [twd-create-react-app](https://github.com/BRIKEV/twd-create-react-app) | Create React App (Webpack) | `require.context` test loading, react-router loaders/actions, CI with twd-cli and contract validation | | [twd-vanillajs](https://github.com/BRIKEV/twd-vanillajs) | Vanilla JS (no bundler) | TWD loaded from a CDN via an import map, counter + todo list, API mocking, CI with twd-cli and contract validation | | [twd-htmx](https://github.com/BRIKEV/twd-htmx) | HTMX (no bundler) | TWD loaded from a CDN, HTMX todo list tested against a real HTML backend with a dev-only reset endpoint | | [twd-cells](https://github.com/BRIKEV/twd-cells) | Open Cells (Lit) | BBVA's Web Components framework: hashbang-router pages, request mocking, real localStorage, and CI with coverage via the vite plugin | | [frontend-challenge](https://github.com/kevinccbsg/frontend-challenge) | React + Vite | Testing Library `render()` component tests running in the browser next to jsdom tests of the same component | ### In-Repo Examples The main TWD repository also includes working examples: - **[twd-test-app](https://github.com/BRIKEV/twd/tree/main/examples/twd-test-app)** — React app with forms, API mocking, and component mocking - **[vue-twd-example](https://github.com/BRIKEV/twd/tree/main/examples/vue-twd-example)** — Vue integration - **[astro-example](https://github.com/BRIKEV/twd/tree/main/examples/astro-example)** — Astro + React integration ## Videos - [TWD: Una nueva forma de testear el frontend](https://www.youtube.com/watch?v=F0b63Cl6_Mo) (Commit Conf, Spanish) - [Testing while developing: una nueva forma de testear en frontend](https://www.youtube.com/watch?v=qsHowBWgJn8) (Spanish) ## Podcasts - **Una nueva forma de testear frontend con TWD | Kevin Martinez #83** (Spanish) - [YouTube](https://www.youtube.com/watch?v=qkFe4kayGIw) | [Spotify](https://spotifycreators-web.app.link/e/a6xQdwYSF0b) - **How to Test This - Episode #14: How to Test with Testing While Developing (TWD) - Kevin Martínez** - [YouTube](https://youtu.be/O4QahYTYQgE) | [Spotify](https://open.spotify.com/episode/6mQV2liEL68mM9JqRXpPVo) ## Blog Posts Written by [Kevin Martinez](https://www.linkedin.com/in/kevinjmartinez/), TWD maintainer. More posts on [DEV.to](https://dev.to/kevinccbsg). Follow on [Bluesky](https://bsky.app/profile/kevintwd.bsky.social). # Accessibility Statement Source: https://twd.dev/accessibility-statement Test While Developing - [twd.dev](https://twd.dev/) > **Status:** Full conformance with WCAG 2.2 Level AA. > > Audited June 2026 by Latam11y. Last updated: 21 June 2026. ## 1. Introduction BRIKEV is committed to digital accessibility and inclusion. We want everyone, including people with disabilities, to be able to successfully use our website and the TWD testing tool. This statement explains the extent to which TWD complies with the requirements of European standard [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_302000/301549/03.02.01_60/en_301549v030201p.pdf) (the technical reference of the [European Accessibility Act (EAA, EU Directive 2019/882)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882)) and the [Web Content Accessibility Guidelines (WCAG) 2.2](https://www.w3.org/TR/WCAG22/). It covers the TWD website ([twd.dev](https://twd.dev/)) and the twd-js tool. We review this statement periodically as the service evolves. ## 2. Service overview **TWD ("Test While Developing")** is an **open-source** frontend testing framework, published under the MIT licence, that allows development teams to write and run tests directly while developing, without needing to switch context. Its main component, **twd-js**, integrates as a browser sidebar in the development environment and is compatible with React, Vue, Angular, Solid.js, React Router, and other Vite-based frameworks. The audited service comprises: - The website [https://twd.dev/](https://twd.dev/): homepage and getting-started documentation. - The **twd-js** tool ([npm package twd-js](https://www.npmjs.com/package/twd-js)), audited as an application installed in the development browser. ## 3. How to use TWD (accessibility and operation) The TWD website has been designed to be navigable and usable with keyboard and screen readers. The main accessibility features are described below. ### Keyboard navigation All links, buttons, and interactive elements are reachable with the Tab key. The focus order is logical and consistent with the visual flow of the page. There are no keyboard traps. ### Focus indicator The focus indicator is visible on all components of the site and the tool, in both light and dark themes. It meets a minimum stroke of 2 px and a 2 px offset from the content, with sufficient contrast against adjacent colours (minimum 3:1). ### Colour contrast Site text meets a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text, in both light and dark themes. Non-text components of the tool meet the minimum 3:1 ratio. ### Link purpose Links are descriptive and distinguishable by more than colour alone. ### Screen reader compatibility Tested with NVDA 2026.1 on Windows 11. Interactive components correctly announce their name, role, and state. ## 4. Accessibility compliance We have evaluated TWD (website and twd-js tool) against the accessibility requirements of Annex I of the EAA, technically implemented through EN 301 549 and WCAG 2.2, across the four POUR principles: | Principle | Summary | | --- | --- | | **Perceivable** | Content is structured with headings, lists, and ARIA landmarks. Text meets the required contrast ratios in both themes. Information is not conveyed by colour alone. | | **Operable** | All functionality is keyboard accessible. Focus indicators are visible and have sufficient contrast. There are no keyboard traps. | | **Understandable** | Content is written in plain, consistent English. Navigation and structure are consistent across all pages. | | **Robust** | The site is built with semantic HTML5 and correct ARIA roles, compatible with current browser versions and assistive technologies. | **Reference standards:** - [WCAG 2.2 Level AA](https://www.w3.org/TR/WCAG22/) - [EN 301 549 v3.2.1](https://www.etsi.org/deliver/etsi_en/301500_302000/301549/03.02.01_60/en_301549v030201p.pdf) - [EAA, Directive 2019/882/EU](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882) ::: info Conformance status **Full conformance with WCAG 2.2 Level AA.** All evaluated Level A and AA success criteria are met. ::: **Audit information:** | | | | --- | --- | | **Audited by** | María Pía Peña Foissac - Latam11y, digital accessibility consultancy ([mariapiapenafoissac@gmail.com](mailto:mariapiapenafoissac@gmail.com)) | | **Date** | June 2026 | | **Methodology** | Manual review with keyboard and NVDA 2026.1 on Windows 11 (26H1); colour contrast verification with Tanaguru Contrast Finder | | **Scope** | [twd.dev](https://twd.dev/) (Home + Getting Started) plus the twd-js tool (browser sidebar) | ## 5. Ongoing monitoring and maintenance Accessibility for TWD is an ongoing process. Measures in place include: - Accessibility review when significant changes are made to the site or tool. - Tracking of reported accessibility issues through the [GitHub repository](https://github.com/BRIKEV/twd/issues). - Commitment to update this statement following new audits or relevant changes. - Monitoring of updates to WCAG and EN 301 549. ## 6. Known limitations There are no known areas of TWD (website or twd-js tool) that are inaccessible. All Level A and AA conformance criteria evaluated have been remediated. This statement will be updated if new limitations are identified. ## 7. Disproportionate burden BRIKEV does not claim any exemption or disproportionate burden in meeting the applicable accessibility requirements. Should a specific situation require such an assessment in the future, it will be documented in accordance with Annex VI of the EAA and this statement will be updated. ## 8. Feedback and contact information If you experience any difficulty accessing any part of TWD, identify an accessibility issue, or have suggestions for improvement, please let us know: - **Email:** [hello.brikev@gmail.com](mailto:hello.brikev@gmail.com?subject=TWD%20Accessibility) - **GitHub Issues:** [github.com/BRIKEV/twd/issues](https://github.com/BRIKEV/twd/issues) - **GitHub Discussions:** [github.com/BRIKEV/twd/discussions](https://github.com/BRIKEV/twd/discussions) When contacting us, please provide as much detail as possible: which page or component, what happened, and what assistive technology you are using. **Response time commitments:** - **5 business days** - acknowledgement. - **30 business days** - resolution or update. ## 9. Document history This accessibility statement was first published on 21 June 2026. It was last reviewed and updated on 21 June 2026. We intend to review it at least annually or whenever significant changes are made to the service. --- Statement prepared in accordance with the [European Accessibility Act (EU Directive 2019/882)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882) and standard [EN 301 549 v3.2.1](https://www.etsi.org/deliver/etsi_en/301500_302000/301549/03.02.01_60/en_301549v030201p.pdf). Audited by María Pía Peña Foissac and Daiana Elizabeth Carbonell, [Latam11y](mailto:mariapiapenafoissac@gmail.com), June 2026. # Recording Runs Source: https://twd.dev/recording `twd-cli` can record a test run to a video file. The point is not a debugging trace, it is an artifact a person will actually watch: attach the flow to a pull request, drop it in your docs, or send it to someone who asked what changed. ```bash npx twd-cli run --record --test "checkout flow" ``` That writes `twd-artifacts/checkout-flow.mp4`. Requires `twd-cli` 1.4.0 or newer. See [github.com/BRIKEV/twd-cli](https://github.com/BRIKEV/twd-cli) for source and release notes. ## Prerequisite: ffmpeg Recording spawns ffmpeg, so it has to be available: ```bash brew install ffmpeg # macOS sudo apt-get install ffmpeg # Linux winget install ffmpeg # Windows ``` Set `record.ffmpegPath` in `twd.config.json` if it is not on your `PATH`. `twd-cli` checks for ffmpeg before launching the browser, so a missing binary fails immediately with install instructions rather than part way through a run. ## Why the run is paced Tests execute in milliseconds. A recording of two tests running at full speed is about a second long, which is not something anyone can follow. The obvious fix is to slow the video down afterwards, and that is what `record.speed` does: an ffmpeg filter that stretches the timeline. It works, but it stretches the same frames over more time, so the frame rate drops in proportion. Measured on identical page activity: | `speed` | duration | effective fps | |---|---|---| | `1` | 1.00s | 30 | | `0.5` | 1.90s | 15.3 | | `0.25` | 3.90s | 7.7 | It also slows the dead air exactly as much as the parts worth watching. TWD takes a different route. Because commands execute inside the page, TWD owns its own command loop and can space the execution itself. Frames are captured at full rate, and the pauses land where something just happened. Typing is spaced per keystroke too, so text appears character by character instead of all at once. The same test recorded both ways. It fills a seven field form and submits it:
    pace: 0 1.8s. The form fills in a handful of frames.
    pace: 300 8.2s. Same test, same assertions.
    This is why recorded runs are **paced by default at 300ms**. `--record` on its own gives you something watchable. ```bash # Default 300ms pace npx twd-cli run --record --test "checkout flow" # Slower, for a more deliberate demo npx twd-cli run --record --record-pace 500 --test "checkout flow" # No pacing, for the fastest possible recorded run npx twd-cli run --record --record-pace 0 --test "checkout flow" ``` Values between 200 and 500 tend to read well. Pacing needs `twd-js` 1.9.0 or newer. On an older version the run still records, unpaced, and warns. ## Watching a run without recording it The same pacing is available in the sidebar, without ffmpeg and without producing a file. Turn it on with the `pace` option: ```ts // vite.config.ts twd({ pace: true }); ``` That adds a speed selector to the sidebar header: | Option | Pace | |---|---| | `Off (full speed)` | `0` | | `Slow (300ms)` | `300` | | `Slower (600ms)` | `600` | The choice is remembered for the tab, so it survives the reloads you get while editing tests. It applies to every run in the page, including runs triggered over [twd-relay](/twd-relay), which is the point: when an agent writes a test and runs it for you, a paced run is one you can actually follow. Leave it on `Off` for normal development, where a run finishing in milliseconds is the feature. Like `record.pace`, this only spaces out commands. It does not change what the tests assert. ## A recorded run is not a CI run Recording changes the conditions the tests run under: - It sets its own viewport, 1280x720 by default, where a normal `twd-cli` run uses Puppeteer's implicit 800x600. - It hides the TWD sidebar and reflows your app to full width. - Pacing inserts real delays between actions, which can mask race conditions. So a recorded run can pass or fail differently from a normal one. Treat the video as a demo artifact and keep running [CI](/ci-execution) unrecorded. ## What ends up in the clip One video per run, containing every matched test back to back. `--test` matches a substring of the full `"suite > test"` path, so a single filter can match several tests. Order follows declaration order in the suite tree, not the order you passed the flags. The filename describes the contents: a single recorded test gets a slug of its full path, so `Login > shows error on bad password` becomes `login-shows-error-on-bad-password.mp4`. Anything else gets `run.`. Re-running overwrites the file. Pace a scoped run rather than a whole suite. A 50 test suite averaging 10 actions per test gains roughly 2.5 minutes at 300ms, and about 4 minutes at 500ms. Hitting `protocolTimeout` is unlikely. A chunk is `chunkSize` tests inside a single browser call bounded by that timeout, so at 300ms you would need around 100 actions in one test to reach it. If you somehow do, lower `chunkSize` or raise `protocolTimeout`. ## Configuration All keys live under `record` in `twd.config.json`: ```json { "record": { "enabled": false, "dir": "./twd-artifacts", "filename": null, "format": "mp4", "viewport": { "width": 1280, "height": 720, "deviceScaleFactor": 1 }, "fps": 30, "speed": 1, "pace": 300, "preRoll": 0, "postRoll": 500, "hideSidebar": true, "ffmpegPath": "ffmpeg" } } ``` | Option | Default | Description | |---|---|---| | `enabled` | `false` | Turn recording on. Same as passing `--record` | | `dir` | `"./twd-artifacts"` | Where the video is written | | `filename` | `null` | Explicit output name. When `null`, derived from the recorded tests | | `format` | `"mp4"` | `"mp4"`, `"webm"` or `"gif"`, all encoded natively | | `viewport` | `1280x720` | Applied only when recording. `width` and `height` set the video dimensions. See the note below on `deviceScaleFactor` | | `fps` | `30` | Capture frame rate | | `speed` | `1` | Post-hoc playback speed. Costs frame rate, prefer `pace` | | `pace` | `300` | Milliseconds held after each command. `0` disables | | `preRoll` | `0` | Milliseconds held on the opening state | | `postRoll` | `500` | Milliseconds held on the final state. See below | | `hideSidebar` | `true` | Hide the TWD sidebar so the frame is just your app | | `ffmpegPath` | `"ffmpeg"` | Path to the binary if it is not on your `PATH` | Four flags override the config: `--record`, `--record-dir `, `--record-speed ` and `--record-pace `. Everything else is config only. ### deviceScaleFactor does not change the output resolution It stays at `1` on purpose. Puppeteer measures the recording with the scale factor forced to `0`, so the emulated value never reaches the encoder. Measured: recording the same page at `2` and at `1` produced byte identical files. It is not inert, though. It is live on the page for the whole run, so raising it changes the environment under test: `srcset` and `image-set` select 2x assets, and code that branches on device pixel ratio takes a different path. That adds to the divergence described above for no gain in the video. Puppeteer's actual output size knob is a `scale` option, which this feature does not expose. To get a bigger clip, raise `width` and `height`. ### Why postRoll defaults to on Chrome only emits a video frame when the page repaints, and each frame is held until the next one arrives, because the next frame's timestamp is what says how long to display the current one. The newest frame is therefore never written, and stopping the recorder repeats the one before it. A settled page produces no more repaints, so waiting alone does not help. Measured: stopping immediately ended two states early, and a 400ms plain wait still ended one state early. `postRoll` briefly repaints the whole viewport with an invisible overlay after the last test, which forces the real final frame through and then holds it. Without it the last thing your test did never appears in the video. ## Next Steps - [CI Execution](/ci-execution) for running tests headlessly in a pipeline - [Writing Tests](/writing-tests) for the commands that appear in the recording