Skip to content

Commit 911eb92

Browse files
committed
Clean up tab logic in App component
1 parent 567130b commit 911eb92

File tree

2 files changed

+245
-77
lines changed

2 files changed

+245
-77
lines changed

webview-ui/src/App.tsx

Lines changed: 46 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,45 @@
11
import { useCallback, useEffect, useState } from "react"
22
import { useEvent } from "react-use"
3+
34
import { ExtensionMessage } from "../../src/shared/ExtensionMessage"
5+
6+
import { vscode } from "./utils/vscode"
7+
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
48
import ChatView from "./components/chat/ChatView"
59
import HistoryView from "./components/history/HistoryView"
610
import SettingsView from "./components/settings/SettingsView"
711
import WelcomeView from "./components/welcome/WelcomeView"
8-
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
9-
import { vscode } from "./utils/vscode"
1012
import McpView from "./components/mcp/McpView"
1113
import PromptsView from "./components/prompts/PromptsView"
1214

13-
const AppContent = () => {
15+
type Tab = "settings" | "history" | "mcp" | "prompts" | "research" | "chat"
16+
17+
const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
18+
chatButtonClicked: "chat",
19+
settingsButtonClicked: "settings",
20+
promptsButtonClicked: "prompts",
21+
mcpButtonClicked: "mcp",
22+
historyButtonClicked: "history",
23+
}
24+
25+
const App = () => {
1426
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
15-
const [showSettings, setShowSettings] = useState(false)
16-
const [showHistory, setShowHistory] = useState(false)
17-
const [showMcp, setShowMcp] = useState(false)
18-
const [showPrompts, setShowPrompts] = useState(false)
1927
const [showAnnouncement, setShowAnnouncement] = useState(false)
28+
const [tab, setTab] = useState<Tab>("chat")
2029

21-
const handleMessage = useCallback((e: MessageEvent) => {
30+
const onMessage = useCallback((e: MessageEvent) => {
2231
const message: ExtensionMessage = e.data
23-
switch (message.type) {
24-
case "action":
25-
switch (message.action!) {
26-
case "settingsButtonClicked":
27-
setShowSettings(true)
28-
setShowHistory(false)
29-
setShowMcp(false)
30-
setShowPrompts(false)
31-
break
32-
case "historyButtonClicked":
33-
setShowSettings(false)
34-
setShowHistory(true)
35-
setShowMcp(false)
36-
setShowPrompts(false)
37-
break
38-
case "mcpButtonClicked":
39-
setShowSettings(false)
40-
setShowHistory(false)
41-
setShowMcp(true)
42-
setShowPrompts(false)
43-
break
44-
case "promptsButtonClicked":
45-
setShowSettings(false)
46-
setShowHistory(false)
47-
setShowMcp(false)
48-
setShowPrompts(true)
49-
break
50-
case "chatButtonClicked":
51-
setShowSettings(false)
52-
setShowHistory(false)
53-
setShowMcp(false)
54-
setShowPrompts(false)
55-
break
56-
}
57-
break
32+
33+
if (message.type === "action" && message.action) {
34+
const newTab = tabsByMessageAction[message.action]
35+
36+
if (newTab) {
37+
setTab(newTab)
38+
}
5839
}
5940
}, [])
6041

61-
useEvent("message", handleMessage)
42+
useEvent("message", onMessage)
6243

6344
useEffect(() => {
6445
if (shouldShowAnnouncement) {
@@ -71,42 +52,30 @@ const AppContent = () => {
7152
return null
7253
}
7354

74-
return (
55+
// Do not conditionally load ChatView, it's expensive and there's state we
56+
// don't want to lose (user input, disableInput, askResponse promise, etc.)
57+
return showWelcome ? (
58+
<WelcomeView />
59+
) : (
7560
<>
76-
{showWelcome ? (
77-
<WelcomeView />
78-
) : (
79-
<>
80-
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
81-
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
82-
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
83-
{showPrompts && <PromptsView onDone={() => setShowPrompts(false)} />}
84-
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
85-
<ChatView
86-
showHistoryView={() => {
87-
setShowSettings(false)
88-
setShowMcp(false)
89-
setShowPrompts(false)
90-
setShowHistory(true)
91-
}}
92-
isHidden={showSettings || showHistory || showMcp || showPrompts}
93-
showAnnouncement={showAnnouncement}
94-
hideAnnouncement={() => {
95-
setShowAnnouncement(false)
96-
}}
97-
/>
98-
</>
99-
)}
61+
{tab === "settings" && <SettingsView onDone={() => setTab("chat")} />}
62+
{tab === "history" && <HistoryView onDone={() => setTab("chat")} />}
63+
{tab === "mcp" && <McpView onDone={() => setTab("chat")} />}
64+
{tab === "prompts" && <PromptsView onDone={() => setTab("chat")} />}
65+
<ChatView
66+
isHidden={tab !== "chat"}
67+
showAnnouncement={showAnnouncement}
68+
hideAnnouncement={() => setShowAnnouncement(false)}
69+
showHistoryView={() => setTab("history")}
70+
/>
10071
</>
10172
)
10273
}
10374

104-
const App = () => {
105-
return (
106-
<ExtensionStateContextProvider>
107-
<AppContent />
108-
</ExtensionStateContextProvider>
109-
)
110-
}
75+
const AppWithProviders = () => (
76+
<ExtensionStateContextProvider>
77+
<App />
78+
</ExtensionStateContextProvider>
79+
)
11180

112-
export default App
81+
export default AppWithProviders
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// npx jest src/__tests__/App.test.tsx
2+
3+
import React from "react"
4+
import { render, screen, act, cleanup } from "@testing-library/react"
5+
import "@testing-library/jest-dom"
6+
7+
import AppWithProviders from "../App"
8+
9+
jest.mock("../utils/vscode", () => ({
10+
vscode: {
11+
postMessage: jest.fn(),
12+
},
13+
}))
14+
15+
jest.mock("../components/chat/ChatView", () => ({
16+
__esModule: true,
17+
default: function ChatView({ isHidden }: { isHidden: boolean }) {
18+
return (
19+
<div data-testid="chat-view" data-hidden={isHidden}>
20+
Chat View
21+
</div>
22+
)
23+
},
24+
}))
25+
26+
jest.mock("../components/settings/SettingsView", () => ({
27+
__esModule: true,
28+
default: function SettingsView({ onDone }: { onDone: () => void }) {
29+
return (
30+
<div data-testid="settings-view" onClick={onDone}>
31+
Settings View
32+
</div>
33+
)
34+
},
35+
}))
36+
37+
jest.mock("../components/history/HistoryView", () => ({
38+
__esModule: true,
39+
default: function HistoryView({ onDone }: { onDone: () => void }) {
40+
return (
41+
<div data-testid="history-view" onClick={onDone}>
42+
History View
43+
</div>
44+
)
45+
},
46+
}))
47+
48+
jest.mock("../components/mcp/McpView", () => ({
49+
__esModule: true,
50+
default: function McpView({ onDone }: { onDone: () => void }) {
51+
return (
52+
<div data-testid="mcp-view" onClick={onDone}>
53+
MCP View
54+
</div>
55+
)
56+
},
57+
}))
58+
59+
jest.mock("../components/prompts/PromptsView", () => ({
60+
__esModule: true,
61+
default: function PromptsView({ onDone }: { onDone: () => void }) {
62+
return (
63+
<div data-testid="prompts-view" onClick={onDone}>
64+
Prompts View
65+
</div>
66+
)
67+
},
68+
}))
69+
70+
jest.mock("../context/ExtensionStateContext", () => ({
71+
useExtensionState: () => ({
72+
didHydrateState: true,
73+
showWelcome: false,
74+
shouldShowAnnouncement: false,
75+
}),
76+
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
77+
}))
78+
79+
describe("App", () => {
80+
beforeEach(() => {
81+
jest.clearAllMocks()
82+
window.removeEventListener("message", () => {})
83+
})
84+
85+
afterEach(() => {
86+
cleanup()
87+
window.removeEventListener("message", () => {})
88+
})
89+
90+
const triggerMessage = (action: string) => {
91+
const messageEvent = new MessageEvent("message", {
92+
data: {
93+
type: "action",
94+
action,
95+
},
96+
})
97+
window.dispatchEvent(messageEvent)
98+
}
99+
100+
it("shows chat view by default", () => {
101+
render(<AppWithProviders />)
102+
103+
const chatView = screen.getByTestId("chat-view")
104+
expect(chatView).toBeInTheDocument()
105+
expect(chatView.getAttribute("data-hidden")).toBe("false")
106+
})
107+
108+
it("switches to settings view when receiving settingsButtonClicked action", async () => {
109+
render(<AppWithProviders />)
110+
111+
act(() => {
112+
triggerMessage("settingsButtonClicked")
113+
})
114+
115+
const settingsView = await screen.findByTestId("settings-view")
116+
expect(settingsView).toBeInTheDocument()
117+
118+
const chatView = screen.getByTestId("chat-view")
119+
expect(chatView.getAttribute("data-hidden")).toBe("true")
120+
})
121+
122+
it("switches to history view when receiving historyButtonClicked action", async () => {
123+
render(<AppWithProviders />)
124+
125+
act(() => {
126+
triggerMessage("historyButtonClicked")
127+
})
128+
129+
const historyView = await screen.findByTestId("history-view")
130+
expect(historyView).toBeInTheDocument()
131+
132+
const chatView = screen.getByTestId("chat-view")
133+
expect(chatView.getAttribute("data-hidden")).toBe("true")
134+
})
135+
136+
it("switches to MCP view when receiving mcpButtonClicked action", async () => {
137+
render(<AppWithProviders />)
138+
139+
act(() => {
140+
triggerMessage("mcpButtonClicked")
141+
})
142+
143+
const mcpView = await screen.findByTestId("mcp-view")
144+
expect(mcpView).toBeInTheDocument()
145+
146+
const chatView = screen.getByTestId("chat-view")
147+
expect(chatView.getAttribute("data-hidden")).toBe("true")
148+
})
149+
150+
it("switches to prompts view when receiving promptsButtonClicked action", async () => {
151+
render(<AppWithProviders />)
152+
153+
act(() => {
154+
triggerMessage("promptsButtonClicked")
155+
})
156+
157+
const promptsView = await screen.findByTestId("prompts-view")
158+
expect(promptsView).toBeInTheDocument()
159+
160+
const chatView = screen.getByTestId("chat-view")
161+
expect(chatView.getAttribute("data-hidden")).toBe("true")
162+
})
163+
164+
it("returns to chat view when clicking done in settings view", async () => {
165+
render(<AppWithProviders />)
166+
167+
act(() => {
168+
triggerMessage("settingsButtonClicked")
169+
})
170+
171+
const settingsView = await screen.findByTestId("settings-view")
172+
173+
act(() => {
174+
settingsView.click()
175+
})
176+
177+
const chatView = screen.getByTestId("chat-view")
178+
expect(chatView.getAttribute("data-hidden")).toBe("false")
179+
expect(screen.queryByTestId("settings-view")).not.toBeInTheDocument()
180+
})
181+
182+
it.each(["history", "mcp", "prompts"])("returns to chat view when clicking done in %s view", async (view) => {
183+
render(<AppWithProviders />)
184+
185+
act(() => {
186+
triggerMessage(`${view}ButtonClicked`)
187+
})
188+
189+
const viewElement = await screen.findByTestId(`${view}-view`)
190+
191+
act(() => {
192+
viewElement.click()
193+
})
194+
195+
const chatView = screen.getByTestId("chat-view")
196+
expect(chatView.getAttribute("data-hidden")).toBe("false")
197+
expect(screen.queryByTestId(`${view}-view`)).not.toBeInTheDocument()
198+
})
199+
})

0 commit comments

Comments
 (0)