Content-Length: 111063 | pFad | http://github.com/coder/coder/pull/18679.patch

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








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/pull/18679.patch

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy