Added unit tests for CategorySelector and Change components (#5118)

This commit is contained in:
Matiss Janis Aboltins
2025-06-14 15:54:13 +01:00
committed by GitHub
parent 6509e80061
commit 7c9f3f241d
9 changed files with 304 additions and 1 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 <workspaceNameFromPackageJson> run test <pathToTest>`
Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better.

View File

@@ -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",

View File

@@ -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(<CategorySelector {...defaultProps} />);
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(
<CategorySelector
{...defaultProps}
setSelectedCategories={setSelectedCategories}
/>,
);
await userEvent.click(screen.getByLabelText('Category 1'));
expect(setSelectedCategories).toHaveBeenCalled();
});
it('calls setSelectedCategories when a group is selected', async () => {
const setSelectedCategories = vi.fn();
render(
<CategorySelector
{...defaultProps}
setSelectedCategories={setSelectedCategories}
/>,
);
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(
<CategorySelector
{...defaultProps}
setSelectedCategories={setSelectedCategories}
/>,
);
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(
<CategorySelector
{...defaultProps}
selectedCategories={[cat1, cat2, cat3, cat4]}
setSelectedCategories={setSelectedCategories}
/>,
);
await userEvent.click(screen.getByRole('button', { name: 'Unselect All' }));
expect(setSelectedCategories).toHaveBeenCalledWith([]);
});
});

View File

@@ -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(<Change amount={12345} />);
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(<Change amount={0} />);
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(<Change amount={-9876} />);
const el = screen.getByText('-98.76');
expect(el).toBeInTheDocument();
expect(el).toHaveStyle(`color: ${theme.errorText}`);
});
it('merges custom style prop', () => {
render(<Change amount={1000} style={{ fontWeight: 'bold' }} />);
const el = screen.getByText('+10.00');
expect(el).toHaveStyle('font-weight: bold');
expect(el).toHaveStyle(`color: ${theme.noticeTextLight}`);
});
});

View File

@@ -1,3 +1,4 @@
import '@testing-library/jest-dom';
import { installPolyfills } from './polyfills';
import { resetMockStore } from './redux/mock';

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Create unit test for CategorySelector and Change components.

View File

@@ -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"