Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { API } from "#/api/api";
import { getAuthorizationKey } from "#/api/queries/authCheck";
import { templateByNameKey } from "#/api/queries/templates";
import { MockTemplate, mockApiError } from "#/testHelpers/entities";
import { withDashboardProvider, withToaster } from "#/testHelpers/storybook";
import { TemplateSettingsLayout } from "../TemplateSettingsLayout";
import TemplateSettingsPage from "./TemplateSettingsPage";

const meta = {
title: "pages/TemplateSettingsPage/TemplateSettingsPage",
component: TemplateSettingsLayout,
decorators: [withToaster, withDashboardProvider],
parameters: {
layout: "fullscreen",
reactRouter: reactRouterParameters({
location: {
path: "/templates/:template/settings",
pathParams: { template: MockTemplate.name },
},
routing: [
{
path: "/templates/:template/settings",
useStoryElement: true,
children: [{ index: true, element: <TemplateSettingsPage /> }],
},
{ path: "/templates/:template", element: <div>Template</div> },
],
}),
queries: [
{
key: templateByNameKey("default", MockTemplate.name),
data: MockTemplate,
},
{
key: getAuthorizationKey({
checks: {
canUpdateTemplate: {
object: {
resource_type: "template",
resource_id: MockTemplate.id,
},
action: "update",
},
},
}),
data: { canUpdateTemplate: true },
},
],
},
} satisfies Meta<typeof TemplateSettingsLayout>;

export default meta;
type Story = StoryObj<typeof meta>;

export const UpdateSucceeds: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const updateTemplateMetaSpy = spyOn(
API,
"updateTemplateMeta",
).mockResolvedValue({ ...MockTemplate, name: "new-name" });
await fillAndSubmitForm(canvas, user);
await waitFor(() => expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1));
},
};

export const DisplaysErrorWhenNameIsTaken: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const updateTemplateMetaSpy = spyOn(
API,
"updateTemplateMeta",
).mockRejectedValue(
mockApiError({
message: `Template with name "test-template" already exists`,
validations: [
{
field: "name",
detail: "This value is already in use and should be unique.",
},
],
}),
);
await fillAndSubmitForm(canvas, user);
await waitFor(() => expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1));
const form = await canvas.findByRole("form", {
name: /template settings/i,
});
await within(form).findByText(
"This value is already in use and should be unique.",
);
},
};

export const DeprecatesTemplateWithAccessControl: Story = {
parameters: {
features: ["access_control"],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const updateTemplateMetaSpy = spyOn(
API,
"updateTemplateMeta",
).mockResolvedValue(MockTemplate);
const deprecationMessage = "This template is deprecated";
await deprecateTemplate(canvas, user, deprecationMessage);
await waitFor(() => expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1));
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
expect(templateId).toEqual(MockTemplate.id);
expect(data).toEqual(
expect.objectContaining({ deprecation_message: deprecationMessage }),
);
},
};

export const DoesNotDeprecateWithoutAccessControl: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const updateTemplateMetaSpy = spyOn(
API,
"updateTemplateMeta",
).mockResolvedValue(MockTemplate);
await deprecateTemplate(
canvas,
user,
"This template should not be able to deprecate",
);
await waitFor(() => expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1));
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
expect(templateId).toEqual(MockTemplate.id);
expect(data).toEqual(expect.objectContaining({ deprecation_message: "" }));
},
};

async function fillAndSubmitForm(
canvas: ReturnType<typeof within>,
user: ReturnType<typeof userEvent.setup>,
) {
const nameField = await canvas.findByLabelText("Name");
await user.clear(nameField);
await user.type(nameField, "Name");

const displayNameField = await canvas.findByLabelText("Display name");
await user.clear(displayNameField);
await user.type(displayNameField, "A display name");

const descriptionField = await canvas.findByLabelText("Description");
await user.clear(descriptionField);
await user.type(descriptionField, "A description");

const iconField = await canvas.findByLabelText("Icon");
await user.clear(iconField);
await user.type(iconField, "vscode.png");

const allowCancelJobsField = canvas.getByRole("checkbox", {
name: /allow users to cancel in-progress workspace jobs/i,
});
await user.click(allowCancelJobsField);

await user.click(await canvas.findByRole("button", { name: /save/i }));
}

