refactor: migrate from CRA to Vite and improve testing

Replace Create React App with Vite build system
Add ESLint configuration and improve TypeScript types
Create VSCode UI component mocks for better testing
Update test files with proper async handling
Add Tailwind CSS integration
Fix accessibility by adding ARIA roles
This commit is contained in:
sam hoang
2025-01-29 21:15:28 +07:00
parent 4026a87d2c
commit 12dd54671a
20 changed files with 11173 additions and 16230 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,5 @@
@import "tailwindcss";
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