e2e
Activate for any work in the tests/e2e/ directory: creating or editing test files (tests/*.test.ts), page objects (pages/), helpers (helpers/), or vitest config. Enforces agent-browser conventions specific to this project.
含まれるファイル(1)
- SKILL.md7.9 KB
SKILL.md(原文)
インストールする前に、エージェントに与えられる指示の中身を確認できます。
E2E Test Conventions
Setup Notes
- Separate
tests/package.json: Root package.json uses Node 10/npm 6 for Gulp builds. Tests live intests/with their ownpackage.json(Node 20 / modern npm). Run tests from insidetests/withnpm test. - BrowserManager import:
import { BrowserManager } from 'agent-browser/dist/browser.js'(no exports map in the package, so the direct path import is required) - System Chrome:
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'— no need to install Playwright browsers separately - Credentials: Copy
tests/e2e/.env.templatetotests/e2e/.envand fill in NetrunnerDB usernames/passwords. The.envfile is gitignored.
Rules — always apply these
-
Run tests after every change:
cd tests && npm test. Never leave the session with failing tests. -
Playwright strict mode — throws if a locator matches more than one element:
- Use
.first()when the same text appears in navbar and body - Use
#idselectors whentext=would be ambiguous - Examples:
.locator('text=Login via NetrunnerDB').first(),#created-titlenottext=Tournaments created by me
- Use
-
Each test navigates fresh — don't use
beforeAllto navigate once. Eachit()should callpage.open()to ensure clean state and prevent test pollution. -
Don't expect specific data values — test data changes. Use flexible assertions:
expect(count).toBeGreaterThan(0)notexpect(count).toBe(10)expect(filteredCount).toBeLessThanOrEqual(initialCount)expect(data.title?.trim().length).toBeGreaterThan(0)
-
Session isolation: call
clearSession(browser)(orpage.context().clearCookies()) at the start of any test that depends on auth state. -
External redirects: after submitting a form that navigates to an external site, call
page.waitForLoadState('domcontentloaded')before interacting with the next page. Use 30 s forwaitForURLthat waits for an OAuth callback. -
Avoid explicit waits; use waitForFunction only when necessary: Never use
waitForTimeout(). AvoidwaitForFunction()when simpler solutions exist:await locator.waitFor({ state: 'visible' })— wait for element to appearawait locator.waitFor({ state: 'hidden' })— wait for element to disappearawait page.waitForURL(/param=/)— wait for URL to match patternawait expect.poll(async () => getValue()).toBe(expected)— poll until assertion passes
Why waitForFunction is bad:
- Mixes raw DOM selectors with Playwright locators — harder to maintain
- Bypasses Playwright's auto-waiting and retry logic
- Often indicates you're waiting for the wrong thing (implementation detail vs user-visible change)
Key insight: Most UI updates happen instantly without network calls. Only add waits when there's actual async behavior:
- Tab clicks: No wait needed if tab content is already in DOM - just wait for an element on the tab
- Client-side filters: No wait needed - results update instantly via Vue reactivity
- Form submission with redirect:
await page.waitForURL(/pattern/) - AJAX-loaded content: Wait for the loader to disappear or content to appear
- Negative assertions:
expect(await el.isVisible()).toBe(false)afterwaitForLoadState - Hidden input values: Prefer visible side effects, but
waitForFunctionis acceptable when there's no visible alternative
Manual polling loop anti-pattern (forbidden):
- Do not write loops like
Date.now() + timeout+while (...)+waitForTimeout(...). - Example to avoid:
const deadline = Date.now() + 8000; while (Date.now() < deadline) { if (await ready.isVisible()) return true; await page.waitForTimeout(250); }
Preferred pattern (explicit steps):
- Wait for page load once.
- Check blockers/negative states (e.g. login prompt) explicitly.
- Wait for one positive marker with
locator.waitFor({ state: 'visible', timeout }).
-
Use Chrome DevTools MCP to inspect DOM: Before writing locators, use
take_snapshotorevaluate_scriptto understand actual element IDs, classes, and structure. Page controls often use<span onclick>not anchor tags. -
Use the test fixture: All tests must use
createBrowserSuitefromtest-fixture.ts:import { createBrowserSuite, it, expect } from '../helpers/test-fixture'; createBrowserSuite('Suite Name', { userType: 'regular' }, (ctx) => { it('test', async () => { const { upcomingPage, resultsPage } = ctx.pages; await upcomingPage.open(); }); });Benefits:
- Automatic tracing per test
- Failure screenshots captured automatically
- All page objects pre-instantiated in
ctx.pages - Proper browser cleanup
-
Debugging artifacts: On failure, find screenshots and traces in
tests/e2e/test-results/:- Screenshots:
{test-name}-failure-{timestamp}.png - Traces:
{test-name}-{timestamp}.zip - View traces:
npx playwright show-trace {trace}.zip
- Screenshots:
Page Object Pattern
Pass BrowserManager, not Page, into page objects. Do not import or type against playwright-core directly — agent-browser wraps it.
Resolve page once in BasePage's constructor so all subclasses access this.page without repeating the call:
// tests/e2e/pages/BasePage.ts
import { BrowserManager } from 'agent-browser/dist/browser.js';
export class BasePage {
protected page: ReturnType<BrowserManager['getPage']>;
constructor(protected browser: BrowserManager) {
this.page = browser.getPage();
}
protected async navigate(path: string, options?: { waitUntil?: string }) {
await this.page.goto(`http://localhost:8000${path}`, options as any);
}
}
Define locators as readonly class properties, not inline in methods — makes selectors easy to find and update:
export class OrganizePage extends BasePage {
readonly loginRequired = this.page.locator('text=Login required');
// .first() because navbar and body both contain this text
readonly loginButton = this.page.locator('text=Login via NetrunnerDB').first();
readonly logoutButton = this.page.locator('#button-logout');
}
OAuth Login Helper
The loginUser(browser, 'regular' | 'admin') helper in e2e/helpers/auth.ts performs the real browser OAuth flow:
- Clears all browser cookies (
page.context().clearCookies()) — isolates each test - Navigates to
/oauth2/redirect— the app redirects to NetrunnerDB - Waits for the NRDB domain, fills
_username/_password, submits - Calls
waitForLoadState('domcontentloaded')after submit to let NRDB process the login - If the OAuth "Allow" form appears (first auth or re-auth), clicks it (10 s window)
- Waits up to 30 s for redirect back to
http://localhost:8000
Call clearSession(browser) at the start of logged-out tests to ensure no leftover cookies.
Parameterized Tests
Use it.each instead of BDD Scenario Outlines:
it.each([
{ filter: 'cardpool', value: 'System Gateway', expected: 15 },
{ filter: 'cardpool', value: 'Uprising', expected: 8 },
{ filter: 'type', value: 'GNK', expected: 20 },
])('filters by $filter = $value shows $expected results',
async ({ filter, value, expected }) => {
await resultsPage.applyFilter(filter, value);
expect(await resultsPage.getTournamentCount()).toBe(expected);
}
);
API Mocking
// tests/e2e/helpers/mockApi.ts
export async function mockResultsApi(browser: BrowserManager) {
await browser.execute('network route "**/api/tournaments/results" --body',
JSON.stringify(resultsFixture));
}
レビュー
まだレビューはありません。使ってみた感想をお寄せください。