Files

99 lines
3.7 KiB
TypeScript

import type { SessionMessageInfo } from "@opencode/client/promise"
import type { MotionKnob } from "@opencode/ui/motion-tuning"
import { expect, sourceURL, story } from "../../stories/playwright/story"
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
const modules = {
fixture: source("./plugin-host.fixture.tsx"),
definition: source("../../gui-plugins/src/sources/index.ts"),
renderer: source("../../gui-plugins/src/sources/renderer.tsx"),
tuning: source("../../ui/src/motion-tuning.ts"),
}
// One turn: the user message the panel's tab is keyed by, and a reply that searched the web.
const messages: SessionMessageInfo[] = [
{ id: "main", type: "user", text: "Who made One Million Checkboxes?", time: { created: 1 } },
{
id: "msg_reply",
type: "assistant",
agent: "build",
model: { id: "claude-sonnet-4", providerID: "anthropic" },
time: { created: 2, completed: 3 },
content: [
{
type: "tool",
id: "call_search",
name: "websearch",
state: {
status: "completed",
input: { query: "one million checkboxes" },
content: [
{ type: "text", text: "## [One Million Checkboxes](https://eieio.games/blog/one-million-checkboxes)" },
],
},
time: { created: 2, ran: 2, completed: 3 },
},
],
},
]
story.beforeEach(async ({ mount }) => {
// Any story loads the app; the fixture mounts the real host beside it.
await mount("ui-line-comment--editor")
})
story("turning the Sources experiment off closes its open panel and loads no more site icons", async ({ page }) => {
// Site icons stay off the network, and load, so each stays an image until the experiment turns off.
await page.route("**/favicon.ico", (route) =>
route.fulfill({
contentType: "image/png",
body: Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64",
),
}),
)
const result = await page.evaluate(
async (input) => {
const [{ mountSessionRegion, until }, { default: definition }, { default: renderer }, { motionTuning }] =
await Promise.all([
import(input.modules.fixture),
import(input.modules.definition),
import(input.modules.renderer),
import(input.modules.tuning),
])
const knob = (id: string) => motionTuning.list().find((item: MotionKnob) => item.id === id)
motionTuning.set(knob("sources.enabled"), true)
motionTuning.set(knob("sources.icons"), "site")
const host: { container: HTMLElement; unmount: () => void } = mountSessionRegion({
active: "sources:main",
messages: JSON.parse(input.messages),
definitions: [{ ...definition, id: "sources", windowEntry: async () => ({ default: renderer }) }],
})
const panel = () => host.container.querySelector('[data-slot="session-sources-panel"]')
const icons = () => host.container.querySelectorAll('[data-slot="site-icon-image"]').length
await until(() => !!panel()?.querySelector('[data-slot="sources-step-row"]'))
panel()?.querySelector<HTMLButtonElement>('[data-slot="sources-step-row"]')?.click()
await until(() => icons() > 0)
const open = { panel: !!panel(), icons: icons() }
motionTuning.set(knob("sources.enabled"), false)
await until(() => !panel())
const off = { panel: !!panel(), icons: icons() }
host.unmount()
return { open, off }
},
// As JSON: the messages' type is too deep for `evaluate`'s serializable argument check.
{ modules, messages: JSON.stringify(messages) },
)
expect(result).toEqual({ open: { panel: true, icons: 1 }, off: { panel: false, icons: 0 } })
})