Compare commits

..

1 Commits

Author SHA1 Message Date
Neil
7d6b1508d6 Revert "Fix wrong month on spending card (#3295)"
This reverts commit 6d84b0e371.
2024-08-20 22:48:26 +01:00
1058 changed files with 23510 additions and 41707 deletions

27
.eslintignore Normal file
View File

@@ -0,0 +1,27 @@
packages/api/app/bundle.api.js
packages/api/dist
packages/api/@types
packages/api/migrations
packages/crdt/dist
packages/desktop-client/bundle.browser.js
packages/desktop-client/build/
packages/desktop-client/build-stats/
packages/desktop-client/public/kcab/
packages/desktop-client/public/data/
packages/desktop-client/**/node_modules/*
packages/desktop-client/node_modules/
packages/desktop-client/src/icons/**/*
packages/desktop-client/test-results/
packages/desktop-electron/client-build/
packages/desktop-electron/dist/
packages/import-ynab4/**/node_modules/*
packages/import-ynab5/**/node_modules/*
packages/loot-core/**/node_modules/*
packages/loot-core/**/lib-dist/*
packages/loot-core/**/proto/*

603
.eslintrc.js Normal file
View File

@@ -0,0 +1,603 @@
const path = require('path');
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = path.join(
__dirname,
'packages',
'eslint-plugin-actual',
'lib',
'rules',
);
const ruleFCMsg =
'Type the props argument and let TS infer or use ComponentType for a component prop';
const restrictedImportPatterns = [
{
group: ['*.api', '*.web', '*.electron'],
message: 'Dont directly reference imports from other platforms',
},
{
group: ['uuid'],
importNames: ['*'],
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
},
];
const restrictedImportColors = [
{
group: ['**/style', '**/colors'],
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
];
module.exports = {
root: true,
env: {
browser: true,
commonjs: true,
es6: true,
jest: true,
node: true,
},
plugins: [
'prettier',
'import',
'rulesdir',
'@typescript-eslint',
'jsx-a11y',
'react-hooks',
],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/typescript',
],
parser: '@typescript-eslint/parser',
parserOptions: { project: [path.join(__dirname, './tsconfig.json')] },
reportUnusedDisableDirectives: true,
globals: {
globalThis: false,
vi: true,
},
rules: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'default-case': ['warn', { commentPattern: '^no default$' }],
'dot-location': ['warn', 'property'],
eqeqeq: ['warn', 'smart'],
'new-parens': 'warn',
'no-array-constructor': 'warn',
'no-caller': 'warn',
'no-cond-assign': ['warn', 'except-parens'],
'no-const-assign': 'warn',
'no-control-regex': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-eval': 'warn',
'no-ex-assign': 'warn',
'no-extend-native': 'warn',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-fallthrough': 'warn',
'no-func-assign': 'warn',
'no-implied-eval': 'warn',
'no-invalid-regexp': 'warn',
'no-iterator': 'warn',
'no-label-var': 'warn',
'no-labels': ['warn', { allowLoop: true, allowSwitch: false }],
'no-lone-blocks': 'warn',
'no-mixed-operators': [
'warn',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: false,
},
],
'no-multi-str': 'warn',
'no-global-assign': 'warn',
'no-unsafe-negation': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-redeclare': 'warn',
'no-regex-spaces': 'warn',
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-shadow-restricted-names': 'warn',
'no-sparse-arrays': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-undef': 'error',
'no-unreachable': 'warn',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-unused-labels': 'warn',
'no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
},
],
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': [
'warn',
{
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false,
},
],
'no-with': 'warn',
'no-whitespace-before-property': 'warn',
'react-hooks/exhaustive-deps': 'warn',
'require-yield': 'warn',
'rest-spread-spacing': ['warn', 'never'],
strict: ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': 'warn',
'valid-typeof': 'warn',
'no-restricted-properties': [
'error',
{
object: 'require',
property: 'ensure',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
{
object: 'System',
property: 'import',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
],
'getter-return': 'warn',
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
'import/first': 'error',
'import/no-amd': 'error',
'import/no-anonymous-default-export': 'warn',
'import/no-webpack-loader-syntax': 'error',
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
'react/forbid-foreign-prop-types': ['warn', { allowInPropTypes: true }],
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-no-duplicate-props': 'warn',
'react/jsx-no-target-blank': 'warn',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': [
'warn',
{
allowAllCaps: true,
ignore: [],
},
],
'react/no-danger-with-children': 'warn',
// Disabled because of undesirable warnings
// See https://github.com/facebook/create-react-app/issues/5204 for
// blockers until its re-enabled
// 'react/no-deprecated': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-typos': 'error',
'react/require-render-return': 'error',
'react/style-prop-object': 'warn',
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{
aspects: ['noHref', 'invalidHref'],
},
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': ['warn', { ignoreNonDOM: true }],
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'warn',
'jsx-a11y/scope': 'warn',
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
'react-hooks/rules-of-hooks': 'error',
'prettier/prettier': 'warn',
// Note: base rule explicitly disabled in favor of the TS one
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
curly: ['warn', 'multi-line', 'consistent'],
'no-restricted-globals': ['warn'].concat(
require('confusing-browser-globals').filter(g => g !== 'self'),
),
'react/jsx-filename-extension': [
'warn',
{ extensions: ['.jsx', '.tsx'], allow: 'as-needed' },
],
'react/jsx-no-useless-fragment': 'warn',
'react/self-closing-comp': 'warn',
'react/no-unstable-nested-components': [
'warn',
{ allowAsProps: true, customValidators: ['formatter'] },
],
'rulesdir/typography': 'warn',
'rulesdir/prefer-if-statement': 'warn',
// https://github.com/eslint/eslint/issues/16954
// https://github.com/eslint/eslint/issues/16953
'no-loop-func': 'off',
// Do don't need this as we're using TypeScript
'react/prop-types': 'off',
// TODO: re-enable these rules
'react/react-in-jsx-scope': 'off',
'no-var': 'warn',
'react/jsx-curly-brace-presence': 'warn',
'object-shorthand': ['warn', 'properties'],
'import/extensions': [
'warn',
'never',
{
json: 'always',
},
],
'import/no-useless-path-segments': 'warn',
'import/no-duplicates': ['warn', { 'prefer-inline': true }],
'import/no-unused-modules': ['warn', { unusedExports: true }],
'import/order': [
'warn',
{
alphabetize: {
caseInsensitive: true,
order: 'asc',
},
groups: [
'builtin', // Built-in types are first
'external',
'parent',
'sibling',
'index', // Then the index file
],
'newlines-between': 'always',
pathGroups: [
// Enforce that React (and react-related packages) is the first import
{ group: 'builtin', pattern: 'react?(-*)', position: 'before' },
// Separate imports from Actual from "real" external imports
{
group: 'external',
pattern: 'loot-{core,design}/**/*',
position: 'after',
},
],
pathGroupsExcludedImportTypes: ['react'],
},
],
'no-restricted-syntax': [
'warn',
{
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
selector:
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
message:
'Using default React import is discouraged, please use named exports directly instead.',
},
{
// forbid <a> in favor of <Link>
selector: 'JSXOpeningElement[name.name="a"]',
message: 'Using <a> is discouraged, please use <Link> instead.',
},
],
'no-restricted-imports': [
'warn',
{ patterns: [...restrictedImportPatterns, ...restrictedImportColors] },
],
'@typescript-eslint/ban-ts-comment': [
'error',
{ 'ts-ignore': 'allow-with-description' },
],
// Rules disable during TS migration
'@typescript-eslint/no-var-requires': 'off',
'prefer-const': 'warn',
'prefer-spread': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-require-imports': 'off',
'import/no-default-export': 'warn',
},
overrides: [
{
files: ['**/*.ts?(x)'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
// typescript-eslint specific options
warnOnUnsupportedTypeScriptVersion: true,
},
plugins: ['@typescript-eslint'],
// If adding a typescript-eslint version of an existing ESLint rule,
// make sure to disable the ESLint rule here.
rules: {
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
'default-case': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
'no-dupe-class-members': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
'no-undef': 'off',
// Add TypeScript specific rules (and turn off ESLint equivalents)
'@typescript-eslint/consistent-type-assertions': 'warn',
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'warn',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'warn',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
typedefs: false,
},
],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'warn',
},
},
{
files: ['.eslintrc.js', './**/.eslintrc.js'],
parserOptions: { project: null },
rules: {
'@typescript-eslint/consistent-type-exports': 'off',
},
},
{
files: [
'./packages/desktop-client/**/*.{ts,tsx}',
'./packages/loot-core/src/client/**/*.{ts,tsx}',
],
rules: {
// enforce type over interface
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
// enforce import type
'@typescript-eslint/consistent-type-imports': [
'warn',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/no-restricted-types': [
'warn',
{
types: {
// forbid FC as superflous
FunctionComponent: { message: ruleFCMsg },
FC: { message: ruleFCMsg },
},
},
],
},
},
{
files: ['./packages/desktop-client/**/*'],
excludedFiles: [
'./packages/desktop-client/src/hooks/useNavigate.{ts,tsx}',
],
rules: {
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['react-router-dom'],
importNames: ['useNavigate'],
message: 'Please use Actuals useNavigate() hook instead.',
},
],
},
],
},
},
{
files: ['./packages/loot-core/src/**/*'],
rules: {
'no-restricted-imports': [
'warn',
{
patterns: [
...restrictedImportPatterns,
{
group: ['loot-core/**'],
message:
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
},
],
},
],
},
},
{
files: [
'packages/loot-core/src/types/**/*',
'packages/loot-core/src/client/state-types/**/*',
'**/icons/**/*',
'**/{mocks,__mocks__}/**/*',
// can't correctly resolve usages
'**/*.{testing,electron,browser,web,api}.ts',
],
rules: { 'import/no-unused-modules': 'off' },
},
{
files: [
'./packages/desktop-client/src/style/index.*',
'./packages/desktop-client/src/style/palette.*',
],
rules: {
'no-restricted-imports': ['off', { patterns: restrictedImportColors }],
},
},
{
files: [
'./packages/api/migrations/*',
'./packages/loot-core/migrations/*',
],
rules: {
'import/no-default-export': 'off',
},
},
{
// TODO: fix the issues in these files
files: [
'./packages/desktop-client/src/components/accounts/Account.jsx',
'./packages/desktop-client/src/components/accounts/MobileAccount.jsx',
'./packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
'./packages/desktop-client/src/components/App.tsx',
'./packages/desktop-client/src/components/budget/BudgetCategories.jsx',
'./packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
'./packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
'./packages/desktop-client/src/components/budget/index.tsx',
'./packages/desktop-client/src/components/budget/MobileBudget.tsx',
'./packages/desktop-client/src/components/budget/rollover/HoldMenu.tsx',
'./packages/desktop-client/src/components/budget/rollover/TransferMenu.tsx',
'./packages/desktop-client/src/components/common/Menu.tsx',
'./packages/desktop-client/src/components/FinancesApp.tsx',
'./packages/desktop-client/src/components/GlobalKeys.ts',
'./packages/desktop-client/src/components/LoggedInUser.tsx',
'./packages/desktop-client/src/components/manager/ManagementApp.jsx',
'./packages/desktop-client/src/components/manager/subscribe/common.tsx',
'./packages/desktop-client/src/components/ManageRules.tsx',
'./packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
'./packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
'./packages/desktop-client/src/components/Modals.tsx',
'./packages/desktop-client/src/components/modals/EditRule.jsx',
'./packages/desktop-client/src/components/modals/ImportTransactions.jsx',
'./packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
'./packages/desktop-client/src/components/Notifications.tsx',
'./packages/desktop-client/src/components/payees/ManagePayees.jsx',
'./packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
'./packages/desktop-client/src/components/payees/PayeeTable.tsx',
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
'./packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
'./packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
'./packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
'./packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
'./packages/desktop-client/src/components/reports/SaveReportName.tsx',
'./packages/desktop-client/src/components/reports/useReport.ts',
'./packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
'./packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
'./packages/desktop-client/src/components/select/DateSelect.tsx',
'./packages/desktop-client/src/components/sidebar/Tools.tsx',
'./packages/desktop-client/src/components/sort.tsx',
'./packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
'./packages/desktop-client/src/components/table.tsx',
'./packages/desktop-client/src/components/Titlebar.tsx',
'./packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
'./packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
'./packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
'./packages/desktop-client/src/components/transactions/TransactionList.jsx',
'./packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
'./packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
'./packages/desktop-client/src/hooks/useAccounts.ts',
'./packages/desktop-client/src/hooks/useCategories.ts',
'./packages/desktop-client/src/hooks/usePayees.ts',
'./packages/desktop-client/src/hooks/useProperFocus.tsx',
'./packages/desktop-client/src/hooks/useSelected.tsx',
'./packages/loot-core/src/client/query-hooks.tsx',
],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'.eslintrc.js',
'*.test.js',
'*.test.ts',
'*.test.jsx',
'*.test.tsx',
],
rules: {
'rulesdir/typography': 'off',
},
},
],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
};

View File

@@ -23,6 +23,8 @@ body:
options:
- label: 'I have searched and found no existing issue'
required: true
- label: 'I will be providing steps how to reproduce the bug (in most cases this will also mean uploading a demo budget file)'
required: true
validations:
required: true
- type: textarea
@@ -34,14 +36,6 @@ body:
value: 'A bug happened!'
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: How can we reproduce the issue?
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
value: 'How can we reproduce the issue?'
validations:
required: true
- type: markdown
id: env-info
attributes:

View File

@@ -6,6 +6,3 @@ contact_links:
- name: Support
url: https://discord.gg/pRYNYr4W5A
about: Need help with something? Having troubles setting up? Or perhaps issues using the API? Reach out to the community on Discord.
- name: Translations
url: https://hosted.weblate.org/projects/actualbudget/actual/
about: Found a string that needs a better translation? Add your suggestion or upvote an existing one in Weblate.

View File

@@ -1,15 +1,5 @@
name: Setup
inputs:
working-directory:
description: 'Working directory to run in, default .'
required: false
default: '.'
download-translations:
description: 'Whether to download translations as part of setup, default true'
required: false
default: 'true'
runs:
using: composite
steps:
@@ -17,28 +7,13 @@ runs:
uses: actions/setup-node@v4
with:
node-version: 18.16.0
- name: Install yarn
run: npm install -g yarn
shell: bash
if: ${{ env.ACT }}
- name: Cache
uses: actions/cache@v4
id: cache
with:
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
path: '**/node_modules'
key: yarn-v1-${{ runner.os }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('**/yarn.lock') }}
- name: Install
working-directory: ${{ inputs.working-directory }}
run: yarn --immutable
shell: bash
if: steps.cache.outputs.cache-hit != 'true'
- name: Download translations
uses: actions/checkout@v4
with:
repository: actualbudget/translations
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
if: ${{ inputs.download-translations == 'true' }}
- name: Remove untranslated languages
run: packages/desktop-client/bin/remove-untranslated-languages
shell: bash
if: ${{ inputs.download-translations == 'true' }}

View File

@@ -40,7 +40,6 @@ jobs:
python3 -m pip install setuptools
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get install flatpak -y
sudo apt-get install flatpak-builder -y
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
@@ -49,17 +48,13 @@ jobs:
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron for Mac
if: ${{ startsWith(matrix.os, 'macos') }}
- name: Build Electron
run: ./bin/package-electron
env:
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
# CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# CSC_LINK: ${{ secrets.CSC_LINK }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- name: Build Electron
if: ${{ ! startsWith(matrix.os, 'macos') }}
run: ./bin/package-electron
- name: Upload Build
uses: actions/upload-artifact@v4
with:
@@ -80,7 +75,6 @@ jobs:
- name: Add to Release
uses: softprops/action-gh-release@v2
with:
draft: true
files: |
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe

View File

@@ -35,7 +35,6 @@ jobs:
python3 -m pip install setuptools
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get install flatpak -y
sudo apt-get install flatpak-builder -y
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

View File

@@ -1,87 +0,0 @@
name: Extract and upload i18n strings
on:
schedule:
# 4am UTC
- cron: "0 4 * * *"
workflow_dispatch:
jobs:
extract-and-upload-i18n-strings:
runs-on: ubuntu-latest
if: github.repository == 'actualbudget/actual'
steps:
- name: Check out main repository
uses: actions/checkout@v4
with:
path: actual
- name: Set up environment
uses: ./actual/.github/actions/setup
with:
working-directory: actual
download-translations: false # As we'll manually clone instead
- name: Configure Git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Configure i18n client
run: |
pip install wlc
- name: Lock translations
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
lock \
actualbudget/actual
- name: Update VCS with latest translations
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
push \
actualbudget/actual
- name: Check out updated translations
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
repository: actualbudget/translations
path: translations
- name: Generate i18n strings
working-directory: actual
run: |
mkdir -p packages/desktop-client/locale/
cp ../translations/en.json packages/desktop-client/locale/
yarn generate:i18n
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
exit 1
fi
- name: Check in new i18n strings
working-directory: translations
run: |
cp ../actual/packages/desktop-client/locale/en.json .
git add .
if git commit -m "Update source strings"; then
git push
else
echo "No changes to commit"
fi
- name: Update Weblate with latest translations
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
pull \
actualbudget/actual
- name: Unlock translations
if: always() # Clean up even on failure
run: |
wlc \
--url https://hosted.weblate.org/api/ \
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
unlock \
actualbudget/actual

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write
steps:
- name: Wait for ${{github.base_ref}} build to succeed
uses: fountainhead/action-wait-for-check@v1.2.0
uses: fountainhead/action-wait-for-check@v1.1.0
id: master-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -33,7 +33,7 @@ jobs:
ref: ${{github.base_ref}}
- name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@v1.2.0
uses: fountainhead/action-wait-for-check@v1.1.0
id: wait-for-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -46,7 +46,7 @@ jobs:
echo "Build failed on PR branch or ${{github.base_ref}}"
exit 1
- name: Download build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v3
id: pr-build
with:
branch: ${{github.base_ref}}
@@ -55,13 +55,12 @@ jobs:
path: base
- name: Download build artifact from PR
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v3
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
name: build-stats
path: head
allow_forks: true
- name: Strip content hashes from stats files
run: |
@@ -71,14 +70,14 @@ jobs:
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
- uses: twk3/rollup-size-compare-action@v1.1.1
- uses: twk3/rollup-size-compare-action@v1.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
current-stats-json-path: ./head/web-stats.json
base-stats-json-path: ./base/web-stats.json
title: desktop-client
- uses: github/webpack-bundlesize-compare-action@v2.1.0
- uses: github/webpack-bundlesize-compare-action@v1.8.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
current-stats-json-path: ./head/loot-core-stats.json

View File

@@ -1,113 +0,0 @@
name: /update-vrt
on:
issue_comment:
types: [ created ]
permissions:
pull-requests: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
cancel-in-progress: true
jobs:
update-vrt:
name: Update VRT
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.41.1-jammy
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
id: comment-branch
- uses: actions/checkout@v4
with:
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
ref: ${{ steps.comment-branch.outputs.head_ref }}
- name: Set up environment
uses: ./.github/actions/setup
- name: Wait for Netlify build to finish
id: netlify
env:
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./.github/actions/netlify-wait-for-build
- name: Run VRT Tests on Netlify URL
run: yarn vrt --update-snapshots
env:
E2E_START_URL: ${{ steps.netlify.outputs.url }}
- name: Create patch
run: |
git config --system --add safe.directory "*"
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git reset
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update VRT"
git format-patch -1 HEAD --stdout > Update-VRT.patch
- uses: actions/upload-artifact@v4
with:
name: patch
path: Update-VRT.patch
push-patch:
runs-on: ubuntu-latest
needs: update-vrt
permissions:
contents: write
pull-requests: write
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
id: comment-branch
- uses: actions/checkout@v4
with:
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
ref: ${{ steps.comment-branch.outputs.head_ref }}
- uses: actions/download-artifact@v4
continue-on-error: true
with:
name: patch
- name: Apply patch and push
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git apply Update-VRT.patch
git add "**/*.png"
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "Update VRT"
git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }}
- name: Add finished reaction
uses: dkershner6/reaction-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
commentId: ${{ github.event.comment.id }}
reaction: "rocket"
add-starting-reaction:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
permissions:
pull-requests: write
steps:
- name: React to comment
uses: dkershner6/reaction-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
commentId: ${{ github.event.comment.id }}
reaction: "+1"

3
.gitignore vendored
View File

@@ -50,6 +50,3 @@ bundle.mobile.js.map
# Local Netlify folder
.netlify
# build output
package.tgz

View File

