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..43e75af1d2f0e --- /dev/null +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.test.tsx @@ -0,0 +1,1014 @@ +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"; +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 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 mockRequiredParameter = createMockParameter({ + name: "required_param", + display_name: "Required Parameter", + description: "A required parameter", + type: "string", + form_type: "input", + required: true, +}); + +describe("DynamicParameter", () => { + const mockOnChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + 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( + , + ); + + 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", () => { + 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( + , + ); + + 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); + }); + + const icons = screen.getAllByRole("img"); + expect( + icons.some((icon) => icon.getAttribute("src") === "/icon2.png"), + ).toBe(true); + }); + }); + + 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( + , + ); + + 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", () => { + 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( + , + ); + 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", () => { + 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( + , + ); + + 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", () => { + 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( + , + ); + + expect(screen.getByText("Slider Parameter")).toBeInTheDocument(); + const slider = screen.getByRole("slider"); + expect(slider).toHaveAttribute("aria-valuenow", "50"); + }); + + 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", () => { + 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( + , + ); + + 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", () => { + 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( + , + ); + + expect(screen.getByText("Error Parameter")).toBeInTheDocument(); + expect( + screen.getByText("This parameter has a validation error"), + ).toBeInTheDocument(); + }); + }); + + 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( + , + ); + + 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"); + + 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( + , + ); + + 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(); + }); + }); + + 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( + , + ); + + 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", () => { + 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( + , + ); + + 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", () => { + 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( + , + ); + + expect(screen.getByText("This is a warning")).toBeInTheDocument(); + expect(screen.getByText("Something might be wrong")).toBeInTheDocument(); + }); + }); +}); pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy