@mars-stack/cli 0.2.0 → 0.2.2

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.
Files changed (173) hide show
  1. package/package.json +2 -2
  2. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  3. package/template/.cursor/rules/data-access.mdc +29 -0
  4. package/template/.cursor/rules/project-structure.mdc +34 -0
  5. package/template/.cursor/rules/security.mdc +25 -0
  6. package/template/.cursor/rules/testing.mdc +24 -0
  7. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  8. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  9. package/template/.cursor/skills/add-audit-log/SKILL.md +373 -0
  10. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  11. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  12. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  13. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  14. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  15. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  16. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  17. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  18. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  19. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  20. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  21. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  22. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  23. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  24. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  25. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  26. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  27. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  28. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  29. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  30. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  31. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  32. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  33. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  34. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  35. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  36. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  37. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  38. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  39. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  40. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  41. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  42. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  43. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  44. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  45. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  46. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  47. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  48. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  49. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  50. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  51. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  52. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  53. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  54. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  55. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  56. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  57. package/template/AGENTS.md +104 -0
  58. package/template/ARCHITECTURE.md +102 -0
  59. package/template/docs/QUALITY_SCORE.md +20 -0
  60. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  61. package/template/docs/design-docs/core-beliefs.md +43 -0
  62. package/template/docs/design-docs/index.md +8 -0
  63. package/template/docs/exec-plans/active/.gitkeep +0 -0
  64. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  65. package/template/docs/exec-plans/tech-debt.md +7 -0
  66. package/template/docs/generated/.gitkeep +0 -0
  67. package/template/docs/product-specs/index.md +7 -0
  68. package/template/docs/references/index.md +18 -0
  69. package/template/e2e/api.spec.ts +20 -0
  70. package/template/e2e/auth.spec.ts +24 -0
  71. package/template/e2e/public.spec.ts +25 -0
  72. package/template/eslint.config.mjs +24 -0
  73. package/template/next-env.d.ts +6 -0
  74. package/template/next.config.ts +45 -0
  75. package/template/package.json +80 -0
  76. package/template/playwright.config.ts +31 -0
  77. package/template/postcss.config.mjs +8 -0
  78. package/template/prisma/generated/prisma/browser.ts +49 -0
  79. package/template/prisma/generated/prisma/client.ts +73 -0
  80. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  81. package/template/prisma/generated/prisma/enums.ts +15 -0
  82. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  83. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  84. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  85. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  86. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  87. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  88. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  89. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  90. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  91. package/template/prisma/generated/prisma/models.ts +17 -0
  92. package/template/prisma/schema/auth.prisma +69 -0
  93. package/template/prisma/schema/base.prisma +8 -0
  94. package/template/prisma/schema/file.prisma +15 -0
  95. package/template/prisma/schema/subscription.prisma +17 -0
  96. package/template/prisma.config.ts +13 -0
  97. package/template/scripts/check-architecture.ts +221 -0
  98. package/template/scripts/check-doc-freshness.ts +242 -0
  99. package/template/scripts/ensure-db.mjs +291 -0
  100. package/template/scripts/generate-docs.ts +143 -0
  101. package/template/scripts/generate-env-example.ts +89 -0
  102. package/template/scripts/seed.ts +56 -0
  103. package/template/scripts/update-quality-score.ts +263 -0
  104. package/template/src/__tests__/architecture.test.ts +114 -0
  105. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  106. package/template/src/app/(auth)/layout.tsx +11 -0
  107. package/template/src/app/(auth)/register/page.tsx +162 -0
  108. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  109. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  110. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  111. package/template/src/app/(auth)/verify/page.tsx +56 -0
  112. package/template/src/app/(protected)/admin/page.tsx +108 -0
  113. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  114. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  115. package/template/src/app/(protected)/layout.tsx +262 -0
  116. package/template/src/app/(protected)/settings/page.tsx +370 -0
  117. package/template/src/app/api/auth/forgot/route.ts +63 -0
  118. package/template/src/app/api/auth/login/route.ts +121 -0
  119. package/template/src/app/api/auth/logout/route.ts +19 -0
  120. package/template/src/app/api/auth/me/route.ts +30 -0
  121. package/template/src/app/api/auth/reset/route.ts +45 -0
  122. package/template/src/app/api/auth/signup/route.ts +85 -0
  123. package/template/src/app/api/auth/verify/route.ts +46 -0
  124. package/template/src/app/api/csrf/route.ts +12 -0
  125. package/template/src/app/api/health/route.ts +10 -0
  126. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  127. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  128. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  129. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  130. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  131. package/template/src/app/api/protected/user/password/route.ts +63 -0
  132. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  133. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  134. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  135. package/template/src/app/api/readiness/route.ts +15 -0
  136. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  137. package/template/src/app/error.tsx +33 -0
  138. package/template/src/app/layout.tsx +29 -0
  139. package/template/src/app/not-found.tsx +20 -0
  140. package/template/src/app/page.tsx +136 -0
  141. package/template/src/app/privacy/page.tsx +178 -0
  142. package/template/src/app/providers.tsx +8 -0
  143. package/template/src/app/terms/page.tsx +139 -0
  144. package/template/src/config/app.config.ts +70 -0
  145. package/template/src/config/routes.ts +17 -0
  146. package/template/src/features/admin/index.ts +11 -0
  147. package/template/src/features/admin/permissions.ts +64 -0
  148. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  149. package/template/src/features/auth/context/index.ts +2 -0
  150. package/template/src/features/auth/index.ts +3 -0
  151. package/template/src/features/auth/server/consent.ts +66 -0
  152. package/template/src/features/auth/server/session-revocation.ts +20 -0
  153. package/template/src/features/auth/server/sessions.ts +66 -0
  154. package/template/src/features/auth/server/user.ts +166 -0
  155. package/template/src/features/auth/types.ts +19 -0
  156. package/template/src/features/auth/validators.ts +29 -0
  157. package/template/src/features/billing/server/index.ts +66 -0
  158. package/template/src/features/billing/types.ts +43 -0
  159. package/template/src/features/uploads/server/index.ts +49 -0
  160. package/template/src/features/uploads/types.ts +26 -0
  161. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  162. package/template/src/lib/core/email/templates/index.ts +4 -0
  163. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  164. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  165. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  166. package/template/src/lib/mars.ts +56 -0
  167. package/template/src/lib/prisma.ts +19 -0
  168. package/template/src/proxy.ts +92 -0
  169. package/template/src/styles/brand.css +17 -0
  170. package/template/src/styles/globals.css +6 -0
  171. package/template/tsconfig.json +59 -0
  172. package/template/vitest.config.ts +41 -0
  173. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,221 @@
1
+ # Skill: Add CRUD Routes
2
+
3
+ Generate a complete set of CRUD API routes for a resource, following MARS conventions for authentication, validation, ownership, and error handling.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to create endpoints for listing, creating, reading, updating, or deleting a resource.
8
+
9
+ ## Prerequisites
10
+
11
+ - A Prisma model exists for the resource (see `add-prisma-model` skill).
12
+ - Server access functions exist in `src/features/<name>/server/`.
13
+ - Zod validation schemas exist in `src/features/<name>/validation/schemas.ts`.
14
+
15
+ ## Generated Structure
16
+
17
+ ```
18
+ src/app/api/protected/<resource>/
19
+ ├── route.ts # GET (list) + POST (create)
20
+ └── [id]/
21
+ └── route.ts # GET (one) + PATCH (update) + DELETE
22
+ ```
23
+
24
+ ## Step 1: Collection Route (`route.ts`)
25
+
26
+ ```typescript
27
+ import { handleApiError, withAuthNoParams, type AuthenticatedRequest } from '@/lib/mars';
28
+ import { findAllForUser, createForUser } from '@/features/<resource>/server';
29
+ import { resourceSchemas } from '@/features/<resource>/validation/schemas';
30
+ import { NextResponse } from 'next/server';
31
+
32
+ export const GET = withAuthNoParams(async (request: AuthenticatedRequest) => {
33
+ try {
34
+ const items = await findAllForUser(request.session.userId);
35
+ return NextResponse.json(items);
36
+ } catch (error) {
37
+ return handleApiError(error, { endpoint: '/api/protected/<resource>' });
38
+ }
39
+ });
40
+
41
+ export const POST = withAuthNoParams(async (request: AuthenticatedRequest) => {
42
+ try {
43
+ const body = resourceSchemas.create.parse(await request.json());
44
+ const item = await createForUser(request.session.userId, body);
45
+ return NextResponse.json(item, { status: 201 });
46
+ } catch (error) {
47
+ return handleApiError(error, { endpoint: '/api/protected/<resource>' });
48
+ }
49
+ });
50
+ ```
51
+
52
+ ## Step 2: Item Route (`[id]/route.ts`)
53
+
54
+ ```typescript
55
+ import { handleApiError, withOwnership, withAuth, type AuthenticatedRequest } from '@/lib/mars';
56
+ import { findById, updateById, deleteById } from '@/features/<resource>/server';
57
+ import { resourceSchemas } from '@/features/<resource>/validation/schemas';
58
+ import { NextResponse } from 'next/server';
59
+
60
+ async function getResourceOwner(
61
+ _request: AuthenticatedRequest,
62
+ context: { params: Promise<{ [key: string]: string }> },
63
+ ): Promise<string | null> {
64
+ const { id } = await context.params;
65
+ const item = await findById(id);
66
+ return item?.userId ?? null;
67
+ }
68
+
69
+ export const GET = withOwnership(getResourceOwner, async (request, context) => {
70
+ try {
71
+ const { id } = await context.params;
72
+ const item = await findById(id);
73
+ return NextResponse.json(item);
74
+ } catch (error) {
75
+ return handleApiError(error, { endpoint: '/api/protected/<resource>/[id]' });
76
+ }
77
+ });
78
+
79
+ export const PATCH = withOwnership(getResourceOwner, async (request, context) => {
80
+ try {
81
+ const { id } = await context.params;
82
+ const body = resourceSchemas.update.parse(await request.json());
83
+ const item = await updateById(id, body);
84
+ return NextResponse.json(item);
85
+ } catch (error) {
86
+ return handleApiError(error, { endpoint: '/api/protected/<resource>/[id]' });
87
+ }
88
+ });
89
+
90
+ export const DELETE = withOwnership(getResourceOwner, async (_request, context) => {
91
+ try {
92
+ const { id } = await context.params;
93
+ await deleteById(id);
94
+ return NextResponse.json({ message: 'Deleted' });
95
+ } catch (error) {
96
+ return handleApiError(error, { endpoint: '/api/protected/<resource>/[id]' });
97
+ }
98
+ });
99
+ ```
100
+
101
+ ## Step 3: Server Access Functions
102
+
103
+ ```typescript
104
+ import 'server-only';
105
+
106
+ import { prisma } from '@/lib/prisma';
107
+ import type { CreateInput, UpdateInput } from '../validation/schemas';
108
+
109
+ export async function findAllForUser(userId: string) {
110
+ return prisma.<resource>.findMany({
111
+ where: { userId },
112
+ orderBy: { createdAt: 'desc' },
113
+ });
114
+ }
115
+
116
+ export async function findById(id: string) {
117
+ return prisma.<resource>.findUnique({ where: { id } });
118
+ }
119
+
120
+ export async function createForUser(userId: string, data: CreateInput) {
121
+ return prisma.<resource>.create({
122
+ data: { ...data, userId },
123
+ });
124
+ }
125
+
126
+ export async function updateById(id: string, data: UpdateInput) {
127
+ return prisma.<resource>.update({
128
+ where: { id },
129
+ data,
130
+ });
131
+ }
132
+
133
+ export async function deleteById(id: string) {
134
+ return prisma.<resource>.delete({ where: { id } });
135
+ }
136
+ ```
137
+
138
+ ## Step 4: Validation Schemas
139
+
140
+ ```typescript
141
+ import { z } from 'zod';
142
+
143
+ export const resourceSchemas = {
144
+ create: z.object({
145
+ name: z.string().min(1, 'Name is required').max(100),
146
+ description: z.string().max(500).optional(),
147
+ }),
148
+ update: z.object({
149
+ name: z.string().min(1).max(100).optional(),
150
+ description: z.string().max(500).optional(),
151
+ }),
152
+ };
153
+
154
+ export type CreateInput = z.infer<typeof resourceSchemas.create>;
155
+ export type UpdateInput = z.infer<typeof resourceSchemas.update>;
156
+ ```
157
+
158
+ ## Pagination (Optional)
159
+
160
+ For resources that may grow large, add pagination to the list endpoint:
161
+
162
+ ```typescript
163
+ export const GET = withAuthNoParams(async (request: AuthenticatedRequest) => {
164
+ try {
165
+ const url = new URL(request.url);
166
+ const page = Math.max(1, Number(url.searchParams.get('page') || '1'));
167
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit') || '20')));
168
+ const skip = (page - 1) * limit;
169
+
170
+ const [items, total] = await Promise.all([
171
+ prisma.<resource>.findMany({
172
+ where: { userId: request.session.userId },
173
+ orderBy: { createdAt: 'desc' },
174
+ skip,
175
+ take: limit,
176
+ }),
177
+ prisma.<resource>.count({
178
+ where: { userId: request.session.userId },
179
+ }),
180
+ ]);
181
+
182
+ return NextResponse.json({
183
+ items,
184
+ pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
185
+ });
186
+ } catch (error) {
187
+ return handleApiError(error, { endpoint: '/api/protected/<resource>' });
188
+ }
189
+ });
190
+ ```
191
+
192
+ ## Admin Variant
193
+
194
+ For admin-only CRUD (e.g., managing all users' resources):
195
+
196
+ ```typescript
197
+ export const GET = withRole(['admin'], async (request, context) => {
198
+ // No userId scoping -- admin sees all
199
+ const items = await prisma.<resource>.findMany({
200
+ orderBy: { createdAt: 'desc' },
201
+ include: { user: { select: { id: true, name: true, email: true } } },
202
+ });
203
+ return NextResponse.json(items);
204
+ });
205
+ ```
206
+
207
+ ## Middleware Registration
208
+
209
+ Add the new protected routes to `src/middleware.ts` CSRF protection if not already covered by the `/api/protected` prefix match.
210
+
211
+ ## Checklist
212
+
213
+ - [ ] Collection route (GET list + POST create)
214
+ - [ ] Item route (GET one + PATCH update + DELETE)
215
+ - [ ] Ownership verification via `withOwnership`
216
+ - [ ] All queries scoped by `userId`
217
+ - [ ] Zod validation on create and update
218
+ - [ ] `handleApiError` in every catch block
219
+ - [ ] Server access functions with `'server-only'`
220
+ - [ ] Pagination for potentially large collections
221
+ - [ ] Tests for each endpoint
@@ -0,0 +1,227 @@
1
+ # Skill: Add an E2E Test
2
+
3
+ Write a Playwright end-to-end test for a MARS feature or user flow.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add E2E tests, integration tests, browser tests, or test a full user flow.
8
+
9
+ ## Architecture
10
+
11
+ MARS uses Playwright for E2E tests. Tests live in `e2e/` at the project root and run against the full application.
12
+
13
+ ## Setup
14
+
15
+ Config lives at `playwright.config.ts`. Key settings:
16
+
17
+ ```typescript
18
+ import { defineConfig } from '@playwright/test';
19
+
20
+ export default defineConfig({
21
+ testDir: './e2e',
22
+ fullyParallel: true,
23
+ retries: process.env.CI ? 2 : 0,
24
+ reporter: process.env.CI ? 'github' : 'html',
25
+ use: {
26
+ baseURL: 'http://localhost:3000',
27
+ trace: 'on-first-retry',
28
+ },
29
+ webServer: {
30
+ command: 'yarn dev',
31
+ port: 3000,
32
+ reuseExistingServer: !process.env.CI,
33
+ },
34
+ });
35
+ ```
36
+
37
+ ## Template: Auth Flow Test
38
+
39
+ ```typescript
40
+ // e2e/auth.spec.ts
41
+ import { test, expect } from '@playwright/test';
42
+
43
+ test.describe('Authentication', () => {
44
+ test('user can sign in', async ({ page }) => {
45
+ await page.goto('/sign-in');
46
+
47
+ await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible();
48
+
49
+ await page.getByLabel('Email').fill('test@example.com');
50
+ await page.getByLabel('Password').fill('password123');
51
+ await page.getByRole('button', { name: /sign in/i }).click();
52
+
53
+ await expect(page).toHaveURL('/dashboard');
54
+ await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
55
+ });
56
+
57
+ test('shows error for invalid credentials', async ({ page }) => {
58
+ await page.goto('/sign-in');
59
+
60
+ await page.getByLabel('Email').fill('wrong@example.com');
61
+ await page.getByLabel('Password').fill('wrongpassword');
62
+ await page.getByRole('button', { name: /sign in/i }).click();
63
+
64
+ await expect(page.getByText(/invalid credentials/i)).toBeVisible();
65
+ await expect(page).toHaveURL('/sign-in');
66
+ });
67
+
68
+ test('redirects to sign-in when accessing protected route', async ({ page }) => {
69
+ await page.goto('/dashboard');
70
+ await expect(page).toHaveURL(/sign-in/);
71
+ });
72
+ });
73
+ ```
74
+
75
+ ## Template: CRUD Flow Test
76
+
77
+ ```typescript
78
+ // e2e/projects.spec.ts
79
+ import { test, expect } from '@playwright/test';
80
+
81
+ test.describe('Projects', () => {
82
+ test.beforeEach(async ({ page }) => {
83
+ // Sign in before each test
84
+ await page.goto('/sign-in');
85
+ await page.getByLabel('Email').fill('test@example.com');
86
+ await page.getByLabel('Password').fill('password123');
87
+ await page.getByRole('button', { name: /sign in/i }).click();
88
+ await expect(page).toHaveURL('/dashboard');
89
+ });
90
+
91
+ test('can create a new project', async ({ page }) => {
92
+ await page.goto('/projects');
93
+ await page.getByRole('button', { name: /create/i }).click();
94
+
95
+ await page.getByLabel('Name').fill('My Test Project');
96
+ await page.getByLabel('Description').fill('A test project for E2E');
97
+ await page.getByRole('button', { name: /save/i }).click();
98
+
99
+ await expect(page.getByText('My Test Project')).toBeVisible();
100
+ });
101
+
102
+ test('can edit a project', async ({ page }) => {
103
+ await page.goto('/projects');
104
+ await page.getByText('My Test Project').click();
105
+ await page.getByRole('button', { name: /edit/i }).click();
106
+
107
+ await page.getByLabel('Name').clear();
108
+ await page.getByLabel('Name').fill('Updated Project');
109
+ await page.getByRole('button', { name: /save/i }).click();
110
+
111
+ await expect(page.getByText('Updated Project')).toBeVisible();
112
+ });
113
+
114
+ test('can delete a project', async ({ page }) => {
115
+ await page.goto('/projects');
116
+ await page.getByText('Updated Project').click();
117
+ await page.getByRole('button', { name: /delete/i }).click();
118
+ await page.getByRole('button', { name: /confirm/i }).click();
119
+
120
+ await expect(page.getByText('Updated Project')).not.toBeVisible();
121
+ });
122
+ });
123
+ ```
124
+
125
+ ## Auth Helper (Reusable Login)
126
+
127
+ For tests that need authentication, create a helper:
128
+
129
+ ```typescript
130
+ // e2e/helpers/auth.ts
131
+ import type { Page } from '@playwright/test';
132
+
133
+ export async function signIn(page: Page, email = 'test@example.com', password = 'password123') {
134
+ await page.goto('/sign-in');
135
+ await page.getByLabel('Email').fill(email);
136
+ await page.getByLabel('Password').fill(password);
137
+ await page.getByRole('button', { name: /sign in/i }).click();
138
+ }
139
+ ```
140
+
141
+ Or use Playwright's `storageState` for persistent auth:
142
+
143
+ ```typescript
144
+ // e2e/auth.setup.ts
145
+ import { test as setup, expect } from '@playwright/test';
146
+
147
+ const authFile = 'e2e/.auth/user.json';
148
+
149
+ setup('authenticate', async ({ page }) => {
150
+ await page.goto('/sign-in');
151
+ await page.getByLabel('Email').fill('test@example.com');
152
+ await page.getByLabel('Password').fill('password123');
153
+ await page.getByRole('button', { name: /sign in/i }).click();
154
+ await expect(page).toHaveURL('/dashboard');
155
+ await page.context().storageState({ path: authFile });
156
+ });
157
+ ```
158
+
159
+ Then in `playwright.config.ts`:
160
+
161
+ ```typescript
162
+ projects: [
163
+ { name: 'setup', testMatch: /.*\.setup\.ts/ },
164
+ {
165
+ name: 'chromium',
166
+ use: { ...devices['Desktop Chrome'], storageState: authFile },
167
+ dependencies: ['setup'],
168
+ },
169
+ ],
170
+ ```
171
+
172
+ ## Patterns
173
+
174
+ ### Testing Form Validation
175
+
176
+ ```typescript
177
+ test('shows validation errors for empty form', async ({ page }) => {
178
+ await page.goto('/contact');
179
+ await page.getByRole('button', { name: /send/i }).click();
180
+
181
+ await expect(page.getByText(/name is required/i)).toBeVisible();
182
+ await expect(page.getByText(/invalid email/i)).toBeVisible();
183
+ });
184
+ ```
185
+
186
+ ### Testing API Responses via Network
187
+
188
+ ```typescript
189
+ test('handles server errors gracefully', async ({ page }) => {
190
+ await page.route('**/api/protected/projects', (route) =>
191
+ route.fulfill({ status: 503, body: JSON.stringify({ error: 'Service unavailable' }) }),
192
+ );
193
+
194
+ await page.goto('/projects');
195
+ await expect(page.getByText(/try again/i)).toBeVisible();
196
+ });
197
+ ```
198
+
199
+ ### Visual Regression
200
+
201
+ ```typescript
202
+ test('dashboard looks correct', async ({ page }) => {
203
+ await page.goto('/dashboard');
204
+ await expect(page).toHaveScreenshot('dashboard.png', {
205
+ maxDiffPixelRatio: 0.01,
206
+ });
207
+ });
208
+ ```
209
+
210
+ ## Running Tests
211
+
212
+ ```bash
213
+ yarn test:e2e # Run all E2E tests
214
+ yarn test:e2e:ui # Open Playwright UI
215
+ npx playwright test --headed # Run with visible browser
216
+ npx playwright show-report # View HTML report
217
+ ```
218
+
219
+ ## Checklist
220
+
221
+ - [ ] Test file in `e2e/` directory
222
+ - [ ] Tests use `test.describe` for grouping
223
+ - [ ] Auth helper or `storageState` for authenticated tests
224
+ - [ ] Uses role-based selectors (`getByRole`, `getByLabel`, `getByText`) over CSS selectors
225
+ - [ ] Tests both happy path and error paths
226
+ - [ ] No hard-coded waits (`waitForTimeout`) -- use `expect` assertions
227
+ - [ ] Test data cleaned up (or use test-specific seed data)