diff --git a/scm-ui/ui-components/src/modals/activeModalCountContext.ts b/scm-ui/ui-components/src/modals/activeModalCountContext.ts index acbb81364a..5aabddc203 100644 --- a/scm-ui/ui-components/src/modals/activeModalCountContext.ts +++ b/scm-ui/ui-components/src/modals/activeModalCountContext.ts @@ -26,4 +26,15 @@ import React from "react"; export type ModalStateContextType = { value: number; increment: () => void; decrement: () => void }; -export default React.createContext({} as ModalStateContextType); +export default React.createContext({ + value: 0, + increment: () => { + // eslint-disable-next-line no-console + console.warn( + "Modals should be declared inside a ModalStateContext. Did you use the deprecated 'confirmAlert' function over the 'ConfirmAlert' component ?" + ); + }, + decrement: () => { + /* Do nothing */ + }, +}); diff --git a/scm-ui/ui-shortcuts/package.json b/scm-ui/ui-shortcuts/package.json index 0a5176f1d8..22247f4c9d 100644 --- a/scm-ui/ui-shortcuts/package.json +++ b/scm-ui/ui-shortcuts/package.json @@ -30,7 +30,8 @@ "@scm-manager/eslint-config": "^2.16.0", "@scm-manager/tsconfig": "^2.13.0", "@testing-library/react-hooks": "8.0.1", - "@testing-library/react": "12.1.5" + "@testing-library/react": "12.1.5", + "jest-extended": "3.1.0" }, "babel": { "presets": [ @@ -43,5 +44,8 @@ }, "publishConfig": { "access": "public" + }, + "jest": { + "setupFilesAfterEnv": ["jest-extended/all"] } } diff --git a/scm-ui/ui-shortcuts/src/index.ts b/scm-ui/ui-shortcuts/src/index.ts index 4162a7fcce..284ec527c0 100644 --- a/scm-ui/ui-shortcuts/src/index.ts +++ b/scm-ui/ui-shortcuts/src/index.ts @@ -25,4 +25,4 @@ export { default as useShortcut } from "./useShortcut"; export { default as useShortcutDocs, ShortcutDocsContextProvider } from "./useShortcutDocs"; export { default as usePauseShortcuts } from "./usePauseShortcuts"; -export { useKeyboardIteratorTarget, KeyboardIterator } from "./iterator/keyboardIterator"; +export { useKeyboardIteratorTarget, KeyboardIterator, KeyboardSubIterator } from "./iterator/keyboardIterator"; diff --git a/scm-ui/ui-shortcuts/src/iterator/callbackIterator.ts b/scm-ui/ui-shortcuts/src/iterator/callbackIterator.ts new file mode 100644 index 0000000000..ff5e4dee85 --- /dev/null +++ b/scm-ui/ui-shortcuts/src/iterator/callbackIterator.ts @@ -0,0 +1,220 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { MutableRefObject, useMemo, useRef } from "react"; + +const INACTIVE_INDEX = -1; + +export type Callback = () => void; + +type Direction = "forward" | "backward"; + +/** + * Restricts the api surface exposed by {@link CallbackIterator} so that we do not have to implement + * the whole class when providing a default context. + */ +export type CallbackRegistry = { + /** + * Registers the given item and returns its index to use in {@link deregister}. + */ + register: (item: Callback | CallbackIterator) => number; + + /** + * Use the index returned from {@link register} to de-register. + */ + deregister: (index: number) => void; +}; + +const isSubiterator = (item?: Callback | CallbackIterator): item is CallbackIterator => + item instanceof CallbackIterator; + +const offset = (direction: Direction) => (direction === "forward" ? 1 : -1); + +/** + * ## Definition + * - A list of callback functions and/or recursively nested iterators + * - The iterator can move in either direction + * - New items can be added/removed on-the-fly + * + * ## Terminology + * - Item: Either a callback or a nested iterator + * - Available: Item is a non-empty iterator OR a regular callback + * - Inactive: Current index is -1 + * - Activate: Move iterator while in inactive state OR call regular callback + * + * ## Moving + * When an iterator is moved in either direction, there are 4 cases: + * + * 1. The iterator is inactive => activate item at first available index from given direction + * 1. The current item is a sub-iterator with more items in the given direction => move the sub-iterator + * 1. The current item is a sub-iterator that has reached its bounds in the given direction => reset sub-iterator & activate item at next available index + * 1. The current item is not a sub-iterator => activate item at next available index + */ +export class CallbackIterator implements CallbackRegistry { + private parent?: CallbackIterator; + + constructor( + private readonly activeIndexRef: MutableRefObject, + private readonly itemsRef: MutableRefObject> + ) {} + + private get activeIndex() { + return this.activeIndexRef.current; + } + + private set activeIndex(newValue: number) { + this.activeIndexRef.current = newValue; + } + + private get items() { + return this.itemsRef.current; + } + + private get currentItem(): Callback | CallbackIterator | undefined { + return this.items[this.activeIndex]; + } + + private get isInactive() { + return this.activeIndex === INACTIVE_INDEX; + } + + private get lastIndex() { + return this.items.length - 1; + } + + private firstIndex = (direction: "forward" | "backward") => { + return direction === "forward" ? 0 : this.lastIndex; + }; + + private firstAvailableIndex = (direction: Direction, fromIndex = this.firstIndex(direction)) => { + for (; direction === "forward" ? fromIndex < this.items.length : fromIndex >= 0; fromIndex += offset(direction)) { + const callback = this.items[fromIndex]; + if (callback) { + if (!isSubiterator(callback) || callback.hasNext(direction)) { + return fromIndex; + } + } + } + return null; + }; + + private hasAvailableIndex = (direction: Direction, fromIndex?: number) => { + return this.firstAvailableIndex(direction, fromIndex) !== null; + }; + + private activateCurrentItem = (direction: Direction) => { + if (isSubiterator(this.currentItem)) { + this.currentItem.move(direction); + } else if (this.currentItem) { + this.currentItem(); + } + }; + + private setIndexAndActivateCurrentItem = (index: number | null, direction: Direction) => { + if (index !== null && index !== INACTIVE_INDEX) { + this.activeIndex = index; + this.activateCurrentItem(direction); + } + }; + + private move = (direction: Direction) => { + if (isSubiterator(this.currentItem) && this.currentItem.hasNext(direction)) { + this.currentItem.move(direction); + } else { + if (isSubiterator(this.currentItem)) { + this.currentItem.reset(); + } + let nextIndex: number | null; + if (this.isInactive) { + nextIndex = this.firstAvailableIndex(direction); + } else { + nextIndex = this.firstAvailableIndex(direction, this.activeIndex + offset(direction)); + } + this.setIndexAndActivateCurrentItem(nextIndex, direction); + } + }; + + private hasNext = (inDirection: Direction): boolean => { + if (this.isInactive) { + return this.hasAvailableIndex(inDirection); + } + if (isSubiterator(this.currentItem) && this.currentItem.hasNext(inDirection)) { + return true; + } + return this.hasAvailableIndex(inDirection, this.activeIndex + offset(inDirection)); + }; + + public next = () => { + if (this.hasNext("forward")) { + return this.move("forward"); + } + }; + + public previous = () => { + if (this.hasNext("backward")) { + return this.move("backward"); + } + }; + + public reset = () => { + this.activeIndex = INACTIVE_INDEX; + for (const cb of this.items) { + if (isSubiterator(cb)) { + cb.reset(); + } + } + }; + + public register = (item: Callback | CallbackIterator) => { + if (isSubiterator(item)) { + item.parent = this; + } + return this.items.push(item) - 1; + }; + + public deregister = (index: number) => { + this.items.splice(index, 1); + if (this.activeIndex === index || this.activeIndex >= this.items.length) { + if (this.hasAvailableIndex("backward", index)) { + this.setIndexAndActivateCurrentItem(this.firstAvailableIndex("backward", index), "backward"); + } else if (this.hasAvailableIndex("forward", index)) { + this.setIndexAndActivateCurrentItem(this.firstAvailableIndex("forward", index), "backward"); + } else if (this.parent) { + if (this.parent.hasNext("forward")) { + this.parent.move("forward"); + } else if (this.parent.hasNext("backward")) { + this.parent.move("backward"); + } + } else { + this.reset(); + } + } + }; +} + +export const useCallbackIterator = (initialIndex = INACTIVE_INDEX) => { + const items = useRef>([]); + const activeIndex = useRef(initialIndex); + return useMemo(() => new CallbackIterator(activeIndex, items), [activeIndex, items]); +}; diff --git a/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.test.tsx b/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.test.tsx index f6379f61cd..dd52b8a817 100644 --- a/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.test.tsx +++ b/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.test.tsx @@ -24,10 +24,16 @@ import { renderHook } from "@testing-library/react-hooks"; import React, { FC } from "react"; -import { KeyboardIteratorContextProvider, useKeyboardIteratorCallback } from "./keyboardIterator"; +import { + KeyboardIteratorContextProvider, + KeyboardSubIterator, + KeyboardSubIteratorContextProvider, + useKeyboardIteratorItem, +} from "./keyboardIterator"; import { render } from "@testing-library/react"; import { ShortcutDocsContextProvider } from "../useShortcutDocs"; import Mousetrap from "mousetrap"; +import "jest-extended"; jest.mock("react-i18next", () => ({ useTranslation: () => [jest.fn()], @@ -49,7 +55,7 @@ const createWrapper = {children}; const Item: FC<{ callback: () => void }> = ({ callback }) => { - useKeyboardIteratorCallback(callback); + useKeyboardIteratorItem(callback); return
  • example
  • ; }; @@ -69,7 +75,7 @@ describe("shortcutIterator", () => { it("should not call callback upon registration", () => { const callback = jest.fn(); - renderHook(() => useKeyboardIteratorCallback(callback), { + renderHook(() => useKeyboardIteratorItem(callback), { wrapper: Wrapper, }); @@ -79,7 +85,7 @@ describe("shortcutIterator", () => { it("should not throw if not inside keyboard iterator context", () => { const callback = jest.fn(); - const { result, unmount } = renderHook(() => useKeyboardIteratorCallback(callback), { + const { result, unmount } = renderHook(() => useKeyboardIteratorItem(callback), { wrapper: DocsWrapper, }); @@ -203,6 +209,26 @@ describe("shortcutIterator", () => { expect(callback3).not.toHaveBeenCalled(); }); + it("should move to existing index when active index in the middle is deregistered", async () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + const { rerender } = render(, { + wrapper: createWrapper(1), + }); + + expect(callback).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + + rerender(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + }); + it("should not move on deregistration if iterator is not active", async () => { const callback = jest.fn(); const callback2 = jest.fn(); @@ -236,4 +262,170 @@ describe("shortcutIterator", () => { expect(callback).not.toHaveBeenCalled(); }); + + describe("With Subiterator", () => { + it("should call in correct order", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + render( + + + + + + + ); + + Mousetrap.trigger("j"); + Mousetrap.trigger("j"); + Mousetrap.trigger("j"); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback3).toHaveBeenCalledTimes(1); + + expect(callback).toHaveBeenCalledBefore(callback2); + expect(callback2).toHaveBeenCalledBefore(callback3); + }); + + it("should call first target that is not an empty subiterator", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + render( + + + + + + + + + + ); + + Mousetrap.trigger("j"); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + }); + + it("should skip empty sub-iterators during navigation", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + render( + + + + + + + + + + + ); + + Mousetrap.trigger("j"); + Mousetrap.trigger("j"); + Mousetrap.trigger("j"); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback3).toHaveBeenCalledTimes(1); + }); + + it("should not enter subiterator if its empty", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + render( + + + + + + + ); + + Mousetrap.trigger("k"); + + expect(callback).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + }); + + it("should not loop", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + + render( + + + + + + + ); + + Mousetrap.trigger("k"); + Mousetrap.trigger("k"); + Mousetrap.trigger("k"); + Mousetrap.trigger("k"); + Mousetrap.trigger("k"); + Mousetrap.trigger("k"); + + expect(callback3).not.toHaveBeenCalled(); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledTimes(1); + + expect(callback2).toHaveBeenCalledBefore(callback); + }); + + it("should move subiterator if its active callback is de-registered", () => { + const callback = jest.fn(); + const callback2 = jest.fn(); + const callback3 = jest.fn(); + const callback4 = jest.fn(); + + const { rerender } = render( + <> + + + + + , + { + wrapper: createWrapper(0), + } + ); + + expect(callback).not.toHaveBeenCalled(); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + expect(callback4).not.toHaveBeenCalled(); + + rerender( + <> + + + + + + ); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback2).not.toHaveBeenCalled(); + expect(callback3).not.toHaveBeenCalled(); + expect(callback4).not.toHaveBeenCalled(); + }); + }); }); diff --git a/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.tsx b/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.tsx index bcf1d1ad11..8cc8ee384b 100644 --- a/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.tsx +++ b/scm-ui/ui-shortcuts/src/iterator/keyboardIterator.tsx @@ -22,18 +22,12 @@ * SOFTWARE. */ -import React, { FC, useCallback, useContext, useEffect, useMemo, useRef } from "react"; +import React, { FC, useCallback, useContext, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useShortcut } from "../index"; +import { Callback, CallbackIterator, CallbackRegistry, useCallbackIterator } from "./callbackIterator"; -type Callback = () => void; - -type KeyboardIteratorContextType = { - register: (callback: Callback) => number; - deregister: (index: number) => void; -}; - -const KeyboardIteratorContext = React.createContext({ +const KeyboardIteratorContext = React.createContext({ register: () => { if (process.env.NODE_ENV === "development") { // eslint-disable-next-line no-console @@ -49,77 +43,45 @@ const KeyboardIteratorContext = React.createContext }, }); -export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex = -1 }) => { +export const useKeyboardIteratorItem = (item: Callback | CallbackIterator) => { + const { register, deregister } = useContext(KeyboardIteratorContext); + useEffect(() => { + const index = register(item); + return () => deregister(index); + }, [item, register, deregister]); +}; + +export const KeyboardSubIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => { + const callbackIterator = useCallbackIterator(initialIndex); + + useKeyboardIteratorItem(callbackIterator); + + return {children}; +}; + +export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => { const [t] = useTranslation("commons"); - const callbacks = useRef>([]); - const activeIndex = useRef(initialIndex); - const executeCallback = useCallback((index: number) => callbacks.current[index](), []); + const callbackIterator = useCallbackIterator(initialIndex); - const navigateBackward = useCallback(() => { - if (activeIndex.current === -1) { - activeIndex.current = callbacks.current.length - 1; - executeCallback(activeIndex.current); - } else if (activeIndex.current > 0) { - activeIndex.current -= 1; - executeCallback(activeIndex.current); - } - }, [executeCallback]); - - const navigateForward = useCallback(() => { - if (activeIndex.current === -1) { - activeIndex.current = 0; - executeCallback(activeIndex.current); - } else if (activeIndex.current < callbacks.current.length - 1) { - activeIndex.current += 1; - executeCallback(activeIndex.current); - } - }, [executeCallback]); - - const value = useMemo( - () => ({ - register: (callback: () => void) => callbacks.current.push(callback) - 1, - deregister: (index: number) => { - callbacks.current.splice(index, 1); - if (callbacks.current.length === 0) { - activeIndex.current = -1; - } else if (activeIndex.current === index || activeIndex.current >= callbacks.current.length) { - if (activeIndex.current > 0) { - activeIndex.current -= 1; - } - executeCallback(activeIndex.current); - } - }, - }), - [executeCallback] - ); - - useShortcut("k", navigateBackward, { + useShortcut("k", callbackIterator.previous.bind(callbackIterator), { description: t("shortcuts.iterator.previous"), }); - useShortcut("j", navigateForward, { + useShortcut("j", callbackIterator.next.bind(callbackIterator), { description: t("shortcuts.iterator.next"), }); useShortcut("tab", () => { - activeIndex.current = -1; + callbackIterator.reset(); return true; }); - return {children}; -}; - -export const useKeyboardIteratorCallback = (callback: Callback) => { - const { register, deregister } = useContext(KeyboardIteratorContext); - useEffect(() => { - const index = register(callback); - return () => deregister(index); - }, [callback, register, deregister]); + return {children}; }; /** - * Use the {@link React.RefObject} returned from this hook to register a target to the nearest enclosing {@link KeyboardIterator}. + * Use the {@link React.RefObject} returned from this hook to register a target to the nearest enclosing {@link KeyboardIterator} or {@link KeyboardSubIterator}. * * @example * const ref = useKeyboardIteratorTarget(); @@ -133,7 +95,7 @@ export function useKeyboardIteratorTarget(): React.RefCallback { ref.current = el; } }, []); - useKeyboardIteratorCallback(callback); + useKeyboardIteratorItem(callback); return refCallback; } @@ -144,7 +106,36 @@ export function useKeyboardIteratorTarget(): React.RefCallback { * * Press `k` to navigate backwards and `j` to navigate forward. * Pressing `tab` will reset the iterator to its initial state. + * + * Use the {@link KeyboardSubIterator} to wrap asynchronously loaded targets. */ export const KeyboardIterator: FC = ({ children }) => ( {children} ); + +/** + * Allows deferred {@link useKeyboardIteratorTarget} invocations enclosed in this sub-iterator to be registered in the correct order within a {@link KeyboardIterator}. + * + * This is especially useful for extension points which might contain further iterable elements that are loaded asynchronously. + * + * @example + * + * + * + * name="repository.overview.top" + * renderAll={true} + * props={{ + * page, + * search, + * namespace, + * }} + * /> + * + * {groups.map((group) => { + * return ; + * })} + * + */ +export const KeyboardSubIterator: FC = ({ children }) => ( + {children} +); diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx index 06c8efdcf7..f32398aee7 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx +++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx @@ -28,7 +28,7 @@ import { NamespaceCollection, Repository } from "@scm-manager/ui-types"; import groupByNamespace from "./groupByNamespace"; import RepositoryGroupEntry from "./RepositoryGroupEntry"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; -import { KeyboardIterator } from "@scm-manager/ui-shortcuts"; +import { KeyboardIterator, KeyboardSubIterator } from "@scm-manager/ui-shortcuts"; type Props = { repositories: Repository[]; @@ -45,16 +45,18 @@ class RepositoryList extends React.Component { const groups = groupByNamespace(repositories, namespaces); return (
    - - name="repository.overview.top" - renderAll={true} - props={{ - page, - search, - namespace, - }} - /> + + + name="repository.overview.top" + renderAll={true} + props={{ + page, + search, + namespace, + }} + /> + {groups.map((group) => { return ; })} diff --git a/yarn.lock b/yarn.lock index 9fc4525a6c..326aebca5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1806,6 +1806,13 @@ dependencies: "@sinclair/typebox" "^0.24.1" +"@jest/schemas@^29.0.0": + version "29.0.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.0.0.tgz#5f47f5994dd4ef067fb7b4188ceac45f77fe952a" + integrity sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA== + dependencies: + "@sinclair/typebox" "^0.24.1" + "@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" @@ -7735,6 +7742,11 @@ diff-sequences@^28.1.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== +diff-sequences@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" + integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -11399,6 +11411,16 @@ jest-diff@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" +jest-diff@^29.0.0: + version "29.3.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.3.1.tgz#d8215b72fed8f1e647aed2cae6c752a89e757527" + integrity sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.3.1" + jest-get-type "^29.2.0" + pretty-format "^29.3.1" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -11483,6 +11505,14 @@ jest-environment-node@^26.6.2: jest-mock "^26.6.2" jest-util "^26.6.2" +jest-extended@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-3.1.0.tgz#7998699751f3b5d9207d212b39c1837f59c7fecc" + integrity sha512-BbuAVUb2dchgwm7euayVt/7hYlkKaknQItKyzie7Li8fmXCglgf21XJeRIdOITZ/cMOTTj5Oh5IjQOxQOe/hfQ== + dependencies: + jest-diff "^29.0.0" + jest-get-type "^29.0.0" + jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" @@ -11498,6 +11528,11 @@ jest-get-type@^28.0.2: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== +jest-get-type@^29.0.0, jest-get-type@^29.2.0: + version "29.2.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" + integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -15184,6 +15219,15 @@ pretty-format@^28.0.0, pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.3.1: + version "29.3.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da" + integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg== + dependencies: + "@jest/schemas" "^29.0.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty-format@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"