diff --git a/Backend/Models/MergeWordSet.cs b/Backend/Models/MergeWordSet.cs index f88dcfb594..48a962278c 100644 --- a/Backend/Models/MergeWordSet.cs +++ b/Backend/Models/MergeWordSet.cs @@ -27,23 +27,18 @@ public MergeWordSet() Id = ""; ProjectId = ""; UserId = ""; - WordIds = new List(); + WordIds = new(); } public MergeWordSet Clone() { - var clone = new MergeWordSet + return new() { Id = Id, ProjectId = ProjectId, UserId = UserId, - WordIds = new List() + WordIds = WordIds.Select(id => id).ToList() }; - foreach (var id in WordIds) - { - clone.WordIds.Add(id); - } - return clone; } public bool ContentEquals(MergeWordSet other) diff --git a/Backend/Models/MergeWords.cs b/Backend/Models/MergeWords.cs index 2490938770..a3cd3e4a67 100644 --- a/Backend/Models/MergeWords.cs +++ b/Backend/Models/MergeWords.cs @@ -22,8 +22,8 @@ public class MergeWords public MergeWords() { - Parent = new Word(); - Children = new List(); + Parent = new(); + Children = new(); DeleteOnly = false; } } @@ -59,8 +59,8 @@ public class MergeUndoIds public MergeUndoIds() { - ParentIds = new List(); - ChildIds = new List(); + ParentIds = new(); + ChildIds = new(); } public MergeUndoIds(List parentIds, List childIds) @@ -71,23 +71,11 @@ public MergeUndoIds(List parentIds, List childIds) public MergeUndoIds Clone() { - var clone = new MergeUndoIds + return new() { - ParentIds = new List(), - ChildIds = new List() + ParentIds = ParentIds.Select(id => id).ToList(), + ChildIds = ChildIds.Select(id => id).ToList() }; - - foreach (var id in ParentIds) - { - clone.ParentIds.Add(id); - } - - foreach (var id in ChildIds) - { - clone.ChildIds.Add(id); - } - - return clone; } public bool ContentEquals(MergeUndoIds other) diff --git a/Backend/Models/Project.cs b/Backend/Models/Project.cs index ccfe2c4463..3725c8b390 100644 --- a/Backend/Models/Project.cs +++ b/Backend/Models/Project.cs @@ -92,22 +92,22 @@ public Project() DefinitionsEnabled = false; GrammaticalInfoEnabled = false; AutocompleteSetting = AutocompleteSetting.On; - SemDomWritingSystem = new WritingSystem(); - VernacularWritingSystem = new WritingSystem(); - AnalysisWritingSystems = new List(); - SemanticDomains = new List(); - ValidCharacters = new List(); - RejectedCharacters = new List(); - CustomFields = new List(); - WordFields = new List(); - PartsOfSpeech = new List(); - InviteTokens = new List(); - WorkshopSchedule = new List(); + SemDomWritingSystem = new(); + VernacularWritingSystem = new(); + AnalysisWritingSystems = new(); + SemanticDomains = new(); + ValidCharacters = new(); + RejectedCharacters = new(); + CustomFields = new(); + WordFields = new(); + PartsOfSpeech = new(); + InviteTokens = new(); + WorkshopSchedule = new(); } public Project Clone() { - var clone = new Project + return new() { Id = Id, Name = Name, @@ -118,55 +118,16 @@ public Project Clone() AutocompleteSetting = AutocompleteSetting, SemDomWritingSystem = SemDomWritingSystem.Clone(), VernacularWritingSystem = VernacularWritingSystem.Clone(), - AnalysisWritingSystems = new List(), - SemanticDomains = new List(), - ValidCharacters = new List(), - RejectedCharacters = new List(), - CustomFields = new List(), - WordFields = new List(), - PartsOfSpeech = new List(), - InviteTokens = new List(), - WorkshopSchedule = new List(), + AnalysisWritingSystems = AnalysisWritingSystems.Select(ws => ws.Clone()).ToList(), + SemanticDomains = SemanticDomains.Select(sd => sd.Clone()).ToList(), + ValidCharacters = ValidCharacters.Select(vc => vc).ToList(), + RejectedCharacters = RejectedCharacters.Select(rc => rc).ToList(), + CustomFields = CustomFields.Select(cf => cf.Clone()).ToList(), + WordFields = WordFields.Select(wf => wf).ToList(), + PartsOfSpeech = PartsOfSpeech.Select(ps => ps).ToList(), + InviteTokens = InviteTokens.Select(it => it.Clone()).ToList(), + WorkshopSchedule = WorkshopSchedule.Select(dt => dt).ToList(), }; - - foreach (var aw in AnalysisWritingSystems) - { - clone.AnalysisWritingSystems.Add(aw.Clone()); - } - foreach (var sd in SemanticDomains) - { - clone.SemanticDomains.Add(sd.Clone()); - } - foreach (var cs in ValidCharacters) - { - clone.ValidCharacters.Add(cs); - } - foreach (var cs in RejectedCharacters) - { - clone.RejectedCharacters.Add(cs); - } - foreach (var cf in CustomFields) - { - clone.CustomFields.Add(cf.Clone()); - } - foreach (var wf in WordFields) - { - clone.WordFields.Add(wf); - } - foreach (var pos in PartsOfSpeech) - { - clone.PartsOfSpeech.Add(pos); - } - foreach (var it in InviteTokens) - { - clone.InviteTokens.Add(it.Clone()); - } - foreach (var dt in WorkshopSchedule) - { - clone.WorkshopSchedule.Add(dt); - } - - return clone; } public bool ContentEquals(Project other) @@ -362,8 +323,8 @@ public class UserCreatedProject public UserCreatedProject() { - Project = new Project(); - User = new User(); + Project = new(); + User = new(); } } diff --git a/Backend/Models/SemanticDomain.cs b/Backend/Models/SemanticDomain.cs index fc076dae2f..f359a995f7 100644 --- a/Backend/Models/SemanticDomain.cs +++ b/Backend/Models/SemanticDomain.cs @@ -94,7 +94,7 @@ public SemanticDomainFull() Name = ""; Id = ""; Description = ""; - Questions = new List(); + Questions = new(); Lang = ""; } @@ -102,13 +102,7 @@ public SemanticDomainFull() { var clone = (SemanticDomainFull)base.Clone(); clone.Description = Description; - clone.Questions = new List(); - - foreach (var question in Questions) - { - clone.Questions.Add(question); - } - + clone.Questions = Questions.Select(q => q).ToList(); return clone; } @@ -173,7 +167,7 @@ public SemanticDomainTreeNode(SemanticDomain sd) Lang = sd.Lang; Name = sd.Name; Id = sd.Id; - Children = new List(); + Children = new(); } } } diff --git a/Backend/Models/Sense.cs b/Backend/Models/Sense.cs index 9f18baad47..4f9e09991d 100644 --- a/Backend/Models/Sense.cs +++ b/Backend/Models/Sense.cs @@ -49,44 +49,25 @@ public Sense() // By default generate a new, unique Guid for each new Sense. Guid = Guid.NewGuid(); Accessibility = Status.Active; - GrammaticalInfo = new GrammaticalInfo(); - Definitions = new List(); - Glosses = new List(); - ProtectReasons = new List(); - SemanticDomains = new List(); + GrammaticalInfo = new(); + Definitions = new(); + Glosses = new(); + ProtectReasons = new(); + SemanticDomains = new(); } public Sense Clone() { - var clone = new Sense + return new() { Guid = Guid, Accessibility = Accessibility, GrammaticalInfo = GrammaticalInfo.Clone(), - Definitions = new List(), - Glosses = new List(), - ProtectReasons = new List(), - SemanticDomains = new List(), + Definitions = Definitions.Select(d => d.Clone()).ToList(), + Glosses = Glosses.Select(g => g.Clone()).ToList(), + ProtectReasons = ProtectReasons.Select(pr => pr.Clone()).ToList(), + SemanticDomains = SemanticDomains.Select(sd => sd.Clone()).ToList(), }; - - foreach (var definition in Definitions) - { - clone.Definitions.Add(definition.Clone()); - } - foreach (var gloss in Glosses) - { - clone.Glosses.Add(gloss.Clone()); - } - foreach (var reason in ProtectReasons) - { - clone.ProtectReasons.Add(reason.Clone()); - } - foreach (var sd in SemanticDomains) - { - clone.SemanticDomains.Add(sd.Clone()); - } - - return clone; } public override bool Equals(object? obj) diff --git a/Backend/Models/Statistics.cs b/Backend/Models/Statistics.cs index ef1d153090..65dc326b1a 100644 --- a/Backend/Models/Statistics.cs +++ b/Backend/Models/Statistics.cs @@ -27,7 +27,7 @@ public SemanticDomainUserCount() { Id = ""; Username = ""; - DomainSet = new HashSet(); + DomainSet = new(); DomainCount = 0; WordCount = 0; } @@ -90,8 +90,8 @@ public class ChartRootData public ChartRootData() { - Dates = new List(); - Datasets = new List(); + Dates = new(); + Datasets = new(); } } @@ -107,7 +107,7 @@ public class Dataset public Dataset(string userName, int data) { UserName = userName; - Data = new List() { data }; + Data = new() { data }; } } diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index 26ec30be15..85bbdd6de7 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -96,13 +96,13 @@ public User() UILang = ""; Token = ""; IsAdmin = false; - WorkedProjects = new Dictionary(); - ProjectRoles = new Dictionary(); + WorkedProjects = new(); + ProjectRoles = new(); } public User Clone() { - var clone = new User + return new() { Id = Id, Avatar = Avatar, @@ -117,21 +117,9 @@ public User Clone() UILang = UILang, Token = Token, IsAdmin = IsAdmin, - WorkedProjects = new Dictionary(), - ProjectRoles = new Dictionary() + WorkedProjects = WorkedProjects.ToDictionary(kv => kv.Key, kv => kv.Value), + ProjectRoles = ProjectRoles.ToDictionary(kv => kv.Key, kv => kv.Value), }; - - foreach (var projId in WorkedProjects.Keys) - { - clone.WorkedProjects.Add(projId, WorkedProjects[projId]); - } - - foreach (var projId in ProjectRoles.Keys) - { - clone.ProjectRoles.Add(projId, ProjectRoles[projId]); - } - - return clone; } public bool ContentEquals(User other) diff --git a/Backend/Models/UserEdit.cs b/Backend/Models/UserEdit.cs index 7d683e740e..3f1c51263f 100644 --- a/Backend/Models/UserEdit.cs +++ b/Backend/Models/UserEdit.cs @@ -27,24 +27,17 @@ public UserEdit() { Id = ""; ProjectId = ""; - Edits = new List(); + Edits = new(); } public UserEdit Clone() { - var clone = new UserEdit + return new() { Id = Id, ProjectId = ProjectId, - Edits = new List() + Edits = Edits.Select(e => e.Clone()).ToList() }; - - foreach (var edit in Edits) - { - clone.Edits.Add(edit.Clone()); - } - - return clone; } public bool ContentEquals(UserEdit other) @@ -133,26 +126,19 @@ public Edit() { Guid = Guid.NewGuid(); GoalType = 0; - StepData = new List(); + StepData = new(); Changes = "{}"; } public Edit Clone() { - var clone = new Edit + return new() { Guid = Guid, GoalType = GoalType, - StepData = new List(), + StepData = StepData.Select(sd => sd).ToList(), Changes = Changes }; - - foreach (var step in StepData) - { - clone.StepData.Add(step); - } - - return clone; } public override bool Equals(object? obj) diff --git a/Backend/Models/Word.cs b/Backend/Models/Word.cs index 2a5e03e917..3266fbbc68 100644 --- a/Backend/Models/Word.cs +++ b/Backend/Models/Word.cs @@ -94,18 +94,18 @@ public Word() OtherField = ""; ProjectId = ""; Accessibility = Status.Active; - Audio = new List(); - EditedBy = new List(); - History = new List(); - ProtectReasons = new List(); - Senses = new List(); - Note = new Note(); - Flag = new Flag(); + Audio = new(); + EditedBy = new(); + History = new(); + ProtectReasons = new(); + Senses = new(); + Note = new(); + Flag = new(); } public Word Clone() { - var clone = new Word + return new() { Id = Id, Guid = Guid, @@ -116,37 +116,14 @@ public Word Clone() OtherField = OtherField, ProjectId = ProjectId, Accessibility = Accessibility, - Audio = new List(), - EditedBy = new List(), - History = new List(), - ProtectReasons = new List(), - Senses = new List(), + Audio = Audio.Select(p => p.Clone()).ToList(), + EditedBy = EditedBy.Select(id => id).ToList(), + History = History.Select(id => id).ToList(), + ProtectReasons = ProtectReasons.Select(pr => pr.Clone()).ToList(), + Senses = Senses.Select(s => s.Clone()).ToList(), Note = Note.Clone(), Flag = Flag.Clone(), }; - - foreach (var audio in Audio) - { - clone.Audio.Add(audio.Clone()); - } - foreach (var id in EditedBy) - { - clone.EditedBy.Add(id); - } - foreach (var id in History) - { - clone.History.Add(id); - } - foreach (var reason in ProtectReasons) - { - clone.ProtectReasons.Add(reason.Clone()); - } - foreach (var sense in Senses) - { - clone.Senses.Add(sense.Clone()); - } - - return clone; } public bool ContentEquals(Word other) diff --git a/src/components/GoalTimeline/tests/GoalRedux.test.tsx b/src/components/GoalTimeline/tests/GoalRedux.test.tsx index 906e4b1438..0a62d9bde8 100644 --- a/src/components/GoalTimeline/tests/GoalRedux.test.tsx +++ b/src/components/GoalTimeline/tests/GoalRedux.test.tsx @@ -33,7 +33,7 @@ import { GoalStatus, GoalType } from "types/goals"; import { Path } from "types/path"; import { newUser } from "types/user"; import * as goalUtilities from "utilities/goalUtilities"; -import { renderWithProviders } from "utilities/testUtilities"; +import { renderWithProviders } from "utilities/testingLibraryUtilities"; jest.mock("backend", () => ({ addGoalToUserEdit: (...args: any[]) => mockAddGoalToUserEdit(...args), diff --git a/src/components/ProjectScreen/tests/ChooseProject.test.tsx b/src/components/ProjectScreen/tests/ChooseProject.test.tsx index c65824877a..1c9a838d7f 100644 --- a/src/components/ProjectScreen/tests/ChooseProject.test.tsx +++ b/src/components/ProjectScreen/tests/ChooseProject.test.tsx @@ -3,9 +3,10 @@ import renderer from "react-test-renderer"; import "tests/reactI18nextMock"; -import { Project } from "api/models"; +import { type Project } from "api/models"; import ChooseProject from "components/ProjectScreen/ChooseProject"; import { newProject } from "types/project"; +import { testInstanceHasText } from "utilities/testRendererUtilities"; import { randomIntString } from "utilities/utilities"; jest.mock("backend", () => ({ @@ -27,13 +28,6 @@ const mockProj = (name: string): Project => ({ let testRenderer: renderer.ReactTestRenderer; -const hasText = (item: renderer.ReactTestInstance, text: string): boolean => { - const found = item.findAll( - (node) => node.children.length === 1 && node.children[0] === text - ); - return found.length !== 0; -}; - it("renders with projects in alphabetical order", async () => { const unordered = ["In the middle", "should be last", "alphabetically first"]; mockGetProjects.mockResolvedValue(unordered.map((name) => mockProj(name))); @@ -42,10 +36,10 @@ it("renders with projects in alphabetical order", async () => { }); const items = testRenderer.root.findAllByType(ListItemButton); expect(items).toHaveLength(unordered.length); - expect(hasText(items[0], unordered[0])).toBeFalsy; - expect(hasText(items[1], unordered[1])).toBeFalsy; - expect(hasText(items[2], unordered[2])).toBeFalsy; - expect(hasText(items[0], unordered[2])).toBeTruthy; - expect(hasText(items[1], unordered[0])).toBeTruthy; - expect(hasText(items[2], unordered[1])).toBeTruthy; + expect(testInstanceHasText(items[0], unordered[0])).toBeFalsy(); + expect(testInstanceHasText(items[1], unordered[1])).toBeFalsy(); + expect(testInstanceHasText(items[2], unordered[2])).toBeFalsy(); + expect(testInstanceHasText(items[0], unordered[2])).toBeTruthy(); + expect(testInstanceHasText(items[1], unordered[0])).toBeTruthy(); + expect(testInstanceHasText(items[2], unordered[1])).toBeTruthy(); }); diff --git a/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx index 0404a736a4..2ca7dc4820 100644 --- a/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx +++ b/src/goals/CharacterInventory/CharInv/CharacterDetail/tests/index.test.tsx @@ -1,10 +1,5 @@ import { Provider } from "react-redux"; -import { - ReactTestInstance, - ReactTestRenderer, - act, - create, -} from "react-test-renderer"; +import { type ReactTestRenderer, act, create } from "react-test-renderer"; import configureMockStore from "redux-mock-store"; import "tests/reactI18nextMock"; @@ -17,7 +12,8 @@ import { } from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace"; import CharacterReplaceDialog from "goals/CharacterInventory/CharInv/CharacterDetail/FindAndReplace/CharacterReplaceDialog"; import { defaultState } from "goals/CharacterInventory/Redux/CharacterInventoryReduxTypes"; -import { StoreState } from "types"; +import { type StoreState } from "types"; +import { testInstanceHasText } from "utilities/testRendererUtilities"; // Dialog uses portals, which are not supported in react-test-renderer. jest.mock("@mui/material", () => { @@ -66,13 +62,6 @@ async function renderCharacterDetail(): Promise { }); } -const hasText = (item: ReactTestInstance, text: string): boolean => { - const found = item.findAll( - (node) => node.children.length === 1 && node.children[0] === text - ); - return found.length !== 0; -}; - beforeEach(async () => { jest.resetAllMocks(); await renderCharacterDetail(); @@ -80,7 +69,7 @@ beforeEach(async () => { describe("CharacterDetail", () => { it("renders with example word", () => { - expect(hasText(charMaster.root, mockPrefix)).toBeTruthy(); + expect(testInstanceHasText(charMaster.root, mockPrefix)).toBeTruthy(); }); describe("FindAndReplace", () => { diff --git a/src/utilities/testRendererUtilities.tsx b/src/utilities/testRendererUtilities.tsx new file mode 100644 index 0000000000..7bdf639ca7 --- /dev/null +++ b/src/utilities/testRendererUtilities.tsx @@ -0,0 +1,13 @@ +import { type ReactTestInstance } from "react-test-renderer"; + +/** Checks if any node in the given `react-test-renderer` instance has the given text. */ +export function testInstanceHasText( + instance: ReactTestInstance, + text: string +): boolean { + return ( + instance.findAll( + (node) => node.children.length === 1 && node.children[0] === text + ).length > 0 + ); +} diff --git a/src/utilities/testUtilities.tsx b/src/utilities/testingLibraryUtilities.tsx similarity index 77% rename from src/utilities/testUtilities.tsx rename to src/utilities/testingLibraryUtilities.tsx index 0f7020dcec..3eb1d7f7aa 100644 --- a/src/utilities/testUtilities.tsx +++ b/src/utilities/testingLibraryUtilities.tsx @@ -7,18 +7,16 @@ import { PersistGate } from "redux-persist/integration/react"; import { defaultState } from "components/App/DefaultState"; import { type AppStore, type RootState, persistor, setupStore } from "store"; -// These test utilities are leveraged from the Redux documentation for Writing Tests: -// https://redux.js.org/usage/writing-tests -// Specifically, see the section on "Integration Testing Connected Components -// and Redux Logic" - -// This type interface extends the default options for render from RTL, as well -// as allows the user to specify other things such as initialState, store. +/** This extends the default options for `render` from `@testing-library/react`, + * allowing the user to specify other things such as `initialState`, `store`. */ interface ExtendedRenderOptions extends Omit { preloadedState?: PreloadedState; store?: AppStore; } +/** This test utility is leveraged from the Redux documentation for Writing Tests: + * https://redux.js.org/usage/writing-tests. Specifically, see the section on + * "Integration Testing Connected Components and Redux Logic" */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function renderWithProviders( ui: ReactElement,