mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 11:39:41 +03:00
312 lines
16 KiB
TypeScript
312 lines
16 KiB
TypeScript
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,
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|