|
1 | 1 | import React from "react" |
2 | | -import { render, waitFor, act } from "@testing-library/react" |
| 2 | +import { render, waitFor, act, fireEvent } from "@testing-library/react" |
3 | 3 | import ChatView from "../ChatView" |
4 | 4 | import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext" |
5 | 5 | import { vscode } from "../../../utils/vscode" |
@@ -60,25 +60,34 @@ interface ChatTextAreaProps { |
60 | 60 | shouldDisableImages?: boolean |
61 | 61 | } |
62 | 62 |
|
63 | | -const mockInputRef = React.createRef<HTMLInputElement>() |
| 63 | +interface ChatTextAreaHandle { |
| 64 | + focus: () => void |
| 65 | + current: HTMLInputElement | null |
| 66 | +} |
| 67 | + |
64 | 68 | const mockFocus = jest.fn() |
65 | 69 |
|
66 | 70 | jest.mock("../ChatTextArea", () => { |
67 | 71 | const mockReact = require("react") |
| 72 | + |
68 | 73 | return { |
69 | 74 | __esModule: true, |
70 | 75 | default: mockReact.forwardRef(function MockChatTextArea( |
71 | 76 | props: ChatTextAreaProps, |
72 | | - ref: React.ForwardedRef<{ focus: () => void }>, |
| 77 | + ref: React.ForwardedRef<ChatTextAreaHandle>, |
73 | 78 | ) { |
74 | | - // Use useImperativeHandle to expose the mock focus method |
75 | | - React.useImperativeHandle(ref, () => ({ |
76 | | - focus: mockFocus, |
77 | | - })) |
| 79 | + const inputRef = mockReact.useRef(null) |
| 80 | + |
| 81 | + mockReact.useImperativeHandle(ref, () => { |
| 82 | + if (inputRef.current) { |
| 83 | + inputRef.current.focus = mockFocus |
| 84 | + } |
| 85 | + return inputRef.current |
| 86 | + }) |
78 | 87 |
|
79 | 88 | return ( |
80 | 89 | <div data-testid="chat-textarea"> |
81 | | - <input ref={mockInputRef} type="text" onChange={(e) => props.onSend(e.target.value)} /> |
| 90 | + <input ref={inputRef} type="text" onChange={(e) => props.onSend(e.target.value)} /> |
82 | 91 | </div> |
83 | 92 | ) |
84 | 93 | }), |
@@ -151,6 +160,10 @@ const mockPostMessage = (state: Partial<ExtensionState>) => { |
151 | 160 | ) |
152 | 161 | } |
153 | 162 |
|
| 163 | +const sleep = (timeout: number) => { |
| 164 | + return act(() => new Promise((resolve) => setTimeout(resolve, timeout))) |
| 165 | +} |
| 166 | + |
154 | 167 | describe("ChatView - Auto Approval Tests", () => { |
155 | 168 | beforeEach(() => { |
156 | 169 | jest.clearAllMocks() |
@@ -1102,12 +1115,6 @@ describe("ChatView - Focus Grabbing Tests", () => { |
1102 | 1115 | }) |
1103 | 1116 |
|
1104 | 1117 | it("does not grab focus when follow-up question presented", async () => { |
1105 | | - const sleep = async (timeout: number) => { |
1106 | | - await act(async () => { |
1107 | | - await new Promise((resolve) => setTimeout(resolve, timeout)) |
1108 | | - }) |
1109 | | - } |
1110 | | - |
1111 | 1118 | render( |
1112 | 1119 | <ExtensionStateContextProvider> |
1113 | 1120 | <ChatView |
@@ -1145,7 +1152,7 @@ describe("ChatView - Focus Grabbing Tests", () => { |
1145 | 1152 | // wait for focus updates (can take 50msecs) |
1146 | 1153 | await sleep(100) |
1147 | 1154 |
|
1148 | | - const FOCUS_CALLS_ON_INIT = 2 |
| 1155 | + const FOCUS_CALLS_ON_INIT = 0 |
1149 | 1156 | expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT) |
1150 | 1157 |
|
1151 | 1158 | // Finish task, and send the followup ask message (streaming unfinished) |
@@ -1195,4 +1202,203 @@ describe("ChatView - Focus Grabbing Tests", () => { |
1195 | 1202 | // focus() should not have been called again |
1196 | 1203 | expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT) |
1197 | 1204 | }) |
| 1205 | + |
| 1206 | + it("grabs focus to restore focus when unhidden", async () => { |
| 1207 | + const { getByTestId, rerender } = render( |
| 1208 | + <ExtensionStateContextProvider> |
| 1209 | + <ChatView |
| 1210 | + isHidden={false} |
| 1211 | + showAnnouncement={false} |
| 1212 | + hideAnnouncement={() => {}} |
| 1213 | + showHistoryView={() => {}} |
| 1214 | + /> |
| 1215 | + </ExtensionStateContextProvider>, |
| 1216 | + ) |
| 1217 | + |
| 1218 | + const textAreaElement = getByTestId("chat-textarea").querySelector("input") |
| 1219 | + |
| 1220 | + // simulate focus on textArea |
| 1221 | + if (textAreaElement) { |
| 1222 | + fireEvent.focus(textAreaElement) |
| 1223 | + } |
| 1224 | + |
| 1225 | + await waitFor(() => { |
| 1226 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1227 | + }) |
| 1228 | + |
| 1229 | + rerender( |
| 1230 | + <ExtensionStateContextProvider> |
| 1231 | + <ChatView |
| 1232 | + isHidden={true} |
| 1233 | + showAnnouncement={false} |
| 1234 | + hideAnnouncement={() => {}} |
| 1235 | + showHistoryView={() => {}} |
| 1236 | + /> |
| 1237 | + </ExtensionStateContextProvider>, |
| 1238 | + ) |
| 1239 | + |
| 1240 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1241 | + |
| 1242 | + rerender( |
| 1243 | + <ExtensionStateContextProvider> |
| 1244 | + <ChatView |
| 1245 | + isHidden={false} |
| 1246 | + showAnnouncement={false} |
| 1247 | + hideAnnouncement={() => {}} |
| 1248 | + showHistoryView={() => {}} |
| 1249 | + /> |
| 1250 | + </ExtensionStateContextProvider>, |
| 1251 | + ) |
| 1252 | + |
| 1253 | + await waitFor(() => { |
| 1254 | + expect(mockFocus).toHaveBeenCalledTimes(2) |
| 1255 | + }) |
| 1256 | + }) |
| 1257 | + |
| 1258 | + it("does not grab focus when unhidden and not previously focused", async () => { |
| 1259 | + const { rerender } = render( |
| 1260 | + <ExtensionStateContextProvider> |
| 1261 | + <ChatView |
| 1262 | + isHidden={false} |
| 1263 | + showAnnouncement={false} |
| 1264 | + hideAnnouncement={() => {}} |
| 1265 | + showHistoryView={() => {}} |
| 1266 | + /> |
| 1267 | + </ExtensionStateContextProvider>, |
| 1268 | + ) |
| 1269 | + |
| 1270 | + rerender( |
| 1271 | + <ExtensionStateContextProvider> |
| 1272 | + <ChatView |
| 1273 | + isHidden={true} |
| 1274 | + showAnnouncement={false} |
| 1275 | + hideAnnouncement={() => {}} |
| 1276 | + showHistoryView={() => {}} |
| 1277 | + /> |
| 1278 | + </ExtensionStateContextProvider>, |
| 1279 | + ) |
| 1280 | + |
| 1281 | + rerender( |
| 1282 | + <ExtensionStateContextProvider> |
| 1283 | + <ChatView |
| 1284 | + isHidden={false} |
| 1285 | + showAnnouncement={false} |
| 1286 | + hideAnnouncement={() => {}} |
| 1287 | + showHistoryView={() => {}} |
| 1288 | + /> |
| 1289 | + </ExtensionStateContextProvider>, |
| 1290 | + ) |
| 1291 | + |
| 1292 | + await sleep(100) |
| 1293 | + expect(mockFocus).toHaveBeenCalledTimes(0) |
| 1294 | + }) |
| 1295 | + |
| 1296 | + const sendActionMessage = (action: string) => { |
| 1297 | + return act(() => { |
| 1298 | + const message = { |
| 1299 | + type: "action", |
| 1300 | + action, |
| 1301 | + } |
| 1302 | + window.postMessage(message, "*") |
| 1303 | + }) |
| 1304 | + } |
| 1305 | + |
| 1306 | + it("grabs focus to restore focus when extension goes invisible and becomes visible again", async () => { |
| 1307 | + const { findByTestId } = render( |
| 1308 | + <ExtensionStateContextProvider> |
| 1309 | + <ChatView |
| 1310 | + isHidden={false} |
| 1311 | + showAnnouncement={false} |
| 1312 | + hideAnnouncement={() => {}} |
| 1313 | + showHistoryView={() => {}} |
| 1314 | + /> |
| 1315 | + </ExtensionStateContextProvider>, |
| 1316 | + ) |
| 1317 | + |
| 1318 | + const textAreaElement = (await findByTestId("chat-textarea")).querySelector("input") |
| 1319 | + |
| 1320 | + // simulate focus on textArea |
| 1321 | + if (textAreaElement) { |
| 1322 | + fireEvent.focus(textAreaElement) |
| 1323 | + } |
| 1324 | + |
| 1325 | + await waitFor(() => { |
| 1326 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1327 | + }) |
| 1328 | + |
| 1329 | + await sendActionMessage("didBecomeInvisible") |
| 1330 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1331 | + |
| 1332 | + // we need processing of message to complete before toggling visibility again. |
| 1333 | + await sleep(0) |
| 1334 | + |
| 1335 | + await sendActionMessage("didBecomeVisible") |
| 1336 | + await waitFor(() => { |
| 1337 | + expect(mockFocus).toHaveBeenCalledTimes(2) |
| 1338 | + }) |
| 1339 | + }) |
| 1340 | + |
| 1341 | + it("does not grab focus when unhidden and not previously focused", async () => { |
| 1342 | + render( |
| 1343 | + <ExtensionStateContextProvider> |
| 1344 | + <ChatView |
| 1345 | + isHidden={false} |
| 1346 | + showAnnouncement={false} |
| 1347 | + hideAnnouncement={() => {}} |
| 1348 | + showHistoryView={() => {}} |
| 1349 | + /> |
| 1350 | + </ExtensionStateContextProvider>, |
| 1351 | + ) |
| 1352 | + |
| 1353 | + await sendActionMessage("didBecomeInvisible") |
| 1354 | + expect(mockFocus).toHaveBeenCalledTimes(0) |
| 1355 | + |
| 1356 | + // we need processing of message to complete before toggling visibility again. |
| 1357 | + await sleep(0) |
| 1358 | + |
| 1359 | + await sendActionMessage("didBecomeVisible") |
| 1360 | + |
| 1361 | + await sleep(100) |
| 1362 | + expect(mockFocus).toHaveBeenCalledTimes(0) |
| 1363 | + }) |
| 1364 | + |
| 1365 | + it("does not grab focus when extension becomes visible when previously focused then blurred", async () => { |
| 1366 | + const { findByTestId } = render( |
| 1367 | + <ExtensionStateContextProvider> |
| 1368 | + <ChatView |
| 1369 | + isHidden={false} |
| 1370 | + showAnnouncement={false} |
| 1371 | + hideAnnouncement={() => {}} |
| 1372 | + showHistoryView={() => {}} |
| 1373 | + /> |
| 1374 | + </ExtensionStateContextProvider>, |
| 1375 | + ) |
| 1376 | + |
| 1377 | + const textAreaElement = (await findByTestId("chat-textarea")).querySelector("input") |
| 1378 | + |
| 1379 | + // simulate focus on textArea |
| 1380 | + if (textAreaElement) { |
| 1381 | + fireEvent.focus(textAreaElement) |
| 1382 | + } |
| 1383 | + |
| 1384 | + await waitFor(() => { |
| 1385 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1386 | + }) |
| 1387 | + |
| 1388 | + // simulate blur on textArea |
| 1389 | + if (textAreaElement) { |
| 1390 | + fireEvent.blur(textAreaElement) |
| 1391 | + } |
| 1392 | + |
| 1393 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1394 | + |
| 1395 | + await sendActionMessage("didBecomeInvisible") |
| 1396 | + |
| 1397 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1398 | + |
| 1399 | + await sendActionMessage("didBecomeVisible") |
| 1400 | + |
| 1401 | + await sleep(100) |
| 1402 | + expect(mockFocus).toHaveBeenCalledTimes(1) |
| 1403 | + }) |
1198 | 1404 | }) |
0 commit comments