Skip to content

Commit 5ac5440

Browse files
hassoncsChris Hasson
andauthored
refactor(e2e, storybook): improve reliability of theme variable extraction by aligning with VS Code (#1801)
Moved the VS Code theme variable extraction script from apps/storybook to apps/playwright-e2e/scripts to enable Playwright to extract theme variables directly from the running VS Code instance. This improves accuracy and maintainability compared to the previous approach of generating variables from theme styles, ensuring we consistently capture all relevant variables across both light and dark themes with minimal upkeep. Additionally, moved verifyExtensionInstalled, closeAllTabs, and waitForAllExtensionActivation from webview-helpers.ts to a new vscode-helpers.ts to better separate VS Code-specific interactions from webview logic, enhancing modularity. Co-authored-by: Chris Hasson <[email protected]>
1 parent 46ab113 commit 5ac5440

File tree

14 files changed

+1867
-467
lines changed

14 files changed

+1867
-467
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
4+
export const extractAllCSSVariables = (): Record<string, string> => {
5+
const variables: Record<string, string> = {}
6+
7+
// There are too many of these and we don't need them
8+
const isExcludedVariable = (property: string): boolean => {
9+
return property.includes("-icon-") && (property.includes("-content") || property.includes("-font-family"))
10+
}
11+
12+
const extractFromStyleRule = (style: CSSStyleDeclaration): void => {
13+
for (let i = 0; i < style.length; i++) {
14+
const property = style[i]
15+
if (property.startsWith("--") && !isExcludedVariable(property)) {
16+
const value = style.getPropertyValue(property).trim()
17+
if (value) {
18+
variables[property] = value
19+
}
20+
}
21+
}
22+
}
23+
24+
const extractFromStylesheets = (): void => {
25+
for (let i = 0; i < document.styleSheets.length; i++) {
26+
const sheet = document.styleSheets[i]
27+
try {
28+
const rules = sheet.cssRules || []
29+
for (const rule of Array.from(rules)) {
30+
if (rule instanceof CSSStyleRule) {
31+
extractFromStyleRule(rule.style)
32+
}
33+
}
34+
} catch (_e) {
35+
// Skip inaccessible stylesheets
36+
}
37+
}
38+
}
39+
40+
const extractFromComputedStyles = (): void => {
41+
const rootStyles = getComputedStyle(document.documentElement)
42+
for (let i = 0; i < rootStyles.length; i++) {
43+
const property = rootStyles[i]
44+
if (property.startsWith("--") && !isExcludedVariable(property)) {
45+
const value = rootStyles.getPropertyValue(property).trim()
46+
if (value && !variables[property]) {
47+
variables[property] = value
48+
}
49+
}
50+
}
51+
}
52+
53+
extractFromStylesheets()
54+
extractFromComputedStyles()
55+
56+
return variables
57+
}
58+
59+
export const generateCSSOutput = (allVariables: Record<string, string>): string => {
60+
const sortedEntries = Object.entries(allVariables).sort(([a], [b]) => a.localeCompare(b))
61+
const cssLines = sortedEntries.map(([key, value]) => `${key}: ${value};`)
62+
63+
return `/* All CSS Variables - Auto-extracted from VS Code via Playwright */
64+
/* Generated on ${new Date().toISOString()} */
65+
66+
${cssLines.map((line) => ` ${line}`).join("\n")}
67+
`
68+
}
69+
70+
export const saveVariablesToFile = async (cssOutput: string, finalFilename: string): Promise<string> => {
71+
const outputDir = path.join(process.cwd(), "../storybook/generated-theme-styles")
72+
await fs.promises.mkdir(outputDir, { recursive: true })
73+
const outputPath = path.join(outputDir, finalFilename)
74+
await fs.promises.writeFile(outputPath, cssOutput)
75+
return outputPath
76+
}
77+

apps/playwright-e2e/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from "./console-logging"
44
export * from "./test-setup-helpers"
55
export * from "./chat-helpers"
66
export * from "./notification-helpers"
7+
export * from "./vscode-helpers"

apps/playwright-e2e/helpers/test-setup-helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// kilocode_change - new file
22
import { type Page } from "@playwright/test"
3-
import { verifyExtensionInstalled, waitForWebviewText, configureApiKeyThroughUI } from "./webview-helpers"
3+
import { waitForWebviewText, configureApiKeyThroughUI } from "./webview-helpers"
4+
import { verifyExtensionInstalled } from "./vscode-helpers"
45

56
export async function setupTestEnvironment(page: Page): Promise<void> {
67
await verifyExtensionInstalled(page)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { type Page, expect } from "@playwright/test"
2+
3+
const modifier = process.platform === "darwin" ? "Meta" : "Control"
4+
5+
export async function verifyExtensionInstalled(page: Page) {
6+
try {
7+
const activityBarIcon = page.locator('[aria-label*="Kilo"], [title*="Kilo"]').first()
8+
expect(await activityBarIcon).toBeDefined()
9+
console.log("✅ Extension installed!")
10+
} catch (_error) {
11+
throw new Error("Failed to find the installed extension! Check if the build failed and try again.")
12+
}
13+
}
14+
15+
export async function closeAllTabs(page: Page): Promise<void> {
16+
const tabs = page.locator(".tab a.label-name")
17+
const count = await tabs.count()
18+
if (count > 0) {
19+
// Close all editor tabs using the default keyboard command [Cmd+K Cmd+W]
20+
await page.keyboard.press(`${modifier}+K`)
21+
await page.keyboard.press(`${modifier}+W`)
22+
23+
const dismissedTabs = page.locator(".tab a.label-name")
24+
await expect(dismissedTabs).not.toBeVisible()
25+
}
26+
}
27+
28+
export async function waitForAllExtensionActivation(page: Page): Promise<void> {
29+
try {
30+
const activatingStatus = page.locator("text=Activating Extensions")
31+
const activatingStatusCount = await activatingStatus.count()
32+
if (activatingStatusCount > 0) {
33+
console.log("⌛️ Waiting for `Activating Extensions` to go away...")
34+
await activatingStatus.waitFor({ state: "hidden", timeout: 10000 })
35+
}
36+
} catch {
37+
// noop
38+
}
39+
}
40+
41+
export async function switchToTheme(page: Page, themeName: string): Promise<void> {
42+
await page.keyboard.press(`${modifier}+K`)
43+
await page.waitForTimeout(100)
44+
await page.keyboard.press(`${modifier}+T`)
45+
await page.waitForTimeout(100)
46+
47+
await page.keyboard.type(themeName)
48+
await page.waitForTimeout(100)
49+
50+
await page.keyboard.press("Enter")
51+
await page.waitForTimeout(100)
52+
}

apps/playwright-e2e/helpers/webview-helpers.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { type Page, type FrameLocator, expect } from "@playwright/test"
33
import type { WebviewMessage } from "../../../src/shared/WebviewMessage"
44
import { ProviderSettings } from "@roo-code/types"
55

6-
const modifier = process.platform === "darwin" ? "Meta" : "Control"
7-
86
const defaultPlaywrightApiConfig = {
97
apiProvider: "openrouter" as const,
108
openRouterApiKey: process.env.OPENROUTER_API_KEY,
@@ -48,16 +46,6 @@ export async function postWebviewMessage(page: Page, message: WebviewMessage): P
4846
}
4947
}
5048

51-
export async function verifyExtensionInstalled(page: Page) {
52-
try {
53-
const activityBarIcon = page.locator('[aria-label*="Kilo"], [title*="Kilo"]').first()
54-
expect(await activityBarIcon).toBeDefined()
55-
console.log("✅ Extension installed!")
56-
} catch (_error) {
57-
throw new Error("Failed to find the installed extension! Check if the build failed and try again.")
58-
}
59-
}
60-
6149
export async function upsertApiConfiguration(page: Page, apiConfiguration?: Partial<ProviderSettings>): Promise<void> {
6250
await postWebviewMessage(page, {
6351
type: "upsertApiConfiguration",
@@ -98,29 +86,3 @@ export async function configureApiKeyThroughUI(page: Page): Promise<void> {
9886
await submitButton.click()
9987
console.log("✅ Provider configured!")
10088
}
101-
102-
export async function closeAllTabs(page: Page): Promise<void> {
103-
const tabs = page.locator(".tab a.label-name")
104-
const count = await tabs.count()
105-
if (count > 0) {
106-
// Close all editor tabs using the default keyboard command [Cmd+K Cmd+W]
107-
await page.keyboard.press(`${modifier}+K`)
108-
await page.keyboard.press(`${modifier}+W`)
109-
110-
const dismissedTabs = page.locator(".tab a.label-name")
111-
await expect(dismissedTabs).not.toBeVisible()
112-
}
113-
}
114-
115-
export async function waitForAllExtensionActivation(page: Page): Promise<void> {
116-
try {
117-
const activatingStatus = page.locator("text=Activating Extensions")
118-
const activatingStatusCount = await activatingStatus.count()
119-
if (activatingStatusCount > 0) {
120-
console.log("⌛️ Waiting for `Activating Extensions` to go away...")
121-
await activatingStatus.waitFor({ state: "hidden", timeout: 10000 })
122-
}
123-
} catch {
124-
// noop
125-
}
126-
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* VS Code theme extraction using Playwright
5+
* Extracts comprehensive CSS variables from running VS Code instance
6+
* Replaces the old theme generation script with complete variable extraction
7+
*/
8+
9+
import { execSync } from "child_process"
10+
import fs from "fs"
11+
import path from "path"
12+
import { fileURLToPath } from "url"
13+
14+
const __filename = fileURLToPath(import.meta.url)
15+
const __dirname = path.dirname(__filename)
16+
17+
const PLAYWRIGHT_E2E_DIR = path.resolve(__dirname, "..")
18+
const STORYBOOK_OUTPUT_DIR = path.resolve(__dirname, "../../storybook/generated-theme-styles")
19+
20+
async function extractThemeVariables() {
21+
console.log("🎨 Extracting VS Code themes using Playwright...\n")
22+
23+
try {
24+
// Run the Playwright extraction test
25+
console.log("🚀 Running Playwright CSS extraction...")
26+
const playwrightCommand = `cd "${PLAYWRIGHT_E2E_DIR}" && npx playwright test --config=scripts/theme-extraction.config.ts --reporter=line`
27+
execSync(playwrightCommand, { stdio: "inherit", cwd: PLAYWRIGHT_E2E_DIR })
28+
29+
// Verify the files were created directly in the storybook directory
30+
const darkOutputPath = path.join(STORYBOOK_OUTPUT_DIR, "dark-modern.css")
31+
const lightOutputPath = path.join(STORYBOOK_OUTPUT_DIR, "light-modern.css")
32+
33+
if (!fs.existsSync(darkOutputPath) || !fs.existsSync(lightOutputPath)) {
34+
throw new Error("Playwright extraction failed - CSS files not found in storybook directory")
35+
}
36+
37+
console.log(`✅ Generated dark-modern.css`)
38+
console.log(`✅ Generated light-modern.css`)
39+
console.log(`\n🎉 Theme extraction complete! Files saved to ${STORYBOOK_OUTPUT_DIR}`)
40+
} catch (error) {
41+
console.error("❌ Theme extraction failed:", error.message)
42+
process.exit(1)
43+
}
44+
}
45+
46+
// Run if called directly
47+
if (import.meta.url === `file://${process.argv[1]}`) {
48+
extractThemeVariables().catch((error) => {
49+
console.error("Fatal error:", error)
50+
process.exit(1)
51+
})
52+
}
53+
54+
export { extractThemeVariables }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { test, expect, type TestFixtures } from "../tests/playwright-base-test"
2+
import { findWebview } from "../helpers"
3+
import { type Page, type FrameLocator } from "@playwright/test"
4+
import { extractAllCSSVariables, generateCSSOutput, saveVariablesToFile } from "../helpers/css-extraction-helpers"
5+
import { switchToTheme } from "../helpers/vscode-helpers"
6+
7+
const extractVariablesForTheme = async (workbox: Page, webviewFrame: FrameLocator) => {
8+
const mainWindowVariables = await workbox.evaluate(extractAllCSSVariables)
9+
const webviewVariables = await webviewFrame.locator("body").evaluate(extractAllCSSVariables)
10+
11+
const allVariables = { ...webviewVariables, ...mainWindowVariables }
12+
13+
return {
14+
mainWindowVariables,
15+
webviewVariables,
16+
allVariables,
17+
}
18+
}
19+
20+
test.describe("CSS Variable Extraction", () => {
21+
test("should extract CSS variables in both light and dark themes", async ({ workbox }: TestFixtures) => {
22+
const webviewFrame = await findWebview(workbox)
23+
24+
await switchToTheme(workbox, "dark")
25+
const darkResults = await extractVariablesForTheme(workbox, webviewFrame)
26+
const darkCSSOutput = generateCSSOutput(darkResults.allVariables)
27+
await saveVariablesToFile(darkCSSOutput, "dark-modern.css")
28+
29+
await switchToTheme(workbox, "light")
30+
const lightResults = await extractVariablesForTheme(workbox, webviewFrame)
31+
const lightCSSOutput = generateCSSOutput(lightResults.allVariables)
32+
await saveVariablesToFile(lightCSSOutput, "light-modern.css")
33+
34+
expect(Object.keys(darkResults.allVariables).length).toBeGreaterThan(0)
35+
expect(Object.keys(lightResults.allVariables).length).toBeGreaterThan(0)
36+
})
37+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineConfig } from "@playwright/test"
2+
import { TestOptions } from "../tests/playwright-base-test"
3+
import * as path from "path"
4+
import { fileURLToPath } from "url"
5+
6+
// ES module equivalent of __dirname
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = path.dirname(__filename)
9+
10+
export default defineConfig<void, TestOptions>({
11+
timeout: 120_000,
12+
expect: { timeout: 30_000 },
13+
reporter: "line",
14+
workers: 1,
15+
retries: 0,
16+
globalSetup: "../playwright.globalSetup",
17+
testDir: ".",
18+
testMatch: "theme-extraction-script.test.ts", // Only run this specific test
19+
outputDir: "../test-results",
20+
projects: [{ name: "VSCode stable", use: { vscodeVersion: "stable" } }],
21+
use: {
22+
trace: "on-first-retry",
23+
video: "retry-with-video",
24+
},
25+
})

apps/playwright-e2e/tests/settings.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { test, expect, type TestFixtures } from "./playwright-base-test"
2-
import { verifyExtensionInstalled, findWebview, upsertApiConfiguration } from "../helpers/webview-helpers"
3-
import { closeAllToastNotifications } from "../helpers"
2+
import { findWebview, upsertApiConfiguration, closeAllToastNotifications, verifyExtensionInstalled } from "../helpers"
43

54
test.describe("Settings", () => {
65
test("screenshots", async ({ workbox: page, takeScreenshot }: TestFixtures) => {

apps/playwright-e2e/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"playwright.config.ts",
2222
"playwright.globalSetup.ts",
2323
"types/**/*",
24-
"helpers/console-logging.ts"
24+
"helpers/**/*",
25+
"scripts/theme-extraction-script.test.ts"
2526
],
2627
"exclude": ["node_modules", "test-results", "tests/**/__tests__/**/*"]
2728
}

0 commit comments

Comments
 (0)