diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.stories.tsx
new file mode 100644
index 0000000000..c33d318984
--- /dev/null
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.stories.tsx
@@ -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: }],
+ },
+ { path: "/templates/:template", element:
Template
},
+ ],
+ }),
+ 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;
+
+export default meta;
+type Story = StoryObj;
+
+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,
+ user: ReturnType,
+) {
+ 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,
+ user: ReturnType,
+ message: string,
+) {
+ const deprecationField = await canvas.findByLabelText("Deprecation Message");
+ await user.type(deprecationField, message);
+ await user.click(await canvas.findByRole("button", { name: /save/i }));
+}
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
index 153205c7cf..ce90944727 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx
@@ -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<
@@ -62,92 +47,7 @@ const validFormValues: FormValues = {
disable_module_cache: false,
};
-const renderTemplateSettingsPage = async () => {
- renderWithTemplateSettingsLayout(, {
- route: `/templates/${MockTemplate.name}/settings`,
- path: "/templates/:template/settings",
- extraRoutes: [
- { path: "/templates/:template", element: Template
},
- ],
- });
- 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,
@@ -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);
-}