Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Install Playwright browsers
run: pnpm dlx playwright install chromium

- name: Build Rspack
run: |
pnpm build:rspack
Expand All @@ -50,3 +53,6 @@ jobs:
- name: Build Rsdoctor
run: |
pnpm build:rsdoctor

- name: Test Rstest
run: pnpm test:rstest
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ lib-cov

# Coverage directory used by tools like istanbul
coverage
!rstest/coverage/
*.lcov

# nyc test coverage
Expand Down
28 changes: 26 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,33 @@ This file provides guidance for AI coding agents working in this repository.

## Repository Overview

- This is a monorepo of example projects for the Rstack ecosystem: Rspack, Rsbuild, Rspress, Rsdoctor, and Rslib.
- This is a monorepo of example projects for the Rstack ecosystem: Rspack, Rsbuild, Rspress, Rsdoctor, Rslib, and Rstest.
- Package manager: `pnpm` (see `package.json#packageManager`).
- Workspace layout: `pnpm-workspace.yaml` includes `rspack/**`, `rsbuild/**`, `rslib/**`, `rspress/**`, `rsdoctor/**`.
- Workspace layout: `pnpm-workspace.yaml` includes `rspack/**`, `rsbuild/**`, `rslib/**`, `rspress/**`, `rsdoctor/**`, `rstest/**`.

## Core Purpose of Examples

**The primary goal of each example is to demonstrate "how a specific API achieves a specific effect through specific configuration".**

When creating or modifying examples:
- **Keep it minimal**: Only include code necessary to demonstrate the target feature/API
- **Avoid over-engineering**: Don't add complex business logic that distracts from the core demonstration
- **Focus on the tool, not the ecosystem**: For example, in a test runner example, focus on the test runner's APIs (mocking, assertions, configuration), not on complex DOM manipulation or third-party library integrations
- **One concept per example**: Each example should ideally demonstrate one main feature or configuration pattern
- **Clarity over completeness**: A simple, clear example is better than a comprehensive but confusing one

Example of good vs bad:
```
# Good: Demonstrates rstest's mocking API
- Simple mock function usage
- Clear before/after assertions
- Minimal setup code

# Bad: Demonstrates rstest's mocking API
- Complex React component with many interactions
- Detailed DOM testing with @testing-library
- Business logic mixed with test assertions
```

## Quick Start (Local)

Expand Down
4 changes: 3 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
"**/rspress/**",
"**/rsdoctor/**",
"**/rslib/**",
"**/rstest/**",
"!**/dist",
"!**/dist-*",
"!**/doc_build",
"!**/auto-imports.d.ts",
"!**/components.d.ts"
"!**/components.d.ts",
"!**/__snapshots__/**"
],
"ignoreUnknown": true
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
"build:rsdoctor": "pnpm --filter \"rsdoctor-*\" build",
"build:rspack": "pnpm --filter \"example-*\" build",
"test:rspack": "pnpm --filter \"example-*\" test",
"test:rstest": "pnpm --filter \"rstest-*\" test",
"build:rspress": "pnpm --filter \"rspress-*\" build",
"build:rslib": "pnpm --filter \"rslib-*\" build",
"prepare": "husky",
"sort-package-json": "npx sort-package-json \"rspack/*/package.json\" \"rsbuild/*/package.json\" \"rspress/*/package.json\" \"rsdoctor/*/package.json\" \"rslib/*/package.json\""
"sort-package-json": "npx sort-package-json \"rspack/*/package.json\" \"rsbuild/*/package.json\" \"rspress/*/package.json\" \"rsdoctor/*/package.json\" \"rslib/*/package.json\" \"rstest/*/package.json\""
},
"lint-staged": {
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
Expand Down
998 changes: 904 additions & 94 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ packages:
- "rslib/**"
- "rspress/**"
- "rsdoctor/**"
- "rstest/**"
- "!**/dist"
26 changes: 26 additions & 0 deletions rstest/browser-rsbuild-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "rstest-browser-rsbuild-react",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"test": "rstest run"
},
"devDependencies": {
"@rsbuild/core": "^1.4.2",
"@rsbuild/plugin-react": "^1.4.2",
"@rstest/adapter-rsbuild": "^0.1.0",
"@rstest/browser": "^0.7.9",
"@rstest/browser-react": "^0.7.9",
"@rstest/core": "^0.7.9",
"@testing-library/dom": "^10.4.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"playwright": "^1.57.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"typescript": "^5.8.3"
}
}
19 changes: 19 additions & 0 deletions rstest/browser-rsbuild-react/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import path from 'node:path';
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
plugins: [pluginReact()],
source: {
entry: {
index: './src/index.tsx',
},
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
},
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
},
},
});
11 changes: 11 additions & 0 deletions rstest/browser-rsbuild-react/rstest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { withRsbuildConfig } from '@rstest/adapter-rsbuild';
import { defineConfig, type ExtendConfigFn } from '@rstest/core';

export default defineConfig({
extends: withRsbuildConfig() as ExtendConfigFn,
browser: {
enabled: true,
browser: 'chromium',
port: 3012,
},
});
40 changes: 40 additions & 0 deletions rstest/browser-rsbuild-react/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState } from 'react';

export interface CounterProps {
initialValue?: number;
step?: number;
}

