mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
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:
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal file
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal 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,
|
||||
})
|
||||
14
webview-ui/src/__mocks__/vscrui.ts
Normal file
14
webview-ui/src/__mocks__/vscrui.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
1
webview-ui/src/react-app-env.d.ts
vendored
1
webview-ui/src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user