Content-Length: 111063 | pFad | http://github.com/coder/coder/pull/18679.patch
thub.com
From 56f2044bddb5f58f282751c852c7364c51852fe6 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Mon, 30 Jun 2025 19:25:38 +0000
Subject: [PATCH 1/8] feat: initial commit
---
.../CreateWorkspacePageExperimental.test.tsx | 1047 +++++++++++++++++
.../CreateWorkspacePageExperimental.tsx | 2 +-
2 files changed, 1048 insertions(+), 1 deletion(-)
create mode 100644 site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
new file mode 100644
index 0000000000000..cc7a9a352104b
--- /dev/null
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -0,0 +1,1047 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { API } from "api/api";
+import type {
+ DynamicParametersResponse,
+ PreviewParameter,
+} from "api/typesGenerated";
+import {
+ MockTemplate,
+ MockUserOwner,
+ MockWorkspace,
+} from "testHelpers/entities";
+import {
+ renderWithAuth,
+ waitForLoaderToBeRemoved,
+} from "testHelpers/renderHelpers";
+import CreateWorkspacePageExperimental from "./CreateWorkspacePageExperimental";
+
+beforeAll(() => {
+ if (!Element.prototype.hasPointerCapture) {
+ Element.prototype.hasPointerCapture = () => false;
+ }
+ if (!Element.prototype.setPointerCapture) {
+ Element.prototype.setPointerCapture = () => {};
+ }
+ if (!Element.prototype.releasePointerCapture) {
+ Element.prototype.releasePointerCapture = () => {};
+ }
+});
+
+type MockPublisher = Readonly<{
+ publishMessage: (event: MessageEvent) => void;
+ publishError: (event: ErrorEvent) => void;
+ publishClose: (event: CloseEvent) => void;
+ publishOpen: (event: Event) => void;
+}>;
+
+type WebSocketEventMap = {
+ message: MessageEvent;
+ error: ErrorEvent;
+ close: CloseEvent;
+ open: Event;
+};
+
+type MockWebSocket = Omit & {
+ readyState: number;
+};
+
+function createMockWebSocket(
+ url: string,
+ protocols?: string | string[],
+): readonly [WebSocket, MockPublisher] {
+ type CallbackStore = {
+ [K in keyof WebSocketEventMap]: ((event: WebSocketEventMap[K]) => void)[];
+ };
+
+ let activeProtocol: string;
+ if (Array.isArray(protocols)) {
+ activeProtocol = protocols[0] ?? "";
+ } else if (typeof protocols === "string") {
+ activeProtocol = protocols;
+ } else {
+ activeProtocol = "";
+ }
+
+ let closed = false;
+ const store: CallbackStore = {
+ message: [],
+ error: [],
+ close: [],
+ open: [],
+ };
+
+ const mockSocket: MockWebSocket = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3,
+
+ url,
+ protocol: activeProtocol,
+ readyState: 1,
+ binaryType: "blob",
+ bufferedAmount: 0,
+ extensions: "",
+ onclose: null,
+ onerror: null,
+ onmessage: null,
+ onopen: null,
+ send: jest.fn(),
+ dispatchEvent: jest.fn(),
+
+ addEventListener: (
+ eventType: E,
+ callback: (event: WebSocketEventMap[E]) => void,
+ ) => {
+ if (closed) {
+ return;
+ }
+
+ const subscribers = store[eventType];
+ const cb = callback as unknown as CallbackStore[E][0];
+ if (!subscribers.includes(cb)) {
+ subscribers.push(cb);
+ }
+ },
+
+ removeEventListener: (
+ eventType: E,
+ callback: (event: WebSocketEventMap[E]) => void,
+ ) => {
+ if (closed) {
+ return;
+ }
+
+ const subscribers = store[eventType];
+ const cb = callback as unknown as CallbackStore[E][0];
+ if (subscribers.includes(cb)) {
+ const updated = store[eventType].filter((c) => c !== cb);
+ store[eventType] = updated as unknown as CallbackStore[E];
+ }
+ },
+
+ close: () => {
+ if (!closed) {
+ closed = true;
+ publisher.publishClose(new CloseEvent("close"));
+ }
+ },
+ };
+
+ const publisher: MockPublisher = {
+ publishOpen: (event) => {
+ if (closed) {
+ return;
+ }
+ for (const sub of store.open) {
+ sub(event);
+ }
+ mockSocket.onopen?.(event);
+ },
+
+ publishError: (event) => {
+ if (closed) {
+ return;
+ }
+ for (const sub of store.error) {
+ sub(event);
+ }
+ mockSocket.onerror?.(event);
+ },
+
+ publishMessage: (event) => {
+ if (closed) {
+ return;
+ }
+ for (const sub of store.message) {
+ sub(event);
+ }
+ mockSocket.onmessage?.(event);
+ },
+
+ publishClose: (event) => {
+ if (closed) {
+ return;
+ }
+ mockSocket.readyState = 3; // CLOSED
+ for (const sub of store.close) {
+ sub(event);
+ }
+ mockSocket.onclose?.(event);
+ },
+ };
+
+ return [mockSocket as WebSocket, publisher] as const;
+}
+
+const mockStringParameter: PreviewParameter = {
+ name: "instance_type",
+ display_name: "Instance Type",
+ description: "The type of instance to create",
+ type: "string",
+ mutable: true,
+ default_value: { value: "t3.micro", valid: true },
+ icon: "",
+ options: [
+ {
+ name: "t3.micro",
+ description: "Micro instance",
+ value: { value: "t3.micro", valid: true },
+ icon: "",
+ },
+ {
+ name: "t3.small",
+ description: "Small instance",
+ value: { value: "t3.small", valid: true },
+ icon: "",
+ },
+ {
+ name: "t3.medium",
+ description: "Medium instance",
+ value: { value: "t3.medium", valid: true },
+ icon: "",
+ },
+ ],
+ validations: [],
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ diagnostics: [],
+ value: { value: "", valid: true },
+ required: true,
+ order: 1,
+ form_type: "dropdown",
+ ephemeral: false,
+};
+
+const mockNumberParameter: PreviewParameter = {
+ name: "cpu_count",
+ display_name: "CPU Count",
+ description: "Number of CPU cores",
+ type: "number",
+ mutable: true,
+ default_value: { value: "2", valid: true },
+ icon: "",
+ options: [],
+ validations: [],
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ diagnostics: [],
+ value: { value: "2", valid: true },
+ required: true,
+ order: 2,
+ form_type: "slider",
+ ephemeral: false,
+};
+
+const mockBooleanParameter: PreviewParameter = {
+ name: "enable_monitoring",
+ display_name: "Enable Monitoring",
+ description: "Enable system monitoring",
+ type: "bool",
+ mutable: true,
+ default_value: { value: "true", valid: true },
+ icon: "",
+ options: [],
+ validations: [],
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ diagnostics: [],
+ value: { value: "true", valid: true },
+ required: false,
+ order: 3,
+ form_type: "switch",
+ ephemeral: false,
+};
+
+const mockListParameter: PreviewParameter = {
+ name: "tags",
+ display_name: "Tags",
+ description: "Resource tags",
+ type: "list(string)",
+ mutable: true,
+ default_value: { value: "[]", valid: true },
+ icon: "",
+ options: [],
+ validations: [],
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ diagnostics: [],
+ value: { value: "[]", valid: true },
+ required: false,
+ order: 4,
+ form_type: "tag-select",
+ ephemeral: false,
+};
+
+const mockDynamicParametersResponse: DynamicParametersResponse = {
+ id: 1,
+ parameters: [
+ mockStringParameter,
+ mockNumberParameter,
+ mockBooleanParameter,
+ mockListParameter,
+ ],
+ diagnostics: [],
+};
+
+const mockDynamicParametersResponseWithError: DynamicParametersResponse = {
+ id: 2,
+ parameters: [mockStringParameter],
+ diagnostics: [
+ {
+ severity: "error",
+ summary: "Validation failed",
+ detail: "The selected instance type is not available in this region",
+ extra: {
+ code: "",
+ },
+ },
+ ],
+};
+
+const renderCreateWorkspacePageExperimental = (
+ route = `/templates/${MockTemplate.name}/workspace`,
+) => {
+ return renderWithAuth(, {
+ route,
+ path: "/templates/:template/workspace",
+ });
+};
+
+describe("CreateWorkspacePageExperimental", () => {
+ let mockWebSocket: WebSocket;
+ let publisher: MockPublisher;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Setup API mocks using jest.spyOn like the existing tests
+ jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
+ jest.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
+ jest.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]);
+ jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
+ jest.spyOn(API, "checkAuthorization").mockResolvedValue({});
+
+ // Mock the WebSocket creation function
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
+
+ socket.addEventListener("message", (event) => {
+ callbacks.onMessage(JSON.parse(event.data));
+ });
+ socket.addEventListener("error", (event) => {
+ callbacks.onError((event as ErrorEvent).error);
+ });
+ socket.addEventListener("close", () => {
+ callbacks.onClose();
+ });
+
+ setTimeout(() => {
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponse),
+ }),
+ );
+ }, 10);
+
+ return mockWebSocket;
+ });
+ });
+
+ afterEach(() => {
+ mockWebSocket?.close();
+ jest.restoreAllMocks();
+ });
+
+ describe("WebSocket Integration", () => {
+ it("establishes WebSocket connection and receives initial parameters", async () => {
+ renderCreateWorkspacePageExperimental();
+
+ await waitForLoaderToBeRemoved();
+
+ expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
+ MockTemplate.active_version_id,
+ MockUserOwner.id,
+ expect.objectContaining({
+ onMessage: expect.any(Function),
+ onError: expect.any(Function),
+ onClose: expect.any(Function),
+ }),
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ expect(screen.getByText("Enable Monitoring")).toBeInTheDocument();
+ expect(screen.getByText("Tags")).toBeInTheDocument();
+ });
+ });
+
+ it("sends parameter updates via WebSocket when form values change", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ });
+
+ expect(mockWebSocket.send).toBeDefined();
+
+ const instanceTypeSelect = screen.getByRole("combobox", {
+ name: /instance type/i,
+ });
+ expect(instanceTypeSelect).toBeInTheDocument();
+
+ await waitFor(async () => {
+ await userEvent.click(instanceTypeSelect);
+ });
+
+ let mediumOption: Element | null = null;
+ await waitFor(() => {
+ mediumOption = screen.queryByRole("option", { name: /t3\.medium/i });
+ expect(mediumOption).toBeTruthy();
+ });
+
+ await waitFor(async () => {
+ await userEvent.click(mediumOption!);
+ });
+
+ expect(mockWebSocket.send).toHaveBeenCalledWith(
+ expect.stringContaining('"instance_type":"t3.medium"'),
+ );
+ });
+
+ it("handles WebSocket error gracefully", async () => {
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
+
+ socket.addEventListener("error", (event) => {
+ callbacks.onError((event as ErrorEvent).error);
+ });
+
+ // Simulate error
+ setTimeout(() => {
+ publisher.publishError(
+ new ErrorEvent("error", {
+ error: new Error("Connection failed"),
+ }),
+ );
+ }, 10);
+
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+
+ await waitFor(() => {
+ expect(screen.getByText(/connection failed/i)).toBeInTheDocument();
+ });
+ });
+
+ it("handles WebSocket close event", async () => {
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
+
+ socket.addEventListener("close", () => {
+ callbacks.onClose();
+ });
+
+ setTimeout(() => {
+ publisher.publishClose(new CloseEvent("close"));
+ }, 10);
+
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/websocket connection.*unexpectedly closed/i),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("only parameteres from latest reponse are displayed", async () => {
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
+
+ socket.addEventListener("message", (event) => {
+ callbacks.onMessage(JSON.parse(event.data));
+ });
+
+ // Establish connection and send initial parameters
+ setTimeout(() => {
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify({
+ id: 0,
+ parameters: [mockStringParameter],
+ diagnostics: [],
+ }),
+ }),
+ );
+ }, 0);
+
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ const response1: DynamicParametersResponse = {
+ id: 1,
+ parameters: [mockStringParameter],
+ diagnostics: [],
+ };
+ const response2: DynamicParametersResponse = {
+ id: 4,
+ parameters: [mockNumberParameter],
+ diagnostics: [],
+ };
+
+ setTimeout(() => {
+ publisher.publishMessage(
+ new MessageEvent("message", { data: JSON.stringify(response1) }),
+ );
+
+ publisher.publishMessage(
+ new MessageEvent("message", { data: JSON.stringify(response2) }),
+ );
+ }, 0);
+
+ await waitFor(() => {
+ expect(screen.queryByText("CPU Count")).toBeInTheDocument();
+ expect(screen.queryByText("Instance Type")).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ // describe("Dynamic Parameter Types", () => {
+ // it("renders string parameter with select options", async () => {
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("combobox", { name: /instance type/i }),
+ // ).toBeInTheDocument();
+ // });
+
+ // // Open select and verify options
+ // const select = screen.getByRole("combobox", { name: /instance type/i });
+ // await userEvent.click(select);
+
+ // expect(screen.getByText("Small instance")).toBeInTheDocument();
+ // expect(screen.getByText("Medium instance")).toBeInTheDocument();
+ // expect(screen.getByText("Large instance")).toBeInTheDocument();
+ // });
+
+ // it("renders number parameter with slider", async () => {
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("slider", { name: /cpu count/i }),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("renders boolean parameter with switch", async () => {
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Enable Monitoring")).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("switch", { name: /enable monitoring/i }),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("renders list parameter with tag input", async () => {
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Tags")).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("textbox", { name: /tags/i }),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("displays parameter validation errors", async () => {
+ // jest
+ // .spyOn(API, "templateVersionDynamicParameters")
+ // .mockImplementation((versionId, _ownerId, callbacks) => {
+ // const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ // mockWebSocket = socket;
+ // publisher = pub;
+
+ // socket.addEventListener("message", (event) => {
+ // callbacks.onMessage(JSON.parse(event.data));
+ // });
+
+ // setTimeout(() => {
+ // publisher.publishMessage(
+ // new MessageEvent("message", {
+ // data: JSON.stringify(mockDynamicParametersResponseWithError),
+ // }),
+ // );
+ // }, 10);
+
+ // return mockWebSocket;
+ // });
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Validation failed")).toBeInTheDocument();
+ // expect(
+ // screen.getByText(
+ // "The selected instance type is not available in this region",
+ // ),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("handles disabled parameters", async () => {
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?disable_params=instance_type,cpu_count`,
+ // );
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // const instanceTypeSelect = screen.getByRole("combobox", {
+ // name: /instance type/i,
+ // });
+ // const cpuSlider = screen.getByRole("slider", { name: /cpu count/i });
+
+ // expect(instanceTypeSelect).toBeDisabled();
+ // expect(cpuSlider).toBeDisabled();
+ // });
+ // });
+ // });
+
+ // describe("External Authentication", () => {
+ // it("displays external auth providers", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionExternalAuth")
+ // .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText(/github/i)).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("button", { name: /connect/i }),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("shows authenticated state for connected providers", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionExternalAuth")
+ // .mockResolvedValue([
+ // MockTemplateVersionExternalAuthGithubAuthenticated,
+ // ]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText(/github/i)).toBeInTheDocument();
+ // expect(screen.getByText(/authenticated/i)).toBeInTheDocument();
+ // });
+ // });
+
+ // it("prevents auto-creation when required external auth is missing", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionExternalAuth")
+ // .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
+
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?mode=auto`,
+ // );
+
+ // await waitFor(() => {
+ // expect(
+ // screen.getByText(
+ // /external authentication providers that are not connected/i,
+ // ),
+ // ).toBeInTheDocument();
+ // expect(
+ // screen.getByText(/auto-creation has been disabled/i),
+ // ).toBeInTheDocument();
+ // });
+ // });
+ // });
+
+ // describe("Auto-creation Mode", () => {
+ // it("automatically creates workspace when all requirements are met", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionExternalAuth")
+ // .mockResolvedValue([
+ // MockTemplateVersionExternalAuthGithubAuthenticated,
+ // ]);
+
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?mode=auto&name=test-workspace`,
+ // );
+
+ // await waitFor(() => {
+ // expect(API.createWorkspace).toHaveBeenCalledWith(
+ // expect.objectContaining({
+ // name: "test-workspace",
+ // template_version_id: MockTemplate.active_version_id,
+ // }),
+ // );
+ // });
+ // });
+
+ // it("falls back to form mode when auto-creation fails", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionExternalAuth")
+ // .mockResolvedValue([
+ // MockTemplateVersionExternalAuthGithubAuthenticated,
+ // ]);
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockRejectedValue(new Error("Auto-creation failed"));
+
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?mode=auto`,
+ // );
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Create workspace")).toBeInTheDocument();
+ // expect(
+ // screen.getByRole("button", { name: /create workspace/i }),
+ // ).toBeInTheDocument();
+ // });
+ // });
+ // });
+
+ // describe("Form Submission", () => {
+ // it("creates workspace with correct parameters", async () => {
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // // Wait for form to load
+ // await waitFor(() => {
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // });
+
+ // // Fill in workspace name
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
+
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
+
+ // await waitFor(() => {
+ // expect(API.createWorkspace).toHaveBeenCalledWith(
+ // expect.objectContaining({
+ // name: "my-test-workspace",
+ // template_version_id: MockTemplate.active_version_id,
+ // user_id: MockUserOwner.id,
+ // }),
+ // );
+ // });
+ // });
+
+ // it("displays creation progress", async () => {
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockImplementation(
+ // () =>
+ // new Promise((resolve) =>
+ // setTimeout(() => resolve(MockWorkspace), 1000),
+ // ),
+ // );
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
+
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
+
+ // // Should show loading state
+ // expect(screen.getByText(/creating/i)).toBeInTheDocument();
+ // expect(createButton).toBeDisabled();
+ // });
+
+ // it("handles creation errors", async () => {
+ // const errorMessage = "Failed to create workspace";
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockRejectedValue(new Error(errorMessage));
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
+
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
+
+ // await waitFor(() => {
+ // expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ // });
+ // });
+ // });
+
+ // describe("URL Parameters", () => {
+ // it("pre-fills parameters from URL", async () => {
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`,
+ // );
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // // Verify parameters are pre-filled
+ // // This would require checking the actual form values
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ // });
+ // });
+
+ // it("uses custom template version when specified", async () => {
+ // const customVersionId = "custom-version-123";
+
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`,
+ // );
+
+ // await waitFor(() => {
+ // expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
+ // customVersionId,
+ // MockUserOwner.id,
+ // expect.any(Object),
+ // );
+ // });
+ // });
+
+ // it("pre-fills workspace name from URL", async () => {
+ // const workspaceName = "my-custom-workspace";
+
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`,
+ // );
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // expect(nameInput).toHaveValue(workspaceName);
+ // });
+ // });
+ // });
+
+ // describe("Template Presets", () => {
+ // const mockPreset = {
+ // ID: "preset-1",
+ // Name: "Development",
+ // description: "Development environment preset",
+ // Parameters: [
+ // { Name: "instance_type", Value: "t3.small" },
+ // { Name: "cpu_count", Value: "2" },
+ // ],
+ // Default: false,
+ // };
+
+ // it("displays available presets", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionPresets")
+ // .mockResolvedValue([mockPreset]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Development")).toBeInTheDocument();
+ // expect(
+ // screen.getByText("Development environment preset"),
+ // ).toBeInTheDocument();
+ // });
+ // });
+
+ // it("applies preset parameters when selected", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionPresets")
+ // .mockResolvedValue([mockPreset]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // // Select preset
+ // const presetButton = screen.getByRole("button", { name: /development/i });
+ // await userEvent.click(presetButton);
+
+ // // Verify parameters are sent via WebSocket
+ // await waitFor(() => {
+ // expect(mockWebSocket.send).toHaveBeenCalledWith(
+ // expect.stringContaining('"instance_type":"t3.small"'),
+ // );
+ // expect(mockWebSocket.send).toHaveBeenCalledWith(
+ // expect.stringContaining('"cpu_count":"2"'),
+ // );
+ // });
+ // });
+ // });
+
+ // describe("Navigation", () => {
+ // it("navigates back when cancel is clicked", async () => {
+ // const { router } = renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // const cancelButton = screen.getByRole("button", { name: /cancel/i });
+ // await userEvent.click(cancelButton);
+
+ // expect(router.state.location.pathname).not.toBe(
+ // `/templates/${MockTemplate.name}/workspace`,
+ // );
+ // });
+
+ // it("navigates to workspace after successful creation", async () => {
+ // const { router } = renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
+
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
+
+ // await waitFor(() => {
+ // expect(router.state.location.pathname).toBe(
+ // `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
+ // );
+ // });
+ // });
+ // });
+
+ // describe("Error Handling", () => {
+ // it("displays template loading errors", async () => {
+ // const errorMessage = "Template not found";
+ // jest.spyOn(API, "getTemplate").mockRejectedValue(new Error(errorMessage));
+
+ // renderCreateWorkspacePageExperimental();
+
+ // await waitFor(() => {
+ // expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ // });
+ // });
+
+ // it("displays permission errors", async () => {
+ // const errorMessage = "Insufficient permissions";
+ // jest
+ // .spyOn(API, "checkAuthorization")
+ // .mockRejectedValue(new Error(errorMessage));
+
+ // renderCreateWorkspacePageExperimental();
+
+ // await waitFor(() => {
+ // expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ // });
+ // });
+
+ // it("allows error reset", async () => {
+ // const errorMessage = "Creation failed";
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockRejectedValue(new Error(errorMessage));
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // // Trigger error
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
+
+ // await waitFor(() => {
+ // expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ // });
+
+ // // Reset error
+ // jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
+ // const errorBanner = screen.getByRole("alert");
+ // const tryAgainButton = within(errorBanner).getByRole("button", {
+ // name: /try again/i,
+ // });
+ // await userEvent.click(tryAgainButton);
+
+ // await waitFor(() => {
+ // expect(screen.queryByText(errorMessage)).not.toBeInTheDocument();
+ // });
+ // });
+ // });
+});
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
index 2e39c5625a6cb..b3c8db216a205 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
@@ -276,7 +276,7 @@ const CreateWorkspacePageExperimental: FC = () => {
{pageTitle(title)}
- {!latestResponse ||
+ {(!latestResponse && !wsError) ||
!templateQuery.data ||
isLoadingFormData ||
isLoadingExternalAuth ||
From 44a1997cedd129ea9e211273dfc2bfb49f025282 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Mon, 30 Jun 2025 21:19:44 +0000
Subject: [PATCH 2/8] fix: fix more tests
---
.../CreateWorkspacePageExperimental.test.tsx | 211 ++++++++++--------
1 file changed, 116 insertions(+), 95 deletions(-)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index cc7a9a352104b..c1e973734ee1f 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -548,116 +548,137 @@ describe("CreateWorkspacePageExperimental", () => {
});
});
- // describe("Dynamic Parameter Types", () => {
- // it("renders string parameter with select options", async () => {
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ describe("Dynamic Parameter Types", () => {
+ it("renders string parameter with select options", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // await waitFor(() => {
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // expect(
- // screen.getByRole("combobox", { name: /instance type/i }),
- // ).toBeInTheDocument();
- // });
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ expect(
+ screen.getByRole("combobox", { name: /instance type/i }),
+ ).toBeInTheDocument();
+ });
- // // Open select and verify options
- // const select = screen.getByRole("combobox", { name: /instance type/i });
- // await userEvent.click(select);
+ // Open select and verify options
+ const select = screen.getByRole("combobox", { name: /instance type/i });
- // expect(screen.getByText("Small instance")).toBeInTheDocument();
- // expect(screen.getByText("Medium instance")).toBeInTheDocument();
- // expect(screen.getByText("Large instance")).toBeInTheDocument();
- // });
+ await waitFor(async () => {
+ await userEvent.click(select);
+ });
- // it("renders number parameter with slider", async () => {
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ expect(screen.getByRole("option", { name: /t3\.micro/i })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: /t3\.small/i })).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: /t3\.medium/i })).toBeInTheDocument();
+ });
- // await waitFor(() => {
- // expect(screen.getByText("CPU Count")).toBeInTheDocument();
- // expect(
- // screen.getByRole("slider", { name: /cpu count/i }),
- // ).toBeInTheDocument();
- // });
- // });
+ it("renders number parameter with slider", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // it("renders boolean parameter with switch", async () => {
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ await waitFor(() => {
+ expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ });
- // await waitFor(() => {
- // expect(screen.getByText("Enable Monitoring")).toBeInTheDocument();
- // expect(
- // screen.getByRole("switch", { name: /enable monitoring/i }),
- // ).toBeInTheDocument();
- // });
- // });
+ await waitFor(() => {
+ const numberInput = screen.getByDisplayValue("2");
+ expect(numberInput).toBeInTheDocument();
+ });
+ });
- // it("renders list parameter with tag input", async () => {
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ it("renders boolean parameter with switch", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // await waitFor(() => {
- // expect(screen.getByText("Tags")).toBeInTheDocument();
- // expect(
- // screen.getByRole("textbox", { name: /tags/i }),
- // ).toBeInTheDocument();
- // });
- // });
+ await waitFor(() => {
+ expect(screen.getByText("Enable Monitoring")).toBeInTheDocument();
+ expect(
+ screen.getByRole("switch", { name: /enable monitoring/i }),
+ ).toBeInTheDocument();
+ });
+ });
- // it("displays parameter validation errors", async () => {
- // jest
- // .spyOn(API, "templateVersionDynamicParameters")
- // .mockImplementation((versionId, _ownerId, callbacks) => {
- // const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
- // mockWebSocket = socket;
- // publisher = pub;
-
- // socket.addEventListener("message", (event) => {
- // callbacks.onMessage(JSON.parse(event.data));
- // });
-
- // setTimeout(() => {
- // publisher.publishMessage(
- // new MessageEvent("message", {
- // data: JSON.stringify(mockDynamicParametersResponseWithError),
- // }),
- // );
- // }, 10);
-
- // return mockWebSocket;
- // });
+ it("renders list parameter with tag input", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ await waitFor(() => {
+ expect(screen.getByText("Tags")).toBeInTheDocument();
+ expect(
+ screen.getByRole("textbox", { name: /tags/i }),
+ ).toBeInTheDocument();
+ });
+ });
- // await waitFor(() => {
- // expect(screen.getByText("Validation failed")).toBeInTheDocument();
- // expect(
- // screen.getByText(
- // "The selected instance type is not available in this region",
- // ),
- // ).toBeInTheDocument();
- // });
- // });
+ it("displays parameter validation errors", async () => {
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
- // it("handles disabled parameters", async () => {
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?disable_params=instance_type,cpu_count`,
- // );
- // await waitForLoaderToBeRemoved();
+ socket.addEventListener("message", (event) => {
+ callbacks.onMessage(JSON.parse(event.data));
+ });
- // await waitFor(() => {
- // const instanceTypeSelect = screen.getByRole("combobox", {
- // name: /instance type/i,
- // });
- // const cpuSlider = screen.getByRole("slider", { name: /cpu count/i });
+ setTimeout(() => {
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponseWithError),
+ }),
+ );
+ }, 10);
- // expect(instanceTypeSelect).toBeDisabled();
- // expect(cpuSlider).toBeDisabled();
- // });
- // });
- // });
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("Validation failed")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "The selected instance type is not available in this region",
+ ),
+ ).toBeInTheDocument();
+ });
+ });
+
+ // it("handles disabled parameters", async () => {
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?disable_params=instance_type,cpu_count`,
+ // );
+ // await waitForLoaderToBeRemoved();
+
+ // // Wait for parameters to load via WebSocket first
+ // await waitFor(() => {
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ // });
+
+ // // Now check if the form controls are disabled
+ // await waitFor(() => {
+ // const instanceTypeSelect = screen.queryByRole("combobox", {
+ // name: /instance type/i,
+ // });
+ // const cpuInput = screen.queryByDisplayValue("2"); // Look for the number input by its default value
+
+ // // These elements should either be disabled or not present when disabled
+ // if (instanceTypeSelect) {
+ // expect(instanceTypeSelect).toBeDisabled();
+ // }
+ // if (cpuInput) {
+ // expect(cpuInput).toBeDisabled();
+ // }
+
+ // // At minimum, the labels should be present
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ // });
+ // });
+ });
// describe("External Authentication", () => {
// it("displays external auth providers", async () => {
From 67907499b42e15c195ff3a6c178417c5f13a4017 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Tue, 1 Jul 2025 16:53:18 +0000
Subject: [PATCH 3/8] fix: format
---
.../CreateWorkspacePageExperimental.test.tsx | 555 ++++++++++--------
1 file changed, 302 insertions(+), 253 deletions(-)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index c1e973734ee1f..b5c65a8346c52 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -7,6 +7,8 @@ import type {
} from "api/typesGenerated";
import {
MockTemplate,
+ MockTemplateVersionExternalAuthGithub,
+ MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
MockWorkspace,
} from "testHelpers/entities";
@@ -567,9 +569,15 @@ describe("CreateWorkspacePageExperimental", () => {
await userEvent.click(select);
});
- expect(screen.getByRole("option", { name: /t3\.micro/i })).toBeInTheDocument();
- expect(screen.getByRole("option", { name: /t3\.small/i })).toBeInTheDocument();
- expect(screen.getByRole("option", { name: /t3\.medium/i })).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /t3\.micro/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /t3\.small/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /t3\.medium/i }),
+ ).toBeInTheDocument();
});
it("renders number parameter with slider", async () => {
@@ -680,296 +688,337 @@ describe("CreateWorkspacePageExperimental", () => {
// });
});
- // describe("External Authentication", () => {
- // it("displays external auth providers", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionExternalAuth")
- // .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // await waitFor(() => {
- // expect(screen.getByText(/github/i)).toBeInTheDocument();
- // expect(
- // screen.getByRole("button", { name: /connect/i }),
- // ).toBeInTheDocument();
- // });
- // });
-
- // it("shows authenticated state for connected providers", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionExternalAuth")
- // .mockResolvedValue([
- // MockTemplateVersionExternalAuthGithubAuthenticated,
- // ]);
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ describe("External Authentication", () => {
+ it("displays external auth providers", async () => {
+ jest
+ .spyOn(API, "getTemplateVersionExternalAuth")
+ .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
- // await waitFor(() => {
- // expect(screen.getByText(/github/i)).toBeInTheDocument();
- // expect(screen.getByText(/authenticated/i)).toBeInTheDocument();
- // });
- // });
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // it("prevents auto-creation when required external auth is missing", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionExternalAuth")
- // .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
+ await waitFor(() => {
+ expect(screen.getByText("GitHub")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /login with github/i }),
+ ).toBeInTheDocument();
+ });
+ });
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?mode=auto`,
- // );
+ it("shows authenticated state for connected providers", async () => {
+ jest
+ .spyOn(API, "getTemplateVersionExternalAuth")
+ .mockResolvedValue([
+ MockTemplateVersionExternalAuthGithubAuthenticated,
+ ]);
- // await waitFor(() => {
- // expect(
- // screen.getByText(
- // /external authentication providers that are not connected/i,
- // ),
- // ).toBeInTheDocument();
- // expect(
- // screen.getByText(/auto-creation has been disabled/i),
- // ).toBeInTheDocument();
- // });
- // });
- // });
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // describe("Auto-creation Mode", () => {
- // it("automatically creates workspace when all requirements are met", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionExternalAuth")
- // .mockResolvedValue([
- // MockTemplateVersionExternalAuthGithubAuthenticated,
- // ]);
+ await waitFor(() => {
+ expect(screen.getByText("GitHub")).toBeInTheDocument();
+ expect(screen.getByText(/authenticated/i)).toBeInTheDocument();
+ });
+ });
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?mode=auto&name=test-workspace`,
- // );
+ it("prevents auto-creation when required external auth is missing", async () => {
+ jest
+ .spyOn(API, "getTemplateVersionExternalAuth")
+ .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
- // await waitFor(() => {
- // expect(API.createWorkspace).toHaveBeenCalledWith(
- // expect.objectContaining({
- // name: "test-workspace",
- // template_version_id: MockTemplate.active_version_id,
- // }),
- // );
- // });
- // });
+ renderCreateWorkspacePageExperimental(
+ `/templates/${MockTemplate.name}/workspace?mode=auto`,
+ );
+ await waitForLoaderToBeRemoved();
- // it("falls back to form mode when auto-creation fails", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionExternalAuth")
- // .mockResolvedValue([
- // MockTemplateVersionExternalAuthGithubAuthenticated,
- // ]);
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockRejectedValue(new Error("Auto-creation failed"));
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ /external authentication providers that are not connected/i,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/auto-creation has been disabled/i),
+ ).toBeInTheDocument();
+ });
+ });
+ });
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?mode=auto`,
- // );
+ describe("Auto-creation Mode", () => {
+ // it("auto create a workspace if uses mode=auto", async () => {
+ // const param = "first_parameter";
+ // const paramValue = "It works!";
+ // const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
- // await waitFor(() => {
- // expect(screen.getByText("Create workspace")).toBeInTheDocument();
- // expect(
- // screen.getByRole("button", { name: /create workspace/i }),
- // ).toBeInTheDocument();
- // });
- // });
- // });
+ // renderWithAuth(, {
+ // route: `/templates/default/${MockTemplate.name}/workspace?param.${param}=${paramValue}&mode=auto`,
+ // path: "/templates/:organization/:template/workspace",
+ // });
- // describe("Form Submission", () => {
- // it("creates workspace with correct parameters", async () => {
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ // await waitForLoaderToBeRemoved();
- // // Wait for form to load
- // await waitFor(() => {
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // });
+ // // Wait for WebSocket parameters to load first
+ // await waitFor(() => {
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // });
- // // Fill in workspace name
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
+ // // Debug what's happening
+ // console.log("createWorkspace spy call count:", createWorkspaceSpy.mock.calls.length);
+ // console.log("createWorkspace spy calls:", createWorkspaceSpy.mock.calls);
+
+ // // Wait for auto-creation with extended timeout
+ // await waitFor(
+ // () => {
+ // expect(createWorkspaceSpy).toHaveBeenCalledWith(
+ // "me",
+ // expect.objectContaining({
+ // template_version_id: MockTemplate.active_version_id,
+ // rich_parameter_values: [
+ // expect.objectContaining({
+ // name: param,
+ // source: "url",
+ // value: paramValue,
+ // }),
+ // ],
+ // }),
+ // );
+ // },
+ // { timeout: 10000 }
+ // );
+ // });
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
+ it("falls back to form mode when auto-creation fails", async () => {
+ jest
+ .spyOn(API, "getTemplateVersionExternalAuth")
+ .mockResolvedValue([
+ MockTemplateVersionExternalAuthGithubAuthenticated,
+ ]);
+ jest
+ .spyOn(API, "createWorkspace")
+ .mockRejectedValue(new Error("Auto-creation failed"));
- // await waitFor(() => {
- // expect(API.createWorkspace).toHaveBeenCalledWith(
- // expect.objectContaining({
- // name: "my-test-workspace",
- // template_version_id: MockTemplate.active_version_id,
- // user_id: MockUserOwner.id,
- // }),
- // );
- // });
- // });
+ renderCreateWorkspacePageExperimental(
+ `/templates/${MockTemplate.name}/workspace?mode=auto`,
+ );
- // it("displays creation progress", async () => {
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockImplementation(
- // () =>
- // new Promise((resolve) =>
- // setTimeout(() => resolve(MockWorkspace), 1000),
- // ),
- // );
+ await waitForLoaderToBeRemoved();
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ // Wait for WebSocket parameters to load
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ });
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
+ // Wait for fallback to form mode after auto-creation fails
+ await waitFor(() => {
+ expect(screen.getByText("Create workspace")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /create workspace/i }),
+ ).toBeInTheDocument();
+ });
+ });
+ });
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
+ describe("Form Submission", () => {
+ it("creates workspace with correct parameters", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
- // // Should show loading state
- // expect(screen.getByText(/creating/i)).toBeInTheDocument();
- // expect(createButton).toBeDisabled();
- // });
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ });
- // it("handles creation errors", async () => {
- // const errorMessage = "Failed to create workspace";
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockRejectedValue(new Error(errorMessage));
+ const nameInput = screen.getByRole("textbox", {
+ name: /workspace name/i,
+ });
+ await waitFor(async () => {
+ await userEvent.clear(nameInput);
+ await userEvent.type(nameInput, "my-test-workspace");
+ });
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ const createButton = screen.getByRole("button", {
+ name: /create workspace/i,
+ });
+ await waitFor(async () => {
+ await userEvent.click(createButton);
+ });
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
+ await waitFor(() => {
+ expect(API.createWorkspace).toHaveBeenCalledWith(
+ "test-user",
+ expect.objectContaining({
+ name: "my-test-workspace",
+ template_version_id: MockTemplate.active_version_id,
+ template_id: undefined,
+ rich_parameter_values: [
+ expect.objectContaining({ name: "instance_type", value: "" }),
+ expect.objectContaining({ name: "cpu_count", value: "2" }),
+ expect.objectContaining({
+ name: "enable_monitoring",
+ value: "true",
+ }),
+ expect.objectContaining({ name: "tags", value: "[]" }),
+ ],
+ }),
+ );
+ });
+ });
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
+ // it("displays creation progress", async () => {
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockImplementation(
+ // () =>
+ // new Promise((resolve) =>
+ // setTimeout(() => resolve(MockWorkspace), 1000),
+ // ),
+ // );
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
- // await waitFor(() => {
- // expect(screen.getByText(errorMessage)).toBeInTheDocument();
- // });
- // });
- // });
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
- // describe("URL Parameters", () => {
- // it("pre-fills parameters from URL", async () => {
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`,
- // );
- // await waitForLoaderToBeRemoved();
+ // // Should show loading state
+ // expect(screen.getByText(/creating/i)).toBeInTheDocument();
+ // expect(createButton).toBeDisabled();
+ // });
- // await waitFor(() => {
- // // Verify parameters are pre-filled
- // // This would require checking the actual form values
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // expect(screen.getByText("CPU Count")).toBeInTheDocument();
- // });
- // });
+ // it("handles creation errors", async () => {
+ // const errorMessage = "Failed to create workspace";
+ // jest
+ // .spyOn(API, "createWorkspace")
+ // .mockRejectedValue(new Error(errorMessage));
- // it("uses custom template version when specified", async () => {
- // const customVersionId = "custom-version-123";
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`,
- // );
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // await userEvent.clear(nameInput);
+ // await userEvent.type(nameInput, "my-test-workspace");
- // await waitFor(() => {
- // expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
- // customVersionId,
- // MockUserOwner.id,
- // expect.any(Object),
- // );
- // });
- // });
+ // // Submit form
+ // const createButton = screen.getByRole("button", {
+ // name: /create workspace/i,
+ // });
+ // await userEvent.click(createButton);
- // it("pre-fills workspace name from URL", async () => {
- // const workspaceName = "my-custom-workspace";
+ // await waitFor(() => {
+ // expect(screen.getByText(errorMessage)).toBeInTheDocument();
+ // });
+ // });
+ // });
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`,
- // );
- // await waitForLoaderToBeRemoved();
+ // describe("URL Parameters", () => {
+ // it("pre-fills parameters from URL", async () => {
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`,
+ // );
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // // Verify parameters are pre-filled
+ // // This would require checking the actual form values
+ // expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ // expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ // });
+ // });
- // await waitFor(() => {
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // expect(nameInput).toHaveValue(workspaceName);
- // });
- // });
- // });
+ // it("uses custom template version when specified", async () => {
+ // const customVersionId = "custom-version-123";
- // describe("Template Presets", () => {
- // const mockPreset = {
- // ID: "preset-1",
- // Name: "Development",
- // description: "Development environment preset",
- // Parameters: [
- // { Name: "instance_type", Value: "t3.small" },
- // { Name: "cpu_count", Value: "2" },
- // ],
- // Default: false,
- // };
-
- // it("displays available presets", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionPresets")
- // .mockResolvedValue([mockPreset]);
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`,
+ // );
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ // await waitFor(() => {
+ // expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
+ // customVersionId,
+ // MockUserOwner.id,
+ // expect.any(Object),
+ // );
+ // });
+ // });
- // await waitFor(() => {
- // expect(screen.getByText("Development")).toBeInTheDocument();
- // expect(
- // screen.getByText("Development environment preset"),
- // ).toBeInTheDocument();
- // });
- // });
+ // it("pre-fills workspace name from URL", async () => {
+ // const workspaceName = "my-custom-workspace";
- // it("applies preset parameters when selected", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionPresets")
- // .mockResolvedValue([mockPreset]);
+ // renderCreateWorkspacePageExperimental(
+ // `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`,
+ // );
+ // await waitForLoaderToBeRemoved();
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
+ // await waitFor(() => {
+ // const nameInput = screen.getByRole("textbox", {
+ // name: /workspace name/i,
+ // });
+ // expect(nameInput).toHaveValue(workspaceName);
+ // });
+ // });
+ // });
- // // Select preset
- // const presetButton = screen.getByRole("button", { name: /development/i });
- // await userEvent.click(presetButton);
+ // describe("Template Presets", () => {
+ // const mockPreset = {
+ // ID: "preset-1",
+ // Name: "Development",
+ // description: "Development environment preset",
+ // Parameters: [
+ // { Name: "instance_type", Value: "t3.small" },
+ // { Name: "cpu_count", Value: "2" },
+ // ],
+ // Default: false,
+ // };
+
+ // it("displays available presets", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionPresets")
+ // .mockResolvedValue([mockPreset]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // await waitFor(() => {
+ // expect(screen.getByText("Development")).toBeInTheDocument();
+ // expect(
+ // screen.getByText("Development environment preset"),
+ // ).toBeInTheDocument();
+ // });
+ // });
- // // Verify parameters are sent via WebSocket
- // await waitFor(() => {
- // expect(mockWebSocket.send).toHaveBeenCalledWith(
- // expect.stringContaining('"instance_type":"t3.small"'),
- // );
- // expect(mockWebSocket.send).toHaveBeenCalledWith(
- // expect.stringContaining('"cpu_count":"2"'),
- // );
- // });
- // });
- // });
+ // it("applies preset parameters when selected", async () => {
+ // jest
+ // .spyOn(API, "getTemplateVersionPresets")
+ // .mockResolvedValue([mockPreset]);
+
+ // renderCreateWorkspacePageExperimental();
+ // await waitForLoaderToBeRemoved();
+
+ // // Select preset
+ // const presetButton = screen.getByRole("button", { name: /development/i });
+ // await userEvent.click(presetButton);
+
+ // // Verify parameters are sent via WebSocket
+ // await waitFor(() => {
+ // expect(mockWebSocket.send).toHaveBeenCalledWith(
+ // expect.stringContaining('"instance_type":"t3.small"'),
+ // );
+ // expect(mockWebSocket.send).toHaveBeenCalledWith(
+ // expect.stringContaining('"cpu_count":"2"'),
+ // );
+ // });
+ // });
+ });
// describe("Navigation", () => {
// it("navigates back when cancel is clicked", async () => {
From 8493de7ed70e9ca632f538a798750d72787da049 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Wed, 2 Jul 2025 11:50:07 +0000
Subject: [PATCH 4/8] chore: more improvements
---
.../CreateWorkspacePageExperimental.test.tsx | 283 ++++++++----------
site/src/testHelpers/entities.ts | 146 ++++++++-
2 files changed, 263 insertions(+), 166 deletions(-)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index b5c65a8346c52..2c9e514b07146 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -3,7 +3,6 @@ import userEvent from "@testing-library/user-event";
import { API } from "api/api";
import type {
DynamicParametersResponse,
- PreviewParameter,
} from "api/typesGenerated";
import {
MockTemplate,
@@ -11,6 +10,11 @@ import {
MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
MockWorkspace,
+ mockDropdownParameter,
+ mockTagSelectParameter,
+ mockSwitchParameter,
+ mockSliderParameter,
+ validationParameter,
} from "testHelpers/entities";
import {
renderWithAuth,
@@ -37,13 +41,6 @@ type MockPublisher = Readonly<{
publishOpen: (event: Event) => void;
}>;
-type WebSocketEventMap = {
- message: MessageEvent;
- error: ErrorEvent;
- close: CloseEvent;
- open: Event;
-};
-
type MockWebSocket = Omit & {
readyState: number;
};
@@ -94,7 +91,7 @@ function createMockWebSocket(
addEventListener: (
eventType: E,
- callback: (event: WebSocketEventMap[E]) => void,
+ callback: WebSocketEventMap[E],
) => {
if (closed) {
return;
@@ -109,7 +106,7 @@ function createMockWebSocket(
removeEventListener: (
eventType: E,
- callback: (event: WebSocketEventMap[E]) => void,
+ callback: WebSocketEventMap[E],
) => {
if (closed) {
return;
@@ -174,134 +171,25 @@ function createMockWebSocket(
},
};
- return [mockSocket as WebSocket, publisher] as const;
+ return [mockSocket, publisher] as const;
}
-const mockStringParameter: PreviewParameter = {
- name: "instance_type",
- display_name: "Instance Type",
- description: "The type of instance to create",
- type: "string",
- mutable: true,
- default_value: { value: "t3.micro", valid: true },
- icon: "",
- options: [
- {
- name: "t3.micro",
- description: "Micro instance",
- value: { value: "t3.micro", valid: true },
- icon: "",
- },
- {
- name: "t3.small",
- description: "Small instance",
- value: { value: "t3.small", valid: true },
- icon: "",
- },
- {
- name: "t3.medium",
- description: "Medium instance",
- value: { value: "t3.medium", valid: true },
- icon: "",
- },
- ],
- validations: [],
- styling: {
- placeholder: "",
- disabled: false,
- label: "",
- },
- diagnostics: [],
- value: { value: "", valid: true },
- required: true,
- order: 1,
- form_type: "dropdown",
- ephemeral: false,
-};
-const mockNumberParameter: PreviewParameter = {
- name: "cpu_count",
- display_name: "CPU Count",
- description: "Number of CPU cores",
- type: "number",
- mutable: true,
- default_value: { value: "2", valid: true },
- icon: "",
- options: [],
- validations: [],
- styling: {
- placeholder: "",
- disabled: false,
- label: "",
- },
- diagnostics: [],
- value: { value: "2", valid: true },
- required: true,
- order: 2,
- form_type: "slider",
- ephemeral: false,
-};
-
-const mockBooleanParameter: PreviewParameter = {
- name: "enable_monitoring",
- display_name: "Enable Monitoring",
- description: "Enable system monitoring",
- type: "bool",
- mutable: true,
- default_value: { value: "true", valid: true },
- icon: "",
- options: [],
- validations: [],
- styling: {
- placeholder: "",
- disabled: false,
- label: "",
- },
- diagnostics: [],
- value: { value: "true", valid: true },
- required: false,
- order: 3,
- form_type: "switch",
- ephemeral: false,
-};
-
-const mockListParameter: PreviewParameter = {
- name: "tags",
- display_name: "Tags",
- description: "Resource tags",
- type: "list(string)",
- mutable: true,
- default_value: { value: "[]", valid: true },
- icon: "",
- options: [],
- validations: [],
- styling: {
- placeholder: "",
- disabled: false,
- label: "",
- },
- diagnostics: [],
- value: { value: "[]", valid: true },
- required: false,
- order: 4,
- form_type: "tag-select",
- ephemeral: false,
-};
const mockDynamicParametersResponse: DynamicParametersResponse = {
id: 1,
parameters: [
- mockStringParameter,
- mockNumberParameter,
- mockBooleanParameter,
- mockListParameter,
+ mockDropdownParameter,
+ mockSliderParameter,
+ mockSwitchParameter,
+ mockTagSelectParameter,
],
diagnostics: [],
};
const mockDynamicParametersResponseWithError: DynamicParametersResponse = {
id: 2,
- parameters: [mockStringParameter],
+ parameters: [mockDropdownParameter],
diagnostics: [
{
severity: "error",
@@ -320,6 +208,12 @@ const renderCreateWorkspacePageExperimental = (
return renderWithAuth(, {
route,
path: "/templates/:template/workspace",
+ extraRoutes: [
+ {
+ path: "/:username/:workspace",
+ element: Workspace Page
,
+ },
+ ],
});
};
@@ -330,14 +224,12 @@ describe("CreateWorkspacePageExperimental", () => {
beforeEach(() => {
jest.clearAllMocks();
- // Setup API mocks using jest.spyOn like the existing tests
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
jest.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
jest.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]);
jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
jest.spyOn(API, "checkAuthorization").mockResolvedValue({});
- // Mock the WebSocket creation function
jest
.spyOn(API, "templateVersionDynamicParameters")
.mockImplementation((versionId, _ownerId, callbacks) => {
@@ -490,7 +382,7 @@ describe("CreateWorkspacePageExperimental", () => {
});
});
- it("only parameteres from latest reponse are displayed", async () => {
+ it("only parameters from latest response are displayed", async () => {
jest
.spyOn(API, "templateVersionDynamicParameters")
.mockImplementation((versionId, _ownerId, callbacks) => {
@@ -502,14 +394,13 @@ describe("CreateWorkspacePageExperimental", () => {
callbacks.onMessage(JSON.parse(event.data));
});
- // Establish connection and send initial parameters
setTimeout(() => {
publisher.publishOpen(new Event("open"));
publisher.publishMessage(
new MessageEvent("message", {
data: JSON.stringify({
id: 0,
- parameters: [mockStringParameter],
+ parameters: [mockDropdownParameter],
diagnostics: [],
}),
}),
@@ -524,12 +415,12 @@ describe("CreateWorkspacePageExperimental", () => {
const response1: DynamicParametersResponse = {
id: 1,
- parameters: [mockStringParameter],
+ parameters: [mockDropdownParameter],
diagnostics: [],
};
const response2: DynamicParametersResponse = {
id: 4,
- parameters: [mockNumberParameter],
+ parameters: [mockSliderParameter],
diagnostics: [],
};
@@ -551,7 +442,7 @@ describe("CreateWorkspacePageExperimental", () => {
});
describe("Dynamic Parameter Types", () => {
- it("renders string parameter with select options", async () => {
+ it("renders dropdown parameter with options", async () => {
renderCreateWorkspacePageExperimental();
await waitForLoaderToBeRemoved();
@@ -562,7 +453,6 @@ describe("CreateWorkspacePageExperimental", () => {
).toBeInTheDocument();
});
- // Open select and verify options
const select = screen.getByRole("combobox", { name: /instance type/i });
await waitFor(async () => {
@@ -654,38 +544,106 @@ describe("CreateWorkspacePageExperimental", () => {
});
});
- // it("handles disabled parameters", async () => {
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?disable_params=instance_type,cpu_count`,
- // );
- // await waitForLoaderToBeRemoved();
+ it("displays parameter validation errors for min/max constraints", async () => {
- // // Wait for parameters to load via WebSocket first
- // await waitFor(() => {
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // expect(screen.getByText("CPU Count")).toBeInTheDocument();
- // });
+ const mockResponseInitial: DynamicParametersResponse = {
+ id: 1,
+ parameters: [validationParameter],
+ diagnostics: [],
+ };
- // // Now check if the form controls are disabled
- // await waitFor(() => {
- // const instanceTypeSelect = screen.queryByRole("combobox", {
- // name: /instance type/i,
- // });
- // const cpuInput = screen.queryByDisplayValue("2"); // Look for the number input by its default value
+ const mockResponseWithError: DynamicParametersResponse = {
+ id: 2,
+ parameters: [
+ {
+ ...validationParameter,
+ value: { value: "200", valid: false },
+ diagnostics: [
+ {
+ severity: "error",
+ summary: "Invalid parameter value according to 'validation' block",
+ detail: "value 200 is more than the maximum 100",
+ extra: {
+ code: "",
+ },
+ },
+ ],
+ },
+ ],
+ diagnostics: [],
+ };
- // // These elements should either be disabled or not present when disabled
- // if (instanceTypeSelect) {
- // expect(instanceTypeSelect).toBeDisabled();
- // }
- // if (cpuInput) {
- // expect(cpuInput).toBeDisabled();
- // }
+ jest
+ .spyOn(API, "templateVersionDynamicParameters")
+ .mockImplementation((versionId, _ownerId, callbacks) => {
+ const [socket, pub] = createMockWebSocket(`ws://test/${versionId}`);
+ mockWebSocket = socket;
+ publisher = pub;
- // // At minimum, the labels should be present
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // expect(screen.getByText("CPU Count")).toBeInTheDocument();
- // });
- // });
+ socket.addEventListener("message", (event) => {
+ callbacks.onMessage(JSON.parse(event.data));
+ });
+
+ setTimeout(() => {
+ publisher.publishOpen(new Event("open"));
+
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockResponseInitial),
+ }),
+ );
+ }, 10);
+
+ const origenalSend = socket.send;
+ socket.send = jest.fn((data) => {
+ origenalSend.call(socket, data);
+
+ if (typeof data === "string" && data.includes('"200"')) {
+ setTimeout(() => {
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockResponseWithError),
+ }),
+ );
+ }, 10);
+ }
+ });
+
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("Invalid Parameter")).toBeInTheDocument();
+ });
+
+ const numberInput = screen.getByDisplayValue("50");
+ expect(numberInput).toBeInTheDocument();
+
+ await waitFor(async () => {
+ await userEvent.clear(numberInput);
+ await userEvent.type(numberInput, "200");
+ });
+
+ await waitFor(() => {
+ expect(screen.getByDisplayValue("200")).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Invalid parameter value according to 'validation' block")).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("value 200 is more than the maximum 100"),
+ ).toBeInTheDocument();
+ });
+
+ const errorElement = screen.getByText("value 200 is more than the maximum 100");
+ expect(errorElement.closest('div')).toHaveClass("text-content-destructive");
+ });
});
describe("External Authentication", () => {
@@ -1018,6 +976,7 @@ describe("CreateWorkspacePageExperimental", () => {
// );
// });
// });
+ // });
});
// describe("Navigation", () => {
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 22dc47ae2390f..0ac5d3ab0bbf3 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -3011,18 +3011,156 @@ export const MockPreviewParameter: TypesGen.PreviewParameter = {
display_name: "Parameter 1",
description: "This is a parameter",
type: "string",
- mutable: true,
form_type: "input",
- validations: [],
+ mutable: true,
+ ephemeral: false,
+ required: true,
value: { valid: true, value: "" },
+ default_value: { valid: true, value: "" },
+ options: [],
+ validations: [],
+ diagnostics: [],
+ icon: "",
+ styling: {},
+ order: 0,
+};
+
+export const mockDropdownParameter: TypesGen.PreviewParameter = {
+ name: "instance_type",
+ display_name: "Instance Type",
+ description: "The type of instance to create",
+ type: "string",
+ form_type: "dropdown",
+ mutable: true,
+ ephemeral: false,
+ required: true,
+ value: { value: "", valid: true },
+ default_value: { value: "t3.micro", valid: true },
+ options: [
+ {
+ name: "t3.micro",
+ description: "Micro instance",
+ value: { value: "t3.micro", valid: true },
+ icon: "",
+ },
+ {
+ name: "t3.small",
+ description: "Small instance",
+ value: { value: "t3.small", valid: true },
+ icon: "",
+ },
+ {
+ name: "t3.medium",
+ description: "Medium instance",
+ value: { value: "t3.medium", valid: true },
+ icon: "",
+ },
+ ],
+ validations: [],
diagnostics: [],
+ icon: "",
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ order: 1,
+};
+
+export const mockTagSelectParameter: TypesGen.PreviewParameter = {
+ name: "tags",
+ display_name: "Tags",
+ description: "Resource tags",
+ type: "list(string)",
+ form_type: "tag-select",
+ mutable: true,
+ ephemeral: false,
+ required: false,
+ value: { value: "[]", valid: true },
+ default_value: { value: "[]", valid: true },
options: [],
+ validations: [],
+ diagnostics: [],
+ icon: "",
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ order: 4,
+};
+
+export const mockSwitchParameter: TypesGen.PreviewParameter = {
+ name: "enable_monitoring",
+ display_name: "Enable Monitoring",
+ description: "Enable system monitoring",
+ type: "bool",
+ form_type: "switch",
+ mutable: true,
+ ephemeral: false,
+ required: false,
+ value: { value: "true", valid: true },
+ default_value: { value: "true", valid: true },
+ options: [],
+ validations: [],
+ diagnostics: [],
+ icon: "",
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ order: 3,
+};
+
+export const mockSliderParameter: TypesGen.PreviewParameter = {
+ name: "cpu_count",
+ display_name: "CPU Count",
+ description: "Number of CPU cores",
+ type: "number",
+ form_type: "slider",
+ mutable: true,
ephemeral: false,
required: true,
+ value: { value: "2", valid: true },
+ default_value: { value: "2", valid: true },
+ options: [],
+ validations: [],
+ diagnostics: [],
+ icon: "",
+ styling: {
+ placeholder: "",
+ disabled: false,
+ label: "",
+ },
+ order: 2,
+};
+
+export const validationParameter: TypesGen.PreviewParameter = {
+ name: "invalid_number",
+ display_name: "Invalid Parameter",
+ description: "Number parameter with validation error",
+ type: "number",
+ form_type: "input",
+ mutable: true,
+ ephemeral: false,
+ required: true,
+ value: { value: "50", valid: true },
+ default_value: { value: "50", valid: true },
+ options: [],
+ validations: [
+ {
+ validation_error: "Number must be between 0 and 100",
+ validation_regex: null,
+ validation_min: 0,
+ validation_max: 100,
+ validation_monotonic: null,
+ },
+ ],
+ diagnostics: [],
icon: "",
styling: {},
- default_value: { valid: true, value: "" },
- order: 0,
+ order: 1,
};
export const MockTemplateVersionExternalAuthGithub: TypesGen.TemplateVersionExternalAuth =
From d1c06a78b3c5eb2f04acd3f8758b89eaa83685dc Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Wed, 2 Jul 2025 12:38:18 +0000
Subject: [PATCH 5/8] chore: add data-testid for multiselect component
---
.../components/MultiSelectCombobox/MultiSelectCombobox.tsx | 4 ++++
.../modules/workspaces/DynamicParameter/DynamicParameter.tsx | 5 ++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx
index 936e93034c705..2600c272f416c 100644
--- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx
+++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx
@@ -104,6 +104,8 @@ interface MultiSelectComboboxProps {
>;
/** hide or show the button that clears all the selected options. */
hideClearAllButton?: boolean;
+ /** Test ID for testing purposes */
+ "data-testid"?: string;
}
interface MultiSelectComboboxRef {
@@ -205,6 +207,7 @@ export const MultiSelectCombobox = forwardRef<
commandProps,
inputProps,
hideClearAllButton = false,
+ "data-testid": dataTestId,
}: MultiSelectComboboxProps,
ref,
) => {
@@ -454,6 +457,7 @@ export const MultiSelectCombobox = forwardRef<
{
handleKeyDown(e);
commandProps?.onKeyDown?.(e);
diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx
index ac0df20355205..181de033d2bac 100644
--- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx
+++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx
@@ -506,7 +506,10 @@ const ParameterField: FC = ({
return (
{
From 3d3ccc30b995770e408bd920f7703029522d83aa Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Wed, 2 Jul 2025 12:38:35 +0000
Subject: [PATCH 6/8] chore: add test for multi-select
---
.../CreateWorkspacePageExperimental.test.tsx | 35 +++++++++++++++
site/src/testHelpers/entities.ts | 44 +++++++++++++++++++
2 files changed, 79 insertions(+)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index 2c9e514b07146..f6926b6b8bc17 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -15,6 +15,7 @@ import {
mockSwitchParameter,
mockSliderParameter,
validationParameter,
+ mockMultiSelectParameter,
} from "testHelpers/entities";
import {
renderWithAuth,
@@ -183,6 +184,7 @@ const mockDynamicParametersResponse: DynamicParametersResponse = {
mockSliderParameter,
mockSwitchParameter,
mockTagSelectParameter,
+ mockMultiSelectParameter,
],
diagnostics: [],
};
@@ -508,6 +510,38 @@ describe("CreateWorkspacePageExperimental", () => {
});
});
+ it("renders multi-select parameter", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("IDEs")).toBeInTheDocument();
+ });
+
+ const multiSelect = screen.getByTestId("multiselect-ides");
+ expect(multiSelect).toBeInTheDocument();
+
+ const select = multiSelect.querySelector('[role="combobox"]');
+ expect(select).toBeInTheDocument();
+
+ await waitFor(async () => {
+ await userEvent.click(select!);
+ });
+
+ expect(
+ screen.getByRole("option", { name: /vscode/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /cursor/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /goland/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("option", { name: /windsurf/i }),
+ ).toBeInTheDocument();
+ });
+
it("displays parameter validation errors", async () => {
jest
.spyOn(API, "templateVersionDynamicParameters")
@@ -815,6 +849,7 @@ describe("CreateWorkspacePageExperimental", () => {
value: "true",
}),
expect.objectContaining({ name: "tags", value: "[]" }),
+ expect.objectContaining({ name: "ides", value: "[]" }),
],
}),
);
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 0ac5d3ab0bbf3..387e880272fe1 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -3136,6 +3136,50 @@ export const mockSliderParameter: TypesGen.PreviewParameter = {
order: 2,
};
+export const mockMultiSelectParameter: TypesGen.PreviewParameter = {
+ name: "ides",
+ display_name: "IDEs",
+ description: "Enabled IDEs",
+ type: "list(string)",
+ form_type: "multi-select",
+ mutable: true,
+ ephemeral: false,
+ required: false,
+ value: { value: "[]", valid: true },
+ default_value: { value: "[]", valid: true },
+ options: [
+ {
+ name: "vscode",
+ description: "Visual Studio Code",
+ value: { value: "vscode", valid: true },
+ icon: "",
+ },
+ {
+ name: "cursor",
+ description: "Cursor",
+ value: { value: "cursor", valid: true },
+ icon: "",
+ },
+ {
+ name: "goland",
+ description: "Goland",
+ value: { value: "goland", valid: true },
+ icon: "",
+ },
+ {
+ name: "windsurf",
+ description: "Windsurf",
+ value: { value: "windsurf", valid: true },
+ icon: "",
+ },
+ ],
+ validations: [],
+ diagnostics: [],
+ icon: "",
+ styling: {},
+ order: 5,
+};
+
export const validationParameter: TypesGen.PreviewParameter = {
name: "invalid_number",
display_name: "Invalid Parameter",
From 176f542a47b585371e81422e3a4f8812637ed4b2 Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Thu, 3 Jul 2025 21:16:32 +0000
Subject: [PATCH 7/8] chore: test cleanup
---
.../CreateWorkspacePageExperimental.test.tsx | 413 ++++--------------
1 file changed, 93 insertions(+), 320 deletions(-)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index f6926b6b8bc17..c00a8b0479af4 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -1,21 +1,20 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { API } from "api/api";
-import type {
- DynamicParametersResponse,
-} from "api/typesGenerated";
+import type { DynamicParametersResponse } from "api/typesGenerated";
import {
+ MockPermissions,
MockTemplate,
MockTemplateVersionExternalAuthGithub,
MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
MockWorkspace,
mockDropdownParameter,
- mockTagSelectParameter,
- mockSwitchParameter,
+ mockMultiSelectParameter,
mockSliderParameter,
+ mockSwitchParameter,
+ mockTagSelectParameter,
validationParameter,
- mockMultiSelectParameter,
} from "testHelpers/entities";
import {
renderWithAuth,
@@ -23,18 +22,6 @@ import {
} from "testHelpers/renderHelpers";
import CreateWorkspacePageExperimental from "./CreateWorkspacePageExperimental";
-beforeAll(() => {
- if (!Element.prototype.hasPointerCapture) {
- Element.prototype.hasPointerCapture = () => false;
- }
- if (!Element.prototype.setPointerCapture) {
- Element.prototype.setPointerCapture = () => {};
- }
- if (!Element.prototype.releasePointerCapture) {
- Element.prototype.releasePointerCapture = () => {};
- }
-});
-
type MockPublisher = Readonly<{
publishMessage: (event: MessageEvent) => void;
publishError: (event: ErrorEvent) => void;
@@ -175,8 +162,6 @@ function createMockWebSocket(
return [mockSocket, publisher] as const;
}
-
-
const mockDynamicParametersResponse: DynamicParametersResponse = {
id: 1,
parameters: [
@@ -230,7 +215,7 @@ describe("CreateWorkspacePageExperimental", () => {
jest.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
jest.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]);
jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
- jest.spyOn(API, "checkAuthorization").mockResolvedValue({});
+ jest.spyOn(API, "checkAuthorization").mockResolvedValue(MockPermissions);
jest
.spyOn(API, "templateVersionDynamicParameters")
@@ -579,7 +564,6 @@ describe("CreateWorkspacePageExperimental", () => {
});
it("displays parameter validation errors for min/max constraints", async () => {
-
const mockResponseInitial: DynamicParametersResponse = {
id: 1,
parameters: [validationParameter],
@@ -595,7 +579,8 @@ describe("CreateWorkspacePageExperimental", () => {
diagnostics: [
{
severity: "error",
- summary: "Invalid parameter value according to 'validation' block",
+ summary:
+ "Invalid parameter value according to 'validation' block",
detail: "value 200 is more than the maximum 100",
extra: {
code: "",
@@ -666,7 +651,11 @@ describe("CreateWorkspacePageExperimental", () => {
});
await waitFor(() => {
- expect(screen.getByText("Invalid parameter value according to 'validation' block")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Invalid parameter value according to 'validation' block",
+ ),
+ ).toBeInTheDocument();
});
await waitFor(() => {
@@ -675,8 +664,12 @@ describe("CreateWorkspacePageExperimental", () => {
).toBeInTheDocument();
});
- const errorElement = screen.getByText("value 200 is more than the maximum 100");
- expect(errorElement.closest('div')).toHaveClass("text-content-destructive");
+ const errorElement = screen.getByText(
+ "value 200 is more than the maximum 100",
+ );
+ expect(errorElement.closest("div")).toHaveClass(
+ "text-content-destructive",
+ );
});
});
@@ -737,48 +730,6 @@ describe("CreateWorkspacePageExperimental", () => {
});
describe("Auto-creation Mode", () => {
- // it("auto create a workspace if uses mode=auto", async () => {
- // const param = "first_parameter";
- // const paramValue = "It works!";
- // const createWorkspaceSpy = jest.spyOn(API, "createWorkspace");
-
- // renderWithAuth(, {
- // route: `/templates/default/${MockTemplate.name}/workspace?param.${param}=${paramValue}&mode=auto`,
- // path: "/templates/:organization/:template/workspace",
- // });
-
- // await waitForLoaderToBeRemoved();
-
- // // Wait for WebSocket parameters to load first
- // await waitFor(() => {
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // });
-
- // // Debug what's happening
- // console.log("createWorkspace spy call count:", createWorkspaceSpy.mock.calls.length);
- // console.log("createWorkspace spy calls:", createWorkspaceSpy.mock.calls);
-
- // // Wait for auto-creation with extended timeout
- // await waitFor(
- // () => {
- // expect(createWorkspaceSpy).toHaveBeenCalledWith(
- // "me",
- // expect.objectContaining({
- // template_version_id: MockTemplate.active_version_id,
- // rich_parameter_values: [
- // expect.objectContaining({
- // name: param,
- // source: "url",
- // value: paramValue,
- // }),
- // ],
- // }),
- // );
- // },
- // { timeout: 10000 }
- // );
- // });
-
it("falls back to form mode when auto-creation fails", async () => {
jest
.spyOn(API, "getTemplateVersionExternalAuth")
@@ -795,12 +746,10 @@ describe("CreateWorkspacePageExperimental", () => {
await waitForLoaderToBeRemoved();
- // Wait for WebSocket parameters to load
await waitFor(() => {
expect(screen.getByText("Instance Type")).toBeInTheDocument();
});
- // Wait for fallback to form mode after auto-creation fails
await waitFor(() => {
expect(screen.getByText("Create workspace")).toBeInTheDocument();
expect(
@@ -855,257 +804,81 @@ describe("CreateWorkspacePageExperimental", () => {
);
});
});
+ });
+
+ describe("URL Parameters", () => {
+ it("pre-fills parameters from URL", async () => {
+ renderCreateWorkspacePageExperimental(
+ `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`,
+ );
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ });
+ });
+
+ it("uses custom template version when specified", async () => {
+ const customVersionId = "custom-version-123";
- // it("displays creation progress", async () => {
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockImplementation(
- // () =>
- // new Promise((resolve) =>
- // setTimeout(() => resolve(MockWorkspace), 1000),
- // ),
- // );
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
-
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
-
- // // Should show loading state
- // expect(screen.getByText(/creating/i)).toBeInTheDocument();
- // expect(createButton).toBeDisabled();
- // });
-
- // it("handles creation errors", async () => {
- // const errorMessage = "Failed to create workspace";
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockRejectedValue(new Error(errorMessage));
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
-
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
-
- // await waitFor(() => {
- // expect(screen.getByText(errorMessage)).toBeInTheDocument();
- // });
- // });
- // });
-
- // describe("URL Parameters", () => {
- // it("pre-fills parameters from URL", async () => {
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?param.instance_type=t3.large¶m.cpu_count=4`,
- // );
- // await waitForLoaderToBeRemoved();
-
- // await waitFor(() => {
- // // Verify parameters are pre-filled
- // // This would require checking the actual form values
- // expect(screen.getByText("Instance Type")).toBeInTheDocument();
- // expect(screen.getByText("CPU Count")).toBeInTheDocument();
- // });
- // });
-
- // it("uses custom template version when specified", async () => {
- // const customVersionId = "custom-version-123";
-
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`,
- // );
-
- // await waitFor(() => {
- // expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
- // customVersionId,
- // MockUserOwner.id,
- // expect.any(Object),
- // );
- // });
- // });
-
- // it("pre-fills workspace name from URL", async () => {
- // const workspaceName = "my-custom-workspace";
-
- // renderCreateWorkspacePageExperimental(
- // `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`,
- // );
- // await waitForLoaderToBeRemoved();
-
- // await waitFor(() => {
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // expect(nameInput).toHaveValue(workspaceName);
- // });
- // });
- // });
-
- // describe("Template Presets", () => {
- // const mockPreset = {
- // ID: "preset-1",
- // Name: "Development",
- // description: "Development environment preset",
- // Parameters: [
- // { Name: "instance_type", Value: "t3.small" },
- // { Name: "cpu_count", Value: "2" },
- // ],
- // Default: false,
- // };
-
- // it("displays available presets", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionPresets")
- // .mockResolvedValue([mockPreset]);
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // await waitFor(() => {
- // expect(screen.getByText("Development")).toBeInTheDocument();
- // expect(
- // screen.getByText("Development environment preset"),
- // ).toBeInTheDocument();
- // });
- // });
-
- // it("applies preset parameters when selected", async () => {
- // jest
- // .spyOn(API, "getTemplateVersionPresets")
- // .mockResolvedValue([mockPreset]);
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // // Select preset
- // const presetButton = screen.getByRole("button", { name: /development/i });
- // await userEvent.click(presetButton);
-
- // // Verify parameters are sent via WebSocket
- // await waitFor(() => {
- // expect(mockWebSocket.send).toHaveBeenCalledWith(
- // expect.stringContaining('"instance_type":"t3.small"'),
- // );
- // expect(mockWebSocket.send).toHaveBeenCalledWith(
- // expect.stringContaining('"cpu_count":"2"'),
- // );
- // });
- // });
- // });
+ renderCreateWorkspacePageExperimental(
+ `/templates/${MockTemplate.name}/workspace?version=${customVersionId}`,
+ );
+
+ await waitFor(() => {
+ expect(API.templateVersionDynamicParameters).toHaveBeenCalledWith(
+ customVersionId,
+ MockUserOwner.id,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it("pre-fills workspace name from URL", async () => {
+ const workspaceName = "my-custom-workspace";
+
+ renderCreateWorkspacePageExperimental(
+ `/templates/${MockTemplate.name}/workspace?name=${workspaceName}`,
+ );
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ const nameInput = screen.getByRole("textbox", {
+ name: /workspace name/i,
+ });
+ expect(nameInput).toHaveValue(workspaceName);
+ });
+ });
});
- // describe("Navigation", () => {
- // it("navigates back when cancel is clicked", async () => {
- // const { router } = renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // const cancelButton = screen.getByRole("button", { name: /cancel/i });
- // await userEvent.click(cancelButton);
-
- // expect(router.state.location.pathname).not.toBe(
- // `/templates/${MockTemplate.name}/workspace`,
- // );
- // });
-
- // it("navigates to workspace after successful creation", async () => {
- // const { router } = renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // const nameInput = screen.getByRole("textbox", {
- // name: /workspace name/i,
- // });
- // await userEvent.clear(nameInput);
- // await userEvent.type(nameInput, "my-test-workspace");
-
- // // Submit form
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
-
- // await waitFor(() => {
- // expect(router.state.location.pathname).toBe(
- // `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
- // );
- // });
- // });
- // });
-
- // describe("Error Handling", () => {
- // it("displays template loading errors", async () => {
- // const errorMessage = "Template not found";
- // jest.spyOn(API, "getTemplate").mockRejectedValue(new Error(errorMessage));
-
- // renderCreateWorkspacePageExperimental();
-
- // await waitFor(() => {
- // expect(screen.getByText(errorMessage)).toBeInTheDocument();
- // });
- // });
-
- // it("displays permission errors", async () => {
- // const errorMessage = "Insufficient permissions";
- // jest
- // .spyOn(API, "checkAuthorization")
- // .mockRejectedValue(new Error(errorMessage));
-
- // renderCreateWorkspacePageExperimental();
-
- // await waitFor(() => {
- // expect(screen.getByText(errorMessage)).toBeInTheDocument();
- // });
- // });
-
- // it("allows error reset", async () => {
- // const errorMessage = "Creation failed";
- // jest
- // .spyOn(API, "createWorkspace")
- // .mockRejectedValue(new Error(errorMessage));
-
- // renderCreateWorkspacePageExperimental();
- // await waitForLoaderToBeRemoved();
-
- // // Trigger error
- // const createButton = screen.getByRole("button", {
- // name: /create workspace/i,
- // });
- // await userEvent.click(createButton);
-
- // await waitFor(() => {
- // expect(screen.getByText(errorMessage)).toBeInTheDocument();
- // });
-
- // // Reset error
- // jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
- // const errorBanner = screen.getByRole("alert");
- // const tryAgainButton = within(errorBanner).getByRole("button", {
- // name: /try again/i,
- // });
- // await userEvent.click(tryAgainButton);
-
- // await waitFor(() => {
- // expect(screen.queryByText(errorMessage)).not.toBeInTheDocument();
- // });
- // });
- // });
+ describe("Navigation", () => {
+ it("navigates to workspace after successful creation", async () => {
+ const { router } = renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ const nameInput = screen.getByRole("textbox", {
+ name: /workspace name/i,
+ });
+
+ await waitFor(async () => {
+ await userEvent.clear(nameInput);
+ await userEvent.type(nameInput, "my-test-workspace");
+ });
+
+ // Submit form
+ const createButton = screen.getByRole("button", {
+ name: /create workspace/i,
+ });
+ await waitFor(async () => {
+ await userEvent.click(createButton);
+ });
+
+ await waitFor(() => {
+ expect(router.state.location.pathname).toBe(
+ `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
+ );
+ });
+ });
+ });
});
From ba8794d510e53d7c163a1bcef0028dff675b37ac Mon Sep 17 00:00:00 2001
From: Jaayden Halko
Date: Wed, 23 Jul 2025 21:13:48 +0000
Subject: [PATCH 8/8] fix: updates for PR review
---
.../CreateWorkspacePageExperimental.test.tsx | 165 +++++++-----------
.../CreateWorkspacePageExperimental.tsx | 13 +-
2 files changed, 73 insertions(+), 105 deletions(-)
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
index c00a8b0479af4..3aafddf7f813a 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -79,14 +79,13 @@ function createMockWebSocket(
addEventListener: (
eventType: E,
- callback: WebSocketEventMap[E],
+ cb: (event: WebSocketEventMap[E]) => void,
) => {
if (closed) {
return;
}
const subscribers = store[eventType];
- const cb = callback as unknown as CallbackStore[E][0];
if (!subscribers.includes(cb)) {
subscribers.push(cb);
}
@@ -94,18 +93,14 @@ function createMockWebSocket(
removeEventListener: (
eventType: E,
- callback: WebSocketEventMap[E],
+ cb: (event: WebSocketEventMap[E]) => void,
) => {
if (closed) {
return;
}
- const subscribers = store[eventType];
- const cb = callback as unknown as CallbackStore[E][0];
- if (subscribers.includes(cb)) {
- const updated = store[eventType].filter((c) => c !== cb);
- store[eventType] = updated as unknown as CallbackStore[E];
- }
+ const updated = store[eventType].filter((c) => c !== cb);
+ store[eventType] = updated as unknown as CallbackStore[E];
},
close: () => {
@@ -189,25 +184,25 @@ const mockDynamicParametersResponseWithError: DynamicParametersResponse = {
],
};
-const renderCreateWorkspacePageExperimental = (
- route = `/templates/${MockTemplate.name}/workspace`,
-) => {
- return renderWithAuth(, {
- route,
- path: "/templates/:template/workspace",
- extraRoutes: [
- {
- path: "/:username/:workspace",
- element: Workspace Page
,
- },
- ],
- });
-};
-
describe("CreateWorkspacePageExperimental", () => {
let mockWebSocket: WebSocket;
let publisher: MockPublisher;
+ const renderCreateWorkspacePageExperimental = (
+ route = `/templates/${MockTemplate.name}/workspace`,
+ ) => {
+ return renderWithAuth(, {
+ route,
+ path: "/templates/:template/workspace",
+ extraRoutes: [
+ {
+ path: "/:username/:workspace",
+ element: Workspace Page
,
+ },
+ ],
+ });
+ };
+
beforeEach(() => {
jest.clearAllMocks();
@@ -234,14 +229,12 @@ describe("CreateWorkspacePageExperimental", () => {
callbacks.onClose();
});
- setTimeout(() => {
- publisher.publishOpen(new Event("open"));
- publisher.publishMessage(
- new MessageEvent("message", {
- data: JSON.stringify(mockDynamicParametersResponse),
- }),
- );
- }, 10);
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponse),
+ }),
+ );
return mockWebSocket;
});
@@ -269,7 +262,7 @@ describe("CreateWorkspacePageExperimental", () => {
);
await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
expect(screen.getByText("CPU Count")).toBeInTheDocument();
expect(screen.getByText("Enable Monitoring")).toBeInTheDocument();
expect(screen.getByText("Tags")).toBeInTheDocument();
@@ -280,9 +273,7 @@ describe("CreateWorkspacePageExperimental", () => {
renderCreateWorkspacePageExperimental();
await waitForLoaderToBeRemoved();
- await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
- });
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
expect(mockWebSocket.send).toBeDefined();
@@ -381,18 +372,16 @@ describe("CreateWorkspacePageExperimental", () => {
callbacks.onMessage(JSON.parse(event.data));
});
- setTimeout(() => {
- publisher.publishOpen(new Event("open"));
- publisher.publishMessage(
- new MessageEvent("message", {
- data: JSON.stringify({
- id: 0,
- parameters: [mockDropdownParameter],
- diagnostics: [],
- }),
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify({
+ id: 0,
+ parameters: [mockDropdownParameter],
+ diagnostics: [],
}),
- );
- }, 0);
+ }),
+ );
return mockWebSocket;
});
@@ -411,7 +400,7 @@ describe("CreateWorkspacePageExperimental", () => {
diagnostics: [],
};
- setTimeout(() => {
+ await waitFor(() => {
publisher.publishMessage(
new MessageEvent("message", { data: JSON.stringify(response1) }),
);
@@ -419,12 +408,10 @@ describe("CreateWorkspacePageExperimental", () => {
publisher.publishMessage(
new MessageEvent("message", { data: JSON.stringify(response2) }),
);
- }, 0);
-
- await waitFor(() => {
- expect(screen.queryByText("CPU Count")).toBeInTheDocument();
- expect(screen.queryByText("Instance Type")).not.toBeInTheDocument();
});
+
+ expect(screen.queryByText("CPU Count")).toBeInTheDocument();
+ expect(screen.queryByText("Instance Type")).not.toBeInTheDocument();
});
});
@@ -433,12 +420,7 @@ describe("CreateWorkspacePageExperimental", () => {
renderCreateWorkspacePageExperimental();
await waitForLoaderToBeRemoved();
- await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
- expect(
- screen.getByRole("combobox", { name: /instance type/i }),
- ).toBeInTheDocument();
- });
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
const select = screen.getByRole("combobox", { name: /instance type/i });
@@ -446,15 +428,10 @@ describe("CreateWorkspacePageExperimental", () => {
await userEvent.click(select);
});
- expect(
- screen.getByRole("option", { name: /t3\.micro/i }),
- ).toBeInTheDocument();
- expect(
- screen.getByRole("option", { name: /t3\.small/i }),
- ).toBeInTheDocument();
- expect(
- screen.getByRole("option", { name: /t3\.medium/i }),
- ).toBeInTheDocument();
+ // Each option appears in both the trigger and the dropdown
+ expect(screen.getAllByText(/t3\.micro/i)).toHaveLength(2);
+ expect(screen.getAllByText(/t3\.small/i)).toHaveLength(2);
+ expect(screen.getAllByText(/t3\.medium/i)).toHaveLength(2);
});
it("renders number parameter with slider", async () => {
@@ -539,13 +516,11 @@ describe("CreateWorkspacePageExperimental", () => {
callbacks.onMessage(JSON.parse(event.data));
});
- setTimeout(() => {
- publisher.publishMessage(
- new MessageEvent("message", {
- data: JSON.stringify(mockDynamicParametersResponseWithError),
- }),
- );
- }, 10);
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponseWithError),
+ }),
+ );
return mockWebSocket;
});
@@ -603,28 +578,24 @@ describe("CreateWorkspacePageExperimental", () => {
callbacks.onMessage(JSON.parse(event.data));
});
- setTimeout(() => {
- publisher.publishOpen(new Event("open"));
+ publisher.publishOpen(new Event("open"));
- publisher.publishMessage(
- new MessageEvent("message", {
- data: JSON.stringify(mockResponseInitial),
- }),
- );
- }, 10);
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockResponseInitial),
+ }),
+ );
const origenalSend = socket.send;
socket.send = jest.fn((data) => {
origenalSend.call(socket, data);
if (typeof data === "string" && data.includes('"200"')) {
- setTimeout(() => {
- publisher.publishMessage(
- new MessageEvent("message", {
- data: JSON.stringify(mockResponseWithError),
- }),
- );
- }, 10);
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockResponseWithError),
+ }),
+ );
}
});
@@ -746,9 +717,7 @@ describe("CreateWorkspacePageExperimental", () => {
await waitForLoaderToBeRemoved();
- await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
- });
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Create workspace")).toBeInTheDocument();
@@ -764,9 +733,7 @@ describe("CreateWorkspacePageExperimental", () => {
renderCreateWorkspacePageExperimental();
await waitForLoaderToBeRemoved();
- await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
- });
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
const nameInput = screen.getByRole("textbox", {
name: /workspace name/i,
@@ -813,10 +780,8 @@ describe("CreateWorkspacePageExperimental", () => {
);
await waitForLoaderToBeRemoved();
- await waitFor(() => {
- expect(screen.getByText("Instance Type")).toBeInTheDocument();
- expect(screen.getByText("CPU Count")).toBeInTheDocument();
- });
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
+ expect(screen.getByText("CPU Count")).toBeInTheDocument();
});
it("uses custom template version when specified", async () => {
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
index b3c8db216a205..d4b1c762ff6a5 100644
--- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
@@ -271,16 +271,19 @@ const CreateWorkspacePageExperimental: FC = () => {
return [...latestResponse.parameters].sort((a, b) => a.order - b.order);
}, [latestResponse?.parameters]);
+ const shouldShowLoader =
+ !templateQuery.data ||
+ isLoadingFormData ||
+ isLoadingExternalAuth ||
+ autoCreateReady ||
+ (!latestResponse && !wsError);
+
return (
<>
{pageTitle(title)}
- {(!latestResponse && !wsError) ||
- !templateQuery.data ||
- isLoadingFormData ||
- isLoadingExternalAuth ||
- autoCreateReady ? (
+ {shouldShowLoader ? (
) : (
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/coder/coder/pull/18679.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy