Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ jobs:

- run: bun install --frozen-lockfile

# Install agent-browser + Chromium
- run: bun add -g agent-browser
# Install agent-browser + Chromium.
# Pinned: the driver is a test dependency that can otherwise change under the
# suite between two runs of the same commit. 0.32.4 is the version the popover
# tests were last proven green on in CI; 0.34.0 fails them even with clickUntil.
- run: bun add -g agent-browser@0.32.4
- run: agent-browser install

# Run Contember migrations & seed
Expand Down
4 changes: 4 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
[test]
preload = ["./tests/setup.ts"]
# NOTE: bun ignores this key — the per-test timeout stays at its 5000ms default.
# Verified with a 6s test, which is killed at 5002ms regardless of the value here.
# Suites that legitimately need longer pass --timeout on the command line
# (see the test:browser script).
timeout = 15000

[install]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev": "tsc --build --watch",
"typecheck": "tsc --build",
"test": "bun test tests/unit tests/react tests/cases tests/*.test.ts tests/*.test.tsx",
"test:browser": "bun test tests/browser/",
"test:browser": "bun test --timeout 30000 tests/browser/",
"test:all": "bun test",
"playground": "cd packages/example && bun run dev",
"playground:contember": "cd packages/example && VITE_CONTEMBER_API_URL=http://localhost:1581 VITE_CONTEMBER_API_TOKEN=0000000000000000000000000000000000000000 bun run dev",
Expand Down
22 changes: 13 additions & 9 deletions tests/browser/articleEditor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from 'bun:test'
import { browserTest, el, tid, waitFor } from './browser.js'
import { browserTest, clickUntil, el, tid, waitFor } from './browser.js'

browserTest('Article Editor', () => {
test('section renders with all sub-components', () => {
Expand All @@ -24,10 +24,12 @@ browserTest('Article Editor', () => {
// Type in the search input to filter, then click the filtered option
waitFor(() => el('[role="dialog"] input').exists)
el('[role="dialog"] input').fill('Jane')
waitFor(() => el('[role="dialog"] button[class]').exists)
el('[role="dialog"] button[class]').click()

waitFor(() => !el('article-save-button').isDisabled)
// Wait for the FILTERED option — the stale pre-filter list also has buttons
waitFor(() => el('[role="dialog"] button[class]').text.includes('Jane'))
clickUntil(
() => el('[role="dialog"] button[class]'),
() => !el('article-save-button').isDisabled,
)
expect(el('article-dirty-notice').exists).toBe(true)
})

Expand All @@ -46,10 +48,12 @@ browserTest('Article Editor', () => {
// Search for the tag and click it
waitFor(() => el('[role="dialog"] input').exists)
el('[role="dialog"] input').fill('TypeScript')
waitFor(() => el('[role="dialog"] button[class]').exists)
el('[role="dialog"] button[class]').click()

waitFor(() => el('tag-badge-TypeScript').exists)
// Wait for the FILTERED option — the stale pre-filter list also has buttons
waitFor(() => el('[role="dialog"] button[class]').text.includes('TypeScript'))
clickUntil(
() => el('[role="dialog"] button[class]'),
() => el('tag-badge-TypeScript').exists,
)
})

}, 'article-editor')
12 changes: 7 additions & 5 deletions tests/browser/authorSelect.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from 'bun:test'
import { browserTest, el, tid, waitFor } from './browser.js'
import { browserTest, clickUntil, el, tid, waitFor } from './browser.js'

browserTest('Article with Author Select', () => {
test('section renders', () => {
Expand All @@ -21,10 +21,12 @@ browserTest('Article with Author Select', () => {
// Type to filter and click an option
waitFor(() => el('[role="dialog"] input').exists)
el('[role="dialog"] input').fill('Bob')
waitFor(() => el('[role="dialog"] button[class]').exists)
el('[role="dialog"] button[class]').click()

waitFor(() => !el('author-select-save-button').isDisabled)
// Wait for the FILTERED option — the stale pre-filter list also has buttons
waitFor(() => el('[role="dialog"] button[class]').text.includes('Bob'))
clickUntil(
() => el('[role="dialog"] button[class]'),
() => !el('author-select-save-button').isDisabled,
)
expect(el('current-author-display').text).toContain('Changes will be applied on save')
})

Expand Down
31 changes: 31 additions & 0 deletions tests/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,37 @@ export function el(selector: string): ElementHandle {
}
}

/**
* Click until the expected outcome materializes — popover option lists re-render
* async (debounced fetches), so a click can land on a node mid-remount and get lost.
* Checks the condition before re-clicking, so a registered click is never repeated.
*/
export function clickUntil(
target: () => ElementHandle,
condition: () => boolean,
{ attempts = 3, settle = 3_000 }: { attempts?: number; settle?: number } = {},
): void {
for (let i = 0; i < attempts; i++) {
try {
if (condition()) return
} catch {
// not ready yet
}
try {
target().click()
} catch {
// target gone — the previous click may have registered and closed the popover
}
try {
waitFor(condition, { timeout: settle })
return
} catch {
// outcome didn't materialize — re-click
}
}
waitFor(condition, { timeout: settle })
}

/**
* Build a `[data-testid="..."]` selector for compound selectors.
* Usage: `el(\`\${tid('parent')} button\`)`
Expand Down