twd-js
Version: 
Test While Developing (TWD) - in-browser testing
462 lines (351 loc) • 13.8 kB
Markdown
# TWD
[](https://github.com/BRIKEV/twd/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/twd-js)
[](./LICENSE)
[](https://qlty.sh/gh/BRIKEV/projects/twd)
[](https://qlty.sh/gh/BRIKEV/projects/twd)
> ⚠️ This is a **beta release** – expect frequent updates and possible breaking changes.
TWD (Testing Web Development) is a library designed to seamlessly integrate testing into your web development workflow. It streamlines the process of writing, running, and managing tests directly in your application, with a modern UI and powerful mocking capabilities.
Currently, TWD supports React, with plans to add more frameworks soon.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Writing Tests](#writing-tests)
  - [Test Structure](#test-structure)
  - [Element Selection](#element-selection)
  - [Assertions](#assertions)
  - [User Interactions](#user-interactions)
- [API Mocking](#api-mocking)
  - [Setup](#setup)
  - [Mock Requests](#mock-requests)
  - [Wait for Requests](#wait-for-requests)
- [API Reference](#api-reference)
  - [Test Functions](#test-functions)
  - [TWD Commands](#twd-commands)
  - [Assertions](#assertions-1)
- [Examples](#examples)
- [Contributing](#contributing)
- [License](#license)
## Features
- 🧪 **In-browser test runner** with a beautiful sidebar UI
- ⚡ **Instant feedback** as you develop
- 🔥 **Mock Service Worker** integration for API/request mocking
- 📝 **Simple, readable test syntax** (inspired by popular test frameworks)
- 🧩 **Automatic test discovery** with Vite support
- 🛠️ **Works with React** (support for more frameworks coming)
## Installation
You can install TWD via npm:
```bash
# with npm
npm install twd-js
# with yarn
yarn add twd-js
# with pnpm
pnpm add twd-js
```
## Quick Start
1. **Add the TWD Sidebar to your React app:**
   ```tsx
   import { StrictMode } from "react";
   import { createRoot } from "react-dom/client";
   import App from "./App";
   import "./index.css";
   // Only load the test sidebar and tests in development mode
    if (import.meta.env.DEV) {
      // Use Vite's glob import to find all test files
      const testModules = import.meta.glob("./**/*.twd.test.ts");
      const { initTests, twd, TWDSidebar } = await import('twd-js');
      const { createRoot } = await import('react-dom/client');
      // You need to pass the test modules, the sidebar component, and createRoot function
      initTests(testModules, <TWDSidebar open={true} position="left" />, createRoot);
      // if you want to use mock requests, you can initialize it here
      twd.initRequestMocking()
        .then(() => {
          console.log("Request mocking initialized");
        })
        .catch((err) => {
          console.error("Error initializing request mocking:", err);
        });
    }
   createRoot(document.getElementById("root")!).render(
     <StrictMode>
       <App />
     </StrictMode>
   );
   ```
   **TWDSidebar Props**
    | Prop      | Type                | Default | Description                                 |
    |-----------|---------------------|---------|---------------------------------------------|
    | open      | boolean             | true    | Whether the sidebar is open by default       |
    | position  | "left" \| "right"   | "left" | Sidebar position (left or right side)
2. **Write your tests:**
   Create files ending with `.twd.test.ts` (or any extension you prefer):
   ```ts
   // src/app.twd.test.ts
   import { describe, it, twd } from "twd-js";
   beforeEach(() => {
     // Reset state before each test
   });
   describe("App interactions", () => {
     it("clicks the button", async () => {
       twd.visit("/");
       const btn = await twd.get("button");
       btn.click();
       const message = await twd.get("#message");
       message.should("have.text", "Hello");
     });
   });
   ```
3. **Auto-load your tests:**
   - With Vite and the new TWD loader:
     ```ts
     // src/main.tsx (or your main entry file)
     import { StrictMode } from 'react';
     import { createRoot } from 'react-dom/client';
     import './index.css';
     import router from './routes.ts';
     import { RouterProvider } from 'react-router';
     // Only load the test sidebar and tests in development mode
     if (import.meta.env.DEV) {
       // Use Vite's glob import to find all test files
       const testModules = import.meta.glob("./**/*.twd.test.ts");
       const { initTests, twd, TWDSidebar } = await import('twd-js');
       const { createRoot } = await import('react-dom/client');
       // You need to pass the test modules, the sidebar component, and createRoot function
       initTests(testModules, <TWDSidebar open={true} position="left" />, createRoot);
       // Optionally initialize request mocking
       twd.initRequestMocking()
         .then(() => {
           console.log("Request mocking initialized");
         })
         .catch((err) => {
           console.error("Error initializing request mocking:", err);
         });
     }
     // ...rest of your app bootstrap
     createRoot(document.getElementById('root')!).render(
       <StrictMode>
         <RouterProvider router={router} />
       </StrictMode>,
     );
     ```
   - Or manually (not recommended):
     ```ts
     // src/main.tsx
     if (import.meta.env.DEV) {
       const testModules = {
         './app.twd.test.ts': () => import('./app.twd.test'),
         './another-test-file.twd.test.ts': () => import('./another-test-file.twd.test'),
       };
       const { initTests, TWDSidebar } = await import('twd-js');
       const { createRoot } = await import('react-dom/client');
       initTests(testModules, <TWDSidebar open={true} position="left" />, createRoot);
     }
     ```
4. **Run your app and open the TWD sidebar** to see and run your tests in the browser.
## Writing Tests
### Test Structure
TWD uses a familiar testing structure with `describe`, `it`, `beforeEach`, and other common testing functions:
```ts
import { describe, it, itSkip, itOnly, beforeEach, twd, userEvent } from "twd-js";
describe("User authentication", () => {
  beforeEach(() => {
    // Reset state before each test
  });
  it("should login successfully", async () => {
    twd.visit("/login");
    // Your test logic here
  });
  
  itSkip("skipped test", () => {
    // This test will be skipped
  });
  
  itOnly("only this test runs", () => {
    // Only this test will run when .only is present
  });
});
```
### Element Selection
TWD provides two main methods for selecting elements:
```ts
// Select a single element
const button = await twd.get("button");
const input = await twd.get("input#email");
// Select multiple elements
const items = await twd.getAll(".item");
items[0].should("be.visible");
```
### Assertions
TWD includes a comprehensive set of assertions for testing element states:
```ts
// Text content
element.should("have.text", "exact text");
element.should("contain.text", "partial text");
element.should("be.empty");
// Attributes and values
element.should("have.attr", "placeholder", "Type here");
element.should("have.value", "input value");
element.should("have.class", "active");
// Element state
element.should("be.disabled");
element.should("be.enabled");
element.should("be.checked");
element.should("be.selected");
element.should("be.focused");
element.should("be.visible");
// Negated assertions
element.should("not.be.disabled");
element.should("not.have.text", "wrong text");
// URL assertions
twd.url().should("eq", "http://localhost:3000/contact");
twd.url().should("contain.url", "/contact");
```
### User Interactions
TWD integrates with `-library/user-event` for realistic user interactions:
```ts
import { userEvent } from "twd-js";
const user = userEvent.setup();
const button = await twd.get("button");
const input = await twd.get("input");
// Click interactions
await user.click(button.el);
await user.dblClick(button.el);
// Typing
await user.type(input.el, "Hello World");
// Form interactions
await user.selectOptions(selectElement.el, "option-value");
```
## API Mocking
### Setup
TWD provides a CLI to easily set up a mock service worker for API/request mocking in your app. You do **not** need to manually register the service worker in your app—TWD handles this automatically when you use `twd.initRequestMocking()` in your tests.
Run the following command in your project root:
```bash
npx twd-js init <public-dir> [--save]
```
- Replace `<public-dir>` with the path to your app's public/static directory (e.g., `public/` or `dist/`).
- Use `--save` to print a registration snippet for your app.
This will copy `mock-sw.js` to your public directory.
### Mock Requests
Use `twd.mockRequest()` to define API mocks in your tests:
```ts
import { twd } from "twd-js";
// Initialize mocking when loading tests
// await twd.initRequestMocking();
it("fetches user data", async () => {
  // Mock the API request
  twd.mockRequest("getUser", {
    method: "GET",
    url: "https://api.example.com/user/123",
    response: {
      id: 123,
      name: "John Doe",
      email: "john@example.com"
    },
    status: 200,
    headers: { "Content-Type": "application/json" }
  });
  
  // Trigger the request in your app
  const button = await twd.get("button[data-testid='load-user']");
  await userEvent.click(button.el);
  
  // Wait for the mock to be called
  const rule = await twd.waitForRequest("getUser");
  console.log("Request body:", rule.request);
  
  // Clean up mocks after test
  twd.clearRequestMockRules();
});
```
### Wait for Requests
TWD provides utilities to wait for mocked requests:
```ts
// Wait for a single request
const rule = await twd.waitForRequest("getUserData");
// Wait for multiple requests
const rules = await twd.waitForRequests(["getUser", "getPosts"]);
// Access request data
console.log("Request body:", rule.request);
console.log("Response:", rule.response);
```
## API Reference
### Test Functions
| Function | Description | Example |
|----------|-------------|---------|
| `describe(name, fn)` | Groups related tests | `describe("User login", () => {...})` |
| `it(name, fn)` | Defines a test case | `it("should login", async () => {...})` |
| `itOnly(name, fn)` | Runs only this test | `itOnly("focused test", () => {...})` |
| `itSkip(name, fn)` | Skips this test | `itSkip("broken test", () => {...})` |
| `beforeEach(fn)` | Runs before each test | `beforeEach(() => {...})` |
### TWD Commands
| Command | Description | Example |
|---------|-------------|---------|
| `twd.get(selector)` | Select single element | `await twd.get("button")` |
| `twd.getAll(selector)` | Select multiple elements | `await twd.getAll(".item")` |
| `twd.visit(url)` | Navigate to URL | `twd.visit("/contact")` |
| `twd.wait(ms)` | Wait for specified time | `await twd.wait(500)` |
| `twd.url()` | Get URL API for assertions | `twd.url().should("contain.url", "/home")` |
### Assertions
#### Element Content
- `have.text` - Exact text match
- `contain.text` - Partial text match  
- `be.empty` - Element has no text content
#### Element Attributes
- `have.attr` - Has specific attribute value
- `have.value` - Input/textarea value
- `have.class` - Has CSS class
#### Element State
- `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
#### URL Assertions
- `eq` - Exact URL match
- `contain.url` - URL contains substring
All assertions can be negated with `not.` prefix (e.g., `not.be.disabled`).
## Examples
### Basic Form Testing
```ts
import { describe, it, twd, userEvent } from "twd-js";
describe("Contact form", () => {
  it("submits form data", async () => {
    twd.visit("/contact");
    
    const user = userEvent.setup();
    const emailInput = await twd.get("input#email");
    const messageInput = await twd.get("textarea#message");
    const submitBtn = await twd.get("button[type='submit']");
    
    await user.type(emailInput.el, "test@example.com");
    await user.type(messageInput.el, "Hello world");
    
    emailInput.should("have.value", "test@example.com");
    messageInput.should("have.value", "Hello world");
    
    await user.click(submitBtn.el);
  });
});
```
### API Mocking with Authentication
```ts
import { describe, it, twd, userEvent } from "twd-js";
describe("Protected routes", () => {
  it("redirects to login when unauthorized", async () => {
    twd.visit("/dashboard");
    await twd.wait(100);
    twd.url().should("contain.url", "/login");
  });
  
  it("loads dashboard with valid session", async () => {
    // Mock authentication check
    twd.mockRequest("authCheck", {
      method: "GET", 
      url: "/api/auth/me",
      response: { id: 1, name: "John Doe" }
    });
    
    twd.visit("/dashboard");
    await twd.waitForRequest("authCheck");
    
    const welcome = await twd.get("h1");
    welcome.should("contain.text", "Welcome, John");
    
    twd.clearRequestMockRules();
  });
});
```
For more comprehensive examples, see the [examples](https://github.com/BRIKEV/twd/tree/main/examples) directory in the repository.
## Contributing
Contributions are welcome! Please open issues or pull requests on [GitHub](https://github.com/BRIKEV/twd).
## License
This project is licensed under the [MIT License](./LICENSE).