red64-cli 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive testing conventions for Next.js with Vitest, React Testing Library, Playwright, and MSW.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Fast feedback**: Unit tests run in milliseconds, no I/O
|
|
10
|
+
- **Test behavior, not implementation**: Assert what the user sees and does, not internal state
|
|
11
|
+
- **Minimal during development**: Focus on critical paths first; add edge cases in dedicated testing phases
|
|
12
|
+
- **Realistic integration**: Use MSW to mock APIs, not implementation details
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Test Organization
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
lib/
|
|
21
|
+
__tests__/
|
|
22
|
+
format-date.test.ts
|
|
23
|
+
user-service.test.ts
|
|
24
|
+
components/
|
|
25
|
+
__tests__/
|
|
26
|
+
user-card.test.tsx
|
|
27
|
+
login-form.test.tsx
|
|
28
|
+
actions/
|
|
29
|
+
__tests__/
|
|
30
|
+
user-actions.test.ts
|
|
31
|
+
tests/
|
|
32
|
+
e2e/
|
|
33
|
+
auth.spec.ts
|
|
34
|
+
dashboard.spec.ts
|
|
35
|
+
fixtures/
|
|
36
|
+
users.ts
|
|
37
|
+
helpers/
|
|
38
|
+
setup.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Pattern**: Colocate unit tests with source using `__tests__/` directories. Place E2E tests in the project root `tests/e2e/`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Vitest Configuration
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// vitest.config.ts
|
|
49
|
+
import { defineConfig } from "vitest/config";
|
|
50
|
+
import react from "@vitejs/plugin-react";
|
|
51
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
plugins: [react(), tsconfigPaths()],
|
|
55
|
+
test: {
|
|
56
|
+
environment: "jsdom",
|
|
57
|
+
globals: true,
|
|
58
|
+
setupFiles: ["./tests/helpers/setup.ts"],
|
|
59
|
+
include: ["src/**/*.test.{ts,tsx}"],
|
|
60
|
+
coverage: {
|
|
61
|
+
provider: "v8",
|
|
62
|
+
include: ["src/**"],
|
|
63
|
+
exclude: ["src/**/*.d.ts", "src/**/__tests__/**"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Setup File
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// tests/helpers/setup.ts
|
|
73
|
+
import "@testing-library/jest-dom/vitest";
|
|
74
|
+
import { cleanup } from "@testing-library/react";
|
|
75
|
+
import { afterEach } from "vitest";
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
cleanup();
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Unit Testing
|
|
85
|
+
|
|
86
|
+
### Pure Functions
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// lib/__tests__/format-date.test.ts
|
|
90
|
+
import { describe, it, expect } from "vitest";
|
|
91
|
+
import { formatRelativeDate } from "../format-date";
|
|
92
|
+
|
|
93
|
+
describe("formatRelativeDate", () => {
|
|
94
|
+
it("returns 'just now' for current time", () => {
|
|
95
|
+
expect(formatRelativeDate(new Date())).toBe("just now");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns '2 hours ago' for two hours past", () => {
|
|
99
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
|
100
|
+
expect(formatRelativeDate(twoHoursAgo)).toBe("2 hours ago");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns absolute date for dates older than 7 days", () => {
|
|
104
|
+
const oldDate = new Date("2023-01-15");
|
|
105
|
+
expect(formatRelativeDate(oldDate)).toMatch(/Jan 15, 2023/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Service Functions
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// lib/__tests__/user-service.test.ts
|
|
114
|
+
import { describe, it, expect, vi } from "vitest";
|
|
115
|
+
import { createUser } from "../services/user-service";
|
|
116
|
+
|
|
117
|
+
// Mock Prisma
|
|
118
|
+
vi.mock("@/lib/prisma", () => ({
|
|
119
|
+
prisma: {
|
|
120
|
+
user: {
|
|
121
|
+
findUnique: vi.fn(),
|
|
122
|
+
create: vi.fn(),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
import { prisma } from "@/lib/prisma";
|
|
128
|
+
|
|
129
|
+
describe("createUser", () => {
|
|
130
|
+
it("creates a user when email is not taken", async () => {
|
|
131
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
|
|
132
|
+
vi.mocked(prisma.user.create).mockResolvedValue({
|
|
133
|
+
id: "1",
|
|
134
|
+
email: "jane@example.com",
|
|
135
|
+
name: "Jane",
|
|
136
|
+
} as any);
|
|
137
|
+
|
|
138
|
+
const user = await createUser({ email: "jane@example.com", name: "Jane" });
|
|
139
|
+
|
|
140
|
+
expect(user.email).toBe("jane@example.com");
|
|
141
|
+
expect(prisma.user.create).toHaveBeenCalledWith({
|
|
142
|
+
data: { email: "jane@example.com", name: "Jane" },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("throws ConflictError when email exists", async () => {
|
|
147
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: "1" } as any);
|
|
148
|
+
|
|
149
|
+
await expect(
|
|
150
|
+
createUser({ email: "taken@example.com", name: "Test" })
|
|
151
|
+
).rejects.toThrow("already exists");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Component Testing
|
|
159
|
+
|
|
160
|
+
### React Testing Library
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// components/__tests__/user-card.test.tsx
|
|
164
|
+
import { render, screen } from "@testing-library/react";
|
|
165
|
+
import { describe, it, expect } from "vitest";
|
|
166
|
+
import { UserCard } from "../user-card";
|
|
167
|
+
|
|
168
|
+
describe("UserCard", () => {
|
|
169
|
+
const user = {
|
|
170
|
+
id: "1",
|
|
171
|
+
name: "Jane Doe",
|
|
172
|
+
email: "jane@example.com",
|
|
173
|
+
avatarUrl: null,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
it("renders user name and email", () => {
|
|
177
|
+
render(<UserCard user={user} />);
|
|
178
|
+
|
|
179
|
+
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByText("jane@example.com")).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("hides email when showEmail is false", () => {
|
|
184
|
+
render(<UserCard user={user} showEmail={false} />);
|
|
185
|
+
|
|
186
|
+
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
|
|
187
|
+
expect(screen.queryByText("jane@example.com")).not.toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("renders initials when no avatar URL", () => {
|
|
191
|
+
render(<UserCard user={user} />);
|
|
192
|
+
|
|
193
|
+
expect(screen.getByText("JD")).toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Testing Interactions
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// components/__tests__/login-form.test.tsx
|
|
202
|
+
import { render, screen } from "@testing-library/react";
|
|
203
|
+
import userEvent from "@testing-library/user-event";
|
|
204
|
+
import { describe, it, expect, vi } from "vitest";
|
|
205
|
+
|
|
206
|
+
describe("LoginForm", () => {
|
|
207
|
+
it("submits form with email and password", async () => {
|
|
208
|
+
const user = userEvent.setup();
|
|
209
|
+
const onSubmit = vi.fn();
|
|
210
|
+
|
|
211
|
+
render(<LoginForm onSubmit={onSubmit} />);
|
|
212
|
+
|
|
213
|
+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
|
|
214
|
+
await user.type(screen.getByLabelText("Password"), "secret123");
|
|
215
|
+
await user.click(screen.getByRole("button", { name: "Sign in" }));
|
|
216
|
+
|
|
217
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
218
|
+
email: "jane@example.com",
|
|
219
|
+
password: "secret123",
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("shows validation error for invalid email", async () => {
|
|
224
|
+
const user = userEvent.setup();
|
|
225
|
+
|
|
226
|
+
render(<LoginForm onSubmit={vi.fn()} />);
|
|
227
|
+
|
|
228
|
+
await user.type(screen.getByLabelText("Email"), "not-an-email");
|
|
229
|
+
await user.click(screen.getByRole("button", { name: "Sign in" }));
|
|
230
|
+
|
|
231
|
+
expect(screen.getByText("Invalid email address")).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("disables submit button while pending", async () => {
|
|
235
|
+
const user = userEvent.setup();
|
|
236
|
+
const onSubmit = vi.fn(() => new Promise(() => {})); // Never resolves
|
|
237
|
+
|
|
238
|
+
render(<LoginForm onSubmit={onSubmit} />);
|
|
239
|
+
|
|
240
|
+
await user.type(screen.getByLabelText("Email"), "jane@example.com");
|
|
241
|
+
await user.type(screen.getByLabelText("Password"), "secret123");
|
|
242
|
+
await user.click(screen.getByRole("button", { name: "Sign in" }));
|
|
243
|
+
|
|
244
|
+
expect(screen.getByRole("button", { name: /signing in/i })).toBeDisabled();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## API Mocking with MSW
|
|
252
|
+
|
|
253
|
+
### Setup
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// tests/helpers/mocks/handlers.ts
|
|
257
|
+
import { http, HttpResponse } from "msw";
|
|
258
|
+
|
|
259
|
+
export const handlers = [
|
|
260
|
+
http.get("/api/users", () => {
|
|
261
|
+
return HttpResponse.json({
|
|
262
|
+
items: [
|
|
263
|
+
{ id: "1", name: "Jane", email: "jane@example.com" },
|
|
264
|
+
{ id: "2", name: "John", email: "john@example.com" },
|
|
265
|
+
],
|
|
266
|
+
total: 2,
|
|
267
|
+
page: 1,
|
|
268
|
+
limit: 20,
|
|
269
|
+
totalPages: 1,
|
|
270
|
+
});
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
http.post("/api/users", async ({ request }) => {
|
|
274
|
+
const body = await request.json();
|
|
275
|
+
return HttpResponse.json(
|
|
276
|
+
{ id: "3", ...body, createdAt: new Date().toISOString() },
|
|
277
|
+
{ status: 201 }
|
|
278
|
+
);
|
|
279
|
+
}),
|
|
280
|
+
];
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// tests/helpers/mocks/server.ts
|
|
285
|
+
import { setupServer } from "msw/node";
|
|
286
|
+
import { handlers } from "./handlers";
|
|
287
|
+
|
|
288
|
+
export const server = setupServer(...handlers);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// tests/helpers/setup.ts (add MSW)
|
|
293
|
+
import { server } from "./mocks/server";
|
|
294
|
+
|
|
295
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
296
|
+
afterEach(() => server.resetHandlers());
|
|
297
|
+
afterAll(() => server.close());
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Override Handlers in Tests
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { http, HttpResponse } from "msw";
|
|
304
|
+
import { server } from "../../tests/helpers/mocks/server";
|
|
305
|
+
|
|
306
|
+
it("shows error when API fails", async () => {
|
|
307
|
+
server.use(
|
|
308
|
+
http.get("/api/users", () => {
|
|
309
|
+
return HttpResponse.json(
|
|
310
|
+
{ error: { code: "INTERNAL_ERROR", message: "Server error" } },
|
|
311
|
+
{ status: 500 }
|
|
312
|
+
);
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
render(<UserList />);
|
|
317
|
+
|
|
318
|
+
await screen.findByText("Failed to load users");
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## E2E Testing with Playwright
|
|
325
|
+
|
|
326
|
+
### Configuration
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// playwright.config.ts
|
|
330
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
331
|
+
|
|
332
|
+
export default defineConfig({
|
|
333
|
+
testDir: "./tests/e2e",
|
|
334
|
+
fullyParallel: true,
|
|
335
|
+
forbidOnly: !!process.env.CI,
|
|
336
|
+
retries: process.env.CI ? 2 : 0,
|
|
337
|
+
workers: process.env.CI ? 1 : undefined,
|
|
338
|
+
reporter: "html",
|
|
339
|
+
use: {
|
|
340
|
+
baseURL: "http://localhost:3000",
|
|
341
|
+
trace: "on-first-retry",
|
|
342
|
+
},
|
|
343
|
+
projects: [
|
|
344
|
+
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
|
345
|
+
{ name: "mobile", use: { ...devices["iPhone 14"] } },
|
|
346
|
+
],
|
|
347
|
+
webServer: {
|
|
348
|
+
command: "pnpm dev",
|
|
349
|
+
url: "http://localhost:3000",
|
|
350
|
+
reuseExistingServer: !process.env.CI,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### E2E Test Pattern
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// tests/e2e/auth.spec.ts
|
|
359
|
+
import { test, expect } from "@playwright/test";
|
|
360
|
+
|
|
361
|
+
test.describe("Authentication", () => {
|
|
362
|
+
test("user can sign in and see dashboard", async ({ page }) => {
|
|
363
|
+
await page.goto("/login");
|
|
364
|
+
|
|
365
|
+
await page.getByLabel("Email").fill("admin@example.com");
|
|
366
|
+
await page.getByLabel("Password").fill("password123");
|
|
367
|
+
await page.getByRole("button", { name: "Sign in" }).click();
|
|
368
|
+
|
|
369
|
+
await expect(page).toHaveURL("/dashboard");
|
|
370
|
+
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("shows error for invalid credentials", async ({ page }) => {
|
|
374
|
+
await page.goto("/login");
|
|
375
|
+
|
|
376
|
+
await page.getByLabel("Email").fill("wrong@example.com");
|
|
377
|
+
await page.getByLabel("Password").fill("wrongpassword");
|
|
378
|
+
await page.getByRole("button", { name: "Sign in" }).click();
|
|
379
|
+
|
|
380
|
+
await expect(page.getByText("Invalid credentials")).toBeVisible();
|
|
381
|
+
await expect(page).toHaveURL("/login");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Page Object Pattern
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// tests/e2e/pages/login-page.ts
|
|
390
|
+
import { type Page, type Locator } from "@playwright/test";
|
|
391
|
+
|
|
392
|
+
export class LoginPage {
|
|
393
|
+
readonly emailInput: Locator;
|
|
394
|
+
readonly passwordInput: Locator;
|
|
395
|
+
readonly submitButton: Locator;
|
|
396
|
+
|
|
397
|
+
constructor(private page: Page) {
|
|
398
|
+
this.emailInput = page.getByLabel("Email");
|
|
399
|
+
this.passwordInput = page.getByLabel("Password");
|
|
400
|
+
this.submitButton = page.getByRole("button", { name: "Sign in" });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async goto() {
|
|
404
|
+
await this.page.goto("/login");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async login(email: string, password: string) {
|
|
408
|
+
await this.emailInput.fill(email);
|
|
409
|
+
await this.passwordInput.fill(password);
|
|
410
|
+
await this.submitButton.click();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Test Fixtures
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// tests/fixtures/users.ts
|
|
421
|
+
export const testUsers = {
|
|
422
|
+
admin: {
|
|
423
|
+
id: "user-admin",
|
|
424
|
+
email: "admin@example.com",
|
|
425
|
+
name: "Admin User",
|
|
426
|
+
role: "ADMIN" as const,
|
|
427
|
+
},
|
|
428
|
+
member: {
|
|
429
|
+
id: "user-member",
|
|
430
|
+
email: "member@example.com",
|
|
431
|
+
name: "Regular User",
|
|
432
|
+
role: "MEMBER" as const,
|
|
433
|
+
},
|
|
434
|
+
} as const;
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Test Commands
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
# Unit tests (fast feedback)
|
|
443
|
+
pnpm vitest run # Run once
|
|
444
|
+
pnpm vitest # Watch mode
|
|
445
|
+
pnpm vitest run src/lib/__tests__/ # Specific directory
|
|
446
|
+
|
|
447
|
+
# Coverage
|
|
448
|
+
pnpm vitest run --coverage
|
|
449
|
+
|
|
450
|
+
# E2E tests
|
|
451
|
+
pnpm playwright test # All browsers
|
|
452
|
+
pnpm playwright test --project=chromium # Single browser
|
|
453
|
+
pnpm playwright test tests/e2e/auth.spec.ts # Single file
|
|
454
|
+
pnpm playwright test --ui # Interactive UI mode
|
|
455
|
+
|
|
456
|
+
# CI pipeline
|
|
457
|
+
pnpm vitest run && pnpm playwright test
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Anti-Patterns
|
|
463
|
+
|
|
464
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
465
|
+
|---|---|---|
|
|
466
|
+
| Testing implementation details | Brittle, breaks on refactor | Test behavior: what the user sees and does |
|
|
467
|
+
| Snapshot tests for everything | Meaningless diffs, rubber-stamp approvals | Use snapshots sparingly; prefer explicit assertions |
|
|
468
|
+
| No MSW for API calls | Tests depend on real API, flaky | Mock at the network level with MSW |
|
|
469
|
+
| Slow unit tests (> 100ms each) | Developers stop running them | Mock I/O, no database in unit tests |
|
|
470
|
+
| Testing third-party libraries | Wasted effort | Trust the library; test your integration |
|
|
471
|
+
| No E2E for critical paths | Bugs in user flows slip through | E2E test login, signup, and primary workflows |
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
_Tests document behavior. Write them for the critical paths first, keep them fast, and assert what the user experiences._
|