From 4838ada9228402fc8dbaf650f56b6d614c76342b Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 10 Sep 2024 04:48:28 +0000 Subject: [PATCH 1/2] chore: add tests for WorkspacePage cross-page navigation --- site/src/contexts/auth/RequireAuth.tsx | 22 ++++- .../WorkspacePage/WorkspacePage.test.tsx | 87 ++++++++++++++++++- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 6 +- site/src/testHelpers/renderHelpers.tsx | 12 ++- 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx index 6d66045b9756a..e558b66c802de 100644 --- a/site/src/contexts/auth/RequireAuth.tsx +++ b/site/src/contexts/auth/RequireAuth.tsx @@ -1,14 +1,30 @@ import { API } from "api/api"; import { isApiError } from "api/errors"; import { Loader } from "components/Loader/Loader"; -import { ProxyProvider } from "contexts/ProxyContext"; -import { DashboardProvider } from "modules/dashboard/DashboardProvider"; +import { ProxyProvider as ProductionProxyProvider } from "contexts/ProxyContext"; +import { DashboardProvider as ProductionDashboardProvider } from "modules/dashboard/DashboardProvider"; import { type FC, useEffect } from "react"; import { Navigate, Outlet, useLocation } from "react-router-dom"; import { embedRedirect } from "utils/redirect"; import { type AuthContextValue, useAuthContext } from "./AuthProvider"; -export const RequireAuth: FC = () => { +type RequireAuthProps = Readonly<{ + ProxyProvider?: typeof ProductionProxyProvider; + DashboardProvider?: typeof ProductionDashboardProvider; +}>; + +/** + * Wraps any component and ensures that the user has been authenticated before + * they can access the component's contents. + * + * In production, it is assumed that this component will not be called with any + * props at all. But to make testing easier, you can call this component with + * specific providers to mock them out. + */ +export const RequireAuth: FC = ({ + DashboardProvider = ProductionDashboardProvider, + ProxyProvider = ProductionProxyProvider, +}) => { const location = useLocation(); const { signOut, isSigningOut, isSignedOut, isSignedIn, isLoading } = useAuthContext(); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index c6b69348e1a8d..2c113b933483e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -3,10 +3,19 @@ import userEvent from "@testing-library/user-event"; import * as apiModule from "api/api"; import type { TemplateVersionParameter, Workspace } from "api/typesGenerated"; import EventSourceMock from "eventsourcemock"; +import { + DashboardContext, + type DashboardProvider, +} from "modules/dashboard/DashboardProvider"; import { http, HttpResponse } from "msw"; +import type { FC } from "react"; +import { type Location, useLocation } from "react-router-dom"; import { + MockAppearanceConfig, MockDeploymentConfig, + MockEntitlements, MockFailedWorkspace, + MockOrganization, MockOutdatedWorkspace, MockStartingWorkspace, MockStoppedWorkspace, @@ -18,14 +27,22 @@ import { MockWorkspaceBuild, MockWorkspaceBuildDelete, } from "testHelpers/entities"; -import { renderWithAuth } from "testHelpers/renderHelpers"; +import { + type RenderWithAuthOptions, + renderWithAuth, +} from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; const { API, MissingBuildParameters } = apiModule; +type RenderWorkspacePageOptions = Omit; + // Renders the workspace page and waits for it be loaded -const renderWorkspacePage = async (workspace: Workspace) => { +const renderWorkspacePage = async ( + workspace: Workspace, + options: RenderWorkspacePageOptions = {}, +) => { jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); @@ -40,6 +57,7 @@ const renderWorkspacePage = async (workspace: Workspace) => { }); renderWithAuth(, { + ...options, route: `/@${workspace.owner_name}/${workspace.name}`, path: "/:username/:workspace", }); @@ -527,4 +545,69 @@ describe("WorkspacePage", () => { ); }); }); + + describe("Navigation to other pages", () => { + it("Shows a quota link when quota budget is greater than 0. Link navigates user to /workspaces route with the URL params populated with the corresponding organization", async () => { + jest.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({ + budget: 25, + credits_consumed: 2, + }); + + const MockDashboardProvider: typeof DashboardProvider = ({ + children, + }) => ( + + {children} + + ); + + let destinationLocation!: Location; + const MockWorkspacesPage: FC = () => { + destinationLocation = useLocation(); + return null; + }; + + const workspace: Workspace = { + ...MockWorkspace, + organization_name: MockOrganization.name, + }; + + await renderWorkspacePage(workspace, { + mockAuthProviders: { + DashboardProvider: MockDashboardProvider, + }, + extraRoutes: [ + { + path: "/workspaces", + element: , + }, + ], + }); + + const quotaLink = await screen.findByRole("link", { + name: /\d+ credits of \d+/i, + }); + + const orgName = encodeURIComponent(MockOrganization.name); + expect( + quotaLink.href.endsWith(`/workspaces?filter=organization:${orgName}`), + ).toBe(true); + + const user = userEvent.setup(); + await user.click(quotaLink); + + expect(destinationLocation.pathname).toBe("/workspaces"); + expect(destinationLocation.search).toBe( + `?filter=organization:${orgName}`, + ); + }); + }); }); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 264a5bf7fda04..c4c9e2e7f3e6b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -147,11 +147,7 @@ export const WorkspaceTopbar: FC = ({ )} diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index a6a41fd2a26ee..30aa6b7e89e10 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -4,9 +4,11 @@ import { waitFor, } from "@testing-library/react"; import { AppProviders } from "App"; +import type { ProxyProvider } from "contexts/ProxyContext"; import { ThemeProvider } from "contexts/ThemeProvider"; import { RequireAuth } from "contexts/auth/RequireAuth"; import { DashboardLayout } from "modules/dashboard/DashboardLayout"; +import type { DashboardProvider } from "modules/dashboard/DashboardProvider"; import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout"; import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; @@ -83,6 +85,11 @@ export type RenderWithAuthOptions = { nonAuthenticatedRoutes?: RouteObject[]; // In case you want to render a layout inside of it children?: RouteObject["children"]; + + mockAuthProviders?: Readonly<{ + DashboardProvider?: typeof DashboardProvider; + ProxyProvider?: typeof ProxyProvider; + }>; }; export function renderWithAuth( @@ -92,12 +99,13 @@ export function renderWithAuth( route = "/", extraRoutes = [], nonAuthenticatedRoutes = [], + mockAuthProviders = {}, children, }: RenderWithAuthOptions = {}, ) { const routes: RouteObject[] = [ { - element: , + element: , children: [{ path, element, children }, ...extraRoutes], }, ...nonAuthenticatedRoutes, @@ -108,8 +116,8 @@ export function renderWithAuth( ); return { - user: MockUser, ...renderResult, + user: MockUser, }; } From d6e4e6723d93df3bdf9bc31a9a9d8ec34b6d3d19 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 16 Sep 2024 20:35:19 +0000 Subject: [PATCH 2/2] fix: update story to use mock organizations menu --- site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 02f34d0189691..ef639d087fb5a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -98,6 +98,7 @@ const defaultFilterProps = getDefaultFilterProps({ user: MockMenu, template: MockMenu, status: MockMenu, + organizations: MockMenu, }, values: { owner: MockUser.username,