mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: save empty template files (#22202)
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 <noreply@anthropic.com> Co-authored-by: Kayla はな <mckayla@hey.com>
This commit is contained in:
@@ -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<typeof MonacoEditor> = {
|
||||
title: "pages/TemplateVersionEditorPage/MonacoEditor",
|
||||
component: MonacoEditor,
|
||||
args: {},
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ height: "400px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof MonacoEditor>;
|
||||
|
||||
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");
|
||||
},
|
||||
};
|
||||
@@ -49,7 +49,7 @@ export const MonacoEditor: FC<MonacoEditorProps> = ({
|
||||
}}
|
||||
path={path}
|
||||
onChange={(newValue) => {
|
||||
if (onChange && newValue) {
|
||||
if (onChange && newValue !== undefined) {
|
||||
onChange(newValue);
|
||||
}
|
||||
}}
|
||||
|
||||
+16
-18
@@ -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: () => <div></div>,
|
||||
@@ -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) => (
|
||||
<textarea
|
||||
data-testid="monaco-editor"
|
||||
@@ -78,27 +78,25 @@ const buildTemplateVersion = async (
|
||||
user: UserEvent,
|
||||
topbar: HTMLElement,
|
||||
) => {
|
||||
jest.spyOn(API, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||
jest.spyOn(API, "createTemplateVersion").mockResolvedValue({
|
||||
vi.spyOn(API, "uploadFile").mockResolvedValueOnce({ hash: "hash" });
|
||||
vi.spyOn(API, "createTemplateVersion").mockResolvedValue({
|
||||
...templateVersion,
|
||||
job: {
|
||||
...templateVersion.job,
|
||||
status: "running",
|
||||
},
|
||||
});
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionByName")
|
||||
.mockResolvedValue(templateVersion);
|
||||
jest
|
||||
.spyOn(apiModule, "watchBuildLogsByTemplateVersionId")
|
||||
.mockImplementation((_, options) => {
|
||||
vi.spyOn(API, "getTemplateVersionByName").mockResolvedValue(templateVersion);
|
||||
vi.spyOn(apiModule, "watchBuildLogsByTemplateVersionId").mockImplementation(
|
||||
(_, options) => {
|
||||
options.onMessage(MockWorkspaceBuildLogs[0]);
|
||||
options.onDone?.();
|
||||
const wsMock = {
|
||||
close: jest.fn(),
|
||||
close: vi.fn(),
|
||||
} as unknown;
|
||||
return wsMock as WebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
const buildButton = within(topbar).getByRole("button", {
|
||||
name: "Build",
|
||||
});
|
||||
@@ -124,10 +122,10 @@ test("Use custom name, message and set it as active when publishing", async () =
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
const patchTemplateVersion = jest
|
||||
const patchTemplateVersion = vi
|
||||
.spyOn(API, "patchTemplateVersion")
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
const updateActiveTemplateVersion = jest
|
||||
const updateActiveTemplateVersion = vi
|
||||
.spyOn(API, "updateActiveTemplateVersion")
|
||||
.mockResolvedValue({ message: "" });
|
||||
const publishButton = within(topbar).getByRole("button", {
|
||||
@@ -170,10 +168,10 @@ test("Do not mark as active if promote is not checked", async () => {
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
const patchTemplateVersion = jest
|
||||
const patchTemplateVersion = vi
|
||||
.spyOn(API, "patchTemplateVersion")
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
const updateActiveTemplateVersion = jest
|
||||
const updateActiveTemplateVersion = vi
|
||||
.spyOn(API, "updateActiveTemplateVersion")
|
||||
.mockResolvedValue({ message: "" });
|
||||
const publishButton = within(topbar).getByRole("button", {
|
||||
@@ -215,7 +213,7 @@ test("Patch request is not send when there are no changes", async () => {
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
const patchTemplateVersion = jest
|
||||
const patchTemplateVersion = vi
|
||||
.spyOn(API, "patchTemplateVersion")
|
||||
.mockResolvedValue(newTemplateVersion);
|
||||
const publishButton = within(topbar).getByRole("button", {
|
||||
@@ -333,7 +331,7 @@ describe.each([
|
||||
askForVariables,
|
||||
}) => {
|
||||
it(testName, async () => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
const queryClient = new QueryClient();
|
||||
queryClient.setQueryData(
|
||||
templateVersionVariablesKey(MockTemplateVersion.id),
|
||||
Reference in New Issue
Block a user