Skip to content

Nate/gui testing #5337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/scopes/Continue.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const router = createMemoryRouter([
Prevents entire app from rerendering continuously with useSetup in App
TODO - look into a more redux-esque way to do this
*/
function SetupListeners() {
export function SetupListeners() {
useSetup();
return <></>;
}
Expand Down
2 changes: 0 additions & 2 deletions gui/src/components/find/FindWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import {
useRef,
useState,
} from "react";
import { useSelector } from "react-redux";
import { HeaderButton, Input } from "..";
import { RootState } from "../../redux/store";
import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";
import { useAppSelector } from "../../redux/hooks";

Expand Down
5 changes: 4 additions & 1 deletion gui/src/components/mainInput/ContinueInputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
: {};

return (
<div className={`${props.hidden ? "hidden" : ""}`}>
<div
className={`${props.hidden ? "hidden" : ""}`}
data-testid="continue-input-box"
>
<div className={`relative flex flex-col px-2`}>
{props.isMainInput && <Lump />}
<GradientBorder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,14 @@ export class IdeMessenger implements IIdeMessenger {
export const IdeMessengerContext = createContext<IIdeMessenger>(
new IdeMessenger(),
);

export const IdeMessengerProvider: React.FC<{
children: React.ReactNode;
messenger?: IIdeMessenger;
}> = ({ children, messenger = new IdeMessenger() }) => {
return (
<IdeMessengerContext.Provider value={messenger}>
{children}
</IdeMessengerContext.Provider>
);
};
122 changes: 122 additions & 0 deletions gui/src/context/MockIdeMessenger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { IDE, PromptLog } from "core";
import {
FromWebviewProtocol,
ToCoreProtocol,
ToWebviewProtocol,
} from "core/protocol";
import { MessageIde } from "core/protocol/messenger/messageIde";
import {
GeneratorReturnType,
GeneratorYieldType,
WebviewSingleProtocolMessage,
} from "core/protocol/util";
import { ChatMessage } from "../redux/store";
import { IIdeMessenger } from "./IdeMessenger";

async function defaultMockHandleMessage<T extends keyof FromWebviewProtocol>(
messageType: T,
data: FromWebviewProtocol[T][0],
): Promise<FromWebviewProtocol[T][1]> {
function returnFor<K extends keyof FromWebviewProtocol>(
_: K,
value: FromWebviewProtocol[K][1],
): FromWebviewProtocol[T][1] {
return value as unknown as FromWebviewProtocol[T][1];
}

switch (messageType) {
case "history/list":
return returnFor("history/list", [
{
title: "Session 1",
sessionId: "session-1",
dateCreated: new Date().toString(),
workspaceDirectory: "/tmp",
},
]);
case "getControlPlaneSessionInfo":
return returnFor("getControlPlaneSessionInfo", {
accessToken: "",
account: {
label: "",
id: "",
},
});
case "config/getSerializedProfileInfo":
return returnFor("config/getSerializedProfileInfo", {
organizations: [],
profileId: "test-profile",
result: {
config: undefined,
errors: [],
configLoadInterrupted: false,
},
selectedOrgId: "local",
});
default:
throw new Error(`Unknown message type ${messageType}`);
}
}

export class MockIdeMessenger implements IIdeMessenger {
ide: IDE;

constructor() {
this.ide = new MessageIde(
(messageType, data) => {
throw new Error("Not implemented");
},
(messageType, callback) => {},
);
}

async *llmStreamChat(
msg: ToCoreProtocol["llm/streamChat"][0],
cancelToken: AbortSignal,
): AsyncGenerator<ChatMessage[], PromptLog | undefined> {
yield [
{
role: "assistant",
content: "This is a test",
},
];

return undefined;
}

post<T extends keyof FromWebviewProtocol>(
messageType: T,
data: FromWebviewProtocol[T][0],
messageId?: string,
attempt?: number,
): void {}

async request<T extends keyof FromWebviewProtocol>(
messageType: T,
data: FromWebviewProtocol[T][0],
): Promise<WebviewSingleProtocolMessage<T>> {
const content = await defaultMockHandleMessage(messageType, data);
return {
status: "success",
content,
done: true,
};
}

respond<T extends keyof ToWebviewProtocol>(
messageType: T,
data: ToWebviewProtocol[T][1],
messageId: string,
): void {}

async *streamRequest<T extends keyof FromWebviewProtocol>(
messageType: T,
data: FromWebviewProtocol[T][0],
cancelToken?: AbortSignal,
): AsyncGenerator<
GeneratorYieldType<FromWebviewProtocol[T][1]>[],
GeneratorReturnType<FromWebviewProtocol[T][1]> | undefined
> {
return undefined;
}
}
62 changes: 62 additions & 0 deletions gui/src/pages/gui/Chat.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { renderWithProviders } from "../../util/test/render";
import { screen, waitFor } from "@testing-library/dom";
import { act, fireEvent } from "@testing-library/react";
import { Chat } from "./Chat";

describe("Chat page test", () => {
it("should render input box", async () => {
await renderWithProviders(<Chat />);
expect(await screen.findByTestId("continue-input-box")).toBeInTheDocument();
});

it("should be able to toggle modes", async () => {
await renderWithProviders(<Chat />);
expect(screen.getByText("Chat")).toBeInTheDocument();

// Simulate cmd+. keyboard shortcut to toggle modes
act(() => {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: ".",
metaKey: true, // cmd key on Mac
}),
);
});

// Check that it switched to Edit mode
expect(await screen.findByText("Edit")).toBeInTheDocument();

act(() => {
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: ".",
metaKey: true, // cmd key on Mac
}),
);
});

// Check that it switched to Agent mode
expect(await screen.findByText("Agent")).toBeInTheDocument();
});

it.skip("should send a message and receive a response", async () => {
const { user, container } = await renderWithProviders(<Chat />);
const inputBox = await waitFor(() =>
container.querySelector(".ProseMirror")!.querySelector("p"),
);
expect(inputBox).toBeDefined();

const sendButton = await screen.findByTestId("submit-input-button");

await act(async () => {
// Focus input box
inputBox!.focus();

// Type message
await user.type(inputBox!, "Hello, world!");

sendButton.click();
});
expect(await screen.findByText("Hello, world!")).toBeInTheDocument();
});
});
26 changes: 14 additions & 12 deletions gui/src/pages/history/history.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import { screen } from "@testing-library/dom";
import { renderWithProviders } from "../../util/test/render";
import HistoryPage from "./index";

const navigateFn = vi.fn();

vi.mock("react-router-dom", async () => {
const original = await vi.importActual("react-router-dom");
return {
...original,
useNavigate: () => navigateFn,
};
});

describe("history Page test", () => {
it("History text is existed after render", () => {
renderWithProviders(<HistoryPage />);
it("History text is existed after render", async () => {
await renderWithProviders(<HistoryPage />);
expect(screen.getByTestId("history-sessions-note")).toBeInTheDocument();
});

it("History shows the first item in the list", async () => {
await renderWithProviders(<HistoryPage />);
const sessionElement = await screen.findByText(
"Session 1",
{},
{
timeout: 3000, // There is a 2000ms timeout before the first call to refreshSessionMetadata is called
},
);
expect(sessionElement).toBeInTheDocument();
});
});
6 changes: 2 additions & 4 deletions gui/src/redux/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,5 @@ export const selectUseActiveFile = createSelector(
(defaultContext) => defaultContext?.includes("activeFile" as any),
);

export const selectUseHub = createSelector(
[(state: RootState) => state.config.config.usePlatform],
(usePlatform) => usePlatform,
);
export const selectUseHub = (state: RootState) =>
state.config.config.usePlatform;
8 changes: 5 additions & 3 deletions gui/src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ const persistedReducer = persistReducer<ReturnType<typeof rootReducer>>(
rootReducer,
);

export function setupStore() {
export function setupStore(options: { ideMessenger?: IIdeMessenger }) {
const ideMessenger = options.ideMessenger ?? new IdeMessenger();

const logger = createLogger({
// Customize logger options if needed
collapsed: true, // Collapse console groups by default
Expand All @@ -126,7 +128,7 @@ export function setupStore() {
serializableCheck: false,
thunk: {
extraArgument: {
ideMessenger: new IdeMessenger(),
ideMessenger,
},
},
}),
Expand All @@ -148,7 +150,7 @@ export type AppThunkDispatch = ThunkDispatch<
UnknownAction
>;

export const store = setupStore();
export const store = setupStore({});

export type RootState = ReturnType<typeof rootReducer>;

Expand Down
Loading
Loading