diff --git a/.cursor/rules/pull-request.mdc b/.cursor/rules/pull-request.mdc new file mode 100644 index 0000000000..29866f035b --- /dev/null +++ b/.cursor/rules/pull-request.mdc @@ -0,0 +1,24 @@ +--- +description: +globs: +alwaysApply: true +--- +Before pushing code changes or opening a pull request, follow these steps: + +1. Check if your branch already has a changelog file in the "upcoming-release-notes" folder. +2. If there is no changelog file for your branch: + a. Find the number of the most recent (highest-numbered) open issue or pull request on GitHub. + b. Increment that number by 1. Use this as the filename for your new changelog file. + c. Create a new markdown file in the "upcoming-release-notes" folder with the following format: + +``` +--- +category: Features OR Maintenance OR Enhancements OR Bugfix +authors: [$GithubUsername] +--- + +$Description +``` + +3. Commit the new changelog file. +4. Proceed with your push or pull request. \ No newline at end of file diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc new file mode 100644 index 0000000000..ff9b6284ec --- /dev/null +++ b/.cursor/rules/typescript.mdc @@ -0,0 +1,32 @@ +--- +description: +globs: *.ts,*.tsx +alwaysApply: false +--- + +You are an expert in TypeScript and React. + +Code Style and Structure + +- Write concise, technical TypeScript code. +- Use functional and declarative programming patterns; avoid classes. +- Prefer iteration and modularization over code duplication. +- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError). +- Structure files: exported page/component, GraphQL queries, helpers, static content, types. + +Naming Conventions + +- Favor named exports for components and utilities. + +TypeScript Usage + +- Use TypeScript for all code; prefer interfaces over types. +- Avoid enums; use objects or maps instead. +- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead. +- Avoid type assertions with `as` or `!`; prefer using `satisfies`. + +Syntax and Formatting + +- Use the "function" keyword for pure functions. +- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. +- Use declarative JSX, keeping JSX minimal and readable. diff --git a/.cursor/rules/unit-tests.mdc b/.cursor/rules/unit-tests.mdc new file mode 100644 index 0000000000..5dbe5044e6 --- /dev/null +++ b/.cursor/rules/unit-tests.mdc @@ -0,0 +1,14 @@ +--- +description: +globs: +alwaysApply: true +--- +Vitest test runner is used for unit tests. + +When running unit tests, always include the flag `--watch=false` to prevent watch mode. + +To run unit tests for a specific package in the monorepo, use the following command: + +`yarn workspace run test ` + +Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better. \ No newline at end of file diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 2c2fa90e8d..547d576490 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -16,6 +16,7 @@ "@swc/helpers": "^0.5.17", "@swc/plugin-react-remove-properties": "^1.5.121", "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/debounce": "^1.2.4", diff --git a/packages/desktop-client/src/components/reports/CategorySelector.test.tsx b/packages/desktop-client/src/components/reports/CategorySelector.test.tsx new file mode 100644 index 0000000000..445dcf5aef --- /dev/null +++ b/packages/desktop-client/src/components/reports/CategorySelector.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import type { + CategoryEntity, + CategoryGroupEntity, +} from 'loot-core/types/models'; + +import { CategorySelector } from './CategorySelector'; + +function makeCategory({ + id, + name, + hidden = false, + group = '', +}: { + id: string; + name: string; + hidden?: boolean; + group?: string; +}): CategoryEntity { + return { id, name, hidden, group } satisfies CategoryEntity; +} + +function makeCategoryGroup({ + id, + name, + categories, +}: { + id: string; + name: string; + categories: CategoryEntity[]; +}): CategoryGroupEntity { + return { id, name, categories } satisfies CategoryGroupEntity; +} + +const cat1 = makeCategory({ id: 'cat1', name: 'Category 1' }); +const cat2 = makeCategory({ id: 'cat2', name: 'Category 2' }); +const cat3 = makeCategory({ id: 'cat3', name: 'Category 3', hidden: true }); +const group1 = makeCategoryGroup({ + id: 'group1', + name: 'Group 1', + categories: [cat1, cat2, cat3], +}); +const cat4 = makeCategory({ id: 'cat4', name: 'Category 4' }); +const group2 = makeCategoryGroup({ + id: 'group2', + name: 'Group 2', + categories: [cat4], +}); +const categoryGroups = [group1, group2]; + +const defaultProps = { + categoryGroups, + selectedCategories: [], + setSelectedCategories: vi.fn(), + showHiddenCategories: true, +}; + +describe('CategorySelector', () => { + it('renders category group and category checkboxes', () => { + render(); + expect(screen.getByLabelText('Group 1')).toBeInTheDocument(); + expect(screen.getByLabelText('Category 1')).toBeInTheDocument(); + expect(screen.getByLabelText('Category 2')).toBeInTheDocument(); + expect(screen.getByLabelText('Category 3')).toBeInTheDocument(); + expect(screen.getByLabelText('Group 2')).toBeInTheDocument(); + expect(screen.getByLabelText('Category 4')).toBeInTheDocument(); + }); + + it('calls setSelectedCategories when a category is selected', async () => { + const setSelectedCategories = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByLabelText('Category 1')); + expect(setSelectedCategories).toHaveBeenCalled(); + }); + + it('calls setSelectedCategories when a group is selected', async () => { + const setSelectedCategories = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByLabelText('Group 1')); + expect(setSelectedCategories).toHaveBeenCalled(); + }); + + it('selects all categories when Select All is clicked', async () => { + const setSelectedCategories = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'Select All' })); + expect(setSelectedCategories).toHaveBeenCalledWith([ + cat1, + cat2, + cat3, + cat4, + ]); + }); + + it('unselects all categories when Unselect All is clicked', async () => { + const setSelectedCategories = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'Unselect All' })); + expect(setSelectedCategories).toHaveBeenCalledWith([]); + }); +}); diff --git a/packages/desktop-client/src/components/reports/Change.test.tsx b/packages/desktop-client/src/components/reports/Change.test.tsx new file mode 100644 index 0000000000..1cd3767c45 --- /dev/null +++ b/packages/desktop-client/src/components/reports/Change.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { theme } from '@actual-app/components/theme'; +import { render, screen } from '@testing-library/react'; + +import { Change } from './Change'; + +describe('Change', () => { + it('renders a positive amount with a plus sign and green color', () => { + render(); + const el = screen.getByText('+123.45'); + expect(el).toBeInTheDocument(); + expect(el).toHaveStyle(`color: ${theme.noticeTextLight}`); + }); + + it('renders zero with a plus sign and green color', () => { + render(); + const el = screen.getByText('+0.00'); + expect(el).toBeInTheDocument(); + expect(el).toHaveStyle(`color: ${theme.noticeTextLight}`); + }); + + it('renders a negative amount with a minus sign and red color', () => { + render(); + const el = screen.getByText('-98.76'); + expect(el).toBeInTheDocument(); + expect(el).toHaveStyle(`color: ${theme.errorText}`); + }); + + it('merges custom style prop', () => { + render(); + const el = screen.getByText('+10.00'); + expect(el).toHaveStyle('font-weight: bold'); + expect(el).toHaveStyle(`color: ${theme.noticeTextLight}`); + }); +}); diff --git a/packages/desktop-client/src/setupTests.js b/packages/desktop-client/src/setupTests.js index b0809ccaf4..dc1e746177 100644 --- a/packages/desktop-client/src/setupTests.js +++ b/packages/desktop-client/src/setupTests.js @@ -1,3 +1,4 @@ +import '@testing-library/jest-dom'; import { installPolyfills } from './polyfills'; import { resetMockStore } from './redux/mock'; diff --git a/upcoming-release-notes/5118.md b/upcoming-release-notes/5118.md new file mode 100644 index 0000000000..af5d8ae2a2 --- /dev/null +++ b/upcoming-release-notes/5118.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Create unit test for CategorySelector and Change components. diff --git a/yarn.lock b/yarn.lock index 5b147987f2..3d2e0ebb63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -136,6 +136,7 @@ __metadata: "@swc/helpers": "npm:^0.5.17" "@swc/plugin-react-remove-properties": "npm:^1.5.121" "@testing-library/dom": "npm:10.4.0" + "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" "@types/debounce": "npm:^1.2.4" @@ -208,6 +209,13 @@ __metadata: languageName: unknown linkType: soft +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.3 + resolution: "@adobe/css-tools@npm:4.4.3" + checksum: 10/701379c514b7a43ca6681705a93cd57ad79565cfef9591122e9499897550cf324a5e5bb1bc51df0e7433cf0e91b962c90f18ac459dcc98b2431daa04aa63cb20 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -6089,6 +6097,21 @@ __metadata: languageName: node linkType: hard +"@testing-library/jest-dom@npm:^6.6.3": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" + dependencies: + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + lodash: "npm:^4.17.21" + redent: "npm:^3.0.0" + checksum: 10/1f3427e45870eab9dcc59d6504b780d4a595062fe1687762ae6e67d06a70bf439b40ab64cf58cbace6293a99e3764d4647fdc8300a633b721764f5ce39dade18 + languageName: node + linkType: hard + "@testing-library/react@npm:16.3.0": version: 16.3.0 resolution: "@testing-library/react@npm:16.3.0" @@ -7863,7 +7886,7 @@ __metadata: languageName: node linkType: hard -"aria-query@npm:^5.3.2": +"aria-query@npm:^5.0.0, aria-query@npm:^5.3.2": version: 5.3.2 resolution: "aria-query@npm:5.3.2" checksum: 10/b2fe9bc98bd401bc322ccb99717c1ae2aaf53ea0d468d6e7aebdc02fac736e4a99b46971ee05b783b08ade23c675b2d8b60e4a1222a95f6e27bc4d2a0bfdcc03 @@ -9439,6 +9462,13 @@ __metadata: languageName: node linkType: hard +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10/f6d38088d870a961794a2580b2b2af1027731bb43261cfdce14f19238a88664b351cc8978abc20f06cc6bbde725699dec8deb6fe9816b139fc3f2af28719e774 + languageName: node + linkType: hard + "csso@npm:^5.0.5": version: 5.0.5 resolution: "csso@npm:5.0.5" @@ -10038,6 +10068,13 @@ __metadata: languageName: node linkType: hard +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: 10/83d3371f8226487fbad36e160d44f1d9017fb26d46faba6a06fcad15f34633fc827b8c3e99d49f71d5f3253d866e2131826866fd0a3c86626f8eccfc361881ff + languageName: node + linkType: hard + "dom-helpers@npm:^5.0.1": version: 5.2.1 resolution: "dom-helpers@npm:5.2.1" @@ -15040,6 +15077,13 @@ __metadata: languageName: node linkType: hard +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 10/bfc6dd03c5eaf623a4963ebd94d087f6f4bbbfd8c41329a7f09706b0cb66969c4ddd336abeb587bc44bc6f08e13bf90f0b374f9d71f9f01e04adc2cd6f083ef1 + languageName: node + linkType: hard + "minimatch@npm:^10.0.3": version: 10.0.3 resolution: "minimatch@npm:10.0.3" @@ -17246,6 +17290,16 @@ __metadata: languageName: node linkType: hard +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: "npm:^4.0.0" + strip-indent: "npm:^3.0.0" + checksum: 10/fa1ef20404a2d399235e83cc80bd55a956642e37dd197b4b612ba7327bf87fa32745aeb4a1634b2bab25467164ab4ed9c15be2c307923dd08b0fe7c52431ae6b + languageName: node + linkType: hard + "redux-thunk@npm:^3.1.0": version: 3.1.0 resolution: "redux-thunk@npm:3.1.0" @@ -18797,6 +18851,15 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: "npm:^1.0.0" + checksum: 10/18f045d57d9d0d90cd16f72b2313d6364fd2cb4bf85b9f593523ad431c8720011a4d5f08b6591c9d580f446e78855c5334a30fb91aa1560f5d9f95ed1b4a0530 + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1"