Skip to content

Commit

Permalink
Merge pull request #1419 from rohinp14/application-settings-management
Browse files Browse the repository at this point in the history
Input Box Support for User Settings + Panel Tests
  • Loading branch information
heswell authored Jul 26, 2024
2 parents 2be4bce + 37e0688 commit 2cb881b
Show file tree
Hide file tree
Showing 11 changed files with 800 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
DefaultUserSettingsForm,
ScrollableUserSettingsPanel,
VariedFormControlUserSettingsForm,
} from "../../../../../showcase/src/examples/Shell/UserSettings.examples";




// Tests for current default user settings panel with only one toggle button form control
describe("Given a single toggle button form control", () => {
it("should have two buttons, with one selected", () => {
cy.mount(<DefaultUserSettingsForm />);
cy.contains("button", "light").should("have.attr", "aria-checked", "true");
cy.contains("button", "dark").should("have.attr", "aria-checked", "false");
});
describe("WHEN the toggle buttons are selected ", () => {
it("should become selected", () => {
cy.mount(<DefaultUserSettingsForm />);
// Clicks and checks the dark button
cy.contains("button", "dark").click();
cy.should("have.attr", "aria-checked", "true");
cy.contains("button", "light").should(
"have.attr",
"aria-checked",
"false"
);
// Clicks and checks the light button
cy.contains("button", "light").click();
cy.should("have.attr", "aria-checked", "true");
cy.contains("button", "dark").should(
"have.attr",
"aria-checked",
"false"
);
});
});
});

// Tests for settings form with multiple components
describe("Given a form with multiple form controls of different types", () => {
it("the button element should have the correct attributes", () => {
cy.mount(<VariedFormControlUserSettingsForm />);
cy.get('[data-field="themeMode"]')
.find("button.saltToggleButton")
.contains("dark")
.should("have.attr", "aria-checked", "false");
cy.get('[data-field="themeMode"]')
.find("button.saltToggleButton")
.contains("light")
.should("have.attr", "aria-checked", "true");
});
//Think of better attributes to check - something
it("the dropdown elements should have the correct attributes", () => {
cy.mount(<VariedFormControlUserSettingsForm />);
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.should("have.attr", "role", "combobox");
cy.get('[data-field="region"]')
.find("button.saltDropdown")
.should("have.attr", "role", "combobox");
});
it("the switch element should have the correct attributes", () => {
cy.mount(<VariedFormControlUserSettingsForm />);
cy.get(".saltSwitch-input").should("have.attr", "type", "checkbox");
});
describe("WHEN the dropdown is changed", () => {
it("should change the displayed text on the dropdown", () => {
cy.mount(<VariedFormControlUserSettingsForm />);
//Tests the date format pattern dropdown
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.click();
cy.contains(".saltOption", "mm/dd/yyyy").click();
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.should("have.text", "mm/dd/yyyy");
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.click();
cy.contains(".saltOption", "dd MM yyyy").click();
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.should("have.text", "dd MM yyyy");
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.click();
cy.contains(".saltOption", "dd/mm/yyyy").click();
cy.get('[data-field="dateFormatPattern"]')
.find("button.saltDropdown")
.should("have.text", "dd/mm/yyyy");
// Tests the region dropdowns
cy.get('[data-field="region"]').find("button.saltDropdown").click();
cy.contains(".saltOption", "Asia Pacific").click();
cy.get('[data-field="region"]')
.find("button.saltDropdown")
.should("have.text", "Asia Pacific");
cy.get('[data-field="region"]').find("button.saltDropdown").click();
cy.contains(".saltOption", "Europe, Middle East & Africa").click();
cy.get('[data-field="region"]')
.find("button.saltDropdown")
.should("have.text", "Europe, Middle East & Africa");
cy.get('[data-field="region"]').find("button.saltDropdown").click();
cy.contains(".saltOption", "US").click();
cy.get('[data-field="region"]')
.find("button.saltDropdown")
.should("have.text", "US");
});
});
describe("WHEN the switch form controlled is clicked", () => {
it("should change colour", () => {
cy.mount(<VariedFormControlUserSettingsForm />);
cy.get('[data-field="greyscale"]').find("input.saltSwitch-input").click();
cy.get('[data-field="greyscale"]')
.find(".saltIcon")
.should("have.attr", "aria-label", "success small solid");
});
});
});

