From 77006f241bb32c89351a6470222ae48877f373c1 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Wed, 25 Feb 2026 13:43:07 -0500 Subject: [PATCH] fix: save empty template files (#22202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Monaco editor wrapper was only calling `onChange` if the template file has content, but we want to allow saving an empty file. Fixes #19721 Claude was used to port tests from jest to vitest, and for the stories. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Kayla はな --- .../MonacoEditor.stories.tsx | 80 +++++++++++++++++++ .../MonacoEditor.tsx | 2 +- ...tsx => TemplateVersionEditorPage.test.tsx} | 34 ++++---- 3 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 site/src/pages/TemplateVersionEditorPage/MonacoEditor.stories.tsx rename site/src/pages/TemplateVersionEditorPage/{TemplateVersionEditorPage.jest.tsx => TemplateVersionEditorPage.test.tsx} (95%) diff --git a/site/src/pages/TemplateVersionEditorPage/MonacoEditor.stories.tsx b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.stories.tsx new file mode 100644 index 0000000000..cbafff62f5 --- /dev/null +++ b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import * as monaco from "monaco-editor"; +import { expect, fn, within } from "storybook/test"; +import { MonacoEditor } from "./MonacoEditor"; + +const meta: Meta = { + title: "pages/TemplateVersionEditorPage/MonacoEditor", + component: MonacoEditor, + args: {}, + parameters: { + layout: "fullscreen", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Empty: Story = {}; + +export const WithContent: Story = { + args: { + value: `terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" +} +`, + path: "main.tf", + }, +}; + +export const WithJSON: Story = { + args: { + value: JSON.stringify({ key: "value", nested: { foo: "bar" } }, null, 2), + path: "config.json", + }, +}; + +export const WithOnChangeHandler: Story = { + args: { + onChange: fn(), + value: "fnord", + }, + // Monaco's textarea does not receive or fire events directly. Instead, we + // have to interact with the editor's model and then assert that the + // onChange callback was called with the new value. + async play({ args, canvasElement }) { + const canvas = within(canvasElement); + const editor = canvas.getByRole("textbox"); + + // there's only one model in the story + const model = monaco.editor.getModels()[0]; + + model.setValue(""); + + await expect(editor).toHaveValue(""); + await expect(args.onChange).toHaveBeenCalledOnce(); + await expect(args.onChange).toHaveBeenCalledWith(""); + + model.setValue("fnord"); + + await expect(editor).toHaveValue("fnord"); + await expect(args.onChange).toHaveBeenCalledTimes(2); + await expect(args.onChange).toHaveBeenLastCalledWith("fnord"); + }, +}; diff --git a/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx index 169b326250..8608c9e8e8 100644 --- a/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/MonacoEditor.tsx @@ -49,7 +49,7 @@ export const MonacoEditor: FC = ({ }} path={path} onChange={(newValue) => { - if (onChange && newValue) { + if (onChange && newValue !== undefined) { onChange(newValue); } }} diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.jest.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx similarity index 95% rename from site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.jest.tsx rename to site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx index f8c316cd00..a3c71bce0f 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.jest.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditorPage.test.tsx @@ -34,7 +34,7 @@ const { API } = apiModule; // 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( +vi.mock( "modules/templates/TemplateResourcesTable/TemplateResourcesTable", () => ({ TemplateResourcesTable: () =>
, @@ -43,7 +43,7 @@ jest.mock( // 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", () => ({ +vi.mock("pages/TemplateVersionEditorPage/MonacoEditor", () => ({ MonacoEditor: (props: MonacoEditorProps) => (