export function Counter({ initialValue = 0, step = 1 }: CounterProps) {
const [count, setCount] = useState(initialValue);

return (
<div data-testid="counter">
<button
type="button"
onClick={() => setCount((c) => c - step)}
aria-label="Decrement"
data-testid="decrement-btn"
>
-
</button>
<span data-testid="counter-value">{count}</span>
<button
type="button"
onClick={() => setCount((c) => c + step)}
aria-label="Increment"
data-testid="increment-btn"
>
+
</button>
<button
type="button"
onClick={() => setCount(initialValue)}
aria-label="Reset"
data-testid="reset-btn"
>
Reset
</button>
</div>
);
}
25 changes: 25 additions & 0 deletions rstest/browser-rsbuild-react/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useCallback, useEffect, useRef, useState } from 'react';

export function useCounter(initialValue = 0, step = 1) {
const [count, setCount] = useState(initialValue);

const increment = useCallback(() => setCount((c) => c + step), [step]);
const decrement = useCallback(() => setCount((c) => c - step), [step]);
const reset = useCallback(() => setCount(initialValue), [initialValue]);

return { count, increment, decrement, reset };
}

export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((v) => !v), []);
return { value, toggle };
}

export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
13 changes: 13 additions & 0 deletions rstest/browser-rsbuild-react/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Counter } from './components/Counter';

const root = document.getElementById('root');
if (root) {
createRoot(root).render(
<StrictMode>
<h1>React Counter App</h1>
<Counter />
</StrictMode>,
);
}
30 changes: 30 additions & 0 deletions rstest/browser-rsbuild-react/tests/adapter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Use @components alias inherited from rsbuild.config.ts
import { Counter } from '@components/Counter';
import { cleanup, render } from '@rstest/browser-react';
import { afterEach, describe, expect, it } from '@rstest/core';
import { getByTestId } from '@testing-library/dom';

declare const __APP_VERSION__: string;

describe('withRsbuildConfig - alias inheritance', () => {
afterEach(() => {
cleanup();
});

it('should resolve @components alias from rsbuild.config.ts', async () => {
const { container } = await render(<Counter initialValue={5} />);
expect(getByTestId(container, 'counter-value').textContent).toBe('5');
});

it('should resolve @/ alias from rsbuild.config.ts', async () => {
// Dynamic import using @/ alias
const { useCounter } = await import('@/hooks');
expect(typeof useCounter).toBe('function');
});
});

describe('withRsbuildConfig - define inheritance', () => {
it('should inherit __APP_VERSION__ from rsbuild.config.ts', () => {
expect(__APP_VERSION__).toBe('1.0.0');
});
});
33 changes: 33 additions & 0 deletions rstest/browser-rsbuild-react/tests/counter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { act, cleanup, render } from '@rstest/browser-react';
import { afterEach, describe, expect, it } from '@rstest/core';
import { getByTestId } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { Counter } from '../src/components/Counter';

describe('render - Component Testing', () => {
afterEach(() => {
cleanup();
});

it('should render with initial value', async () => {
const { container } = await render(<Counter initialValue={10} />);
expect(getByTestId(container, 'counter-value').textContent).toBe('10');
});

it('should increment on button click', async () => {
const user = userEvent.setup();
const { container } = await render(<Counter />);

await act(() => user.click(getByTestId(container, 'increment-btn')));

expect(getByTestId(container, 'counter-value').textContent).toBe('1');
});

it('should clean up on unmount', async () => {
const { container, unmount } = await render(<Counter />);
expect(container.querySelector('[data-testid="counter"]')).not.toBeNull();

await unmount();
expect(container.innerHTML).toBe('');
});
});
55 changes: 55 additions & 0 deletions rstest/browser-rsbuild-react/tests/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { act, cleanup, renderHook } from '@rstest/browser-react';
import { afterEach, describe, expect, it } from '@rstest/core';
import { useCounter, usePrevious, useToggle } from '../src/hooks';

describe('renderHook - useCounter', () => {
afterEach(() => {
cleanup();
});

it('should initialize with value', async () => {
const { result } = await renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});

it('should increment with act', async () => {
const { result } = await renderHook(() => useCounter(0));

await act(() => result.current.increment());

expect(result.current.count).toBe(1);
});
});

describe('renderHook - useToggle', () => {
afterEach(() => {
cleanup();
});

it('should toggle value', async () => {
const { result } = await renderHook(() => useToggle(false));

await act(() => result.current.toggle());
expect(result.current.value).toBe(true);

await act(() => result.current.toggle());
expect(result.current.value).toBe(false);
});
});

describe('renderHook - usePrevious', () => {
afterEach(() => {
cleanup();
});

it('should return previous value after rerender', async () => {
let value = 1;
const { result, rerender } = await renderHook(() => usePrevious(value));

expect(result.current).toBeUndefined();

value = 2;
await rerender();
expect(result.current).toBe(1);
});
});
18 changes: 18 additions & 0 deletions rstest/browser-rsbuild-react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"types": ["@rstest/core"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
},
"include": ["src", "tests", "rstest.config.ts", "rsbuild.config.ts"]
}
18 changes: 18 additions & 0 deletions rstest/browser-rsbuild-vanilla/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "rstest-browser-rsbuild-vanilla",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"test": "rstest run"
},
"devDependencies": {
"@rsbuild/core": "^1.7.1",
"@rstest/browser": "^0.7.9",
"@rstest/core": "^0.7.9",
"playwright": "^1.57.0",
"typescript": "^5.8.3"
}
}
Loading