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 originalSend = socket.send; + socket.send = jest.fn((data) => { + originalSend.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 originalSend = socket.send; socket.send = jest.fn((data) => { originalSend.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 ? ( ) : ( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy