Skip to content

Commit 33252a6

Browse files
authored
test(app): general settings, shortcuts, providers and status popover (anomalyco#11517)
1 parent e70d984 commit 33252a6

13 files changed

Lines changed: 1185 additions & 67 deletions

packages/app/e2e/AGENTS.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# E2E Testing Guide
2+
3+
## Build/Lint/Test Commands
4+
5+
```bash
6+
# Run all e2e tests
7+
bun test:e2e
8+
9+
# Run specific test file
10+
bun test:e2e -- app/home.spec.ts
11+
12+
# Run single test by title
13+
bun test:e2e -- -g "home renders and shows core entrypoints"
14+
15+
# Run tests with UI mode (for debugging)
16+
bun test:e2e:ui
17+
18+
# Run tests locally with full server setup
19+
bun test:e2e:local
20+
21+
# View test report
22+
bun test:e2e:report
23+
24+
# Typecheck
25+
bun typecheck
26+
```
27+
28+
## Test Structure
29+
30+
All tests live in `packages/app/e2e/`:
31+
32+
```
33+
e2e/
34+
├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk)
35+
├── actions.ts # Reusable action helpers
36+
├── selectors.ts # DOM selectors
37+
├── utils.ts # Utilities (serverUrl, modKey, path helpers)
38+
└── [feature]/
39+
└── *.spec.ts # Test files
40+
```
41+
42+
## Test Patterns
43+
44+
### Basic Test Structure
45+
46+
```typescript
47+
import { test, expect } from "../fixtures"
48+
import { promptSelector } from "../selectors"
49+
import { withSession } from "../actions"
50+
51+
test("test description", async ({ page, sdk, gotoSession }) => {
52+
await gotoSession() // or gotoSession(sessionID)
53+
54+
// Your test code
55+
await expect(page.locator(promptSelector)).toBeVisible()
56+
})
57+
```
58+
59+
### Using Fixtures
60+
61+
- `page` - Playwright page
62+
- `sdk` - OpenCode SDK client for API calls
63+
- `gotoSession(sessionID?)` - Navigate to session
64+
65+
### Helper Functions
66+
67+
**Actions** (`actions.ts`):
68+
69+
- `openPalette(page)` - Open command palette
70+
- `openSettings(page)` - Open settings dialog
71+
- `closeDialog(page, dialog)` - Close any dialog
72+
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
73+
- `withSession(sdk, title, callback)` - Create temp session
74+
- `clickListItem(container, filter)` - Click list item by key/text
75+
76+
**Selectors** (`selectors.ts`):
77+
78+
- `promptSelector` - Prompt input
79+
- `terminalSelector` - Terminal panel
80+
- `sessionItemSelector(id)` - Session in sidebar
81+
- `listItemSelector` - Generic list items
82+
83+
**Utils** (`utils.ts`):
84+
85+
- `modKey` - Meta (Mac) or Control (Linux/Win)
86+
- `serverUrl` - Backend server URL
87+
- `sessionPath(dir, id?)` - Build session URL
88+
89+
## Code Style Guidelines
90+
91+
### Imports
92+
93+
Always import from `../fixtures`, not `@playwright/test`:
94+
95+
```typescript
96+
// ✅ Good
97+
import { test, expect } from "../fixtures"
98+
99+
// ❌ Bad
100+
import { test, expect } from "@playwright/test"
101+
```
102+
103+
### Naming Conventions
104+
105+
- Test files: `feature-name.spec.ts`
106+
- Test names: lowercase, descriptive: `"sidebar can be toggled"`
107+
- Variables: camelCase
108+
- Constants: SCREAMING_SNAKE_CASE
109+
110+
### Error Handling
111+
112+
Tests should clean up after themselves:
113+
114+
```typescript
115+
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
116+
await withSession(sdk, "test session", async (session) => {
117+
await gotoSession(session.id)
118+
// Test code...
119+
}) // Auto-deletes session
120+
})
121+
```
122+
123+
### Timeouts
124+
125+
Default: 60s per test, 10s per assertion. Override when needed:
126+
127+
```typescript
128+
test.setTimeout(120_000) // For long LLM operations
129+
test("slow test", async () => {
130+
await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
131+
})
132+
```
133+
134+
### Selectors
135+
136+
Use `data-component`, `data-action`, or semantic roles:
137+
138+
```typescript
139+
// ✅ Good
140+
await page.locator('[data-component="prompt-input"]').click()
141+
await page.getByRole("button", { name: "Open settings" }).click()
142+
143+
// ❌ Bad
144+
await page.locator(".css-class-name").click()
145+
await page.locator("#id-name").click()
146+
```
147+
148+
### Keyboard Shortcuts
149+
150+
Use `modKey` for cross-platform compatibility:
151+
152+
```typescript
153+
import { modKey } from "../utils"
154+
155+
await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
156+
await page.keyboard.press(`${modKey}+Comma`) // Open settings
157+
```
158+
159+
## Writing New Tests
160+
161+
1. Choose appropriate folder or create new one
162+
2. Import from `../fixtures`
163+
3. Use helper functions from `../actions` and `../selectors`
164+
4. Clean up any created resources
165+
5. Use specific selectors (avoid CSS classes)
166+
6. Test one feature per test file
167+
168+
## Local Development
169+
170+
For UI debugging, use:
171+
172+
```bash
173+
bun test:e2e:ui
174+
```
175+
176+
This opens Playwright's interactive UI for step-through debugging.

packages/app/e2e/actions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,25 @@ export async function withSession<T>(
269269
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
270270
}
271271
}
272+
273+
export async function openStatusPopover(page: Page) {
274+
await defocus(page)
275+
276+
const rightSection = page.locator(titlebarRightSelector)
277+
const trigger = rightSection.getByRole("button", { name: /status/i }).first()
278+
279+
const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') })
280+
281+
const opened = await popoverBody
282+
.isVisible()
283+
.then((x) => x)
284+
.catch(() => false)
285+
286+
if (!opened) {
287+
await expect(trigger).toBeVisible()
288+
await trigger.click()
289+
await expect(popoverBody).toBeVisible()
290+
}
291+
292+
return { rightSection, popoverBody }
293+
}

packages/app/e2e/fixtures.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { seedProjects } from "./actions"
33
import { promptSelector } from "./selectors"
44
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
55

6+
7+
export const settingsKey = "settings.v3"
8+
69
type TestFixtures = {
710
sdk: ReturnType<typeof createSdk>
811
gotoSession: (sessionID?: string) => Promise<void>

packages/app/e2e/selectors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ export const terminalSelector = '[data-component="terminal"]'
33

44
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
55
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
6+
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
7+
export const settingsThemeSelector = '[data-action="settings-theme"]'
8+
export const settingsFontSelector = '[data-action="settings-font"]'
9+
export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
10+
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
11+
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
12+
export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
13+
export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
14+
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
15+
export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
16+
export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
617

718
export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
819

@@ -33,3 +44,5 @@ export const listItemSelector = '[data-slot="list-item"]'
3344
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
3445

3546
export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
47+
48+
export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]`

0 commit comments

Comments
 (0)