packages/app/component-tests/timeline-detail.spec.ts
Rama dev · réplica de lectura del repositorio original.
import { expect, story } from "../../stories/playwright/story"
story("maps grouped and collapsed switches to the timeline settings", async ({ mount }) => {
const component = await mount("settings-timeline-detail--interactive")
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const shell = component.getByRole("group", { name: "Shell", exact: true })
const grouped = shell.getByRole("switch", { name: "Shell grouped", exact: true })
const collapsed = shell.getByRole("switch", { name: "Shell collapsed", exact: true })
const value = component.locator('[data-slot="timeline-detail-fixture-value"]')
await expect(component.getByRole("switch")).toHaveCount(10)
await expect(component.getByText("Activity", { exact: true })).toHaveCount(0)
await expect(grouped).toBeChecked()
await expect(collapsed).toBeChecked()
await shell.locator('[data-field="placement"] [data-slot="switch-control"]').click()
await shell.locator('[data-field="details"] [data-slot="switch-control"]').click()
await expect(value).toContainText('"shell":{"placement":"separate","details":"expanded"}')
await grouped.focus()
await grouped.press("Space")
await collapsed.focus()
await collapsed.press("Space")
await expect(value).toContainText('"shell":{"placement":"grouped","details":"collapsed"}')
await expect(component.getByRole("group", { name: "Subagents", exact: true }).getByRole("switch")).toHaveCount(1)
})
story("replaces hidden switches with solid lines and restores options", async ({ mount, page }) => {
const component = await mount("settings-timeline-detail--interactive")
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const shell = component.getByRole("group", { name: "Shell", exact: true })
const visibility = shell.getByRole("button", { name: "Shell visibility" })
const label = shell.locator('[data-slot="timeline-detail-activity"] > label')
const color = await label.evaluate((element) => getComputedStyle(element).color)
const iconColor = await visibility.evaluate((element) => getComputedStyle(element).color)
await shell.locator('[data-field="placement"] [data-slot="switch-control"]').click()
await shell.locator('[data-field="details"] [data-slot="switch-control"]').click()
await visibility.hover()
await expect(page.getByRole("tooltip")).toHaveText("Hide")
expect(await page.getByRole("tooltip").evaluate((element) => element.getBoundingClientRect().bottom)).toBeLessThan(
await visibility.evaluate((element) => element.getBoundingClientRect().top),
)
await visibility.click()
await expect(visibility).toHaveAttribute("aria-pressed", "false")
await expect(shell.getByRole("switch")).toHaveCount(0)
await expect(shell.locator('[data-slot="timeline-detail-unavailable"]')).toHaveCount(2)
await expect(shell.locator('[data-slot="timeline-detail-unavailable"]').first()).toHaveCSS(
"border-top-style",
"solid",
)
await expect(component.locator('[data-slot="timeline-detail-fixture-value"]')).toContainText(
'"shell":{"placement":"hidden","details":"expanded"}',
)
await expect(label).not.toHaveCSS("color", color)
await expect(visibility.locator("use")).toHaveAttribute("href", "#opencode-v2-icon-outline-eye-slash")
await component.getByRole("button", { name: "Advanced", exact: true }).hover()
await expect(visibility).not.toHaveCSS("color", iconColor)
await expect(shell.locator('[data-slot="timeline-detail-unavailable"]').first()).toHaveCSS(
"border-top-color",
await visibility.evaluate((element) => getComputedStyle(element).color),
)
await visibility.hover()
await expect(page.getByRole("tooltip")).toHaveText("Show")
expect(await page.getByRole("tooltip").evaluate((element) => element.getBoundingClientRect().bottom)).toBeLessThan(
await visibility.evaluate((element) => element.getBoundingClientRect().top),
)
await visibility.click()
await expect(visibility).toHaveAttribute("aria-pressed", "true")
await expect(label).toHaveCSS("color", color)
await expect(visibility.locator("use")).toHaveAttribute("href", "#opencode-v2-icon-outline-eye")
await expect(shell.locator('[data-slot="timeline-detail-unavailable"]')).toHaveCount(0)
await expect(shell.getByRole("switch", { name: "Shell grouped", exact: true })).toBeEnabled()
await expect(shell.getByRole("switch", { name: "Shell collapsed", exact: true })).toBeEnabled()
await expect(shell.getByRole("switch", { name: "Shell grouped", exact: true })).not.toBeChecked()
await expect(shell.getByRole("switch", { name: "Shell collapsed", exact: true })).not.toBeChecked()
})
story("toggles visibility by clicking the activity label", async ({ mount }) => {
const component = await mount("settings-timeline-detail--interactive")
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const shell = component.getByRole("group", { name: "Shell", exact: true })
const label = shell.locator('[data-slot="timeline-detail-activity"] > label')
const visibility = shell.getByRole("button", { name: "Shell visibility" })
await expect(label).toHaveCSS("cursor", "default")
await label.click()
await expect(visibility).toHaveAttribute("aria-pressed", "false")
await expect(shell.getByRole("switch")).toHaveCount(0)
await expect(shell.locator('[data-slot="timeline-detail-unavailable"]')).toHaveCount(2)
await label.click()
await expect(visibility).toHaveAttribute("aria-pressed", "true")
await expect(shell.getByRole("switch", { name: "Shell grouped", exact: true })).toBeChecked()
await expect(shell.getByRole("switch", { name: "Shell collapsed", exact: true })).toBeChecked()
})
story("only highlights the eye when hovering the icon, not its activity label", async ({ mount }) => {
const component = await mount("settings-timeline-detail--interactive")
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const shell = component.getByRole("group", { name: "Shell", exact: true })
const visibility = shell.getByRole("button", { name: "Shell visibility" })
const label = shell.locator('[data-slot="timeline-detail-activity"] > label')
for (const hidden of [false, true]) {
if (hidden) await label.click()
await visibility.hover()
await expect(visibility).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)")
await label.hover()
await expect(visibility).toHaveCSS("background-color", "rgba(0, 0, 0, 0)")
}
})
story("opens advanced on returning to custom settings but not presets", async ({ mount }) => {
const component = await mount("settings-timeline-detail--interactive")
const advanced = component.getByRole("button", { name: "Advanced", exact: true })
await expect(advanced).toHaveAttribute("aria-expanded", "false")
await advanced.click()
await component.locator('[data-category="shell"][data-field="placement"] [data-slot="switch-control"]').click()
await expect(component.getByRole("slider")).toHaveAttribute("aria-valuetext", "Custom")
await advanced.click()
await expect(advanced).toHaveAttribute("aria-expanded", "false")
await component.getByRole("button", { name: "Leave settings" }).click()
await expect(advanced).toHaveCount(0)
await component.getByRole("button", { name: "Return to settings" }).click()
await expect(advanced).toHaveAttribute("aria-expanded", "true")
await expect(component.getByRole("switch", { name: "Shell grouped", exact: true })).not.toBeChecked()
await component.getByRole("slider").press("End")
await expect(component.getByRole("slider")).toHaveAttribute("aria-valuetext", "Everything")
await component.getByRole("button", { name: "Leave settings" }).click()
await component.getByRole("button", { name: "Return to settings" }).click()
await expect(advanced).toHaveAttribute("aria-expanded", "false")
})
story("keeps visibility and switches in sync with the preset slider", async ({ mount }) => {
const component = await mount("settings-timeline-detail--interactive")
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const slider = component.getByRole("slider", { name: "Timeline detail" })
await slider.focus()
await slider.press("Home")
await expect(slider).toHaveAttribute("aria-valuetext", "Messages only")
// Messages only hides every activity but keeps commentary, which is text.
await expect(component.getByRole("switch")).toHaveAccessibleName("Commentary grouped")
await expect(component.locator('[data-slot="timeline-detail-unavailable"]')).toHaveCount(9)
await expect(component.locator('[data-action="timeline-detail-visibility"][aria-pressed="false"]')).toHaveCount(6)
await component.getByRole("button", { name: "Shell visibility" }).click()
await expect(component.getByRole("switch", { name: "Shell grouped", exact: true })).toBeChecked()
await expect(slider).toHaveAttribute("aria-valuetext", "Custom")
await slider.focus()
await slider.press("End")
await expect(slider).toHaveAttribute("aria-valuetext", "Everything")
await expect(component.getByRole("switch")).toHaveCount(10)
await expect(component.locator('[data-slot="timeline-detail-unavailable"]')).toHaveCount(0)
await expect(component.locator('[data-action="timeline-detail-visibility"][aria-pressed="true"]')).toHaveCount(7)
await expect(component.getByRole("switch", { checked: true })).toHaveCount(0)
})
for (const direction of ["ltr", "rtl"]) {
for (const theme of ["light", "dark"]) {
story(`fits narrow and wide layouts in ${direction}, ${theme}`, async ({ mount, page }, testInfo) => {
await page.setViewportSize({ width: 900, height: 900 })
const component = await mount("settings-timeline-detail--interactive", { globals: { direction, theme } })
await expect(component.locator('[data-slot="timeline-detail-scale"]')).toHaveCSS("margin-top", "16px")
const advanced = component.locator('[data-slot="timeline-detail-advanced"]')
await expect(advanced).toHaveCSS("margin-top", "0px")
await expect(advanced).toHaveCSS("padding-top", "8px")
await expect(component.locator('[data-slot="timeline-detail-summary"]')).toHaveCSS(
"color",
await component
.locator('[data-slot="settings-row-title"]')
.evaluate((element) => getComputedStyle(element).color),
)
await component.getByRole("button", { name: "Advanced", exact: true }).click()
const track = component.locator('[data-slot="timeline-detail-track"]')
await expect(track).toHaveCSS(
"--timeline-detail-track-background",
await track.evaluate(
(element, theme) =>
getComputedStyle(element)
.getPropertyValue(theme === "light" ? "--v2-background-bg-layer-04" : "--v2-background-bg-layer-03")
.trim(),
theme,
),
)
if (theme === "light") {
await expect(track.locator("span").last()).toHaveCSS("background-image", "none")
await expect(track).toHaveCSS(
"--timeline-detail-marker-background",
await track.evaluate((element) => getComputedStyle(element).getPropertyValue("--v2-grey-500").trim()),
)
}
if (theme === "dark") {
await expect(track.locator("span").last()).not.toHaveCSS("background-image", "none")
}
await page.screenshot({ path: testInfo.outputPath(`timeline-${theme}-${direction}.png`) })
const list = component.locator('[data-slot="timeline-detail-list"]')
const columns = component.locator('[data-slot="timeline-detail-columns"]')
await expect(component.locator('[data-slot="timeline-detail-categories"]')).toHaveCSS("margin-top", "0px")
const advancedTrigger = component.getByRole("button", { name: "Advanced", exact: true })
expect(
await advancedTrigger.evaluate((element) => {
const trigger = element.getBoundingClientRect()
const heading = document
.querySelector('[data-slot="timeline-detail-columns"] > :nth-child(2)')!
.getBoundingClientRect()
return Math.abs(trigger.y + trigger.height / 2 - (heading.y + heading.height / 2))
}),
).toBeLessThan(1)
expect(
await list.evaluate((element) => {
const list = element.getBoundingClientRect()
const trigger = document
.querySelector('[data-slot="timeline-detail-advanced"] > [data-slot="collapsible-trigger"]')!
.getBoundingClientRect()
return list.y - trigger.bottom
}),
).toBe(8)
for (const [column, field] of [
[2, "placement"],
[3, "details"],
] as const) {
const heading = await columns.locator(`> :nth-child(${column})`).evaluate((element) => {
const rect = element.getBoundingClientRect()
return rect.x + rect.width / 2
})
const toggle = await component
.locator(`[data-category="shell"][data-field="${field}"] [data-slot="switch-control"]`)
.evaluate((element) => {
const rect = element.getBoundingClientRect()
return rect.x + rect.width / 2
})
expect(Math.abs(heading - toggle)).toBeLessThan(1)
}
await expect(component.locator('[data-slot="timeline-detail-activity"]').first()).toHaveCSS("gap", "8px")
for (const width of [900, 320]) {
await page.setViewportSize({ width, height: 900 })
await expect(component.getByRole("switch", { name: "Shell grouped", exact: true })).toBeVisible()
expect(await list.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true)
const visibility = component.getByRole("button", { name: "Shell visibility" })
await visibility.focus()
await visibility.press("Space")
await expect(visibility).toHaveAttribute("aria-pressed", "false")
await expect(component.getByRole("switch", { name: "Shell grouped", exact: true })).toHaveCount(0)
await expect(
component
.getByRole("group", { name: "Shell", exact: true })
.locator('[data-slot="timeline-detail-unavailable"]'),
).toHaveCount(2)
await visibility.press("Space")
await expect(visibility).toHaveAttribute("aria-pressed", "true")
const slider = component.getByRole("slider", { name: "Timeline detail" })
const track = component.locator('[data-slot="timeline-detail-track"]')
await expect(slider).not.toHaveCSS("cursor", "pointer")
expect(await track.evaluate((element) => element.getBoundingClientRect().width)).toBe(
await component
.locator('[data-slot="timeline-detail-scale"]')
.evaluate((element) => element.getBoundingClientRect().width),
)
await slider.focus()
await slider.press("Home")
for (const position of [0, 1, 2, 3, 4]) {
if (position > 0) await slider.press("ArrowUp")
await expect(track).toHaveCSS("--timeline-detail-progress", `${position * 25}%`)
await expect(track.locator("span[data-selected]")).toHaveCount(position + 1)
await expect(track.locator("span").nth(position)).toHaveCSS(
"background-color",
await track.evaluate((element) => getComputedStyle(element, "::before").backgroundColor),
)
const fill = await track.evaluate((element) => {
const style = getComputedStyle(element, "::before")
return {
fraction: parseFloat(style.width) / element.getBoundingClientRect().width,
start: style.getPropertyValue("inset-inline-start"),
color: style.backgroundColor,
remainder: getComputedStyle(element).backgroundColor,
}
})
expect(fill.fraction).toBeCloseTo(position / 4, 2)
expect(fill.start).toBe("0px")
expect(fill.color).not.toBe(fill.remainder)
const marker = await track.locator("span").nth(position).boundingBox()
const bounds = await track.boundingBox()
expect(marker).not.toBeNull()
expect(bounds).not.toBeNull()
expect((marker!.x + marker!.width / 2 - bounds!.x) / bounds!.width).toBeCloseTo(
direction === "rtl" ? 1 - position / 4 : position / 4,
2,
)
}
}
})
}
}