Merge pull request #636 from samhvw8/feat/vite-tailwind

refactor: migrate from CRA to Vite and improve testing
This commit is contained in:
Matt Rubens
2025-01-30 11:40:27 -05:00
committed by GitHub
22 changed files with 11277 additions and 16240 deletions

View File

@@ -0,0 +1,117 @@
import React from "react"
interface VSCodeProps {
children?: React.ReactNode
onClick?: () => void
onChange?: (e: any) => void
onInput?: (e: any) => void
appearance?: string
checked?: boolean
value?: string | number
placeholder?: string
href?: string
"data-testid"?: string
style?: React.CSSProperties
slot?: string
role?: string
disabled?: boolean
className?: string
title?: string
}
export const VSCodeButton: React.FC<VSCodeProps> = ({ children, onClick, appearance, className, ...props }) => {
// For icon buttons, render children directly without any wrapping
if (appearance === "icon") {
return React.createElement(
"button",
{
onClick,
className: `${className || ""}`,
"data-appearance": appearance,
...props,
},
children,
)
}
// For regular buttons
return React.createElement(
"button",
{
onClick,
className: className,
...props,
},
children,
)
}
export const VSCodeCheckbox: React.FC<VSCodeProps> = ({ children, onChange, checked, ...props }) =>
React.createElement("label", {}, [
React.createElement("input", {
key: "input",
type: "checkbox",
checked,
onChange: (e: any) => onChange?.({ target: { checked: e.target.checked } }),
"aria-label": typeof children === "string" ? children : undefined,
...props,
}),
children && React.createElement("span", { key: "label" }, children),
])
export const VSCodeTextField: React.FC<VSCodeProps> = ({ children, value, onInput, placeholder, ...props }) =>
React.createElement("div", { style: { position: "relative", display: "inline-block", width: "100%" } }, [
React.createElement("input", {
key: "input",
type: "text",
value,
onChange: (e: any) => onInput?.({ target: { value: e.target.value } }),
placeholder,
...props,
}),
children,
])
export const VSCodeTextArea: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
React.createElement("textarea", {
value,
onChange: (e: any) => onChange?.({ target: { value: e.target.value } }),
...props,
})
export const VSCodeLink: React.FC<VSCodeProps> = ({ children, href, ...props }) =>
React.createElement("a", { href: href || "#", ...props }, children)
export const VSCodeDropdown: React.FC<VSCodeProps> = ({ children, value, onChange, ...props }) =>
React.createElement("select", { value, onChange, ...props }, children)
export const VSCodeOption: React.FC<VSCodeProps> = ({ children, value, ...props }) =>
React.createElement("option", { value, ...props }, children)
export const VSCodeRadio: React.FC<VSCodeProps> = ({ children, value, checked, onChange, ...props }) =>
React.createElement("label", { style: { display: "inline-flex", alignItems: "center" } }, [
React.createElement("input", {
key: "input",
type: "radio",
value,
checked,
onChange,
...props,
}),
children && React.createElement("span", { key: "label", style: { marginLeft: "4px" } }, children),
])
export const VSCodeRadioGroup: React.FC<VSCodeProps> = ({ children, onChange, ...props }) =>
React.createElement("div", { role: "radiogroup", onChange, ...props }, children)
export const VSCodeSlider: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
React.createElement("input", {
type: "range",
value,
onChange: (e: any) => onChange?.({ target: { value: Number(e.target.value) } }),
min: 0,
max: 1,
step: 0.01,
style: { flexGrow: 1, height: "2px" },
...props,
})

View File

@@ -0,0 +1,14 @@
import React from "react"
export const Checkbox = ({ children, checked, onChange }: any) =>
React.createElement("div", { "data-testid": "mock-checkbox", onClick: onChange }, children)
export const Dropdown = ({ children, value, onChange }: any) =>
React.createElement("div", { "data-testid": "mock-dropdown", onClick: onChange }, children)
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
export type DropdownOption = {
label: string
value: string
}

View File

