Compare commits
112 Commits
cursor/pro
...
Transactio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ededcd854 | ||
|
|
fbc6a42662 | ||
|
|
247b0d970a | ||
|
|
b29a12799c | ||
|
|
b3c62fd69d | ||
|
|
cbac6116d4 | ||
|
|
e83cfba357 | ||
|
|
0cac66b203 | ||
|
|
7983ee45e1 | ||
|
|
844cd3433a | ||
|
|
ae6bea2b15 | ||
|
|
37481535e7 | ||
|
|
45a4f0a40d | ||
|
|
9a3e33c0d7 | ||
|
|
25d072944e | ||
|
|
cf8a4b6e6a | ||
|
|
55b1ed170b | ||
|
|
b266ebf1ea | ||
|
|
6826ca0e4b | ||
|
|
80aee4ee71 | ||
|
|
ab4aa21343 | ||
|
|
9dd0284e31 | ||
|
|
dcc879294c | ||
|
|
002f74a8fa | ||
|
|
b6f80c26e6 | ||
|
|
ee71130d56 | ||
|
|
6ed18d8f8c | ||
|
|
1737674b9e | ||
|
|
e4617e8cd4 | ||
|
|
57d01467ca | ||
|
|
8019d9f61b | ||
|
|
ddbefc790e | ||
|
|
7eaf23eb7c | ||
|
|
8f284e7b60 | ||
|
|
be35328e42 | ||
|
|
23f1b7d3c0 | ||
|
|
8b1aa6fb93 | ||
|
|
155558ee62 | ||
|
|
94332016e8 | ||
|
|
0af2c6c2fb | ||
|
|
97482a082d | ||
|
|
31a9ba629b | ||
|
|
7c19a6333c | ||
|
|
2bce9d707c | ||
|
|
4e42cda29e | ||
|
|
0ce07d7692 | ||
|
|
0e3415c145 | ||
|
|
19675a7de6 | ||
|
|
5a1ceed7d9 | ||
|
|
86c1c30c97 | ||
|
|
96ac1292f9 | ||
|
|
328dfae8bf | ||
|
|
37247395e2 | ||
|
|
02b0e24d6e | ||
|
|
dfaa75f1cf | ||
|
|
adae3e4352 | ||
|
|
7a3794295f | ||
|
|
c1d97fcc75 | ||
|
|
3715f16888 | ||
|
|
edad7ce0e3 | ||
|
|
737341ffb6 | ||
|
|
6147495003 | ||
|
|
614bedcfbf | ||
|
|
248b1034d7 | ||
|
|
4f88afa266 | ||
|
|
5c23aad3c2 | ||
|
|
5b9bcc94f6 | ||
|
|
87d54251cd | ||
|
|
2e439aacba | ||
|
|
244140314c | ||
|
|
f7b40fca64 | ||
|
|
dc811552be | ||
|
|
295839ebbb | ||
|
|
99ca34458e | ||
|
|
90ac8d8520 | ||
|
|
52aeec2d59 | ||
|
|
0c280d60f6 | ||
|
|
148ca92584 | ||
|
|
90e848ebe8 | ||
|
|
b034d5039f | ||
|
|
5ac29473f2 | ||
|
|
3b0db2bed7 | ||
|
|
7a886810bc | ||
|
|
8bf0997275 | ||
|
|
2f965266ab | ||
|
|
499f24f7fd | ||
|
|
4c5be62f56 | ||
|
|
1446c7d93f | ||
|
|
ad9980307e | ||
|
|
d4ad31fb0c | ||
|
|
05355788e4 | ||
|
|
805e2b1807 | ||
|
|
e54dc0c1ca | ||
|
|
e1c2f0a181 | ||
|
|
cc2e329e8e | ||
|
|
71f849d1e1 | ||
|
|
0ea8bc1fb4 | ||
|
|
f0c7953c0b | ||
|
|
4cf5f9b183 | ||
|
|
80fd997540 | ||
|
|
da93ddf63b | ||
|
|
7846d2e787 | ||
|
|
ca6d80461a | ||
|
|
fa14cbb697 | ||
|
|
1210a74b4a | ||
|
|
534c1e6680 | ||
|
|
14d436712a | ||
|
|
e9f3925124 | ||
|
|
f28229be99 | ||
|
|
1fc922c672 | ||
|
|
c712217a7c | ||
|
|
3559b2df3a |
@@ -1,12 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
When running yarn commands - always run them in the root directory. Do not run them in child workspaces.
|
||||
|
||||
The following commands can be useful:
|
||||
|
||||
- `yarn typecheck` to run typechecker
|
||||
- `yarn lint` to run the code linter and formatter
|
||||
- `yarn lint:fix` to fix some of the code lint issues (running this is preferred over `yarn lint`)
|
||||
- `yarn test` to run all the tests
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: *.ts,*.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
You are an expert in TypeScript and React.
|
||||
|
||||
Code Style and Structure
|
||||
|
||||
- Write concise, technical TypeScript code.
|
||||
- Use functional and declarative programming patterns; avoid classes.
|
||||
- Prefer iteration and modularization over code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
|
||||
- Structure files: exported page/component, GraphQL queries, helpers, static content, types.
|
||||
- When creating a new component, place it in its own file rather than grouping multiple components in a single file.
|
||||
|
||||
Naming Conventions
|
||||
|
||||
- Favor named exports for components and utilities.
|
||||
|
||||
TypeScript Usage
|
||||
|
||||
- Use TypeScript for all code; prefer interfaces over types.
|
||||
- Avoid enums; use objects or maps instead.
|
||||
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
|
||||
- Avoid type assertions with `as` or `!`; prefer using `satisfies`.
|
||||
|
||||
Syntax and Formatting
|
||||
|
||||
- Use the "function" keyword for pure functions.
|
||||
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
|
||||
- Use declarative JSX, keeping JSX minimal and readable.
|
||||
|
||||
Change validation
|
||||
|
||||
- Run `yarn typecheck` in the root directory to validate that the generated TypeScript code is correct
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Vitest test runner is used for unit tests.
|
||||
|
||||
When running unit tests, always include the flag `--watch=false` to prevent watch mode.
|
||||
|
||||
To run unit tests for a specific package in the monorepo, use the following command:
|
||||
|
||||
`yarn workspace <workspaceNameFromPackageJson> run test <pathToTest>`
|
||||
|
||||
Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better.
|
||||
3
.cursor/worktrees.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"setup-worktree": ["yarn"]
|
||||
}
|
||||
12
.github/actions/setup/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
@@ -32,6 +32,16 @@ runs:
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Ensure Lage cache directory exists
|
||||
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
shell: bash
|
||||
- name: Cache Lage
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
lage-${{ runner.os }}-
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
|
||||
6
.github/workflows/build.yml
vendored
@@ -24,6 +24,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build API
|
||||
run: cd packages/api && yarn build
|
||||
- name: Create package tgz
|
||||
@@ -40,6 +42,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build CRDT
|
||||
run: cd packages/crdt && yarn build
|
||||
- name: Create package tgz
|
||||
@@ -75,6 +79,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build Server
|
||||
run: yarn workspace @actual-app/sync-server build
|
||||
- name: Upload Build
|
||||
|
||||
10
.github/workflows/check.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
typecheck:
|
||||
@@ -25,6 +27,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
validate-cli:
|
||||
@@ -33,6 +37,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
- name: Check that the built CLI works
|
||||
@@ -43,6 +49,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
@@ -53,6 +61,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
15
.github/workflows/docker-edge.yml
vendored
@@ -1,21 +1,16 @@
|
||||
name: Build Edge Docker Image
|
||||
|
||||
# Edge Docker images are built for every commit, and daily
|
||||
# Edge Docker images are built for every push to master
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docker-edge-build
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
10
.github/workflows/e2e-test.yml
vendored
@@ -32,11 +32,13 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Run E2E Tests on Netlify URL
|
||||
run: yarn e2e
|
||||
env:
|
||||
@@ -53,11 +55,13 @@ jobs:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Run Desktop app E2E Tests
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||
@@ -74,7 +78,7 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
|
||||
7
.github/workflows/electron-master.yml
vendored
@@ -44,9 +44,9 @@ jobs:
|
||||
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
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron for Mac
|
||||
@@ -57,6 +57,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
|
||||
6
.github/workflows/electron-pr.yml
vendored
@@ -39,9 +39,9 @@ jobs:
|
||||
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
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Handle feature requests
|
||||
run: node .github/actions/handle-feature-requests.js
|
||||
env:
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
|
||||
2
.github/workflows/publish-npm-packages.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
|
||||
2
.github/workflows/size-compare.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: base
|
||||
|
||||
@@ -62,6 +63,7 @@ jobs:
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
|
||||
119
.github/workflows/update-vrt.yml
vendored
@@ -1,119 +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.52.0-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: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
- 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
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
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:${BRANCH_NAME}
|
||||
- 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'
|
||||
156
.github/workflows/vrt-update-apply.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
name: VRT Update - Apply
|
||||
# SECURITY: This workflow runs in trusted base repo context.
|
||||
# It treats the patch artifact as untrusted data, validates it contains only PNGs,
|
||||
# and safely applies it to the contributor's fork branch.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['VRT Update - Generate']
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
apply-vrt-updates:
|
||||
name: Apply VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download patch artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-patch-*
|
||||
path: /tmp/artifacts
|
||||
|
||||
- name: Download metadata artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-metadata-*
|
||||
path: /tmp/metadata
|
||||
|
||||
- name: Extract metadata
|
||||
id: metadata
|
||||
run: |
|
||||
# Find the metadata directory (will be vrt-metadata-{PR_NUMBER})
|
||||
METADATA_DIR=$(find /tmp/metadata -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
|
||||
if [ -z "$METADATA_DIR" ]; then
|
||||
echo "No metadata found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER=$(cat "$METADATA_DIR/pr-number.txt")
|
||||
HEAD_REF=$(cat "$METADATA_DIR/head-ref.txt")
|
||||
HEAD_REPO=$(cat "$METADATA_DIR/head-repo.txt")
|
||||
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Found PR #$PR_NUMBER: $HEAD_REPO @ $HEAD_REF"
|
||||
|
||||
- name: Checkout fork branch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.metadata.outputs.head_repo }}
|
||||
ref: ${{ steps.metadata.outputs.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate and apply patch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
id: apply
|
||||
run: |
|
||||
# Find the patch file
|
||||
PATCH_DIR=$(find /tmp/artifacts -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
PATCH_FILE="$PATCH_DIR/vrt-update.patch"
|
||||
|
||||
if [ ! -f "$PATCH_FILE" ]; then
|
||||
echo "No patch file found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found patch file: $PATCH_FILE"
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
echo "Validating patch contains only PNG files..."
|
||||
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files! Rejecting for security."
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch validation failed: contains non-PNG files" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract file list for verification
|
||||
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
|
||||
echo "Patch modifies $FILES_CHANGED PNG file(s)"
|
||||
|
||||
# Configure git
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Apply patch
|
||||
echo "Applying patch..."
|
||||
if git apply --check "$PATCH_FILE" 2>&1; then
|
||||
git apply "$PATCH_FILE"
|
||||
|
||||
# Stage only PNG files (extra safety)
|
||||
git add "**/*.png"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes after applying patch"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit
|
||||
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${{ steps.metadata.outputs.pr_number }}"
|
||||
|
||||
echo "applied=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Patch could not be applied cleanly"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch conflicts with current branch state" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
env:
|
||||
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
|
||||
HEAD_REPO: ${{ steps.metadata.outputs.head_repo }}
|
||||
run: |
|
||||
git push origin "HEAD:refs/heads/$HEAD_REF"
|
||||
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
|
||||
|
||||
- name: Comment on PR - Success
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '✅ VRT screenshots have been automatically updated.'
|
||||
});
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `❌ Failed to apply VRT updates: ${error}\n\nPlease check the workflow logs for details.`
|
||||
});
|
||||
105
.github/workflows/vrt-update-generate.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: VRT Update - Generate
|
||||
# SECURITY: This workflow runs in untrusted fork context with no write permissions.
|
||||
# It only generates VRT patch artifacts that are later applied by vrt-update-apply.yml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '.github/workflows/vrt-update-generate.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
generate-vrt-updates:
|
||||
name: Generate VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
|
||||
- name: Create patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
# Trust the repository directory (required for container environments)
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Stage only PNG files
|
||||
git add "**/*.png"
|
||||
|
||||
# Check if there are any changes
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Create commit and patch
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Patch created successfully with PNG changes only"
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-patch-${{ github.event.pull_request.number }}
|
||||
path: vrt-update.patch
|
||||
retention-days: 5
|
||||
|
||||
- name: Save PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
run: |
|
||||
mkdir -p pr-metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.ref }}" > pr-metadata/head-ref.txt
|
||||
echo "${{ github.event.pull_request.head.repo.full_name }}" > pr-metadata/head-repo.txt
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-metadata-${{ github.event.pull_request.number }}
|
||||
path: pr-metadata/
|
||||
retention-days: 5
|
||||
8
.gitignore
vendored
@@ -7,9 +7,6 @@ Actual-*
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
# Secrets
|
||||
.secret-tokens
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
@@ -26,6 +23,8 @@ packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
packages/desktop-client/service-worker
|
||||
packages/plugins-service/dist
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
@@ -63,3 +62,6 @@ build/
|
||||
|
||||
# .d.ts files aren't type-checked with skipLibCheck set to true
|
||||
*.d.ts
|
||||
|
||||
# Lage cache
|
||||
.lage/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export APPLE_ID=example@email.com
|
||||
export APPLE_APP_SPECIFIC_PASSWORD=password
|
||||
942
.yarn/releases/yarn-4.10.3.cjs
vendored
Executable file
948
.yarn/releases/yarn-4.9.1.cjs
vendored
@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
585
AGENTS.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# AGENTS.md - Guide for AI Agents Working with Actual Budget
|
||||
|
||||
This guide provides comprehensive information for AI agents (like Cursor) working with the Actual Budget codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Actual Budget** is a local-first personal finance tool written in TypeScript/JavaScript. It's 100% free and open-source with synchronization capabilities across devices.
|
||||
|
||||
- **Repository**: https://github.com/actualbudget/actual
|
||||
- **Community Docs**: https://github.com/actualbudget/docs or https://actualbudget.org/docs
|
||||
- **License**: MIT
|
||||
- **Primary Language**: TypeScript (with React)
|
||||
- **Build System**: Yarn 4 workspaces (monorepo)
|
||||
|
||||
## Quick Start Commands
|
||||
|
||||
### Essential Commands (Run from Root)
|
||||
|
||||
```bash
|
||||
# Type checking (ALWAYS run before committing)
|
||||
yarn typecheck
|
||||
|
||||
# Linting and formatting (with auto-fix)
|
||||
yarn lint:fix
|
||||
|
||||
# Run all tests
|
||||
yarn test
|
||||
|
||||
# Start development server (browser)
|
||||
yarn start
|
||||
|
||||
# Start with sync server
|
||||
yarn start:server-dev
|
||||
|
||||
# Start desktop app development
|
||||
yarn start:desktop
|
||||
```
|
||||
|
||||
### Important Rules
|
||||
|
||||
- **ALWAYS run yarn commands from the root directory** - never run them in child workspaces
|
||||
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
|
||||
- Tests run once and exit by default (using `vitest --run`)
|
||||
|
||||
### Task Orchestration with Lage
|
||||
|
||||
The project uses **[lage](https://microsoft.github.io/lage/)** (a task runner for JavaScript monorepos) to efficiently run tests and other tasks across multiple workspaces:
|
||||
|
||||
- **Parallel execution**: Runs tests in parallel across workspaces for faster feedback
|
||||
- **Smart caching**: Caches test results to skip unchanged packages (cached in `.lage/` directory)
|
||||
- **Dependency awareness**: Understands workspace dependencies and execution order
|
||||
- **Continues on error**: Uses `--continue` flag to run all packages even if one fails
|
||||
|
||||
**Lage Commands:**
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages
|
||||
yarn test # Equivalent to: lage test --continue
|
||||
|
||||
# Run tests without cache (for debugging/CI)
|
||||
yarn test:debug # Equivalent to: lage test --no-cache --continue
|
||||
```
|
||||
|
||||
Configuration is in `lage.config.js` at the project root.
|
||||
|
||||
## Architecture & Package Structure
|
||||
|
||||
### Core Packages
|
||||
|
||||
#### 1. **loot-core** (`packages/loot-core/`)
|
||||
|
||||
The core application logic that runs on any platform.
|
||||
|
||||
- Business logic, database operations, and calculations
|
||||
- Platform-agnostic code
|
||||
- Exports for both browser and node environments
|
||||
- Test commands:
|
||||
|
||||
```bash
|
||||
# Run all loot-core tests
|
||||
yarn workspace loot-core run test
|
||||
|
||||
# Or run tests across all packages using lage
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### 2. **desktop-client** (`packages/desktop-client/` - aliased as `@actual-app/web`)
|
||||
|
||||
The React-based UI for web and desktop.
|
||||
|
||||
- React components using functional programming patterns
|
||||
- E2E tests using Playwright
|
||||
- Vite for bundling
|
||||
- Commands:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn workspace @actual-app/web start:browser
|
||||
|
||||
# Build
|
||||
yarn workspace @actual-app/web build
|
||||
|
||||
# E2E tests
|
||||
yarn workspace @actual-app/web e2e
|
||||
|
||||
# Visual regression tests
|
||||
yarn workspace @actual-app/web vrt
|
||||
```
|
||||
|
||||
#### 3. **desktop-electron** (`packages/desktop-electron/`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
- Window management and native OS integration
|
||||
- E2E tests for Electron-specific features
|
||||
|
||||
#### 4. **api** (`packages/api/` - aliased as `@actual-app/api`)
|
||||
|
||||
Public API for programmatic access to Actual.
|
||||
|
||||
- Node.js API
|
||||
- Designed for integrations and automation
|
||||
- Commands:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
yarn workspace @actual-app/api build
|
||||
|
||||
# Run tests
|
||||
yarn workspace @actual-app/api test
|
||||
|
||||
# Or use lage to run all tests
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
|
||||
|
||||
Synchronization server for multi-device support.
|
||||
|
||||
- Express-based server
|
||||
- Currently transitioning to TypeScript (mostly JavaScript)
|
||||
- Commands:
|
||||
```bash
|
||||
yarn workspace @actual-app/sync-server start
|
||||
```
|
||||
|
||||
#### 6. **component-library** (`packages/component-library/` - aliased as `@actual-app/components`)
|
||||
|
||||
Reusable React UI components.
|
||||
|
||||
- Shared components like Button, Input, Menu, etc.
|
||||
- Theme system and design tokens
|
||||
- Icons (375+ icons in SVG/TSX format)
|
||||
|
||||
#### 7. **crdt** (`packages/crdt/` - aliased as `@actual-app/crdt`)
|
||||
|
||||
CRDT (Conflict-free Replicated Data Type) implementation for data synchronization.
|
||||
|
||||
- Protocol buffers for serialization
|
||||
- Core sync logic
|
||||
|
||||
#### 8. **plugins-service** (`packages/plugins-service/`)
|
||||
|
||||
Service for handling plugins/extensions.
|
||||
|
||||
#### 9. **eslint-plugin-actual** (`packages/eslint-plugin-actual/`)
|
||||
|
||||
Custom ESLint rules specific to Actual.
|
||||
|
||||
- `no-untranslated-strings`: Enforces i18n usage
|
||||
- `prefer-trans-over-t`: Prefers Trans component over t() function
|
||||
- `prefer-logger-over-console`: Enforces using logger instead of console
|
||||
- `typography`: Typography rules
|
||||
- `prefer-if-statement`: Prefers explicit if statements
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Making Changes
|
||||
|
||||
When implementing changes:
|
||||
|
||||
1. Read relevant files to understand current implementation
|
||||
2. Make focused, incremental changes
|
||||
3. Run type checking: `yarn typecheck`
|
||||
4. Run linting: `yarn lint:fix`
|
||||
5. Run relevant tests
|
||||
6. Fix any linter errors that are introduced
|
||||
|
||||
### 2. Testing Strategy
|
||||
|
||||
**Unit Tests (Vitest)**
|
||||
|
||||
The project uses **lage** for running tests across all workspaces efficiently.
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages (using lage)
|
||||
yarn test
|
||||
|
||||
# Run tests without cache (for debugging)
|
||||
yarn test:debug
|
||||
|
||||
# Run tests for a specific package
|
||||
yarn workspace loot-core run test
|
||||
|
||||
# Run a specific test file (watch mode)
|
||||
yarn workspace loot-core run test path/to/test.test.ts
|
||||
```
|
||||
|
||||
**E2E Tests (Playwright)**
|
||||
|
||||
```bash
|
||||
# Run E2E tests for web
|
||||
yarn e2e
|
||||
|
||||
# Desktop Electron E2E (includes full build)
|
||||
yarn e2e:desktop
|
||||
|
||||
# Visual regression tests
|
||||
yarn vrt
|
||||
|
||||
# Visual regression in Docker (consistent environment)
|
||||
yarn vrt:docker
|
||||
|
||||
# Run E2E tests for a specific package
|
||||
yarn workspace @actual-app/web e2e
|
||||
```
|
||||
|
||||
**Testing Best Practices:**
|
||||
|
||||
- Minimize mocked dependencies - prefer real implementations
|
||||
- Use descriptive test names
|
||||
- Vitest globals are available: `describe`, `it`, `expect`, `beforeEach`, etc.
|
||||
- For sync-server tests, globals are explicitly defined in config
|
||||
|
||||
### 3. Type Checking
|
||||
|
||||
TypeScript configuration uses:
|
||||
|
||||
- Incremental compilation
|
||||
- Strict type checking with `typescript-strict-plugin`
|
||||
- Platform-specific exports in `loot-core` (node vs browser)
|
||||
|
||||
Always run `yarn typecheck` before committing.
|
||||
|
||||
### 4. Internationalization (i18n)
|
||||
|
||||
- Use `Trans` component instead of `t()` function when possible
|
||||
- All user-facing strings must be translated
|
||||
- Generate i18n files: `yarn generate:i18n`
|
||||
- Custom ESLint rules enforce translation usage
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
**Type Usage:**
|
||||
|
||||
- Use TypeScript for all code
|
||||
- Prefer `type` over `interface`
|
||||
- Avoid `enum` - use objects or maps
|
||||
- Avoid `any` or `unknown` unless absolutely necessary
|
||||
- Look for existing type definitions in the codebase
|
||||
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
|
||||
- Use inline type imports: `import { type MyType } from '...'`
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., `isLoaded`, `hasError`)
|
||||
- Named exports for components and utilities (avoid default exports except in specific cases)
|
||||
|
||||
**Code Structure:**
|
||||
|
||||
- Functional and declarative programming patterns - avoid classes
|
||||
- Use the `function` keyword for pure functions
|
||||
- Prefer iteration and modularization over code duplication
|
||||
- Structure files: exported component/page, helpers, static content, types
|
||||
- Create new components in their own files
|
||||
|
||||
**React Patterns:**
|
||||
|
||||
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
|
||||
- Don't use `React.*` patterns - use named imports instead
|
||||
- Use `<Link>` instead of `<a>` tags
|
||||
- Use custom hooks from `src/hooks` (not react-router directly):
|
||||
- `useNavigate()` from `src/hooks` (not react-router)
|
||||
- `useDispatch()`, `useSelector()`, `useStore()` from `src/redux` (not react-redux)
|
||||
- Avoid unstable nested components
|
||||
- Use `satisfies` for type narrowing
|
||||
|
||||
**JSX Style:**
|
||||
|
||||
- Declarative JSX, minimal and readable
|
||||
- Avoid unnecessary curly braces in conditionals
|
||||
- Use concise syntax for simple statements
|
||||
- Prefer explicit expressions (`condition && <Component />`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically organized by ESLint with the following order:
|
||||
|
||||
1. React imports (first)
|
||||
2. Built-in Node.js modules
|
||||
3. External packages
|
||||
4. Actual packages (`loot-core`, `@actual-app/components` - legacy pattern `loot-design` may appear in old code)
|
||||
5. Parent imports
|
||||
6. Sibling imports
|
||||
7. Index imports
|
||||
|
||||
Always maintain newlines between import groups.
|
||||
|
||||
### Platform-Specific Code
|
||||
|
||||
- Don't directly reference platform-specific imports (`.api`, `.web`, `.electron`)
|
||||
- Use conditional exports in `loot-core` for platform-specific code
|
||||
- Platform resolution happens at build time via package.json exports
|
||||
|
||||
### Restricted Patterns
|
||||
|
||||
**Never:**
|
||||
|
||||
- Use `console.*` (use logger instead - enforced by ESLint)
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
- Never update git config
|
||||
- Never run destructive git operations (force push, hard reset) unless explicitly requested
|
||||
- Never skip hooks (--no-verify, --no-gpg-sign)
|
||||
- Never force push to main/master
|
||||
- Never commit unless explicitly asked
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
### Typical Component File
|
||||
|
||||
```typescript
|
||||
import { type ComponentType } from 'react';
|
||||
// ... other imports
|
||||
|
||||
type MyComponentProps = {
|
||||
// Props definition
|
||||
};
|
||||
|
||||
export function MyComponent({ prop1, prop2 }: MyComponentProps) {
|
||||
// Component logic
|
||||
return (
|
||||
// JSX
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Test File
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
// ... imports
|
||||
|
||||
describe('ComponentName', () => {
|
||||
it('should behave as expected', () => {
|
||||
// Test logic
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Important Directories & Files
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- `/package.json` - Root workspace configuration, scripts
|
||||
- `/lage.config.js` - Lage task runner configuration
|
||||
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
||||
- `/tsconfig.json` - Root TypeScript configuration
|
||||
- `/.cursorignore`, `/.gitignore` - Ignored files
|
||||
- `/yarn.lock` - Dependency lockfile (Yarn 4)
|
||||
|
||||
### Documentation
|
||||
|
||||
- `/README.md` - Project overview
|
||||
- `/CONTRIBUTING.md` - Points to community docs
|
||||
- `/upcoming-release-notes/` - Release notes for next version
|
||||
- `/CODEOWNERS` - Code ownership definitions
|
||||
|
||||
### Build Artifacts (Don't Edit)
|
||||
|
||||
- `packages/*/lib-dist/` - Built output
|
||||
- `packages/*/dist/` - Built output
|
||||
- `packages/*/build/` - Built output
|
||||
- `packages/desktop-client/playwright-report/` - Test reports
|
||||
- `packages/desktop-client/test-results/` - Test results
|
||||
- `.lage/` - Lage task runner cache (improves test performance)
|
||||
|
||||
### Key Source Directories
|
||||
|
||||
- `packages/loot-core/src/client/` - Client-side core logic
|
||||
- `packages/loot-core/src/server/` - Server-side core logic
|
||||
- `packages/loot-core/src/shared/` - Shared utilities
|
||||
- `packages/loot-core/src/types/` - Type definitions
|
||||
- `packages/desktop-client/src/components/` - React components
|
||||
- `packages/desktop-client/src/hooks/` - Custom React hooks
|
||||
- `packages/desktop-client/e2e/` - End-to-end tests
|
||||
- `packages/component-library/src/` - Reusable components
|
||||
- `packages/component-library/src/icons/` - Icon components (auto-generated, don't edit)
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages (recommended)
|
||||
yarn test
|
||||
|
||||
# Unit test for a specific file in loot-core (watch mode)
|
||||
yarn workspace loot-core run test src/path/to/file.test.ts
|
||||
|
||||
# E2E test for a specific file
|
||||
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Browser build
|
||||
yarn build:browser
|
||||
|
||||
# Desktop build
|
||||
yarn build:desktop
|
||||
|
||||
# API build
|
||||
yarn build:api
|
||||
|
||||
# Sync server build
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
### Type Checking Specific Packages
|
||||
|
||||
TypeScript uses project references. Run `yarn typecheck` from root to check all packages.
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
```bash
|
||||
# Run tests in debug mode (without parallelization)
|
||||
yarn test:debug
|
||||
|
||||
# Run specific E2E test with headed browser
|
||||
yarn workspace @actual-app/web run playwright test --headed --debug accounts.test.ts
|
||||
```
|
||||
|
||||
### Working with Icons
|
||||
|
||||
Icons in `packages/component-library/src/icons/` are auto-generated. Don't manually edit them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Type Errors
|
||||
|
||||
1. Run `yarn typecheck` to see all type errors
|
||||
2. Check if types are imported correctly
|
||||
3. Look for existing type definitions in `packages/loot-core/src/types/`
|
||||
4. Use `satisfies` instead of `as` for type narrowing
|
||||
|
||||
### Linter Errors
|
||||
|
||||
1. Run `yarn lint:fix` to auto-fix many issues
|
||||
2. Check ESLint output for specific rule violations
|
||||
3. Custom rules:
|
||||
- `actual/no-untranslated-strings` - Add i18n
|
||||
- `actual/prefer-trans-over-t` - Use Trans component
|
||||
- `actual/prefer-logger-over-console` - Use logger
|
||||
- Check `eslint.config.mjs` for complete rules
|
||||
|
||||
### Test Failures
|
||||
|
||||
1. Check if test is running in correct environment (node vs web)
|
||||
2. For Vitest: check `vitest.config.ts` or `vitest.web.config.ts`
|
||||
3. For Playwright: check `playwright.config.ts`
|
||||
4. Ensure mock minimization - prefer real implementations
|
||||
5. **Lage cache issues**: Clear cache with `rm -rf .lage` if tests behave unexpectedly
|
||||
6. **Tests continue on error**: With `--continue` flag, all packages run even if one fails
|
||||
|
||||
### Import Resolution Issues
|
||||
|
||||
1. Check `tsconfig.json` for path mappings
|
||||
2. Check package.json `exports` field (especially for loot-core)
|
||||
3. Verify platform-specific imports (`.web`, `.electron`, `.api`)
|
||||
4. Use absolute imports in `desktop-client` (enforced by ESLint)
|
||||
|
||||
### Build Failures
|
||||
|
||||
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
|
||||
2. Reinstall dependencies: `yarn install`
|
||||
3. Check Node.js version (requires >=20)
|
||||
4. Check Yarn version (requires ^4.9.1)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Located alongside source files or in `__tests__` directories
|
||||
- Use `.test.ts`, `.test.tsx`, `.spec.js` extensions
|
||||
- Vitest is the test runner
|
||||
- Minimize mocking - prefer real implementations
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Located in `packages/desktop-client/e2e/`
|
||||
- Use Playwright test runner
|
||||
- Visual regression snapshots in `*-snapshots/` directories
|
||||
- Page models in `e2e/page-models/` for reusable page interactions
|
||||
- Mobile tests have `.mobile.test.ts` suffix
|
||||
|
||||
### Visual Regression Tests (VRT)
|
||||
|
||||
- Snapshots stored per test file in `*-snapshots/` directories
|
||||
- Use Docker for consistent environment: `yarn vrt:docker`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Community Documentation**: https://actualbudget.org/docs/contributing/
|
||||
- **Discord Community**: https://discord.gg/pRYNYr4W5A
|
||||
- **GitHub Issues**: https://github.com/actualbudget/actual/issues
|
||||
- **Feature Requests**: Label "needs votes" sorted by reactions
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] No new console.\* usage (use logger)
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Prefer `type` over `interface`
|
||||
- [ ] Named exports used (not default exports)
|
||||
- [ ] Imports are properly ordered
|
||||
- [ ] Platform-specific code uses proper exports
|
||||
- [ ] No unnecessary type assertions
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
When creating pull requests:
|
||||
|
||||
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Bundle Size**: Check with rollup-plugin-visualizer
|
||||
- **Type Checking**: Uses incremental compilation
|
||||
- **Testing**: Tests run in parallel by default
|
||||
- **Linting**: ESLint caches results for faster subsequent runs
|
||||
|
||||
## Workspace Commands Reference
|
||||
|
||||
```bash
|
||||
# List all workspaces
|
||||
yarn workspaces list
|
||||
|
||||
# Run command in specific workspace
|
||||
yarn workspace <workspace-name> run <command>
|
||||
|
||||
# Run command in all workspaces
|
||||
yarn workspaces foreach --all run <command>
|
||||
|
||||
# Install production dependencies only (for server deployment)
|
||||
yarn install:server
|
||||
```
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
- **Node.js**: >=20
|
||||
- **Yarn**: ^4.9.1 (managed by packageManager field)
|
||||
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The codebase is actively being migrated:
|
||||
|
||||
- **JavaScript → TypeScript**: sync-server is in progress
|
||||
- **Classes → Functions**: Prefer functional patterns
|
||||
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
|
||||
|
||||
When working with older code, follow the newer patterns described in this guide.
|
||||
@@ -5,7 +5,7 @@
|
||||
# you are doing.
|
||||
###################################################
|
||||
|
||||
FROM node:20-bullseye as dev
|
||||
FROM node:22-bookworm as dev
|
||||
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||
WORKDIR /app
|
||||
CMD ["sh", "./bin/docker-start"]
|
||||
|
||||
@@ -16,6 +16,7 @@ packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
@@ -61,14 +62,11 @@ yarn workspace desktop-electron update-client
|
||||
echo "Skipping exe build"
|
||||
else
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "Created release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
yarn build
|
||||
fi
|
||||
fi
|
||||
)
|
||||
|
||||
@@ -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.52.0-jammy /bin/bash \
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import globals from 'globals';
|
||||
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
@@ -71,7 +72,7 @@ const confusingBrowserGlobals = [
|
||||
'top',
|
||||
];
|
||||
|
||||
export default pluginTypescript.config(
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
@@ -83,6 +84,7 @@ export default pluginTypescript.config(
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
@@ -98,6 +100,7 @@ export default pluginTypescript.config(
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'packages/sync-server/build/',
|
||||
'packages/plugins-service/dist/',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
],
|
||||
|
||||
30
lage.config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/** @type {import('lage').ConfigOptions} */
|
||||
module.exports = {
|
||||
pipeline: {
|
||||
test: {
|
||||
type: 'npmScript',
|
||||
options: {
|
||||
outputGlob: [
|
||||
'coverage/**',
|
||||
'**/test-results/**',
|
||||
'**/playwright-report/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
type: 'npmScript',
|
||||
cache: true,
|
||||
options: {
|
||||
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
cacheOptions: {
|
||||
cacheStorageConfig: {
|
||||
provider: 'local',
|
||||
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
||||
},
|
||||
},
|
||||
npmClient: 'yarn',
|
||||
concurrency: 2,
|
||||
};
|
||||
52
package.json
@@ -23,28 +23,31 @@
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"desktop-dependencies": "yarn rebuild-electron && yarn workspace loot-core build:browser",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --exclude desktop-electron --parallel --verbose run e2e",
|
||||
"test": "lage test --continue",
|
||||
"test:debug": "lage test --no-cache --continue",
|
||||
"e2e": "yarn workspace @actual-app/web run e2e",
|
||||
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
|
||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt": "yarn workspace @actual-app/web run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "prettier --check . && eslint . --max-warnings 0",
|
||||
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
|
||||
@@ -55,32 +58,33 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/node": "^22.18.11",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.0.0-rc.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^15.15.0",
|
||||
"globals": "^16.4.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lage": "^2.14.14",
|
||||
"lint-staged": "^16.2.3",
|
||||
"minimatch": "^10.0.3",
|
||||
"node-jq": "^6.0.1",
|
||||
"node-jq": "^6.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-limit": "^6.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"p-limit": "^7.1.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -88,7 +92,7 @@
|
||||
"socks": ">=2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"node": ">=22",
|
||||
"yarn": "^4.9.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -97,7 +101,7 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"browserslist": [
|
||||
"electron >= 35.0",
|
||||
"defaults"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.9.0",
|
||||
"version": "25.10.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -19,19 +19,19 @@
|
||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp default-db.sqlite dist/",
|
||||
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run build:app && yarn run build:crdt && vitest",
|
||||
"test": "yarn run build:app && yarn run build:crdt && vitest --run",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export function amountToInteger(n) {
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export function integerToAmount(n) {
|
||||
return parseFloat((n / 100).toFixed(2));
|
||||
}
|
||||
6
packages/api/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
// eslint-disable-next-line import/extensions
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
@@ -5,5 +5,11 @@ export default {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest"
|
||||
"test": "vitest --run"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,10 @@ export default defineConfig({
|
||||
globals: true,
|
||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||
environment: 'node',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.8.0",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
@@ -54,6 +54,6 @@
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:web": "ENV=web vitest -c vitest.web.config.ts"
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
@@ -11,15 +12,20 @@ import {
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
|
||||
* The child element should have a `ref` prop for this to work. For child components which receives a ref via another prop
|
||||
* e.g. `inputRef`, use a function as child and pass the ref to the appropriate prop.
|
||||
*/
|
||||
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
|
||||
children:
|
||||
| ReactElement<{ ref: Ref<T> }>
|
||||
| ((ref: RefObject<T | null>) => ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* InitialFocus sets focus on its child element
|
||||
* when it mounts.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
|
||||
* @param {ReactElement | function} children - A single React element or a function that returns a React element.
|
||||
* The child element should have a `ref` prop for this to work. For child components which receives a ref via another prop
|
||||
* e.g. `inputRef`, use a function as child and pass the ref to the appropriate prop.
|
||||
*/
|
||||
export function InitialFocus<T extends HTMLElement = HTMLElement>({
|
||||
children,
|
||||
|
||||
@@ -154,4 +154,10 @@ export const styles: Record<string, any> = {
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
mobileListItem: {
|
||||
borderBottom: `1px solid ${theme.tableBorder}`,
|
||||
backgroundColor: theme.tableBackground,
|
||||
padding: 16,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,12 @@ export default defineConfig({
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "rm -rf dist && yarn run build:node",
|
||||
"test": "vitest --globals"
|
||||
"test": "vitest --run --globals"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"protoc-gen-js": "^3.21.4-4",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/desktop-client/.gitignore
vendored
@@ -14,6 +14,9 @@ build-electron
|
||||
build-stats
|
||||
stats.json
|
||||
|
||||
# generated service worker
|
||||
service-worker/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -28,3 +31,6 @@ public/*.wasm
|
||||
|
||||
# translations
|
||||
locale/
|
||||
|
||||
# service worker build output
|
||||
dev-dist
|
||||
|
||||
@@ -65,10 +65,10 @@ Run manually:
|
||||
|
||||
```sh
|
||||
# Run docker container
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
|
||||
# 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.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-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
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@@ -123,11 +123,14 @@ test.describe('Accounts', () => {
|
||||
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.waitFor({ state: 'visible' });
|
||||
|
||||
if (screenshot) await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await importButton.click();
|
||||
|
||||
await expect(importButton).not.toBeVisible();
|
||||
@@ -146,12 +149,14 @@ test.describe('Accounts', () => {
|
||||
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 importButton.waitFor({ state: 'visible' });
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await expect(importButton).toBeDisabled();
|
||||
await expect(await importButton.innerText()).toMatch(
|
||||
/Import 0 transactions/,
|
||||
|
||||
64
packages/desktop-client/e2e/bank-sync.mobile.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { type MobileBankSyncPage } from './page-models/mobile-bank-sync-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
|
||||
test.describe('Mobile Bank Sync', () => {
|
||||
let page: Page;
|
||||
let navigation: MobileNavigation;
|
||||
let bankSyncPage: MobileBankSyncPage;
|
||||
let configurationPage: 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();
|
||||
|
||||
bankSyncPage = await navigation.goToBankSyncPage();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await bankSyncPage.waitToLoad();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Bank Sync' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(bankSyncPage.searchBox).toBeVisible();
|
||||
await expect(bankSyncPage.searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Filter accounts…',
|
||||
);
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('searches for accounts', async () => {
|
||||
await bankSyncPage.searchFor('Checking');
|
||||
await expect(bankSyncPage.searchBox).toHaveValue('Checking');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('page handles empty state gracefully', async () => {
|
||||
await bankSyncPage.searchFor('NonExistentAccount123456789');
|
||||
|
||||
const emptyMessage = page.getByText(/No accounts found/);
|
||||
await expect(emptyMessage).toBeVisible();
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
35
packages/desktop-client/e2e/bank-sync.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { type BankSyncPage } from './page-models/bank-sync-page';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
test.describe('Bank Sync', () => {
|
||||
let page: Page;
|
||||
let navigation: Navigation;
|
||||
let bankSyncPage: BankSyncPage;
|
||||
let configurationPage: ConfigurationPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new Navigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
bankSyncPage = await navigation.goToBankSyncPage();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await bankSyncPage.waitToLoad();
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 78 KiB |
13
packages/desktop-client/e2e/page-models/bank-sync-page.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
export class BankSyncPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async waitToLoad() {
|
||||
await this.page.waitForSelector('text=Bank Sync', { timeout: 10000 });
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,14 @@ export class CustomReportPage {
|
||||
async selectMode(mode: 'total' | 'time') {
|
||||
switch (mode) {
|
||||
case 'total':
|
||||
await this.pageContent.getByRole('button', { name: 'Total' }).click();
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Total', exact: true })
|
||||
.click();
|
||||
break;
|
||||
case 'time':
|
||||
await this.pageContent.getByRole('button', { name: 'Time' }).click();
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Time', exact: true })
|
||||
.click();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized mode: ${mode}`);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
export class MobileBankSyncPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly accountsList: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder(/Filter accounts/i);
|
||||
this.accountsList = page.getByRole('main');
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
timeout?: number;
|
||||
}) {
|
||||
await this.accountsList.waitFor(options);
|
||||
}
|
||||
|
||||
async waitToLoad() {
|
||||
await this.page.waitForSelector('text=Bank Sync', { timeout: 10000 });
|
||||
}
|
||||
|
||||
async searchFor(term: string) {
|
||||
await this.searchBox.fill(term);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
import { MobileAccountsPage } from './mobile-accounts-page';
|
||||
import { MobileBankSyncPage } from './mobile-bank-sync-page';
|
||||
import { MobileBudgetPage } from './mobile-budget-page';
|
||||
import { MobilePayeesPage } from './mobile-payees-page';
|
||||
import { MobileReportsPage } from './mobile-reports-page';
|
||||
import { MobileRulesPage } from './mobile-rules-page';
|
||||
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';
|
||||
@@ -14,6 +16,7 @@ const NAV_LINKS_HIDDEN_BY_DEFAULT = [
|
||||
'Schedules',
|
||||
'Payees',
|
||||
'Rules',
|
||||
'Bank Sync',
|
||||
'Settings',
|
||||
];
|
||||
const ROUTES_BY_PAGE = {
|
||||
@@ -21,7 +24,9 @@ const ROUTES_BY_PAGE = {
|
||||
Accounts: '/accounts',
|
||||
Transaction: '/transactions/new',
|
||||
Reports: '/reports',
|
||||
Payees: '/payees',
|
||||
Rules: '/rules',
|
||||
'Bank Sync': '/bank-sync',
|
||||
Settings: '/settings',
|
||||
};
|
||||
|
||||
@@ -166,6 +171,13 @@ export class MobileNavigation {
|
||||
);
|
||||
}
|
||||
|
||||
async goToPayeesPage() {
|
||||
return await this.navigateToPage(
|
||||
'Payees',
|
||||
() => new MobilePayeesPage(this.page),
|
||||
);
|
||||
}
|
||||
|
||||
async goToRulesPage() {
|
||||
return await this.navigateToPage(
|
||||
'Rules',
|
||||
@@ -173,6 +185,13 @@ export class MobileNavigation {
|
||||
);
|
||||
}
|
||||
|
||||
async goToBankSyncPage() {
|
||||
return await this.navigateToPage(
|
||||
'Bank Sync',
|
||||
() => new MobileBankSyncPage(this.page),
|
||||
);
|
||||
}
|
||||
|
||||
async goToSettingsPage() {
|
||||
return await this.navigateToPage(
|
||||
'Settings',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
export class MobilePayeesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly payeesList: Locator;
|
||||
readonly emptyMessage: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder('Filter payees…');
|
||||
this.payeesList = page.getByRole('grid', { name: 'Payees' });
|
||||
this.emptyMessage = page.getByText('No payees found.');
|
||||
this.loadingIndicator = page.getByTestId('animated-loading');
|
||||
}
|
||||
|
||||
async waitFor(options?: {
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
timeout?: number;
|
||||
}) {
|
||||
await this.payeesList.waitFor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for payees using the search box
|
||||
*/
|
||||
async searchFor(text: string) {
|
||||
await this.searchBox.fill(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the search box
|
||||
*/
|
||||
async clearSearch() {
|
||||
await this.searchBox.fill('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nth payee item (0-based index)
|
||||
*/
|
||||
getNthPayee(index: number) {
|
||||
return this.getAllPayees().nth(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible payee items
|
||||
*/
|
||||
getAllPayees() {
|
||||
return this.payeesList.getByRole('gridcell');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a payee to view/edit rules
|
||||
*/
|
||||
async clickPayee(index: number) {
|
||||
const payee = this.getNthPayee(index);
|
||||
await payee.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of visible payees
|
||||
*/
|
||||
async getPayeeCount() {
|
||||
const payees = this.getAllPayees();
|
||||
return await payees.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for loading to complete
|
||||
*/
|
||||
async waitForLoadingToComplete(timeout: number = 10000) {
|
||||
await this.loadingIndicator.waitFor({ state: 'hidden', timeout });
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,14 @@ export class MobileRulesPage {
|
||||
* Get the nth rule item (0-based index)
|
||||
*/
|
||||
getNthRule(index: number) {
|
||||
return this.page
|
||||
.getByRole('button')
|
||||
.filter({ hasText: /IF|THEN/ })
|
||||
.nth(index);
|
||||
return this.getAllRules().nth(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible rule items
|
||||
*/
|
||||
getAllRules() {
|
||||
return this.page.getByRole('button').filter({ hasText: /IF|THEN/ });
|
||||
return this.page.getByRole('grid', { name: 'Rules' }).getByRole('row');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,28 +73,6 @@ export class MobileRulesPage {
|
||||
return await rules.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the search bar has a border
|
||||
*/
|
||||
async hasSearchBarBorder() {
|
||||
const searchContainer = this.searchBox.locator('..');
|
||||
const borderStyle = await searchContainer.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.borderBottomWidth;
|
||||
});
|
||||
return borderStyle === '2px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the background color of the search box
|
||||
*/
|
||||
async getSearchBackgroundColor() {
|
||||
return await this.searchBox.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.backgroundColor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a rule contains specific text
|
||||
*/
|
||||
@@ -112,7 +87,7 @@ export class MobileRulesPage {
|
||||
*/
|
||||
async getRuleStage(index: number) {
|
||||
const rule = this.getNthRule(index);
|
||||
const stageBadge = rule.locator('span').first();
|
||||
const stageBadge = rule.getByTestId('rule-stage-badge');
|
||||
return await stageBadge.textContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { AccountPage } from './account-page';
|
||||
import { BankSyncPage } from './bank-sync-page';
|
||||
import { PayeesPage } from './payees-page';
|
||||
import { ReportsPage } from './reports-page';
|
||||
import { RulesPage } from './rules-page';
|
||||
@@ -66,6 +67,19 @@ export class Navigation {
|
||||
return new PayeesPage(this.page);
|
||||
}
|
||||
|
||||
async goToBankSyncPage() {
|
||||
const bankSyncLink = this.page.getByRole('link', { name: 'Bank Sync' });
|
||||
|
||||
// Expand the "more" menu only if it is not already expanded
|
||||
if (!(await bankSyncLink.isVisible())) {
|
||||
await this.page.getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
await bankSyncLink.click();
|
||||
|
||||
return new BankSyncPage(this.page);
|
||||
}
|
||||
|
||||
async goToSettingsPage() {
|
||||
const settingsLink = this.page.getByRole('link', { name: 'Settings' });
|
||||
|
||||
|
||||
@@ -122,7 +122,15 @@ export class RulesPage {
|
||||
|
||||
if (op && !fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (field) {
|
||||
@@ -133,12 +141,26 @@ export class RulesPage {
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.click();
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (op && fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page.getByRole('button', { name: op, exact: true }).click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (value) {
|
||||
|
||||
@@ -28,7 +28,10 @@ export class SchedulesPage {
|
||||
|
||||
await this._fillScheduleFields(data);
|
||||
|
||||
await this.page.getByRole('button', { name: 'Add' }).click();
|
||||
await this.page
|
||||
.getByTestId('schedule-edit-modal')
|
||||
.getByRole('button', { name: 'Add' })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
112
packages/desktop-client/e2e/payees.mobile.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import { type MobilePayeesPage } from './page-models/mobile-payees-page';
|
||||
|
||||
test.describe('Mobile Payees', () => {
|
||||
let page: Page;
|
||||
let navigation: MobileNavigation;
|
||||
let payeesPage: MobilePayeesPage;
|
||||
let configurationPage: ConfigurationPage;
|
||||
|
||||
test.beforeEach(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new MobileNavigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({
|
||||
width: 350,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
|
||||
// Navigate to payees page and wait for it to load
|
||||
payeesPage = await navigation.goToPayeesPage();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('checks the page visuals', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Check that the header is present
|
||||
await expect(page.getByRole('heading', { name: 'Payees' })).toBeVisible();
|
||||
|
||||
// Check that the search box is present with proper placeholder
|
||||
await expect(payeesPage.searchBox).toBeVisible();
|
||||
await expect(payeesPage.searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Filter payees…',
|
||||
);
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('filters out unrelated payees', async () => {
|
||||
await payeesPage.searchFor('asdfasdf-nonsense');
|
||||
|
||||
// Get the text 'No payees found.' from the page
|
||||
const noPayeesMessage = page.getByText('No payees found.');
|
||||
|
||||
// Assert it is visible
|
||||
await expect(noPayeesMessage).toBeVisible();
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('clicking on a payee opens rule creation form', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
|
||||
await payeesPage.clickPayee(0);
|
||||
|
||||
// Should navigate to rules page for creating a new rule
|
||||
await expect(page).toHaveURL(/\/rules/);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('page handles empty state gracefully', async () => {
|
||||
// Search for something that won't match to get empty state
|
||||
await payeesPage.searchFor('NonExistentPayee123456789');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that empty message is shown
|
||||
const emptyMessage = page.getByText('No payees found.');
|
||||
await expect(emptyMessage).toBeVisible();
|
||||
|
||||
// Check that no payee items are visible
|
||||
const payees = payeesPage.getAllPayees();
|
||||
await expect(payees).toHaveCount(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('search functionality works correctly', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
// Test searching for a specific payee
|
||||
await payeesPage.searchFor('Fast Internet');
|
||||
|
||||
// Should show at least one result
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
expect(payeeCount).toBeGreaterThan(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
// Clear search
|
||||
await payeesPage.clearSearch();
|
||||
|
||||
// Should show all payees again
|
||||
const allPayeeCount = await payeesPage.getPayeeCount();
|
||||
expect(allPayeeCount).toBeGreaterThan(payeeCount);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |