Skip to content

Commit 6dbfe06

Browse files
authored
chore: add navigation test for workspace details page (#14629)
* chore: add tests for WorkspacePage cross-page navigation * fix: update story to use mock organizations menu
1 parent 5db1400 commit 6dbfe06

File tree

5 files changed

+116
-12
lines changed

5 files changed

+116
-12
lines changed

site/src/contexts/auth/RequireAuth.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import { API } from "api/api";
22
import { isApiError } from "api/errors";
33
import { Loader } from "components/Loader/Loader";
4-
import { ProxyProvider } from "contexts/ProxyContext";
5-
import { DashboardProvider } from "modules/dashboard/DashboardProvider";
4+
import { ProxyProvider as ProductionProxyProvider } from "contexts/ProxyContext";
5+
import { DashboardProvider as ProductionDashboardProvider } from "modules/dashboard/DashboardProvider";
66
import { type FC, useEffect } from "react";
77
import { Navigate, Outlet, useLocation } from "react-router-dom";
88
import { embedRedirect } from "utils/redirect";
99
import { type AuthContextValue, useAuthContext } from "./AuthProvider";
1010

11-
export const RequireAuth: FC = () => {
11+
type RequireAuthProps = Readonly<{
12+
ProxyProvider?: typeof ProductionProxyProvider;
13+
DashboardProvider?: typeof ProductionDashboardProvider;
14+
}>;
15+
16+
/**
17+
* Wraps any component and ensures that the user has been authenticated before
18+
* they can access the component's contents.
19+
*
20+
* In production, it is assumed that this component will not be called with any
21+
* props at all. But to make testing easier, you can call this component with
22+
* specific providers to mock them out.
23+
*/
24+
export const RequireAuth: FC<RequireAuthProps> = ({
25+
DashboardProvider = ProductionDashboardProvider,
26+
ProxyProvider = ProductionProxyProvider,
27+
}) => {
1228
const location = useLocation();
1329
const { signOut, isSigningOut, isSignedOut, isSignedIn, isLoading } =
1430
useAuthContext();

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ import userEvent from "@testing-library/user-event";
33
import * as apiModule from "api/api";
44
import type { TemplateVersionParameter, Workspace } from "api/typesGenerated";
55
import EventSourceMock from "eventsourcemock";
6+
import {
7+
DashboardContext,
8+
type DashboardProvider,
9+
} from "modules/dashboard/DashboardProvider";
610
import { http, HttpResponse } from "msw";
11+
import type { FC } from "react";
12+
import { type Location, useLocation } from "react-router-dom";
713
import {
14+
MockAppearanceConfig,
815
MockDeploymentConfig,
16+
MockEntitlements,
917
MockFailedWorkspace,
18+
MockOrganization,
1019
MockOutdatedWorkspace,
1120
MockStartingWorkspace,
1221
MockStoppedWorkspace,
@@ -18,14 +27,22 @@ import {
1827
MockWorkspaceBuild,
1928
MockWorkspaceBuildDelete,
2029
} from "testHelpers/entities";
21-
import { renderWithAuth } from "testHelpers/renderHelpers";
30+
import {
31+
type RenderWithAuthOptions,
32+
renderWithAuth,
33+
} from "testHelpers/renderHelpers";
2234
import { server } from "testHelpers/server";
2335
import { WorkspacePage } from "./WorkspacePage";
2436

2537
const { API, MissingBuildParameters } = apiModule;
2638

39+
type RenderWorkspacePageOptions = Omit<RenderWithAuthOptions, "route" | "path">;
40+
2741
// Renders the workspace page and waits for it be loaded
28-
const renderWorkspacePage = async (workspace: Workspace) => {
42+
const renderWorkspacePage = async (
43+
workspace: Workspace,
44+
options: RenderWorkspacePageOptions = {},
45+
) => {
2946
jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace);
3047
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
3148
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([]);
@@ -40,6 +57,7 @@ const renderWorkspacePage = async (workspace: Workspace) => {
4057
});
4158

4259
renderWithAuth(<WorkspacePage />, {
60+
...options,
4361
route: `/@${workspace.owner_name}/${workspace.name}`,
4462
path: "/:username/:workspace",
4563
});
@@ -527,4 +545,69 @@ describe("WorkspacePage", () => {
527545
);
528546
});
529547
});
548+
549+
describe("Navigation to other pages", () => {
550+
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 () => {
551+
jest.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({
552+
budget: 25,
553+
credits_consumed: 2,
554+
});
555+
556+
const MockDashboardProvider: typeof DashboardProvider = ({
557+
children,
558+
}) => (
559+
<DashboardContext.Provider
560+
value={{
561+
appearance: MockAppearanceConfig,
562+
entitlements: MockEntitlements,
563+
experiments: [],
564+
organizations: [MockOrganization],
565+
showOrganizations: true,
566+
}}
567+
>
568+
{children}
569+
</DashboardContext.Provider>
570+
);
571+
572+
let destinationLocation!: Location;
573+
const MockWorkspacesPage: FC = () => {
574+
destinationLocation = useLocation();
575+
return null;
576+
};
577+
578+
const workspace: Workspace = {
579+
...MockWorkspace,
580+
organization_name: MockOrganization.name,
581+
};
582+
583+
await renderWorkspacePage(workspace, {
584+
mockAuthProviders: {
585+
DashboardProvider: MockDashboardProvider,
586+
},
587+
extraRoutes: [
588+
{
589+
path: "/workspaces",
590+
element: <MockWorkspacesPage />,
591+
},
592+
],
593+
});
594+
595+
const quotaLink = await screen.findByRole<HTMLAnchorElement>("link", {
596+
name: /\d+ credits of \d+/i,
597+
});
598+
599+
const orgName = encodeURIComponent(MockOrganization.name);
600+
expect(
601+
quotaLink.href.endsWith(`/workspaces?filter=organization:${orgName}`),
602+
).toBe(true);
603+
604+
const user = userEvent.setup();
605+
await user.click(quotaLink);
606+
607+
expect(destinationLocation.pathname).toBe("/workspaces");
608+
expect(destinationLocation.search).toBe(
609+
`?filter=organization:${orgName}`,
610+
);
611+
});
612+
});
530613
});

