Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore(site): clean up mocks after each test #12805

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 11 commits into from
Apr 1, 2024
12 changes: 12 additions & 0 deletions site/jest.polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ Object.defineProperties(globalThis, {
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
matchMedia: {
value: (query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}),
},
});
2 changes: 1 addition & 1 deletion site/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ beforeAll(() =>
afterEach(() => {
cleanup();
server.resetHandlers();
jest.clearAllMocks();
jest.resetAllMocks();
});

// Clean up after the tests are finished.
Expand Down
15 changes: 0 additions & 15 deletions site/src/hooks/useClipboard-http.test.ts

This file was deleted.

15 changes: 0 additions & 15 deletions site/src/hooks/useClipboard-https.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
import { act, renderHook } from "@testing-library/react";
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
import { ThemeProvider } from "contexts/ThemeProvider";
import {
type UseClipboardInput,
type UseClipboardResult,
useClipboard,
} from "./useClipboard";

describe(useClipboard.name, () => {
describe("HTTP (non-secure) connections", () => {
scheduleClipboardTests({ isHttps: false });
});

describe("HTTPS (secure/default) connections", () => {
scheduleClipboardTests({ isHttps: true });
});
});

/**
* @file This is a very weird test setup.
*
Expand Down Expand Up @@ -41,25 +60,6 @@
* order of operations involving closure, but you have no idea why the code
* is working, and it's impossible to debug.
*/
import { act, renderHook } from "@testing-library/react";
import { GlobalSnackbar } from "components/GlobalSnackbar/GlobalSnackbar";
import {
type UseClipboardInput,
type UseClipboardResult,
useClipboard,
} from "./useClipboard";

const initialExecCommand = global.document.execCommand;
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.restoreAllMocks();
jest.useRealTimers();
global.document.execCommand = initialExecCommand;
});

type MockClipboardEscapeHatches = Readonly<{
getMockText: () => string;
setMockText: (newText: string) => void;
Expand Down Expand Up @@ -124,10 +124,10 @@ function renderUseClipboard(inputs: UseClipboardInput) {
{
initialProps: inputs,
wrapper: ({ children }) => (
<>
<>{children}</>
<ThemeProvider>
{children}
<GlobalSnackbar />
</>
</ThemeProvider>
),
},
);
Expand All @@ -137,9 +137,10 @@ type ScheduleConfig = Readonly<{ isHttps: boolean }>;

export function scheduleClipboardTests({ isHttps }: ScheduleConfig) {
const mockClipboardInstance = makeMockClipboard(isHttps);

const originalNavigator = window.navigator;
beforeAll(() => {

beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
...originalNavigator,
clipboard: mockClipboardInstance,
Expand Down Expand Up @@ -170,6 +171,7 @@ export function scheduleClipboardTests({ isHttps }: ScheduleConfig) {
});

afterEach(() => {
jest.useRealTimers();
mockClipboardInstance.setMockText("");
mockClipboardInstance.setSimulateFailure(false);
});
Expand Down
60 changes: 59 additions & 1 deletion site/src/pages/TerminalPage/TerminalAlerts.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,66 @@
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { type FC, useState } from "react";
import { type FC, useState, useEffect, useRef } from "react";
import type { WorkspaceAgent } from "api/typesGenerated";
import { Alert, type AlertProps } from "components/Alert/Alert";
import { docs } from "utils/docs";
import type { ConnectionStatus } from "./types";

type TerminalAlertsProps = {
agent: WorkspaceAgent | undefined;
status: ConnectionStatus;
onAlertChange: () => void;
};

export const TerminalAlerts = ({
agent,
status,
onAlertChange,
}: TerminalAlertsProps) => {
const lifecycleState = agent?.lifecycle_state;
const prevLifecycleState = useRef(lifecycleState);
useEffect(() => {
prevLifecycleState.current = lifecycleState;
}, [lifecycleState]);

// We want to observe the children of the wrapper to detect when the alert
// changes. So the terminal page can resize itself.
//
// Would it be possible to just always call fit() when this component
// re-renders instead of using an observer?
//
// This is a good question and the why this does not work is that the .fit()
// needs to run after the render so in this case, I just think the mutation
// observer is more reliable. I could use some hacky setTimeout inside of
// useEffect to do that, I guess, but I don't think it would be any better.
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!wrapperRef.current) {
return;
}
const observer = new MutationObserver(onAlertChange);
observer.observe(wrapperRef.current, { childList: true });

return () => {
observer.disconnect();
};
}, [onAlertChange]);

return (
<div ref={wrapperRef}>
{status === "disconnected" ? (
<DisconnectedAlert />
) : lifecycleState === "start_error" ? (
<ErrorScriptAlert />
) : lifecycleState === "starting" ? (
<LoadingScriptsAlert />
) : lifecycleState === "ready" &&
prevLifecycleState.current === "starting" ? (
<LoadedScriptsAlert />
) : null}
</div>
);
};

