Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ghosetuhin authored Dec 3, 2023
2 parents 5f0eea4 + 6bfd958 commit ff83de0
Show file tree
Hide file tree
Showing 20 changed files with 278 additions and 23 deletions.
1 change: 1 addition & 0 deletions packages/api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ app/bundle.api.js*
app/stats.json
migrations
default-db.sqlite
mocks/budgets/**/*
21 changes: 21 additions & 0 deletions packages/api/__snapshots__/methods.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`API setup and teardown successfully loads budget 1`] = `
Array [
"2016-10",
"2016-11",
"2016-12",
"2017-01",
"2017-02",
"2017-03",
"2017-04",
"2017-05",
"2017-06",
"2017-07",
"2017-08",
"2017-09",
"2017-10",
"2017-11",
"2017-12",
]
`;
2 changes: 0 additions & 2 deletions packages/api/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable import/no-unused-modules */

// eslint-disable-next-line import/extensions
import * as bundle from './app/bundle.api.js';
import * as injected from './injected';
Expand Down
24 changes: 24 additions & 0 deletions packages/api/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
moduleFileExtensions: [
'testing.js',
'testing.ts',
'api.js',
'api.ts',
'api.tsx',
'electron.js',
'electron.ts',
'mjs',
'js',
'ts',
'tsx',
'json',
],
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/'],
watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'],
setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'],
transformIgnorePatterns: ['/node_modules/'],
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
};
147 changes: 147 additions & 0 deletions packages/api/methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as fs from 'fs/promises';
import * as path from 'path';

import * as api from './index';

const budgetName = 'test-budget';

beforeEach(async () => {
// we need real datetime if we are going to mix new timestamps with our mock data
global.restoreDateNow();

const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
await fs.rm(budgetPath, { force: true, recursive: true });

await createTestBudget('default-budget-template', budgetName);
await api.init({
dataDir: path.join(__dirname, '/mocks/budgets/'),
});
});

afterEach(async () => {
global.currentMonth = null;
await api.shutdown();
});

async function createTestBudget(templateName: string, name: string) {
const templatePath = path.join(
__dirname,
'/../loot-core/src/mocks/files',
templateName,
);
const budgetPath = path.join(__dirname, '/mocks/budgets/', name);

await fs.mkdir(budgetPath);
await fs.copyFile(
path.join(templatePath, 'metadata.json'),
path.join(budgetPath, 'metadata.json'),
);
await fs.copyFile(
path.join(templatePath, 'db.sqlite'),
path.join(budgetPath, 'db.sqlite'),
);
}

describe('API setup and teardown', () => {
// apis: loadBudget, getBudgetMonths
test('successfully loads budget', async () => {
await expect(api.loadBudget(budgetName)).resolves.toBeUndefined();

await expect(api.getBudgetMonths()).resolves.toMatchSnapshot();
});
});

describe('API CRUD operations', () => {
beforeEach(async () => {
// load test budget
await api.loadBudget(budgetName);
});

// apis: setBudgetAmount, setBudgetCarryover, getBudgetMonth
test('Budgets: successfully update budgets', async () => {
const month = '2023-10';
global.currentMonth = month;

// create some new categories to test with
const groupId = await api.createCategoryGroup({
name: 'tests',
});
const categoryId = await api.createCategory({
name: 'test-budget',
group_id: groupId,
});

await api.setBudgetAmount(month, categoryId, 100);
await api.setBudgetCarryover(month, categoryId, true);

const budgetMonth = await api.getBudgetMonth(month);
expect(budgetMonth.categoryGroups).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: groupId,
categories: expect.arrayContaining([
expect.objectContaining({
id: categoryId,
budgeted: 100,
carryover: true,
}),
]),
}),
]),
);
});

//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount
test('Accounts: successfully complete account operators', async () => {
const accountId1 = await api.createAccount(
{ name: 'test-account1', offbudget: true },
1000,
);
const accountId2 = await api.createAccount({ name: 'test-account2' }, 0);
let accounts = await api.getAccounts();

// accounts successfully created
expect(accounts).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: accountId1,
name: 'test-account1',
offbudget: true,
}),
expect.objectContaining({ id: accountId2, name: 'test-account2' }),
]),
);

await api.updateAccount(accountId1, { offbudget: false });
await api.closeAccount(accountId1, accountId2, null);
await api.deleteAccount(accountId2);

// accounts successfully updated, and one of them deleted
accounts = await api.getAccounts();
expect(accounts).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: accountId1,
name: 'test-account1',
closed: true,
offbudget: false,
}),
expect.not.objectContaining({ id: accountId2 }),
]),
);

await api.reopenAccount(accountId1);

// the non-deleted account is reopened
accounts = await api.getAccounts();
expect(accounts).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: accountId1,
name: 'test-account1',
closed: false,
}),
]),
);
});
});
Empty file.
7 changes: 6 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"build:node": "tsc --p tsconfig.dist.json",
"build:migrations": "cp migrations/*.sql dist/migrations",
"build:default-db": "cp default-db.sqlite dist/",
"build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db"
"build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
"test": "yarn run build:app && jest -c jest.config.js"
},
"dependencies": {
"better-sqlite3": "^9.1.1",
Expand All @@ -25,7 +26,11 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@swc/core": "^1.3.82",
"@swc/jest": "^0.2.29",
"@types/jest": "^27.5.0",
"@types/uuid": "^9.0.2",
"jest": "^27.0.0",
"typescript": "^5.0.2"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import React, { type ComponentProps, memo, useState } from 'react';

import DotsHorizontalTriple from '../../icons/v1/DotsHorizontalTriple';
import { theme, styles } from '../../style';
Expand All @@ -10,12 +10,19 @@ import { Tooltip } from '../tooltips';
import RenderMonths from './RenderMonths';
import { getScrollbarWidth } from './util';

type BudgetTotalsProps = {
MonthComponent: ComponentProps<typeof RenderMonths>['component'];
toggleHiddenCategories: () => void;
expandAllCategories: () => void;
collapseAllCategories: () => void;
};

const BudgetTotals = memo(function BudgetTotals({
MonthComponent,
toggleHiddenCategories,
expandAllCategories,
collapseAllCategories,
}) {
}: BudgetTotalsProps) {
const [menuOpen, setMenuOpen] = useState(false);
return (
<View
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useState } from 'react';

import { type BoundActions } from '../../hooks/useActions';
import { styles, theme } from '../../style';
import { type CommonModalProps } from '../../types/modals';
import Block from '../common/Block';
import Button from '../common/Button';
import Modal from '../common/Modal';
import Text from '../common/Text';
import View from '../common/View';

function getErrorMessage(error) {
function getErrorMessage(error: 'not-ynab4' | boolean) {
switch (error) {
case 'not-ynab4':
return 'This file is not valid. Please select a .ynab4 file';
Expand All @@ -16,10 +18,15 @@ function getErrorMessage(error) {
}
}

function Import({ modalProps, actions }) {
type ImportProps = {
modalProps: CommonModalProps;
actions: BoundActions;
};

function Import({ modalProps, actions }: ImportProps) {
const [error] = useState(false);

function onSelectType(type) {
function onSelectType(type: 'ynab4' | 'ynab5' | 'actual') {
switch (type) {
case 'ynab4':
actions.pushModal('import-ynab4');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function WelcomeScreen() {
}}
>
<Button onClick={() => pushModal('import')}>Import my budget</Button>
<Button type="primary" onClick={createBudget}>
<Button type="primary" onClick={() => createBudget()}>
Start fresh
</Button>
</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React from 'react';
import React, { type ReactElement } from 'react';

import * as d from 'date-fns';

import { theme } from '../../style';
import Block from '../common/Block';

function DateRange({ start, end }) {
start = d.parseISO(start);
end = d.parseISO(end);
type DateRangeProps = {
start: string;
end: string;
};

let content;
if (start.getYear() !== end.getYear()) {
function DateRange({
start: startProp,
end: endProp,
}: DateRangeProps): ReactElement {
const start = d.parseISO(startProp);
const end = d.parseISO(endProp);

let content: string | ReactElement;
if (start.getFullYear() !== end.getFullYear()) {
content = (
<div>
{d.format(start, 'MMM yyyy')} - {d.format(end, 'MMM yyyy')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, type SetStateAction } from 'react';

import { useSpreadsheet } from 'loot-core/src/client/SpreadsheetProvider';

function useReport(sheetName, getData) {
function useReport(
sheetName: string,
getData: (
spreadsheet: ReturnType<typeof useSpreadsheet>,
setData: (results: unknown) => SetStateAction<unknown>,
) => Promise<void>,
) {
const spreadsheet = useSpreadsheet();
const [results, setResults] = useState(null);

Expand All @@ -15,7 +21,6 @@ function useReport(sheetName, getData) {
cleanup?.();
};
}, [getData]);

return results;
}

Expand Down
5 changes: 1 addition & 4 deletions packages/loot-core/src/client/actions/budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,7 @@ export function closeBudgetUI() {
};
}

export function deleteBudget(
id: string | undefined,
cloudFileId: string | undefined,
) {
export function deleteBudget(id?: string, cloudFileId?: string) {
return async (dispatch: Dispatch) => {
await send('delete-budget', { id, cloudFileId });
await dispatch(loadAllFiles());
Expand Down
12 changes: 11 additions & 1 deletion packages/loot-core/src/client/state-types/modals.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type File } from '../../types/file';
import type { AccountEntity, GoCardlessToken } from '../../types/models';
import type { RuleEntity } from '../../types/models/rule';
import type { EmptyObject, StripNever } from '../../types/util';
import type * as constants from '../constants';

export type ModalType = keyof FinanceModals;

export type OptionlessModal = {
Expand Down Expand Up @@ -70,6 +70,16 @@ type FinanceModals = {
onSuccess: (data: GoCardlessToken) => Promise<void>;
};

'delete-budget': { file: File };

import: null;

'import-ynab4': null;

'import-ynab5': null;

'import-actual': null;

'create-encryption-key': { recreate?: boolean };
'fix-encryption-key': {
hasExistingKey?: boolean;
Expand Down
Loading

0 comments on commit ff83de0

Please sign in to comment.