twd-test-writer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TWD Test Writing Guide

TWD测试编写指南

You are writing tests for TWD (Test While Developing), an in-browser testing library. Tests run in the browser (not Node.js) with a sidebar UI for instant visual feedback. Syntax is similar to Jest/Cypress but with key differences.
Key characteristics:
  • Designed for SPAs (React, Vue, Angular, Solid.js)
  • Not suitable for SSR-first architectures (Next.js App Router)
  • Uses Mock Service Worker (MSW) for API mocking
  • Uses
    @testing-library/dom
    for element queries
你正在为**TWD(开发中测试,Test While Developing)**编写测试,这是一个浏览器端测试库。测试在浏览器(而非Node.js)中运行,带有侧边栏UI可提供即时视觉反馈。语法与Jest/Cypress类似,但存在关键差异。
核心特性:
  • 专为SPA(React、Vue、Angular、Solid.js)设计
  • 不适用于SSR优先架构(如Next.js App Router)
  • 使用Mock Service Worker (MSW) 进行API模拟
  • 使用
    @testing-library/dom
    进行元素查询

Required Imports

必需的导入

Every TWD test file needs these exact imports:
typescript
import { twd, userEvent, screenDom } from "twd-js";
import { describe, it, beforeEach, afterEach, expect } from "twd-js/runner";
Package exports:
  • twd-js
    — Main API (
    twd
    ,
    userEvent
    ,
    screenDom
    ,
    screenDomGlobal
    )
  • twd-js/runner
    — Test functions (
    describe
    ,
    it
    ,
    beforeEach
    ,
    afterEach
    ,
    expect
    )
  • twd-js/ui
    — UI components (
    MockedComponent
    )
NEVER import
describe
,
it
,
beforeEach
,
expect
from Jest, Mocha, or other libraries. They MUST come from
twd-js/runner
.
每个TWD测试文件都需要以下确切导入:
typescript
import { twd, userEvent, screenDom } from "twd-js";
import { describe, it, beforeEach, afterEach, expect } from "twd-js/runner";
包导出内容:
  • twd-js
    — 主API(
    twd
    userEvent
    screenDom
    screenDomGlobal
  • twd-js/runner
    — 测试函数(
    describe
    it
    beforeEach
    afterEach
    expect
  • twd-js/ui
    — UI组件(
    MockedComponent
切勿从Jest、Mocha或其他库导入
describe
it
beforeEach
expect
。它们必须来自
twd-js/runner

File Naming

文件命名规则

Test files must follow:
*.twd.test.ts
or
*.twd.test.tsx
测试文件必须遵循:
*.twd.test.ts
*.twd.test.tsx

Core Rules

核心规则

Async/Await is Required

必须使用Async/Await

typescript
// twd.get() and twd.getAll() are async — ALWAYS await
const button = await twd.get("button");
const items = await twd.getAll(".item");

// userEvent methods are async — ALWAYS await
await userEvent.click(button.el);
await userEvent.type(input, "text");

// Test functions should be async
it("should do something", async () => { /* ... */ });
typescript
// twd.get() 和 twd.getAll() 是异步函数——必须使用await
const button = await twd.get("button");
const items = await twd.getAll(".item");

// userEvent方法是异步函数——必须使用await
await userEvent.click(button.el);
await userEvent.type(input, "text");

// 测试函数应为异步
it("should do something", async () => { /* ... */ });

Element Selection

元素选择

Preferred: Testing Library queries via
screenDom
typescript
// By role (most accessible — RECOMMENDED)
const button = screenDom.getByRole("button", { name: "Submit" });
const heading = screenDom.getByRole("heading", { name: "Welcome", level: 1 });

// By label (for form inputs)
const emailInput = screenDom.getByLabelText("Email Address");

// By text content
const message = screenDom.getByText("Success!");
const partial = screenDom.getByText(/welcome/i);

// By test ID
const card = screenDom.getByTestId("user-card");

// Query variants
screenDom.getByRole("button");        // Throws if not found
screenDom.queryByRole("button");      // Returns null if not found
await screenDom.findByRole("button"); // Waits for element (async)
screenDom.getAllByRole("button");     // Returns array
For modals/portals use
screenDomGlobal
:
typescript
import { screenDomGlobal } from "twd-js";
const modal = screenDomGlobal.getByRole("dialog");
Fallback: CSS selectors via
twd.get()
typescript
const button = await twd.get("button");
const byId = await twd.get("#email");
const byClass = await twd.get(".error-message");
const multiple = await twd.getAll(".item");
推荐方式:通过
screenDom
使用Testing Library查询
typescript
// 通过角色(最具可访问性——推荐)
const button = screenDom.getByRole("button", { name: "Submit" });
const heading = screenDom.getByRole("heading", { name: "Welcome", level: 1 });

// 通过标签(适用于表单输入)
const emailInput = screenDom.getByLabelText("Email Address");

// 通过文本内容
const message = screenDom.getByText("Success!");
const partial = screenDom.getByText(/welcome/i);

// 通过测试ID
const card = screenDom.getByTestId("user-card");

// 查询变体
screenDom.getByRole("button");        // 未找到时抛出错误
screenDom.queryByRole("button");      // 未找到时返回null
await screenDom.findByRole("button"); // 等待元素出现(异步)
screenDom.getAllByRole("button");     // 返回数组
对于模态框/门户使用
screenDomGlobal
typescript
import { screenDomGlobal } from "twd-js";
const modal = screenDomGlobal.getByRole("dialog");
备选方式:通过
twd.get()
使用CSS选择器
typescript
const button = await twd.get("button");
const byId = await twd.get("#email");
const byClass = await twd.get(".error-message");
const multiple = await twd.getAll(".item");

User Interactions

用户交互

typescript
const user = userEvent.setup();

// With screenDom elements (direct)
await user.click(screenDom.getByRole("button", { name: "Save" }));
await user.type(screenDom.getByLabelText("Email"), "hello@example.com");

// With twd.get() elements (use .el for raw DOM)
const twdButton = await twd.get(".save-btn");
await user.click(twdButton.el);

// Other interactions
await user.dblClick(element);
await user.clear(input);
await user.selectOptions(select, "option-value");
await user.keyboard("{Enter}");
typescript
const user = userEvent.setup();

// 直接使用screenDom元素
await user.click(screenDom.getByRole("button", { name: "Save" }));
await user.type(screenDom.getByLabelText("Email"), "hello@example.com");

// 使用twd.get()元素(需用.el获取原始DOM)
const twdButton = await twd.get(".save-btn");
await user.click(twdButton.el);

// 其他交互
await user.dblClick(element);
await user.clear(input);
await user.selectOptions(select, "option-value");
await user.keyboard("{Enter}");

Assertions

断言

Method style (on twd elements):
typescript
const element = await twd.get("h1");
element.should("have.text", "Welcome");
element.should("contain.text", "come");
element.should("be.visible");
element.should("not.be.visible");
element.should("have.class", "header");
element.should("have.value", "test@example.com");
element.should("have.attr", "type", "submit");
element.should("be.disabled");
element.should("be.enabled");
element.should("be.checked");
element.should("be.focused");
element.should("be.empty");
Function style (any element):
typescript
twd.should(screenDom.getByRole("button"), "be.visible");
twd.should(screenDom.getByRole("button"), "have.text", "Submit");
URL assertions:
typescript
await twd.url().should("eq", "http://localhost:3000/dashboard");
await twd.url().should("contain.url", "/dashboard");
Chai expect (for non-element assertions):
typescript
expect(array).to.have.length(3);
expect(value).to.equal("expected");
expect(obj).to.deep.equal({ key: "value" });
方法风格(针对twd元素):
typescript
const element = await twd.get("h1");
element.should("have.text", "Welcome");
element.should("contain.text", "come");
element.should("be.visible");
element.should("not.be.visible");
element.should("have.class", "header");
element.should("have.value", "test@example.com");
element.should("have.attr", "type", "submit");
element.should("be.disabled");
element.should("be.enabled");
element.should("be.checked");
element.should("be.focused");
element.should("be.empty");
函数风格(针对任何元素):
typescript
twd.should(screenDom.getByRole("button"), "be.visible");
twd.should(screenDom.getByRole("button"), "have.text", "Submit");
URL断言:
typescript
await twd.url().should("eq", "http://localhost:3000/dashboard");
await twd.url().should("contain.url", "/dashboard");
Chai expect(针对非元素断言):
typescript
expect(array).to.have.length(3);
expect(value).to.equal("expected");
expect(obj).to.deep.equal({ key: "value" });

Navigation and Waiting

导航与等待

typescript
await twd.visit("/");
await twd.visit("/login");
await twd.wait(1000); // Wait for time (ms)
await screenDom.findByText("Success!"); // Wait for element
await twd.notExists(".loading-spinner"); // Wait for element to NOT exist
typescript
await twd.visit("/");
await twd.visit("/login");
await twd.wait(1000); // 等待指定时间(毫秒)
await screenDom.findByText("Success!"); // 等待元素出现
await twd.notExists(".loading-spinner"); // 等待元素消失

API Mocking

API模拟

TWD uses Mock Service Worker. Always mock BEFORE the request fires.
typescript
// Mock GET request
await twd.mockRequest("getUser", {
  method: "GET",
  url: "/api/user/123",
  response: { id: 123, name: "John Doe" },
  status: 200,
});

// Mock POST request
await twd.mockRequest("createUser", {
  method: "POST",
  url: "/api/users",
  response: { id: 456, created: true },
  status: 201,
});

// URL patterns with regex
await twd.mockRequest("getUserById", {
  method: "GET",
  url: /\/api\/users\/\d+/,
  response: { id: 999, name: "Dynamic User" },
  urlRegex: true,
});

// Error responses
await twd.mockRequest("serverError", {
  method: "GET",
  url: "/api/data",
  response: { error: "Server error" },
  status: 500,
});

// Wait for request and inspect body
const rule = await twd.waitForRequest("submitForm");
expect(rule.request).to.deep.equal({ email: "test@example.com" });

// Wait for multiple requests
await twd.waitForRequests(["getUser", "getPosts"]);
TWD使用Mock Service Worker。务必在请求发送前进行模拟。
typescript
// 模拟GET请求
await twd.mockRequest("getUser", {
  method: "GET",
  url: "/api/user/123",
  response: { id: 123, name: "John Doe" },
  status: 200,
});

// 模拟POST请求
await twd.mockRequest("createUser", {
  method: "POST",
  url: "/api/users",
  response: { id: 456, created: true },
  status: 201,
});

// 使用正则表达式匹配URL
await twd.mockRequest("getUserById", {
  method: "GET",
  url: /\/api\/users\/\d+/,
  response: { id: 999, name: "Dynamic User" },
  urlRegex: true,
});

// 模拟错误响应
await twd.mockRequest("serverError", {
  method: "GET",
  url: "/api/data",
  response: { error: "Server error" },
  status: 500,
});

// 等待请求并检查请求体
const rule = await twd.waitForRequest("submitForm");
expect(rule.request).to.deep.equal({ email: "test@example.com" });

// 等待多个请求
await twd.waitForRequests(["getUser", "getPosts"]);

Component Mocking

组件模拟

tsx
// In your component — wrap with MockedComponent
import { MockedComponent } from "twd-js/ui";

function Dashboard() {
  return (
    <MockedComponent name="ExpensiveChart">
      <ExpensiveChart data={data} />
    </MockedComponent>
  );
}
typescript
// In your test
twd.mockComponent("ExpensiveChart", () => (
  <div data-testid="mock-chart">Mocked Chart</div>
));
tsx
// 在组件中——用MockedComponent包裹
import { MockedComponent } from "twd-js/ui";

function Dashboard() {
  return (
    <MockedComponent name="ExpensiveChart">
      <ExpensiveChart data={data} />
    </MockedComponent>
  );
}
typescript
// 在测试中
twd.mockComponent("ExpensiveChart", () => (
  <div data-testid="mock-chart">Mocked Chart</div>
));

Module Stubbing with Sinon

使用Sinon进行模块存根

Tests run in the browser. ESM named exports are IMMUTABLE and cannot be stubbed.
Solution: wrap hooks/services in objects with default export.
typescript
// hooks/useAuth.ts — CORRECT: stubbable
const useAuth = () => useAuth0();
export default { useAuth };

// hooks/useAuth.ts — WRONG: cannot be stubbed
export const useAuth = () => useAuth0();
typescript
// In test:
import Sinon from "sinon";
import authModule from "../hooks/useAuth";

Sinon.stub(authModule, "useAuth").returns({
  isAuthenticated: true,
  user: { name: "John" },
});
// Always Sinon.restore() in beforeEach
测试在浏览器中运行。ESM命名导出是不可变的无法被存根
解决方案:将钩子/服务包裹在默认导出的对象中。
typescript
// hooks/useAuth.ts — 正确:可被存根
const useAuth = () => useAuth0();
export default { useAuth };

// hooks/useAuth.ts — 错误:无法被存根
export const useAuth = () => useAuth0();
typescript
// 在测试中:
import Sinon from "sinon";
import authModule from "../hooks/useAuth";

Sinon.stub(authModule, "useAuth").returns({
  isAuthenticated: true,
  user: { name: "John" },
});
// 务必在beforeEach中调用Sinon.restore()

Standard Test Template

标准测试模板

typescript
import { twd, userEvent, screenDom } from "twd-js";
import { describe, it, beforeEach, expect } from "twd-js/runner";

describe("Feature Name", () => {
  beforeEach(() => {
    twd.clearRequestMockRules();
    twd.clearComponentMocks();
  });

  it("should display the page correctly", async () => {
    // 1. Setup mocks BEFORE visiting
    await twd.mockRequest("getData", {
      method: "GET",
      url: "/api/data",
      response: { items: [] },
      status: 200,
    });

    // 2. Navigate
    await twd.visit("/page");

    // 3. Wait for async operations
    await twd.waitForRequest("getData");

    // 4. Interact
    const user = userEvent.setup();
    const button = screenDom.getByRole("button", { name: "Load" });
    await user.click(button);

    // 5. Assert
    const message = await twd.get(".message");
    message.should("have.text", "No items found");
  });

  it.only("debug this test", async () => { /* Only this test runs */ });
  it.skip("skip this test", async () => { /* This test won't run */ });
});
typescript
import { twd, userEvent, screenDom } from "twd-js";
import { describe, it, beforeEach, expect } from "twd-js/runner";

describe("Feature Name", () => {
  beforeEach(() => {
    twd.clearRequestMockRules();
    twd.clearComponentMocks();
  });

  it("should display the page correctly", async () => {
    // 1. 在访问前设置模拟
    await twd.mockRequest("getData", {
      method: "GET",
      url: "/api/data",
      response: { items: [] },
      status: 200,
    });

    // 2. 导航到页面
    await twd.visit("/page");

    // 3. 等待异步操作完成
    await twd.waitForRequest("getData");

    // 4. 进行用户交互
    const user = userEvent.setup();
    const button = screenDom.getByRole("button", { name: "Load" });
    await user.click(button);

    // 5. 执行断言
    const message = await twd.get(".message");
    message.should("have.text", "No items found");
  });

  it.only("debug this test", async () => { /* 仅运行此测试 */ });
  it.skip("skip this test", async () => { /* 跳过此测试 */ });
});

Common Mistakes to AVOID

需避免的常见错误

  1. Forgetting
    await
    on
    twd.get()
    ,
    userEvent.*
    ,
    twd.visit()
    ,
    screenDom.findBy*
  2. Mocking AFTER visit — always mock before
    twd.visit()
    or the action triggering the request
  3. Not clearing mocks — always
    twd.clearRequestMockRules()
    and
    twd.clearComponentMocks()
    in
    beforeEach
  4. Using Node.js APIs — tests run in the browser, no
    fs
    ,
    path
    , etc.
  5. Importing from wrong package
    describe
    /
    it
    /
    beforeEach
    from
    twd-js/runner
    , NOT Jest/Mocha
  6. Using Cypress syntax — no
    cy.get()
    ,
    cy.visit()
    . Use
    twd.get()
    ,
    twd.visit()
  7. Stubbing named exports — ESM makes them immutable. Use the default-export object pattern
  8. Using global describe/it — always import from
    twd-js/runner
  1. 忘记使用
    await
    twd.get()
    userEvent.*
    twd.visit()
    screenDom.findBy*
    均为异步操作
  2. 在访问后才设置模拟:务必在
    twd.visit()
    或触发请求的操作前设置模拟
  3. 未清除模拟:务必在
    beforeEach
    中调用
    twd.clearRequestMockRules()
    twd.clearComponentMocks()
  4. 使用Node.js API:测试在浏览器中运行,无法使用
    fs
    path
    等Node.js API
  5. 从错误的包导入
    describe
    /
    it
    /
    beforeEach
    必须来自
    twd-js/runner
    ,而非Jest/Mocha
  6. 使用Cypress语法:请勿使用
    cy.get()
    cy.visit()
    ,应使用
    twd.get()
    twd.visit()
  7. 存根命名导出:ESM命名导出是不可变的,请使用默认导出的对象模式
  8. 使用全局的describe/it:务必从
    twd-js/runner
    导入