From 227085afaa58dd8b97927e1568964e4f63cad21d Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Jul 2025 12:43:46 +0000 Subject: [PATCH 1/4] chore: test improvements --- site/jest.setup.ts | 6 + .../DynamicParameter.test.tsx | 914 ++++++++++++++++++ 2 files changed, 920 insertions(+) create mode 100644 site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx diff --git a/site/jest.setup.ts b/site/jest.setup.ts index 31868b0d92d80..f90f5353b1c63 100644 --- a/site/jest.setup.ts +++ b/site/jest.setup.ts @@ -40,6 +40,12 @@ jest.mock("contexts/useProxyLatency", () => ({ global.scrollTo = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn(); +// Polyfill pointer capture methods for JSDOM compatibility with Radix UI +window.HTMLElement.prototype.hasPointerCapture = jest + .fn() + .mockReturnValue(false); +window.HTMLElement.prototype.setPointerCapture = jest.fn(); +window.HTMLElement.prototype.releasePointerCapture = jest.fn(); window.open = jest.fn(); navigator.sendBeacon = jest.fn(); diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx new file mode 100644 index 0000000000000..27d3a4b64455d --- /dev/null +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx @@ -0,0 +1,914 @@ +import { screen, waitFor, fireEvent, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { PreviewParameter } from "api/typesGenerated"; +import { render } from "testHelpers/renderHelpers"; +import { DynamicParameter } from "./DynamicParameter"; + +const createMockParameter = ( + overrides: Partial = {}, +): PreviewParameter => ({ + name: "test_param", + display_name: "Test Parameter", + description: "A test parameter", + type: "string", + mutable: true, + default_value: { value: "", valid: true }, + icon: "", + options: [], + validations: [], + styling: { + placeholder: "", + disabled: false, + label: "", + }, + diagnostics: [], + value: { value: "", valid: true }, + required: false, + order: 1, + form_type: "input", + ephemeral: false, + ...overrides, +}); + +const mockStringParameter = createMockParameter({ + name: "string_param", + display_name: "String Parameter", + description: "A string input parameter", + type: "string", + form_type: "input", + default_value: { value: "default_value", valid: true }, +}); + +const mockTextareaParameter = createMockParameter({ + name: "textarea_param", + display_name: "Textarea Parameter", + description: "A textarea input parameter", + type: "string", + form_type: "textarea", + default_value: { value: "default\nmultiline\nvalue", valid: true }, +}); + +const mockSelectParameter = createMockParameter({ + name: "select_param", + display_name: "Select Parameter", + description: "A select parameter with options", + type: "string", + form_type: "dropdown", + default_value: { value: "option1", valid: true }, + options: [ + { + name: "Option 1", + description: "First option", + value: { value: "option1", valid: true }, + icon: "", + }, + { + name: "Option 2", + description: "Second option", + value: { value: "option2", valid: true }, + icon: "/icon2.png", + }, + { + name: "Option 3", + description: "Third option", + value: { value: "option3", valid: true }, + icon: "", + }, + ], +}); + +const mockRadioParameter = createMockParameter({ + name: "radio_param", + display_name: "Radio Parameter", + description: "A radio button parameter", + type: "string", + form_type: "radio", + default_value: { value: "radio1", valid: true }, + options: [ + { + name: "Radio 1", + description: "First radio option", + value: { value: "radio1", valid: true }, + icon: "", + }, + { + name: "Radio 2", + description: "Second radio option", + value: { value: "radio2", valid: true }, + icon: "", + }, + ], +}); + +const mockCheckboxParameter = createMockParameter({ + name: "checkbox_param", + display_name: "Checkbox Parameter", + description: "A checkbox parameter", + type: "bool", + form_type: "checkbox", + default_value: { value: "true", valid: true }, +}); + +const mockSwitchParameter = createMockParameter({ + name: "switch_param", + display_name: "Switch Parameter", + description: "A switch parameter", + type: "bool", + form_type: "switch", + default_value: { value: "false", valid: true }, +}); + +const mockSliderParameter = createMockParameter({ + name: "slider_param", + display_name: "Slider Parameter", + description: "A slider parameter", + type: "number", + form_type: "slider", + default_value: { value: "50", valid: true }, + validations: [ + { + validation_min: 0, + validation_max: 100, + validation_error: "Value must be between 0 and 100", + validation_regex: null, + validation_monotonic: null, + }, + ], +}); + +const mockTagsParameter = createMockParameter({ + name: "tags_param", + display_name: "Tags Parameter", + description: "A tags parameter", + type: "list(string)", + form_type: "tag-select", + default_value: { value: '["tag1", "tag2"]', valid: true }, +}); + +const mockMultiSelectParameter = createMockParameter({ + name: "multiselect_param", + display_name: "Multi-Select Parameter", + description: "A multi-select parameter", + type: "list(string)", + form_type: "multi-select", + default_value: { value: '["option1", "option3"]', valid: true }, + options: [ + { + name: "Option 1", + description: "First option", + value: { value: "option1", valid: true }, + icon: "", + }, + { + name: "Option 2", + description: "Second option", + value: { value: "option2", valid: true }, + icon: "", + }, + { + name: "Option 3", + description: "Third option", + value: { value: "option3", valid: true }, + icon: "", + }, + { + name: "Option 4", + description: "Fourth option", + value: { value: "option4", valid: true }, + icon: "", + }, + ], +}); + +const mockErrorParameter = createMockParameter({ + name: "error_param", + display_name: "Error Parameter", + description: "A parameter with validation error", + type: "string", + form_type: "error", + diagnostics: [ + { + severity: "error", + summary: "Validation Error", + detail: "This parameter has a validation error", + extra: { + code: "validation_error", + }, + }, + ], +}); + +const mockRequiredParameter = createMockParameter({ + name: "required_param", + display_name: "Required Parameter", + description: "A required parameter", + type: "string", + form_type: "input", + required: true, +}); + +const mockImmutableParameter = createMockParameter({ + name: "immutable_param", + display_name: "Immutable Parameter", + description: "An immutable parameter", + type: "string", + form_type: "input", + mutable: false, + default_value: { value: "immutable_value", valid: true }, +}); + +const mockEphemeralParameter = createMockParameter({ + name: "ephemeral_param", + display_name: "Ephemeral Parameter", + description: "An ephemeral parameter", + type: "string", + form_type: "input", + ephemeral: true, +}); + +const mockParameterWithIcon = createMockParameter({ + name: "icon_param", + display_name: "Parameter with Icon", + description: "A parameter with an icon", + type: "string", + form_type: "input", + icon: "/test-icon.png", +}); + +describe("DynamicParameter", () => { + const mockOnChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Input Parameter", () => { + it("renders string input parameter correctly", () => { + render( + , + ); + + expect(screen.getByText("String Parameter")).toBeInTheDocument(); + expect(screen.getByText("A string input parameter")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toHaveValue("test_value"); + }); + + it("calls onChange when input value changes", async () => { + render( + , + ); + + const input = screen.getByRole("textbox"); + + await waitFor(async () => { + await userEvent.type(input, "new_value"); + }); + + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith("new_value"); + }); + }); + + it("shows required indicator for required parameters", () => { + render( + , + ); + + expect(screen.getByText("*")).toBeInTheDocument(); + }); + + it("disables input when disabled prop is true", () => { + render( + , + ); + + expect(screen.getByRole("textbox")).toBeDisabled(); + }); + + it("displays parameter icon when provided", () => { + render( + , + ); + + const icon = screen.getByRole("img"); + expect(icon).toHaveAttribute("src", "/test-icon.png"); + }); + }); + + describe("Textarea Parameter", () => { + it("renders textarea parameter correctly", () => { + const testValue = "multiline\ntext\nvalue"; + render( + , + ); + + expect(screen.getByText("Textarea Parameter")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toHaveValue(testValue); + }); + + it("handles textarea value changes", async () => { + render( + , + ); + + const textarea = screen.getByRole("textbox"); + await waitFor(async () => { + await userEvent.type(textarea, "line1{enter}line2{enter}line3"); + }); + + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith("line1\nline2\nline3"); + }); + }); + }); + + describe("Select Parameter", () => { + it("renders select parameter with options", () => { + render( + , + ); + + expect(screen.getByText("Select Parameter")).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + it("displays all options when opened", async () => { + render( + , + ); + + const select = screen.getByRole("combobox"); + await waitFor(async () => { + await userEvent.click(select); + }); + + // Option 1 exists in the trigger and the dropdown + expect(screen.getAllByText("Option 1")).toHaveLength(2); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + expect(screen.getByText("Option 3")).toBeInTheDocument(); + }); + + it("calls onChange when option is selected", async () => { + render( + , + ); + + const select = screen.getByRole("combobox"); + await waitFor(async () => { + await userEvent.click(select); + }); + + const option2 = screen.getByText("Option 2"); + await waitFor(async () => { + await userEvent.click(option2); + }); + + expect(mockOnChange).toHaveBeenCalledWith("option2"); + }); + + it("displays option icons when provided", async () => { + render( + , + ); + + const select = screen.getByRole("combobox"); + await waitFor(async () => { + await userEvent.click(select); + }); + + // Option 2 has an icon + const icons = screen.getAllByRole("img"); + expect( + icons.some((icon) => icon.getAttribute("src") === "/icon2.png"), + ).toBe(true); + }); + }); + + describe("Radio Parameter", () => { + it("renders radio parameter with options", () => { + render( + , + ); + + expect(screen.getByText("Radio Parameter")).toBeInTheDocument(); + expect(screen.getByRole("radiogroup")).toBeInTheDocument(); + expect(screen.getByRole("radio", { name: /radio 1/i })).toBeChecked(); + expect(screen.getByRole("radio", { name: /radio 2/i })).not.toBeChecked(); + }); + + it("calls onChange when radio option is selected", async () => { + render( + , + ); + + const radio2 = screen.getByRole("radio", { name: /radio 2/i }); + await waitFor(async () => { + await userEvent.click(radio2); + }); + + expect(mockOnChange).toHaveBeenCalledWith("radio2"); + }); + }); + + describe("Checkbox Parameter", () => { + it("Renders checkbox parameter correctly and handles unchecked to checked transition", async () => { + render( + , + ); + expect(screen.getByText("Checkbox Parameter")).toBeInTheDocument(); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).not.toBeChecked(); + + await waitFor(async () => { + await userEvent.click(checkbox); + }); + + expect(mockOnChange).toHaveBeenCalledWith("true"); + }); + }); + + describe("Switch Parameter", () => { + it("renders switch parameter correctly", () => { + render( + , + ); + + expect(screen.getByText("Switch Parameter")).toBeInTheDocument(); + expect(screen.getByRole("switch")).not.toBeChecked(); + }); + + it("handles switch state changes", async () => { + render( + , + ); + + const switchElement = screen.getByRole("switch"); + await waitFor(async () => { + await userEvent.click(switchElement); + }); + + expect(mockOnChange).toHaveBeenCalledWith("true"); + }); + }); + + describe("Slider Parameter", () => { + it("renders slider parameter correctly", () => { + render( + , + ); + + expect(screen.getByText("Slider Parameter")).toBeInTheDocument(); + const slider = screen.getByRole("slider"); + expect(slider).toHaveAttribute("aria-valuenow", "50"); + }); + + it("handles slider value changes", async () => { + render( + , + ); + + const slider = screen.getByRole("slider"); + + fireEvent.keyDown(slider, { key: "ArrowRight" }); + + expect(slider).toHaveAttribute("tabindex", "0"); + expect(slider).not.toHaveAttribute("aria-disabled"); + }); + + it("respects min/max constraints from validation_condition", () => { + render( + , + ); + + const slider = screen.getByRole("slider"); + expect(slider).toHaveAttribute("aria-valuemin", "0"); + expect(slider).toHaveAttribute("aria-valuemax", "100"); + }); + }); + + describe("Tags Parameter", () => { + it("renders tags parameter correctly", () => { + render( + , + ); + + expect(screen.getByText("Tags Parameter")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + it("handles tag additions", async () => { + render( + , + ); + + const input = screen.getByRole("textbox"); + await waitFor(async () => { + await userEvent.type(input, "newtag,"); + }); + + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith('["tag1","newtag"]'); + }); + }); + + it("handles tag removals", async () => { + render( + , + ); + + const deleteButtons = screen.getAllByTestId("CancelIcon"); + await waitFor(async () => { + await userEvent.click(deleteButtons[0]); + }); + + expect(mockOnChange).toHaveBeenCalledWith('["tag2"]'); + }); + }); + + describe("Multi-Select Parameter", () => { + it("renders multi-select parameter correctly", () => { + render( + , + ); + + expect(screen.getByText("Multi-Select Parameter")).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + it("displays selected options", () => { + render( + , + ); + + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 3")).toBeInTheDocument(); + }); + + it("handles option selection", async () => { + render( + , + ); + + const combobox = screen.getByRole("combobox"); + await waitFor(async () => { + await userEvent.click(combobox); + }); + + const option2 = screen.getByText("Option 2"); + await waitFor(async () => { + await userEvent.click(option2); + }); + + expect(mockOnChange).toHaveBeenCalledWith('["option1","option2"]'); + }); + + it("handles option deselection", async () => { + render( + , + ); + + const removeButtons = screen.getAllByTestId("clear-option-button"); + await waitFor(async () => { + await userEvent.click(removeButtons[0]); + }); + + expect(mockOnChange).toHaveBeenCalledWith('["option2"]'); + }); + }); + + describe("Error Parameter", () => { + it("renders error parameter with validation message", () => { + render( + , + ); + + expect(screen.getByText("Error Parameter")).toBeInTheDocument(); + expect( + screen.getByText("This parameter has a validation error"), + ).toBeInTheDocument(); + }); + }); + + describe("Parameter Badges", () => { + it("shows immutable indicator for immutable parameters", () => { + render( + , + ); + + expect(screen.getByText("Immutable")).toBeInTheDocument(); + }); + + it("shows autofill indicator when autofill is true", () => { + render( + , + ); + + expect(screen.getByText(/URL Autofill/i)).toBeInTheDocument(); + }); + + it("shows ephemeral indicator for ephemeral parameters", () => { + render( + , + ); + + expect(screen.getByText("Ephemeral")).toBeInTheDocument(); + }); + + it("shows preset indicator when isPreset is true", () => { + render( + , + ); + + expect(screen.getByText(/preset/i)).toBeInTheDocument(); + }); + }); + + describe("Accessibility", () => { + it("associates labels with form controls", () => { + render( + , + ); + + const input = screen.getByRole("textbox"); + const label = screen.getByText("String Parameter"); + + expect(input).toHaveAccessibleName("String Parameter"); + }); + + it("marks required fields appropriately", () => { + render( + , + ); + + const input = screen.getByRole("textbox"); + expect(input).toBeRequired(); + }); + }); + + describe("Debounced Input", () => { + it("debounces input changes for text inputs", async () => { + jest.useFakeTimers(); + + render( + , + ); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: "abc" } }); + + expect(mockOnChange).not.toHaveBeenCalled(); + + act(() => { + jest.runAllTimers(); + }); + + expect(mockOnChange).toHaveBeenCalledWith("abc"); + + jest.useRealTimers(); + }); + + it("debounces textarea changes", async () => { + jest.useFakeTimers(); + + render( + , + ); + + const textarea = screen.getByRole("textbox"); + fireEvent.change(textarea, { target: { value: "line1\nline2" } }); + + expect(mockOnChange).not.toHaveBeenCalled(); + + act(() => { + jest.runAllTimers(); + }); + + expect(mockOnChange).toHaveBeenCalledWith("line1\nline2"); + + jest.useRealTimers(); + }); + }); + + describe("Edge Cases", () => { + it("handles empty parameter options gracefully", () => { + const paramWithEmptyOptions = createMockParameter({ + form_type: "dropdown", + options: [], + }); + + render( + , + ); + + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + + it("handles null/undefined values", () => { + render( + , + ); + + expect(screen.getByRole("textbox")).toHaveValue(""); + }); + + it("handles invalid JSON in list parameters", () => { + render( + , + ); + + // Should not crash and should render the component + expect(screen.getByText("Tags Parameter")).toBeInTheDocument(); + }); + + it("handles parameters with very long descriptions", () => { + const longDescriptionParam = createMockParameter({ + description: "A".repeat(1000), + }); + + render( + , + ); + + expect(screen.getByText("A".repeat(1000))).toBeInTheDocument(); + }); + + it("handles parameters with special characters in names", () => { + const specialCharParam = createMockParameter({ + name: "param-with_special.chars", + display_name: "Param with Special Characters!@#$%", + }); + + render( + , + ); + + expect( + screen.getByText("Param with Special Characters!@#$%"), + ).toBeInTheDocument(); + }); + }); +}); From 8f0fb6893e50c319264c8b3bb4052fc3d275a758 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Jul 2025 12:46:14 +0000 Subject: [PATCH 2/4] fix: format --- .../workspaces/DynamicParameter/DynamicParameter.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx index 27d3a4b64455d..ffe794564cd0c 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor, fireEvent, act } from "@testing-library/react"; +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { PreviewParameter } from "api/typesGenerated"; import { render } from "testHelpers/renderHelpers"; From 575b0f0fca6ae158f5e6d643ffb7e3732018c980 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Jul 2025 15:45:26 +0000 Subject: [PATCH 3/4] chore: test improvements --- .../DynamicParameter.test.tsx | 142 +++++++++++++++--- 1 file changed, 122 insertions(+), 20 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx index ffe794564cd0c..f0e966804efaf 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx @@ -235,6 +235,54 @@ const mockParameterWithIcon = createMockParameter({ icon: "/test-icon.png", }); +const mockNumberInputParameter = createMockParameter({ + name: "number_input_param", + display_name: "Number Input Parameter", + description: "A numeric input parameter with min/max validations", + type: "number", + form_type: "input", + default_value: { value: "5", valid: true }, + validations: [ + { + validation_min: 1, + validation_max: 10, + validation_error: "Value must be between 1 and 10", + validation_regex: null, + validation_monotonic: null, + }, + ], +}); + +// Mock for a masked input parameter (e.g. secrets) +const mockMaskedInputParameter = createMockParameter({ + name: "masked_input_param", + display_name: "Masked Input Parameter", + type: "string", + form_type: "input", + styling: { + placeholder: "********", + disabled: false, + label: "", + mask_input: true, + }, +}); + +// Mock parameter that contains a warning diagnostic +const mockWarningParameter = createMockParameter({ + name: "warning_param", + display_name: "Warning Parameter", + description: "Parameter with a warning diagnostic", + form_type: "input", + diagnostics: [ + { + severity: "warning", + summary: "This is a warning", + detail: "Something might be wrong", + extra: { code: "warning" }, + }, + ], +}); + describe("DynamicParameter", () => { const mockOnChange = jest.fn(); @@ -421,7 +469,6 @@ describe("DynamicParameter", () => { await userEvent.click(select); }); - // Option 2 has an icon const icons = screen.getAllByRole("img"); expect( icons.some((icon) => icon.getAttribute("src") === "/icon2.png"), @@ -532,23 +579,6 @@ describe("DynamicParameter", () => { expect(slider).toHaveAttribute("aria-valuenow", "50"); }); - it("handles slider value changes", async () => { - render( - , - ); - - const slider = screen.getByRole("slider"); - - fireEvent.keyDown(slider, { key: "ArrowRight" }); - - expect(slider).toHaveAttribute("tabindex", "0"); - expect(slider).not.toHaveAttribute("aria-disabled"); - }); - it("respects min/max constraints from validation_condition", () => { render( { ); const input = screen.getByRole("textbox"); - const label = screen.getByText("String Parameter"); expect(input).toHaveAccessibleName("String Parameter"); }); @@ -872,7 +901,6 @@ describe("DynamicParameter", () => { />, ); - // Should not crash and should render the component expect(screen.getByText("Tags Parameter")).toBeInTheDocument(); }); @@ -911,4 +939,78 @@ describe("DynamicParameter", () => { ).toBeInTheDocument(); }); }); + + describe("Number Input Parameter", () => { + it("renders number input with correct min/max attributes", () => { + render( + , + ); + + const input = screen.getByRole("spinbutton"); + expect(input).toHaveAttribute("min", "1"); + expect(input).toHaveAttribute("max", "10"); + }); + + it("calls onChange when numeric value changes (debounced)", () => { + jest.useFakeTimers(); + render( + , + ); + + const input = screen.getByRole("spinbutton"); + fireEvent.change(input, { target: { value: "7" } }); + + act(() => { + jest.runAllTimers(); + }); + + expect(mockOnChange).toHaveBeenCalledWith("7"); + jest.useRealTimers(); + }); + }); + + describe("Masked Input Parameter", () => { + it("renders a password field by default and toggles visibility on mouse events", async () => { + render( + , + ); + + const input = screen.getByLabelText("Masked Input Parameter"); + expect(input).toHaveAttribute("type", "password"); + + const toggleButton = screen.getByRole("button"); + fireEvent.mouseDown(toggleButton); + expect(input).toHaveAttribute("type", "text"); + + fireEvent.mouseUp(toggleButton); + expect(input).toHaveAttribute("type", "password"); + }); + }); + + describe("Parameter Diagnostics", () => { + it("renders warning diagnostics for non-error parameters", () => { + render( + , + ); + + expect(screen.getByText("This is a warning")).toBeInTheDocument(); + expect(screen.getByText("Something might be wrong")).toBeInTheDocument(); + }); + }); }); From 76fd44722d82642f9d3f57f47488178306e23b15 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 3 Jul 2025 17:02:53 +0000 Subject: [PATCH 4/4] chore: reorganize --- .../DynamicParameter.test.tsx | 432 +++++++++--------- 1 file changed, 215 insertions(+), 217 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx index f0e966804efaf..43e75af1d2f0e 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx @@ -48,94 +48,6 @@ const mockTextareaParameter = createMockParameter({ default_value: { value: "default\nmultiline\nvalue", valid: true }, }); -const mockSelectParameter = createMockParameter({ - name: "select_param", - display_name: "Select Parameter", - description: "A select parameter with options", - type: "string", - form_type: "dropdown", - default_value: { value: "option1", valid: true }, - options: [ - { - name: "Option 1", - description: "First option", - value: { value: "option1", valid: true }, - icon: "", - }, - { - name: "Option 2", - description: "Second option", - value: { value: "option2", valid: true }, - icon: "/icon2.png", - }, - { - name: "Option 3", - description: "Third option", - value: { value: "option3", valid: true }, - icon: "", - }, - ], -}); - -const mockRadioParameter = createMockParameter({ - name: "radio_param", - display_name: "Radio Parameter", - description: "A radio button parameter", - type: "string", - form_type: "radio", - default_value: { value: "radio1", valid: true }, - options: [ - { - name: "Radio 1", - description: "First radio option", - value: { value: "radio1", valid: true }, - icon: "", - }, - { - name: "Radio 2", - description: "Second radio option", - value: { value: "radio2", valid: true }, - icon: "", - }, - ], -}); - -const mockCheckboxParameter = createMockParameter({ - name: "checkbox_param", - display_name: "Checkbox Parameter", - description: "A checkbox parameter", - type: "bool", - form_type: "checkbox", - default_value: { value: "true", valid: true }, -}); - -const mockSwitchParameter = createMockParameter({ - name: "switch_param", - display_name: "Switch Parameter", - description: "A switch parameter", - type: "bool", - form_type: "switch", - default_value: { value: "false", valid: true }, -}); - -const mockSliderParameter = createMockParameter({ - name: "slider_param", - display_name: "Slider Parameter", - description: "A slider parameter", - type: "number", - form_type: "slider", - default_value: { value: "50", valid: true }, - validations: [ - { - validation_min: 0, - validation_max: 100, - validation_error: "Value must be between 0 and 100", - validation_regex: null, - validation_monotonic: null, - }, - ], -}); - const mockTagsParameter = createMockParameter({ name: "tags_param", display_name: "Tags Parameter", @@ -145,59 +57,6 @@ const mockTagsParameter = createMockParameter({ default_value: { value: '["tag1", "tag2"]', valid: true }, }); -const mockMultiSelectParameter = createMockParameter({ - name: "multiselect_param", - display_name: "Multi-Select Parameter", - description: "A multi-select parameter", - type: "list(string)", - form_type: "multi-select", - default_value: { value: '["option1", "option3"]', valid: true }, - options: [ - { - name: "Option 1", - description: "First option", - value: { value: "option1", valid: true }, - icon: "", - }, - { - name: "Option 2", - description: "Second option", - value: { value: "option2", valid: true }, - icon: "", - }, - { - name: "Option 3", - description: "Third option", - value: { value: "option3", valid: true }, - icon: "", - }, - { - name: "Option 4", - description: "Fourth option", - value: { value: "option4", valid: true }, - icon: "", - }, - ], -}); - -const mockErrorParameter = createMockParameter({ - name: "error_param", - display_name: "Error Parameter", - description: "A parameter with validation error", - type: "string", - form_type: "error", - diagnostics: [ - { - severity: "error", - summary: "Validation Error", - detail: "This parameter has a validation error", - extra: { - code: "validation_error", - }, - }, - ], -}); - const mockRequiredParameter = createMockParameter({ name: "required_param", display_name: "Required Parameter", @@ -207,82 +66,6 @@ const mockRequiredParameter = createMockParameter({ required: true, }); -const mockImmutableParameter = createMockParameter({ - name: "immutable_param", - display_name: "Immutable Parameter", - description: "An immutable parameter", - type: "string", - form_type: "input", - mutable: false, - default_value: { value: "immutable_value", valid: true }, -}); - -const mockEphemeralParameter = createMockParameter({ - name: "ephemeral_param", - display_name: "Ephemeral Parameter", - description: "An ephemeral parameter", - type: "string", - form_type: "input", - ephemeral: true, -}); - -const mockParameterWithIcon = createMockParameter({ - name: "icon_param", - display_name: "Parameter with Icon", - description: "A parameter with an icon", - type: "string", - form_type: "input", - icon: "/test-icon.png", -}); - -const mockNumberInputParameter = createMockParameter({ - name: "number_input_param", - display_name: "Number Input Parameter", - description: "A numeric input parameter with min/max validations", - type: "number", - form_type: "input", - default_value: { value: "5", valid: true }, - validations: [ - { - validation_min: 1, - validation_max: 10, - validation_error: "Value must be between 1 and 10", - validation_regex: null, - validation_monotonic: null, - }, - ], -}); - -// Mock for a masked input parameter (e.g. secrets) -const mockMaskedInputParameter = createMockParameter({ - name: "masked_input_param", - display_name: "Masked Input Parameter", - type: "string", - form_type: "input", - styling: { - placeholder: "********", - disabled: false, - label: "", - mask_input: true, - }, -}); - -// Mock parameter that contains a warning diagnostic -const mockWarningParameter = createMockParameter({ - name: "warning_param", - display_name: "Warning Parameter", - description: "Parameter with a warning diagnostic", - form_type: "input", - diagnostics: [ - { - severity: "warning", - summary: "This is a warning", - detail: "Something might be wrong", - extra: { code: "warning" }, - }, - ], -}); - describe("DynamicParameter", () => { const mockOnChange = jest.fn(); @@ -291,6 +74,15 @@ describe("DynamicParameter", () => { }); describe("Input Parameter", () => { + const mockParameterWithIcon = createMockParameter({ + name: "icon_param", + display_name: "Parameter with Icon", + description: "A parameter with an icon", + type: "string", + form_type: "input", + icon: "/test-icon.png", + }); + it("renders string input parameter correctly", () => { render( { }); describe("Select Parameter", () => { + const mockSelectParameter = createMockParameter({ + name: "select_param", + display_name: "Select Parameter", + description: "A select parameter with options", + type: "string", + form_type: "dropdown", + default_value: { value: "option1", valid: true }, + options: [ + { + name: "Option 1", + description: "First option", + value: { value: "option1", valid: true }, + icon: "", + }, + { + name: "Option 2", + description: "Second option", + value: { value: "option2", valid: true }, + icon: "/icon2.png", + }, + { + name: "Option 3", + description: "Third option", + value: { value: "option3", valid: true }, + icon: "", + }, + ], + }); + it("renders select parameter with options", () => { render( { }); describe("Radio Parameter", () => { + const mockRadioParameter = createMockParameter({ + name: "radio_param", + display_name: "Radio Parameter", + description: "A radio button parameter", + type: "string", + form_type: "radio", + default_value: { value: "radio1", valid: true }, + options: [ + { + name: "Radio 1", + description: "First radio option", + value: { value: "radio1", valid: true }, + icon: "", + }, + { + name: "Radio 2", + description: "Second radio option", + value: { value: "radio2", valid: true }, + icon: "", + }, + ], + }); + it("renders radio parameter with options", () => { render( { }); describe("Checkbox Parameter", () => { + const mockCheckboxParameter = createMockParameter({ + name: "checkbox_param", + display_name: "Checkbox Parameter", + description: "A checkbox parameter", + type: "bool", + form_type: "checkbox", + default_value: { value: "true", valid: true }, + }); + it("Renders checkbox parameter correctly and handles unchecked to checked transition", async () => { render( { }); describe("Switch Parameter", () => { + const mockSwitchParameter = createMockParameter({ + name: "switch_param", + display_name: "Switch Parameter", + description: "A switch parameter", + type: "bool", + form_type: "switch", + default_value: { value: "false", valid: true }, + }); + it("renders switch parameter correctly", () => { render( { }); describe("Slider Parameter", () => { + const mockSliderParameter = createMockParameter({ + name: "slider_param", + display_name: "Slider Parameter", + description: "A slider parameter", + type: "number", + form_type: "slider", + default_value: { value: "50", valid: true }, + validations: [ + { + validation_min: 0, + validation_max: 100, + validation_error: "Value must be between 0 and 100", + validation_regex: null, + validation_monotonic: null, + }, + ], + }); + it("renders slider parameter correctly", () => { render( { }); describe("Multi-Select Parameter", () => { + const mockMultiSelectParameter = createMockParameter({ + name: "multiselect_param", + display_name: "Multi-Select Parameter", + description: "A multi-select parameter", + type: "list(string)", + form_type: "multi-select", + default_value: { value: '["option1", "option3"]', valid: true }, + options: [ + { + name: "Option 1", + description: "First option", + value: { value: "option1", valid: true }, + icon: "", + }, + { + name: "Option 2", + description: "Second option", + value: { value: "option2", valid: true }, + icon: "", + }, + { + name: "Option 3", + description: "Third option", + value: { value: "option3", valid: true }, + icon: "", + }, + { + name: "Option 4", + description: "Fourth option", + value: { value: "option4", valid: true }, + icon: "", + }, + ], + }); + it("renders multi-select parameter correctly", () => { render( { }); describe("Error Parameter", () => { + const mockErrorParameter = createMockParameter({ + name: "error_param", + display_name: "Error Parameter", + description: "A parameter with validation error", + type: "string", + form_type: "error", + diagnostics: [ + { + severity: "error", + summary: "Validation Error", + detail: "This parameter has a validation error", + extra: { + code: "validation_error", + }, + }, + ], + }); + it("renders error parameter with validation message", () => { render( { }); describe("Parameter Badges", () => { + const mockEphemeralParameter = createMockParameter({ + name: "ephemeral_param", + display_name: "Ephemeral Parameter", + description: "An ephemeral parameter", + type: "string", + form_type: "input", + ephemeral: true, + }); + + const mockImmutableParameter = createMockParameter({ + name: "immutable_param", + display_name: "Immutable Parameter", + description: "An immutable parameter", + type: "string", + form_type: "input", + mutable: false, + default_value: { value: "immutable_value", valid: true }, + }); + it("shows immutable indicator for immutable parameters", () => { render( { }); describe("Number Input Parameter", () => { + const mockNumberInputParameter = createMockParameter({ + name: "number_input_param", + display_name: "Number Input Parameter", + description: "A numeric input parameter with min/max validations", + type: "number", + form_type: "input", + default_value: { value: "5", valid: true }, + validations: [ + { + validation_min: 1, + validation_max: 10, + validation_error: "Value must be between 1 and 10", + validation_regex: null, + validation_monotonic: null, + }, + ], + }); + it("renders number input with correct min/max attributes", () => { render( { }); describe("Masked Input Parameter", () => { + const mockMaskedInputParameter = createMockParameter({ + name: "masked_input_param", + display_name: "Masked Input Parameter", + type: "string", + form_type: "input", + styling: { + placeholder: "********", + disabled: false, + label: "", + mask_input: true, + }, + }); + it("renders a password field by default and toggles visibility on mouse events", async () => { render( { }); describe("Parameter Diagnostics", () => { + const mockWarningParameter = createMockParameter({ + name: "warning_param", + display_name: "Warning Parameter", + description: "Parameter with a warning diagnostic", + form_type: "input", + diagnostics: [ + { + severity: "warning", + summary: "This is a warning", + detail: "Something might be wrong", + extra: { code: "warning" }, + }, + ], + }); + it("renders warning diagnostics for non-error parameters", () => { render(