// Tests for scrolling functionality of form with multiple components
describe("Given a form with a large number of components", () => {
it("should scroll", () => {
cy.mount(<ScrollableUserSettingsPanel />);
cy.get('[data-field="field1"]').should("be.visible");
cy.get('[data-field="field45"]').not("be.visible");
cy.scrollTo("bottom");
cy.wait(500);
cy.get('[data-field="field1"]').not("be.visible");
cy.get('[data-field="field45"]').should("be.visible");
cy.scrollTo("top");
});
});
116 changes: 89 additions & 27 deletions vuu-ui/packages/vuu-shell/src/user-settings/SettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import {
DropdownProps,
FormField,
FormFieldLabel,
Input,
Option,
Switch,
ToggleButton,
ToggleButtonGroup,
ToggleButtonGroupProps,
} from "@salt-ds/core";
import { VuuInput } from "@finos/vuu-ui-controls";
import {
FormEventHandler,
HTMLAttributes,
SyntheticEvent,
useCallback,
useState,
} from "react";

export interface SettingsSchema {
properties: SettingsProperty[];
}
Expand Down Expand Up @@ -84,13 +84,20 @@ const defaultPropertyValue: Record<
};

// Determine the form control type to be displayed
export function getFormControl(
property: SettingsProperty,
changeHandler: FormEventHandler,
selectHandler: DropdownProps["onSelectionChange"],
currentValue: VuuRowDataItemType = property.defaultValue ??
defaultPropertyValue[property.type]
) {
export function FormControl({
property,
changeHandler,
selectHandler,
inputHandler,
currentValue = property.defaultValue ?? defaultPropertyValue[property.type],
}: {
property: SettingsProperty;
changeHandler: FormEventHandler;
selectHandler: DropdownProps["onSelectionChange"];
inputHandler: FormEventHandler;
currentValue: VuuRowDataItemType;
}) {
const [value, setValue] = useState(currentValue);
if (isBooleanProperty(property)) {
const checked =
typeof currentValue === "boolean"
Expand Down Expand Up @@ -137,22 +144,62 @@ export function getFormControl(
);
}
} else {
const value = isStringOrNumber(currentValue)
? currentValue
: isStringOrNumber(property.defaultValue)
? property.defaultValue
: "";
return <Input value={value} />;
const valid = isValidInput(currentValue, property.type);
const content = getTooltipContent(property.type, valid);
const TooltipProps = {
tooltipContent: content,
};
return (
<VuuInput
key={property.name}
onCommit={inputHandler}
onChange={(e) => setValue((e.target as HTMLInputElement).value)}
validationStatus={valid}
TooltipProps={TooltipProps}
value={value as string}
/>
);
}
return null;
}

//Validation logic for input boxes
const isValidInput = (value: unknown, type: unknown) => {
if (value === "") {
return undefined;
}
if (type === "string") {
return "success";
} else if (type === "number") {
if (Number.isNaN(Number(value))) {
return "error";
}
return "success";
}
};

//Function to Generate Tooltip Content
function getTooltipContent(type: string, valid: string | undefined) {
if (valid === "error") {
if (type === "number") {
return <p>Field is expecting a number</p>;
} else if (type === "string") {
return <p>Field is expecting a string</p>;
} else {
return <p>Please contact Admin for more information on expected type</p>;
}
} else {
return undefined;
}
}

export type SettingsFormProps = SettingsProps & HTMLAttributes<HTMLDivElement>;

