UNPKG

@octopusdeploy/design-system-components

Version:
523 lines (522 loc) • 30.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); const testing_library_1 = require("@octopusdeploy/testing-library"); const react_1 = require("@testing-library/react"); const react_2 = require("react"); const vitest_1 = require("vitest"); const Button_1 = require("../../../Button"); const TextField_1 = require("../TextField"); (0, vitest_1.describe)("<TextField />", () => { (0, vitest_1.test)("renders with correct label", () => { const input = renderTextInput({ label: "Test Label" }); input.shouldHaveLabel("Test Label"); input.shouldBeInDocument(); }); (0, vitest_1.test)("renders with placeholder text", () => { const placeholder = "Enter your text here"; const input = renderTextInput({ label: "Test Label", placeholder }); input.shouldHavePlaceholder(placeholder); }); (0, vitest_1.test)("renders with initial value", () => { const value = "Initial value"; const input = renderTextInput({ label: "Test Label", value }); input.shouldHaveValue(value); }); (0, vitest_1.test)("renders with description", () => { const description = "This is a helpful description"; const input = renderTextInput({ label: "Test Label", description }); input.shouldHaveDescription(description); }); (0, vitest_1.describe)("validationState", () => { (0, vitest_1.test)("defaults to error state when validationMessage is provided without validationState", () => { const validationMessage = "This field is required"; const input = renderTextInput({ label: "Test Label", validationMessage }); input.shouldHaveValidationMessage(validationMessage); input.shouldShowErrorValidationState(); }); (0, vitest_1.test)("renders error state when validationState=error and a validationMessage is provided", () => { const validationMessage = "This field is required"; const input = renderTextInput({ label: "Test Label", validationMessage, validationState: "error" }); input.shouldHaveValidationMessage(validationMessage); input.shouldShowErrorValidationState(); }); (0, vitest_1.test)("renders success state when validationState=success and a validationMessage is provided", () => { const validationMessage = "Looks good"; const input = renderTextInput({ label: "Test Label", validationMessage, validationState: "success" }); input.shouldHaveValidationMessage(validationMessage); input.shouldShowSuccessValidationState(); }); (0, vitest_1.test)("does not render any validation state when validationState=error but no validationMessage is provided", () => { const input = renderTextInput({ label: "Test Label", validationState: "error" }); input.shouldNotShowAnyValidationState(); }); (0, vitest_1.test)("does not render any validation state when validationState=success but no validationMessage is provided", () => { const input = renderTextInput({ label: "Test Label", validationState: "success" }); input.shouldNotShowAnyValidationState(); }); }); (0, vitest_1.test)("uses provided name attribute", () => { const name = "custom-name"; const input = renderTextInput({ label: "Test Label", name }); input.shouldHaveName(name); }); (0, vitest_1.test)("has autofocus when autoFocus is true", () => { const input = renderTextInput({ label: "Test Label", autoFocus: true }); input.shouldHaveFocus(); }); (0, vitest_1.test)("handles empty onChange gracefully", () => { (0, react_1.render)((0, jsx_runtime_1.jsx)(TextField_1.TextField, { label: "Test Label", onChange: vitest_1.vi.fn() })); testing_library_1.domQueries.getTextInput("Test Label").assertIsInTheDocument(); }); (0, vitest_1.test)("calls onChange when user types", async () => { const input = renderTextInput({ label: "Test Label" }); await input.type("hello"); input.shouldHaveCalledOnChangeWith("hello"); }); (0, vitest_1.test)("supports different input types", () => { const emailInput = renderTextInput({ label: "Email", type: "email" }); const passwordInput = renderTextInput({ label: "Password", type: "password" }); const numberInput = renderTextInput({ label: "Number", type: "number" }); emailInput.shouldHaveType("email"); passwordInput.shouldHaveType("password"); numberInput.shouldHaveType("number"); }); (0, vitest_1.describe)("disabled and readonly properties", () => { (0, vitest_1.test)("renders as disabled when disabled prop is true", () => { const input = renderTextInput({ label: "Test Label", disabled: true }); input.shouldBeDisabled(); }); (0, vitest_1.test)("disabled input cannot be typed in", async () => { const input = renderTextInput({ label: "Test Label", disabled: true }); await input.attemptToType("should not work"); input.shouldNotHaveCalledOnChange(); }); (0, vitest_1.test)("renders as readonly when readOnly prop is true", () => { const input = renderTextInput({ label: "Test Label", readOnly: true }); input.shouldBeReadOnly(); }); (0, vitest_1.test)("readonly input cannot be typed in", async () => { const input = renderTextInput({ label: "Test Label", readOnly: true }); await input.attemptToType("should not work"); input.shouldNotHaveCalledOnChange(); }); }); (0, vitest_1.describe)("default, required and optional marker properties", () => { (0, vitest_1.test)("renders with required marker when hasRequiredMarker is true", () => { const input = renderTextInput({ label: "Test Label", hasRequiredMarker: true }); input.shouldHaveRequiredMarker(); }); (0, vitest_1.test)("renders with optional marker when hasOptionalMarker is true", () => { const input = renderTextInput({ label: "Test Label", hasOptionalMarker: true }); input.shouldHaveOptionalMarker(); }); (0, vitest_1.test)("renders with default marker when hasDefaultMarker is true", () => { const input = renderTextInput({ label: "Test Label", hasDefaultMarker: true }); input.shouldHaveDefaultMarker(); }); }); (0, vitest_1.describe)("accessibility attributes", () => { (0, vitest_1.test)("has correct accessibility attributes when error is present", () => { const validationMessage = "Invalid input"; const input = renderTextInput({ label: "Test Label", validationMessage }); input.shouldHaveAriaInvalid(true); input.shouldHaveAriaDescribedByError(); }); (0, vitest_1.test)("has correct accessibility attributes when description is present", () => { const description = "Help text"; const input = renderTextInput({ label: "Test Label", description }); input.shouldHaveAriaDescribedByDescription(); }); (0, vitest_1.test)("has correct accessibility attributes when both description and error are present", () => { const description = "Help text"; const validationMessage = "Error message"; const input = renderTextInput({ label: "Test Label", description, validationMessage }); input.shouldHaveAriaDescribedByBoth(); input.shouldHaveAriaInvalid(true); }); }); (0, vitest_1.describe)("local validation", () => { (0, vitest_1.test)("shows a required error once the field is cleared", async () => { const input = renderTextInput({ label: "Test Label", required: true, value: "hello" }); await input.clear(); input.shouldHaveError("This field is required."); }); (0, vitest_1.test)("does not show a required error while the field has a value", async () => { const input = renderTextInput({ label: "Test Label", required: true }); await input.type("hello"); input.shouldNotHaveError("This field is required."); }); (0, vitest_1.test)("clears the required error once a value is entered", async () => { const input = renderTextInput({ label: "Test Label", required: true, value: "hello" }); await input.clear(); input.shouldHaveError("This field is required."); await input.type("hello again"); input.shouldNotHaveError("This field is required."); }); (0, vitest_1.test)("does not show a required error for a required number field with value 0", async () => { const input = renderTextInput({ label: "Test Label", type: "number", required: true, value: "5" }); await input.type("0"); input.shouldNotHaveError("This field is required."); }); (0, vitest_1.test)("shows a minimum value error when the number entered is below min", async () => { const input = renderTextInput({ label: "Test Label", type: "number", min: 10 }); await input.type("5"); input.shouldHaveError("This field's minimum allowed value is 10."); }); (0, vitest_1.test)("shows a maximum value error when the number entered is above max", async () => { const input = renderTextInput({ label: "Test Label", type: "number", max: 10 }); await input.type("15"); input.shouldHaveError("This field's maximum allowed value is 10."); }); (0, vitest_1.test)("does not show a min/max error when the number entered is within range", async () => { const input = renderTextInput({ label: "Test Label", type: "number", min: 1, max: 10 }); await input.type("5"); input.shouldNotHaveError("This field's minimum allowed value is 1."); input.shouldNotHaveError("This field's maximum allowed value is 10."); }); (0, vitest_1.test)("min/max validation does not apply when type is not number", async () => { const input = renderTextInput({ label: "Test Label", min: 10, max: 20 }); await input.type("999"); input.shouldNotHaveError("minimum allowed value"); input.shouldNotHaveError("maximum allowed value"); }); (0, vitest_1.test)("shows a minimum length error when the text entered is shorter than minLength", async () => { const input = renderTextInput({ label: "Test Label", minLength: 5 }); await input.type("ab"); input.shouldHaveError("This field's value must be 5 characters or more."); }); (0, vitest_1.test)("shows a maximum length error when the text entered exceeds maxLength", async () => { const input = renderTextInput({ label: "Test Label", maxLength: 5 }); await input.type("abcdef"); input.shouldHaveError("This field's value must be 5 characters or less."); }); (0, vitest_1.test)("does not show a length error when the text entered is within range", async () => { const input = renderTextInput({ label: "Test Label", minLength: 2, maxLength: 5 }); await input.type("abc"); input.shouldNotHaveError("characters or more"); input.shouldNotHaveError("characters or less"); }); (0, vitest_1.test)("does not show a minLength error for an empty, non-required field", async () => { const input = renderTextInput({ label: "Test Label", minLength: 5, value: "hello" }); await input.clear(); input.shouldNotHaveError("characters or more"); }); }); (0, vitest_1.describe)("validation on external value change", () => { (0, vitest_1.test)("shows a required error when the value is cleared externally", () => { const input = renderTextInputWithExternalValue({ label: "Test Label", required: true, value: "hello" }); input.shouldNotHaveError("This field is required."); input.setValue(""); input.shouldHaveError("This field is required."); }); (0, vitest_1.test)("clears the required error when a value is set externally", () => { const input = renderTextInputWithExternalValue({ label: "Test Label", required: true, value: "hello" }); input.setValue(""); input.shouldHaveError("This field is required."); input.setValue("world"); input.shouldNotHaveError("This field is required."); }); }); (0, vitest_1.describe)("external validation message dismissal", () => { (0, vitest_1.test)("keeps showing the external validation message when the field has not been edited", () => { const input = renderTextInput({ label: "Test Label", value: "hello", validationMessage: "Server error" }); input.shouldHaveError("Server error"); }); (0, vitest_1.test)("dismisses the external validation message once the field is edited", async () => { const input = renderTextInput({ label: "Test Label", value: "hello", validationMessage: "Server error" }); await input.type("hello there"); input.shouldNotHaveError("Server error"); }); (0, vitest_1.test)("shows a new external validation message even without a further edit", async () => { const input = renderTextInput({ label: "Test Label", value: "hello", validationMessage: "Server error" }); await input.type("hello there"); input.shouldNotHaveError("Server error"); input.setExternalValidationMessage("A different server error"); input.shouldHaveError("A different server error"); }); // Mirrors a real resubmit: useMutation clears its error to undefined at the start of every submit, // before the new (possibly identical) result comes back - that transition through undefined is what // lets an unchanged message reappear correctly instead of being mistaken for the stale, dismissed one. (0, vitest_1.test)("shows the same external validation message again if it is cleared and re-set, as on a resubmit", async () => { const input = renderTextInput({ label: "Test Label", value: "hello", validationMessage: "Server error" }); await input.type("hello there"); input.shouldNotHaveError("Server error"); input.setExternalValidationMessage(undefined); input.setExternalValidationMessage("Server error"); input.shouldHaveError("Server error"); }); // Documents the remaining, narrower tradeoff: if a caller replaces the message directly with an // identical one - without clearing it to undefined first - that's indistinguishable from the old, // already-dismissed one and stays hidden until the next edit. (0, vitest_1.test)("keeps an external validation message dismissed if the identical message text replaces it directly, without clearing first", async () => { const input = renderTextInput({ label: "Test Label", value: "hello", validationMessage: "Server error" }); await input.type("hello there"); input.shouldNotHaveError("Server error"); input.setExternalValidationMessage("Server error"); input.shouldNotHaveError("Server error"); }); }); (0, vitest_1.describe)("prefix property", () => { (0, vitest_1.test)("renders with string prefix", () => { const input = renderTextInput({ label: "Test Label", prefix: "$" }); input.shouldHavePrefix("$"); }); (0, vitest_1.test)("renders with long string prefix", () => { const longPrefix = "dollars"; const input = renderTextInput({ label: "Test Label", prefix: longPrefix }); input.shouldHavePrefix("dollars"); }); (0, vitest_1.test)("does not render prefix when not provided", () => { const input = renderTextInput({ label: "Test Label" }); input.shouldNotHavePrefix(); }); (0, vitest_1.test)("prefix has correct accessibility attributes", () => { const prefix = "ABCDEFG"; const input = renderTextInput({ label: "Test Label", prefix }); input.shouldHavePrefixWithTitle(prefix); }); }); (0, vitest_1.describe)("actions property", () => { (0, vitest_1.test)("renders a single action button", () => { const input = renderTextInput({ label: "Test Label", actions: [(0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "\uD83D\uDD17" }), onClick: vitest_1.vi.fn(), importance: "ghost", accessibleName: "Bind variable" }, "bind")] }); input.shouldHaveActionButton("Bind variable"); }); (0, vitest_1.test)("renders multiple action buttons", () => { const input = renderTextInput({ label: "Test Label", actions: [(0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "#" }), onClick: vitest_1.vi.fn(), importance: "ghost", accessibleName: "Insert variable" }, "insert"), (0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "+" }), onClick: vitest_1.vi.fn(), importance: "ghost", accessibleName: "Add" }, "add")], }); input.shouldHaveActionButton("Insert variable"); input.shouldHaveActionButton("Add"); }); (0, vitest_1.test)("action button is clickable", async () => { const handleClick = vitest_1.vi.fn(); const input = renderTextInput({ label: "Test Label", actions: [(0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "\uD83D\uDD17" }), onClick: handleClick, importance: "ghost", accessibleName: "Bind variable" }, "bind")] }); await input.clickActionButton("Bind variable"); input.shouldHaveCalledActionClick(handleClick); }); (0, vitest_1.test)("renders actions alongside suffix", () => { const suffix = (0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "\uD83D\uDC41" }), onClick: vitest_1.vi.fn(), importance: "ghost", accessibleName: "Show password" }); const input = renderTextInput({ label: "Test Label", actions: [(0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "\uD83D\uDD17" }), onClick: vitest_1.vi.fn(), importance: "ghost", accessibleName: "Bind variable" }, "bind")], suffix }); input.shouldHaveActionButton("Bind variable"); input.shouldHaveActionButton("Show password"); }); (0, vitest_1.test)("does not render actions when not provided", () => { const input = renderTextInput({ label: "Test Label" }); input.shouldNotHaveAnyButtons(); }); }); (0, vitest_1.describe)("suffix property", () => { (0, vitest_1.test)("renders with string suffix", () => { const input = renderTextInput({ label: "Test Label", suffix: "sec" }); input.shouldHaveSuffix("sec"); }); (0, vitest_1.test)("renders with long string suffix and truncates it", () => { const longSuffix = "hello"; const input = renderTextInput({ label: "Test Label", suffix: longSuffix }); input.shouldHaveSuffix("hell"); }); (0, vitest_1.test)("renders with React element suffix", () => { const buttonSuffix = (0, jsx_runtime_1.jsx)("button", { type: "button", children: "X" }); const input = renderTextInput({ label: "Test Label", suffix: buttonSuffix }); input.shouldHaveSuffixElement(); input.shouldHaveSuffixButton(); }); (0, vitest_1.test)("renders with Button component suffix", () => { const buttonSuffix = (0, jsx_runtime_1.jsx)(Button_1.Button, { icon: (0, jsx_runtime_1.jsx)("span", { children: "\uD83D\uDD0D" }), onClick: () => { }, importance: "primary" }); const input = renderTextInput({ label: "Test Label", suffix: buttonSuffix }); input.shouldHaveSuffixElement(); input.shouldHaveSuffixButton(); }); (0, vitest_1.test)("does not render suffix when not provided", () => { const input = renderTextInput({ label: "Test Label" }); input.shouldNotHaveSuffix(); }); (0, vitest_1.test)("suffix button is clickable", async () => { const handleClick = vitest_1.vi.fn(); const buttonSuffix = ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleClick, children: "Click me" })); const input = renderTextInput({ label: "Test Label", suffix: buttonSuffix }); await input.clickSuffixButton(); input.shouldHaveCalledSuffixClick(handleClick); }); }); }); function kindOf(type) { if (type === "number") return "number"; if (type === "password") return "password"; return "text"; } function renderTextInput(options) { const onChange = vitest_1.vi.fn(); const { rerender } = (0, react_1.render)((0, jsx_runtime_1.jsx)(ControlledTextField, { ...options, onChange: onChange })); const setValidationMessage = (validationMessage) => rerender((0, jsx_runtime_1.jsx)(ControlledTextField, { ...options, onChange: onChange, validationMessage: validationMessage })); return getTextInputInteractions(options.label, onChange, kindOf(options.type), setValidationMessage); } // Renders TextField directly rather than through ControlledTextField, so the value is driven purely by // the prop and a test can change it externally via rerender, mimicking a caller setting the value. function renderTextInputWithExternalValue(options) { const onChange = vitest_1.vi.fn(); const { rerender } = (0, react_1.render)((0, jsx_runtime_1.jsx)(TextField_1.TextField, { ...options, onChange: onChange })); const setValue = (value) => rerender((0, jsx_runtime_1.jsx)(TextField_1.TextField, { ...options, value: value, onChange: onChange })); return { ...getTextInputInteractions(options.label, onChange, kindOf(options.type), () => { }), setValue }; } // Keeps the field genuinely controlled (mirroring how a real consumer feeds onChange back into value), so // typing accumulates correctly across keystrokes instead of resetting to the initial prop on every render. function ControlledTextField({ onChange, value: initialValue, ...options }) { const [value, setValue] = (0, react_2.useState)(initialValue); return ((0, jsx_runtime_1.jsx)(TextField_1.TextField, { ...options, value: value, onChange: (newValue) => { setValue(newValue); onChange(newValue); } })); } function getTextInputInteractions(label, onChange, kind = "text", setValidationMessage) { // The field's accessible role (and so which domQueries getter locates it) depends on `type`: number // inputs are "spinbutton", password inputs expose no role (queried by a dedicated helper instead), and // everything else (text, email, tel, url, search, time) is "textbox". const field = () => (kind === "number" ? testing_library_1.domQueries.getNumberInput(label) : kind === "password" ? testing_library_1.domQueries.getPasswordInput(label) : testing_library_1.domQueries.getTextInput(label)); return { shouldBeInDocument: () => { field().assertIsInTheDocument(); }, shouldHaveLabel: (labelText) => { testing_library_1.domQueries.getTextInput(labelText).assertIsInTheDocument(); }, shouldHavePlaceholder: (placeholder) => { (0, vitest_1.expect)(field().innerElement).toHaveAttribute("placeholder", placeholder); }, shouldHaveValue: (value) => { (0, vitest_1.expect)(field().innerElement).toHaveValue(value); }, shouldHaveDescription: (description) => { testing_library_1.domQueries.getText(description).assertIsInTheDocument(); }, shouldHaveError: (error) => { testing_library_1.domQueries.getText(error).assertIsInTheDocument(); }, shouldNotHaveError: (error) => { testing_library_1.domQueries.queryText(error).assertIsNotInTheDocument(); }, shouldHaveValidationMessage: (message) => { testing_library_1.domQueries.getText(message).assertIsInTheDocument(); }, shouldShowErrorValidationState: () => { testing_library_1.domQueries.getImage("Error").assertIsInTheDocument(); }, shouldShowSuccessValidationState: () => { testing_library_1.domQueries.getImage("Success").assertIsInTheDocument(); }, shouldNotShowAnyValidationState: () => { testing_library_1.domQueries.queryImage("Error").assertIsNotInTheDocument(); testing_library_1.domQueries.queryImage("Success").assertIsNotInTheDocument(); }, shouldBeDisabled: () => { field().assertIsDisabled(); }, shouldBeReadOnly: () => { (0, vitest_1.expect)(field().innerElement).toHaveAttribute("readonly"); }, shouldHaveFocus: () => { field().assertHasFocus(); }, shouldHaveRequiredMarker: () => { testing_library_1.domQueries.getText("(required)").assertIsInTheDocument(); }, shouldHaveOptionalMarker: () => { testing_library_1.domQueries.getText("(optional)").assertIsInTheDocument(); }, shouldHaveDefaultMarker: () => { testing_library_1.domQueries.getText("(Default)").assertIsInTheDocument(); }, shouldHaveType: (type) => { (0, vitest_1.expect)(field().innerElement).toHaveAttribute("type", type); }, shouldHaveName: (name) => { (0, vitest_1.expect)(field().innerElement).toHaveAttribute("name", name); }, shouldHaveAriaInvalid: (invalid) => { (0, vitest_1.expect)(field().innerElement).toHaveAttribute("aria-invalid", invalid.toString()); }, shouldHaveAriaDescribedByError: () => { const ariaDescribedBy = field().innerElement.getAttribute("aria-describedby"); (0, vitest_1.expect)(ariaDescribedBy).toContain("validation"); }, shouldHaveAriaDescribedByDescription: () => { const ariaDescribedBy = field().innerElement.getAttribute("aria-describedby"); (0, vitest_1.expect)(ariaDescribedBy).toContain("description"); }, shouldHaveAriaDescribedByBoth: () => { const ariaDescribedBy = field().innerElement.getAttribute("aria-describedby"); (0, vitest_1.expect)(ariaDescribedBy).toContain("description"); (0, vitest_1.expect)(ariaDescribedBy).toContain("validation"); }, type: async (text) => { if (kind === "number") { await testing_library_1.domQueries.getNumberInput(label).replaceNumber(Number(text)); } else { await testing_library_1.domQueries.getTextInput(label).replaceText(text); } }, clear: async () => { if (kind === "number") { await testing_library_1.domQueries.getNumberInput(label).clear(); } else { await testing_library_1.domQueries.getTextInput(label).replaceText(""); } }, attemptToType: async (text) => { await testing_library_1.domQueries.getTextInput(label).typeText(text); }, shouldHaveCalledOnChangeWith: (value) => { (0, vitest_1.expect)(onChange).toHaveBeenCalledWith(value); }, shouldNotHaveCalledOnChange: () => { (0, vitest_1.expect)(onChange).not.toHaveBeenCalled(); }, shouldHaveSuffix: (suffixText) => { testing_library_1.domQueries.getText(suffixText).assertIsInTheDocument(); }, shouldHaveSuffixElement: () => { field().assertIsInTheDocument(); }, shouldHaveSuffixButton: () => { testing_library_1.domQueries.getButton(undefined).assertIsInTheDocument(); }, shouldNotHaveSuffix: () => { testing_library_1.domQueries.queryButton(undefined).assertIsNotInTheDocument(); (0, vitest_1.expect)(field().innerElement).not.toHaveClass("inputStylesWithSuffix"); }, clickSuffixButton: async () => { await testing_library_1.domQueries.getButton(undefined).click(); }, shouldHaveCalledSuffixClick: (clickHandler) => { (0, vitest_1.expect)(clickHandler).toHaveBeenCalled(); }, shouldHaveActionButton: (accessibleName) => { testing_library_1.domQueries.getButton(accessibleName).assertIsInTheDocument(); }, clickActionButton: (accessibleName) => testing_library_1.domQueries.getButton(accessibleName).click(), shouldHaveCalledActionClick: (clickHandler) => { (0, vitest_1.expect)(clickHandler).toHaveBeenCalled(); }, shouldNotHaveAnyButtons: () => { (0, vitest_1.expect)(testing_library_1.domQueries.queryAllButtons(undefined)).toHaveLength(0); }, shouldHavePrefix: (prefixText) => { testing_library_1.domQueries.getText(prefixText).assertIsInTheDocument(); }, shouldNotHavePrefix: () => { (0, vitest_1.expect)(field().innerElement).not.toHaveClass("inputStylesWithPrefix"); }, shouldHavePrefixWithTitle: (title) => { testing_library_1.domQueries.getText(title).assertHasTitle(title); }, setExternalValidationMessage: (validationMessage) => { setValidationMessage(validationMessage); }, }; }