@@ -37,9 +37,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
const isLastApiReqInterrupted = useMemo(() => {
// Check if last api_req_started is cancelled
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
if (lastApiReqStarted?.text != null) {
const info = JSON.parse(lastApiReqStarted.text)
if (info.cancelReason != null) {
if (lastApiReqStarted?.text) {
const info = JSON.parse(lastApiReqStarted.text) as { cancelReason: string | null }
if (info && info.cancelReason !== null) {
return true
}
}

View File

@@ -2,6 +2,7 @@ import { render, fireEvent, screen } from "@testing-library/react"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import AutoApproveMenu from "../AutoApproveMenu"
import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
import { experimentDefault } from "../../../../../src/shared/experiments"
// Mock the ExtensionStateContext hook
jest.mock("../../../context/ExtensionStateContext")
@@ -41,6 +42,8 @@ describe("AutoApproveMenu", () => {
openAiModels: [],
mcpServers: [],
filePaths: [],
experiments: experimentDefault,
customModes: [],
// Auto-approve specific properties
alwaysAllowReadOnly: false,
@@ -49,6 +52,7 @@ describe("AutoApproveMenu", () => {
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
alwaysApproveResubmit: false,
alwaysAllowModeSwitch: false,
autoApprovalEnabled: false,
// Required setter functions
@@ -59,6 +63,7 @@ describe("AutoApproveMenu", () => {
setAlwaysAllowExecute: jest.fn(),
setAlwaysAllowBrowser: jest.fn(),
setAlwaysAllowMcp: jest.fn(),
setAlwaysAllowModeSwitch: jest.fn(),
setShowAnnouncement: jest.fn(),
setAllowedCommands: jest.fn(),
setSoundEnabled: jest.fn(),
@@ -77,9 +82,13 @@ describe("AutoApproveMenu", () => {
setListApiConfigMeta: jest.fn(),
onUpdateApiConfig: jest.fn(),
setMode: jest.fn(),
setCustomPrompts: jest.fn(),
setCustomModePrompts: jest.fn(),
setCustomSupportPrompts: jest.fn(),
setEnhancementApiConfigId: jest.fn(),
setAutoApprovalEnabled: jest.fn(),
setExperimentEnabled: jest.fn(),
handleInputChange: jest.fn(),
setCustomModes: jest.fn(),
}
beforeEach(() => {

View File

@@ -16,7 +16,6 @@ jest.mock("../../../components/common/MarkdownBlock")
// Get the mocked postMessage function
const mockPostMessage = vscode.postMessage as jest.Mock
/* eslint-enable import/first */
// Mock ExtensionStateContext
jest.mock("../../../context/ExtensionStateContext")

View File

@@ -202,6 +202,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<VSCodeRadioGroup
style={{ display: "flex", flexWrap: "wrap" }}
value={sortOption}
role="radiogroup"
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="oldest">Oldest</VSCodeRadio>

View File

@@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import "@testing-library/jest-dom"
import PromptsView from "../PromptsView"
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
@@ -98,20 +98,14 @@ describe("PromptsView", () => {
expect(codeTab).toHaveAttribute("data-active", "false")
})
it("handles prompt changes correctly", () => {
it("handles prompt changes correctly", async () => {
renderPromptsView()
const textarea = screen.getByTestId("code-prompt-textarea")
fireEvent(
textarea,
new CustomEvent("change", {
detail: {
target: {
value: "New prompt value",
},
},
}),
)
// Get the textarea
const textarea = await waitFor(() => screen.getByTestId("code-prompt-textarea"))
fireEvent.change(textarea, {
target: { value: "New prompt value" },
})
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "updatePrompt",
@@ -163,24 +157,18 @@ describe("PromptsView", () => {
expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument()
})
it("handles API configuration selection", () => {
it("handles API configuration selection", async () => {
renderPromptsView()
// Click the ENHANCE tab first to show the API config dropdown
const enhanceTab = screen.getByTestId("ENHANCE-tab")
fireEvent.click(enhanceTab)
const dropdown = screen.getByTestId("api-config-dropdown")
fireEvent(
dropdown,
new CustomEvent("change", {
detail: {
target: {
value: "config1",
},
},
}),
)
// Wait for the ENHANCE tab click to take effect
const dropdown = await waitFor(() => screen.getByTestId("api-config-dropdown"))
fireEvent.change(dropdown, {
target: { value: "config1" },
})
expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith("config1")
expect(vscode.postMessage).toHaveBeenCalledWith({
@@ -198,13 +186,9 @@ describe("PromptsView", () => {
})
const textarea = screen.getByTestId("global-custom-instructions-textarea")
const changeEvent = new CustomEvent("change", {
detail: { target: { value: "" } },
fireEvent.change(textarea, {
target: { value: "" },
})
Object.defineProperty(changeEvent, "target", {
value: { value: "" },
})
await fireEvent(textarea, changeEvent)
expect(setCustomInstructions).toHaveBeenCalledWith(undefined)
expect(vscode.postMessage).toHaveBeenCalledWith({

View File

@@ -41,7 +41,10 @@ describe("ApiConfigManager", () => {
const defaultProps = {
currentApiConfigName: "Default Config",
listApiConfigMeta: [{ name: "Default Config" }, { name: "Another Config" }],
listApiConfigMeta: [
{ id: "default", name: "Default Config" },
{ id: "another", name: "Another Config" },
],
onSelectConfig: mockOnSelectConfig,
onDeleteConfig: mockOnDeleteConfig,
onRenameConfig: mockOnRenameConfig,
@@ -120,7 +123,7 @@ describe("ApiConfigManager", () => {
})
it("disables delete button when only one config exists", () => {
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ name: "Default Config" }]} />)
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ id: "default", name: "Default Config" }]} />)
const deleteButton = screen.getByTitle("Cannot delete the only profile")
expect(deleteButton).toHaveAttribute("disabled")

View File

@@ -1,3 +1,67 @@
@import "tailwindcss";
@layer base {
/* Theme Variables - VSCode Integration */
:root {
/* Base Colors */
--background: var(--vscode-editor-background);
--foreground: var(--vscode-editor-foreground);
/* Component Colors */
--card: var(--vscode-editor-background);
--card-foreground: var(--vscode-editor-foreground);
--popover: var(--vscode-menu-background, var(--vscode-editor-background));
--popover-foreground: var(--vscode-menu-foreground, var(--vscode-editor-foreground));
/* Button Colors */
--primary: var(--vscode-button-background);
--primary-foreground: var(--vscode-button-foreground);
--secondary: var(--vscode-button-secondaryBackground);
--secondary-foreground: var(--vscode-button-secondaryForeground);
--accent: var(--vscode-focusBorder);
--accent-foreground: var(--vscode-button-foreground);
/* State Colors */
--muted: var(--vscode-disabledForeground);
--muted-foreground: var(--vscode-descriptionForeground);
--destructive: var(--vscode-errorForeground);
--destructive-foreground: var(--vscode-editor-background);
/* UI Elements */
--border: var(--vscode-widget-border);
--input: var(--vscode-input-background);
--ring: var(--vscode-focusBorder);
--radius: 0.5rem;
/* Chart Colors - Using VSCode's chart colors */
--chart-1: var(--vscode-charts-red);
--chart-2: var(--vscode-charts-blue);
--chart-3: var(--vscode-charts-yellow);
--chart-4: var(--vscode-charts-orange);
--chart-5: var(--vscode-charts-green);
}
}
@layer components {
/* Border Styles */
.border,
.border-r,
.border-l,
.border-t,
.border-b,
.border-x,
.border-y {
border-color: var(--border);
}
/* Code Block Styles */
pre,
code {
background-color: var(--vscode-textCodeBlock-background);
}
}
/* Form Element Focus States */
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}

View File

@@ -2,7 +2,6 @@ import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import "../../node_modules/@vscode/codicons/dist/codicon.css"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
@@ -11,8 +10,3 @@ root.render(
<App />
</React.StrictMode>,
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,15 +0,0 @@
import { ReportHandler } from "web-vitals"
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals