autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Playwright Skill
|
|
2
|
+
|
|
3
|
+
## Test Structure
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { test, expect } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
test.describe('Authentication', () => {
|
|
8
|
+
test.beforeEach(async ({ page }) => {
|
|
9
|
+
await page.goto('/login');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('should login successfully', async ({ page }) => {
|
|
13
|
+
// Use accessible selectors (recommended)
|
|
14
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
15
|
+
await page.getByLabel('Password').fill('password123');
|
|
16
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
17
|
+
|
|
18
|
+
// Web-first assertions (auto-wait)
|
|
19
|
+
await expect(page).toHaveURL('/dashboard');
|
|
20
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should show error for invalid credentials', async ({ page }) => {
|
|
24
|
+
await page.getByLabel('Email').fill('wrong@example.com');
|
|
25
|
+
await page.getByLabel('Password').fill('wrongpassword');
|
|
26
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
27
|
+
|
|
28
|
+
await expect(page.getByText('Invalid credentials')).toBeVisible();
|
|
29
|
+
await expect(page).toHaveURL('/login');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
## Locators (Selectors)
|
|
35
|
+
\`\`\`typescript
|
|
36
|
+
// Recommended: Accessible selectors
|
|
37
|
+
page.getByRole('button', { name: 'Submit' });
|
|
38
|
+
page.getByRole('textbox', { name: 'Email' });
|
|
39
|
+
page.getByRole('checkbox', { name: 'Remember me' });
|
|
40
|
+
page.getByRole('link', { name: 'Sign up' });
|
|
41
|
+
page.getByRole('heading', { name: 'Welcome', level: 1 });
|
|
42
|
+
|
|
43
|
+
page.getByLabel('Email'); // Form labels
|
|
44
|
+
page.getByPlaceholder('Enter email');
|
|
45
|
+
page.getByText('Submit'); // Exact text
|
|
46
|
+
page.getByText(/submit/i); // Regex
|
|
47
|
+
page.getByAltText('Logo'); // Image alt
|
|
48
|
+
page.getByTitle('Close'); // Title attribute
|
|
49
|
+
|
|
50
|
+
// Test IDs (when accessibility isn't possible)
|
|
51
|
+
page.getByTestId('submit-button'); // data-testid="submit-button"
|
|
52
|
+
|
|
53
|
+
// CSS/XPath (use sparingly)
|
|
54
|
+
page.locator('.submit-btn');
|
|
55
|
+
page.locator('#email');
|
|
56
|
+
page.locator('css=button.primary');
|
|
57
|
+
page.locator('xpath=//button[@type="submit"]');
|
|
58
|
+
|
|
59
|
+
// Chaining and filtering
|
|
60
|
+
page.getByRole('listitem').filter({ hasText: 'Product 1' });
|
|
61
|
+
page.getByRole('listitem').filter({ has: page.getByRole('button') });
|
|
62
|
+
page.locator('article').filter({ hasText: 'Playwright' }).getByRole('link');
|
|
63
|
+
|
|
64
|
+
// Nth element
|
|
65
|
+
page.getByRole('listitem').nth(2);
|
|
66
|
+
page.getByRole('listitem').first();
|
|
67
|
+
page.getByRole('listitem').last();
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
## Actions
|
|
71
|
+
\`\`\`typescript
|
|
72
|
+
// Click actions
|
|
73
|
+
await page.getByRole('button').click();
|
|
74
|
+
await page.getByRole('button').dblclick();
|
|
75
|
+
await page.getByRole('button').click({ button: 'right' });
|
|
76
|
+
await page.getByRole('button').click({ modifiers: ['Shift'] });
|
|
77
|
+
await page.getByRole('link').click({ force: true }); // Skip actionability checks
|
|
78
|
+
|
|
79
|
+
// Input actions
|
|
80
|
+
await page.getByLabel('Name').fill('John Doe');
|
|
81
|
+
await page.getByLabel('Name').clear();
|
|
82
|
+
await page.getByLabel('Name').type('John', { delay: 100 }); // Simulate typing
|
|
83
|
+
await page.getByLabel('Name').press('Enter');
|
|
84
|
+
await page.getByLabel('Name').pressSequentially('Hello'); // Key by key
|
|
85
|
+
|
|
86
|
+
// Select and checkbox
|
|
87
|
+
await page.getByLabel('Country').selectOption('US');
|
|
88
|
+
await page.getByLabel('Country').selectOption({ label: 'United States' });
|
|
89
|
+
await page.getByRole('checkbox').check();
|
|
90
|
+
await page.getByRole('checkbox').uncheck();
|
|
91
|
+
await page.getByRole('radio', { name: 'Option 1' }).check();
|
|
92
|
+
|
|
93
|
+
// File upload
|
|
94
|
+
await page.getByLabel('Upload').setInputFiles('file.pdf');
|
|
95
|
+
await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf']);
|
|
96
|
+
|
|
97
|
+
// Drag and drop
|
|
98
|
+
await page.getByText('Drag me').dragTo(page.getByText('Drop here'));
|
|
99
|
+
|
|
100
|
+
// Hover
|
|
101
|
+
await page.getByRole('button').hover();
|
|
102
|
+
|
|
103
|
+
// Focus
|
|
104
|
+
await page.getByLabel('Email').focus();
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
## Assertions
|
|
108
|
+
\`\`\`typescript
|
|
109
|
+
// Page assertions
|
|
110
|
+
await expect(page).toHaveURL('/dashboard');
|
|
111
|
+
await expect(page).toHaveURL(/dashboard/);
|
|
112
|
+
await expect(page).toHaveTitle('Dashboard');
|
|
113
|
+
|
|
114
|
+
// Element assertions (auto-wait)
|
|
115
|
+
await expect(locator).toBeVisible();
|
|
116
|
+
await expect(locator).toBeHidden();
|
|
117
|
+
await expect(locator).toBeEnabled();
|
|
118
|
+
await expect(locator).toBeDisabled();
|
|
119
|
+
await expect(locator).toBeChecked();
|
|
120
|
+
await expect(locator).toBeFocused();
|
|
121
|
+
await expect(locator).toBeEditable();
|
|
122
|
+
|
|
123
|
+
// Content assertions
|
|
124
|
+
await expect(locator).toHaveText('Hello');
|
|
125
|
+
await expect(locator).toHaveText(/hello/i);
|
|
126
|
+
await expect(locator).toContainText('Hello');
|
|
127
|
+
await expect(locator).toHaveValue('input value');
|
|
128
|
+
await expect(locator).toHaveAttribute('href', '/home');
|
|
129
|
+
await expect(locator).toHaveClass(/active/);
|
|
130
|
+
await expect(locator).toHaveCSS('color', 'rgb(0, 0, 0)');
|
|
131
|
+
await expect(locator).toHaveCount(5);
|
|
132
|
+
|
|
133
|
+
// Negation
|
|
134
|
+
await expect(locator).not.toBeVisible();
|
|
135
|
+
|
|
136
|
+
// Soft assertions (don't stop test on failure)
|
|
137
|
+
await expect.soft(locator).toHaveText('Hello');
|
|
138
|
+
\`\`\`
|
|
139
|
+
|
|
140
|
+
## Page Object Model
|
|
141
|
+
\`\`\`typescript
|
|
142
|
+
// pages/LoginPage.ts
|
|
143
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
144
|
+
|
|
145
|
+
export class LoginPage {
|
|
146
|
+
readonly page: Page;
|
|
147
|
+
readonly emailInput: Locator;
|
|
148
|
+
readonly passwordInput: Locator;
|
|
149
|
+
readonly submitButton: Locator;
|
|
150
|
+
readonly errorMessage: Locator;
|
|
151
|
+
|
|
152
|
+
constructor(page: Page) {
|
|
153
|
+
this.page = page;
|
|
154
|
+
this.emailInput = page.getByLabel('Email');
|
|
155
|
+
this.passwordInput = page.getByLabel('Password');
|
|
156
|
+
this.submitButton = page.getByRole('button', { name: 'Sign in' });
|
|
157
|
+
this.errorMessage = page.getByRole('alert');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async goto() {
|
|
161
|
+
await this.page.goto('/login');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async login(email: string, password: string) {
|
|
165
|
+
await this.emailInput.fill(email);
|
|
166
|
+
await this.passwordInput.fill(password);
|
|
167
|
+
await this.submitButton.click();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async expectError(message: string) {
|
|
171
|
+
await expect(this.errorMessage).toHaveText(message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// tests/login.spec.ts
|
|
176
|
+
import { test, expect } from '@playwright/test';
|
|
177
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
178
|
+
|
|
179
|
+
test('should login', async ({ page }) => {
|
|
180
|
+
const loginPage = new LoginPage(page);
|
|
181
|
+
await loginPage.goto();
|
|
182
|
+
await loginPage.login('user@example.com', 'password123');
|
|
183
|
+
await expect(page).toHaveURL('/dashboard');
|
|
184
|
+
});
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
## Fixtures
|
|
188
|
+
\`\`\`typescript
|
|
189
|
+
// fixtures.ts
|
|
190
|
+
import { test as base, expect } from '@playwright/test';
|
|
191
|
+
import { LoginPage } from './pages/LoginPage';
|
|
192
|
+
import { DashboardPage } from './pages/DashboardPage';
|
|
193
|
+
|
|
194
|
+
type MyFixtures = {
|
|
195
|
+
loginPage: LoginPage;
|
|
196
|
+
dashboardPage: DashboardPage;
|
|
197
|
+
authenticatedPage: void;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const test = base.extend<MyFixtures>({
|
|
201
|
+
loginPage: async ({ page }, use) => {
|
|
202
|
+
const loginPage = new LoginPage(page);
|
|
203
|
+
await use(loginPage);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
dashboardPage: async ({ page }, use) => {
|
|
207
|
+
await use(new DashboardPage(page));
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// Auto-login fixture
|
|
211
|
+
authenticatedPage: async ({ page }, use) => {
|
|
212
|
+
// Set auth cookies/storage
|
|
213
|
+
await page.context().addCookies([{
|
|
214
|
+
name: 'auth-token',
|
|
215
|
+
value: 'test-token',
|
|
216
|
+
domain: 'localhost',
|
|
217
|
+
path: '/',
|
|
218
|
+
}]);
|
|
219
|
+
await use();
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Usage in tests
|
|
224
|
+
test('should show dashboard', async ({ dashboardPage, authenticatedPage }) => {
|
|
225
|
+
await dashboardPage.goto();
|
|
226
|
+
await dashboardPage.expectWelcomeMessage();
|
|
227
|
+
});
|
|
228
|
+
\`\`\`
|
|
229
|
+
|
|
230
|
+
## API Testing
|
|
231
|
+
\`\`\`typescript
|
|
232
|
+
import { test, expect } from '@playwright/test';
|
|
233
|
+
|
|
234
|
+
test('API: should create user', async ({ request }) => {
|
|
235
|
+
const response = await request.post('/api/users', {
|
|
236
|
+
data: { email: 'test@example.com', name: 'Test' },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(response.ok()).toBeTruthy();
|
|
240
|
+
expect(response.status()).toBe(201);
|
|
241
|
+
|
|
242
|
+
const user = await response.json();
|
|
243
|
+
expect(user).toMatchObject({
|
|
244
|
+
email: 'test@example.com',
|
|
245
|
+
name: 'Test',
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('API: should handle authentication', async ({ request }) => {
|
|
250
|
+
// Login and get token
|
|
251
|
+
const loginResponse = await request.post('/api/login', {
|
|
252
|
+
data: { email: 'test@example.com', password: 'password123' },
|
|
253
|
+
});
|
|
254
|
+
const { token } = await loginResponse.json();
|
|
255
|
+
|
|
256
|
+
// Use token in subsequent requests
|
|
257
|
+
const response = await request.get('/api/profile', {
|
|
258
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(response.ok()).toBeTruthy();
|
|
262
|
+
});
|
|
263
|
+
\`\`\`
|
|
264
|
+
|
|
265
|
+
## Network Mocking
|
|
266
|
+
\`\`\`typescript
|
|
267
|
+
test('should mock API response', async ({ page }) => {
|
|
268
|
+
// Mock API response
|
|
269
|
+
await page.route('/api/users', async (route) => {
|
|
270
|
+
await route.fulfill({
|
|
271
|
+
status: 200,
|
|
272
|
+
contentType: 'application/json',
|
|
273
|
+
body: JSON.stringify([{ id: 1, name: 'Mocked User' }]),
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await page.goto('/users');
|
|
278
|
+
await expect(page.getByText('Mocked User')).toBeVisible();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should intercept and modify response', async ({ page }) => {
|
|
282
|
+
await page.route('/api/users', async (route) => {
|
|
283
|
+
const response = await route.fetch();
|
|
284
|
+
const json = await response.json();
|
|
285
|
+
json.push({ id: 999, name: 'Injected User' });
|
|
286
|
+
await route.fulfill({ response, json });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('should abort request', async ({ page }) => {
|
|
291
|
+
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
|
|
292
|
+
await page.goto('/page'); // Images won't load
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Wait for specific request
|
|
296
|
+
const responsePromise = page.waitForResponse('/api/users');
|
|
297
|
+
await page.getByRole('button', { name: 'Load' }).click();
|
|
298
|
+
const response = await responsePromise;
|
|
299
|
+
\`\`\`
|
|
300
|
+
|
|
301
|
+
## Visual Testing
|
|
302
|
+
\`\`\`typescript
|
|
303
|
+
test('should match screenshot', async ({ page }) => {
|
|
304
|
+
await page.goto('/');
|
|
305
|
+
|
|
306
|
+
// Full page screenshot
|
|
307
|
+
await expect(page).toHaveScreenshot('homepage.png');
|
|
308
|
+
|
|
309
|
+
// Element screenshot
|
|
310
|
+
await expect(page.getByRole('main')).toHaveScreenshot('main-content.png');
|
|
311
|
+
|
|
312
|
+
// With options
|
|
313
|
+
await expect(page).toHaveScreenshot('homepage.png', {
|
|
314
|
+
maxDiffPixels: 100,
|
|
315
|
+
threshold: 0.2,
|
|
316
|
+
animations: 'disabled',
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Update screenshots: npx playwright test --update-snapshots
|
|
321
|
+
\`\`\`
|
|
322
|
+
|
|
323
|
+
## Configuration (playwright.config.ts)
|
|
324
|
+
\`\`\`typescript
|
|
325
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
326
|
+
|
|
327
|
+
export default defineConfig({
|
|
328
|
+
testDir: './tests',
|
|
329
|
+
fullyParallel: true,
|
|
330
|
+
forbidOnly: !!process.env.CI,
|
|
331
|
+
retries: process.env.CI ? 2 : 0,
|
|
332
|
+
workers: process.env.CI ? 1 : undefined,
|
|
333
|
+
reporter: [['html'], ['list']],
|
|
334
|
+
|
|
335
|
+
use: {
|
|
336
|
+
baseURL: 'http://localhost:3000',
|
|
337
|
+
trace: 'on-first-retry',
|
|
338
|
+
screenshot: 'only-on-failure',
|
|
339
|
+
video: 'on-first-retry',
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
projects: [
|
|
343
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
344
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
345
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
346
|
+
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
|
|
347
|
+
],
|
|
348
|
+
|
|
349
|
+
webServer: {
|
|
350
|
+
command: 'npm run dev',
|
|
351
|
+
url: 'http://localhost:3000',
|
|
352
|
+
reuseExistingServer: !process.env.CI,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
\`\`\`
|
|
356
|
+
|
|
357
|
+
## ❌ DON'T
|
|
358
|
+
- Use hard-coded waits (\`page.waitForTimeout\`)
|
|
359
|
+
- Use CSS selectors when accessible selectors work
|
|
360
|
+
- Test implementation details
|
|
361
|
+
- Share state between tests
|
|
362
|
+
- Forget to handle dynamic content
|
|
363
|
+
|
|
364
|
+
## ✅ DO
|
|
365
|
+
- Use web-first assertions (auto-wait)
|
|
366
|
+
- Use accessible selectors (getByRole, getByLabel)
|
|
367
|
+
- Use Page Object Model for large apps
|
|
368
|
+
- Run tests in parallel
|
|
369
|
+
- Use fixtures for shared setup
|
|
370
|
+
- Use API testing for backend validation
|
|
371
|
+
- Use trace viewer for debugging
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# PostgreSQL Skill
|
|
2
|
+
|
|
3
|
+
## Schema Design
|
|
4
|
+
\`\`\`sql
|
|
5
|
+
-- Table with constraints
|
|
6
|
+
CREATE TABLE users (
|
|
7
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
9
|
+
name VARCHAR(100) NOT NULL,
|
|
10
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
11
|
+
role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
|
|
12
|
+
settings JSONB DEFAULT '{}',
|
|
13
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
14
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
15
|
+
deleted_at TIMESTAMPTZ
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- Foreign key with cascade
|
|
19
|
+
CREATE TABLE posts (
|
|
20
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
21
|
+
title VARCHAR(255) NOT NULL,
|
|
22
|
+
content TEXT,
|
|
23
|
+
published BOOLEAN DEFAULT FALSE,
|
|
24
|
+
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
25
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- Many-to-many junction table
|
|
29
|
+
CREATE TABLE post_tags (
|
|
30
|
+
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
|
|
31
|
+
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
|
|
32
|
+
PRIMARY KEY (post_id, tag_id)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- Enum type (alternative to CHECK constraint)
|
|
36
|
+
CREATE TYPE user_status AS ENUM ('active', 'inactive', 'suspended');
|
|
37
|
+
ALTER TABLE users ADD COLUMN status user_status DEFAULT 'active';
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
## Indexes
|
|
41
|
+
\`\`\`sql
|
|
42
|
+
-- B-tree index (default, good for equality and range)
|
|
43
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
44
|
+
|
|
45
|
+
-- Compound index (order matters!)
|
|
46
|
+
CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);
|
|
47
|
+
|
|
48
|
+
-- Partial index (smaller, more efficient)
|
|
49
|
+
CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL;
|
|
50
|
+
|
|
51
|
+
-- GIN index for JSONB
|
|
52
|
+
CREATE INDEX idx_users_settings ON users USING GIN (settings);
|
|
53
|
+
|
|
54
|
+
-- GIN index for full-text search
|
|
55
|
+
CREATE INDEX idx_posts_content_search ON posts USING GIN (to_tsvector('english', content));
|
|
56
|
+
|
|
57
|
+
-- Unique index with condition
|
|
58
|
+
CREATE UNIQUE INDEX idx_unique_email_active ON users(email) WHERE deleted_at IS NULL;
|
|
59
|
+
|
|
60
|
+
-- Covering index (includes non-key columns)
|
|
61
|
+
CREATE INDEX idx_posts_author ON posts(author_id) INCLUDE (title, created_at);
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Common Queries
|
|
65
|
+
\`\`\`sql
|
|
66
|
+
-- Pagination (offset-based)
|
|
67
|
+
SELECT * FROM posts
|
|
68
|
+
WHERE published = true
|
|
69
|
+
ORDER BY created_at DESC
|
|
70
|
+
LIMIT 20 OFFSET 40;
|
|
71
|
+
|
|
72
|
+
-- Pagination (cursor-based, more efficient)
|
|
73
|
+
SELECT * FROM posts
|
|
74
|
+
WHERE published = true AND created_at < '2024-01-15T10:00:00Z'
|
|
75
|
+
ORDER BY created_at DESC
|
|
76
|
+
LIMIT 20;
|
|
77
|
+
|
|
78
|
+
-- Upsert (INSERT ... ON CONFLICT)
|
|
79
|
+
INSERT INTO users (email, name)
|
|
80
|
+
VALUES ('user@example.com', 'John')
|
|
81
|
+
ON CONFLICT (email)
|
|
82
|
+
DO UPDATE SET name = EXCLUDED.name, updated_at = NOW();
|
|
83
|
+
|
|
84
|
+
-- Returning inserted/updated data
|
|
85
|
+
INSERT INTO users (email, name) VALUES ('new@example.com', 'New User')
|
|
86
|
+
RETURNING id, email, created_at;
|
|
87
|
+
|
|
88
|
+
-- Conditional update
|
|
89
|
+
UPDATE users
|
|
90
|
+
SET role = 'admin', updated_at = NOW()
|
|
91
|
+
WHERE id = '...'
|
|
92
|
+
RETURNING *;
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## JSONB Operations
|
|
96
|
+
\`\`\`sql
|
|
97
|
+
-- Query JSONB field
|
|
98
|
+
SELECT * FROM users WHERE settings->>'theme' = 'dark';
|
|
99
|
+
SELECT * FROM users WHERE settings @> '{"notifications": true}';
|
|
100
|
+
|
|
101
|
+
-- Update JSONB field
|
|
102
|
+
UPDATE users
|
|
103
|
+
SET settings = settings || '{"theme": "light"}'
|
|
104
|
+
WHERE id = '...';
|
|
105
|
+
|
|
106
|
+
-- Remove JSONB key
|
|
107
|
+
UPDATE users
|
|
108
|
+
SET settings = settings - 'oldKey'
|
|
109
|
+
WHERE id = '...';
|
|
110
|
+
|
|
111
|
+
-- JSONB array operations
|
|
112
|
+
SELECT * FROM users WHERE settings->'tags' ? 'premium';
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
## CTEs (Common Table Expressions)
|
|
116
|
+
\`\`\`sql
|
|
117
|
+
-- Basic CTE
|
|
118
|
+
WITH active_users AS (
|
|
119
|
+
SELECT * FROM users WHERE deleted_at IS NULL
|
|
120
|
+
)
|
|
121
|
+
SELECT u.*, COUNT(p.id) as post_count
|
|
122
|
+
FROM active_users u
|
|
123
|
+
LEFT JOIN posts p ON u.id = p.author_id
|
|
124
|
+
GROUP BY u.id;
|
|
125
|
+
|
|
126
|
+
-- Recursive CTE (hierarchical data)
|
|
127
|
+
WITH RECURSIVE category_tree AS (
|
|
128
|
+
-- Base case
|
|
129
|
+
SELECT id, name, parent_id, 0 as depth
|
|
130
|
+
FROM categories
|
|
131
|
+
WHERE parent_id IS NULL
|
|
132
|
+
|
|
133
|
+
UNION ALL
|
|
134
|
+
|
|
135
|
+
-- Recursive case
|
|
136
|
+
SELECT c.id, c.name, c.parent_id, ct.depth + 1
|
|
137
|
+
FROM categories c
|
|
138
|
+
JOIN category_tree ct ON c.parent_id = ct.id
|
|
139
|
+
)
|
|
140
|
+
SELECT * FROM category_tree ORDER BY depth, name;
|
|
141
|
+
|
|
142
|
+
-- CTE for data modification
|
|
143
|
+
WITH deleted AS (
|
|
144
|
+
DELETE FROM users WHERE last_login < NOW() - INTERVAL '1 year'
|
|
145
|
+
RETURNING *
|
|
146
|
+
)
|
|
147
|
+
INSERT INTO deleted_users SELECT * FROM deleted;
|
|
148
|
+
\`\`\`
|
|
149
|
+
|
|
150
|
+
## Window Functions
|
|
151
|
+
\`\`\`sql
|
|
152
|
+
-- Row number for pagination
|
|
153
|
+
SELECT *, ROW_NUMBER() OVER (ORDER BY created_at DESC) as row_num
|
|
154
|
+
FROM posts;
|
|
155
|
+
|
|
156
|
+
-- Running total
|
|
157
|
+
SELECT
|
|
158
|
+
date,
|
|
159
|
+
amount,
|
|
160
|
+
SUM(amount) OVER (ORDER BY date) as running_total
|
|
161
|
+
FROM transactions;
|
|
162
|
+
|
|
163
|
+
-- Rank within groups
|
|
164
|
+
SELECT
|
|
165
|
+
author_id,
|
|
166
|
+
title,
|
|
167
|
+
created_at,
|
|
168
|
+
ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY created_at DESC) as post_rank
|
|
169
|
+
FROM posts;
|
|
170
|
+
|
|
171
|
+
-- Get previous/next values
|
|
172
|
+
SELECT
|
|
173
|
+
title,
|
|
174
|
+
created_at,
|
|
175
|
+
LAG(title) OVER (ORDER BY created_at) as prev_title,
|
|
176
|
+
LEAD(title) OVER (ORDER BY created_at) as next_title
|
|
177
|
+
FROM posts;
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
## Full-Text Search
|
|
181
|
+
\`\`\`sql
|
|
182
|
+
-- Basic full-text search
|
|
183
|
+
SELECT * FROM posts
|
|
184
|
+
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 'search terms');
|
|
185
|
+
|
|
186
|
+
-- With ranking
|
|
187
|
+
SELECT
|
|
188
|
+
title,
|
|
189
|
+
ts_rank(to_tsvector('english', content), query) as rank
|
|
190
|
+
FROM posts, plainto_tsquery('english', 'search terms') query
|
|
191
|
+
WHERE to_tsvector('english', content) @@ query
|
|
192
|
+
ORDER BY rank DESC;
|
|
193
|
+
|
|
194
|
+
-- Generated tsvector column (for performance)
|
|
195
|
+
ALTER TABLE posts ADD COLUMN search_vector tsvector
|
|
196
|
+
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || COALESCE(content, ''))) STORED;
|
|
197
|
+
CREATE INDEX idx_posts_search ON posts USING GIN (search_vector);
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
## Query Optimization
|
|
201
|
+
\`\`\`sql
|
|
202
|
+
-- Analyze query plan
|
|
203
|
+
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
|
|
204
|
+
|
|
205
|
+
-- Analyze with buffers and timing
|
|
206
|
+
EXPLAIN (ANALYZE, BUFFERS, TIMING) SELECT * FROM posts WHERE author_id = '...';
|
|
207
|
+
|
|
208
|
+
-- Update table statistics
|
|
209
|
+
ANALYZE users;
|
|
210
|
+
|
|
211
|
+
-- Check index usage
|
|
212
|
+
SELECT
|
|
213
|
+
schemaname, tablename, indexname, idx_scan, idx_tup_read
|
|
214
|
+
FROM pg_stat_user_indexes
|
|
215
|
+
ORDER BY idx_scan DESC;
|
|
216
|
+
|
|
217
|
+
-- Find slow queries (enable pg_stat_statements)
|
|
218
|
+
SELECT query, calls, mean_exec_time, total_exec_time
|
|
219
|
+
FROM pg_stat_statements
|
|
220
|
+
ORDER BY total_exec_time DESC
|
|
221
|
+
LIMIT 10;
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
## Transactions & Locking
|
|
225
|
+
\`\`\`sql
|
|
226
|
+
-- Transaction with isolation level
|
|
227
|
+
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
|
228
|
+
-- ... operations ...
|
|
229
|
+
COMMIT;
|
|
230
|
+
|
|
231
|
+
-- Advisory locks (application-level)
|
|
232
|
+
SELECT pg_advisory_lock(123);
|
|
233
|
+
-- ... do work ...
|
|
234
|
+
SELECT pg_advisory_unlock(123);
|
|
235
|
+
|
|
236
|
+
-- Row-level lock
|
|
237
|
+
SELECT * FROM accounts WHERE id = '...' FOR UPDATE;
|
|
238
|
+
-- Now other transactions wait for this lock
|
|
239
|
+
\`\`\`
|
|
240
|
+
|
|
241
|
+
## ❌ DON'T
|
|
242
|
+
- Use SELECT * in production queries
|
|
243
|
+
- Create indexes on every column (maintain costs)
|
|
244
|
+
- Use OFFSET for deep pagination
|
|
245
|
+
- Store large blobs in PostgreSQL (use object storage)
|
|
246
|
+
- Skip EXPLAIN ANALYZE for slow queries
|
|
247
|
+
- Use implicit type conversions in WHERE clauses
|
|
248
|
+
|
|
249
|
+
## ✅ DO
|
|
250
|
+
- Use UUIDs or BIGSERIAL for primary keys
|
|
251
|
+
- Create partial indexes for filtered queries
|
|
252
|
+
- Use JSONB for flexible schema parts
|
|
253
|
+
- Use CTEs for complex queries
|
|
254
|
+
- Use cursor-based pagination for large datasets
|
|
255
|
+
- Run ANALYZE after bulk data changes
|
|
256
|
+
- Use connection pooling (PgBouncer)
|
|
257
|
+
- Set appropriate work_mem for complex queries
|