Skip to content

Commit

Permalink
[MergeDragDrop] Add basic tests (#2792)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Nov 15, 2023
1 parent c3bd185 commit 642eae3
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 75 deletions.
42 changes: 18 additions & 24 deletions src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand All @@ -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 }));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> => {
await act(async () => {
testRenderer = create(
<Provider
store={configureMockStore()({ ...defaultState, mergeDuplicateGoal })}
>
<MergeDragDrop />
</Provider>
);
});
};

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");
});
});
4 changes: 2 additions & 2 deletions src/goals/MergeDuplicates/Redux/MergeDupsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ 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);
}
}

export function orderSense(payload: OrderSensePayload): PayloadAction {
if (payload.ref.order === undefined) {
if (payload.src.order === undefined) {
return orderSenseAction(payload);
} else {
return orderDuplicateAction(payload);
Expand Down
20 changes: 10 additions & 10 deletions src/goals/MergeDuplicates/Redux/MergeDupsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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<string[]> 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) {
Expand All @@ -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) => {
Expand Down
8 changes: 3 additions & 5 deletions src/goals/MergeDuplicates/Redux/MergeDupsReduxTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 642eae3

Please sign in to comment.