site/src/pages/WorkspacePage/WorkspaceTopbar.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
147147
<OrganizationBreadcrumb
148148
orgName={orgDisplayName}
149149
orgIconUrl={activeOrg?.icon}
150-
orgPageUrl={
151-
showOrganizations
152-
? `/organizations/${encodeURIComponent(workspace.organization_name)}`
153-
: undefined
154-
}
150+
orgPageUrl={`/organizations/${encodeURIComponent(workspace.organization_name)}`}
155151
/>
156152
</>
157153
)}

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
9898
user: MockMenu,
9999
template: MockMenu,
100100
status: MockMenu,
101+
organizations: MockMenu,
101102
},
102103
values: {
103104
owner: MockUser.username,

site/src/testHelpers/renderHelpers.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {
44
waitFor,
55
} from "@testing-library/react";
66
import { AppProviders } from "App";
7+
import type { ProxyProvider } from "contexts/ProxyContext";
78
import { ThemeProvider } from "contexts/ThemeProvider";
89
import { RequireAuth } from "contexts/auth/RequireAuth";
910
import { DashboardLayout } from "modules/dashboard/DashboardLayout";
11+
import type { DashboardProvider } from "modules/dashboard/DashboardProvider";
1012
import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout";
1113
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout";
1214
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
@@ -83,6 +85,11 @@ export type RenderWithAuthOptions = {
8385
nonAuthenticatedRoutes?: RouteObject[];
8486
// In case you want to render a layout inside of it
8587
children?: RouteObject["children"];
88+
89+
mockAuthProviders?: Readonly<{
90+
DashboardProvider?: typeof DashboardProvider;
91+
ProxyProvider?: typeof ProxyProvider;
92+
}>;
8693
};
8794

8895
export function renderWithAuth(
@@ -92,12 +99,13 @@ export function renderWithAuth(
9299
route = "/",
93100
extraRoutes = [],
94101
nonAuthenticatedRoutes = [],
102+
mockAuthProviders = {},
95103
children,
96104
}: RenderWithAuthOptions = {},
97105
) {
98106
const routes: RouteObject[] = [
99107
{
100-
element: <RequireAuth />,
108+
element: <RequireAuth {...mockAuthProviders} />,
101109
children: [{ path, element, children }, ...extraRoutes],
102110
},
103111
...nonAuthenticatedRoutes,
@@ -108,8 +116,8 @@ export function renderWithAuth(
108116
);
109117

110118
return {
111-
user: MockUser,
112119
...renderResult,
120+
user: MockUser,
113121
};
114122
}
115123

0 commit comments

Comments
 (0)