// Generates application settings form component
export const SettingsForm = ({
settingsSchema: applicationSettingsSchema,
settings: applicationSettings,
onSettingChanged: onApplicationSettingChanged,
settingsSchema,
settings,
onSettingChanged,
...htmlAttributes
}: SettingsFormProps) => {
const getFieldNameFromEventTarget = (evt: SyntheticEvent) => {
Expand All @@ -164,36 +211,51 @@ export const SettingsForm = ({
}
};

// Change Handler for toggle and input buttons
// Change Handler for toggle and switch buttons
const changeHandler = useCallback<FormEventHandler>(
(event) => {
const fieldName = getFieldNameFromEventTarget(event);
const { checked, value } = event.target as HTMLInputElement;
onApplicationSettingChanged(fieldName, checked ?? value);
onSettingChanged(fieldName, checked ?? value);
},
[onApplicationSettingChanged]
[onSettingChanged]
);

// Change handler for selection form controls
const selectHandler = useCallback(
(event: SyntheticEvent, [selected]: string[]) => {
const fieldName = getFieldNameFromEventTarget(event);
onApplicationSettingChanged(fieldName, selected);
onSettingChanged(fieldName, selected);
},
[onApplicationSettingChanged]
[onSettingChanged]
);

// Change Handler for input boxes
const inputHandler = useCallback<FormEventHandler>(
(event) => {
const fieldName = getFieldNameFromEventTarget(event);
const { value } = event.target as HTMLInputElement;
if (!Number.isNaN(Number(value)) && value != "") {
const numValue = Number(value);
onSettingChanged(fieldName, numValue);
} else {
onSettingChanged(fieldName, value);
}
},
[onSettingChanged]
);
return (
<div {...htmlAttributes}>
{applicationSettingsSchema.properties.map((property) => (
{settingsSchema.properties.map((property) => (
<FormField data-field={property.name} key={property.name}>
<FormFieldLabel>{property.label}</FormFieldLabel>
{getFormControl(
{FormControl({
property,
changeHandler,
selectHandler,
applicationSettings[property.name]
)}
inputHandler,
currentValue: settings[property.name],
})}
</FormField>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vuuUserSettingsPanel {
height: 100%;
}
overflow: auto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useWindow } from "@salt-ds/window";
import { HTMLAttributes } from "react";
import { useApplicationSettings } from "../application-provider";
import { SettingsForm } from "./SettingsForm";
import cx from "clsx";

import userSettingsPanelCss from "./UserSettingsPanel.css";

Expand Down Expand Up @@ -31,7 +32,7 @@ export const UserSettingsPanel = ({
// We could render a list of input boxes but lets require a schema for now.
if (userSettingsSchema) {
return (
<div {...htmlAttributes} className={classBase}>
<div {...htmlAttributes} className={cx(classBase, "vuuScrollable")}>
<SettingsForm
settings={userSettings}
settingsSchema={userSettingsSchema}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DefaultVuuInput,
VuuInputWithErrorMessageTooltipRight,
VuuInputWithValidation,
} from "../../../../../../showcase/src/examples/UiControls/VuuInput.examples";

describe("VuuInput", () => {
Expand Down Expand Up @@ -31,3 +32,45 @@ describe("VuuInput", () => {
});
});
});

describe("Given a VuuInput box with input validation", () => {
describe("WHEN invalid input is provided", () => {
it("Then box will turn red and tooltip will display on hover", () => {
cy.mount(<VuuInputWithValidation />);
cy.findByTestId("vuu-input").type("hello{enter}");
cy.findAllByTestId("vuu-input").find(".vuuInput-errorIcon").realHover();
cy.wait(500);
cy.findAllByTestId("vuu-input").should("have.class", "vuuInput-error");
cy.get(".vuuTooltip").should("be.visible");
});
});
describe("WHEN valid input is provided", () => {
it("Then the box will turn green and no tooltip will be displayed", () => {
cy.mount(<VuuInputWithValidation />);
cy.findByTestId("vuu-input").type("012345{enter}");
cy.findByTestId("vuu-input").should("have.class", "saltInput-success");
cy.findByRole("img").should("have.class", "saltStatusAdornment-success");
});
});
describe("WHEN no input is provded", () => {
it("Then the box will not change", () => {
cy.mount(<VuuInputWithValidation />);
cy.findByTestId("vuu-input").type("{enter}");
cy.findByTestId("vuu-input").should("have.class", "saltInput-primary");
});
});
describe("WHEN input provided overflows", () => {
it("Then box will store the complete value", () => {
cy.mount(<VuuInputWithValidation />);
cy.findByTestId("vuu-input").type(
"01234567890123456789012345678901234567890123456789012345678901234567890{enter}"
);
cy.findAllByTestId("vuu-input")
.find("input.saltInput-input")
.should(
"have.value",
"01234567890123456789012345678901234567890123456789012345678901234567890"
);
});
});
});
Loading

0 comments on commit 2cb881b

Please sign in to comment.