Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb606b1c

Browse files
committed
Merge branch 'master' into learn/first
2 parents6aeeac3 +5dfe76b commitb606b1c

File tree

2,410 files changed

+211042
-138136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2,410 files changed

+211042
-138136
lines changed

‎.cursor/rules/e2e-testing.mdc‎

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
---
2+
description: E2E testing best practices for Playwright tests in Studio
3+
globs:
4+
- e2e/studio/**/*.ts
5+
- e2e/studio/**/*.spec.ts
6+
alwaysApply: true
7+
---
8+
9+
# E2E Testing Best Practices
10+
11+
## Getting Context
12+
13+
Before writing or modifying tests, use the Playwright MCP to understand:
14+
15+
- Available page elements and their roles/locators
16+
- Current page state and network activity
17+
- Existing test patterns in the codebase
18+
19+
Avoid extensive code reading - let Playwright's inspection tools guide your understanding of the UI.
20+
21+
## Avoiding Race Conditions
22+
23+
### Set up API waiters BEFORE triggering actions
24+
25+
The most common source of flaky tests is race conditions between UI actions and API calls. Always create response waiters before clicking buttons or navigating.
26+
27+
```ts
28+
// ❌ Bad - race condition: response might complete before waiter is set up
29+
await page.getByRole('button', { name: 'Save' }).click()
30+
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
31+
32+
// ✅ Good - waiter is ready before action
33+
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
34+
await page.getByRole('button', { name: 'Save' }).click()
35+
await apiPromise
36+
```
37+
38+
### Use `createApiResponseWaiter` for pre-navigation waits
39+
40+
When you need to wait for a response that happens during navigation:
41+
42+
```ts
43+
// ✅ Good - waiter created before navigation
44+
const loadPromise = waitForTableToLoad(page, ref)
45+
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
46+
await loadPromise
47+
```
48+
49+
### Wait for multiple related API calls with Promise.all
50+
51+
When an action triggers multiple API calls, wait for all of them:
52+
53+
```ts
54+
// ✅ Good - wait for all related API calls
55+
const createTablePromise = waitForApiResponseWithTimeout(page, (response) =>
56+
response.url().includes('query?key=table-create')
57+
)
58+
const tablesPromise = waitForApiResponseWithTimeout(page, (response) =>
59+
response.url().includes('tables?include_columns=true')
60+
)
61+
const entitiesPromise = waitForApiResponseWithTimeout(page, (response) =>
62+
response.url().includes('query?key=entity-types-')
63+
)
64+
65+
await page.getByRole('button', { name: 'Save' }).click()
66+
await Promise.all([createTablePromise, tablesPromise, entitiesPromise])
67+
```
68+
69+
## Waiting Strategies
70+
71+
### Prefer Playwright's built-in auto-waiting
72+
73+
Playwright automatically waits for elements to be actionable. Use this instead of manual timeouts:
74+
75+
```ts
76+
// ❌ Bad - arbitrary timeout
77+
await page.waitForTimeout(2000)
78+
await page.getByRole('button', { name: 'Submit' }).click()
79+
80+
// ✅ Good - auto-waits for element to be visible and enabled
81+
await page.getByRole('button', { name: 'Submit' }).click()
82+
```
83+
84+
### Use `expect.poll` for dynamic assertions
85+
86+
When waiting for state to change:
87+
88+
```ts
89+
// ✅ Good - polls until condition is met
90+
await expect
91+
.poll(async () => {
92+
return await page.getByLabel(`View ${tableName}`).count()
93+
})
94+
.toBe(0)
95+
```
96+
97+
### Use `waitForSelector` with state for element lifecycle
98+
99+
```ts
100+
// ✅ Good - wait for panel to close
101+
await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })
102+
```
103+
104+
### Avoid `networkidle` - use specific API waits instead
105+
106+
```ts
107+
// ❌ Bad - unreliable and slow
108+
await page.waitForLoadState('networkidle')
109+
110+
// ✅ Good - wait for specific API response
111+
await waitForApiResponse(page, 'pg-meta', ref, 'tables')
112+
```
113+
114+
### Use timeouts sparingly and only for non-API waits
115+
116+
```ts
117+
// ✅ Acceptable - waiting for client-side debounce
118+
await page.getByRole('textbox').fill('search term')
119+
await page.waitForTimeout(300) // Allow debounce to complete
120+
121+
// ✅ Acceptable - waiting for clipboard API
122+
await page.evaluate(() => navigator.clipboard.readText())
123+
await page.waitForTimeout(500)
124+
```
125+
126+
## Test Structure
127+
128+
### Use the custom test utility
129+
130+
Always import from the custom test utility for consistent fixtures:
131+
132+
```ts
133+
import { test } from '../utils/test.js'
134+
```
135+
136+
### Use `withFileOnceSetup` for expensive setup
137+
138+
When setup is expensive (cleanup, seeding), run it once per file:
139+
140+
```ts
141+
test.beforeAll(async ({ browser, ref }) => {
142+
await withFileOnceSetup(import.meta.url, async () => {
143+
const ctx = await browser.newContext()
144+
const page = await ctx.newPage()
145+
146+
// Expensive setup logic (e.g., cleanup old test data)
147+
await deleteTestTables(page, ref)
148+
})
149+
})
150+
151+
test.afterAll(async () => {
152+
await releaseFileOnceCleanup(import.meta.url)
153+
})
154+
```
155+
156+
### Dismiss toasts before interacting with UI
157+
158+
Toasts can overlay buttons and block interactions:
159+
160+
```ts
161+
const dismissToastsIfAny = async (page: Page) => {
162+
const closeButtons = page.getByRole('button', { name: 'Close toast' })
163+
const count = await closeButtons.count()
164+
for (let i = 0; i < count; i++) {
165+
await closeButtons.nth(i).click()
166+
}
167+
}
168+
169+
// ✅ Good - dismiss toasts before clicking
170+
await dismissToastsIfAny(page)
171+
await page.getByRole('button', { name: 'New table' }).click()
172+
```
173+
174+
## Assertions
175+
176+
### Always include descriptive messages
177+
178+
```ts
179+
// ❌ Bad - no context on failure
180+
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
181+
182+
// ✅ Good - clear message on failure
183+
await expect(
184+
page.getByRole('button', { name: 'Save' }),
185+
'Save button should be visible after form is filled'
186+
).toBeVisible()
187+
```
188+
189+
### Use appropriate timeouts for slow operations
190+
191+
```ts
192+
// ✅ Good - explicit timeout for slow operations
193+
await expect(
194+
page.getByText(`Table ${tableName} is good to go!`),
195+
'Success toast should be visible after table creation'
196+
).toBeVisible({ timeout: 50000 })
197+
```
198+
199+
## Locators
200+
201+
### Prefer role-based locators
202+
203+
```ts
204+
// ✅ Good - semantic and resilient
205+
page.getByRole('button', { name: 'Save' })
206+
page.getByRole('textbox', { name: 'Username' })
207+
page.getByRole('menuitem', { name: 'Delete' })
208+
209+
// ❌ Avoid - brittle CSS selectors
210+
page.locator('.btn-primary')
211+
page.locator('#submit-button')
212+
```
213+
214+
### Use test IDs for complex elements
215+
216+
```ts
217+
// ✅ Good - stable identifier for complex elements
218+
page.getByTestId('table-editor-side-panel')
219+
page.getByTestId('action-bar-save-row')
220+
```
221+
222+
### Use `filter` for finding elements in context
223+
224+
```ts
225+
// ✅ Good - find button within specific row
226+
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
227+
await bucketRow.getByRole('button').click()
228+
```
229+
230+
## Helper Functions
231+
232+
### Extract reusable operations into helpers
233+
234+
Create helper functions for common operations:
235+
236+
```ts
237+
// e2e/studio/utils/storage-helpers.ts
238+
export const createBucket = async (
239+
page: Page,
240+
ref: string,
241+
bucketName: string,
242+
isPublic: boolean = false
243+
) => {
244+
await navigateToStorageFiles(page, ref)
245+
246+
// Check if already exists
247+
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
248+
if ((await bucketRow.count()) > 0) return
249+
250+
await dismissToastsIfAny(page)
251+
252+
// Create bucket with proper waits
253+
const apiPromise = waitForApiResponse(page, 'storage', ref, 'bucket', { method: 'POST' })
254+
await page.getByRole('button', { name: 'New bucket' }).click()
255+
await page.getByRole('textbox', { name: 'Bucket name' }).fill(bucketName)
256+
await page.getByRole('button', { name: 'Create' }).click()
257+
await apiPromise
258+
259+
await expect(
260+
page.getByRole('row').filter({ hasText: bucketName }),
261+
`Bucket ${bucketName} should be visible`
262+
).toBeVisible()
263+
}
264+
```
265+
266+
### Use the existing wait utilities
267+
268+
```ts
269+
import {
270+
createApiResponseWaiter,
271+
waitForApiResponse,
272+
waitForGridDataToLoad,
273+
waitForTableToLoad,
274+
} from '../utils/wait-for-response.js'
275+
```
276+
277+
## API Mocking
278+
279+
### Mock APIs for isolated testing
280+
281+
```ts
282+
// ✅ Good - mock API response
283+
await page.route('*/**/logs.all*', async (route) => {
284+
await route.fulfill({ body: JSON.stringify(mockAPILogs) })
285+
})
286+
```
287+
288+
### Use soft waits for optional API calls
289+
290+
```ts
291+
// ✅ Good - don't fail if API doesn't respond
292+
await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', {
293+
soft: true,
294+
fallbackWaitMs: 1000,
295+
})
296+
```
297+
298+
## Cleanup
299+
300+
### Clean up test data in beforeAll/beforeEach
301+
302+
```ts
303+
test.beforeEach(async ({ page, ref }) => {
304+
await deleteAllBuckets(page, ref)
305+
})
306+
```
307+
308+
### Handle existing state gracefully
309+
310+
```ts
311+
// ✅ Good - check before trying to delete
312+
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
313+
if ((await bucketRow.count()) === 0) return
314+
// proceed with deletion
315+
```
316+
317+
### Reset local storage when needed
318+
319+
```ts
320+
import { resetLocalStorage } from '../utils/reset-local-storage.js'
321+
322+
// Clean up after tests that modify local storage
323+
await resetLocalStorage(page, ref)
324+
```

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp