coder/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.t...

329 lines
9.8 KiB
TypeScript

import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent, { type UserEvent } from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { QueryClient } from "react-query";
import { RouterProvider, createMemoryRouter } from "react-router-dom";
import * as api from "api/api";
import { templateVersionVariablesKey } from "api/queries/templates";
import type { TemplateVersion } from "api/typesGenerated";
import { AppProviders } from "App";
import { RequireAuth } from "contexts/auth/RequireAuth";
import {
MockTemplate,
MockTemplateVersion,
MockTemplateVersionVariable1,
MockTemplateVersionVariable2,
MockWorkspaceBuildLogs,
} from "testHelpers/entities";
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import type { MonacoEditorProps } from "./MonacoEditor";
import { Language } from "./PublishTemplateVersionDialog";
import TemplateVersionEditorPage from "./TemplateVersionEditorPage";
// For some reason this component in Jest is throwing a MUI style warning so,
// since we don't need it for this test, we can mock it out
jest.mock(
"modules/templates/TemplateResourcesTable/TemplateResourcesTable",
() => ({
TemplateResourcesTable: () => <div></div>,
}),
);
// Occasionally, Jest encounters HTML5 canvas errors. As the MonacoEditor is not
// required for these tests, we can safely mock it.
jest.mock("pages/TemplateVersionEditorPage/MonacoEditor", () => ({
MonacoEditor: (props: MonacoEditorProps) => (
<textarea
data-testid="monaco-editor"
value={props.value}
onChange={(e) => {
props.onChange?.(e.target.value);
}}
/>
),
}));
const renderTemplateEditorPage = () => {
renderWithAuth(<TemplateVersionEditorPage />, {
route: `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`,
path: "/templates/:template/versions/:version/edit",
extraRoutes: [
{
path: "/templates/:templateId",
element: <div></div>,
},
],
});
};
const typeOnEditor = async (value: string, user: UserEvent) => {
const editor = await screen.findByTestId("monaco-editor");
await user.type(editor, value);
};
const buildTemplateVersion = async (
templateVersion: TemplateVersion,
user: UserEvent,
topbar: HTMLElement,
) => {
jest.spyOn(api, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
jest.spyOn(api, "createTemplateVersion").mockResolvedValue({
...templateVersion,
job: {
...templateVersion.job,
status: "running",
},
});
jest
.spyOn(api, "getTemplateVersionByName")
.mockResolvedValue(templateVersion);
jest
.spyOn(api, "watchBuildLogsByTemplateVersionId")
.mockImplementation((_, options) => {
options.onMessage(MockWorkspaceBuildLogs[0]);
options.onDone?.();
const wsMock = {
close: jest.fn(),
} as unknown;
return wsMock as WebSocket;
});
const buildButton = within(topbar).getByRole("button", {
name: "Build",
});
await user.click(buildButton);
await within(topbar).findByText("Success");
};
test("Use custom name, message and set it as active when publishing", async () => {
const user = userEvent.setup();
renderTemplateEditorPage();
const topbar = await screen.findByTestId("topbar");
const newTemplateVersion: TemplateVersion = {
...MockTemplateVersion,
id: "new-version-id",
name: "new-version",
};
await typeOnEditor("new content", user);
await buildTemplateVersion(newTemplateVersion, user, topbar);
// Publish
const patchTemplateVersion = jest
.spyOn(api, "patchTemplateVersion")
.mockResolvedValue(newTemplateVersion);
const updateActiveTemplateVersion = jest
.spyOn(api, "updateActiveTemplateVersion")
.mockResolvedValue({ message: "" });
const publishButton = within(topbar).getByRole("button", {
name: "Publish",
});
await user.click(publishButton);
const publishDialog = await screen.findByTestId("dialog");
const nameField = within(publishDialog).getByLabelText("Version name");
await user.clear(nameField);
await user.type(nameField, "v1.0");
const messageField = within(publishDialog).getByLabelText("Message");
await user.clear(messageField);
await user.type(messageField, "Informative message");
await user.click(
within(publishDialog).getByRole("button", { name: "Publish" }),
);
await waitFor(() => {
expect(patchTemplateVersion).toBeCalledWith("new-version-id", {
name: "v1.0",
message: "Informative message",
});
});
expect(updateActiveTemplateVersion).toBeCalledWith("test-template", {
id: "new-version-id",
});
});
test("Do not mark as active if promote is not checked", async () => {
const user = userEvent.setup();
renderTemplateEditorPage();
const topbar = await screen.findByTestId("topbar");
const newTemplateVersion = {
...MockTemplateVersion,
id: "new-version-id",
name: "new-version",
};
await typeOnEditor("new content", user);
await buildTemplateVersion(newTemplateVersion, user, topbar);
// Publish
const patchTemplateVersion = jest
.spyOn(api, "patchTemplateVersion")
.mockResolvedValue(newTemplateVersion);
const updateActiveTemplateVersion = jest
.spyOn(api, "updateActiveTemplateVersion")
.mockResolvedValue({ message: "" });
const publishButton = within(topbar).getByRole("button", {
name: "Publish",
});
await user.click(publishButton);
const publishDialog = await screen.findByTestId("dialog");
const nameField = within(publishDialog).getByLabelText("Version name");
await user.clear(nameField);
await user.type(nameField, "v1.0");
await user.click(
within(publishDialog).getByLabelText(Language.defaultCheckboxLabel),
);
await user.click(
within(publishDialog).getByRole("button", { name: "Publish" }),
);
await waitFor(() => {
expect(patchTemplateVersion).toBeCalledWith("new-version-id", {
name: "v1.0",
message: "",
});
});
expect(updateActiveTemplateVersion).toBeCalledTimes(0);
});
test("Patch request is not send when there are no changes", async () => {
const user = userEvent.setup();
renderTemplateEditorPage();
const topbar = await screen.findByTestId("topbar");
const newTemplateVersion = {
...MockTemplateVersion,
id: "new-version-id",
name: "new-version",
message: "",
};
await typeOnEditor("new content", user);
await buildTemplateVersion(newTemplateVersion, user, topbar);
// Publish
const patchTemplateVersion = jest
.spyOn(api, "patchTemplateVersion")
.mockResolvedValue(newTemplateVersion);
const publishButton = within(topbar).getByRole("button", {
name: "Publish",
});
await user.click(publishButton);
const publishDialog = await screen.findByTestId("dialog");
// It is using the name from the template
const nameField = within(publishDialog).getByLabelText("Version name");
expect(nameField).toHaveValue(newTemplateVersion.name);
// Publish
await user.click(
within(publishDialog).getByRole("button", { name: "Publish" }),
);
expect(patchTemplateVersion).toBeCalledTimes(0);
});
describe.each([
{
testName: "Do not ask when template version has no errors",
initialVariables: undefined,
loadedVariables: undefined,
templateVersion: MockTemplateVersion,
askForVariables: false,
},
{
testName:
"Do not ask when template version has no errors even when having previously loaded variables",
initialVariables: [
MockTemplateVersionVariable1,
MockTemplateVersionVariable2,
],
loadedVariables: undefined,
templateVersion: MockTemplateVersion,
askForVariables: false,
},
{
testName: "Ask when template version has errors",
initialVariables: undefined,
templateVersion: {
...MockTemplateVersion,
job: {
...MockTemplateVersion.job,
error_code: "REQUIRED_TEMPLATE_VARIABLES",
},
},
loadedVariables: [
MockTemplateVersionVariable1,
MockTemplateVersionVariable2,
],
askForVariables: true,
},
])(
"Missing template variables",
({
testName,
initialVariables,
loadedVariables,
templateVersion,
askForVariables,
}) => {
it(testName, async () => {
jest.resetAllMocks();
const queryClient = new QueryClient();
queryClient.setQueryData(
templateVersionVariablesKey(MockTemplateVersion.id),
initialVariables,
);
server.use(
http.get(
"/api/v2/organizations/:org/templates/:template/versions/:version",
() => {
return HttpResponse.json(templateVersion);
},
),
);
if (loadedVariables) {
server.use(
http.get("/api/v2/templateversions/:version/variables", () => {
return HttpResponse.json(loadedVariables);
}),
);
}
render(
<AppProviders queryClient={queryClient}>
<RouterProvider
router={createMemoryRouter(
[
{
element: <RequireAuth />,
children: [
{
element: <TemplateVersionEditorPage />,
path: "/templates/:template/versions/:version/edit",
},
],
},
],
{
initialEntries: [
`/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`,
],
},
)}
/>
</AppProviders>,
);
await waitForLoaderToBeRemoved();
const dialogSelector = /template variables/i;
if (askForVariables) {
await screen.findByText(dialogSelector);
} else {
expect(screen.queryByText(dialogSelector)).not.toBeInTheDocument();
}
});
},
);