@@ -74,6 +74,6 @@ Make Actual Budget accessible to more people by helping with the [Internationali
## Sponsors
Thanks to our wonderful sponsors who make Actual Budget possible!
Thanks to our wonderful sponsors who make Actual budget possible!
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>

View File

@@ -4,15 +4,6 @@ ROOT=`dirname $0`
cd "$ROOT/.."
echo "Updating translations..."
if ! [ -d packages/desktop-client/locale ]; then
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
fi
pushd packages/desktop-client/locale > /dev/null
git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
yarn workspace loot-core build:browser
yarn workspace @actual-app/web build:browser

View File

@@ -36,7 +36,7 @@ fi
yarn workspace loot-core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build
yarn workspace @actual-app/web build --mode=desktop
yarn workspace desktop-electron update-client

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -1,816 +0,0 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import globals from 'globals';
import pluginImport from 'eslint-plugin-import';
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
import pluginPrettier from 'eslint-plugin-prettier/recommended';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginRulesDir from 'eslint-plugin-rulesdir';
import pluginTypescript from 'typescript-eslint';
import tsParser from '@typescript-eslint/parser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
pluginRulesDir.RULES_DIR = path.join(
__dirname,
'packages',
'eslint-plugin-actual',
'lib',
'rules',
);
const confusingBrowserGlobals = [
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
'addEventListener',
'blur',
'close',
'closed',
'confirm',
'defaultStatus',
'defaultstatus',
'event',
'external',
'find',
'focus',
'frameElement',
'frames',
'history',
'innerHeight',
'innerWidth',
'length',
'location',
'locationbar',
'menubar',
'moveBy',
'moveTo',
'name',
'onblur',
'onerror',
'onfocus',
'onload',
'onresize',
'onunload',
'open',
'opener',
'opera',
'outerHeight',
'outerWidth',
'pageXOffset',
'pageYOffset',
'parent',
'print',
'removeEventListener',
'resizeBy',
'resizeTo',
'screen',
'screenLeft',
'screenTop',
'screenX',
'screenY',
'scroll',
'scrollbars',
'scrollBy',
'scrollTo',
'scrollX',
'scrollY',
'status',
'statusbar',
'stop',
'toolbar',
'top',
];
/** @type {import('eslint').Linter.Config[]} */
export default [
{
ignores: [
'packages/api/app/bundle.api.js',
'packages/api/dist',
'packages/api/@types',
'packages/api/migrations',
'packages/crdt/dist',
'packages/desktop-client/bundle.browser.js',
'packages/desktop-client/build/',
'packages/desktop-client/build-electron/',
'packages/desktop-client/build-stats/',
'packages/desktop-client/public/kcab/',
'packages/desktop-client/public/data/',
'packages/desktop-client/**/node_modules/*',
'packages/desktop-client/node_modules/',
'packages/desktop-client/src/icons/**/*',
'packages/desktop-client/test-results/',
'packages/desktop-client/playwright-report/',
'packages/desktop-electron/client-build/',
'packages/desktop-electron/build/',
'packages/desktop-electron/dist/',
'packages/import-ynab4/**/node_modules/*',
'packages/import-ynab5/**/node_modules/*',
'packages/loot-core/**/node_modules/*',
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'.yarn/*',
'.github/*',
],
},
{
linterOptions: {
reportUnusedDisableDirectives: true,
},
languageOptions: {
globals: {
...globals.browser,
...globals.commonjs,
...globals.jest,
...globals.node,
globalThis: false,
vi: true,
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
},
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
pluginPrettier,
...pluginTypescript.configs.recommended,
pluginImport.flatConfigs.recommended,
{
plugins: {
'react-hooks': pluginReactHooks,
'jsx-a11y': pluginJSXA11y,
rulesdir: pluginRulesDir,
},
},
{
files: ['**/*.{js,ts,jsx,tsx}'],
rules: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'default-case': [
'warn',
{
commentPattern: '^no default$',
},
],
curly: ['warn', 'multi-line', 'consistent'],
'dot-location': ['warn', 'property'],
eqeqeq: ['warn', 'smart'],
'new-parens': 'warn',
'no-array-constructor': 'warn',
'no-caller': 'warn',
'no-cond-assign': ['warn', 'except-parens'],
'no-const-assign': 'warn',
'no-control-regex': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-eval': 'warn',
'no-ex-assign': 'warn',
'no-extend-native': 'warn',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-fallthrough': 'warn',
'no-func-assign': 'warn',
'no-implied-eval': 'warn',
'no-invalid-regexp': 'warn',
'no-iterator': 'warn',
'no-label-var': 'warn',
'no-labels': [
'warn',
{
allowLoop: true,
allowSwitch: false,
},
],
'no-lone-blocks': 'warn',
'no-mixed-operators': [
'warn',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: false,
},
],
'no-multi-str': 'warn',
'no-global-assign': 'warn',
'no-unsafe-negation': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-redeclare': 'warn',
'no-regex-spaces': 'warn',
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-shadow-restricted-names': 'warn',
'no-sparse-arrays': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-undef': 'error',
'no-unreachable': 'warn',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-unused-labels': 'warn',
'no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
},
],
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': [
'warn',
{
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false,
},
],
'no-with': 'warn',
'no-whitespace-before-property': 'warn',
'require-yield': 'warn',
'rest-spread-spacing': ['warn', 'never'],
strict: ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': 'warn',
'valid-typeof': 'warn',
'no-restricted-properties': [
'error',
{
object: 'require',
property: 'ensure',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
{
object: 'System',
property: 'import',
message:
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
},
],
'getter-return': 'warn',
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
'import/first': 'error',
'import/no-amd': 'error',
'import/no-anonymous-default-export': 'warn',
'import/no-webpack-loader-syntax': 'error',
'import/extensions': [
'warn',
'never',
{
json: 'always',
},
],
'import/no-useless-path-segments': 'warn',
'import/no-duplicates': [
'warn',
{
'prefer-inline': true,
},
],
'import/order': [
'warn',
{
alphabetize: {
caseInsensitive: true,
order: 'asc',
},
groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
pathGroups: [
{
// Enforce that React (and react-related packages) is the first import
group: 'builtin',
pattern: 'react?(-*)',
position: 'before',
},
{
// Separate imports from Actual from "real" external imports
group: 'external',
pattern: 'loot-{core,design}/**/*',
position: 'after',
},
],
pathGroupsExcludedImportTypes: ['react'],
},
],
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
'react/forbid-foreign-prop-types': [
'warn',
{
allowInPropTypes: true,
},
],
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-no-duplicate-props': 'warn',
'react/jsx-no-target-blank': 'warn',
'react/jsx-no-undef': 'error',
'react/jsx-pascal-case': [
'warn',
{
allowAllCaps: true,
ignore: [],
},
],
'react/no-danger-with-children': 'warn',
// Disabled because of undesirable warnings
// See https://github.com/facebook/create-react-app/issues/5204 for
// blockers until its re-enabled
// 'react/no-deprecated': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-typos': 'error',
'react/require-render-return': 'error',
'react/style-prop-object': 'warn',
'react/jsx-no-useless-fragment': 'warn',
'react/self-closing-comp': 'warn',
'react/jsx-filename-extension': [
'warn',
{
extensions: ['.jsx', '.tsx'],
allow: 'as-needed',
},
],
'react/no-unstable-nested-components': [
'warn',
{
allowAsProps: true,
customValidators: ['formatter'],
},
],
// Don't need this as we're using TypeScript
'react/prop-types': 'off',
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{
aspects: ['noHref', 'invalidHref'],
},
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': [
'warn',
{
ignoreNonDOM: true,
},
],
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'warn',
'jsx-a11y/scope': 'warn',
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: '(useQuery)',
},
],
'rulesdir/typography': 'warn',
'rulesdir/prefer-if-statement': 'warn',
// Note: base rule explicitly disabled in favor of the TS one
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
'no-restricted-globals': ['warn', ...confusingBrowserGlobals],
// https://github.com/eslint/eslint/issues/16954
// https://github.com/eslint/eslint/issues/16953
'no-loop-func': 'off',
// TODO: re-enable these rules
'react/react-in-jsx-scope': 'off',
'no-var': 'warn',
'react/jsx-curly-brace-presence': 'warn',
'object-shorthand': ['warn', 'properties'],
'no-restricted-syntax': [
'warn',
{
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
selector:
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
message:
'Using default React import is discouraged, please use named exports directly instead.',
},
{
// forbid <a> in favor of <Link>
selector: 'JSXOpeningElement[name.name="a"]',
message: 'Using <a> is discouraged, please use <Link> instead.',
},
],
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['*.api', '*.web', '*.electron'],
message: "Don't directly reference imports from other platforms",
},
{
group: ['uuid'],
importNames: ['*'],
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
},
{
group: ['**/style', '**/colors'],
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
],
},
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description',
},
],
// Rules disabled during TS migration
'@typescript-eslint/no-var-requires': 'off',
'prefer-const': 'warn',
'prefer-spread': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-require-imports': 'off',
'import/no-default-export': 'warn',
},
},
{
files: ['**/*.ts?(x)'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2018,
sourceType: 'module',
parserOptions: {
project: [path.join(__dirname, './tsconfig.json')],
ecmaFeatures: {
jsx: true,
},
// typescript-eslint specific options
warnOnUnsupportedTypeScriptVersion: true,
},
},
// If adding a typescript-eslint version of an existing ESLint rule,
// make sure to disable the ESLint rule here.
rules: {
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
'default-case': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
'no-dupe-class-members': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
'no-undef': 'off',
// Add TypeScript specific rules (and turn off ESLint equivalents)
'@typescript-eslint/consistent-type-assertions': 'warn',
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'warn',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'warn',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
typedefs: false,
},
],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'warn',
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
'packages/loot-core/src/client/**/*.{ts,tsx}',
],
rules: {
// enforce type over interface
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
// enforce import type
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/no-restricted-types': [
'warn',
{
types: {
// forbid FC as superflous
FunctionComponent: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
FC: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
},
},
],
},
},
{
files: ['packages/desktop-client/**/*'],
ignores: ['packages/desktop-client/src/hooks/useNavigate.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-router-dom',
importNames: ['useNavigate'],
message:
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
},
],
},
],
},
},
{
files: ['packages/desktop-client/**/*', 'packages/loot-core/**/*'],
ignores: ['packages/desktop-client/src/redux/index.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'warn',
{
paths: [
{
name: 'react-redux',
importNames: ['useDispatch'],
message:
"Please import Actual's useDispatch() hook from `src/redux` instead.",
},
{
name: 'react-redux',
importNames: ['useSelector'],
message:
"Please import Actual's useSelector() hook from `src/redux` instead.",
},
],
},
],
},
},
{
files: ['packages/loot-core/src/**/*'],
rules: {
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['*.api', '*.web', '*.electron'],
message: "Don't directly reference imports from other platforms",
},
{
group: ['uuid'],
importNames: ['*'],
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
},
{
group: ['loot-core/**'],
message:
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
},
],
},
],
},
},
{
files: [
'packages/loot-core/src/types/**/*',
'packages/loot-core/src/client/state-types/**/*',
'**/icons/**/*',
'**/{mocks,__mocks__}/**/*',
// can't correctly resolve usages
'**/*.{testing,electron,browser,web,api}.ts',
],
rules: {
'import/no-unused-modules': 'off',
},
},
{
files: [
'packages/desktop-client/src/style/index.*',
'packages/desktop-client/src/style/palette.*',
],
rules: {
'no-restricted-imports': [
'off',
{
patterns: [
{
group: ['**/style', '**/colors'],
importNames: ['colors'],
message: 'Please use themes instead of colors',
},
],
},
],
},
},
{
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
rules: {
'import/no-default-export': 'off',
},
},
{},
{
// TODO: fix the issues in these files
files: [
'packages/desktop-client/src/components/accounts/Account.jsx',
'packages/desktop-client/src/components/accounts/MobileAccount.jsx',
'packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
'packages/desktop-client/src/components/App.tsx',
'packages/desktop-client/src/components/budget/BudgetCategories.jsx',
'packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
'packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
'packages/desktop-client/src/components/budget/index.tsx',
'packages/desktop-client/src/components/budget/MobileBudget.tsx',
'packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
'packages/desktop-client/src/components/common/Menu.tsx',
'packages/desktop-client/src/components/FinancesApp.tsx',
'packages/desktop-client/src/components/GlobalKeys.ts',
'packages/desktop-client/src/components/LoggedInUser.tsx',
'packages/desktop-client/src/components/manager/ManagementApp.jsx',
'packages/desktop-client/src/components/manager/subscribe/common.tsx',
'packages/desktop-client/src/components/ManageRules.tsx',
'packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
'packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
'packages/desktop-client/src/components/Modals.tsx',
'packages/desktop-client/src/components/modals/EditRule.jsx',
'packages/desktop-client/src/components/modals/ImportTransactions.jsx',
'packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
'packages/desktop-client/src/components/Notifications.tsx',
'packages/desktop-client/src/components/payees/ManagePayees.jsx',
'packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
'packages/desktop-client/src/components/payees/PayeeTable.tsx',
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
'packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
'packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
'packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
'packages/desktop-client/src/components/reports/SaveReportName.tsx',
'packages/desktop-client/src/components/reports/useReport.ts',
'packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
'packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
'packages/desktop-client/src/components/select/DateSelect.tsx',
'packages/desktop-client/src/components/sidebar/Tools.tsx',
'packages/desktop-client/src/components/sort.tsx',
'packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
'packages/desktop-client/src/components/table.tsx',
'packages/desktop-client/src/components/Titlebar.tsx',
'packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
'packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
'packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
'packages/desktop-client/src/components/transactions/TransactionList.jsx',
'packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
'packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
'packages/desktop-client/src/hooks/useAccounts.ts',
'packages/desktop-client/src/hooks/useCategories.ts',
'packages/desktop-client/src/hooks/usePayees.ts',
'packages/desktop-client/src/hooks/useProperFocus.tsx',
'packages/desktop-client/src/hooks/useSelected.tsx',
'packages/loot-core/src/client/query-hooks.tsx',
],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'eslint.config.mjs',
'**/*.test.js',
'**/*.test.ts',
'**/*.test.jsx',
'**/*.test.tsx',
],
rules: {
'rulesdir/typography': 'off',
},
},
];

