Skip to content

Commit 97265fd

Browse files
test(webview): capture typed host messages (#1446)
Co-authored-by: Roomote <roomote@roomote.dev>
1 parent 8187d3c commit 97265fd

5 files changed

Lines changed: 32 additions & 12 deletions

File tree

webview-ui/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Skip a visual test when the change is behavior-only (state transitions, handler
6666
- Keep behavioral assertions in Vitest. A `*.visual.tsx` test should establish a deterministic state and make a focused screenshot assertion.
6767
- Register browser-owned stories in `playwright/gallery/stories.tsx` under a stable, descriptive ID and mount them with `mount(storyId, props)`. Props must be serializable; callbacks, React state, providers, and query clients stay inside the story so every mount starts fresh.
6868
- Keep gallery-wide production CSS, theme fixtures, image setup, aliases, and mocks in `playwright/gallery/main.tsx` and `playwright/vite.config.ts` rather than duplicating setup in specs.
69+
- Use `playwright/vscode-messages.ts` to inspect outbound `vscode.postMessage` payloads. The gallery resets captured messages before every mount; do not parse the browser console for host messages.
6970
- Use `playwright/layout-contracts.ts` for bounded-layout checks. Critical real stories should cover WCAG text spacing at the 320px reflow width, horizontal overflow, clipped text and controls, action-row containment, and focused-control visibility without adding snapshots for that geometry matrix.
7071
- Run visual comparisons with `pnpm test:visual:docker` from `webview-ui/`.
7172
- Update intentional baselines with `pnpm test:visual:docker:update` and commit the resulting `__screenshots__` files with the UI change.

webview-ui/playwright/gallery/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
<title>webview-ui visual test gallery</title>
77
<script>
88
globalThis.z = undefined
9+
globalThis.__vscodeMessages = []
10+
globalThis.acquireVsCodeApi = () => ({
11+
postMessage: (message) => globalThis.__vscodeMessages.push(message),
12+
getState: () => {
13+
const state = localStorage.getItem("vscodeState")
14+
return state ? JSON.parse(state) : undefined
15+
},
16+
setState: (state) => {
17+
localStorage.setItem("vscodeState", JSON.stringify(state))
18+
return state
19+
},
20+
})
921
</script>
1022
</head>
1123
<body class="vscode-dark" data-vscode-theme-id="Default Dark Modern">

webview-ui/playwright/gallery/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ window.unmount = async () => {
4545
container
4646
.querySelectorAll("[data-playwright-mounted]")
4747
.forEach((element) => element.removeAttribute("data-playwright-mounted"))
48+
window.__vscodeMessages = []
4849
}
4950

5051
window.mount = async ({ story, props = {} }) => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Page } from "@playwright/test"
2+
3+
import type { WebviewMessage } from "@roo/WebviewMessage"
4+
5+
declare global {
6+
interface Window {
7+
__vscodeMessages: WebviewMessage[]
8+
}
9+
}
10+
11+
export function getCapturedVscodeMessages(page: Page): Promise<WebviewMessage[]> {
12+
return page.evaluate(() => window.__vscodeMessages)
13+
}

webview-ui/src/components/chat/__tests__/Announcement.links.visual.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
11
import { expect, test } from "../../../../playwright/coverage-fixture"
2+
import { getCapturedVscodeMessages } from "../../../../playwright/vscode-messages"
23

34
test("announcement links open exactly once through the extension host", async ({ mount, page }) => {
4-
// The webview's vscode.postMessage falls back to console.log in a plain
5-
// browser, so host-bound openExternal messages surface as console output.
6-
const hostMessages: { type?: string; url?: string }[] = []
7-
page.on("console", (message) => {
8-
void Promise.all(message.args().map((arg) => arg.jsonValue())).then((args) => {
9-
const payload = args[0] as { type?: string; url?: string } | undefined
10-
if (payload && typeof payload === "object" && payload.type === "openExternal") {
11-
hostMessages.push(payload)
12-
}
13-
})
14-
})
15-
165
await mount("announcement-links")
176

187
// Mirrors VS Code's webview bootstrap (handleInnerClick): a document-level
@@ -50,6 +39,7 @@ test("announcement links open exactly once through the extension host", async ({
5039
await page.locator("#control-link").click()
5140

5241
// Exactly one host message per link, in render order.
42+
const hostMessages = (await getCapturedVscodeMessages(page)).filter((message) => message.type === "openExternal")
5343
expect(hostMessages).toEqual([
5444
{ type: "openExternal", url: "https://zoocode.dev/models" },
5545
{ type: "openExternal", url: "https://github.com/Zoo-Code-Org/Zoo-Code" },
@@ -65,4 +55,7 @@ test("announcement links open exactly once through the extension host", async ({
6555
expect(interceptedLinks).toHaveLength(1)
6656
// The control href resolves to an absolute URL, so match on the fragment.
6757
expect(interceptedLinks[0]).toMatch(/#control$/)
58+
59+
await mount("announcement-links")
60+
expect(await getCapturedVscodeMessages(page)).toEqual([])
6861
})

0 commit comments

Comments
 (0)