Agent skill
e2e
E2E testing patterns for kspec web UI. Use when writing Playwright tests, debugging hydration issues, or working with daemon fixtures.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/e2e-kynetic-ai-kynetic-spec
SKILL.md
E2E Testing Skill
Learnings and patterns from completed E2E work (tasks.spec.ts, items.spec.ts).
Quick Reference
| Scenario | Pattern | Example |
|---|---|---|
| Isolated daemon | import { test } from '../fixtures/test-base' |
Every E2E test |
| Select dropdown (bits-ui) | Click trigger → getByRole('option') or locator('[data-slot="select-item"]') |
Filter tests |
| Text input | pressSequentially() with delay |
Tag filter |
| Wait for API | waitForRequest() |
Action tests |
| Wait for URL | waitForURL(/pattern/) |
Navigation tests |
| Scoped selectors | .locator('> div').first().getByTestId() |
Tree nav |
| Content check | toContainText() not just toBeVisible() |
All tests |
Fixture Architecture
Test-base.ts Pattern
The test-base.ts fixture creates a completely isolated environment per test:
// Key steps performed automatically:
1. Kill any existing daemon on port 3456
2. Create temp dir with .kspec/ subdirectory
3. Copy E2E fixtures from packages/web-ui/tests/fixtures/
4. Initialize git repo with test user config
5. Create fake shadow worktree structure:
- .git/worktrees/-kspec/ directory
- .kspec/.git file with "gitdir:" pointer
6. Start daemon: kspec serve start --daemon --port 3456
7. Set WEB_UI_DIR for static serving
8. Wait 2 seconds for daemon readiness
9. Provide isolated fixture to test
10. Cleanup: stop daemon, remove temp directory
Key file locations:
packages/web-ui/tests/fixtures/- E2E YAML fixturespackages/web-ui/tests/fixtures/test-base.ts- Fixture setuppackages/web-ui/tests/e2e/*.spec.ts- Test files
Fixture Isolation is Non-Negotiable
E2E fixtures MUST be separate from unit test fixtures:
tests/fixtures/ ← Unit test fixtures (shared)
packages/web-ui/tests/fixtures/ ← E2E fixtures (isolated)
Why: E2E daemon mutations affect fixtures. Unit tests need stable data.
ULID Format Requirements
Critical: ULIDs use Crockford base32 which excludes I, L, O, U.
// INVALID ULIDs - contain forbidden characters
'01TASK100...' // I is invalid
'01MODULE0...' // O and U are invalid
'01TRAIT10...' // I is invalid
// VALID ULIDs - only 0-9, A-H, J-K, M-N, P-T, V-Z
'01TASK0000...' // T, A, S, K are all valid
'01TRATT100...' // No I, L, O, U
Recommendation: Use testUlid() helper from tests/helpers/cli.ts:
import { testUlid, testUlids } from '../../helpers/cli';
const taskId = testUlid('TASK'); // '01TASK00000000000000000000'
const traitId = testUlid('TRAIT'); // '01TRAJT0000000000000000000' (I auto-replaced)
const [id1, id2, id3] = testUlids('TASK', 3);
Svelte 5 SSR/Hydration Gotchas
Problem: $state Variables Don't React After Hydration
Symptom: Detail panel state updates correctly but UI doesn't re-render.
Root cause: adapter-static pre-renders pages. After hydration, reactive subscriptions with $state can disconnect.
Solution 1 (Preferred): Disable SSR for interactive pages
// +page.ts
export const ssr = false;
Solution 2: Use $state object pattern with flushSync
let panel = $state<{ open: boolean; task: Task | null }>({
open: false,
task: null
});
// When updating, use flushSync for synchronous DOM update
import { flushSync } from 'svelte';
flushSync(() => {
panel.task = task;
panel.open = true;
});
Problem: URL Param Changes Don't Trigger Reactivity
Symptom: Clicking navigation that uses goto() doesn't reload data.
Root cause: Client-side navigation bypasses component mount.
Solution: Use reactive $: blocks instead of onMount:
// BAD - only runs once
onMount(() => {
loadItem(ref);
});
// GOOD - reacts to URL changes
$: if (ref && open) {
loadItem(ref);
}
Problem: bits-ui Select Returns Array
Symptom: Selecting "in_progress" produces ['i', 'n', '_', 'p', 'r', 'o', 'g', 'r', 'e', 's', 's', 'in_progress']
Solution: Take last element if array:
bits-ui Select E2E Selectors
bits-ui Select components use both standard ARIA roles and data-slot attributes. Either selector approach works:
// Open dropdown
await page.getByTestId('my-select-trigger').click();
// Option 1: Standard ARIA role (recommended - more semantic)
await page.getByRole('option', { name: 'Option Name' }).click();
// Option 2: data-slot attribute (alternative)
const dropdown = page.locator('[data-slot="select-content"]');
await dropdown.locator('[data-slot="select-item"]').filter({ hasText: 'Option Name' }).click();
Note: Each select item element has both role="option" and data-slot="select-item" attributes.
function updateFilter(key: string, value: string | string[] | undefined) {
let actualValue: string | undefined;
if (Array.isArray(value)) {
actualValue = value.length > 0 ? value[value.length - 1] : undefined;
} else {
actualValue = value;
}
// ... rest of filter logic
}
Common Test Patterns
Filter Testing
// Select dropdown - both role="option" and data-slot work
const filterStatus = page.getByTestId('filter-status');
await filterStatus.click();
// Click option using ARIA role (recommended)
await page.getByRole('option', { name: 'Pending', exact: true }).click();
await page.waitForURL(/status=pending/);
// Text input - use pressSequentially, not fill()
const filterTag = page.getByTestId('filter-tag');
await filterTag.click();
await filterTag.pressSequentially('e2e', { delay: 50 });
await page.waitForURL(/tag=e2e/);
Detail Panel Testing
// Open panel
const item = page.getByTestId('list-item').first();
await item.click();
// Verify panel content
const panel = page.getByTestId('detail-panel');
await expect(panel).toBeVisible();
await expect(panel.getByTestId('title')).toContainText('Expected Title');
API Request Verification
// Verify API call was made, not just UI state
const requestPromise = page.waitForRequest((req) =>
req.url().includes('/api/tasks/') && req.url().includes('/start')
);
await startButton.click();
const request = await requestPromise;
expect(request.method()).toBe('POST');
Tree/Hierarchy Navigation
// Get module node
const specTree = page.getByTestId('spec-tree').first();
const moduleNode = specTree.locator('[data-testid*="tree-node-module"]').first();
// Expand module (expand toggle is sibling to node-title)
const expandToggle = moduleNode.locator('> div').first().getByTestId('expand-toggle');
await expandToggle.click();
// Find child feature after expansion
const childContainer = moduleNode.getByTestId('tree-node-child');
const featureNode = childContainer.locator('[data-testid*="tree-node-feature"]').first();
await expect(featureNode).toBeVisible();
Navigation Verification
// Verify navigation occurred
const link = page.getByTestId('spec-ref-link');
await link.click();
await page.waitForURL(/\/items\?ref=/);
// Verify content loaded after navigation
const detail = page.getByTestId('item-detail');
await expect(detail).toBeVisible();
data-testid Conventions
| Component | Pattern | Example |
|---|---|---|
| Lists | {entity}-list |
task-list, spec-tree |
| List items | {entity}-list-item or tree-node-{type} |
task-list-item, tree-node-module |
| Detail panels | {entity}-detail-panel |
task-detail-panel, spec-detail-panel |
| Filters | filter-{name} |
filter-status, filter-tag |
| Badges | {entity}-{field} |
task-status-badge, task-priority |
| Buttons | {action}-{entity}-button or {action}-button |
start-task-button, expand-toggle |
| Counts | {entity}-count-{status} |
task-count-ready, task-count-blocked |
WebSocket Testing
Current Limitation
Daemon WebSocket in E2E tests returns 200 instead of 101 for upgrade handshake.
Impact: WebSocket connection tests fail in E2E environment.
Workaround Options
-
Skip with documentation:
typescripttest.skip('counts animate on WebSocket update', async ({ page }) => { // Skipped: Daemon WebSocket upgrade returns 200 in E2E environment // See: AGENTS.md "CI Limitations" section }); -
Test API triggers instead: Verify API calls that would trigger WebSocket events:
typescripttest('task update triggers refetch', async ({ page, daemon }) => { // Trigger update via API const response = await fetch(`${daemon.url}/api/tasks/${id}/start`, { method: 'POST' }); expect(response.ok).toBe(true); // Verify UI updates (without WebSocket, need page reload) await page.reload(); await expect(page.getByTestId('task-status')).toContainText('In Progress'); });
When WebSocket Works
Full test pattern for future:
test('counts animate on update', async ({ page, daemon }) => {
await page.goto('/');
// Get initial count
const countEl = page.getByTestId('task-count-in_progress');
const initialCount = await countEl.textContent();
// Trigger task start via API
await fetch(`${daemon.url}/api/tasks/${taskId}/start`, { method: 'POST' });
// Wait for animation class
await expect(countEl).toHaveClass(/animate-pulse/);
// Verify count changed
await expect(countEl).not.toContainText(initialCount!);
});
New E2E Test Checklist
Before writing a new E2E test:
- Fixtures have required data (tasks, items, meta with variety)
- ULIDs are valid Crockford base32 (no I, L, O, U)
- Import from
../fixtures/test-base(not Playwright direct) - Use
daemonfixture parameter for isolated environment - Components have data-testid attributes for selectors
- Use scoped selectors with
.first()for ambiguous queries - Verify API calls with
waitForRequest(), not just UI - Check URL params with
waitForURL()for navigation tests - Test content with
toContainText(), not just visibility - Include variety: happy path, edge cases, responsive
Fixture Data Requirements
Task Status Variety
Fixtures should include tasks with all statuses:
pending(ready - no unmet dependencies)in_progress(started)pending_review(code done, awaiting merge)blocked(has unmet dependencies)completed(done)
Hierarchy for Tree Tests
modules:
- _ulid: 01M0DULE00000000000000000
type: module
features:
- _ulid: 01FEATURE0000000000000000
type: feature
requirements:
- _ulid: 01REQVREMENT000000000000
type: requirement
Linked Data
- Tasks with
spec_refpointing to spec items - Items with
traitsarray referencing trait items - Observations with
spec_refortask_reflinks
Debugging Tips
Test Passes Locally, Fails in CI
- Check file watchers: CI containers don't support recursive
fs.watch. Tests using watchers are skipped in CI. - Check port conflicts: E2E uses port 3456. Previous daemon may not have cleaned up.
- Check fixture paths: CI paths may differ. Use relative paths in fixtures.
Element Not Found
- SSR timing: Add
{ timeout: 5000 }to assertions - Scope correctly: Use
element.getByTestId()notpage.getByTestId() - Wait for load:
await page.waitForLoadState('networkidle')
State Not Updating
- Check SSR: Add
export const ssr = falseto page - Check reactivity: Use
$:blocks for URL-driven state - Check flushSync: For synchronous DOM updates
Anti-Patterns
Don't: Conditional Assertions
// BAD - hides failures
if (await element.isVisible()) {
await expect(element).toContainText('value');
}
// GOOD - fails clearly
await expect(element).toBeVisible();
await expect(element).toContainText('value');
Don't: Share Fixtures with Unit Tests
// BAD - mutations affect unit tests
await copyFixtures('../../tests/fixtures/');
// GOOD - isolated E2E fixtures
await copyFixtures('../fixtures/'); // packages/web-ui/tests/fixtures/
Don't: Test UI State Only
// BAD - doesn't verify API call
await startButton.click();
await expect(statusBadge).toContainText('In Progress');
// GOOD - verifies both
const req = await page.waitForRequest(r => r.url().includes('/start'));
expect(req.method()).toBe('POST');
await expect(statusBadge).toContainText('In Progress');
Related Skills
/svelte-5- Svelte 5 patterns, SSR, reactivity/local-review- Pre-PR quality checks/task-work- Task lifecycle workflow
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?