View File

@@ -38,33 +38,33 @@
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"lint": "eslint . --max-warnings 0",
"lint": "eslint . --max-warnings 0 --ext .js,.jsx,.ts,.tsx",
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
"typecheck": "yarn tsc && tsc-strict",
"jq": "./node_modules/node-jq/bin/jq",
"prepare": "husky"
},
"devDependencies": {
"@typescript-eslint/parser": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"confusing-browser-globals": "^1.0.11",
"cross-env": "^7.0.3",
"eslint": "^9.17.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-rulesdir": "^0.2.2",
"globals": "^15.13.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.9",
"node-jq": "^4.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"prettier": "3.3.3",
"source-map-support": "^0.5.21",
"typescript": "^5.5.4",
"typescript-eslint": "^8.18.1",
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {

View File

@@ -8,7 +8,7 @@ import type {
import type { InitConfig } from 'loot-core/server/main';
// @ts-ignore: bundle not available until we build it
// eslint-disable-next-line import/extensions, import/no-unresolved
// eslint-disable-next-line import/extensions
import * as bundle from './app/bundle.api.js';
import * as injected from './injected';
import { validateNodeVersion } from './validateNodeVersion';

View File

@@ -86,10 +86,7 @@ export function addTransactions(
}
export function importTransactions(accountId, transactions) {
return send('api/transactions-import', {
accountId,
transactions,
});
return send('api/transactions-import', { accountId, transactions });
}
export function getTransactions(accountId, startDate, endDate) {

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "25.1.0",
"version": "6.9.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
@@ -23,7 +23,7 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^11.7.0",
"better-sqlite3": "^9.6.0",
"compare-versions": "^6.1.0",
"node-fetch": "^3.3.2",
"uuid": "^9.0.1"

View File

@@ -6,11 +6,9 @@ node_modules
# testing
coverage
test-results
playwright-report
# production
build
build-electron
build-stats
stats.json
@@ -25,6 +23,3 @@ public/kcab
public/data
public/data-file-index.txt
public/*.wasm
# translations
locale/

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Local path to the cloned translations repository
const localRepoPath = './packages/desktop-client/locale';
// Compare JSON files and delete incomplete ones
const processTranslations = () => {
try {
const files = fs.readdirSync(localRepoPath);
const enJsonPath = path.join(localRepoPath, 'en.json');
if (!fs.existsSync(enJsonPath)) {
throw new Error('en.json not found in the repository.');
}
const enJson = JSON.parse(fs.readFileSync(enJsonPath, 'utf8'));
const enKeysCount = Object.keys(enJson).length;
console.log(`en.json has ${enKeysCount} keys.`);
files.forEach((file) => {
if (file === 'en.json' || path.extname(file) !== '.json') return;
const filePath = path.join(localRepoPath, file);
const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const fileKeysCount = Object.keys(jsonData).length;
// Calculate the percentage of keys present compared to en.json
const percentage = (fileKeysCount / enKeysCount) * 100;
console.log(`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`);
if (percentage < 50) {
fs.unlinkSync(filePath);
console.log(`Deleted ${file} due to insufficient keys.`);
} else {
console.log(`Keeping ${file}.`);
}
});
console.log('Processing completed.');
} catch (error) {
console.error(`Error: ${error.message}`);
}
};
processTranslations();

View File

@@ -1,64 +0,0 @@
import { test, expect } from '@playwright/test';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
test.describe('Mobile Accounts', () => {
let page;
let navigation;
let configurationPage;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
navigation = new MobileNavigation(page);
configurationPage = new ConfigurationPage(page);
await page.setViewportSize({
width: 350,
height: 600,
});
await page.goto('/');
await configurationPage.createTestFile();
});
test.afterEach(async () => {
await page.close();
});
test('opens the accounts page and asserts on balances', async () => {
const accountsPage = await navigation.goToAccountsPage();
await accountsPage.waitFor();
const account = await accountsPage.getNthAccount(1);
await expect(account.name).toHaveText('Ally Savings');
await expect(account.balance).toHaveText('7,653.00');
await expect(page).toMatchThemeScreenshots();
});
test('opens individual account page and checks that filtering is working', async () => {
const accountsPage = await navigation.goToAccountsPage();
await accountsPage.waitFor();
const accountPage = await accountsPage.openNthAccount(0);
await accountPage.waitFor();
await expect(accountPage.heading).toHaveText('Bank of America');
await expect(accountPage.transactionList).toBeVisible();
await expect(await accountPage.getBalance()).toBeGreaterThan(0);
await expect(accountPage.noTransactionsMessage).not.toBeVisible();
await expect(page).toMatchThemeScreenshots();
await accountPage.searchByText('nothing should be found');
await expect(accountPage.noTransactionsMessage).toBeVisible();
await expect(accountPage.transactions).toHaveCount(0);
await expect(page).toMatchThemeScreenshots();
await accountPage.clearSearch();
await expect(accountPage.transactions).not.toHaveCount(0);
await accountPage.searchByText('Kroger');
await expect(accountPage.transactions).not.toHaveCount(0);
await expect(page).toMatchThemeScreenshots();
});
});

View File

@@ -1,5 +1,3 @@
import { join } from 'path';
import { test, expect } from '@playwright/test';
import { ConfigurationPage } from './page-models/configuration-page';
@@ -11,7 +9,7 @@ test.describe('Accounts', () => {
let configurationPage;
let accountPage;
test.beforeEach(async ({ browser }) => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
navigation = new Navigation(page);
configurationPage = new ConfigurationPage(page);
@@ -20,7 +18,7 @@ test.describe('Accounts', () => {
await configurationPage.createTestFile();
});
test.afterEach(async () => {
test.afterAll(async () => {
await page.close();
});
@@ -54,17 +52,15 @@ test.describe('Accounts', () => {
await expect(page).toMatchThemeScreenshots();
});
test.describe('On Budget Accounts', () => {
test.describe('Budgeted Accounts', () => {
// Reset filters
test.afterEach(async () => {
await accountPage.removeFilter(0);
});
test('creates a transfer from two existing transactions', async () => {
accountPage = await navigation.goToAccountPage('On budget');
await accountPage.waitFor();
await expect(accountPage.accountName).toHaveText('On Budget Accounts');
accountPage = await navigation.goToAccountPage('For budget');
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');
await accountPage.filterByNote('Test Acc Transfer');
@@ -103,61 +99,4 @@ test.describe('Accounts', () => {
await expect(transaction.account).toHaveText('Ally Savings');
});
});
test.describe('Import Transactions', () => {
test.beforeEach(async () => {
accountPage = await navigation.createAccount({
name: 'CSV import',
offBudget: false,
balance: 0,
});
await accountPage.waitFor();
});
async function importCsv(screenshot = false) {
const fileChooserPromise = page.waitForEvent('filechooser');
await accountPage.page.getByRole('button', { name: 'Import' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
if (screenshot) await expect(page).toMatchThemeScreenshots();
const importButton = accountPage.page.getByRole('button', {
name: /Import \d+ transactions/,
});
await importButton.click();
await expect(importButton).not.toBeVisible();
}
test('imports transactions from a CSV file', async () => {
await importCsv(true);
});
test('import csv file twice', async () => {
await importCsv(false);
const fileChooserPromise = page.waitForEvent('filechooser');
await accountPage.page.getByRole('button', { name: 'Import' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
await expect(page).toMatchThemeScreenshots();
const importButton = accountPage.page.getByRole('button', {
name: /Import \d+ transactions/,
});
await expect(importButton).toBeDisabled();
await expect(await importButton.innerText()).toMatch(
/Import 0 transactions/,
);
await accountPage.page.getByRole('button', { name: 'Close' }).click();
await expect(importButton).not.toBeVisible();
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,281 +0,0 @@
import { test, expect } from '@playwright/test';
import * as monthUtils from 'loot-core/src/shared/months';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
const budgetTypes = ['Envelope', 'Tracking'];
budgetTypes.forEach(budgetType => {
test.describe(`Mobile Budget [${budgetType}]`, () => {
let page;
let navigation;
let configurationPage;
let previousGlobalIsTesting;
test.beforeAll(() => {
// TODO: Hack, properly mock the currentMonth function
previousGlobalIsTesting = global.IS_TESTING;
global.IS_TESTING = true;
});
test.afterAll(() => {
// TODO: Hack, properly mock the currentMonth function
global.IS_TESTING = previousGlobalIsTesting;
});
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
navigation = new MobileNavigation(page);
configurationPage = new ConfigurationPage(page);
await page.setViewportSize({
width: 350,
height: 600,
});
await page.goto('/');
await configurationPage.createTestFile();
if (budgetType === 'Tracking') {
// Set budget type to tracking
const settingsPage = await navigation.goToSettingsPage();
await settingsPage.useBudgetType('tracking');
}
});
test.afterEach(async () => {
await page.close();
});
test('loads the budget page with budgeted amounts', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
await expect(budgetPage.categoryNames).toHaveText([
'Food',
'Restaurants',
'Entertainment',
'Clothing',
'General',
'Gift',
'Medical',
'Savings',
'Cell',
'Internet',
'Mortgage',
'Water',
'Power',
'Starting Balances',
'Misc',
'Income',
]);
await expect(page).toMatchThemeScreenshots();
});
// Page Header Tests
test('checks that clicking the Actual logo in the page header opens the budget page menu', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
await budgetPage.openBudgetPageMenu();
const budgetPageMenuModal = page.getByRole('dialog');
await expect(budgetPageMenuModal).toBeVisible();
await expect(page).toMatchThemeScreenshots();
});
test("checks that clicking the left arrow in the page header shows the previous month's budget", async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const selectedMonth = await budgetPage.getSelectedMonth();
const displayMonth = monthUtils.format(
selectedMonth,
budgetPage.MONTH_HEADER_DATE_FORMAT,
);
await expect(budgetPage.heading).toHaveText(displayMonth);
const previousMonth = await budgetPage.goToPreviousMonth();
const previousDisplayMonth = monthUtils.format(
previousMonth,
budgetPage.MONTH_HEADER_DATE_FORMAT,
);
await expect(budgetPage.heading).toHaveText(previousDisplayMonth);
await expect(page).toMatchThemeScreenshots();
});
test('checks that clicking the month in the page header opens the month menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const selectedMonth = await budgetPage.getSelectedMonth();
await budgetPage.openMonthMenu();
const monthMenuModal = page.getByRole('dialog');
const monthMenuModalHeading = monthMenuModal.getByRole('heading');
const displayMonth = monthUtils.format(
selectedMonth,
budgetPage.MONTH_HEADER_DATE_FORMAT,
);
await expect(monthMenuModalHeading).toHaveText(displayMonth);
await expect(page).toMatchThemeScreenshots();
});
test("checks that clicking the right arrow in the page header shows the next month's budget", async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const selectedMonth = await budgetPage.getSelectedMonth();
const displayMonth = monthUtils.format(
selectedMonth,
budgetPage.MONTH_HEADER_DATE_FORMAT,
);
await expect(budgetPage.heading).toHaveText(displayMonth);
const nextMonth = await budgetPage.goToNextMonth();
const nextDisplayMonth = monthUtils.format(
nextMonth,
budgetPage.MONTH_HEADER_DATE_FORMAT,
);
await expect(budgetPage.heading).toHaveText(nextDisplayMonth);
await expect(page).toMatchThemeScreenshots();
});
// Category / Category Group Menu Tests
test('checks that clicking the category group name opens the category group menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryGroupName = await budgetPage.getCategoryGroupNameForRow(0);
await budgetPage.openCategoryGroupMenu(categoryGroupName);
const categoryMenuModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(categoryMenuModalHeading).toHaveText(categoryGroupName);
await expect(page).toMatchThemeScreenshots();
});
test('checks that clicking the category name opens the category menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryName = await budgetPage.getCategoryNameForRow(0);
await budgetPage.openCategoryMenu(categoryName);
const categoryMenuModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(categoryMenuModalHeading).toHaveText(categoryName);
await expect(page).toMatchThemeScreenshots();
});
// Budgeted Cell Tests
test('checks that clicking the budgeted cell opens the budget menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryName = await budgetPage.getCategoryNameForRow(0);
await budgetPage.openBudgetMenu(categoryName);
const budgetMenuModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(budgetMenuModalHeading).toHaveText(categoryName);
await expect(page).toMatchThemeScreenshots();
});
test('updates the budgeted amount', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryName = await budgetPage.getCategoryNameForRow(0);
// Set to 100.00
await budgetPage.setBudget(categoryName, 10000);
const budgetedButton =
await budgetPage.getButtonForBudgeted(categoryName);
await expect(budgetedButton).toHaveText('100.00');
await expect(page).toMatchThemeScreenshots();
});
// Spent Cell Tests
test('checks that clicking spent cell redirects to the category transactions page', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryName = await budgetPage.getCategoryNameForRow(0);
const accountPage = await budgetPage.openSpentPage(categoryName);
await expect(accountPage.heading).toContainText(categoryName);
await expect(accountPage.transactionList).toBeVisible();
await expect(page).toMatchThemeScreenshots();
});
// Balance Cell Tests
test('checks that clicking the balance cell opens the balance menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
const categoryName = await budgetPage.getCategoryNameForRow(0);
await budgetPage.openBalanceMenu(categoryName);
const balanceMenuModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(balanceMenuModalHeading).toHaveText(categoryName);
await expect(page).toMatchThemeScreenshots();
});
if (budgetType === 'Envelope') {
test('checks that clicking the To Budget/Overbudgeted amount opens the budget summary menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
await budgetPage.openEnvelopeBudgetSummaryMenu();
const summaryModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(summaryModalHeading).toHaveText('Budget Summary');
await expect(page).toMatchThemeScreenshots();
});
}
if (budgetType === 'Tracking') {
test('checks that clicking the Saved/Projected Savings/Overspent amount opens the budget summary menu modal', async () => {
const budgetPage = await navigation.goToBudgetPage();
await budgetPage.waitForBudgetTable();
await budgetPage.openTrackingBudgetSummaryMenu();
const summaryModalHeading = page
.getByRole('dialog')
.getByRole('heading');
await expect(summaryModalHeading).toHaveText('Budget Summary');
await expect(page).toMatchThemeScreenshots();
});
}
});
});

Some files were not shown because too many files have changed in this diff Show More