Merge pull request #1025 from RooVetGit/cte/app-tabs

This commit is contained in:
Chris Estreich 2025-02-17 08:20:01 -08:00 committed by GitHub
commit f3d4c37b2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 247 additions and 79 deletions

View file

@ -1,64 +1,45 @@
import { useCallback, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
import { vscode } from "./utils/vscode"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import ChatView from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
import SettingsView from "./components/settings/SettingsView"
import WelcomeView from "./components/welcome/WelcomeView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/McpView"
import PromptsView from "./components/prompts/PromptsView"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
const [showSettings, setShowSettings] = useState(false)
const [showHistory, setShowHistory] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [showPrompts, setShowPrompts] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
const handleMessage = useCallback((e: MessageEvent) => {
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
chatButtonClicked: "chat",
settingsButtonClicked: "settings",
promptsButtonClicked: "prompts",
mcpButtonClicked: "mcp",
historyButtonClicked: "history",
}
const App = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [tab, setTab] = useState<Tab>("chat")
const onMessage = useCallback((e: MessageEvent) => {
const message: ExtensionMessage = e.data
switch (message.type) {
case "action":
switch (message.action!) {
case "settingsButtonClicked":
setShowSettings(true)
setShowHistory(false)
setShowMcp(false)
setShowPrompts(false)
break
case "historyButtonClicked":
setShowSettings(false)
setShowHistory(true)
setShowMcp(false)
setShowPrompts(false)
break
case "mcpButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(true)
setShowPrompts(false)
break
case "promptsButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowPrompts(true)
break
case "chatButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
setShowPrompts(false)
break
}
break
if (message.type === "action" && message.action) {
const newTab = tabsByMessageAction[message.action]
if (newTab) {
setTab(newTab)
}
}
}, [])
useEvent("message", handleMessage)
useEvent("message", onMessage)
useEffect(() => {
if (shouldShowAnnouncement) {
@ -71,42 +52,30 @@ const AppContent = () => {
return null
}
return (
// Do not conditionally load ChatView, it's expensive and there's state we
// don't want to lose (user input, disableInput, askResponse promise, etc.)
return showWelcome ? (
<WelcomeView />
) : (
<>
{showWelcome ? (
<WelcomeView />
) : (
<>
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
{showPrompts && <PromptsView onDone={() => setShowPrompts(false)} />}
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
<ChatView
showHistoryView={() => {
setShowSettings(false)
setShowMcp(false)
setShowPrompts(false)
setShowHistory(true)
}}
isHidden={showSettings || showHistory || showMcp || showPrompts}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => {
setShowAnnouncement(false)
}}
/>
</>
)}
{tab === "settings" && <SettingsView onDone={() => setTab("chat")} />}
{tab === "history" && <HistoryView onDone={() => setTab("chat")} />}
{tab === "mcp" && <McpView onDone={() => setTab("chat")} />}
{tab === "prompts" && <PromptsView onDone={() => setTab("chat")} />}
<ChatView
isHidden={tab !== "chat"}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => setShowAnnouncement(false)}
showHistoryView={() => setTab("history")}
/>
</>
)
}
const App = () => {
return (
<ExtensionStateContextProvider>
<AppContent />
</ExtensionStateContextProvider>
)
}
const AppWithProviders = () => (
<ExtensionStateContextProvider>
<App />
</ExtensionStateContextProvider>
)
export default App
export default AppWithProviders

View file

@ -0,0 +1,199 @@
// npx jest src/__tests__/App.test.tsx
import React from "react"
import { render, screen, act, cleanup } from "@testing-library/react"
import "@testing-library/jest-dom"
import AppWithProviders from "../App"
jest.mock("../utils/vscode", () => ({
vscode: {
postMessage: jest.fn(),
},
}))
jest.mock("../components/chat/ChatView", () => ({
__esModule: true,
default: function ChatView({ isHidden }: { isHidden: boolean }) {
return (
<div data-testid="chat-view" data-hidden={isHidden}>
Chat View
</div>
)
},
}))
jest.mock("../components/settings/SettingsView", () => ({
__esModule: true,
default: function SettingsView({ onDone }: { onDone: () => void }) {
return (
<div data-testid="settings-view" onClick={onDone}>
Settings View
</div>
)
},
}))
jest.mock("../components/history/HistoryView", () => ({
__esModule: true,
default: function HistoryView({ onDone }: { onDone: () => void }) {
return (
<div data-testid="history-view" onClick={onDone}>
History View
</div>
)
},
}))
jest.mock("../components/mcp/McpView", () => ({
__esModule: true,
default: function McpView({ onDone }: { onDone: () => void }) {
return (
<div data-testid="mcp-view" onClick={onDone}>
MCP View
</div>
)
},
}))
jest.mock("../components/prompts/PromptsView", () => ({
__esModule: true,
default: function PromptsView({ onDone }: { onDone: () => void }) {
return (
<div data-testid="prompts-view" onClick={onDone}>
Prompts View
</div>
)
},
}))
jest.mock("../context/ExtensionStateContext", () => ({
useExtensionState: () => ({
didHydrateState: true,
showWelcome: false,
shouldShowAnnouncement: false,
}),
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
describe("App", () => {
beforeEach(() => {
jest.clearAllMocks()
window.removeEventListener("message", () => {})
})
afterEach(() => {
cleanup()
window.removeEventListener("message", () => {})
})
const triggerMessage = (action: string) => {
const messageEvent = new MessageEvent("message", {
data: {
type: "action",
action,
},
})
window.dispatchEvent(messageEvent)
}
it("shows chat view by default", () => {
render(<AppWithProviders />)
const chatView = screen.getByTestId("chat-view")
expect(chatView).toBeInTheDocument()
expect(chatView.getAttribute("data-hidden")).toBe("false")
})
it("switches to settings view when receiving settingsButtonClicked action", async () => {
render(<AppWithProviders />)
act(() => {
triggerMessage("settingsButtonClicked")
})
const settingsView = await screen.findByTestId("settings-view")
expect(settingsView).toBeInTheDocument()
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
})
it("switches to history view when receiving historyButtonClicked action", async () => {
render(<AppWithProviders />)
act(() => {
triggerMessage("historyButtonClicked")
})
const historyView = await screen.findByTestId("history-view")
expect(historyView).toBeInTheDocument()
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
})
it("switches to MCP view when receiving mcpButtonClicked action", async () => {
render(<AppWithProviders />)
act(() => {
triggerMessage("mcpButtonClicked")
})
const mcpView = await screen.findByTestId("mcp-view")
expect(mcpView).toBeInTheDocument()
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
})
it("switches to prompts view when receiving promptsButtonClicked action", async () => {
render(<AppWithProviders />)
act(() => {
triggerMessage("promptsButtonClicked")
})
const promptsView = await screen.findByTestId("prompts-view")
expect(promptsView).toBeInTheDocument()
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("true")
})
it("returns to chat view when clicking done in settings view", async () => {
render(<AppWithProviders />)
act(() => {
triggerMessage("settingsButtonClicked")
})
const settingsView = await screen.findByTestId("settings-view")
act(() => {
settingsView.click()
})
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("false")
expect(screen.queryByTestId("settings-view")).not.toBeInTheDocument()
})
it.each(["history", "mcp", "prompts"])("returns to chat view when clicking done in %s view", async (view) => {
render(<AppWithProviders />)
act(() => {
triggerMessage(`${view}ButtonClicked`)
})
const viewElement = await screen.findByTestId(`${view}-view`)
act(() => {
viewElement.click()
})
const chatView = screen.getByTestId("chat-view")
expect(chatView.getAttribute("data-hidden")).toBe("false")
expect(screen.queryByTestId(`${view}-view`)).not.toBeInTheDocument()
})
})