Compare commits
2 Commits
mobile/lin
...
cursor/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9af9dc84e9 | ||
|
|
57710006e0 |
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"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: 22
|
||||
node-version: 20
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
@@ -32,16 +32,6 @@ 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
|
||||
|
||||
24
.github/workflows/build.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
repository_dispatch:
|
||||
types: [vrt-update-applied]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -22,10 +24,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- 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,10 +44,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- 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
|
||||
@@ -58,6 +64,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
@@ -77,10 +87,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- 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
|
||||
|
||||
34
.github/workflows/check.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
repository_dispatch:
|
||||
types: [vrt-update-applied]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -15,30 +17,36 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
validate-cli:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- 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
|
||||
@@ -47,20 +55,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
migrations:
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# For repository_dispatch events, checkout the PR branch
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_ref || github.ref }}
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.head_repo || github.repository }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
4
.github/workflows/e2e-test.yml
vendored
@@ -37,8 +37,6 @@ jobs:
|
||||
- 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:
|
||||
@@ -60,8 +58,6 @@ jobs:
|
||||
- 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
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- 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: 22
|
||||
node-version: 20
|
||||
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: 22
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
|
||||
26
.github/workflows/vrt-update-apply.yml
vendored
@@ -130,6 +130,30 @@ jobs:
|
||||
git push origin "HEAD:refs/heads/$HEAD_REF"
|
||||
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
|
||||
|
||||
- name: Trigger CI workflows
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Dispatch a custom event to trigger CI workflows
|
||||
// This will cause the CI workflows to run in the PR context
|
||||
try {
|
||||
await github.rest.repos.createDispatchEvent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
event_type: 'vrt-update-applied',
|
||||
client_payload: {
|
||||
pr_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
head_ref: '${{ steps.metadata.outputs.head_ref }}',
|
||||
head_repo: '${{ steps.metadata.outputs.head_repo }}'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Successfully triggered CI workflows via repository_dispatch');
|
||||
} catch (error) {
|
||||
console.log(`Failed to trigger CI workflows: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Comment on PR - Success
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
@@ -139,7 +163,7 @@ jobs:
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '✅ VRT screenshots have been automatically updated.'
|
||||
body: '✅ VRT screenshots have been automatically updated and CI workflows have been triggered.'
|
||||
});
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
|
||||
3
.gitignore
vendored
@@ -62,6 +62,3 @@ build/
|
||||
|
||||
# .d.ts files aren't type-checked with skipLibCheck set to true
|
||||
*.d.ts
|
||||
|
||||
# Lage cache
|
||||
.lage/
|
||||
|
||||
77
AGENTS.md
@@ -40,28 +40,7 @@ yarn start:desktop
|
||||
|
||||
- **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.
|
||||
- Include `--watch=false` flag when running unit tests to prevent watch mode
|
||||
|
||||
## Architecture & Package Structure
|
||||
|
||||
@@ -75,13 +54,8 @@ The core application logic that runs on any platform.
|
||||
- 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
|
||||
yarn workspace loot-core run test --watch=false
|
||||
```
|
||||
|
||||
#### 2. **desktop-client** (`packages/desktop-client/` - aliased as `@actual-app/web`)
|
||||
@@ -121,16 +95,9 @@ 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
|
||||
yarn workspace @actual-app/api test --watch=false
|
||||
```
|
||||
|
||||
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
|
||||
@@ -190,29 +157,24 @@ When implementing changes:
|
||||
|
||||
**Unit Tests (Vitest)**
|
||||
|
||||
The project uses **lage** for running tests across all workspaces efficiently.
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages (using lage)
|
||||
# All tests
|
||||
yarn test
|
||||
|
||||
# Run tests without cache (for debugging)
|
||||
yarn test:debug
|
||||
# Specific package
|
||||
yarn workspace loot-core run test --watch=false
|
||||
|
||||
# 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
|
||||
# Specific test file
|
||||
yarn workspace loot-core run test path/to/test.test.ts --watch=false
|
||||
```
|
||||
|
||||
**E2E Tests (Playwright)**
|
||||
|
||||
```bash
|
||||
# Run E2E tests for web
|
||||
yarn e2e
|
||||
# Desktop client E2E
|
||||
yarn workspace @actual-app/web e2e
|
||||
|
||||
# Desktop Electron E2E (includes full build)
|
||||
# Desktop Electron E2E
|
||||
yarn e2e:desktop
|
||||
|
||||
# Visual regression tests
|
||||
@@ -220,9 +182,6 @@ 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:**
|
||||
@@ -370,7 +329,6 @@ describe('ComponentName', () => {
|
||||
### 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
|
||||
@@ -390,7 +348,6 @@ describe('ComponentName', () => {
|
||||
- `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
|
||||
|
||||
@@ -409,11 +366,8 @@ describe('ComponentName', () => {
|
||||
### 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
|
||||
# Unit test for a specific file in loot-core
|
||||
yarn workspace loot-core run test src/path/to/file.test.ts --watch=false
|
||||
|
||||
# E2E test for a specific file
|
||||
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
|
||||
@@ -478,8 +432,6 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
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
|
||||
|
||||
@@ -514,7 +466,8 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
|
||||
### Visual Regression Tests (VRT)
|
||||
|
||||
- Snapshots stored per test file in `*-snapshots/` directories
|
||||
- Run with `VRT=true` environment variable
|
||||
- Snapshots stored per test file
|
||||
- Use Docker for consistent environment: `yarn vrt:docker`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# you are doing.
|
||||
###################################################
|
||||
|
||||
FROM node:22-bookworm as dev
|
||||
FROM node:20-bullseye as dev
|
||||
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||
WORKDIR /app
|
||||
CMD ["sh", "./bin/docker-start"]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -72,7 +71,7 @@ const confusingBrowserGlobals = [
|
||||
'top',
|
||||
];
|
||||
|
||||
export default defineConfig(
|
||||
export default pluginTypescript.config(
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/** @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,
|
||||
};
|
||||
15
package.json
@@ -40,12 +40,12 @@
|
||||
"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": "lage test --continue",
|
||||
"test:debug": "lage test --no-cache --continue",
|
||||
"e2e": "yarn workspace @actual-app/web run e2e",
|
||||
"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",
|
||||
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
|
||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
||||
"vrt": "yarn workspace @actual-app/web run vrt",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.18.11",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"cross-env": "^10.1.0",
|
||||
@@ -68,12 +68,11 @@
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^16.4.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lage": "^2.14.14",
|
||||
"lint-staged": "^16.2.3",
|
||||
"minimatch": "^10.0.3",
|
||||
"node-jq": "^6.3.1",
|
||||
@@ -92,7 +91,7 @@
|
||||
"socks": ">=2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"node": ">=20",
|
||||
"yarn": "^4.9.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"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 --run",
|
||||
"test": "yarn run build:app && yarn run build:crdt && vitest",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,11 +5,5 @@ 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 --run"
|
||||
"test": "vitest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,5 @@ export default defineConfig({
|
||||
globals: true,
|
||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||
environment: 'node',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 --run -c vitest.web.config.ts"
|
||||
"test:web": "ENV=web vitest -c vitest.web.config.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,6 @@ export default defineConfig({
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "rm -rf dist && yarn run build:node",
|
||||
"test": "vitest --run --globals"
|
||||
"test": "vitest --globals"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
|
||||
@@ -9,6 +9,7 @@ rm -fr build
|
||||
|
||||
export IS_GENERIC_BROWSER=1
|
||||
export REACT_APP_BACKEND_WORKER_HASH=`ls "$ROOT"/../public/kcab/kcab.worker.*.js | sed 's/.*kcab\.worker\.\(.*\)\.js/\1/'`
|
||||
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH=`ls "$ROOT"/../service-worker/plugin-sw.*.js | sed 's/.*plugin-sw\.\(.*\)\.js/\1/'`
|
||||
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ cd "$ROOT/.."
|
||||
export IS_GENERIC_BROWSER=1
|
||||
export PORT=3001
|
||||
export REACT_APP_BACKEND_WORKER_HASH="dev"
|
||||
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH="dev"
|
||||
|
||||
yarn start
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
@@ -123,14 +123,11 @@ 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();
|
||||
@@ -149,14 +146,12 @@ 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/,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,35 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 88 KiB |
@@ -1,13 +0,0 @@
|
||||
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,14 +29,10 @@ export class CustomReportPage {
|
||||
async selectMode(mode: 'total' | 'time') {
|
||||
switch (mode) {
|
||||
case 'total':
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Total', exact: true })
|
||||
.click();
|
||||
await this.pageContent.getByRole('button', { name: 'Total' }).click();
|
||||
break;
|
||||
case 'time':
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Time', exact: true })
|
||||
.click();
|
||||
await this.pageContent.getByRole('button', { name: 'Time' }).click();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized mode: ${mode}`);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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,6 @@ 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';
|
||||
@@ -16,7 +15,6 @@ const NAV_LINKS_HIDDEN_BY_DEFAULT = [
|
||||
'Schedules',
|
||||
'Payees',
|
||||
'Rules',
|
||||
'Bank Sync',
|
||||
'Settings',
|
||||
];
|
||||
const ROUTES_BY_PAGE = {
|
||||
@@ -26,7 +24,6 @@ const ROUTES_BY_PAGE = {
|
||||
Reports: '/reports',
|
||||
Payees: '/payees',
|
||||
Rules: '/rules',
|
||||
'Bank Sync': '/bank-sync',
|
||||
Settings: '/settings',
|
||||
};
|
||||
|
||||
@@ -185,13 +182,6 @@ export class MobileNavigation {
|
||||
);
|
||||
}
|
||||
|
||||
async goToBankSyncPage() {
|
||||
return await this.navigateToPage(
|
||||
'Bank Sync',
|
||||
() => new MobileBankSyncPage(this.page),
|
||||
);
|
||||
}
|
||||
|
||||
async goToSettingsPage() {
|
||||
return await this.navigateToPage(
|
||||
'Settings',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -67,19 +66,6 @@ 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' });
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 91 KiB |
@@ -59,10 +59,8 @@ test.describe('Mobile Rules', () => {
|
||||
});
|
||||
|
||||
test('clicking on a rule opens edit form', async () => {
|
||||
await expect(async () => {
|
||||
const ruleCount = await rulesPage.getRuleCount();
|
||||
expect(ruleCount).toBeGreaterThan(0);
|
||||
}).toPass();
|
||||
const ruleCount = await rulesPage.getRuleCount();
|
||||
expect(ruleCount).toBeGreaterThan(0);
|
||||
|
||||
await rulesPage.clickRule(0);
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |