mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 17:47:26 -05:00
Added unit tests for CategorySelector and Change components (#5118)
This commit is contained in:
committed by
GitHub
parent
6509e80061
commit
7c9f3f241d
24
.cursor/rules/pull-request.mdc
Normal file
24
.cursor/rules/pull-request.mdc
Normal 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.
|
||||
32
.cursor/rules/typescript.mdc
Normal file
32
.cursor/rules/typescript.mdc
Normal 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.
|
||||
14
.cursor/rules/unit-tests.mdc
Normal file
14
.cursor/rules/unit-tests.mdc
Normal 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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { installPolyfills } from './polyfills';
|
||||
import { resetMockStore } from './redux/mock';
|
||||
|
||||
|
||||
6
upcoming-release-notes/5118.md
Normal file
6
upcoming-release-notes/5118.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Create unit test for CategorySelector and Change components.
|
||||
65
yarn.lock
65
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"
|
||||
|
||||
Reference in New Issue
Block a user