From d3f6508e3297faf02e9338ec617e6fa60090ca61 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Wed, 7 May 2025 08:20:23 +0400 Subject: [PATCH] refactor(frontend): Auth (#8308) --- .../chat/action-suggestions.test.tsx | 57 +++++++++++-------- .../analytics-consent-form-modal.test.tsx | 9 +-- .../components/features/auth-modal.test.tsx | 21 ++++--- .../conversation-panel.test.tsx | 43 +++++++------- .../features/home/home-header.test.tsx | 9 +-- .../features/home/repo-connector.test.tsx | 49 ++++++++++------ .../features/home/task-card.test.tsx | 9 +-- .../features/home/task-suggestions.test.tsx | 13 ++--- .../features/sidebar/sidebar.test.tsx | 5 +- .../context/ws-client-provider.test.tsx | 9 +-- .../hooks/mutation/use-save-settings.test.tsx | 9 +-- frontend/__tests__/routes/_oh.app.test.tsx | 30 ---------- .../__tests__/routes/app-settings.test.tsx | 3 +- .../__tests__/routes/git-settings.test.tsx | 15 +++-- .../__tests__/routes/home-screen.test.tsx | 41 ++++++++----- .../__tests__/routes/llm-settings.test.tsx | 3 +- frontend/__tests__/routes/settings.test.tsx | 24 ++++---- .../convert-raw-providers-to-list.test.ts | 22 +++++++ .../features/chat/action-suggestions.tsx | 5 +- .../features/home/repo-connector.tsx | 5 +- .../components/features/sidebar/sidebar.tsx | 10 +--- frontend/src/context/auth-context.tsx | 52 ----------------- frontend/src/context/ws-client-provider.tsx | 6 +- frontend/src/entry.client.tsx | 17 ++---- frontend/src/hooks/mutation/use-logout.ts | 31 +++------- frontend/src/hooks/query/use-api-keys.ts | 4 +- frontend/src/hooks/query/use-git-user.ts | 22 ++----- frontend/src/hooks/query/use-is-authed.ts | 7 +-- frontend/src/hooks/query/use-settings.ts | 22 ++----- .../src/hooks/query/use-suggested-tasks.ts | 6 +- .../src/hooks/query/use-user-repositories.ts | 11 +--- frontend/src/hooks/use-app-logout.ts | 11 ---- frontend/src/hooks/use-auth-url.ts | 25 +++----- frontend/src/hooks/use-user-providers.ts | 9 +++ frontend/src/query-client-config.ts | 25 +++++--- frontend/src/routes/git-settings.tsx | 8 +-- frontend/src/routes/home.tsx | 6 +- .../utils/convert-raw-providers-to-list.ts | 14 +++++ frontend/test-utils.tsx | 25 ++++---- 39 files changed, 302 insertions(+), 390 deletions(-) delete mode 100644 frontend/__tests__/routes/_oh.app.test.tsx create mode 100644 frontend/__tests__/utils/convert-raw-providers-to-list.test.ts delete mode 100644 frontend/src/context/auth-context.tsx delete mode 100644 frontend/src/hooks/use-app-logout.ts create mode 100644 frontend/src/hooks/use-user-providers.ts create mode 100644 frontend/src/utils/convert-raw-providers-to-list.ts diff --git a/frontend/__tests__/components/chat/action-suggestions.test.tsx b/frontend/__tests__/components/chat/action-suggestions.test.tsx index 455588b56a..1a5d885667 100644 --- a/frontend/__tests__/components/chat/action-suggestions.test.tsx +++ b/frontend/__tests__/components/chat/action-suggestions.test.tsx @@ -1,8 +1,9 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ActionSuggestions } from "#/components/features/chat/action-suggestions"; -import { useAuth } from "#/context/auth-context"; -import { useSelector } from "react-redux"; +import OpenHands from "#/api/open-hands"; +import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; // Mock dependencies vi.mock("posthog-js", () => ({ @@ -11,8 +12,12 @@ vi.mock("posthog-js", () => ({ }, })); +const { useSelectorMock } = vi.hoisted(() => ({ + useSelectorMock: vi.fn(), +})); + vi.mock("react-redux", () => ({ - useSelector: vi.fn(), + useSelector: useSelectorMock, })); vi.mock("#/context/auth-context", () => ({ @@ -24,34 +29,46 @@ vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { - "ACTION$PUSH_TO_BRANCH": "Push to Branch", - "ACTION$PUSH_CREATE_PR": "Push & Create PR", - "ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR" + ACTION$PUSH_TO_BRANCH: "Push to Branch", + ACTION$PUSH_CREATE_PR: "Push & Create PR", + ACTION$PUSH_CHANGES_TO_PR: "Push Changes to PR", }; return translations[key] || key; }, }), })); +const renderActionSuggestions = () => + render( {}} />, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + describe("ActionSuggestions", () => { // Setup mocks for each test beforeEach(() => { vi.clearAllMocks(); - - (useAuth as any).mockReturnValue({ - providersAreSet: true, + const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + provider_tokens_set: { + github: "some-token", + }, }); - (useSelector as any).mockReturnValue({ + useSelectorMock.mockReturnValue({ selectedRepository: "test-repo", }); }); - it("should render both GitHub buttons when GitHub token is set and repository is selected", () => { - render( {}} />); + it("should render both GitHub buttons when GitHub token is set and repository is selected", async () => { + renderActionSuggestions(); // Find all buttons with data-testid="suggestion" - const buttons = screen.getAllByTestId("suggestion"); + const buttons = await screen.findAllByTestId("suggestion"); // Check if we have at least 2 buttons expect(buttons.length).toBeGreaterThanOrEqual(2); @@ -69,30 +86,24 @@ describe("ActionSuggestions", () => { }); it("should not render buttons when GitHub token is not set", () => { - (useAuth as any).mockReturnValue({ - providersAreSet: false, - }); - - render( {}} />); + renderActionSuggestions(); expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument(); }); it("should not render buttons when no repository is selected", () => { - (useSelector as any).mockReturnValue({ + useSelectorMock.mockReturnValue({ selectedRepository: null, }); - render( {}} />); + renderActionSuggestions(); expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument(); }); it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => { // This test verifies that the prompts are different in the component - const component = render( - {}} />, - ); + renderActionSuggestions(); // Get the component instance to access the internal values const pushBranchPrompt = diff --git a/frontend/__tests__/components/features/analytics/analytics-consent-form-modal.test.tsx b/frontend/__tests__/components/features/analytics/analytics-consent-form-modal.test.tsx index d5e1a16370..7400babc53 100644 --- a/frontend/__tests__/components/features/analytics/analytics-consent-form-modal.test.tsx +++ b/frontend/__tests__/components/features/analytics/analytics-consent-form-modal.test.tsx @@ -4,7 +4,6 @@ import { render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal"; import OpenHands from "#/api/open-hands"; -import { AuthProvider } from "#/context/auth-context"; describe("AnalyticsConsentFormModal", () => { it("should call saveUserSettings with consent", async () => { @@ -14,11 +13,9 @@ describe("AnalyticsConsentFormModal", () => { render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); diff --git a/frontend/__tests__/components/features/auth-modal.test.tsx b/frontend/__tests__/components/features/auth-modal.test.tsx index 1803e644ca..15f2e52655 100644 --- a/frontend/__tests__/components/features/auth-modal.test.tsx +++ b/frontend/__tests__/components/features/auth-modal.test.tsx @@ -2,22 +2,15 @@ import { render, screen } from "@testing-library/react"; import { it, describe, expect, vi, beforeEach, afterEach } from "vitest"; import userEvent from "@testing-library/user-event"; import { AuthModal } from "#/components/features/waitlist/auth-modal"; -import * as AuthHook from "#/context/auth-context"; // Mock the useAuthUrl hook vi.mock("#/hooks/use-auth-url", () => ({ - useAuthUrl: () => "https://gitlab.com/oauth/authorize" + useAuthUrl: () => "https://gitlab.com/oauth/authorize", })); describe("AuthModal", () => { beforeEach(() => { vi.stubGlobal("location", { href: "" }); - vi.spyOn(AuthHook, "useAuth").mockReturnValue({ - providersAreSet: false, - setProvidersAreSet: vi.fn(), - providerTokensSet: [], - setProviderTokensSet: vi.fn() - }); }); afterEach(() => { @@ -28,8 +21,12 @@ describe("AuthModal", () => { it("should render the GitHub and GitLab buttons", () => { render(); - const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" }); - const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" }); + const githubButton = screen.getByRole("button", { + name: "GITHUB$CONNECT_TO_GITHUB", + }); + const gitlabButton = screen.getByRole("button", { + name: "GITLAB$CONNECT_TO_GITLAB", + }); expect(githubButton).toBeInTheDocument(); expect(gitlabButton).toBeInTheDocument(); @@ -40,7 +37,9 @@ describe("AuthModal", () => { const mockUrl = "https://github.com/login/oauth/authorize"; render(); - const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" }); + const githubButton = screen.getByRole("button", { + name: "GITHUB$CONNECT_TO_GITHUB", + }); await user.click(githubButton); expect(window.location.href).toBe(mockUrl); diff --git a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx index ce8ad1c8a7..9d19ff1107 100644 --- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx +++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx @@ -1,19 +1,13 @@ import { screen, waitFor, within } from "@testing-library/react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { - QueryClientProvider, - QueryClient, - QueryClientConfig, -} from "@tanstack/react-query"; +import { QueryClientConfig } from "@tanstack/react-query"; import userEvent from "@testing-library/user-event"; import { createRoutesStub } from "react-router"; import React from "react"; +import { renderWithProviders } from "test-utils"; import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel"; import OpenHands from "#/api/open-hands"; -import { AuthProvider } from "#/context/auth-context"; import { clickOnEditButton } from "./utils"; -import { queryClientConfig } from "#/query-client-config"; -import { renderWithProviders } from "test-utils"; describe("ConversationPanel", () => { const onCloseMock = vi.fn(); @@ -29,9 +23,9 @@ describe("ConversationPanel", () => { preloadedState: { metrics: { cost: null, - usage: null - } - } + usage: null, + }, + }, }); beforeAll(() => { @@ -75,7 +69,9 @@ describe("ConversationPanel", () => { vi.clearAllMocks(); vi.restoreAllMocks(); // Setup default mock for getUserConversations - vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([...mockConversations]); + vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([ + ...mockConversations, + ]); }); it("should render the conversations", async () => { @@ -129,7 +125,9 @@ describe("ConversationPanel", () => { const cancelButton = screen.getByRole("button", { name: /cancel/i }); await user.click(cancelButton); - expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /cancel/i }), + ).not.toBeInTheDocument(); // Ensure the conversation is not deleted cards = await screen.findAllByTestId("conversation-card"); @@ -168,9 +166,12 @@ describe("ConversationPanel", () => { const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); getUserConversationsSpy.mockImplementation(async () => mockData); - const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation"); + const deleteUserConversationSpy = vi.spyOn( + OpenHands, + "deleteUserConversation", + ); deleteUserConversationSpy.mockImplementation(async (id: string) => { - const index = mockData.findIndex(conv => conv.conversation_id === id); + const index = mockData.findIndex((conv) => conv.conversation_id === id); if (index !== -1) { mockData.splice(index, 1); } @@ -178,7 +179,7 @@ describe("ConversationPanel", () => { renderConversationPanel(); - let cards = await screen.findAllByTestId("conversation-card"); + const cards = await screen.findAllByTestId("conversation-card"); expect(cards).toHaveLength(3); const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button"); @@ -192,7 +193,9 @@ describe("ConversationPanel", () => { const confirmButton = screen.getByRole("button", { name: /confirm/i }); await user.click(confirmButton); - expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /confirm/i }), + ).not.toBeInTheDocument(); // Wait for the cards to update await waitFor(() => { @@ -298,9 +301,9 @@ describe("ConversationPanel", () => { preloadedState: { metrics: { cost: null, - usage: null - } - } + usage: null, + }, + }, }); const toggleButton = screen.getByText("Toggle"); diff --git a/frontend/__tests__/components/features/home/home-header.test.tsx b/frontend/__tests__/components/features/home/home-header.test.tsx index d9d9e4d67b..fc0201ce13 100644 --- a/frontend/__tests__/components/features/home/home-header.test.tsx +++ b/frontend/__tests__/components/features/home/home-header.test.tsx @@ -5,7 +5,6 @@ import { createRoutesStub } from "react-router"; import { setupStore } from "test-utils"; import { describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; -import { AuthProvider } from "#/context/auth-context"; import { HomeHeader } from "#/components/features/home/home-header"; import OpenHands from "#/api/open-hands"; @@ -24,11 +23,9 @@ const renderHomeHeader = () => { return render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); diff --git a/frontend/__tests__/components/features/home/repo-connector.test.tsx b/frontend/__tests__/components/features/home/repo-connector.test.tsx index d4429f0a70..7a076c912e 100644 --- a/frontend/__tests__/components/features/home/repo-connector.test.tsx +++ b/frontend/__tests__/components/features/home/repo-connector.test.tsx @@ -1,16 +1,16 @@ import { render, screen, waitFor, within } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { setupStore } from "test-utils"; import { Provider } from "react-redux"; import { createRoutesStub, Outlet } from "react-router"; import OpenHands from "#/api/open-hands"; -import { AuthProvider } from "#/context/auth-context"; import { GitRepository } from "#/types/git"; import { RepoConnector } from "#/components/features/home/repo-connector"; +import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; -const renderRepoConnector = (initialProvidersAreSet = true) => { +const renderRepoConnector = () => { const mockRepoSelection = vi.fn(); const RouterStub = createRoutesStub([ { @@ -40,11 +40,9 @@ const renderRepoConnector = (initialProvidersAreSet = true) => { return render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); @@ -65,6 +63,17 @@ const MOCK_RESPOSITORIES: GitRepository[] = [ }, ]; +beforeEach(() => { + const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + provider_tokens_set: { + github: "some-token", + gitlab: null, + }, + }); +}); + describe("RepoConnector", () => { it("should render the repository connector section", () => { renderRepoConnector(); @@ -99,7 +108,7 @@ describe("RepoConnector", () => { renderRepoConnector(); - const launchButton = screen.getByTestId("repo-launch-button"); + const launchButton = await screen.findByTestId("repo-launch-button"); expect(launchButton).toBeDisabled(); // Wait for the loading state to be replaced with the dropdown @@ -147,7 +156,7 @@ describe("RepoConnector", () => { const repoConnector = screen.getByTestId("repo-connector"); const launchButton = - within(repoConnector).getByTestId("repo-launch-button"); + await within(repoConnector).findByTestId("repo-launch-button"); await userEvent.click(launchButton); // repo not selected yet @@ -184,7 +193,7 @@ describe("RepoConnector", () => { renderRepoConnector(); - const launchButton = screen.getByTestId("repo-launch-button"); + const launchButton = await screen.findByTestId("repo-launch-button"); // Wait for the loading state to be replaced with the dropdown const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown")); @@ -197,14 +206,22 @@ describe("RepoConnector", () => { }); it("should not display a button to settings if the user is signed in with their git provider", async () => { - renderRepoConnector(true); - expect( - screen.queryByTestId("navigate-to-settings-button"), - ).not.toBeInTheDocument(); + renderRepoConnector(); + + await waitFor(() => { + expect( + screen.queryByTestId("navigate-to-settings-button"), + ).not.toBeInTheDocument(); + }); }); it("should display a button to settings if the user needs to sign in with their git provider", async () => { - renderRepoConnector(false); + const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + provider_tokens_set: {}, + }); + renderRepoConnector(); const goToSettingsButton = await screen.findByTestId( "navigate-to-settings-button", diff --git a/frontend/__tests__/components/features/home/task-card.test.tsx b/frontend/__tests__/components/features/home/task-card.test.tsx index 19cbc47a19..3aad56ed10 100644 --- a/frontend/__tests__/components/features/home/task-card.test.tsx +++ b/frontend/__tests__/components/features/home/task-card.test.tsx @@ -7,7 +7,6 @@ import { createRoutesStub } from "react-router"; import { setupStore } from "test-utils"; import { SuggestedTask } from "#/components/features/home/tasks/task.types"; import OpenHands from "#/api/open-hands"; -import { AuthProvider } from "#/context/auth-context"; import { TaskCard } from "#/components/features/home/tasks/task-card"; import { GitRepository } from "#/types/git"; @@ -41,11 +40,9 @@ const renderTaskCard = (task = MOCK_TASK_1) => { return render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); diff --git a/frontend/__tests__/components/features/home/task-suggestions.test.tsx b/frontend/__tests__/components/features/home/task-suggestions.test.tsx index 8dfe75d0f3..a8b7735e89 100644 --- a/frontend/__tests__/components/features/home/task-suggestions.test.tsx +++ b/frontend/__tests__/components/features/home/task-suggestions.test.tsx @@ -7,9 +7,8 @@ import { setupStore } from "test-utils"; import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions"; import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api"; import { MOCK_TASKS } from "#/mocks/task-suggestions-handlers"; -import { AuthProvider } from "#/context/auth-context"; -const renderTaskSuggestions = (initialProvidersAreSet = true) => { +const renderTaskSuggestions = () => { const RouterStub = createRoutesStub([ { Component: TaskSuggestions, @@ -28,11 +27,9 @@ const renderTaskSuggestions = (initialProvidersAreSet = true) => { return render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); @@ -85,7 +82,7 @@ describe("TaskSuggestions", () => { getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS); renderTaskSuggestions(); - const skeletons = screen.getAllByTestId("task-group-skeleton"); + const skeletons = await screen.findAllByTestId("task-group-skeleton"); expect(skeletons.length).toBeGreaterThan(0); await waitFor(() => { diff --git a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx index 0039a1819b..9229cb3828 100644 --- a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx +++ b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils"; import { createRoutesStub } from "react-router"; +import { waitFor } from "@testing-library/react"; import { Sidebar } from "#/components/features/sidebar/sidebar"; import OpenHands from "#/api/open-hands"; @@ -24,8 +25,8 @@ describe("Sidebar", () => { vi.clearAllMocks(); }); - it("should fetch settings data on mount", () => { + it("should fetch settings data on mount", async () => { renderSidebar(); - expect(getSettingsSpy).toHaveBeenCalled(); + await waitFor(() => expect(getSettingsSpy).toHaveBeenCalled()); }); }); diff --git a/frontend/__tests__/context/ws-client-provider.test.tsx b/frontend/__tests__/context/ws-client-provider.test.tsx index 3d41a2eab3..777cbd1225 100644 --- a/frontend/__tests__/context/ws-client-provider.test.tsx +++ b/frontend/__tests__/context/ws-client-provider.test.tsx @@ -8,7 +8,6 @@ import { WsClientProvider, useWsClient, } from "#/context/ws-client-provider"; -import { AuthProvider } from "#/context/auth-context"; describe("Propagate error message", () => { it("should do nothing when no message was passed from server", () => { @@ -91,11 +90,9 @@ describe("WsClientProvider", () => { const { getByText } = render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); diff --git a/frontend/__tests__/hooks/mutation/use-save-settings.test.tsx b/frontend/__tests__/hooks/mutation/use-save-settings.test.tsx index d1a6a9d71b..37c8695096 100644 --- a/frontend/__tests__/hooks/mutation/use-save-settings.test.tsx +++ b/frontend/__tests__/hooks/mutation/use-save-settings.test.tsx @@ -3,18 +3,15 @@ import { describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import OpenHands from "#/api/open-hands"; import { useSaveSettings } from "#/hooks/mutation/use-save-settings"; -import { AuthProvider } from "#/context/auth-context"; describe("useSaveSettings", () => { it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => { const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings"); const { result } = renderHook(() => useSaveSettings(), { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); diff --git a/frontend/__tests__/routes/_oh.app.test.tsx b/frontend/__tests__/routes/_oh.app.test.tsx deleted file mode 100644 index 1b35e0430f..0000000000 --- a/frontend/__tests__/routes/_oh.app.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createRoutesStub } from "react-router"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { renderWithProviders } from "test-utils"; -import { screen, waitFor } from "@testing-library/react"; -import App from "#/routes/conversation"; -import OpenHands from "#/api/open-hands"; -import * as CustomToast from "#/utils/custom-toast-handlers"; - -describe("App", () => { - const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast"); - - const RouteStub = createRoutesStub([ - { Component: App, path: "/conversation/:conversationId" }, - ]); - - beforeAll(() => { - vi.mock("#/hooks/use-terminal", () => ({ - useTerminal: vi.fn(), - })); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it("should render", async () => { - renderWithProviders(); - await screen.findByTestId("app-route"); - }); -}); diff --git a/frontend/__tests__/routes/app-settings.test.tsx b/frontend/__tests__/routes/app-settings.test.tsx index bee0075a3a..4927846c3c 100644 --- a/frontend/__tests__/routes/app-settings.test.tsx +++ b/frontend/__tests__/routes/app-settings.test.tsx @@ -5,7 +5,6 @@ import userEvent from "@testing-library/user-event"; import AppSettingsScreen from "#/routes/app-settings"; import OpenHands from "#/api/open-hands"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; -import { AuthProvider } from "#/context/auth-context"; import { AvailableLanguages } from "#/i18n"; import * as CaptureConsent from "#/utils/handle-capture-consent"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; @@ -14,7 +13,7 @@ const renderAppSettingsScreen = () => render(, { wrapper: ({ children }) => ( - {children} + {children} ), }); diff --git a/frontend/__tests__/routes/git-settings.test.tsx b/frontend/__tests__/routes/git-settings.test.tsx index 68f00a5ac7..efc680596a 100644 --- a/frontend/__tests__/routes/git-settings.test.tsx +++ b/frontend/__tests__/routes/git-settings.test.tsx @@ -6,7 +6,6 @@ import userEvent from "@testing-library/user-event"; import GitSettingsScreen from "#/routes/git-settings"; import OpenHands from "#/api/open-hands"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; -import { AuthProvider } from "#/context/auth-context"; import { GetConfigResponse } from "#/api/open-hands.types"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; import { SecretsService } from "#/api/secrets-service"; @@ -46,7 +45,7 @@ const renderGitSettingsScreen = () => { { wrapper: ({ children }) => ( - {children} + {children} ), }, @@ -55,9 +54,7 @@ const renderGitSettingsScreen = () => { const rerenderGitSettingsScreen = () => rerender( - - - + , ); @@ -141,8 +138,8 @@ describe("Content", () => { getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: { - github: null, - gitlab: null, + github: "some-token", + gitlab: "some-token", }, }); queryClient.invalidateQueries(); @@ -166,7 +163,7 @@ describe("Content", () => { getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: { - gitlab: null, + gitlab: "some-token", }, }); queryClient.invalidateQueries(); @@ -293,6 +290,7 @@ describe("Form submission", () => { ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: { github: null, + gitlab: "some-token", }, }); @@ -323,6 +321,7 @@ describe("Form submission", () => { ...MOCK_DEFAULT_USER_SETTINGS, provider_tokens_set: { github: null, + gitlab: "some-token", }, }); diff --git a/frontend/__tests__/routes/home-screen.test.tsx b/frontend/__tests__/routes/home-screen.test.tsx index 412de4798a..5996c2a657 100644 --- a/frontend/__tests__/routes/home-screen.test.tsx +++ b/frontend/__tests__/routes/home-screen.test.tsx @@ -6,10 +6,10 @@ import { createRoutesStub } from "react-router"; import { Provider } from "react-redux"; import { createAxiosNotFoundErrorObject, setupStore } from "test-utils"; import HomeScreen from "#/routes/home"; -import { AuthProvider } from "#/context/auth-context"; import { GitRepository } from "#/types/git"; import OpenHands from "#/api/open-hands"; import MainApp from "#/routes/root-layout"; +import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; const RouterStub = createRoutesStub([ { @@ -32,15 +32,13 @@ const RouterStub = createRoutesStub([ }, ]); -const renderHomeScreen = (initialProvidersAreSet = true) => +const renderHomeScreen = () => render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); @@ -61,6 +59,17 @@ const MOCK_RESPOSITORIES: GitRepository[] = [ ]; describe("HomeScreen", () => { + beforeEach(() => { + const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + provider_tokens_set: { + github: "some-token", + gitlab: null, + }, + }); + }); + it("should render", () => { renderHomeScreen(); screen.getByTestId("home-screen"); @@ -69,8 +78,10 @@ describe("HomeScreen", () => { it("should render the repository connector and suggested tasks sections", async () => { renderHomeScreen(); - screen.getByTestId("repo-connector"); - screen.getByTestId("task-suggestions"); + await waitFor(() => { + screen.getByTestId("repo-connector"); + screen.getByTestId("task-suggestions"); + }); }); it("should have responsive layout for mobile and desktop screens", async () => { @@ -91,7 +102,7 @@ describe("HomeScreen", () => { renderHomeScreen(); - const taskSuggestions = screen.getByTestId("task-suggestions"); + const taskSuggestions = await screen.findByTestId("task-suggestions"); // Initially, all tasks should be visible await waitFor(() => { @@ -126,7 +137,7 @@ describe("HomeScreen", () => { renderHomeScreen(); - const taskSuggestions = screen.getByTestId("task-suggestions"); + const taskSuggestions = await screen.findByTestId("task-suggestions"); // Initially, all tasks should be visible await waitFor(() => { @@ -164,7 +175,7 @@ describe("HomeScreen", () => { describe("launch buttons", () => { const setupLaunchButtons = async () => { let headerLaunchButton = screen.getByTestId("header-launch-button"); - let repoLaunchButton = screen.getByTestId("repo-launch-button"); + let repoLaunchButton = await screen.findByTestId("repo-launch-button"); let tasksLaunchButtons = await screen.findAllByTestId("task-launch-button"); @@ -256,7 +267,7 @@ describe("HomeScreen", () => { }); it("should hide the suggested tasks section if not authed with git(hub|lab)", async () => { - renderHomeScreen(false); + renderHomeScreen(); const taskSuggestions = screen.queryByTestId("task-suggestions"); const repoConnector = screen.getByTestId("repo-connector"); @@ -267,6 +278,10 @@ describe("HomeScreen", () => { }); describe("Settings 404", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index f294f9a552..29f1c3a21f 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -8,7 +8,6 @@ import { MOCK_DEFAULT_USER_SETTINGS, resetTestHandlersMockSettings, } from "#/mocks/handlers"; -import { AuthProvider } from "#/context/auth-context"; import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; @@ -16,7 +15,7 @@ const renderLlmSettingsScreen = () => render(, { wrapper: ({ children }) => ( - {children} + {children} ), }); diff --git a/frontend/__tests__/routes/settings.test.tsx b/frontend/__tests__/routes/settings.test.tsx index 566f17f96a..23f14ebdec 100644 --- a/frontend/__tests__/routes/settings.test.tsx +++ b/frontend/__tests__/routes/settings.test.tsx @@ -2,24 +2,24 @@ import { render, screen, within } from "@testing-library/react"; import { createRoutesStub } from "react-router"; import { describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { AuthProvider } from "#/context/auth-context"; import SettingsScreen from "#/routes/settings"; import OpenHands from "#/api/open-hands"; // Mock the i18next hook vi.mock("react-i18next", async () => { - const actual = await vi.importActual("react-i18next"); + const actual = + await vi.importActual("react-i18next"); return { ...actual, useTranslation: () => ({ t: (key: string) => { const translations: Record = { - "SETTINGS$NAV_GIT": "Git", - "SETTINGS$NAV_APPLICATION": "Application", - "SETTINGS$NAV_CREDITS": "Credits", - "SETTINGS$NAV_API_KEYS": "API Keys", - "SETTINGS$NAV_LLM": "LLM", - "SETTINGS$TITLE": "Settings" + SETTINGS$NAV_GIT: "Git", + SETTINGS$NAV_APPLICATION: "Application", + SETTINGS$NAV_CREDITS: "Credits", + SETTINGS$NAV_API_KEYS: "API Keys", + SETTINGS$NAV_LLM: "LLM", + SETTINGS$TITLE: "Settings", }; return translations[key] || key; }, @@ -71,11 +71,9 @@ describe("Settings Screen", () => { const queryClient = new QueryClient(); return render(, { wrapper: ({ children }) => ( - - - {children} - - + + {children} + ), }); }; diff --git a/frontend/__tests__/utils/convert-raw-providers-to-list.test.ts b/frontend/__tests__/utils/convert-raw-providers-to-list.test.ts new file mode 100644 index 0000000000..d2a756cbe7 --- /dev/null +++ b/frontend/__tests__/utils/convert-raw-providers-to-list.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { Provider } from "#/types/settings"; +import { convertRawProvidersToList } from "#/utils/convert-raw-providers-to-list"; + +describe("convertRawProvidersToList", () => { + it("should convert raw provider tokens to a list of providers", () => { + const example1: Partial> | undefined = { + github: "test-token", + gitlab: "test-token", + }; + const example2: Partial> | undefined = { + github: "", + }; + const example3: Partial> | undefined = { + gitlab: null, + }; + + expect(convertRawProvidersToList(example1)).toEqual(["github", "gitlab"]); + expect(convertRawProvidersToList(example2)).toEqual(["github"]); + expect(convertRawProvidersToList(example3)).toEqual(["gitlab"]); + }); +}); diff --git a/frontend/src/components/features/chat/action-suggestions.tsx b/frontend/src/components/features/chat/action-suggestions.tsx index ef8d4d7817..1e36e26ab4 100644 --- a/frontend/src/components/features/chat/action-suggestions.tsx +++ b/frontend/src/components/features/chat/action-suggestions.tsx @@ -4,8 +4,8 @@ import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import { SuggestionItem } from "#/components/features/suggestions/suggestion-item"; import type { RootState } from "#/store"; -import { useAuth } from "#/context/auth-context"; import { I18nKey } from "#/i18n/declaration"; +import { useUserProviders } from "#/hooks/use-user-providers"; interface ActionSuggestionsProps { onSuggestionsClick: (value: string) => void; @@ -15,13 +15,14 @@ export function ActionSuggestions({ onSuggestionsClick, }: ActionSuggestionsProps) { const { t } = useTranslation(); - const { providersAreSet } = useAuth(); + const { providers } = useUserProviders(); const { selectedRepository } = useSelector( (state: RootState) => state.initialQuery, ); const [hasPullRequest, setHasPullRequest] = React.useState(false); + const providersAreSet = providers.length > 0; const isGitLab = selectedRepository !== null && selectedRepository.git_provider && diff --git a/frontend/src/components/features/home/repo-connector.tsx b/frontend/src/components/features/home/repo-connector.tsx index 6dc91a50d4..92426e6eea 100644 --- a/frontend/src/components/features/home/repo-connector.tsx +++ b/frontend/src/components/features/home/repo-connector.tsx @@ -1,20 +1,21 @@ import { useTranslation } from "react-i18next"; import { ConnectToProviderMessage } from "./connect-to-provider-message"; -import { useAuth } from "#/context/auth-context"; import { RepositorySelectionForm } from "./repo-selection-form"; import { useConfig } from "#/hooks/query/use-config"; import { RepoProviderLinks } from "./repo-provider-links"; +import { useUserProviders } from "#/hooks/use-user-providers"; interface RepoConnectorProps { onRepoSelection: (repoTitle: string | null) => void; } export function RepoConnector({ onRepoSelection }: RepoConnectorProps) { - const { providersAreSet } = useAuth(); + const { providers } = useUserProviders(); const { data: config } = useConfig(); const { t } = useTranslation(); const isSaaS = config?.APP_MODE === "saas"; + const providersAreSet = providers.length > 0; return (
{ - await logout(); - posthog.reset(); - }; - return ( <>