Skip to content

Commit 9512bb4

Browse files
committed
test(webview): cover theme probe lifecycle
1 parent 46f3510 commit 9512bb4

4 files changed

Lines changed: 220 additions & 1 deletion

File tree

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,70 @@ describe("ClineProvider", () => {
771771
await expect(provider.postMessageToWebview(message)).resolves.toBeUndefined()
772772
})
773773

774+
describe("theme fixture probes", () => {
775+
const fixture = {
776+
themeId: "Default Dark Modern",
777+
bodyClass: "vscode-dark",
778+
variables: { "--vscode-foreground": "#cccccc" },
779+
}
780+
const originalProbeSetting = process.env.ROO_CODE_THEME_FIXTURE_PROBE
781+
782+
beforeEach(() => {
783+
process.env.ROO_CODE_THEME_FIXTURE_PROBE = "1"
784+
})
785+
786+
afterEach(() => {
787+
if (originalProbeSetting === undefined) {
788+
delete process.env.ROO_CODE_THEME_FIXTURE_PROBE
789+
} else {
790+
process.env.ROO_CODE_THEME_FIXTURE_PROBE = originalProbeSetting
791+
}
792+
vi.useRealTimers()
793+
})
794+
795+
test("rejects requests when probing is disabled", async () => {
796+
delete process.env.ROO_CODE_THEME_FIXTURE_PROBE
797+
798+
await expect(provider.requestWebviewThemeFixture()).rejects.toThrow("Theme fixture probing is disabled")
799+
})
800+
801+
test("posts a request and resolves the matching response", async () => {
802+
const postMessageSpy = vi.spyOn(provider, "postMessageToWebview").mockResolvedValue(undefined)
803+
const request = provider.requestWebviewThemeFixture()
804+
await Promise.resolve()
805+
const requestId = postMessageSpy.mock.calls[0]?.[0].requestId
806+
const unknownFixture = { ...fixture, themeId: "Unexpected Theme" }
807+
808+
expect(requestId).toBeTruthy()
809+
expect(postMessageSpy).toHaveBeenCalledWith({ type: "themeFixtureProbeRequest", requestId })
810+
provider.resolveWebviewThemeFixtureProbe("unknown-request", unknownFixture)
811+
provider.resolveWebviewThemeFixtureProbe(requestId!, fixture)
812+
813+
await expect(request).resolves.toEqual(fixture)
814+
})
815+
816+
test("rejects a request after its timeout", async () => {
817+
vi.useFakeTimers()
818+
vi.spyOn(provider, "postMessageToWebview").mockResolvedValue(undefined)
819+
const request = provider.requestWebviewThemeFixture(100)
820+
const rejection = expect(request).rejects.toThrow("Theme fixture probe timed out after 100ms")
821+
822+
await vi.advanceTimersByTimeAsync(100)
823+
await rejection
824+
})
825+
826+
test("rejects pending requests when webview resources are cleared", async () => {
827+
vi.spyOn(provider, "postMessageToWebview").mockResolvedValue(undefined)
828+
const request = provider.requestWebviewThemeFixture()
829+
const rejection = expect(request).rejects.toThrow(
830+
"Webview was disposed before the theme fixture probe completed",
831+
)
832+
833+
provider["clearWebviewResources"]()
834+
await rejection
835+
})
836+
})
837+
774838
test("postStateToWebview does not force action navigation for non-compliant MDM state", async () => {
775839
const mdmService = {
776840
requiresCloudAuth: vi.fn().mockReturnValue(true),

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,58 @@ const mockClineProvider = {
117117
},
118118
log: vi.fn(),
119119
postStateToWebview: vi.fn(),
120+
resolveWebviewThemeFixtureProbe: vi.fn(),
120121
getCurrentTask: vi.fn(),
121122
getTaskWithId: vi.fn(),
122123
createTaskWithHistoryItem: vi.fn(),
123124
getSkillsManager: vi.fn(),
124125
cwd: "/mock/workspace",
125126
} as unknown as ClineProvider
126127

128+
describe("webviewMessageHandler - theme fixture probes", () => {
129+
const originalProbeSetting = process.env.ROO_CODE_THEME_FIXTURE_PROBE
130+
const themeFixture = {
131+
themeId: "Default Dark Modern",
132+
bodyClass: "vscode-dark",
133+
variables: { "--vscode-foreground": "#cccccc" },
134+
}
135+
136+
beforeEach(() => {
137+
vi.clearAllMocks()
138+
process.env.ROO_CODE_THEME_FIXTURE_PROBE = "1"
139+
})
140+
141+
afterEach(() => {
142+
if (originalProbeSetting === undefined) {
143+
delete process.env.ROO_CODE_THEME_FIXTURE_PROBE
144+
} else {
145+
process.env.ROO_CODE_THEME_FIXTURE_PROBE = originalProbeSetting
146+
}
147+
})
148+
149+
it("resolves a complete response when probing is enabled", async () => {
150+
await webviewMessageHandler(mockClineProvider, {
151+
type: "themeFixtureProbeResponse",
152+
requestId: "request-1",
153+
themeFixture,
154+
})
155+
156+
expect(mockClineProvider.resolveWebviewThemeFixtureProbe).toHaveBeenCalledWith("request-1", themeFixture)
157+
})
158+
159+
it("ignores incomplete or disabled responses", async () => {
160+
await webviewMessageHandler(mockClineProvider, { type: "themeFixtureProbeResponse" })
161+
delete process.env.ROO_CODE_THEME_FIXTURE_PROBE
162+
await webviewMessageHandler(mockClineProvider, {
163+
type: "themeFixtureProbeResponse",
164+
requestId: "request-1",
165+
themeFixture,
166+
})
167+
168+
expect(mockClineProvider.resolveWebviewThemeFixtureProbe).not.toHaveBeenCalled()
169+
})
170+
})
171+
127172
import { t } from "../../../i18n"
128173

129174
vi.mock("vscode", () => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it, vi } from "vitest"
2+
import type * as vscode from "vscode"
3+
4+
import { API } from "../api"
5+
import type { ClineProvider } from "../../core/webview/ClineProvider"
6+
7+
vi.mock("@roo-code/ipc", () => ({
8+
IpcServer: class {},
9+
}))
10+
11+
describe("API - theme fixture probe", () => {
12+
it("delegates capture to the sidebar provider", async () => {
13+
const fixture = {
14+
themeId: "Default Dark Modern",
15+
bodyClass: "vscode-dark",
16+
variables: { "--vscode-foreground": "#cccccc" },
17+
}
18+
const requestWebviewThemeFixture = vi.fn().mockResolvedValue(fixture)
19+
const provider = {
20+
context: {},
21+
on: vi.fn(),
22+
requestWebviewThemeFixture,
23+
} as unknown as ClineProvider
24+
const outputChannel = { appendLine: vi.fn() } as unknown as vscode.OutputChannel
25+
const api = new API(outputChannel, provider)
26+
27+
await expect(api.captureWebviewThemeFixture()).resolves.toEqual(fixture)
28+
expect(requestWebviewThemeFixture).toHaveBeenCalledOnce()
29+
})
30+
})

webview-ui/src/utils/__tests__/useThemeFixtureProbe.test.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
import { captureWebviewThemeFixture } from "../useThemeFixtureProbe"
1+
import { act, renderHook } from "@testing-library/react"
2+
3+
import { vscode } from "../vscode"
4+
import { captureWebviewThemeFixture, useThemeFixtureProbe } from "../useThemeFixtureProbe"
25

36
describe("captureWebviewThemeFixture", () => {
7+
afterEach(() => {
8+
document.documentElement.removeAttribute("style")
9+
document.body.removeAttribute("style")
10+
document.body.removeAttribute("class")
11+
delete document.body.dataset.vscodeThemeId
12+
vi.unstubAllGlobals()
13+
vi.restoreAllMocks()
14+
})
15+
416
it("captures only resolved VS Code custom properties", () => {
17+
document.documentElement.style.setProperty("--vscode-root", "#ffffff")
518
document.body.className = "vscode-dark extra-class"
619
document.body.dataset.vscodeThemeId = "Default Dark Modern"
720
document.body.style.colorScheme = "dark"
@@ -13,9 +26,76 @@ describe("captureWebviewThemeFixture", () => {
1326
themeId: "Default Dark Modern",
1427
bodyClass: "vscode-dark extra-class",
1528
variables: {
29+
"--vscode-root": "",
1630
"--vscode-z-last": "rgb(2, 2, 2)",
1731
"--vscode-a-first": "#010101",
1832
},
1933
})
2034
})
35+
36+
it("responds after two animation frames", () => {
37+
const callbacks = new Map<number, FrameRequestCallback>()
38+
let nextFrame = 0
39+
vi.stubGlobal(
40+
"requestAnimationFrame",
41+
vi.fn((callback: FrameRequestCallback) => {
42+
const id = ++nextFrame
43+
callbacks.set(id, callback)
44+
return id
45+
}),
46+
)
47+
vi.stubGlobal(
48+
"cancelAnimationFrame",
49+
vi.fn((id: number) => callbacks.delete(id)),
50+
)
51+
const postMessage = vi.spyOn(vscode, "postMessage").mockImplementation(() => undefined)
52+
const { unmount } = renderHook(() => useThemeFixtureProbe())
53+
54+
act(() => {
55+
window.dispatchEvent(
56+
new MessageEvent("message", { data: { type: "themeFixtureProbeRequest", requestId: "request-1" } }),
57+
)
58+
})
59+
expect(postMessage).not.toHaveBeenCalled()
60+
61+
act(() => {
62+
callbacks.get(1)?.(0)
63+
})
64+
expect(postMessage).not.toHaveBeenCalled()
65+
66+
act(() => {
67+
callbacks.get(2)?.(0)
68+
})
69+
70+
expect(postMessage).toHaveBeenCalledWith(
71+
expect.objectContaining({ type: "themeFixtureProbeResponse", requestId: "request-1" }),
72+
)
73+
unmount()
74+
})
75+
76+
it("ignores incomplete requests and cancels pending frames on unmount", () => {
77+
const callbacks = new Map<number, FrameRequestCallback>()
78+
vi.stubGlobal(
79+
"requestAnimationFrame",
80+
vi.fn((callback: FrameRequestCallback) => {
81+
callbacks.set(1, callback)
82+
return 1
83+
}),
84+
)
85+
const cancelAnimationFrame = vi.fn((id: number) => callbacks.delete(id))
86+
vi.stubGlobal("cancelAnimationFrame", cancelAnimationFrame)
87+
const postMessage = vi.spyOn(vscode, "postMessage").mockImplementation(() => undefined)
88+
const { unmount } = renderHook(() => useThemeFixtureProbe())
89+
90+
act(() => {
91+
window.dispatchEvent(new MessageEvent("message", { data: { type: "themeFixtureProbeRequest" } }))
92+
window.dispatchEvent(
93+
new MessageEvent("message", { data: { type: "themeFixtureProbeRequest", requestId: "pending" } }),
94+
)
95+
})
96+
unmount()
97+
98+
expect(postMessage).not.toHaveBeenCalled()
99+
expect(cancelAnimationFrame).toHaveBeenCalledWith(1)
100+
})
21101
})

0 commit comments

Comments
 (0)