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
4 changes: 4 additions & 0 deletions packages/app/component-tests/activity-chips.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { expect, story } from "../../stories/playwright/story"

story("shows how long each background item has been running, ticking while open", async ({ mount, page }) => {
// Time moves only when the test advances it, so waiting for fonts cannot add a tick.
await page.clock.install({ time: new Date("2026-10-19T12:00:00Z") })
await page.clock.pauseAt(new Date("2026-10-19T12:00:00Z"))
await mount("session-activity-chips--subagents-and-shells")

await page.locator('[data-slot="session-activity-chip"]').click()
const elapsed = page.locator('[data-slot="session-activity-panel"] [data-slot="session-activity-elapsed"]')

// The story starts its subagents 12 s and 65 s ago, and its shells 62 min and 341 s ago.
await expect(elapsed).toHaveText(["12s", "1m 05s", "1h 02m", "5m 41s"])
// A late Inter swap widens the panel; measure after fonts settle.
await page.evaluate(() => document.fonts.ready.then(() => undefined))
const width = await page.locator('[data-slot="session-activity-panel"]').evaluate((node) => node.clientWidth)

await page.clock.runFor(1_000)
Expand Down
6 changes: 3 additions & 3 deletions packages/app/component-tests/composer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ story("grows suggestions while preserving visible timeline context", async ({ mo
const boundary = component.locator('[data-slot="composer-suggestion-boundary-story"]')
const suggestions = component.locator('[data-component="composer-suggestions"]')

await expect(suggestions).toHaveCSS("max-height", "166px")
await expect(suggestions).toHaveCSS("max-height", "164px")
await expect(suggestions).toHaveCSS("scroll-padding-bottom", "18px")
await expect.poll(() => suggestions.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
await expect
Expand All @@ -497,12 +497,12 @@ story("grows suggestions while preserving visible timeline context", async ({ mo
await boundary.evaluate((element) => {
element.style.height = "400px"
})
await expect(suggestions).toHaveCSS("max-height", "306px")
await expect(suggestions).toHaveCSS("max-height", "304px")
})

story("ignores a detached suggestion boundary", async ({ mount }) => {
const component = await mount("opencode-composer-flow--detached-command-suggestion-boundary")
await expect(component.locator('[data-component="composer-suggestions"]')).toHaveCSS("max-height", "306px")
await expect(component.locator('[data-component="composer-suggestions"]')).toHaveCSS("max-height", "304px")
})

// ThemeProvider writes resolved token values into a <style> block, so toggling data-color-scheme by hand
Expand Down
88 changes: 43 additions & 45 deletions packages/app/component-tests/timeline-virtualizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,53 +59,51 @@ for (const input of [
{ name: "offset-only", count: 1, rowHeight: 600, reveal: { scrollTop: 0 } },
{ name: "zero-height", count: 4, rowHeight: 60, reveal: {} },
]) {
story(
`reveals ready measured rows after an ${input.name} reconnect${input.name === "offset-only" ? " @quarantine" : ""}`,
async ({ page }) => {
await page.evaluate(
async ({ fixture, input }) => {
const { mountTimelineVirtualizer } = await import(fixture)
mountTimelineVirtualizer(input)
},
{ fixture, input },
)
const root = page.getByTestId("timeline-virtualizer-fixture")
const content = root.locator("[data-timeline-virtual-content]")
await expect(root).toHaveAttribute("data-observed-height", "180")
await expect(content).toHaveCSS("visibility", "hidden")
await expect(content.locator("[data-timeline-key]")).toHaveCount(1)
story(`reveals ready measured rows after an ${input.name} reconnect`, async ({ page }) => {
await page.evaluate(
async ({ fixture, input }) => {
const { mountTimelineVirtualizer } = await import(fixture)
mountTimelineVirtualizer(input)
},
{ fixture, input },
)
const root = page.getByTestId("timeline-virtualizer-fixture")
const content = root.locator("[data-timeline-virtual-content]")
await expect(root).toHaveAttribute("data-observed-height", "180")
await expect(content).toHaveCSS("visibility", "hidden")
await expect(content.locator("[data-timeline-key]")).toHaveCount(1)

if (input.name === "offset-only") {
await expect(root).toHaveAttribute("data-last-scroll-top", "484")
await root.locator("[data-scrollable]").dispatchEvent("wheel", { deltaY: -1 })
await expect(root.getByTestId("timeline-controls")).toHaveAttribute("data-pinned", "false")
}
if (input.name === "offset-only") {
// Pinned to the end: the 600px row and the virtualizer's 82px tail, less the 180px viewport.
await expect(root).toHaveAttribute("data-last-scroll-top", "502")
await root.locator("[data-scrollable]").dispatchEvent("wheel", { deltaY: -1 })
await expect(root.getByTestId("timeline-controls")).toHaveAttribute("data-pinned", "false")
}

if (input.name === "zero-height") {
await root.getByRole("button", { name: "Hide viewport", exact: true }).click()
// Wait for ResizeObserver to clear the actual range, not just for display:none.
await expect(root).toHaveAttribute("data-observed-height", "0")
await expect(content.locator("[data-timeline-key]")).toHaveCount(0)
}
if (input.name === "zero-height") {
await root.getByRole("button", { name: "Hide viewport", exact: true }).click()
// Wait for ResizeObserver to clear the actual range, not just for display:none.
await expect(root).toHaveAttribute("data-observed-height", "0")
await expect(content.locator("[data-timeline-key]")).toHaveCount(0)
}

await expect(root).not.toHaveAttribute("data-first-reveal")
const resizes = await root.getAttribute("data-viewport-resizes")
await root.getByRole("button", { name: "Reconnect ready rows", exact: true }).click()
await expect(content).toHaveCSS("visibility", "visible")
await expect(root).toHaveAttribute("data-first-reveal", /.+/)
expect(await root.evaluate((element) => JSON.parse(element.dataset.firstReveal!))).toMatchObject({
rows: input.count === 1 ? [0] : [0, 1, 2, 3],
pendingMarkdown: 0,
clipped: [],
viewportHeight: 180,
...input.reveal,
})
await expect(root).not.toHaveAttribute("data-first-reveal")
const resizes = await root.getAttribute("data-viewport-resizes")
await root.getByRole("button", { name: "Reconnect ready rows", exact: true }).click()
await expect(content).toHaveCSS("visibility", "visible")
await expect(root).toHaveAttribute("data-first-reveal", /.+/)
expect(await root.evaluate((element) => JSON.parse(element.dataset.firstReveal!))).toMatchObject({
rows: input.count === 1 ? [0] : [0, 1, 2, 3],
pendingMarkdown: 0,
clipped: [],
viewportHeight: 180,
...input.reveal,
})

if (input.name === "offset-only") {
// This repair must not depend on another native scroll or resize delivery.
await expect(root).toHaveAttribute("data-scrolls", "0")
await expect(root).toHaveAttribute("data-viewport-resizes", resizes!)
}
},
)
if (input.name === "offset-only") {
// This repair must not depend on another native scroll or resize delivery.
await expect(root).toHaveAttribute("data-scrolls", "0")
await expect(root).toHaveAttribute("data-viewport-resizes", resizes!)
}
})
}
59 changes: 28 additions & 31 deletions packages/app/component-tests/websearch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,39 +51,36 @@ story("sizes provider options to their content", async ({ mount, page }) => {
for (const width of [360, 1200]) {
for (const direction of ["ltr", "rtl"]) {
for (const theme of ["light", "dark"]) {
story(
`selects and confirms a provider at ${width}px in ${direction} ${theme}${width === 360 ? " @quarantine" : ""}`,
async ({ mount, page }) => {
await page.setViewportSize({ width, height: 900 })
story(`selects and confirms a provider at ${width}px in ${direction} ${theme}`, async ({ mount, page }) => {
await page.setViewportSize({ width, height: 900 })

const component = await mount("app-current-session-surface--web-search-request", {
globals: { theme, direction },
})
const component = await mount("app-current-session-surface--web-search-request", {
globals: { theme, direction },
})

const card = component.getByRole("region", { name: "Third-party web search" })
const select = card.getByRole("button", { name: /^Search provider/ })
await expect(select).toBeEnabled()
await expect(card).toHaveCSS("direction", direction)
expect(await card.evaluate((node) => node.scrollWidth <= node.clientWidth)).toBe(true)
await select.focus()
await select.press("Enter")
const list = page.getByRole("listbox", { name: "Search provider", exact: true })
await expect(list).toHaveCSS("direction", direction)
await list.getByRole("option", { name: "Parallel", exact: true }).click()
await expect(select).toHaveText("Parallel")
await expect(card).toBeVisible()
await expect(component.getByRole("status")).toHaveText("Ready")
await expect(select).toBeFocused()
await select.press("Tab")
await expect(card.getByRole("button", { name: "Don’t use search", exact: true })).toBeFocused()
await page.keyboard.press("Tab")
const enable = card.getByRole("button", { name: "Enable", exact: true })
await expect(enable).toBeFocused()
await enable.press("Enter")
await expect(component.getByRole("status")).toHaveText("Web search selection (local only): parallel")
await expect(card).toHaveCount(0)
},
)
const card = component.getByRole("region", { name: "Third-party web search" })
const select = card.getByRole("button", { name: /^Search provider/ })
await expect(select).toBeEnabled()
await expect(card).toHaveCSS("direction", direction)
expect(await card.evaluate((node) => node.scrollWidth <= node.clientWidth)).toBe(true)
await select.focus()
await select.press("Enter")
const list = page.getByRole("listbox", { name: "Search provider", exact: true })
await expect(list).toHaveCSS("direction", direction)
await list.getByRole("option", { name: "Parallel", exact: true }).click()
await expect(select).toHaveText("Parallel")
await expect(card).toBeVisible()
await expect(component.getByRole("status")).toHaveText("Ready")
await expect(select).toBeFocused()
await select.press("Tab")
await expect(card.getByRole("button", { name: "Don’t use search", exact: true })).toBeFocused()
await page.keyboard.press("Tab")
const enable = card.getByRole("button", { name: "Enable", exact: true })
await expect(enable).toBeFocused()
await enable.press("Enter")
await expect(component.getByRole("status")).toHaveText("Web search selection (local only): parallel")
await expect(card).toHaveCount(0)
})
}
}
}
40 changes: 5 additions & 35 deletions packages/app/e2e/QUARANTINE.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,12 @@
# Quarantined browser tests

Beta pull request and push runs skip the 23 tests tagged `@quarantine` (`--grep-invert @quarantine`). Most failed in every beta run, so they gave no signal and slowed every leg with retries. `dev` runs are unchanged.
Beta pull request and push runs skip tests tagged `@quarantine` (`--grep-invert @quarantine`), so a test that fails in every run stops slowing every leg with retries. `dev` runs are unchanged.

To run only the quarantined tests, use `gh workflow run check-test.yml --ref beta -f quarantine=true`. Their legs report without failing the e2e checks. `nightly-beta.yml` dispatches that run, plus a full beta run, every night. GitHub fires schedules only from the default branch (`dev`), so the nightly run starts once that workflow is on `dev`; until then, dispatch by hand.
To run only the quarantined tests, use `gh workflow run check-test.yml --ref beta -f quarantine=true`. Their legs report without failing the e2e checks. `nightly-beta.yml` dispatches that run, plus a full beta run, every night.

**Rule:** fix a quarantined test or delete it within 14 days of quarantine. When a fix lands, remove the tag and its row in the same pull request. Never quarantine a test that fails because of your own change; fix the test or the change.

Quarantined 2026-10-21, so the deadline is **2026-11-04**; the rows first seen red on 2026-10-22 were added that day and are due **2026-11-05**. "First red" is the earliest beta run seen failing; runs before 2026-10-20 05:32 UTC were not collected. "Unowned" means `git blame` points at the history import (#53822).
No tests are quarantined. When you quarantine one, list it below with its failure signature, the date it was first seen red, and its owner.

## App e2e (`packages/app/e2e`)

| Test | Failure signature | First red | Owner |
| ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------ | ----------- |
| `regression/timeline-cache.spec.ts` › focuses Find in the selected cached timeline | `timeline-search-bar input` `toBeFocused()`: inactive | ≤ 2026-10-20 05:32 | unowned |
| `regression/timeline-cache.spec.ts` › disposes the old workspace's shell while destination history is loading | `[data-session-title]` text has a `smoke-project/` prefix | ≤ 2026-10-20 05:32 | unowned |
| `regression/mobile-timeline-scroll.spec.ts` › Pixel 7 › returns Home after scrolling the touch target out of view (touchEnd) | `Part 66.` `not.toBeInViewport()`: still in viewport | ≤ 2026-10-20 05:32 | unowned |
| `regression/mobile-timeline-scroll.spec.ts` › Pixel 7 › returns Home after scrolling the touch target out of view (touchCancel) | same | ≤ 2026-10-20 05:32 | unowned |
| `regression/mobile-timeline-scroll.spec.ts` › iPhone 13 › returns Home after scrolling the touch target out of view (touchEnd) | same | ≤ 2026-10-20 05:32 | unowned |
| `regression/mobile-timeline-scroll.spec.ts` › iPhone 13 › returns Home after scrolling the touch target out of view (touchCancel) | same | ≤ 2026-10-20 05:32 | unowned |
| `regression/new-session.spec.ts` › keeps the user message blue for a managed worktree | `user-message-text` color `rgb(245, 248, 255)`, received `rgb(242, 242, 242)` | ≤ 2026-10-20 11:29 | Luke Parker |
| `regression/new-session.spec.ts` › keeps the user message blue for the main root written with Windows case and separators | same | ≤ 2026-10-20 11:29 | Luke Parker |
| `regression/review.spec.ts` › open file tab browses, searches, and tracks missing files | file search request has an unexpected `directory` field | ≤ 2026-10-20 11:29 | unowned |
| `regression/review.spec.ts` › rereads a file each time an artifact link opens it | `first draft` never visible in `#review-panel` | ≤ 2026-10-20 11:29 | unowned |
| `regression/session-queue.spec.ts` › keeps finished tools above a pending steer follow-up | pending steer color `rgb(242, 242, 242)`, received `rgb(250, 250, 250)` | ≤ 2026-10-20 11:29 | unowned |
| `regression/tabs.spec.ts` › switching between busy sessions keeps the busy indicators and the activity capsule at rest | `html` keeps `data-test-capsule-missing` | ≤ 2026-10-20 05:32 | Kit Langton |
| `regression/review.spec.ts` › restores review state and the side-panel tab per session | heading `Alpha review state` not found | ≤ 2026-10-22 16:26 | unowned |
| `regression/session-queue.spec.ts` › reverting to another prompt replaces the file chips of an earlier restore | `Rewind to here` resolves to 2 buttons (strict mode) | ≤ 2026-10-22 16:26 | unowned |
| `regression/session-queue.spec.ts` › waiting steers group under one Pending line, each keeping its own actions beside its bubble | `Move to queue` resolves to 2 buttons (strict mode) | ≤ 2026-10-22 16:26 | Kit Langton |
| `regression/session-timeline-accessibility.spec.ts` › shows the agent name in message meta when agents are on | meta `Pending·Build·…`, expected `/^Build\s·\s/` | ≤ 2026-10-22 16:26 | Kit Langton |
| `regression/timeline-stream.spec.ts` › static projection › renders current protocol notices in CLI order | `session-timeline-notice` classes differ | ≤ 2026-10-22 16:26 | unowned |
| `regression/timeline-stream.spec.ts` › background shortcut › holds a revealed shell from far below just under its stuck group header | predicate expected `0`, received `1` (10 s) | ≤ 2026-10-22 16:26 | unowned |

## App component tests (`packages/app/component-tests`)

| Test | Failure signature | First red | Owner |
| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------ | ------- |
| `websearch.spec.ts` › selects and confirms a provider at 360px in ltr light | 60 s timeout clicking the `Parallel` option: element is not stable | ≤ 2026-10-20 06:39 | unowned |
| `websearch.spec.ts` › selects and confirms a provider at 360px in ltr dark | same | ≤ 2026-10-20 06:39 | unowned |
| `websearch.spec.ts` › selects and confirms a provider at 360px in rtl light | same | ≤ 2026-10-20 06:39 | unowned |
| `websearch.spec.ts` › selects and confirms a provider at 360px in rtl dark | same | ≤ 2026-10-20 06:39 | unowned |
| `timeline-virtualizer.spec.ts` › reveals ready measured rows after an offset-only reconnect | `data-last-scroll-top` `484`, received `502` | ≤ 2026-10-20 06:39 | unowned |
| Test | Failure signature | First red | Owner |
| ---- | ----------------- | --------- | ----- |
Loading
Loading