async function deprecateTemplate(
canvas: ReturnType<typeof within>,
user: ReturnType<typeof userEvent.setup>,
message: string,
) {
const deprecationField = await canvas.findByLabelText("Deprecation Message");
await user.type(deprecationField, message);
await user.click(await canvas.findByRole("button", { name: /save/i }));
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { API, withDefaultFeatures } from "#/api/api";
import type { UpdateTemplateMeta } from "#/api/typesGenerated";
import {
MockEntitlements,
MockTemplate,
mockApiError,
} from "#/testHelpers/entities";
import {
renderWithTemplateSettingsLayout,
waitForLoaderToBeRemoved,
} from "#/testHelpers/renderHelpers";
import { server } from "#/testHelpers/server";
import { validationSchema } from "./TemplateSettingsForm";
import TemplateSettingsPage from "./TemplateSettingsPage";

type FormValues = Required<
Omit<
Expand Down Expand Up @@ -62,92 +47,7 @@ const validFormValues: FormValues = {
disable_module_cache: false,
};

const renderTemplateSettingsPage = async () => {
renderWithTemplateSettingsLayout(<TemplateSettingsPage />, {
route: `/templates/${MockTemplate.name}/settings`,
path: "/templates/:template/settings",
extraRoutes: [
{ path: "/templates/:template", element: <div>Template</div> },
],
});
await waitForLoaderToBeRemoved();
};

const fillAndSubmitForm = async ({
name,
display_name,
description,
icon,
allow_user_cancel_workspace_jobs,
}: FormValues) => {
const nameField = await screen.findByLabelText("Name");
await userEvent.clear(nameField);
await userEvent.type(nameField, name);

const displayNameField = await screen.findByLabelText("Display name");
await userEvent.clear(displayNameField);
await userEvent.type(displayNameField, display_name);

const descriptionField = await screen.findByLabelText("Description");
await userEvent.clear(descriptionField);
await userEvent.type(descriptionField, description);

const iconField = await screen.findByLabelText("Icon");
await userEvent.clear(iconField);
await userEvent.type(iconField, icon);

const allowCancelJobsField = screen.getByRole("checkbox", {
name: /allow users to cancel in-progress workspace jobs/i,
});
// checkbox is checked by default, so it must be clicked to get unchecked
if (!allow_user_cancel_workspace_jobs) {
await userEvent.click(allowCancelJobsField);
}

const submitButton = await screen.findByText(/save/i);
await userEvent.click(submitButton);
};

describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("succeeds", async () => {
await renderTemplateSettingsPage();
vi.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
...MockTemplate,
...validFormValues,
});
await fillAndSubmitForm(validFormValues);
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
});

it("displays an error if the name is taken", async () => {
await renderTemplateSettingsPage();
vi.spyOn(API, "updateTemplateMeta").mockRejectedValueOnce(
mockApiError({
message: `Template with name "test-template" already exists`,
validations: [
{
field: "name",
detail: "This value is already in use and should be unique.",
},
],
}),
);
await fillAndSubmitForm(validFormValues);
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
const form = await screen.findByRole("form", {
name: /template settings/i,
});
expect(
await within(form).findByText(
"This value is already in use and should be unique.",
),
).toBeInTheDocument();
});

describe("TemplateSettingsPage", () => {
it("allows a description of 128 chars", () => {
const values: UpdateTemplateMeta = {
...validFormValues,
Expand All @@ -167,66 +67,4 @@ describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
const validate = () => validationSchema.validateSync(values);
expect(validate).toThrowError();
});

describe("Deprecate template", () => {
it("deprecates a template when has access control", async () => {
server.use(
http.get("/api/v2/entitlements", () => {
return HttpResponse.json({
...MockEntitlements,
features: withDefaultFeatures({
access_control: { enabled: true, entitlement: "entitled" },
}),
});
}),
);
const updateTemplateMetaSpy = vi.spyOn(API, "updateTemplateMeta");
const deprecationMessage = "This template is deprecated";

await renderTemplateSettingsPage();
await deprecateTemplate(deprecationMessage);
await waitFor(() =>
expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1),
);

const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
expect(templateId).toEqual(MockTemplate.id);
expect(data).toEqual(
expect.objectContaining({ deprecation_message: deprecationMessage }),
);
});

it("does not deprecate a template when does not have access control", async () => {
server.use(
http.get("/api/v2/entitlements", () => {
return HttpResponse.json({
...MockEntitlements,
features: withDefaultFeatures({
access_control: { enabled: false, entitlement: "not_entitled" },
}),
});
}),
);
const updateTemplateMetaSpy = vi.spyOn(API, "updateTemplateMeta");

await renderTemplateSettingsPage();
await deprecateTemplate("This template should not be able to deprecate");
await waitFor(() =>
expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1),
);

const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
expect(templateId).toEqual(MockTemplate.id);
expect(data).toEqual(
expect.objectContaining({ deprecation_message: "" }),
);
});
});
});

async function deprecateTemplate(message: string) {
const deprecationField = screen.getByLabelText("Deprecation Message");
await userEvent.type(deprecationField, message);
const submitButton = await screen.findByRole("button", { name: /save/i });
await userEvent.click(submitButton);
}
Loading