export const ErrorScriptAlert: FC = () => {
return (
Expand Down
60 changes: 18 additions & 42 deletions site/src/pages/TerminalPage/TerminalPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,27 @@ import {
MockWorkspace,
MockWorkspaceAgent,
} from "testHelpers/entities";
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { renderWithAuth } from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import TerminalPage, { Language } from "./TerminalPage";

Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

const renderTerminal = async (
route = `/${MockUser.username}/${MockWorkspace.name}/terminal`,
) => {
const utils = renderWithAuth(<TerminalPage />, {
route,
path: "/:username/:workspace/terminal",
});
await waitForLoaderToBeRemoved();
await waitFor(() => {
// To avoid 'act' errors during testing, we ensure the component is
// completely rendered without any outstanding state updates. This is
// accomplished by incorporating a 'data-status' attribute into the
// component. We then observe this attribute for any changes, as we cannot
// rely on other screen elements to indicate completion.
const wrapper =
utils.container.querySelector<HTMLDivElement>("[data-status]")!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big fan of the updated name

expect(wrapper.dataset.state).not.toBe("initializing");
});
return utils;
};

Expand All @@ -58,11 +50,15 @@ const expectTerminalText = (container: HTMLElement, text: string) => {
};

describe("TerminalPage", () => {
afterEach(() => {
WS.clean();
});

it("loads the right workspace data", async () => {
const spy = jest
jest
.spyOn(API, "getWorkspaceByOwnerAndName")
.mockResolvedValue(MockWorkspace);
const ws = new WS(
new WS(
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
);
await renderTerminal(
Expand All @@ -75,57 +71,45 @@ describe("TerminalPage", () => {
{ include_deleted: true },
);
});
spy.mockRestore();
ws.close();
});

it("shows an error if fetching workspace fails", async () => {
// Given
server.use(
http.get("/api/v2/users/:userId/workspace/:workspaceName", () => {
return HttpResponse.json({ id: "workspace-id" }, { status: 500 });
}),
);

// When
const { container } = await renderTerminal();

// Then
await expectTerminalText(container, Language.workspaceErrorMessagePrefix);
});

it("shows an error if the websocket fails", async () => {
// Given
server.use(
http.get("/api/v2/workspaceagents/:agentId/pty", () => {
return HttpResponse.json({}, { status: 500 });
}),
);

// When
const { container } = await renderTerminal();

// Then
await expectTerminalText(container, Language.websocketErrorMessagePrefix);
});

it("renders data from the backend", async () => {
// Given
const ws = new WS(
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
);
const text = "something to render";

// When
const { container } = await renderTerminal();

// Then
// Ideally we could use ws.connected but that seems to pause React updates.
// For now, wait for the initial resize message instead.
await ws.nextMessage;
ws.send(text);

await expectTerminalText(container, text);
ws.close();
});

// Ideally we could just pass the correct size in the web socket URL without
Expand All @@ -134,40 +118,32 @@ describe("TerminalPage", () => {
// in the other tests since ws.connected appears to pause React updates. So
// for now the initial resize message (and this test) are here to stay.
it("resizes on connect", async () => {
// Given
const ws = new WS(
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
);

// When
await renderTerminal();

// Then
const msg = await ws.nextMessage;
const req = JSON.parse(new TextDecoder().decode(msg as Uint8Array));
expect(req.height).toBeGreaterThan(0);
expect(req.width).toBeGreaterThan(0);
ws.close();
});

it("supports workspace.agent syntax", async () => {
// Given
const ws = new WS(
`ws://localhost/api/v2/workspaceagents/${MockWorkspaceAgent.id}/pty`,
);
const text = "something to render";

// When
const { container } = await renderTerminal(
`/some-user/${MockWorkspace.name}.${MockWorkspaceAgent.name}/terminal`,
);

// Then
// Ideally we could use ws.connected but that seems to pause React updates.
// For now, wait for the initial resize message instead.
await ws.nextMessage;
ws.send(text);
await expectTerminalText(container, text);
ws.close();
});
});
Loading