From 642eae345f377f409f32dfae8039fa9c5504044c Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Wed, 15 Nov 2023 16:45:52 -0500 Subject: [PATCH] [MergeDragDrop] Add basic tests (#2792) --- .../MergeDupsStep/MergeDragDrop/index.tsx | 42 ++--- .../MergeDragDrop/tests/index.test.tsx | 158 ++++++++++++++++++ .../MergeDuplicates/Redux/MergeDupsActions.ts | 4 +- .../MergeDuplicates/Redux/MergeDupsReducer.ts | 20 +-- .../Redux/MergeDupsReduxTypes.ts | 8 +- .../Redux/tests/MergeDupsDataMock.ts | 54 +++--- .../Redux/tests/MergeDupsReducer.test.tsx | 14 +- 7 files changed, 225 insertions(+), 75 deletions(-) create mode 100644 src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/tests/index.test.tsx diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx index be213702a9..08a13106cb 100644 --- a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx @@ -34,30 +34,28 @@ export default function MergeDragDrop(): ReactElement { const treeWords = mergeState.tree.words; function handleDrop(res: DropResult): void { - const senseRef: MergeTreeReference = JSON.parse(res.draggableId); - const sourceId = res.source.droppableId; - if ( - treeWords[sourceId]?.protected && - Object.keys(treeWords[sourceId].sensesGuids).length == 1 - ) { + const src: MergeTreeReference = JSON.parse(res.draggableId); + const srcWordId = res.source.droppableId; + const srcWord = treeWords[srcWordId]; + if (srcWord?.protected && Object.keys(srcWord.sensesGuids).length === 1) { // Case 0: The final sense of a protected word cannot be moved. return; } else if (res.destination?.droppableId === trashId) { // Case 1: The sense was dropped on the trash icon. - if (senseRef.isSenseProtected) { + if (src.isSenseProtected) { // Case 1a: Cannot delete a protected sense. return; } setSenseToDelete(res.draggableId); } else if (res.combine) { // Case 2: the sense was dropped on another sense. - if (senseRef.isSenseProtected) { + if (src.isSenseProtected) { // Case 2a: Cannot merge a protected sense into another sense. - if (sourceId !== res.combine.droppableId) { + if (srcWordId !== res.combine.droppableId) { // The target sense is in a different word, so move instead of combine. dispatch( moveSense({ - ref: senseRef, + src, destWordId: res.combine.droppableId, destOrder: 0, }) @@ -72,37 +70,33 @@ export default function MergeDragDrop(): ReactElement { // Case 2b: If the target is a sidebar sub-sense, it cannot receive a combine. return; } - dispatch(combineSense({ src: senseRef, dest: combineRef })); + dispatch(combineSense({ src, dest: combineRef })); } else if (res.destination) { - const destId = res.destination.droppableId; + const destWordId = res.destination.droppableId; // Case 3: The sense was dropped in a droppable. - if (sourceId !== destId) { + if (srcWordId !== destWordId) { // Case 3a: The source, dest droppables are different. - if (destId.split(" ").length > 1) { + if (destWordId.split(" ").length > 1) { // If the destination is SidebarDrop, it cannot receive drags from elsewhere. return; } // Move the sense to the dest MergeWord. dispatch( - moveSense({ - ref: senseRef, - destWordId: destId, - destOrder: res.destination.index, - }) + moveSense({ src, destWordId, destOrder: res.destination.index }) ); } else { // Case 3b: The source & dest droppables are the same, so we reorder, not move. - const order = res.destination.index; + const destOrder = res.destination.index; if ( - senseRef.order === order || - (order === 0 && - senseRef.order !== undefined && + src.order === destOrder || + (destOrder === 0 && + src.order !== undefined && sidebar.senses[0].protected) ) { // If the sense wasn't moved or was moved within the sidebar above a protected sense, do nothing. return; } - dispatch(orderSense({ ref: senseRef, order: order })); + dispatch(orderSense({ src, destOrder })); } } } diff --git a/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/tests/index.test.tsx b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/tests/index.test.tsx new file mode 100644 index 0000000000..a381da3a3a --- /dev/null +++ b/src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/tests/index.test.tsx @@ -0,0 +1,158 @@ +import { IconButton } from "@mui/material"; +import { Provider } from "react-redux"; +import { ReactTestRenderer, act, create } from "react-test-renderer"; +import configureMockStore from "redux-mock-store"; + +import "tests/reactI18nextMock"; + +import { GramCatGroup, Sense } from "api/models"; +import { defaultState } from "components/App/DefaultState"; +import MergeDragDrop from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop"; +import DragSense from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DragSense"; +import DropWord from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord"; +import { + convertSenseToMergeTreeSense, + newMergeTreeWord, +} from "goals/MergeDuplicates/MergeDupsTreeTypes"; +import { MergeTreeState } from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes"; +import { newSemanticDomain } from "types/semanticDomain"; +import { + newDefinition, + newGrammaticalInfo, + newSense, + newWord, +} from "types/word"; + +jest.mock("react-beautiful-dnd", () => ({ + ...jest.requireActual("react-beautiful-dnd"), + Draggable: ({ children }: any) => + children({ draggableProps: {}, innerRef: jest.fn() }, {}, {}), + Droppable: ({ children }: any) => children({ innerRef: jest.fn() }, {}), +})); +jest.mock("react-router-dom", () => ({ + useNavigate: jest.fn(), +})); + +jest.mock("backend", () => ({})); +jest.mock("goals/MergeDuplicates/Redux/MergeDupsActions", () => ({ + setSidebar: (...args: any[]) => mockSetSidebar(...args), +})); +jest.mock("types/hooks", () => { + return { + ...jest.requireActual("types/hooks"), + useAppDispatch: () => jest.fn(), + }; +}); + +const mockSetSidebar = jest.fn(); + +let testRenderer: ReactTestRenderer; + +// Words/Senses to be used for a preloaded mergeDuplicateGoal state +const senseBah: Sense = { + ...newSense("bah"), + guid: "guid-sense-bah", + definitions: [newDefinition("defBah")], +}; +const senseBaj: Sense = { + ...newSense("baj"), + guid: "guid-sense-baj", + definitions: [newDefinition("defBaj")], +}; +const senseBar: Sense = { + ...newSense("bar"), + guid: "guid-sense-bar", + semanticDomains: [newSemanticDomain("3", "Language and thought")], +}; +const senseBaz: Sense = { + ...newSense("baz"), + guid: "guid-sense-baz", + grammaticalInfo: { ...newGrammaticalInfo(), catGroup: GramCatGroup.Verb }, +}; + +const wordFoo1 = { + ...newWord("foo"), + id: "wordId-foo1", + senses: [senseBah, senseBaj], +}; +const wordFoo2 = { + ...newWord("foo"), + id: "wordId-foo2", + senses: [senseBar, senseBaz], +}; + +// Scenario: +// Word1: +// vern: foo +// senses: bah/baj +// Word2: +// vern: foo +// senses: bar, baz +const mockTwoWordState = (): MergeTreeState => ({ + data: { + senses: { + [senseBah.guid]: convertSenseToMergeTreeSense(senseBah, wordFoo1.id, 0), + [senseBaj.guid]: convertSenseToMergeTreeSense(senseBaj, wordFoo1.id, 1), + [senseBar.guid]: convertSenseToMergeTreeSense(senseBar, wordFoo2.id, 0), + [senseBaz.guid]: convertSenseToMergeTreeSense(senseBaz, wordFoo2.id, 1), + }, + words: { [wordFoo1.id]: wordFoo1, [wordFoo2.id]: wordFoo2 }, + }, + tree: { + sidebar: { senses: [], wordId: "", mergeSenseId: "" }, + words: { + [wordFoo1.id]: newMergeTreeWord(wordFoo1.vernacular, { + word1_senseA: [senseBah.guid, senseBaj.guid], + }), + [wordFoo2.id]: newMergeTreeWord(wordFoo2.vernacular, { + word2_senseA: [senseBar.guid], + word2_senseB: [senseBaz.guid], + }), + }, + }, + mergeWords: [], +}); + +const renderMergeDragDrop = async ( + mergeDuplicateGoal: MergeTreeState +): Promise => { + await act(async () => { + testRenderer = create( + + + + ); + }); +}; + +beforeEach(async () => { + jest.clearAllMocks(); + await renderMergeDragDrop(mockTwoWordState()); +}); + +describe("MergeDragDrop", () => { + it("render all columns with right number of senses", async () => { + const wordCols = testRenderer.root.findAllByType(DropWord); + expect(wordCols).toHaveLength(3); + expect(wordCols[0].findAllByType(DragSense)).toHaveLength(1); + expect(wordCols[1].findAllByType(DragSense)).toHaveLength(2); + expect(wordCols[2].findAllByType(DragSense)).toHaveLength(0); + }); + + it("renders with button for opening the sidebar", async () => { + const iconButtons = testRenderer.root.findAllByType(IconButton); + const sidebarButtons = iconButtons.filter((b) => + b.props.id.includes("sidebar") + ); + expect(sidebarButtons).toHaveLength(1); + mockSetSidebar.mockReset(); + await act(async () => { + sidebarButtons[0].props.onClick(); + }); + expect(mockSetSidebar).toHaveBeenCalledTimes(1); + const callArg = mockSetSidebar.mock.calls[0][0]; + expect(callArg.mergeSenseId).toEqual("word1_senseA"); + }); +}); diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts index 88a697a4ce..ea0929456a 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsActions.ts @@ -68,7 +68,7 @@ export function getMergeWords(): Action { } export function moveSense(payload: MoveSensePayload): PayloadAction { - if (payload.ref.order === undefined) { + if (payload.src.order === undefined) { return moveSenseAction(payload); } else { return moveDuplicateAction(payload); @@ -76,7 +76,7 @@ export function moveSense(payload: MoveSensePayload): PayloadAction { } export function orderSense(payload: OrderSensePayload): PayloadAction { - if (payload.ref.order === undefined) { + if (payload.src.order === undefined) { return orderSenseAction(payload); } else { return orderDuplicateAction(payload); diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts index 9334e402db..71535a124c 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts @@ -134,11 +134,11 @@ const mergeDuplicatesSlice = createSlice({ } }, moveSenseAction: (state, action) => { - const srcWordId = action.payload.ref.wordId; + const srcWordId = action.payload.src.wordId; const destWordId = action.payload.destWordId; - const srcOrder = action.payload.ref.order; + const srcOrder = action.payload.src.order; if (srcOrder === undefined && srcWordId !== destWordId) { - const mergeSenseId = action.payload.ref.mergeSenseId; + const mergeSenseId = action.payload.src.mergeSenseId; const words = state.tree.words; @@ -166,7 +166,7 @@ const mergeDuplicatesSlice = createSlice({ } }, moveDuplicateAction: (state, action) => { - const srcRef = action.payload.ref; + const srcRef = action.payload.src; // Verify that the ref.order field is defined if (srcRef.order !== undefined) { const destWordId = action.payload.destWordId; @@ -207,10 +207,10 @@ const mergeDuplicatesSlice = createSlice({ } }, orderDuplicateAction: (state, action) => { - const ref = action.payload.ref; + const ref = action.payload.src; const oldOrder = ref.order; - const newOrder = action.payload.order; + const newOrder = action.payload.destOrder; // Ensure the reorder is valid. if (oldOrder !== undefined && oldOrder !== newOrder) { @@ -227,14 +227,14 @@ const mergeDuplicatesSlice = createSlice({ } }, orderSenseAction: (state, action) => { - const word = state.tree.words[action.payload.ref.wordId]; + const word = state.tree.words[action.payload.src.wordId]; // Convert the Hash to an array to expose the order. const sensePairs = Object.entries(word.sensesGuids); - const mergeSenseId = action.payload.ref.mergeSenseId; + const mergeSenseId = action.payload.src.mergeSenseId; const oldOrder = sensePairs.findIndex((p) => p[0] === mergeSenseId); - const newOrder = action.payload.order; + const newOrder = action.payload.destOrder; // Ensure the move is valid. if (oldOrder !== -1 && newOrder !== undefined && oldOrder !== newOrder) { @@ -248,7 +248,7 @@ const mergeDuplicatesSlice = createSlice({ word.sensesGuids[key] = value; } - state.tree.words[action.payload.ref.wordId] = word; + state.tree.words[action.payload.src.wordId] = word; } }, setSidebarAction: (state, action) => { diff --git a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts index 9f526b59cb..73ab2148b3 100644 --- a/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts +++ b/src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts @@ -21,15 +21,13 @@ export interface MergeTreeState { mergeWords: MergeWords[]; } -export interface MoveSensePayload { - ref: MergeTreeReference; +export interface MoveSensePayload extends OrderSensePayload { destWordId: string; - destOrder: number; } export interface OrderSensePayload { - ref: MergeTreeReference; - order: number; + src: MergeTreeReference; + destOrder: number; } export interface SetVernacularPayload { diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts index 0f70b78534..649d60392b 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsDataMock.ts @@ -122,25 +122,25 @@ export const mergeTwoWordsScenario: GetMergeWordsScenario = { mergeDuplicateGoal: { data: { senses: { - "guid-sense-bah": convertSenseToMergeTreeSense( + [senseBah.guid]: convertSenseToMergeTreeSense( senseBah, wordFoo1.id, 0 ), - "guid-sense-bar": convertSenseToMergeTreeSense( + [senseBar.guid]: convertSenseToMergeTreeSense( senseBar, wordFoo2.id, 0 ), - "guid-sense-baz": convertSenseToMergeTreeSense( + [senseBaz.guid]: convertSenseToMergeTreeSense( senseBaz, wordFoo2.id, 1 ), }, words: { - "wordId-foo1": wordFoo1, - "wordId-foo2": wordFoo2, + [wordFoo1.id]: wordFoo1, + [wordFoo2.id]: wordFoo2, }, }, tree: { @@ -150,7 +150,7 @@ export const mergeTwoWordsScenario: GetMergeWordsScenario = { mergeSenseId: "", }, words: { - "wordId-foo2": convertWordToMergeTreeWord({ + [wordFoo2.id]: convertWordToMergeTreeWord({ ...wordFoo2, senses: [senseBar, senseBaz, senseBah], }), @@ -162,11 +162,11 @@ export const mergeTwoWordsScenario: GetMergeWordsScenario = { }, expectedResult: [ { - parent: "wordId-foo2", - senses: ["guid-sense-bah", "guid-sense-bar", "guid-sense-baz"], + parent: wordFoo2.id, + senses: [senseBah.guid, senseBar.guid, senseBaz.guid], semDoms: ["3", "4"], defs: [[], [], [definitionBah]], - children: ["wordId-foo1", "wordId-foo2"], + children: [wordFoo1.id, wordFoo2.id], }, ], }; @@ -188,25 +188,25 @@ export const mergeTwoSensesScenario: GetMergeWordsScenario = { mergeDuplicateGoal: { data: { senses: { - "guid-sense-bah": convertSenseToMergeTreeSense( + [senseBah.guid]: convertSenseToMergeTreeSense( senseBah, wordFoo1.id, 0 ), - "guid-sense-bar": convertSenseToMergeTreeSense( + [senseBar.guid]: convertSenseToMergeTreeSense( senseBar, wordFoo2.id, 0 ), - "guid-sense-baz": convertSenseToMergeTreeSense( + [senseBaz.guid]: convertSenseToMergeTreeSense( senseBaz, wordFoo2.id, 1 ), }, words: { - "wordId-foo1": wordFoo1, - "wordId-foo2": wordFoo2, + [wordFoo1.id]: wordFoo1, + [wordFoo2.id]: wordFoo2, }, }, tree: { @@ -216,7 +216,7 @@ export const mergeTwoSensesScenario: GetMergeWordsScenario = { mergeSenseId: "", }, words: { - "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + [wordFoo2.id]: newMergeTreeWord(wordFoo2.vernacular, { word2_senseA: [senseBar.guid], word2_senseB: [senseBaz.guid, senseBah.guid], }), @@ -228,11 +228,11 @@ export const mergeTwoSensesScenario: GetMergeWordsScenario = { }, expectedResult: [ { - parent: "wordId-foo2", - senses: ["guid-sense-bar", "guid-sense-baz"], + parent: wordFoo2.id, + senses: [senseBar.guid, senseBaz.guid], semDoms: ["3", "4"], defs: [[], [definitionBah]], - children: ["wordId-foo1", "wordId-foo2"], + children: [wordFoo1.id, wordFoo2.id], }, ], }; @@ -254,25 +254,25 @@ export const mergeTwoDefinitionsScenario: GetMergeWordsScenario = { mergeDuplicateGoal: { data: { senses: { - "guid-sense-bah": convertSenseToMergeTreeSense( + [senseBah.guid]: convertSenseToMergeTreeSense( senseBah, wordFoo1.id, 0 ), - "guid-sense-bar": convertSenseToMergeTreeSense( + [senseBar.guid]: convertSenseToMergeTreeSense( senseBar, wordFoo2.id, 0 ), - "guid-sense-bag": convertSenseToMergeTreeSense( + [senseBag.guid]: convertSenseToMergeTreeSense( senseBag, wordFoo2.id, 1 ), }, words: { - "wordId-foo1": wordFoo1, - "wordId-foo2": { ...wordFoo2, senses: [senseBar, senseBag] }, + [wordFoo1.id]: wordFoo1, + [wordFoo2.id]: { ...wordFoo2, senses: [senseBar, senseBag] }, }, }, tree: { @@ -282,7 +282,7 @@ export const mergeTwoDefinitionsScenario: GetMergeWordsScenario = { mergeSenseId: "", }, words: { - "wordId-foo2": newMergeTreeWord(wordFoo2.vernacular, { + [wordFoo2.id]: newMergeTreeWord(wordFoo2.vernacular, { word2_senseA: [senseBar.guid], word2_senseB: [senseBag.guid, senseBah.guid], }), @@ -294,11 +294,11 @@ export const mergeTwoDefinitionsScenario: GetMergeWordsScenario = { }, expectedResult: [ { - parent: "wordId-foo2", - senses: ["guid-sense-bag", "guid-sense-bar"], + parent: wordFoo2.id, + senses: [senseBag.guid, senseBar.guid], semDoms: ["3", "3", "4"], defs: [[], [definitionBagBah]], - children: ["wordId-foo1", "wordId-foo2"], + children: [wordFoo1.id, wordFoo2.id], }, ], }; diff --git a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx index 8147bfee19..1960d90679 100644 --- a/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx +++ b/src/goals/MergeDuplicates/Redux/tests/MergeDupsReducer.test.tsx @@ -423,7 +423,7 @@ describe("MergeDupsReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); const testAction = moveSense({ - ref: testRef, + src: testRef, destWordId: wordId, destOrder: 1, }); @@ -451,7 +451,7 @@ describe("MergeDupsReducer", () => { // Intercept the uuid that will be assigned. const nextGuid = getMockUuid(false); const testAction = moveSense({ - ref: testRef, + src: testRef, destWordId: destWordId, destOrder: 2, }); @@ -478,7 +478,7 @@ describe("MergeDupsReducer", () => { const destWordId = "word1"; const testAction = moveSense({ - ref: testRef, + src: testRef, destWordId: destWordId, destOrder: 1, }); @@ -505,7 +505,7 @@ describe("MergeDupsReducer", () => { const destWordId = "word1"; const testAction = moveSense({ - ref: testRef, + src: testRef, destWordId: destWordId, destOrder: 1, }); @@ -532,7 +532,7 @@ describe("MergeDupsReducer", () => { const destWordId = "word2"; const testAction = moveSense({ - ref: testRef, + src: testRef, destWordId: destWordId, destOrder: 1, }); @@ -555,7 +555,7 @@ describe("MergeDupsReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId, order: 0 }; - const testAction = orderSense({ ref: testRef, order: 1 }); + const testAction = orderSense({ src: testRef, destOrder: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids[mergeSenseId] = [ @@ -571,7 +571,7 @@ describe("MergeDupsReducer", () => { const mergeSenseId = `${wordId}_senseA`; const testRef: MergeTreeReference = { wordId, mergeSenseId }; - const testAction = orderSense({ ref: testRef, order: 1 }); + const testAction = orderSense({ src: testRef, destOrder: 1 }); const expectedWords = testTreeWords(); expectedWords[wordId].sensesGuids = {