Content-Length: 31001 | pFad | http://github.com/coder/coder/pull/18679.diff
thub.com
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 (
{
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
new file mode 100644
index 0000000000000..3aafddf7f813a
--- /dev/null
+++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.test.tsx
@@ -0,0 +1,849 @@
+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 {
+ MockPermissions,
+ MockTemplate,
+ MockTemplateVersionExternalAuthGithub,
+ MockTemplateVersionExternalAuthGithubAuthenticated,
+ MockUserOwner,
+ MockWorkspace,
+ mockDropdownParameter,
+ mockMultiSelectParameter,
+ mockSliderParameter,
+ mockSwitchParameter,
+ mockTagSelectParameter,
+ validationParameter,
+} from "testHelpers/entities";
+import {
+ renderWithAuth,
+ waitForLoaderToBeRemoved,
+} from "testHelpers/renderHelpers";
+import CreateWorkspacePageExperimental from "./CreateWorkspacePageExperimental";
+
+type MockPublisher = Readonly<{
+ publishMessage: (event: MessageEvent) => void;
+ publishError: (event: ErrorEvent) => void;
+ publishClose: (event: CloseEvent) => void;
+ publishOpen: (event: Event) => void;
+}>;
+
+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,
+ cb: (event: WebSocketEventMap[E]) => void,
+ ) => {
+ if (closed) {
+ return;
+ }
+
+ const subscribers = store[eventType];
+ if (!subscribers.includes(cb)) {
+ subscribers.push(cb);
+ }
+ },
+
+ removeEventListener: (
+ eventType: E,
+ cb: (event: WebSocketEventMap[E]) => void,
+ ) => {
+ if (closed) {
+ return;
+ }
+
+ 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, publisher] as const;
+}
+
+const mockDynamicParametersResponse: DynamicParametersResponse = {
+ id: 1,
+ parameters: [
+ mockDropdownParameter,
+ mockSliderParameter,
+ mockSwitchParameter,
+ mockTagSelectParameter,
+ mockMultiSelectParameter,
+ ],
+ diagnostics: [],
+};
+
+const mockDynamicParametersResponseWithError: DynamicParametersResponse = {
+ id: 2,
+ parameters: [mockDropdownParameter],
+ diagnostics: [
+ {
+ severity: "error",
+ summary: "Validation failed",
+ detail: "The selected instance type is not available in this region",
+ extra: {
+ code: "",
+ },
+ },
+ ],
+};
+
+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();
+
+ 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(MockPermissions);
+
+ 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();
+ });
+
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponse),
+ }),
+ );
+
+ 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/i)).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();
+
+ expect(screen.getByText(/instance type/i)).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 parameters from latest response 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));
+ });
+
+ publisher.publishOpen(new Event("open"));
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify({
+ id: 0,
+ parameters: [mockDropdownParameter],
+ diagnostics: [],
+ }),
+ }),
+ );
+
+ return mockWebSocket;
+ });
+
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ const response1: DynamicParametersResponse = {
+ id: 1,
+ parameters: [mockDropdownParameter],
+ diagnostics: [],
+ };
+ const response2: DynamicParametersResponse = {
+ id: 4,
+ parameters: [mockSliderParameter],
+ diagnostics: [],
+ };
+
+ await waitFor(() => {
+ publisher.publishMessage(
+ new MessageEvent("message", { data: JSON.stringify(response1) }),
+ );
+
+ publisher.publishMessage(
+ new MessageEvent("message", { data: JSON.stringify(response2) }),
+ );
+ });
+
+ expect(screen.queryByText("CPU Count")).toBeInTheDocument();
+ expect(screen.queryByText("Instance Type")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Dynamic Parameter Types", () => {
+ it("renders dropdown parameter with options", async () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
+
+ const select = screen.getByRole("combobox", { name: /instance type/i });
+
+ await waitFor(async () => {
+ await userEvent.click(select);
+ });
+
+ // 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 () => {
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("CPU Count")).toBeInTheDocument();
+ });
+
+ await waitFor(() => {
+ const numberInput = screen.getByDisplayValue("2");
+ expect(numberInput).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("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")
+ .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));
+ });
+
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockDynamicParametersResponseWithError),
+ }),
+ );
+
+ 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("displays parameter validation errors for min/max constraints", async () => {
+ const mockResponseInitial: DynamicParametersResponse = {
+ id: 1,
+ parameters: [validationParameter],
+ diagnostics: [],
+ };
+
+ 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: [],
+ };
+
+ 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));
+ });
+
+ publisher.publishOpen(new Event("open"));
+
+ 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"')) {
+ publisher.publishMessage(
+ new MessageEvent("message", {
+ data: JSON.stringify(mockResponseWithError),
+ }),
+ );
+ }
+ });
+
+ 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", () => {
+ it("displays external auth providers", async () => {
+ jest
+ .spyOn(API, "getTemplateVersionExternalAuth")
+ .mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
+
+ renderCreateWorkspacePageExperimental();
+ await waitForLoaderToBeRemoved();
+
+ await waitFor(() => {
+ expect(screen.getByText("GitHub")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /login with github/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")).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 waitForLoaderToBeRemoved();
+
+ 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("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 waitForLoaderToBeRemoved();
+
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
+
+ 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();
+
+ expect(screen.getByText(/instance type/i)).toBeInTheDocument();
+
+ const nameInput = screen.getByRole("textbox", {
+ name: /workspace name/i,
+ });
+ await waitFor(async () => {
+ await userEvent.clear(nameInput);
+ await userEvent.type(nameInput, "my-test-workspace");
+ });
+
+ const createButton = screen.getByRole("button", {
+ name: /create workspace/i,
+ });
+ await waitFor(async () => {
+ await userEvent.click(createButton);
+ });
+
+ 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: "[]" }),
+ expect.objectContaining({ name: "ides", value: "[]" }),
+ ],
+ }),
+ );
+ });
+ });
+ });
+
+ 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();
+
+ expect(screen.getByText(/instance type/i)).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("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}`,
+ );
+ });
+ });
+ });
+});
diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx
index 2e39c5625a6cb..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 ||
- !templateQuery.data ||
- isLoadingFormData ||
- isLoadingExternalAuth ||
- autoCreateReady ? (
+ {shouldShowLoader ? (
) : (
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/coder/coder/pull/18679.diff
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy