Compare commits

..

13 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
2044580642 Notes tag autocomplete 2024-05-01 14:45:29 -07:00
Joel Jeremy Marquez
01a2f0665e VRT 2024-04-30 07:23:09 -07:00
Joel Jeremy Marquez
12b5e0ac7c Revert pill colors 2024-04-29 15:42:38 -07:00
Joel Jeremy Marquez
060716b53c Fix overlapping UI 2024-04-29 15:17:50 -07:00
Joel Jeremy Marquez
6f1b66f9dc Append note tag filters 2024-04-29 15:12:11 -07:00
Joel Jeremy Marquez
eea961a20d Cleanup style 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
5da0f86384 Remove font weight 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
176a4782a1 Rename variables 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
2ece4d35a7 Fix filtering 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
fe2166207b Update colors 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
849a43d9ce Fix tests - extract the handler to higher level component 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
30d4ca9ac6 Release notes 2024-04-27 14:23:04 -07:00
Joel Jeremy Marquez
f49c7f516e Format notes as clickable tags 2024-04-27 14:23:03 -07:00
1157 changed files with 32817 additions and 51677 deletions

View File

@@ -14,7 +14,6 @@ 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/dist/

View File

@@ -1,3 +1,4 @@
/* eslint-disable rulesdir/typography */
const path = require('path');
const rulesDirPlugin = require('eslint-plugin-rulesdir');
@@ -33,23 +34,9 @@ const restrictedImportColors = [
];
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',
],
plugins: ['prettier', 'import', 'rulesdir', '@typescript-eslint'],
extends: [
'react-app',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:prettier/recommended',
@@ -64,184 +51,6 @@ module.exports = {
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
@@ -251,7 +60,6 @@ module.exports = {
{
varsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
@@ -358,63 +166,9 @@ module.exports = {
'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 },
@@ -435,7 +189,7 @@ module.exports = {
'warn',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/no-restricted-types': [
'@typescript-eslint/ban-types': [
'warn',
{
types: {
@@ -443,6 +197,7 @@ module.exports = {
FunctionComponent: { message: ruleFCMsg },
FC: { message: ruleFCMsg },
},
extendDefaults: true,
},
],
},
@@ -526,8 +281,8 @@ module.exports = {
'./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/budget/rollover/HoldTooltip.tsx',
'./packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx',
'./packages/desktop-client/src/components/common/Menu.tsx',
'./packages/desktop-client/src/components/FinancesApp.tsx',
'./packages/desktop-client/src/components/GlobalKeys.ts',
@@ -577,23 +332,8 @@ module.exports = {
'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

@@ -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 }}
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:
@@ -67,22 +62,13 @@ jobs:
path: |
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@v4
with:
name: actual-electron-${{ matrix.os }}-appx
path: |
packages/desktop-electron/dist/*.appx
- name: Add to Release
uses: softprops/action-gh-release@v2
with:
files: |
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak

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
@@ -53,13 +52,5 @@ jobs:
path: |
packages/desktop-electron/dist/*.dmg
packages/desktop-electron/dist/*.exe
!packages/desktop-electron/dist/Actual-windows.exe
packages/desktop-electron/dist/*.AppImage
packages/desktop-electron/dist/*.flatpak
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@v4
with:
name: actual-electron-${{ matrix.os }}-appx
path: |
packages/desktop-electron/dist/*.appx

View File

@@ -1,43 +0,0 @@
name: Deploy Netlify Release
defaults:
run:
shell: bash
env:
CI: true
on:
push:
tags:
- v**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Repository Checkout
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Install Netlify
run: npm install netlify-cli@17.10.1 -g
- name: Build Actual
run: ./bin/package-browser
- name: Deploy to Netlify
id: netlify_deploy
run: |
netlify deploy \
--dir packages/desktop-client/build \
--site ${{ secrets.NETLIFY_SITE_ID }} \
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
--filter @actual-app/web \
--prod

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

@@ -7,20 +7,10 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v8
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
days-before-stale: 30
days-before-close: 5
days-before-issue-stale: -1
stale-wip:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
days-before-stale: 7
any-of-labels: ':construction: WIP'
days-before-close: -1
days-before-issue-stale: -1

View File

@@ -1,55 +0,0 @@
name: /update-vrt
on:
issue_comment:
types: [ created ]
permissions:
pull-requests: write
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
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: Commit and push changes
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 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 }}

27
.github/workflows/wip.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Add WIP
on:
pull_request_target:
types:
- opened
jobs:
add_wip_prefix:
if: |
join(github.event.pull_request.requested_reviewers) == ''
&& !contains(github.event.pull_request.title, 'WIP')
&& !contains(github.event.pull_request.labels.*.name, 'WIP')
&& github.event.pull_request.draft != true
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Add WIP
env:
TITLE: ${{ github.event.pull_request.title }}
shell: bash
run: |
echo ${{ secrets.GITHUB_TOKEN }} | gh auth login --with-token
gh pr edit ${{ github.event.pull_request.number }} -t "[WIP] ${TITLE}"

7
.gitignore vendored
View File

@@ -21,7 +21,6 @@ packages/api/dist
packages/api/@types
packages/crdt/dist
packages/desktop-electron/client-build
packages/desktop-electron/build
packages/desktop-electron/.electron-symbols
packages/desktop-electron/dist
packages/desktop-electron/loot-core
@@ -47,9 +46,3 @@ bundle.mobile.js.map
# Misc
.#*
# Local Netlify folder
.netlify
# build output
package.tgz

View File

@@ -1 +0,0 @@
yarn lint-staged

893
.yarn/releases/yarn-4.0.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.0.2.cjs

View File

@@ -14,40 +14,22 @@ Want to say thanks? Click the ⭐ at the top of the page.
- Actual [discord](https://discord.gg/pRYNYr4W5A) community.
- Actual [Community Documentation](https://actualbudget.org/docs)
- [Frequently asked questions](https://actualbudget.org/docs/faq)
## Installation
There are four ways to deploy Actual:
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
1. One-click deployment [via PikaPods](https://www.pikapods.com/pods?run=actual) (~1.40 $/month) - recommended for non-technical users
1. Managed hosting [via Fly.io](https://actualbudget.org/docs/install/fly) (~1.50 $/month)
1. Self-hosted by using [a Docker image](https://actualbudget.org/docs/install/docker)
1. Local-only apps - [downloadable Windows, Mac and Linux apps](https://actualbudget.org/download/) you can run on your device
### The easy way: using a server (recommended)
Learn more in the [installation instructions docs](https://actualbudget.org/docs/install/).
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
## Ready to Start Budgeting?
Read about [Envelope budgeting](https://actualbudget.org/docs/getting-started/envelope-budgeting) to know more about the idea behind Actual Budget.
### Are you new to budgeting or want to start fresh?
Check out the community's [Starting Fresh](https://actualbudget.org/docs/getting-started/starting-fresh) guide so you can quickly get up and running!
### Are you migrating from other budgeting apps?
Check out the community's [Migration](https://actualbudget.org/docs/migration/) guide to start jumping on the Actual Budget train!
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.org/docs/install/local)
## Documentation
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.org/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
## Contributing
Actual is a community driven product. Learn more about [contributing to Actual](https://actualbudget.org/docs/contributing/).
### Code structure
## Code structure
The Actual app is split up into a few packages:
@@ -57,23 +39,15 @@ The Actual app is split up into a few packages:
More information on the project structure is available in our [community documentation](https://actualbudget.org/docs/contributing/project-details).
### Feature Requests
## Feature Requests
Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc).
Vote for your favorite requests by reacting :+1: to the top comment of the request.
To add new feature requests, open a new Issue of the "Feature Request" type.
### Translation
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
## Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/e20537dd8b74956f86736726ccfbc6f0565bec22.svg 'Repobeats analytics image')
## 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

@@ -34,9 +34,11 @@ if [ "$OSTYPE" == "msys" ]; then
fi
fi
yarn rebuild-electron
yarn workspace loot-core build:node
yarn workspace @actual-app/web build --mode=desktop
yarn workspace @actual-app/web build
yarn workspace desktop-electron update-client
@@ -48,10 +50,10 @@ yarn workspace desktop-electron update-client
if [ -f ../../.secret-tokens ]; then
source ../../.secret-tokens
fi
yarn build
yarn build --publish never --arm64 --x64
echo "\nCreated release"
else
SKIP_NOTARIZATION=true yarn build
SKIP_NOTARIZATION=true yarn build --publish never --x64
fi
)

View File

@@ -1,32 +0,0 @@
#!/bin/sh
# See here for more information: https://github.com/actualbudget/actual/tree/master/packages/desktop-client#visual-regression
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
yarn
fi
E2E_START_URL="${E2E_START_URL:-https://localhost:3001}"
VRT_ARGS=""
# Loop through all arguments
while [ $# -gt 0 ]; do
key="$1"
case $key in
--e2e-start-url)
E2E_START_URL="$2"
shift
;;
*)
VRT_ARGS="$VRT_ARGS $1"
;;
esac
shift
done
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 \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -30,42 +30,33 @@
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:api": "yarn workspace @actual-app/api build",
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"test": "yarn workspaces foreach --all --parallel --verbose run test",
"test:debug": "yarn workspaces foreach --all --verbose run test",
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
"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 --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"
"jq": "./node_modules/node-jq/bin/jq"
},
"devDependencies": {
"@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": "^8.57.0",
"eslint": "^8.37.0",
"eslint-config-prettier": "^9.1.0",
"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.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-config-react-app": "7.0.1",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-rulesdir": "^0.2.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.9",
"node-jq": "^4.0.1",
"npm-run-all": "^4.1.5",
"prettier": "3.3.3",
"prettier": "3.2.4",
"source-map-support": "^0.5.21",
"typescript": "^5.5.4",
"typescript-strict-plugin": "^2.4.4"
"typescript": "^5.0.2",
"typescript-strict-plugin": "^2.2.2-beta.2"
},
"resolutions": {
"rollup": "4.9.4"
@@ -73,10 +64,7 @@
"engines": {
"node": ">=18.0.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
},
"packageManager": "yarn@4.3.1",
"packageManager": "yarn@4.0.2",
"browserslist": [
"electron 24.0",
"defaults"

View File

@@ -58,19 +58,6 @@ describe('API CRUD operations', () => {
await api.loadBudget(budgetName);
});
// api: getBudgets
test('getBudgets', async () => {
const budgets = await api.getBudgets();
expect(budgets).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'test-budget',
name: 'Default Test Db',
}),
]),
);
});
// apis: getCategoryGroups, createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
test('CategoryGroups: successfully update category groups', async () => {
const month = '2023-10';
@@ -81,22 +68,28 @@ describe('API CRUD operations', () => {
expect(groups).toEqual(
expect.arrayContaining([
expect.objectContaining({
hidden: false,
hidden: 0,
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
is_income: false,
is_income: 0,
name: 'Usual Expenses',
sort_order: 16384,
tombstone: 0,
}),
expect.objectContaining({
hidden: false,
hidden: 0,
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
is_income: false,
is_income: 0,
name: 'Investments and Savings',
sort_order: 32768,
tombstone: 0,
}),
expect.objectContaining({
hidden: false,
hidden: 0,
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
is_income: true,
is_income: 1,
name: 'Income',
sort_order: 32768,
tombstone: 0,
}),
]),
);
@@ -258,7 +251,7 @@ describe('API CRUD operations', () => {
);
});
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount, getAccountBalance
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount
test('Accounts: successfully complete account operators', async () => {
const accountId1 = await api.createAccount(
{ name: 'test-account1', offbudget: true },
@@ -279,9 +272,6 @@ describe('API CRUD operations', () => {
]),
);
expect(await api.getAccountBalance(accountId1)).toEqual(1000);
expect(await api.getAccountBalance(accountId2)).toEqual(0);
await api.updateAccount(accountId1, { offbudget: false });
await api.closeAccount(accountId1, accountId2, null);
await api.deleteAccount(accountId2);
@@ -356,221 +346,13 @@ describe('API CRUD operations', () => {
);
});
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
test('Rules: successfully update rules', async () => {
await api.createPayee({ name: 'test-payee' });
await api.createPayee({ name: 'test-payee2' });
// create our test rules
const rule = await api.createRule({
stage: 'pre',
conditionsOp: 'and',
conditions: [
{
field: 'payee',
op: 'is',
value: 'test-payee',
},
],
actions: [
{
op: 'set',
field: 'category',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
},
],
});
const rule2 = await api.createRule({
stage: 'pre',
conditionsOp: 'and',
conditions: [
{
field: 'payee',
op: 'is',
value: 'test-payee2',
},
],
actions: [
{
op: 'set',
field: 'category',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
},
],
});
// get existing rules
const rules = await api.getRules();
expect(rules).toEqual(
expect.arrayContaining([
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee2',
}),
]),
conditionsOp: 'and',
id: rule2.id,
stage: 'pre',
}),
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee',
}),
]),
conditionsOp: 'and',
id: rule.id,
stage: 'pre',
}),
]),
);
// get by payee
expect(await api.getPayeeRules('test-payee')).toEqual(
expect.arrayContaining([
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee',
}),
]),
conditionsOp: 'and',
id: rule.id,
stage: 'pre',
}),
]),
);
expect(await api.getPayeeRules('test-payee2')).toEqual(
expect.arrayContaining([
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee2',
}),
]),
conditionsOp: 'and',
id: rule2.id,
stage: 'pre',
}),
]),
);
// update one rule
const updatedRule = {
...rule,
stage: 'post',
conditionsOp: 'or',
};
expect(await api.updateRule(updatedRule)).toEqual(updatedRule);
expect(await api.getRules()).toEqual(
expect.arrayContaining([
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee',
}),
]),
conditionsOp: 'or',
id: rule.id,
stage: 'post',
}),
expect.objectContaining({
actions: expect.arrayContaining([
expect.objectContaining({
field: 'category',
op: 'set',
type: 'id',
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
}),
]),
conditions: expect.arrayContaining([
expect.objectContaining({
field: 'payee',
op: 'is',
type: 'id',
value: 'test-payee2',
}),
]),
conditionsOp: 'and',
id: rule2.id,
stage: 'pre',
}),
]),
);
// delete rules
await api.deleteRule(rules[1].id);
expect(await api.getRules()).toHaveLength(1);
await api.deleteRule(rules[0].id);
expect(await api.getRules()).toHaveLength(0);
});
// apis: addTransactions, getTransactions, importTransactions, updateTransaction, deleteTransaction
test('Transactions: successfully update transactions', async () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
let newTransaction = [
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
{ date: '2023-11-03', imported_id: '11', amount: 100 },
{ date: '2023-11-03', imported_id: '11', amount: 100 },
];
const addResult = await api.addTransactions(accountId, newTransaction, {
@@ -579,11 +361,6 @@ describe('API CRUD operations', () => {
});
expect(addResult).toBe('ok');
expect(await api.getAccountBalance(accountId)).toEqual(200);
expect(
await api.getAccountBalance(accountId, new Date(2023, 10, 2)),
).toEqual(0);
// confirm added transactions exist
let transactions = await api.getTransactions(
accountId,
@@ -598,9 +375,8 @@ describe('API CRUD operations', () => {
expect(transactions).toHaveLength(2);
newTransaction = [
{ date: '2023-12-03', imported_id: '11', amount: 100, notes: 'notes' },
{ date: '2023-12-03', imported_id: '12', amount: 100, notes: 'notes' },
{ date: '2023-12-03', imported_id: '22', amount: 200, notes: '' },
{ date: '2023-12-03', imported_id: '11', amount: 100 },
{ date: '2023-12-03', imported_id: '22', amount: 200 },
];
const reconciled = await api.importTransactions(accountId, newTransaction);
@@ -616,22 +392,9 @@ describe('API CRUD operations', () => {
'2023-12-31',
);
expect(transactions).toEqual(
expect.arrayContaining([
expect.objectContaining({ imported_id: '22', amount: 200 }),
]),
);
expect(transactions).toHaveLength(1);
// confirm imported transactions update perfomed
transactions = await api.getTransactions(
accountId,
'2023-11-01',
'2023-11-30',
);
expect(transactions).toEqual(
expect.arrayContaining([
expect.objectContaining({ notes: 'notes', amount: 100 }),
]),
expect.arrayContaining(
newTransaction.map(trans => expect.objectContaining(trans)),
),
);
expect(transactions).toHaveLength(2);

View File

@@ -31,18 +31,10 @@ export async function downloadBudget(syncId, { password }: { password? } = {}) {
return send('api/download-budget', { syncId, password });
}
export async function getBudgets() {
return send('api/get-budgets');
}
export async function sync() {
return send('api/sync');
}
export async function runBankSync(args?: { accountId: string }) {
return send('api/bank-sync', args);
}
export async function batchBudgetUpdates(func) {
await send('api/batch-budget-start');
try {
@@ -129,10 +121,6 @@ export function deleteAccount(id) {
return send('api/account-delete', { id });
}
export function getAccountBalance(id, cutoff?) {
return send('api/account-balance', { id, cutoff });
}
export function getCategoryGroups() {
return send('api/category-groups-get');
}
@@ -165,10 +153,6 @@ export function deleteCategory(id, transferCategoryId?) {
return send('api/category-delete', { id, transferCategoryId });
}
export function getCommonPayees() {
return send('api/common-payees-get');
}
export function getPayees() {
return send('api/payees-get');
}
@@ -184,35 +168,3 @@ export function updatePayee(id, fields) {
export function deletePayee(id) {
return send('api/payee-delete', { id });
}
export function mergePayees(targetId, mergeIds) {
return send('api/payees-merge', { targetId, mergeIds });
}
export function getRules() {
return send('api/rules-get');
}
export function getPayeeRules(id) {
return send('api/payee-rules-get', { id });
}
export function createRule(rule) {
return send('api/rule-create', { rule });
}
export function updateRule(rule) {
return send('api/rule-update', { rule });
}
export function deleteRule(id: string) {
return send('api/rule-delete', id);
}
export function holdBudgetForNextMonth(month, amount) {
return send('api/budget-hold-for-next-month', { month, amount });
}
export function resetBudgetHold(month) {
return send('api/budget-reset-hold', { month });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "24.11.0",
"version": "6.7.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
@@ -9,8 +9,7 @@
"main": "dist/index.js",
"types": "@types/index.d.ts",
"files": [
"dist",
"@types"
"dist"
],
"scripts": {
"build:app": "yarn workspace loot-core build:api",
@@ -22,19 +21,18 @@
"clean": "rm -rf dist @types"
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^9.6.0",
"better-sqlite3": "^9.3.0",
"compare-versions": "^6.1.0",
"node-fetch": "^3.3.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@swc/core": "^1.5.3",
"@swc/jest": "^0.2.36",
"@swc/core": "^1.3.105",
"@swc/jest": "^0.2.31",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.2",
"jest": "^27.5.1",
"tsc-alias": "^1.8.8",
"typescript": "^5.5.4"
"typescript": "^5.0.2"
}
}

View File

@@ -20,12 +20,12 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@swc/core": "^1.5.3",
"@swc/jest": "^0.2.36",
"@swc/core": "^1.3.105",
"@swc/jest": "^0.2.31",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.2",
"jest": "^27.5.1",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.5.4"
"typescript": "^5.0.2"
}
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as merkle from './merkle';
import { Timestamp } from './timestamp';

View File

@@ -134,7 +134,6 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
node2 = node2[diffkey] || emptyTrie();
}
// eslint-disable-next-line no-unreachable
return null;
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Timestamp } from './timestamp';
describe('Timestamp', function () {

View File

@@ -154,7 +154,7 @@ export class Timestamp {
/**
* maximum timestamp
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
static max = Timestamp.parse(
'9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF',
)!;
@@ -294,7 +294,7 @@ export class Timestamp {
/**
* zero/minimum timestamp
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
static zero = Timestamp.parse(
'1970-01-01T00:00:00.000Z-0000-0000000000000000',
)!;

View File

@@ -6,7 +6,6 @@ node_modules
# testing
coverage
test-results
playwright-report
# production
build

View File

@@ -45,22 +45,7 @@ HTTPS=true docker compose up --build
Note the network IP address and port the dev instance is listening on.
Next, navigate to the root of your project folder, run the standardized docker container, and launch the visual regression tests from within it.
Run via yarn:
```sh
# By default, this connects to https://localhost:3001
yarn vrt:docker
# To use a different ip and port:
yarn vrt:docker --e2e-start-url https://ip:port
# To update snapshots, use the following command:
yarn vrt:docker --e2e-start-url https://ip:port --update-snapshots
```
Run manually:
Next, navigate to the root of your project folder, run the standartised docker container, and launch the visual regression tests from within it.
```sh
# Run docker container
@@ -69,8 +54,7 @@ docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
# Use the ip and port noted earlier
# Run the VRT tests: important - they MUST be ran against a HTTPS server. Use the ip and port noted earlier
E2E_START_URL=https://ip:port yarn vrt
# To update snapshots, use the following command:
@@ -81,15 +65,6 @@ E2E_START_URL=https://ip:port yarn vrt
You can also run the tests against a remote server by passing the URL:
Run in standardized docker container:
```sh
E2E_START_URL=https://my-remote-server.com yarn vrt:docker
# Or pass in server URL as argument
yarn vrt:docker --e2e-start-url https://my-remote-server.com
```
Run locally:
```sh
E2E_START_URL=https://my-remote-server.com yarn vrt
```

View File

@@ -1,57 +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();
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();
const accountPage = await accountsPage.openNthAccount(0);
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.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();
});
@@ -101,60 +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,
});
});
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: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 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: 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