From 3dece871ea504ffd6b9bc391b41dc8a805864c0c Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Mon, 4 Nov 2024 15:03:36 -0800 Subject: [PATCH 1/5] add comment explaining the purpose behind compareAgentTrees (#608) --- src/util/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/util/index.ts b/src/util/index.ts index d5b950a5..4ba05790 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -107,6 +107,14 @@ export const roundToTimeStepPrecision = ( return Math.round(input * multiplier) / multiplier; }; +/** +Compare two instaces of UIDisplayData to see if they have the same agents +and display states. +This data structure is used to store different color settings. +We don't want to ever try and apply the color settings from one trajectory +to another, even if by chance they shared the same file name, or other +metadata. +*/ export const compareAgentTrees = (a: UIDisplayData, b: UIDisplayData) => { if (a.length !== b.length) { return false; From 482e5a0d9a7fd7033199dbb388e2a1eca8ac2e6c Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Tue, 12 Nov 2024 08:31:19 -0800 Subject: [PATCH 2/5] remove erroneous overflow bar (#611) --- src/components/SideBarContents/style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SideBarContents/style.css b/src/components/SideBarContents/style.css index 8f286278..62a43230 100644 --- a/src/components/SideBarContents/style.css +++ b/src/components/SideBarContents/style.css @@ -2,7 +2,6 @@ display: flex; flex-direction: column; height: 100%; - overflow-y: auto; } .container label { From 88eb4d675e4689556bf0156ced9c28c6238f019b Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Wed, 13 Nov 2024 15:31:44 -0800 Subject: [PATCH 3/5] keyboard navigation : feature/kb open dropdowns (#595) * prototype focus styling for header elements * use custom dropdown wrapper to keep stylesheets dry * focus style on AICS logo button * updated focus and hover stylings for header buttons and dropdown items * remove commented code * add custom hook for focus mangement in dropdown menus * move dropdown focus concerns from hook into CustomDropdown * use same mouseenter handler for button and menu in CustomDropdown * define button click handler outside props in CustomDropdown * remove nested conditional in key handler * preserve focus and keyboard opening of dropdowns after quick hover event --- src/components/CustomDropdown/index.tsx | 107 +++++++++++++++++++++++- src/components/NavButton/index.tsx | 62 +++++++------- src/constants/index.ts | 1 + src/constants/interfaces.ts | 6 ++ 4 files changed, 146 insertions(+), 30 deletions(-) diff --git a/src/components/CustomDropdown/index.tsx b/src/components/CustomDropdown/index.tsx index f5ab5fbf..4dbe58e9 100644 --- a/src/components/CustomDropdown/index.tsx +++ b/src/components/CustomDropdown/index.tsx @@ -1,6 +1,13 @@ -import React, { ReactNode } from "react"; +import React, { + KeyboardEventHandler, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; import { Dropdown, DropDownProps, MenuProps } from "antd"; -import { ButtonClass } from "../../constants/interfaces"; +import { ButtonClass, DropdownState } from "../../constants/interfaces"; +import { DROPDOWN_HOVER_DELAY } from "../../constants"; import NavButton from "../NavButton"; import styles from "./style.css"; @@ -22,16 +29,112 @@ const CustomDropdown: React.FC = ({ placement, disabled, }) => { + const [dropdownState, setDropdownState] = useState( + DropdownState.CLOSED + ); + + const triggerRef = useRef(null); + const dropdownRef = useRef(null); + const closeTimeoutRef = useRef(null); + + /** + * Prevents the menu wrapper from capturing focus, + * this prevents losing focus to the body + * when "Escape" is pressed. + */ + useEffect(() => { + const element = dropdownRef.current; + if (element) { + const menuElement = element.querySelector( + ".ant-dropdown-menu" + ) as HTMLElement; + if (menuElement) { + if (dropdownState === DropdownState.FORCED_OPEN) { + menuElement.setAttribute("tabIndex", "-1"); + } else if (dropdownState === DropdownState.CLOSED) { + menuElement.setAttribute("tabIndex", "0"); + } + } + } + }, [dropdownRef, dropdownState]); + + /** + * Manually handling keydown and hover behavior because + * our focus management overrides the defaults of the antd components. + */ + const openTriggers = new Set(["Enter", " ", "ArrowDown"]); + + const handleKeyDown: KeyboardEventHandler = (event) => { + if (event.key === "Escape") { + event.preventDefault(); + setDropdownState(DropdownState.CLOSED); + triggerRef.current?.focus(); + } + if ( + openTriggers.has(event.key) && + dropdownState !== DropdownState.FORCED_OPEN + ) { + event.preventDefault(); + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + setDropdownState(DropdownState.FORCED_OPEN); // Opened by keyboard + } + }; + + const handleMouseEnter = () => { + if (dropdownState === DropdownState.CLOSED) { + setDropdownState(DropdownState.OPEN); + } + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + }; + + const handleMouseLeaveWithDelay = () => { + if (dropdownState !== DropdownState.FORCED_OPEN) { + closeTimeoutRef.current = setTimeout(() => { + setDropdownState(DropdownState.CLOSED); + }, DROPDOWN_HOVER_DELAY); + } + }; + + const buttonClickHandler = () => { + setDropdownState( + dropdownState === DropdownState.CLOSED + ? DropdownState.OPEN + : DropdownState.CLOSED + ); + }; + return ( ( +
+ {menu} +
+ )} > } titleText={titleText} icon={icon} buttonType={buttonType} + clickHandler={buttonClickHandler} + onKeyDown={handleKeyDown} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeaveWithDelay} />
); diff --git a/src/components/NavButton/index.tsx b/src/components/NavButton/index.tsx index b6e7fe71..f989bbb2 100644 --- a/src/components/NavButton/index.tsx +++ b/src/components/NavButton/index.tsx @@ -1,7 +1,6 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode, forwardRef } from "react"; import { Button, ButtonProps } from "antd"; import classNames from "classnames"; - import styles from "./style.css"; import { ButtonClass } from "../../constants/interfaces"; @@ -13,32 +12,39 @@ export interface NavButtonProps extends ButtonProps { isDisabled?: boolean; } -const NavButton: React.FC = ({ - className, - titleText, - buttonType = ButtonClass.Action, - icon, - clickHandler, - isDisabled, - ...props -}) => { - // NavButtons default to action button styling, provide secondary or primary to override - const buttonClassNames = classNames( - className, - styles.navButton, - styles[buttonType], - { [styles.disabled]: isDisabled } - ); +const NavButton = forwardRef( + ( + { + className, + titleText, + buttonType = ButtonClass.Action, + icon, + clickHandler, + isDisabled, + ...props + }, + ref + ) => { + const buttonClassNames = classNames( + className, + styles.navButton, + styles[buttonType], + { [styles.disabled]: isDisabled } + ); + + return ( + + ); + } +); - return ( - - ); -}; +NavButton.displayName = "NavButton"; export default NavButton; diff --git a/src/constants/index.ts b/src/constants/index.ts index 273bf714..9e57f406 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -69,3 +69,4 @@ export const MAX_CONVERSION_FILE_SIZE = 2e8; // 200 MB export const CONTROLS_MIN_WIDTH = 650; export const CONTROLS_MIN_HEIGHT = 320; export const SCALE_BAR_MIN_WIDTH = 550; +export const DROPDOWN_HOVER_DELAY = 300; diff --git a/src/constants/interfaces.ts b/src/constants/interfaces.ts index 1abd73b2..16e71b44 100644 --- a/src/constants/interfaces.ts +++ b/src/constants/interfaces.ts @@ -92,3 +92,9 @@ export interface ColorChange { agent: SelectionEntry; color: string; } + +export enum DropdownState { + OPEN = "open", + CLOSED = "closed", + FORCED_OPEN = "forced_open", +} From 9fe9f69c7d523ac61463df06cbbba377e3d6a9dc Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Wed, 13 Nov 2024 15:31:59 -0800 Subject: [PATCH 4/5] fix: stop recording icon (#610) * proper classnames to display stop recording icon * correct positioning of record icons --- .../RecordMoviesComponent/index.tsx | 10 ++++--- .../RecordMoviesComponent/style.css | 26 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/RecordMoviesComponent/index.tsx b/src/components/RecordMoviesComponent/index.tsx index 294e1141..056a5c8d 100644 --- a/src/components/RecordMoviesComponent/index.tsx +++ b/src/components/RecordMoviesComponent/index.tsx @@ -63,14 +63,14 @@ const RecordMovieComponent = (props: RecordMovieComponentProps) => { * In this icon we are stacking glyphs to create multicolor icons via icomoon */ const startRecordingIcon = ( - +
- +
); const activeRecordingIcon = ( @@ -81,7 +81,11 @@ const RecordMovieComponent = (props: RecordMovieComponentProps) => { if (!isRecording) { return startRecordingIcon; } else if (isHovering) { - return "stop-record-icon"; + return classNames( + styles.iconContainer, + "icon-moon", + "stop-record-icon" + ); } else return activeRecordingIcon; }; diff --git a/src/components/RecordMoviesComponent/style.css b/src/components/RecordMoviesComponent/style.css index 660501af..660d5161 100644 --- a/src/components/RecordMoviesComponent/style.css +++ b/src/components/RecordMoviesComponent/style.css @@ -5,6 +5,8 @@ } .icon-container { + display: flex; + align-items: center; height: 100%; width: 100%; } @@ -22,18 +24,18 @@ } @keyframes pulse-red { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 #FF5252B3; - } - 70% { - transform: scale(1); - box-shadow: 0 0 0 10px #FF525200; - } - 100% { - transform: scale(0.95); - box-shadow: 0 0 0 0 #FF525200; - } + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 #ff5252b3; + } + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px #ff525200; + } + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 #ff525200; + } } .status-container { From 5c070a9484719acf88145da134cdee3fb545a8b2 Mon Sep 17 00:00:00 2001 From: Joe Heffernan Date: Mon, 18 Nov 2024 12:52:22 -0800 Subject: [PATCH 5/5] Feature/selector org (#606) * add compoundSelectors to redux * add ColorSettings enum and use container specific selector in ModelPanel * add currentColorSettings to selection branch state * rename ColorSetting from plural * fix typo in selection branch spread in getCurrentUIData test * add explanatory comment to compoundSelectors --- src/containers/ModelPanel/index.tsx | 6 +- src/containers/ModelPanel/selectors.test.ts | 75 ++++++++++++++++-- src/containers/ModelPanel/selectors.ts | 26 +++++- .../compoundSelectors.test.ts | 79 +++++++++++++++++++ src/state/compoundSelectors/index.ts | 34 ++++++++ src/state/selection/reducer.ts | 2 + src/state/selection/selectors/basic.ts | 2 + src/state/selection/types.ts | 5 ++ src/state/trajectory/selectors/index.ts | 26 +----- .../trajectory/selectors/selectors.test.ts | 67 +--------------- 10 files changed, 220 insertions(+), 102 deletions(-) create mode 100644 src/state/compoundSelectors/compoundSelectors.test.ts create mode 100644 src/state/compoundSelectors/index.ts diff --git a/src/containers/ModelPanel/index.tsx b/src/containers/ModelPanel/index.tsx index 66b3f57e..b21a2967 100644 --- a/src/containers/ModelPanel/index.tsx +++ b/src/containers/ModelPanel/index.tsx @@ -10,10 +10,7 @@ import { requestTrajectory, changeToNetworkedFile, } from "../../state/trajectory/actions"; -import { - getUiDisplayDataTree, - getIsNetworkedFile, -} from "../../state/trajectory/selectors"; +import { getIsNetworkedFile } from "../../state/trajectory/selectors"; import { AgentRenderingCheckboxMap, ChangeAgentsRenderingStateAction, @@ -44,6 +41,7 @@ import { getSelectAllVisibilityMap, getSelectNoneVisibilityMap, getIsSharedCheckboxIndeterminate, + getUiDisplayDataTree, } from "./selectors"; import styles from "./style.css"; diff --git a/src/containers/ModelPanel/selectors.test.ts b/src/containers/ModelPanel/selectors.test.ts index 8f2acb2c..50146f21 100644 --- a/src/containers/ModelPanel/selectors.test.ts +++ b/src/containers/ModelPanel/selectors.test.ts @@ -1,7 +1,9 @@ +import { initialState, State } from "../../state"; import { getSelectAllVisibilityMap, getSelectNoneVisibilityMap, getIsSharedCheckboxIndeterminate, + getUiDisplayDataTree, } from "./selectors"; const mockUiDisplayData = [ @@ -50,9 +52,8 @@ const mockUiDisplayData = [ describe("ModelPanel selectors", () => { describe("getSelectAllVisibilityMap", () => { it("Returns an agent visibility map with all possible states", () => { - const result = getSelectAllVisibilityMap.resultFunc( - mockUiDisplayData - ); + const result = + getSelectAllVisibilityMap.resultFunc(mockUiDisplayData); const expected = { agentWithChildren1: ["", "state1"], agentWithChildren2: ["", "state1"], @@ -64,9 +65,8 @@ describe("ModelPanel selectors", () => { describe("getSelectNoneVisibilityMap", () => { it("Returns an agent visibility map with none of the possible states", () => { - const result = getSelectNoneVisibilityMap.resultFunc( - mockUiDisplayData - ); + const result = + getSelectNoneVisibilityMap.resultFunc(mockUiDisplayData); const expected = { agentWithChildren1: [], agentWithChildren2: [], @@ -164,4 +164,67 @@ describe("ModelPanel selectors", () => { expect(result).toBe(true); }); }); + describe("getUiDisplayDataTree", () => { + it("returns an empty array if ui display data is empty", () => { + expect(getUiDisplayDataTree(initialState)).toStrictEqual([]); + }); + it("correctly maps agent display info to an array of display data", () => { + const state: State = { + ...initialState, + trajectory: { + ...initialState.trajectory, + defaultUIData: [ + { + name: "agent1", + displayStates: [], + color: "#bbbbbb", + }, + { + name: "agent2", + color: "#aaaaaa", + displayStates: [ + { + name: "state1", + id: "state1_id", + color: "#000000", + }, + { + name: "state2", + id: "state2_id", + color: "#000000", + }, + ], + }, + ], + }, + }; + + const expected = [ + { + title: "agent1", + key: "agent1", + children: [], + color: "#bbbbbb", + }, + { + title: "agent2", + key: "agent2", + color: "#aaaaaa", + children: [ + { + color: "#000000", + label: "state1", + value: "state1_id", + }, + { + color: "#000000", + label: "state2", + value: "state2_id", + }, + ], + }, + ]; + expect(getUiDisplayDataTree(state)).toStrictEqual(expected); + }); + }); }); diff --git a/src/containers/ModelPanel/selectors.ts b/src/containers/ModelPanel/selectors.ts index dd896220..da201768 100644 --- a/src/containers/ModelPanel/selectors.ts +++ b/src/containers/ModelPanel/selectors.ts @@ -2,9 +2,33 @@ import { createSelector } from "reselect"; import { isEmpty } from "lodash"; import { AgentDisplayNode } from "../../components/AgentTree"; -import { getUiDisplayDataTree } from "../../state/trajectory/selectors"; import { getAgentVisibilityMap } from "../../state/selection/selectors"; import { AgentRenderingCheckboxMap } from "../../state/selection/types"; +import { getCurrentUIData } from "../../state/compoundSelectors"; +import { UIDisplayData } from "@aics/simularium-viewer"; + +export const getUiDisplayDataTree = createSelector( + [getCurrentUIData], + (uiDisplayData: UIDisplayData) => { + if (!uiDisplayData.length) { + return []; + } + return uiDisplayData.map((agent) => ({ + title: agent.name, + key: agent.name, + color: agent.color, + children: agent.displayStates.length + ? [ + ...agent.displayStates.map((state) => ({ + label: state.name, + value: state.id, + color: state.color, + })), + ] + : [], + })); + } +); // Returns an agent visibility map that indicates all states should be visible export const getSelectAllVisibilityMap = createSelector( diff --git a/src/state/compoundSelectors/compoundSelectors.test.ts b/src/state/compoundSelectors/compoundSelectors.test.ts new file mode 100644 index 00000000..008ca7ff --- /dev/null +++ b/src/state/compoundSelectors/compoundSelectors.test.ts @@ -0,0 +1,79 @@ +import { getCurrentUIData } from "."; +import { initialState } from ".."; +import { ColorSetting } from "../selection/types"; + +describe("getCurrentUIData", () => { + it("returns empty array if default UI data has not been entered yet", () => { + expect(getCurrentUIData(initialState)).toEqual([]); + 1; + }); + it("returns selectedUIDisplayData if colorSetting is equal to ColorSetting.UserSelected", () => { + expect( + getCurrentUIData({ + ...initialState, + trajectory: { + ...initialState.trajectory, + defaultUIData: [ + { + name: "agent1", + displayStates: [], + color: "#bbbbbb", + }, + ], + }, + selection: { + ...initialState.selection, + currentColorSetting: ColorSetting.UserSelected, + selectedUIDisplayData: [ + { + name: "agent1", + displayStates: [], + color: "#000", + }, + ], + }, + }) + ).toEqual([ + { + name: "agent1", + displayStates: [], + color: "#000", + }, + ]); + }); + + it("returns defaultUIData if colorSetting is euqal to ColorSetting.Default", () => { + expect( + getCurrentUIData({ + ...initialState, + trajectory: { + ...initialState.trajectory, + defaultUIData: [ + { + name: "agent1", + displayStates: [], + color: "#bbbbbb", + }, + ], + }, + selection: { + ...initialState.selection, + currentColorSetting: ColorSetting.Default, + selectedUIDisplayData: [ + { + name: "agent1", + displayStates: [], + color: "#000", + }, + ], + }, + }) + ).toEqual([ + { + name: "agent1", + displayStates: [], + color: "#bbbbbb", + }, + ]); + }); +}); diff --git a/src/state/compoundSelectors/index.ts b/src/state/compoundSelectors/index.ts new file mode 100644 index 00000000..0f9f50d7 --- /dev/null +++ b/src/state/compoundSelectors/index.ts @@ -0,0 +1,34 @@ +import { createSelector } from "reselect"; +import { UIDisplayData } from "@aics/simularium-viewer"; + +import { getDefaultUIDisplayData } from "../trajectory/selectors"; +import { + getCurrentColorSetting, + getSelectedUIDisplayData, +} from "../selection/selectors"; +import { ColorSetting } from "../selection/types"; + +/** + * compoundSelectors are selectors that consume state from multiple branches + * of state, so don't belong in a particular branch's selectors file, + * and are consumed by multiple containers, and so don't belong in a particular + * container's selectors file. + */ + +export const getCurrentUIData = createSelector( + [getCurrentColorSetting, getSelectedUIDisplayData, getDefaultUIDisplayData], + ( + colorSetting: ColorSetting, + sessionData: UIDisplayData, + defaultData: UIDisplayData + ) => { + const fileHasBeenParsed = defaultData.length > 0; + if (!fileHasBeenParsed) { + return []; + } + if (colorSetting === ColorSetting.UserSelected) { + return sessionData; + } + return defaultData; + } +); diff --git a/src/state/selection/reducer.ts b/src/state/selection/reducer.ts index d0b06472..8b502b07 100644 --- a/src/state/selection/reducer.ts +++ b/src/state/selection/reducer.ts @@ -26,6 +26,7 @@ import { SetRecentColorsAction, SetSelectedAgentMetadataAction, SetSelectedUIDisplayDataAction, + ColorSetting, } from "./types"; export const initialState = { @@ -36,6 +37,7 @@ export const initialState = { recentColors: [], selectedAgentMetadata: {}, selectedUIDisplayData: [], + currentColorSetting: ColorSetting.Default, }; const actionToConfigMap: TypeToDescriptionMap = { diff --git a/src/state/selection/selectors/basic.ts b/src/state/selection/selectors/basic.ts index e9b91488..671aab37 100644 --- a/src/state/selection/selectors/basic.ts +++ b/src/state/selection/selectors/basic.ts @@ -13,3 +13,5 @@ export const getSelectedAgentMetadata = (state: State) => state.selection.selectedAgentMetadata; export const getSelectedUIDisplayData = (state: State) => state.selection.selectedUIDisplayData; +export const getCurrentColorSetting = (state: State) => + state.selection.currentColorSetting; diff --git a/src/state/selection/types.ts b/src/state/selection/types.ts index db427384..dc1239bc 100644 --- a/src/state/selection/types.ts +++ b/src/state/selection/types.ts @@ -87,3 +87,8 @@ export interface SetSelectedUIDisplayDataAction { payload: UIDisplayData; type: string; } + +export enum ColorSetting { + UserSelected = "userSelected", + Default = "default", +} diff --git a/src/state/trajectory/selectors/index.ts b/src/state/trajectory/selectors/index.ts index ecd074b2..ea882f4f 100644 --- a/src/state/trajectory/selectors/index.ts +++ b/src/state/trajectory/selectors/index.ts @@ -1,12 +1,11 @@ import { createSelector } from "reselect"; -import { UIDisplayData } from "@aics/simularium-viewer"; import { isNetworkSimFileInterface, LocalSimFile, NetworkedSimFile, } from "../types"; -import { getSimulariumFile, getDefaultUIDisplayData } from "./basic"; +import { getSimulariumFile } from "./basic"; export const getIsNetworkedFile = createSelector( [getSimulariumFile], @@ -18,27 +17,4 @@ export const getIsNetworkedFile = createSelector( } ); -export const getUiDisplayDataTree = createSelector( - [getDefaultUIDisplayData], - (uiDisplayData: UIDisplayData) => { - if (!uiDisplayData.length) { - return []; - } - return uiDisplayData.map((agent) => ({ - title: agent.name, - key: agent.name, - color: agent.color, - children: agent.displayStates.length - ? [ - ...agent.displayStates.map((state) => ({ - label: state.name, - value: state.id, - color: state.color, - })), - ] - : [], - })); - } -); - export * from "./basic"; diff --git a/src/state/trajectory/selectors/selectors.test.ts b/src/state/trajectory/selectors/selectors.test.ts index f236abe7..0fb77378 100644 --- a/src/state/trajectory/selectors/selectors.test.ts +++ b/src/state/trajectory/selectors/selectors.test.ts @@ -1,7 +1,7 @@ import { initialState } from "../../index"; import { State } from "../../types"; -import { getIsNetworkedFile, getUiDisplayDataTree } from "."; +import { getIsNetworkedFile } from "."; describe("trajectory composed selectors", () => { describe("getIsNetworkedFile", () => { @@ -37,69 +37,4 @@ describe("trajectory composed selectors", () => { expect(getIsNetworkedFile(state)).toBe(true); }); }); - - describe("getUiDisplayDataTree", () => { - it("returns an empty array if ui display data is empty", () => { - expect(getUiDisplayDataTree(initialState)).toStrictEqual([]); - }); - it("correctly maps agent display info to an array of display data", () => { - const state: State = { - ...initialState, - trajectory: { - ...initialState.trajectory, - defaultUIData: [ - { - name: "agent1", - displayStates: [], - color: "#bbbbbb", - }, - { - name: "agent2", - color: "#aaaaaa", - displayStates: [ - { - name: "state1", - id: "state1_id", - color: "#000000", - }, - { - name: "state2", - id: "state2_id", - color: "#000000", - }, - ], - }, - ], - }, - }; - - const expected = [ - { - title: "agent1", - key: "agent1", - children: [], - color: "#bbbbbb", - }, - { - title: "agent2", - key: "agent2", - color: "#aaaaaa", - children: [ - { - color: "#000000", - label: "state1", - value: "state1_id", - }, - { - color: "#000000", - label: "state2", - value: "state2_id", - }, - ], - }, - ]; - - expect(getUiDisplayDataTree(state)).toStrictEqual(expected); - }); - }); });