diff --git a/.github/workflows.md b/.github/workflows.md new file mode 100644 index 0000000000..fe57bb0401 --- /dev/null +++ b/.github/workflows.md @@ -0,0 +1,6 @@ + +The following workflows are allowed on: https://github.com/qdraw/starsky/settings/actions + +``` +ljharb/rebase@master@,peter-evans/create-pull-request@*,EndBug/add-and-commit@*,everlytic/branch-merge*,docker/login-action@*,docker/metadata-action@*,docker/build-push-action@*,codecov/codecov-action@*,chizkiyahu/delete-untagged-ghcr-action@*,cypress-io/github-action@*,softprops/action-gh-release@* +``` \ No newline at end of file diff --git a/starsky/Dockerfile b/starsky/Dockerfile index 915e47a5cc..a08b82b6bc 100644 --- a/starsky/Dockerfile +++ b/starsky/Dockerfile @@ -60,7 +60,7 @@ RUN \ RUN \ if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - echo $TARGETPLATFORM ; \ + echo "$TARGETPLATFORM" ; \ dotnet publish -c release -o out --runtime linux-x64 --self-contained false --no-restore ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ dotnet publish -c release -o out --runtime linux-arm64 --self-contained false --no-restore ; \ diff --git a/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs b/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs index 1587e16b2f..aed07aacd4 100644 --- a/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs +++ b/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs @@ -53,6 +53,7 @@ public static async Task> StreamFile(this HttpRequest request, } [SuppressMessage("Usage", "S125:Remove this commented out code")] + [SuppressMessage("Usage", "S2589:contentDisposition null")] public static async Task> StreamFile(string? contentType, Stream requestBody, AppSettings appSettings, ISelectorStorage selectorStorage, string? headerFileName = null) { diff --git a/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs b/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs index 14ae19d848..7fcd438f51 100644 --- a/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs +++ b/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs @@ -93,7 +93,7 @@ public string ScriptPlain internal string? GetCurrentUserId() { - if (_httpContext == null || _httpContext?.HttpContext?.User.Identity?.IsAuthenticated == false) + if (_httpContext?.HttpContext?.User.Identity?.IsAuthenticated == false) { return string.Empty; } diff --git a/starsky/starsky/clientapp/.storybook/main.ts b/starsky/starsky/clientapp/.storybook/main.ts index a12290b0c6..6adf216825 100644 --- a/starsky/starsky/clientapp/.storybook/main.ts +++ b/starsky/starsky/clientapp/.storybook/main.ts @@ -5,7 +5,6 @@ const config: StorybookConfig = { addons: [ "@storybook/addon-links", "@storybook/addon-essentials", - "@storybook/addon-onboarding", "@storybook/addon-interactions" ], framework: { diff --git a/starsky/starsky/clientapp/package.json b/starsky/starsky/clientapp/package.json index 904a2ef4db..a99212c890 100644 --- a/starsky/starsky/clientapp/package.json +++ b/starsky/starsky/clientapp/package.json @@ -168,7 +168,7 @@ "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/ban-types": "warn", "no-case-declarations": "warn", - "react/display-name": "warn", + "react/display-name": "off", "react/prop-types": "warn", "@typescript-eslint/no-loss-of-precision": "warn", "react/react-in-jsx-scope": "off" diff --git a/starsky/starsky/clientapp/src/components/atoms/drop-area/drop-area.tsx b/starsky/starsky/clientapp/src/components/atoms/drop-area/drop-area.tsx index 066986191c..1174204c0a 100644 --- a/starsky/starsky/clientapp/src/components/atoms/drop-area/drop-area.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/drop-area/drop-area.tsx @@ -34,7 +34,7 @@ const containsFiles = (event: DragEvent) => { * @param props Endpoints, settings to enable drag 'n drop, add extra classes */ const DropArea: React.FunctionComponent = (props) => { - const [dragActive, setDrag] = useState(false); + const [dragActive, setDragActive] = useState(false); const [dragTarget, setDragTarget] = useState( document.createElement("span") as Element ); @@ -65,7 +65,7 @@ const DropArea: React.FunctionComponent = (props) => { */ const onDrop = (event: DragEvent) => { event.preventDefault(); - setDrag(false); + setDragActive(false); if (!event.dataTransfer) return; @@ -89,7 +89,7 @@ const DropArea: React.FunctionComponent = (props) => { const onDragEnter = (event: DragEvent) => { event.preventDefault(); if (!event.target || !containsFiles(event)) return; - setDrag(true); + setDragActive(true); setDragTarget(event.target as Element); setDropEffect(event); }; @@ -103,7 +103,7 @@ const DropArea: React.FunctionComponent = (props) => { event.preventDefault(); if (!containsFiles(event) || (event.target as Element) !== dragTarget) return; - setDrag(false); + setDragActive(false); }; /** @@ -113,7 +113,7 @@ const DropArea: React.FunctionComponent = (props) => { const onDragOver = (event: DragEvent) => { event.preventDefault(); if (!containsFiles(event)) return; - setDrag(true); + setDragActive(true); setDropEffect(event); }; diff --git a/starsky/starsky/clientapp/src/components/atoms/file-hash-image/file-hash-image.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/file-hash-image/file-hash-image.spec.tsx index a38819d399..0c08bb8e3a 100644 --- a/starsky/starsky/clientapp/src/components/atoms/file-hash-image/file-hash-image.spec.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/file-hash-image/file-hash-image.spec.tsx @@ -52,10 +52,7 @@ describe("FileHashImage", () => { // need to await here const component = await render( - + ); expect(detectRotationSpy).toBeCalled(); @@ -94,10 +91,7 @@ describe("FileHashImage", () => { .mockImplementationOnce(() => mockGetIConnectionDefault); const component = render( - + ); // need to await here @@ -125,10 +119,7 @@ describe("FileHashImage", () => { .mockImplementationOnce(() => mockGetIConnectionDefault); const component = render( - + ); // need to await here diff --git a/starsky/starsky/clientapp/src/components/atoms/file-hash-image/pan-and-zoom-image.tsx b/starsky/starsky/clientapp/src/components/atoms/file-hash-image/pan-and-zoom-image.tsx index 5c2c1bf373..5d83092545 100644 --- a/starsky/starsky/clientapp/src/components/atoms/file-hash-image/pan-and-zoom-image.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/file-hash-image/pan-and-zoom-image.tsx @@ -31,7 +31,7 @@ export type PositionObject = { * @param param0: IPanAndZoomImage */ const PanAndZoomImage = ({ src, id, ...props }: IPanAndZoomImage) => { - const [isPanning, setPanning] = useState(false); + const [panning, setPanning] = useState(false); const [image, setImage] = useState({ width: 0, height: 0 } as ImageObject); const defaultPosition = { @@ -66,12 +66,12 @@ const PanAndZoomImage = ({ src, id, ...props }: IPanAndZoomImage) => { }; // for performance reasons the classes is kept in a function const mouseMove = (event: MouseEvent) => { - new OnMoveMouseTouchAction(isPanning, setPosition, position).mousemove( + new OnMoveMouseTouchAction(panning, setPosition, position).mousemove( event ); }; const touchMove = (event: TouchEvent) => { - new OnMoveMouseTouchAction(isPanning, setPosition, position).touchMove( + new OnMoveMouseTouchAction(panning, setPosition, position).touchMove( event ); }; @@ -121,7 +121,7 @@ const PanAndZoomImage = ({ src, id, ...props }: IPanAndZoomImage) => { let className = "pan-zoom-image-container"; if (position.z !== 1) { - className = !isPanning + className = !panning ? "pan-zoom-image-container grab" : "pan-zoom-image-container is-panning"; } diff --git a/starsky/starsky/clientapp/src/components/atoms/list-image/list-image.tsx b/starsky/starsky/clientapp/src/components/atoms/list-image/list-image.tsx index 09bb30b2c2..c255beb262 100644 --- a/starsky/starsky/clientapp/src/components/atoms/list-image/list-image.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/list-image/list-image.tsx @@ -22,9 +22,7 @@ const ListImage: React.FunctionComponent = memo((props) => { const [src, setSrc] = useState(props.fileHash); - const [alwaysLoadImage] = useState( - localStorage.getItem("alwaysLoadImage") === "true" - ); + const alwaysLoadImage = localStorage.getItem("alwaysLoadImage") === "true"; // Reset Loading after changing page const [isLoading, setIsLoading] = useState(true); @@ -45,7 +43,8 @@ const ListImage: React.FunctionComponent = memo((props) => { // to stop loading images after a url change const history = useLocation(); - const [historyLocation] = useState(history.location.search); + const historyLocation = history.location.search; + useEffect(() => { // use ?f only to support details // need to refresh diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.stories.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.stories.tsx new file mode 100644 index 0000000000..95f4d44ea4 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.stories.tsx @@ -0,0 +1,24 @@ +import MenuOption from "./menu-option"; + +export default { + title: "components/atoms/menu-option" +}; + +export const Default = () => { + return ( + {}} + testName="test" + isReadOnly={false} + setEnableMoreMenu={(value) => { + alert(value); + }} + /> + ); +}; + +Default.story = { + name: "default" +}; diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx index 38446645cb..814f5b8f51 100644 --- a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx @@ -11,6 +11,7 @@ interface IMenuOptionProps { setEnableMoreMenu?: React.Dispatch>; } +// eslint-disable-next-line react/display-name const MenuOption: React.FunctionComponent = memo( ({ localization, @@ -24,6 +25,17 @@ const MenuOption: React.FunctionComponent = memo( const language = new Language(settings.language); const Message = language.key(localization); + function onClickHandler() { + if (isReadOnly) { + return; + } + // close menu + if (setEnableMoreMenu) { + setEnableMoreMenu(false); + } + set(!isSet); + } + return ( <> { @@ -31,14 +43,9 @@ const MenuOption: React.FunctionComponent = memo( tabIndex={0} data-test={testName} className={!isReadOnly ? "menu-option" : "menu-option disabled"} - onClick={() => { - if (!isReadOnly) { - // close menu - if (setEnableMoreMenu) { - setEnableMoreMenu(false); - } - set(!isSet); - } + onClick={onClickHandler} + onKeyDown={(event) => { + event.key === "Enter" && onClickHandler(); }} > {Message} diff --git a/starsky/starsky/clientapp/src/components/atoms/modal/modal.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/modal/modal.spec.tsx index 09cbf61fba..e5ef8b6fcb 100644 --- a/starsky/starsky/clientapp/src/components/atoms/modal/modal.spec.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/modal/modal.spec.tsx @@ -1,4 +1,9 @@ -import { render, RenderResult, screen } from "@testing-library/react"; +import { + fireEvent, + render, + RenderResult, + screen +} from "@testing-library/react"; import Modal from "./modal"; describe("Modal", () => { @@ -26,7 +31,7 @@ describe("Modal", () => { }; } - it("modal-exit-button", () => { + it("modal-exit-button click", () => { const { handleExit, element } = renderModal(); screen.queryAllByTestId("modal-exit-button")[0].click(); @@ -34,6 +39,28 @@ describe("Modal", () => { element.unmount(); }); + it("modal-exit-button keyDown tab ignores", () => { + const { handleExit, element } = renderModal(); + + const menuOption = screen.queryAllByTestId("modal-exit-button")[0]; + + fireEvent.keyDown(menuOption, { key: "Tab" }); + + expect(handleExit).toBeCalledTimes(0); + element.unmount(); + }); + + it("modal-exit-button keyDown enter", () => { + const { handleExit, element } = renderModal(); + + const menuOption = screen.queryAllByTestId("modal-exit-button")[0]; + + fireEvent.keyDown(menuOption, { key: "Enter" }); + + expect(handleExit).toBeCalledTimes(1); + element.unmount(); + }); + it("modal-bg", () => { const { handleExit, element } = renderModal(); diff --git a/starsky/starsky/clientapp/src/components/atoms/modal/modal.tsx b/starsky/starsky/clientapp/src/components/atoms/modal/modal.tsx index 0801fe9a90..f296a24cdf 100644 --- a/starsky/starsky/clientapp/src/components/atoms/modal/modal.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/modal/modal.tsx @@ -42,13 +42,13 @@ export default function Modal({ const language = new Language(settings.language); const MessageCloseDialog = language.text("Sluiten", "Close"); - const [hasUpdated, forceUpdate] = useState(false); + const [forceUpdate, setForceUpdate] = useState(false); const exitButton = useRef(null); const modal = useRef(null); useEffect(() => { - return modalInsertPortalDiv(modal, hasUpdated, forceUpdate, id); + return modalInsertPortalDiv(modal, forceUpdate, setForceUpdate, id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -68,6 +68,9 @@ export default function Modal({ return ReactDOM.createPortal(
ifModalOpenHandleExit(event, handleExit)} + onKeyDown={(event) => { + event.key === "Enter" && handleExit(); + }} data-test={dataTest} className={`modal-bg ${ isOpen ? ` ${ModalOpenClassName} ` + className : "" diff --git a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.stories.tsx b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.stories.tsx index 0ad05eaaad..4a20cb42c9 100644 --- a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.stories.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.stories.tsx @@ -5,7 +5,15 @@ export default { }; export const Default = () => { - return {}}>test; + return ( + { + alert("test"); + }} + > + test + + ); }; Default.story = { diff --git a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx index 7e43875870..8c34277814 100644 --- a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx @@ -47,6 +47,9 @@ const MoreMenu: React.FunctionComponent = ({
setEnableMoreMenu(false)} + onKeyDown={(event) => { + event.key === "Enter" && setEnableMoreMenu(false); + }} data-test="menu-context" className={ enableMoreMenu ? "menu-context" : "menu-context menu-context--hide" diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx index 6b84c26092..6b921df8da 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx @@ -76,7 +76,7 @@ const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => { } as ISidebarUpdate); // Add/Hide disabled state - const [isInputEnabled, setInputEnabled] = useState(false); + const [inputEnabled, setInputEnabled] = useState(false); // preloading icon const [isLoading, setIsLoading] = useState(false); @@ -217,7 +217,7 @@ const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => { contentEditable={!state.isReadOnly && select.length !== 0} > - {isInputEnabled && select.length !== 0 ? ( + {inputEnabled && select.length !== 0 ? ( )} - {isInputEnabled && select.length !== 0 ? ( + {inputEnabled && select.length !== 0 ? (
); }; diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.spec.tsx index b6a1b2da7f..fdc60755e8 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.spec.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.spec.tsx @@ -1,4 +1,10 @@ -import { act, render, screen } from "@testing-library/react"; +import { + act, + createEvent, + fireEvent, + render, + screen +} from "@testing-library/react"; import { IFileIndexItem, newIFileIndexItemArray @@ -64,6 +70,58 @@ describe("archive-sidebar-selection-list", () => { component.unmount(); }); + it("toggleSelection keyboard keyDown it hits", () => { + const component = render( + + ); + + const spy = jest.spyOn(URLPath.prototype, "toggleSelection"); + + const selectionList = screen.queryByTestId( + "sidebar-selection-list" + ) as HTMLElement; + const element = selectionList.children[0].querySelector( + ".close" + ) as HTMLElement; + + act(() => { + const inputEvent = createEvent.keyDown(element, { key: "Enter" }); + fireEvent(element, inputEvent); + }); + + expect(spy).toBeCalledTimes(1); + + spy.mockClear(); + + component.unmount(); + }); + + it("toggleSelection keyboard keyDown it ignores", () => { + const component = render( + + ); + + const spy = jest.spyOn(URLPath.prototype, "toggleSelection"); + + const selectionList = screen.queryByTestId( + "sidebar-selection-list" + ) as HTMLElement; + const element = selectionList.children[0].querySelector( + ".close" + ) as HTMLElement; + + act(() => { + const inputEvent = createEvent.keyDown(element, { key: "Tab" }); + fireEvent(element, inputEvent); + }); + + expect(spy).toBeCalledTimes(0); + + spy.mockClear(); + + component.unmount(); + }); + it("allSelection", () => { const component = render( diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx index ff94f7a2ff..d5f118d188 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx @@ -12,6 +12,7 @@ interface IDetailViewSidebarSelectionListProps { } const ArchiveSidebarSelectionList: React.FunctionComponent = + // eslint-disable-next-line react/display-name memo((props) => { // content const settings = useGlobalSettings(); @@ -92,11 +93,15 @@ const ArchiveSidebarSelectionList: React.FunctionComponent (
  • - { + event.key === "Enter" && toggleSelection(item); + }} onClick={() => toggleSelection(item)} className="close" + title={item} /> - {item} + {item}
  • ) ) diff --git a/starsky/starsky/clientapp/src/components/molecules/color-class-filter/color-class-filter.tsx b/starsky/starsky/clientapp/src/components/molecules/color-class-filter/color-class-filter.tsx index f2076cf21a..b6c46359fb 100644 --- a/starsky/starsky/clientapp/src/components/molecules/color-class-filter/color-class-filter.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/color-class-filter/color-class-filter.tsx @@ -51,20 +51,20 @@ const ColorClassFilter: React.FunctionComponent = memo( collectionsCount: props.itemsCount ? props.itemsCount : 0 }; } - const [colorClassUsage, setIsColorClassUsage] = useState( + const [colorClassUsage, setColorClassUsage] = useState( props.colorClassUsage ); useEffect(() => { - setIsColorClassUsage(state.colorClassUsage); + setColorClassUsage(state.colorClassUsage); // it should not update when the prop are changing }, [state.colorClassUsage]); - const [colorClassActiveList, setIsColorClassActiveList] = useState( + const [colorClassActiveList, setColorClassActiveList] = useState( props.colorClassActiveList ); useEffect(() => { - setIsColorClassActiveList(state.colorClassActiveList); + setColorClassActiveList(state.colorClassActiveList); // it should not update when the prop are changing }, [state.colorClassActiveList]); diff --git a/starsky/starsky/clientapp/src/components/molecules/list-image-view-select-container/list-image-view-select-container.tsx b/starsky/starsky/clientapp/src/components/molecules/list-image-view-select-container/list-image-view-select-container.tsx index 4144fdafc7..6ce3164116 100644 --- a/starsky/starsky/clientapp/src/components/molecules/list-image-view-select-container/list-image-view-select-container.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/list-image-view-select-container/list-image-view-select-container.tsx @@ -52,7 +52,7 @@ const ListImageViewSelectContainer: React.FunctionComponent = } const preloader = ; - const [isPreloaderState, setPreloaderState] = React.useState(false); + const [preloaderState, setPreloaderState] = React.useState(false); function preloaderStateOnClick(event: React.MouseEvent) { // Command (mac) or ctrl click means open new window @@ -105,7 +105,7 @@ const ListImageViewSelectContainer: React.FunctionComponent = data-filepath={item.filePath} > {/* for slow connections show preloader icon */} - {isPreloaderState ? preloader : null} + {preloaderState ? preloader : null} {/* the a href to the child page */} { expect(screen.getByTestId("modal-move-folder-to-trash")).toBeTruthy(); }); + it("opens the modal when the menu option is keyDowned", () => { + jest + .spyOn(ModalMoveFolderToTrash, "default") + .mockImplementationOnce(() => ( +
    + )); + + render( + + ); + + const menuOption = screen.getByTestId("move-folder-to-trash"); + fireEvent.keyDown(menuOption, { key: "Enter" }); + + expect(screen.getByTestId("modal-move-folder-to-trash")).toBeTruthy(); + }); + + it("not opens the modal when the menu option is keyDowned but wrong key so ignored", () => { + jest + .spyOn(ModalMoveFolderToTrash, "default") + .mockImplementationOnce(() => ( +
    + )); + + render( + + ); + + const menuOption = screen.getByTestId("move-folder-to-trash"); + fireEvent.keyDown(menuOption, { key: "Tab" }); + + expect(screen.queryByTestId("modal-move-folder-to-trash")).toBeFalsy(); + }); + it("opens the modal when the menu option is clicked 1", () => { console.log("----------"); @@ -82,4 +124,87 @@ describe("MenuOptionMoveFolderToTrash", () => { expect(modalSpy).toBeCalledTimes(0); }); + + it("opens the modal when the menu option is keyDown tab so skip", () => { + console.log("----------"); + + const modalSpy = jest + .spyOn(Modal, "default") + .mockImplementationOnce((props) => { + act(() => { + props.handleExit(); + }); + return <>{props.children}; + }); + + jest + .spyOn(ModalMoveFolderToTrash, "default") + .mockImplementationOnce((props) => { + act(() => { + props.handleExit(); + }); + return <>; + }); + + const setEnableMoreMenuSpy = jest.fn(); + render( + + ); + + const menuOption = screen.getByTestId("move-folder-to-trash"); + fireEvent.keyDown(menuOption, { key: "Tab" }); + + expect(screen.getByTestId("move-folder-to-trash")).toBeTruthy(); + + expect(setEnableMoreMenuSpy).toBeCalledTimes(0); + + expect(modalSpy).toBeCalledTimes(0); + }); + + it("opens the modal when the menu option is keyDown enter 1", () => { + console.log("----------"); + + const modalSpy = jest + .spyOn(Modal, "default") + .mockImplementationOnce((props) => { + act(() => { + props.handleExit(); + }); + return <>{props.children}; + }); + + jest + .spyOn(ModalMoveFolderToTrash, "default") + .mockImplementationOnce((props) => { + act(() => { + props.handleExit(); + }); + return <>; + }); + + const setEnableMoreMenuSpy = jest.fn(); + render( + + ); + + const menuOption = screen.getByTestId("move-folder-to-trash"); + fireEvent.keyDown(menuOption, { key: "Enter" }); + + expect(screen.getByTestId("move-folder-to-trash")).toBeTruthy(); + + expect(setEnableMoreMenuSpy).toBeCalledTimes(1); + expect(setEnableMoreMenuSpy).toBeCalledWith(false); + + expect(modalSpy).toBeCalledTimes(0); + }); }); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash.tsx index 3f46ff4139..5b79681282 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash.tsx @@ -12,27 +12,27 @@ interface IMenuOptionMoveToTrashProps { const MenuOptionMoveFolderToTrash: React.FunctionComponent = memo(({ isReadOnly, subPath, setEnableMoreMenu }) => { - const [isModalMoveFolderToTrashOpen, setModalMoveFolderToTrashOpen] = + const [modalMoveFolderToTrashOpen, setModalMoveFolderToTrashOpen] = useState(false); return ( <> {/* Modal move folder to trash */} - {isModalMoveFolderToTrashOpen ? ( + {modalMoveFolderToTrashOpen ? ( { - setModalMoveFolderToTrashOpen(!isModalMoveFolderToTrashOpen); + setModalMoveFolderToTrashOpen(!modalMoveFolderToTrashOpen); }} subPath={subPath} setIsLoading={() => {}} - isOpen={isModalMoveFolderToTrashOpen} + isOpen={modalMoveFolderToTrashOpen} /> ) : null} { }); describe("context", () => { - it("check if dispatch", async () => { + it("check if dispatch when click", async () => { jest.spyOn(FetchPost, "default").mockReset(); const test = { ...newIArchive(), @@ -82,6 +82,107 @@ describe("MenuOptionMoveToTrash", () => { component.unmount(); }); + it("check if dispatch when keyDown enter", async () => { + jest.spyOn(FetchPost, "default").mockReset(); + const test = { + ...newIArchive(), + fileIndexItems: [ + { + ...newIFileIndexItem(), + parentDirectory: "/", + fileName: "test.jpg" + } as IFileIndexItem + ] + } as IArchiveProps; + + const mockIConnectionDefault: Promise = + Promise.resolve({ + ...newIConnectionDefault(), + data: null, + statusCode: 200 + }); + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const dispatch = jest.fn(); + const component = await render( + + ); + + const trashButton = screen.queryByTestId("trash") as HTMLButtonElement; + expect(trashButton).toBeTruthy(); + + await act(async () => { + await fireEvent.keyDown(trashButton, { + key: "Enter" + }); + }); + + expect(fetchPostSpy).toBeCalled(); + expect(dispatch).toBeCalled(); + expect(dispatch).toBeCalledWith({ + toRemoveFileList: ["/test.jpg"], + type: "remove" + }); + component.unmount(); + }); + + it("check if dispatch when keyDown tab so skip", async () => { + jest.spyOn(FetchPost, "default").mockReset(); + const test = { + ...newIArchive(), + fileIndexItems: [ + { + ...newIFileIndexItem(), + parentDirectory: "/", + fileName: "test.jpg" + } as IFileIndexItem + ] + } as IArchiveProps; + + const mockIConnectionDefault: Promise = + Promise.resolve({ + ...newIConnectionDefault(), + data: null, + statusCode: 200 + }); + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const dispatch = jest.fn(); + const component = await render( + + ); + + const trashButton = screen.queryByTestId("trash") as HTMLButtonElement; + expect(trashButton).toBeTruthy(); + + await act(async () => { + await fireEvent.keyDown(trashButton, { + key: "Tab" + }); + }); + + expect(fetchPostSpy).toBeCalledTimes(0); + expect(dispatch).toBeCalledTimes(0); + + component.unmount(); + }); + it("check if not dispatch when error", () => { console.log("-- check if not dispatch when error"); @@ -178,6 +279,7 @@ describe("MenuOptionMoveToTrash", () => { component.unmount(); }); }); + describe("context 2", () => { it("check if when pressing Delete key", () => { console.log("-- check if when pressing Delete key"); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.stories.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.stories.tsx index bbc29502e7..ff32825cba 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.stories.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.stories.tsx @@ -22,11 +22,15 @@ export const Default = () => { <> {}}> {}} + setSelect={() => { + alert("done"); + }} select={["test.jpg"]} isReadOnly={false} state={test} - dispatch={() => {}} + dispatch={() => { + alert("done"); + }} /> diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.tsx index a5de60f0c5..61b1a6d5a0 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-move-to-trash/menu-option-move-to-trash.tsx @@ -25,6 +25,7 @@ interface IMenuOptionMoveToTrashProps { * Used from Archive and Search */ const MenuOptionMoveToTrash: React.FunctionComponent = + // eslint-disable-next-line react/display-name memo(({ state, dispatch, select, setSelect, isReadOnly }) => { const settings = useGlobalSettings(); const language = new Language(settings.language); @@ -98,7 +99,11 @@ const MenuOptionMoveToTrash: React.FunctionComponent= 1 ? (
  • { + event.key === "Enter" && moveToTrashSelection(); + }} onClick={() => moveToTrashSelection()} > {MessageMoveToTrash} diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.spec.tsx new file mode 100644 index 0000000000..3667ca282c --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.spec.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render } from "@testing-library/react"; +import { MenuOptionSelectionAll } from "./menu-option-selection-all"; + +describe("MenuOptionSelectionAll", () => { + it("renders", () => { + render( + {}} + /> + ); + }); + + it("renders 2", () => { + render( + {}} + /> + ); + }); + + it("keyDown tab skipped", () => { + const allSelection = jest.fn(); + const component = render( + + ); + + const allItem = component.queryByTestId("select-all") as HTMLElement; + expect(allItem).toBeTruthy(); + + fireEvent.keyDown(allItem, { + key: "Tab" + }); + + expect(allSelection).toBeCalledTimes(0); + }); + + it("keyDown enter continue", () => { + const allSelection = jest.fn(); + const component = render( + + ); + + fireEvent.keyDown(component.queryByTestId("select-all") as HTMLElement, { + key: "Enter" + }); + + expect(allSelection).toBeCalledTimes(1); + }); + + it("click continue", () => { + const allSelection = jest.fn(); + const component = render( + + ); + + const item = component.queryByTestId("select-all") as HTMLElement; + item.click(); + + expect(allSelection).toBeCalledTimes(1); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.tsx new file mode 100644 index 0000000000..598a1360e3 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-all/menu-option-selection-all.tsx @@ -0,0 +1,36 @@ +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; + +export interface IMenuOptionUndoSelectionProps { + select: string[]; + state?: IArchiveProps; + allSelection: () => void; +} + +export const MenuOptionSelectionAll: React.FunctionComponent< + IMenuOptionUndoSelectionProps +> = ({ select, state, allSelection }) => { + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageSelectAll = language.key(localization.MessageSelectAll); + + return ( + <> + {select.length !== state?.fileIndexItems?.length ? ( +
  • allSelection()} + tabIndex={0} + onKeyDown={(event) => { + event.key === "Enter" && allSelection(); + }} + > + {MessageSelectAll} +
  • + ) : null} + + ); +}; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.spec.tsx new file mode 100644 index 0000000000..1e32e2419f --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.spec.tsx @@ -0,0 +1,72 @@ +import { fireEvent, render } from "@testing-library/react"; +import { MenuOptionSelectionUndo } from "./menu-option-selection-undo"; + +describe("MenuOptionSelectionUndo", () => { + it("renders", () => { + render( + {}} + /> + ); + }); + + it("renders 2", () => { + render( + {}} + /> + ); + }); + + it("keyDown tab skipped", () => { + const undoSelection = jest.fn(); + const component = render( + + ); + + fireEvent.keyDown( + component.queryByTestId("undo-selection") as HTMLElement, + { key: "Tab" } + ); + + expect(undoSelection).toBeCalledTimes(0); + }); + + it("keyDown enter continue", () => { + const undoSelection = jest.fn(); + const component = render( + + ); + + fireEvent.keyDown( + component.queryByTestId("undo-selection") as HTMLElement, + { key: "Enter" } + ); + + expect(undoSelection).toBeCalledTimes(1); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.tsx new file mode 100644 index 0000000000..8f475416ca --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-selection-undo/menu-option-selection-undo.tsx @@ -0,0 +1,36 @@ +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; + +export interface IMenuOptionSelectionUndoProps { + select: string[]; + state?: IArchiveProps; + undoSelection: () => void; +} + +export const MenuOptionSelectionUndo: React.FunctionComponent< + IMenuOptionSelectionUndoProps +> = ({ select, state, undoSelection }) => { + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageUndoSelection = language.key(localization.MessageUndoSelection); + + return ( + <> + {select.length === state?.fileIndexItems?.length ? ( +
  • undoSelection()} + tabIndex={0} + onKeyDown={(event) => { + event.key === "Enter" && undoSelection(); + }} + > + {MessageUndoSelection} +
  • + ) : null} + + ); +}; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.spec.tsx new file mode 100644 index 0000000000..109ee0ed70 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.spec.tsx @@ -0,0 +1,95 @@ +import { fireEvent, render } from "@testing-library/react"; +import { MenuSelectCount } from "./menu-select-count"; + +describe("MenuOptionSelectionAll", () => { + it("renders", () => { + render( + {}} /> + ); + }); + + it("selected-0 keyDown tab skipped", () => { + const removeSidebarSelection = jest.fn(); + const component = render( + + ); + + const allItem = component.queryByTestId("selected-0") as HTMLElement; + expect(allItem).toBeTruthy(); + + fireEvent.keyDown(allItem, { + key: "Tab" + }); + + expect(removeSidebarSelection).toBeCalledTimes(0); + }); + + it("selected-0 keyDown enter continue", () => { + const removeSidebarSelection = jest.fn(); + const component = render( + + ); + + fireEvent.keyDown(component.queryByTestId("selected-0") as HTMLElement, { + key: "Enter" + }); + + expect(removeSidebarSelection).toBeCalledTimes(1); + }); + + it("selected-0 click continue", () => { + const removeSidebarSelection = jest.fn(); + const component = render( + + ); + + const item = component.queryByTestId("selected-0") as HTMLElement; + item.click(); + + expect(removeSidebarSelection).toBeCalledTimes(1); + }); + + it("selected-1 keyDown tab skipped", () => { + const removeSidebarSelection = jest.fn(); + const component = render( + + ); + + const allItem = component.queryByTestId("selected-1") as HTMLElement; + expect(allItem).toBeTruthy(); + + fireEvent.keyDown(allItem, { + key: "Tab" + }); + + expect(removeSidebarSelection).toBeCalledTimes(0); + }); + + it("selected-1 keyDown enter continue", () => { + const removeSidebarSelection = jest.fn(); + const component = render( + + ); + + fireEvent.keyDown(component.queryByTestId("selected-1") as HTMLElement, { + key: "Enter" + }); + + expect(removeSidebarSelection).toBeCalledTimes(1); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.tsx new file mode 100644 index 0000000000..f568839141 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-select-count/menu-select-count.tsx @@ -0,0 +1,54 @@ +import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; + +export interface IMenuSelectCountProps { + select?: string[]; + removeSidebarSelection: () => void; +} + +export const MenuSelectCount: React.FunctionComponent< + IMenuSelectCountProps +> = ({ select, removeSidebarSelection }) => { + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageNoneSelected = language.key(localization.MessageNoneSelected); + + const MessageSelectPresentPerfect = language.key( + localization.MessageSelectPresentPerfect + ); + + return ( + <> + {select && select.length === 0 ? ( + + ) : null} + + {select && select.length >= 1 ? ( + + ) : null} + + ); +}; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.spec.tsx new file mode 100644 index 0000000000..37649810be --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.spec.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render } from "@testing-library/react"; +import { MenuSelectFurther } from "./menu-select-further.tsx"; + +describe("MenuSelectFurther", () => { + const toggleLabelsMock = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not render div with class "header" when select prop is not provided', () => { + const { queryByTestId } = render( + + ); + expect(queryByTestId("select-further")).toBeNull(); + }); + + it('should render div with class "header" and handle events when select prop is provided', () => { + const { getByTestId } = render( + + ); + const item = getByTestId("select-further"); + expect(item).toBeTruthy(); + + fireEvent.click(item); + expect(toggleLabelsMock).toHaveBeenCalledTimes(1); + + fireEvent.keyDown(item, { key: "Enter", code: "Enter" }); + expect(toggleLabelsMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.tsx new file mode 100644 index 0000000000..a96b499bfa --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-select-further/menu-select-further.tsx @@ -0,0 +1,39 @@ +import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; + +export interface IMenuSelectFurtherProps { + select?: string[]; + toggleLabels: () => void; +} + +export const MenuSelectFurther: React.FunctionComponent< + IMenuSelectFurtherProps +> = ({ select, toggleLabels }) => { + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageSelectFurther = language.key(localization.MessageSelectFurther); + + return ( + <> + {select ? ( +
    +
    { + toggleLabels(); + }} + onKeyDown={(event) => { + event.key === "Enter" && toggleLabels(); + }} + > + {MessageSelectFurther} +
    +
    + ) : ( + "" + )} + + ); +}; diff --git a/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.spec.tsx index 9a33505c16..019d557af7 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.spec.tsx @@ -1,4 +1,10 @@ -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { + act, + createEvent, + fireEvent, + render, + screen +} from "@testing-library/react"; import React from "react"; import { Root, createRoot } from "react-dom/client"; import { IDetailView } from "../../../interfaces/IDetailView"; @@ -36,6 +42,40 @@ describe("DetailViewMp4", () => { component.unmount(); }); + it("keyDown Tab ignore", () => { + const component = render(); + + const playSpy = jest + .spyOn(HTMLMediaElement.prototype, "play") + .mockReset() + .mockImplementationOnce(() => Promise.resolve()); + + const figure = screen.queryByTestId("video") as HTMLElement; + const inputEvent = createEvent.keyDown(figure, { key: "Tab" }); + fireEvent(figure, inputEvent); + + expect(playSpy).toBeCalledTimes(0); + + component.unmount(); + }); + + it("keyDown Enter video resolve", () => { + const component = render(); + + const playSpy = jest + .spyOn(HTMLMediaElement.prototype, "play") + .mockReset() + .mockImplementationOnce(() => Promise.resolve()); + + const figure = screen.queryByTestId("video") as HTMLElement; + const inputEvent = createEvent.keyDown(figure, { key: "Enter" }); + fireEvent(figure, inputEvent); + + expect(playSpy).toBeCalledTimes(1); + + component.unmount(); + }); + it("click to play video rejected", async () => { const component = render(); @@ -56,6 +96,7 @@ describe("DetailViewMp4", () => { await component.unmount(); }); }); + it("click to play video and timeupdate", () => { const component = render(); @@ -79,6 +120,54 @@ describe("DetailViewMp4", () => { component.unmount(); }); + it("keyDown Enter to play video and timeupdate", () => { + const component = render(); + + const playSpy = jest + .spyOn(HTMLMediaElement.prototype, "play") + .mockReset() + .mockImplementationOnce(() => { + return Promise.resolve(); + }); + + expect(screen.queryByTestId("video-time")?.textContent).toBe(""); + + const figure = screen.queryByTestId("video") as HTMLElement; + const inputEvent = createEvent.keyDown(figure, { key: "Enter" }); + fireEvent(figure, inputEvent); + + expect(screen.queryByTestId("video-time")?.textContent).toBe( + "0:00 / 0:00" + ); + + expect(playSpy).toBeCalled(); + + component.unmount(); + }); + + it("keyDown Tab ignored", () => { + const component = render(); + + const playSpy = jest + .spyOn(HTMLMediaElement.prototype, "play") + .mockReset() + .mockImplementationOnce(() => { + return Promise.resolve(); + }); + + expect(screen.queryByTestId("video-time")?.textContent).toBe(""); + + const figure = screen.queryByTestId("video") as HTMLElement; + const inputEvent = createEvent.keyDown(figure, { key: "Tab" }); + fireEvent(figure, inputEvent); + + expect(screen.queryByTestId("video-time")?.textContent).toBe(""); + + expect(playSpy).toBeCalledTimes(0); + + component.unmount(); + }); + it("progress DOM", (done) => { const component = document.createElement("div"); document.body.appendChild(component); // Append the component to the body diff --git a/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.tsx b/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.tsx index ce93d8abe4..2b758f8824 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detail-view-media/detail-view-mp4.tsx @@ -24,6 +24,7 @@ function GetVideoClass(isPaused: boolean, isStarted: boolean): string { } } +// eslint-disable-next-line react/display-name const DetailViewMp4: React.FunctionComponent = memo(() => { // content const settings = useGlobalSettings(); @@ -46,7 +47,7 @@ const DetailViewMp4: React.FunctionComponent = memo(() => { const { state } = React.useContext(DetailViewContext); /** update to make useEffect simpler te read */ - const [downloadApi, setDownloadPhotoApi] = useState( + const [downloadPhotoApi, setDownloadPhotoApi] = useState( new UrlQuery().UrlDownloadPhotoApi( new URLPath().encodeURI( new URLPath().getFilePath(history.location.search) @@ -101,8 +102,8 @@ const DetailViewMp4: React.FunctionComponent = memo(() => { const scrubberRef = useRef(null); const timeRef = useRef(null); - const [isPaused, setPaused] = useState(true); - const [isStarted, setStarted] = useState(false); + const [paused, setPaused] = useState(true); + const [started, setStarted] = useState(false); const videoRefCurrent = videoRef.current; useEffect(() => { @@ -135,7 +136,7 @@ const DetailViewMp4: React.FunctionComponent = memo(() => { setStarted(true); - if (isPaused) { + if (paused) { const promise = videoRef.current.play(); promise?.catch(() => { @@ -231,7 +232,11 @@ const DetailViewMp4: React.FunctionComponent = memo(() => { {!isError ? (
    { + event.key === "Enter" && playPause(); + event.key === "Enter" && timeUpdate(); + }} onClick={() => { playPause(); timeUpdate(); @@ -243,16 +248,19 @@ const DetailViewMp4: React.FunctionComponent = memo(() => { controls={false} preload="metadata" > - +
    diff --git a/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx b/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx index fec6466ff5..a556de2119 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx @@ -114,7 +114,7 @@ const DetailViewSidebar: React.FunctionComponent = setCollections(collectionsList); } // For the display - const [isFormEnabled, setFormEnabled] = React.useState(true); + const [formEnabled, setFormEnabled] = React.useState(true); useEffect(() => { if (!fileIndexItem.status) return; switch (fileIndexItem.status) { @@ -226,7 +226,7 @@ const DetailViewSidebar: React.FunctionComponent = data-test="detailview-sidebar-tags" maxlength={1024} reference={tagsReference} - contentEditable={isFormEnabled} + contentEditable={formEnabled} > {fileIndexItem.tags} @@ -242,7 +242,7 @@ const DetailViewSidebar: React.FunctionComponent = maxlength={1024} name="description" reference={descriptionReference} - contentEditable={isFormEnabled} + contentEditable={formEnabled} > {fileIndexItem.description} @@ -252,7 +252,7 @@ const DetailViewSidebar: React.FunctionComponent = name="title" maxlength={1024} reference={titleReference} - contentEditable={isFormEnabled} + contentEditable={formEnabled} > {fileIndexItem.title} @@ -281,7 +281,7 @@ const DetailViewSidebar: React.FunctionComponent = }} filePath={fileIndexItem.filePath} currentColorClass={fileIndexItem.colorClass} - isEnabled={isFormEnabled} + isEnabled={formEnabled} />
    {fileIndexItem.latitude || @@ -298,7 +298,7 @@ const DetailViewSidebar: React.FunctionComponent =
    @@ -342,7 +342,7 @@ const DetailViewSidebar: React.FunctionComponent = { component.unmount(); }); + it("click on select button and change url", () => { + Router.navigate("/"); + + const component = render(); + + const selectButton = screen.getByTestId("menu-item-select"); + expect(Router.state.location.search).toBe(""); + + expect(selectButton).toBeTruthy(); + + selectButton?.click(); + + expect(Router.state.location.search).toBe("?select="); + + // and clean + component.unmount(); + }); + + it("keyDown Tab on select button but skip", () => { + Router.navigate("/"); + + const component = render(); + + const selectButton = screen.getByTestId("menu-item-select"); + expect(Router.state.location.search).toBe(""); + + expect(selectButton).toBeTruthy(); + + fireEvent.keyDown(selectButton, { + key: "Tab" + }); + + expect(Router.state.location.search).toBe(""); + + // and clean + component.unmount(); + }); + + it("keyDown Enter on select button and change url", () => { + Router.navigate("/"); + + const component = render(); + + const selectButton = screen.getByTestId("menu-item-select"); + expect(Router.state.location.search).toBe(""); + + expect(selectButton).toBeTruthy(); + + fireEvent.keyDown(selectButton, { + key: "Enter" + }); + + expect(Router.state.location.search).toBe("?select="); + + // and clean + component.unmount(); + }); + it("[menu archive] check if on click the hamburger opens", () => { + Router.navigate("/"); + const component = render(); const hamburger = component.getByTestId("hamburger"); @@ -178,6 +239,93 @@ describe("MenuArchive", () => { component.unmount(); }); + it("[archive] menu keyDown tab mkdir so skip", async () => { + jest.spyOn(React, "useContext").mockReset(); + + Router.navigate("/"); + + const state = { + subPath: "/", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + const mkdirModalSpy = jest + .spyOn(ModalArchiveMkdir, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + jest + .spyOn(React, "useContext") + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(t); + + const mkdir = screen.getByTestId("mkdir"); + + // need async + await fireEvent.keyDown(mkdir, { + key: "Tab" + }); + + expect(mkdirModalSpy).toBeCalledTimes(0); + + component.unmount(); + }); + + it("[archive] menu keyDown enter mkdir", async () => { + jest.spyOn(React, "useContext").mockReset(); + + Router.navigate("/"); + + const state = { + subPath: "/", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + const mkdirModalSpy = jest + .spyOn(ModalArchiveMkdir, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + jest + .spyOn(React, "useContext") + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(t); + + const mkdir = screen.getByTestId("mkdir"); + + // need async + await fireEvent.keyDown(mkdir, { + key: "Enter" + }); + + expect(mkdirModalSpy).toBeCalledTimes(1); + + component.unmount(); + }); + it("[archive] menu click rename (dir)", async () => { Router.navigate("/?f=/test"); @@ -338,6 +486,110 @@ describe("MenuArchive", () => { Router.navigate("/"); }); + it("[archive] display options keyDown (default menu)", () => { + jest + .spyOn(Link, "default") + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>); + + Router.navigate("/?f=/test"); + + const state = { + subPath: "/test", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + const renameModalSpy = jest + .spyOn(ModalDisplayOptions, "default") + .mockImplementationOnce(() => { + return <>; + }); + + jest + .spyOn(React, "useContext") + .mockReset() + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(); + + const displayOptions = screen.getByTestId("display-options"); + expect(displayOptions).not.toBeNull(); + + act(() => { + fireEvent.keyDown(displayOptions, { key: "Enter" }); + }); + + expect(renameModalSpy).toBeCalled(); + + component.unmount(); + + Router.navigate("/"); + }); + + it("[archive] display options keyDown tab so ignore (default menu)", () => { + jest + .spyOn(Link, "default") + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>) + .mockImplementationOnce(() => <>); + + Router.navigate("/?f=/test"); + + const state = { + subPath: "/test", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + const renameModalSpy = jest + .spyOn(ModalDisplayOptions, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + jest + .spyOn(React, "useContext") + .mockReset() + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(); + + const displayOptions = screen.getByTestId("display-options"); + expect(displayOptions).not.toBeNull(); + + act(() => { + // should ignore + fireEvent.keyDown(displayOptions, { key: "Tab" }); + }); + + expect(renameModalSpy).toBeCalledTimes(0); + + component.unmount(); + + Router.navigate("/"); + }); + it("[archive] display options (select menu)", () => { jest.spyOn(React, "useContext").mockReset(); @@ -825,6 +1077,52 @@ describe("MenuArchive", () => { component.unmount(); }); + it("[archive] menu click publish", () => { + Router.navigate("/?select=test1.jpg"); + + const state = { + subPath: "/", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()); + + jest + .spyOn(React, "useContext") + .mockReset() + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(); + + expect(Router.state.location.search).toBe("?select=test1.jpg"); + + const selectFurther = screen.queryByTestId("select-further"); + expect(selectFurther).not.toBeNull(); + + act(() => { + selectFurther?.click(); + }); + + expect(Router.state.location.search).toBe( + "?select=test1.jpg&sidebar=true" + ); + + component.unmount(); + }); + it("readonly - menu click mkdir", () => { jest.spyOn(React, "useContext").mockReset(); @@ -853,12 +1151,8 @@ describe("MenuArchive", () => { jest .spyOn(React, "useContext") - .mockImplementationOnce(() => { - return contextValues; - }) - .mockImplementationOnce(() => { - return contextValues; - }); + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); const component = render(); @@ -894,9 +1188,9 @@ describe("MenuArchive", () => { return <>; }); - jest.spyOn(React, "useContext").mockImplementationOnce(() => { - return contextValues; - }); + jest + .spyOn(React, "useContext") + .mockImplementationOnce(() => contextValues); const component = render(); @@ -904,5 +1198,38 @@ describe("MenuArchive", () => { component.unmount(); }); + + it("NavContainer MenuSearchBar callback does change state [MenuArchive]", () => { + jest.spyOn(MenuSearchBar, "default").mockImplementationOnce((prop) => { + if (prop.callback) { + prop.callback("test"); + } + return <>test; + }); + const state = { + subPath: "/", + fileIndexItems: [ + { + status: IExifStatus.Ok, + filePath: "/trashed/test1.jpg", + fileName: "test1.jpg" + } + ] + } as IArchive; + const contextValues = { state, dispatch: jest.fn() }; + + jest + .spyOn(React, "useContext") + .mockImplementationOnce(() => contextValues) + .mockImplementationOnce(() => contextValues); + + const component = render(); + + const navOpen = screen.queryByTestId("nav-open") as HTMLDivElement; + + expect(navOpen).toBeTruthy(); + + component.unmount(); + }); }); }); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx index aa74e2dcd8..7832e09b0d 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx @@ -22,6 +22,10 @@ import MoreMenu from "../../atoms/more-menu/more-menu"; import MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; import MenuOptionMoveFolderToTrash from "../../molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash"; import MenuOptionMoveToTrash from "../../molecules/menu-option-move-to-trash/menu-option-move-to-trash"; +import { MenuOptionSelectionAll } from "../../molecules/menu-option-selection-all/menu-option-selection-all"; +import { MenuOptionSelectionUndo } from "../../molecules/menu-option-selection-undo/menu-option-selection-undo"; +import { MenuSelectCount } from "../../molecules/menu-select-count/menu-select-count"; +import { MenuSelectFurther } from "../../molecules/menu-select-further/menu-select-further"; import ModalDropAreaFilesAdded from "../../molecules/modal-drop-area-files-added/modal-drop-area-files-added"; import ModalArchiveMkdir from "../modal-archive-mkdir/modal-archive-mkdir"; import ModalArchiveRename from "../modal-archive-rename/modal-archive-rename"; @@ -33,17 +37,13 @@ import NavContainer from "../nav-container/nav-container"; interface IMenuArchiveProps {} +// eslint-disable-next-line react/display-name const MenuArchive: React.FunctionComponent = memo(() => { const settings = useGlobalSettings(); const language = new Language(settings.language); // Content const MessageSelectAction = language.text("Selecteer", "Select"); - const MessageSelectPresentPerfect = language.text("geselecteerd", "selected"); - const MessageNoneSelected = language.text( - "Niets geselecteerd", - "Nothing selected" - ); const MessageMkdir = language.text("Map maken", "Create folder"); const MessageRenameDir = language.text("Naam wijzigen", "Rename"); const MessageDisplayOptions = language.text( @@ -51,13 +51,6 @@ const MenuArchive: React.FunctionComponent = memo(() => { "Display options" ); - const MessageSelectFurther = language.text( - "Verder selecteren", - "Select further" - ); - const MessageSelectAll = language.text("Alles selecteren", "Select all"); - const MessageUndoSelection = language.text("Undo selectie", "Undo selection"); - const [hamburgerMenu, setHamburgerMenu] = React.useState(false); const [enableMoreMenu, setEnableMoreMenu] = React.useState(false); @@ -78,7 +71,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { useHotKeys({ key: "a", ctrlKeyOrMetaKey: true }, allSelection, []); /* only update when the state is changed */ - const [isReadOnly, setReadOnly] = React.useState(state.isReadOnly); + const [readOnly, setReadOnly] = React.useState(state.isReadOnly); useEffect(() => { setReadOnly(state.isReadOnly); }, [state.isReadOnly]); @@ -114,7 +107,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { ); const UploadMenuItem = () => { - if (isReadOnly) + if (readOnly) return (
  • Upload @@ -176,7 +169,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { ) : null} {/* Modal new directory */} - {isModalMkdirOpen && !isReadOnly ? ( + {isModalMkdirOpen && !readOnly ? ( = memo(() => { /> ) : null} - {isModalRenameFolder && !isReadOnly && state.subPath !== "/" ? ( + {isModalRenameFolder && !readOnly && state.subPath !== "/" ? ( = memo(() => { setHamburgerMenu={setHamburgerMenu} /> - {select && select.length === 0 ? ( - - ) : null} - {select && select.length >= 1 ? ( - - ) : null} + + {!select ? (
    = memo(() => { onClick={() => { removeSidebarSelection(); }} + onKeyDown={(event) => { + event.key === "Enter" && removeSidebarSelection(); + }} > {MessageSelectAction}
    @@ -270,16 +249,25 @@ const MenuArchive: React.FunctionComponent = memo(() => { enableMoreMenu={enableMoreMenu} >
  • setModalMkdirOpen(!isModalMkdirOpen)} + onKeyDown={(event) => { + event.key === "Enter" && setModalMkdirOpen(!isModalMkdirOpen); + }} > {MessageMkdir}
  • setDisplayOptionsOpen(!isDisplayOptionsOpen)} + onKeyDown={(event) => { + event.key === "Enter" && + setDisplayOptionsOpen(!isModalMkdirOpen); + }} > {MessageDisplayOptions}
  • @@ -293,7 +281,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { {state ? : null}
  • = memo(() => {
  • = memo(() => { setEnableMoreMenu={setEnableMoreMenu} enableMoreMenu={enableMoreMenu} > - {select.length === state.fileIndexItems.length ? ( -
  • undoSelection()} - > - {MessageUndoSelection} -
  • - ) : null} - {select.length !== state.fileIndexItems.length ? ( -
  • allSelection()} - > - {MessageSelectAll} -
  • - ) : null} + + + + {select.length >= 1 ? ( <> = memo(() => { dispatch={dispatch} select={select} setSelect={setSelect} - isReadOnly={isReadOnly} + isReadOnly={readOnly} /> ) : null} @@ -386,20 +368,7 @@ const MenuArchive: React.FunctionComponent = memo(() => {
    - {select ? ( -
    -
    { - toggleLabels(); - }} - > - {MessageSelectFurther} -
    -
    - ) : ( - "" - )} + ); }); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.spec.tsx index 088a00c554..3d005f106f 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.spec.tsx @@ -328,6 +328,64 @@ describe("MenuDetailView", () => { }); }); + it("export keyDown [menu] Tab so ignore", () => { + const exportModal = jest + .spyOn(ModalExport, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const exportButton = component.queryByTestId("export") as HTMLElement; + expect(exportButton).toBeTruthy(); + + act(() => { + fireEvent.keyDown(exportButton, { key: "Tab" }); + }); + + expect(exportModal).toBeCalledTimes(0); + + // to avoid polling afterwards + act(() => { + component.unmount(); + }); + }); + + it("export keyDown [menu] Enter so continue", () => { + const exportModal = jest + .spyOn(ModalExport, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const exportButton = component.queryByTestId("export") as HTMLElement; + expect(exportButton).toBeTruthy(); + + act(() => { + fireEvent.keyDown(exportButton, { key: "Enter" }); + }); + + expect(exportModal).toBeCalledTimes(1); + + // to avoid polling afterwards + act(() => { + component.unmount(); + }); + }); + it("labels click .item--labels [menu]", () => { const component = render( @@ -353,6 +411,62 @@ describe("MenuDetailView", () => { }); }); + it("labels keyDown .item--labels [menu] Tab so ignore", () => { + const component = render( + + + + ); + + const labels = component.queryByTestId( + "menu-detail-view-labels" + ) as HTMLElement; + expect(labels).toBeTruthy(); + + act(() => { + fireEvent.keyDown(labels, { key: "Tab" }); + }); + + const urlObject = new URLPath().StringToIUrl(window.location.search); + + expect(urlObject.details).toBeFalsy(); + + // don't keep any menus open + act(() => { + component.unmount(); + // reset afterwards + Router.navigate("/"); + }); + }); + + it("labels keyDown .item--labels [menu] Enter", () => { + const component = render( + + + + ); + + const labels = component.queryByTestId( + "menu-detail-view-labels" + ) as HTMLElement; + expect(labels).toBeTruthy(); + + act(() => { + fireEvent.keyDown(labels, { key: "Enter" }); + }); + + const urlObject = new URLPath().StringToIUrl(window.location.search); + + expect(urlObject.details).toBeTruthy(); + + // don't keep any menus open + act(() => { + component.unmount(); + // reset afterwards + Router.navigate("/"); + }); + }); + it("labels click (in MoreMenu)", () => { const component = render( @@ -427,6 +541,66 @@ describe("MenuDetailView", () => { }); }); + it("[menu detail] move keyDown tab so ignore", () => { + const moveModal = jest + .spyOn(ModalMoveFile, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const move = component.queryByTestId("move") as HTMLElement; + expect(move).toBeTruthy(); + + act(() => { + fireEvent.keyDown(move, { key: "Tab" }); + }); + + expect(moveModal).toBeCalledTimes(0); + + // reset afterwards + act(() => { + Router.navigate("/"); + component.unmount(); + }); + }); + + it("[menu detail] move keyDown enter", () => { + const moveModal = jest + .spyOn(ModalMoveFile, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const move = component.queryByTestId("move") as HTMLElement; + expect(move).toBeTruthy(); + + act(() => { + fireEvent.keyDown(move, { key: "Enter" }); + }); + + expect(moveModal).toBeCalledTimes(1); + + // reset afterwards + act(() => { + Router.navigate("/"); + component.unmount(); + }); + }); + it("rename click", () => { const renameModal = jest .spyOn(ModalDetailviewRenameFile, "default") @@ -454,13 +628,132 @@ describe("MenuDetailView", () => { }); }); - it("trash click to trash", () => { + it("rename keyDown tab so ignore", () => { + const renameModal = jest + .spyOn(ModalDetailviewRenameFile, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const rename = component.queryByTestId("rename") as HTMLElement; + expect(rename).toBeTruthy(); + + act(() => { + fireEvent.keyDown(rename, { key: "Tab" }); + }); + + expect(renameModal).toBeCalledTimes(0); + + act(() => { + component.unmount(); + }); + }); + + it("rename keyDown enter so continue", () => { + const renameModal = jest + .spyOn(ModalDetailviewRenameFile, "default") + .mockReset() + .mockImplementationOnce(() => { + return <>; + }); + + const component = render( + + + + ); + + const rename = component.queryByTestId("rename") as HTMLElement; + expect(rename).toBeTruthy(); + + act(() => { + fireEvent.keyDown(rename, { key: "Enter" }); + }); + + expect(renameModal).toBeCalled(); + + act(() => { + component.unmount(); + }); + }); + + it("trash keyDown to trash so tab so skip", () => { + // spy on fetch + // use this import => import * as FetchPost from '../shared/fetch-post'; + const mockIConnectionDefault: Promise = + Promise.resolve({ statusCode: 200 } as IConnectionDefault); + const spy = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const component = render( + + + + ); + + const trash = component.queryByTestId("trash") as HTMLElement; + expect(trash).toBeTruthy(); + + act(() => { + fireEvent.keyDown(trash, { key: "Tab" }); + }); + + expect(spy).toBeCalledTimes(0); + + act(() => { + component.unmount(); + }); + }); + + it("trash keyDown to trash enter continue", () => { + // spy on fetch + // use this import => import * as FetchPost from '../shared/fetch-post'; + const mockIConnectionDefault: Promise = + Promise.resolve({ statusCode: 200 } as IConnectionDefault); + const spy = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const component = render( + + + + ); + + const trash = component.queryByTestId("trash") as HTMLElement; + expect(trash).toBeTruthy(); + + act(() => { + fireEvent.keyDown(trash, { key: "Enter" }); + }); + + expect(spy).toBeCalledTimes(1); + expect(spy).toBeCalledWith( + new UrlQuery().UrlMoveToTrashApi(), + "f=%2Ftest%2Fimage.jpg" + ); + + act(() => { + component.unmount(); + }); + }); + + it("trash keyDown to trash", () => { // spy on fetch // use this import => import * as FetchPost from '../shared/fetch-post'; const mockIConnectionDefault: Promise = Promise.resolve({ statusCode: 200 } as IConnectionDefault); const spy = jest .spyOn(FetchPost, "default") + .mockReset() .mockImplementationOnce(() => mockIConnectionDefault); const component = render( @@ -576,6 +869,117 @@ describe("MenuDetailView", () => { jest.useRealTimers(); }); }); + + it("press click menu-detail-view-close-details button", () => { + Router.navigate("/?details=true"); + + const updateState = { + ...state, + fileIndexItem: { + ...state.fileIndexItem + } + }; + + const component = render( + + + + ); + + expect(Router.state.location.search).toBe("?details=true"); + + const closeButton = component.queryByTestId( + "menu-detail-view-close-details" + ) as HTMLElement; + expect(closeButton).toBeTruthy(); + + closeButton?.click(); + + // act(() => { + // fireEvent.keyDown(closeButton, { key: "Enter" }); + // }); + + expect(Router.state.location.search).toBe("?details=false"); + + act(() => { + // reset afterwards + component.unmount(); + Router.navigate("/"); + }); + }); + + it("press keyDown enter menu-detail-view-close-details button", () => { + Router.navigate("/?details=true"); + + const updateState = { + ...state, + fileIndexItem: { + ...state.fileIndexItem + } + }; + + const component = render( + + + + ); + + expect(Router.state.location.search).toBe("?details=true"); + + const closeButton = component.queryByTestId( + "menu-detail-view-close-details" + ) as HTMLElement; + expect(closeButton).toBeTruthy(); + + act(() => { + fireEvent.keyDown(closeButton, { key: "Enter" }); + }); + + expect(Router.state.location.search).toBe("?details=false"); + + act(() => { + // reset afterwards + component.unmount(); + Router.navigate("/"); + }); + }); + + it("press keyDown tab skip menu-detail-view-close-details button so skips", () => { + Router.navigate("/?details=true"); + + const updateState = { + ...state, + fileIndexItem: { + ...state.fileIndexItem + } + }; + + const component = render( + + + + ); + + expect(Router.state.location.search).toBe("?details=true"); + + const closeButton = component.queryByTestId( + "menu-detail-view-close-details" + ) as HTMLElement; + expect(closeButton).toBeTruthy(); + + act(() => { + fireEvent.keyDown(closeButton, { key: "Tab" }); + }); + + // keep the same + expect(Router.state.location.search).toBe("?details=true"); + + act(() => { + // reset afterwards + component.unmount(); + Router.navigate("/"); + }); + }); }); describe("file is marked as deleted", () => { diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.tsx index 7da7ec0ffd..26a38b850b 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-detail-view/menu-detail-view.tsx @@ -95,7 +95,7 @@ const MenuDetailView: React.FunctionComponent = ({ const history = useLocation(); - const [isDetails, setDetails] = React.useState( + const [details, setDetails] = React.useState( new URLPath().StringToIUrl(history.location.search).details ); useEffect(() => { @@ -127,7 +127,7 @@ const MenuDetailView: React.FunctionComponent = ({ function toggleLabels() { const urlObject = new URLPath().StringToIUrl(history.location.search); - urlObject.details = !isDetails; + urlObject.details = !details; setDetails(urlObject.details); setRecentEdited(false); // disable to avoid animation history.navigate(new URLPath().IUrlToString(urlObject), { replace: true }); @@ -384,7 +384,7 @@ const MenuDetailView: React.FunctionComponent = ({ setModalPublishOpen={setModalPublishOpen} /> -
    +
    {/* in directory state aka no search */} {!isSearchQuery ? ( @@ -424,6 +424,9 @@ const MenuDetailView: React.FunctionComponent = ({ onClick={() => { toggleLabels(); }} + onKeyDown={(event) => { + event.key === "Enter" && toggleLabels(); + }} > Labels @@ -439,10 +442,13 @@ const MenuDetailView: React.FunctionComponent = ({ } data-test="export" onClick={() => setModalExportOpen(!isModalExportOpen)} + onKeyDown={(event) => { + event.key === "Enter" && setModalExportOpen(!isModalExportOpen); + }} > Download - {!isDetails ? ( + {!details ? (
  • = ({ className={!isReadOnly ? "menu-option" : "menu-option disabled"} data-test="move" onClick={() => setModalMoveFile(!isModalMoveFile)} + onKeyDown={(event) => { + event.key === "Enter" && setModalMoveFile(!isModalMoveFile); + }} > {MessageMove}
  • @@ -465,6 +474,10 @@ const MenuDetailView: React.FunctionComponent = ({ className={!isReadOnly ? "menu-option" : "menu-option disabled"} data-test="rename" onClick={() => setModalRenameFileOpen(!isModalRenameFileOpen)} + onKeyDown={(event) => { + event.key === "Enter" && + setModalRenameFileOpen(!isModalRenameFileOpen); + }} > {MessageRenameFileName} @@ -473,6 +486,9 @@ const MenuDetailView: React.FunctionComponent = ({ className={!isReadOnly ? "menu-option" : "menu-option disabled"} data-test="trash" onClick={TrashFile} + onKeyDown={(event) => { + event.key === "Enter" && TrashFile(); + }} > {!isMarkedAsDeleted ? MessageMoveToTrash @@ -508,13 +524,17 @@ const MenuDetailView: React.FunctionComponent = ({
    - {isDetails ? ( + {details ? (
    { toggleLabels(); }} + onKeyDown={(event) => { + event.key === "Enter" && toggleLabels(); + }} > {MessageCloseDetailScreenDialog} {isRecentEdited ? ( diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx index 097924e74b..9fedca7c9a 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx @@ -5,7 +5,9 @@ import * as useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; import { IArchive } from "../../../interfaces/IArchive"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { Router } from "../../../router-app/router-app"; +import * as MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; import MenuSearch from "./menu-search"; + describe("MenuSearch", () => { it("renders", () => { render(); @@ -96,5 +98,27 @@ describe("MenuSearch", () => { jest.spyOn(React, "useContext").mockRestore(); component.unmount(); }); + + it("NavContainer MenuSearchBar callback does change state [MenuSearch]", () => { + jest.spyOn(MenuSearchBar, "default").mockImplementationOnce((prop) => { + if (prop.callback) { + prop.callback("test"); + } + return <>test; + }); + + const component = render( + + ); + + const navOpen = screen.queryByTestId("nav-open") as HTMLDivElement; + + expect(navOpen).toBeTruthy(); + + component.unmount(); + }); }); }); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx index 1e2394dc7e..b5fac98502 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx @@ -18,6 +18,10 @@ import MenuOption from "../../atoms/menu-option/menu-option"; import MoreMenu from "../../atoms/more-menu/more-menu"; import MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; import MenuOptionMoveToTrash from "../../molecules/menu-option-move-to-trash/menu-option-move-to-trash"; +import { MenuOptionSelectionAll } from "../../molecules/menu-option-selection-all/menu-option-selection-all"; +import { MenuOptionSelectionUndo } from "../../molecules/menu-option-selection-undo/menu-option-selection-undo"; +import { MenuSelectCount } from "../../molecules/menu-select-count/menu-select-count"; +import { MenuSelectFurther } from "../../molecules/menu-select-further/menu-select-further"; import ModalDownload from "../modal-download/modal-download"; import ModalPublishToggleWrapper from "../modal-publish/modal-publish-toggle-wrapper"; import NavContainer from "../nav-container/nav-container"; @@ -39,18 +43,7 @@ export const MenuSearch: React.FunctionComponent = ({ const language = new Language(settings.language); // Content - const MessageNoneSelected = language.text( - "Niets geselecteerd", - "Nothing selected" - ); - const MessageSelectPresentPerfect = language.text("geselecteerd", "selected"); const MessageSelectAction = language.text("Selecteer", "Select"); - const MessageSelectAll = language.text("Alles selecteren", "Select all"); - const MessageUndoSelection = language.text("Undo selectie", "Undo selection"); - const MessageSelectFurther = language.text( - "Verder selecteren", - "Select further" - ); // Selection const history = useLocation(); @@ -121,30 +114,12 @@ export const MenuSearch: React.FunctionComponent = ({ setHamburgerMenu={setHamburgerMenu} /> - {select && select.length === 0 ? ( - - ) : null} - {select && select.length >= 1 ? ( - - ) : null} + - {/* te select button with checkbox*/} + {/* the select button with checkbox*/} {!select ? (
    = ({ onClick={() => { removeSidebarSelection(); }} + onKeyDown={(event) => { + event.key === "Enter" && removeSidebarSelection(); + }} > {MessageSelectAction}
    @@ -162,7 +140,13 @@ export const MenuSearch: React.FunctionComponent = ({ {/* when selected */} {select ? ( -
    toggleLabels()}> +
    toggleLabels()} + onKeyDown={(event) => { + event.key === "Enter" && toggleLabels(); + }} + > Labels
    ) : null} @@ -181,13 +165,11 @@ export const MenuSearch: React.FunctionComponent = ({ setEnableMoreMenu={setEnableMoreMenu} enableMoreMenu={enableMoreMenu} > -
  • allSelection()} - > - {MessageSelectAll} -
  • + ) : null} @@ -197,24 +179,18 @@ export const MenuSearch: React.FunctionComponent = ({ setEnableMoreMenu={setEnableMoreMenu} enableMoreMenu={enableMoreMenu} > - {select.length === state.fileIndexItems.length ? ( -
  • undoSelection()} - > - {MessageUndoSelection} -
  • - ) : null} - {select.length !== state.fileIndexItems.length ? ( -
  • allSelection()} - > - {MessageSelectAll} -
  • - ) : null} + + + + = ({
    - {select ? ( -
    -
    { - toggleLabels(); - }} - > - {MessageSelectFurther} -
    -
    - ) : ( - "" - )} + ); }; diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.spec.tsx index 7efe2e4be6..7a1f5b5451 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import { act } from "react-dom/test-utils"; import * as useFetch from "../../../hooks/use-fetch"; @@ -13,6 +13,7 @@ import { Router } from "../../../router-app/router-app"; import * as FetchPost from "../../../shared/fetch-post"; import { UrlQuery } from "../../../shared/url-query"; import * as Modal from "../../atoms/modal/modal"; +import * as MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; import * as NavContainer from "../nav-container/nav-container"; import MenuTrash from "./menu-trash"; @@ -87,7 +88,7 @@ describe("MenuTrash", () => { component.unmount(); }); - it("select toggle", () => { + it("select toggle click", () => { jest.spyOn(React, "useContext").mockImplementationOnce(() => { return contextValues; }); @@ -116,6 +117,69 @@ describe("MenuTrash", () => { component.unmount(); }); + it("select toggle keyDown enter continue", () => { + jest.spyOn(React, "useContext").mockImplementationOnce(() => { + return contextValues; + }); + + // usage ==> import * as useFetch from '../hooks/use-fetch'; + jest.spyOn(useFetch, "default").mockImplementationOnce(() => { + return newIConnectionDefault(); + }); + + act(() => { + Router.navigate("/"); + }); + + const component = render( + + ); + + const menuTrashItemSelect = screen.queryByTestId( + "menu-trash-item-select" + ) as HTMLDivElement; + expect(menuTrashItemSelect).toBeTruthy(); + + fireEvent.keyDown(menuTrashItemSelect, { + key: "Enter" + }); + + expect(Router.state.location.search).toBe("?select="); + component.unmount(); + }); + + it("select toggle keyDown tab skip", () => { + jest.spyOn(React, "useContext").mockImplementationOnce(() => { + return contextValues; + }); + + // usage ==> import * as useFetch from '../hooks/use-fetch'; + jest.spyOn(useFetch, "default").mockImplementationOnce(() => { + return newIConnectionDefault(); + }); + + act(() => { + Router.navigate("/"); + }); + + const component = render( + + ); + + const menuTrashItemSelect = screen.queryByTestId( + "menu-trash-item-select" + ) as HTMLDivElement; + expect(menuTrashItemSelect).toBeTruthy(); + + fireEvent.keyDown(menuTrashItemSelect, { + key: "Tab" + }); + + expect(Router.state.location.search).toBe(""); + + component.unmount(); + }); + it("more select all", () => { // usage ==> import * as useFetch from '../hooks/use-fetch'; jest.spyOn(useFetch, "default").mockImplementationOnce(() => { @@ -238,6 +302,90 @@ describe("MenuTrash", () => { }); }); + it("more keyDown delete tab so skip", () => { + // usage ==> import * as useFetch from '../hooks/use-fetch'; + jest.spyOn(useFetch, "default").mockImplementationOnce(() => { + return newIConnectionDefault(); + }); + + jest.spyOn(window, "scrollTo").mockImplementationOnce(() => {}); + + const modalSpy = jest + .spyOn(Modal, "default") + .mockReset() + .mockImplementationOnce(({ children }) => { + return <>{children}; + }); + + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/?select=test1.jpg"); + }); + + const component = render( + + ); + + const item = screen.queryByTestId("delete") as HTMLElement; + + act(() => { + fireEvent.keyDown(item, { key: "Tab" }); + }); + + expect(modalSpy).toBeCalledTimes(0); + + expect(window.location.search).toBe("?select=test1.jpg"); + + // cleanup + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/"); + component.unmount(); + }); + }); + + it("more keyDown delete enter", () => { + // usage ==> import * as useFetch from '../hooks/use-fetch'; + jest.spyOn(useFetch, "default").mockImplementationOnce(() => { + return newIConnectionDefault(); + }); + + jest.spyOn(window, "scrollTo").mockImplementationOnce(() => {}); + + const modalSpy = jest + .spyOn(Modal, "default") + .mockReset() + .mockImplementationOnce(({ children }) => { + return <>{children}; + }); + + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/?select=test1.jpg"); + }); + + const component = render( + + ); + + const item = screen.queryByTestId("delete") as HTMLElement; + + act(() => { + fireEvent.keyDown(item, { key: "Enter" }); + }); + + expect(modalSpy).toBeCalledTimes(1); + + expect(window.location.search).toBe("?select=test1.jpg"); + + // cleanup + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/"); + component.unmount(); + }); + }); + it("more force delete, expect modal 2", () => { // usage ==> import * as useFetch from '../hooks/use-fetch'; jest.spyOn(useFetch, "default").mockImplementationOnce(() => { @@ -248,14 +396,15 @@ describe("MenuTrash", () => { const modalSpy = jest .spyOn(Modal, "default") + .mockReset() .mockImplementationOnce(({ children }) => { return {children}; }); + Router.navigate("/?select=test1.jpg"); const component = render( ); - Router.navigate("/?select=test1.jpg"); const item = screen.queryByTestId("delete"); @@ -279,18 +428,10 @@ describe("MenuTrash", () => { // usage ==> import * as useFetch from '../hooks/use-fetch'; jest .spyOn(useFetch, "default") - .mockImplementationOnce(() => { - return newIConnectionDefault(); - }) - .mockImplementationOnce(() => { - return newIConnectionDefault(); - }) - .mockImplementationOnce(() => { - return newIConnectionDefault(); - }) - .mockImplementationOnce(() => { - return newIConnectionDefault(); - }); + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()); // spy on fetch // use this import => import * as FetchPost from '../shared/fetch-post'; @@ -332,6 +473,57 @@ describe("MenuTrash", () => { }); }); + it("more restore-from-trash keyboardDown", async () => { + // usage ==> import * as useFetch from '../hooks/use-fetch'; + jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()) + .mockImplementationOnce(() => newIConnectionDefault()); + + // spy on fetch + // use this import => import * as FetchPost from '../shared/fetch-post'; + const mockIConnectionDefault: Promise = + Promise.resolve(newIConnectionDefault()); + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/?select=test1.jpg"); + }); + + const component = render( + + ); + + const item = screen.queryByTestId("restore-from-trash") as HTMLElement; + + // // need to await here + await act(async () => { + fireEvent.keyDown(item, { + key: "Enter" + }); + }); + + expect(Router.state.location.search).toBe("?select="); + + expect(fetchPostSpy).toBeCalled(); + expect(fetchPostSpy).toBeCalledWith( + new UrlQuery().UrlReplaceApi(), + "fieldName=tags&search=%21delete%21&f=%2Fundefined%2Ftest1.jpg" + ); + + // cleanup + act(() => { + // to use with: => import { act } from 'react-dom/test-utils'; + Router.navigate("/"); + component.unmount(); + }); + }); + it("keyboard ctrl a and command a", () => { jest.spyOn(React, "useContext").mockRestore(); jest.spyOn(NavContainer, "default").mockImplementationOnce(() => <>); @@ -387,5 +579,24 @@ describe("MenuTrash", () => { jest.spyOn(React, "useContext").mockRestore(); component.unmount(); }); + + it("NavContainer MenuSearchBar callback does change state [MenuTrash]", () => { + jest.spyOn(MenuSearchBar, "default").mockImplementationOnce((prop) => { + if (prop.callback) { + prop.callback("test"); + } + return <>test; + }); + + const component = render( + + ); + + const navOpen = screen.queryByTestId("nav-open") as HTMLDivElement; + + expect(navOpen).toBeTruthy(); + + component.unmount(); + }); }); }); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx index fdf0f3fec3..75d2c08beb 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx @@ -15,6 +15,9 @@ import HamburgerMenuToggle from "../../atoms/hamburger-menu-toggle/hamburger-men import MoreMenu from "../../atoms/more-menu/more-menu"; import Preloader from "../../atoms/preloader/preloader"; import MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; +import { MenuOptionSelectionAll } from "../../molecules/menu-option-selection-all/menu-option-selection-all"; +import { MenuOptionSelectionUndo } from "../../molecules/menu-option-selection-undo/menu-option-selection-undo"; +import { MenuSelectCount } from "../../molecules/menu-select-count/menu-select-count"; import ModalForceDelete from "../modal-force-delete/modal-force-delete"; import NavContainer from "../nav-container/nav-container"; @@ -32,13 +35,7 @@ const MenuTrash: React.FunctionComponent = ({ // Content const MessageSelectAction = language.text("Selecteer", "Select"); - const MessageSelectPresentPerfect = language.text("geselecteerd", "selected"); - const MessageNoneSelected = language.text( - "Niets geselecteerd", - "Nothing selected" - ); const MessageSelectAll = language.text("Alles selecteren", "Select all"); - const MessageUndoSelection = language.text("Undo selectie", "Undo selection"); const MessageRestoreFromTrash = language.text( "Zet terug uit prullenmand", "Restore from Trash" @@ -137,26 +134,10 @@ const MenuTrash: React.FunctionComponent = ({ setHamburgerMenu={setHamburgerMenu} /> - {select && select.length === 0 ? ( - - ) : null} - {select && select.length >= 1 ? ( - - ) : null} + {!select && state.fileIndexItems.length >= 1 ? (
    = ({ onClick={() => { removeSidebarSelection(); }} + onKeyDown={(event) => { + event.key === "Enter" && removeSidebarSelection(); + }} > {MessageSelectAction}
    @@ -207,28 +191,26 @@ const MenuTrash: React.FunctionComponent = ({ setEnableMoreMenu={setEnableMoreMenu} enableMoreMenu={enableMoreMenu} > - {select.length === state.fileIndexItems.length ? ( -
  • undoSelection()} - > - {MessageUndoSelection} -
  • - ) : null} - {select.length !== state.fileIndexItems.length ? ( -
  • allSelection()} - > - {MessageSelectAll} -
  • - ) : null} + + + +
  • undoTrash()} + tabIndex={0} + onKeyDown={(event) => { + event.key === "Enter" && undoTrash(); + }} > {MessageRestoreFromTrash}
  • @@ -236,6 +218,10 @@ const MenuTrash: React.FunctionComponent = ({ className="menu-option" data-test="delete" onClick={() => setModalDeleteOpen(true)} + tabIndex={0} + onKeyDown={(event) => { + event.key === "Enter" && setModalDeleteOpen(true); + }} > {MessageDeleteImmediately} diff --git a/starsky/starsky/clientapp/src/components/organisms/nav-container/nav-container.tsx b/starsky/starsky/clientapp/src/components/organisms/nav-container/nav-container.tsx index c72be239cf..b8a03aedf7 100644 --- a/starsky/starsky/clientapp/src/components/organisms/nav-container/nav-container.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/nav-container/nav-container.tsx @@ -8,7 +8,10 @@ const NavContainer: React.FunctionComponent = ({ hamburgerMenu }) => { return ( -