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
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
- name: Install yarn
|
- name: Install yarn
|
||||||
run: npm install -g yarn
|
run: npm install -g yarn
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -32,16 +32,6 @@ runs:
|
|||||||
with:
|
with:
|
||||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
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)) }}
|
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
|
- name: Install
|
||||||
working-directory: ${{ inputs.working-directory }}
|
working-directory: ${{ inputs.working-directory }}
|
||||||
run: yarn --immutable
|
run: yarn --immutable
|
||||||
|
|||||||
24
.github/workflows/build.yml
vendored
@@ -12,6 +12,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
|
repository_dispatch:
|
||||||
|
types: [vrt-update-applied]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -22,10 +24,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Build API
|
- name: Build API
|
||||||
run: cd packages/api && yarn build
|
run: cd packages/api && yarn build
|
||||||
- name: Create package tgz
|
- name: Create package tgz
|
||||||
@@ -40,10 +44,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Build CRDT
|
- name: Build CRDT
|
||||||
run: cd packages/crdt && yarn build
|
run: cd packages/crdt && yarn build
|
||||||
- name: Create package tgz
|
- name: Create package tgz
|
||||||
@@ -58,6 +64,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Build Web
|
- name: Build Web
|
||||||
@@ -77,10 +87,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Build Server
|
- name: Build Server
|
||||||
run: yarn workspace @actual-app/sync-server build
|
run: yarn workspace @actual-app/sync-server build
|
||||||
- name: Upload Build
|
- name: Upload Build
|
||||||
|
|||||||
34
.github/workflows/check.yml
vendored
@@ -5,6 +5,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
|
repository_dispatch:
|
||||||
|
types: [vrt-update-applied]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -15,30 +17,36 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: yarn typecheck
|
run: yarn typecheck
|
||||||
validate-cli:
|
validate-cli:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Build Web
|
- name: Build Web
|
||||||
run: yarn build:server
|
run: yarn build:server
|
||||||
- name: Check that the built CLI works
|
- name: Check that the built CLI works
|
||||||
@@ -47,20 +55,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
download-translations: 'false'
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
migrations:
|
migrations:
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
- name: Check migrations
|
- name: Check migrations
|
||||||
run: node ./.github/actions/check-migrations.js
|
run: node ./.github/actions/check-migrations.js
|
||||||
|
|||||||
4
.github/workflows/e2e-test.yml
vendored
@@ -37,8 +37,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
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
|
- name: Run E2E Tests on Netlify URL
|
||||||
run: yarn e2e
|
run: yarn e2e
|
||||||
env:
|
env:
|
||||||
@@ -60,8 +58,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Trust the repository directory
|
|
||||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
||||||
- name: Run Desktop app E2E Tests
|
- name: Run Desktop app E2E Tests
|
||||||
run: |
|
run: |
|
||||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
- name: Handle feature requests
|
- name: Handle feature requests
|
||||||
run: node .github/actions/handle-feature-requests.js
|
run: node .github/actions/handle-feature-requests.js
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
- name: Setup node and npm registry
|
- name: Setup node and npm registry
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Publish Web
|
- name: Publish Web
|
||||||
|
|||||||
2
.github/workflows/publish-npm-packages.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
|||||||
- name: Setup node and npm registry
|
- name: Setup node and npm registry
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 20
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Publish Web
|
- name: Publish Web
|
||||||
|
|||||||
26
.github/workflows/vrt-update-apply.yml
vendored
@@ -130,6 +130,30 @@ jobs:
|
|||||||
git push origin "HEAD:refs/heads/$HEAD_REF"
|
git push origin "HEAD:refs/heads/$HEAD_REF"
|
||||||
echo "Successfully pushed VRT updates to $HEAD_REPO@$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
|
- name: Comment on PR - Success
|
||||||
if: steps.apply.outputs.applied == 'true'
|
if: steps.apply.outputs.applied == 'true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
@@ -139,7 +163,7 @@ jobs:
|
|||||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
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
|
- 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 files aren't type-checked with skipLibCheck set to true
|
||||||
*.d.ts
|
*.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
|
- **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
|
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
|
||||||
- Tests run once and exit by default (using `vitest --run`)
|
- Include `--watch=false` flag when running unit tests to prevent watch mode
|
||||||
|
|
||||||
### 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
|
## Architecture & Package Structure
|
||||||
|
|
||||||
@@ -75,13 +54,8 @@ The core application logic that runs on any platform.
|
|||||||
- Platform-agnostic code
|
- Platform-agnostic code
|
||||||
- Exports for both browser and node environments
|
- Exports for both browser and node environments
|
||||||
- Test commands:
|
- Test commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all loot-core tests
|
yarn workspace loot-core run test --watch=false
|
||||||
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`)
|
#### 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
|
- Node.js API
|
||||||
- Designed for integrations and automation
|
- Designed for integrations and automation
|
||||||
- Commands:
|
- Commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
|
||||||
yarn workspace @actual-app/api build
|
yarn workspace @actual-app/api build
|
||||||
|
yarn workspace @actual-app/api test --watch=false
|
||||||
# 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`)
|
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
|
||||||
@@ -190,29 +157,24 @@ When implementing changes:
|
|||||||
|
|
||||||
**Unit Tests (Vitest)**
|
**Unit Tests (Vitest)**
|
||||||
|
|
||||||
The project uses **lage** for running tests across all workspaces efficiently.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests across all packages (using lage)
|
# All tests
|
||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
# Run tests without cache (for debugging)
|
# Specific package
|
||||||
yarn test:debug
|
yarn workspace loot-core run test --watch=false
|
||||||
|
|
||||||
# Run tests for a specific package
|
# Specific test file
|
||||||
yarn workspace loot-core run test
|
yarn workspace loot-core run test path/to/test.test.ts --watch=false
|
||||||
|
|
||||||
# Run a specific test file (watch mode)
|
|
||||||
yarn workspace loot-core run test path/to/test.test.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**E2E Tests (Playwright)**
|
**E2E Tests (Playwright)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run E2E tests for web
|
# Desktop client E2E
|
||||||
yarn e2e
|
yarn workspace @actual-app/web e2e
|
||||||
|
|
||||||
# Desktop Electron E2E (includes full build)
|
# Desktop Electron E2E
|
||||||
yarn e2e:desktop
|
yarn e2e:desktop
|
||||||
|
|
||||||
# Visual regression tests
|
# Visual regression tests
|
||||||
@@ -220,9 +182,6 @@ yarn vrt
|
|||||||
|
|
||||||
# Visual regression in Docker (consistent environment)
|
# Visual regression in Docker (consistent environment)
|
||||||
yarn vrt:docker
|
yarn vrt:docker
|
||||||
|
|
||||||
# Run E2E tests for a specific package
|
|
||||||
yarn workspace @actual-app/web e2e
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Testing Best Practices:**
|
**Testing Best Practices:**
|
||||||
@@ -370,7 +329,6 @@ describe('ComponentName', () => {
|
|||||||
### Configuration Files
|
### Configuration Files
|
||||||
|
|
||||||
- `/package.json` - Root workspace configuration, scripts
|
- `/package.json` - Root workspace configuration, scripts
|
||||||
- `/lage.config.js` - Lage task runner configuration
|
|
||||||
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
||||||
- `/tsconfig.json` - Root TypeScript configuration
|
- `/tsconfig.json` - Root TypeScript configuration
|
||||||
- `/.cursorignore`, `/.gitignore` - Ignored files
|
- `/.cursorignore`, `/.gitignore` - Ignored files
|
||||||
@@ -390,7 +348,6 @@ describe('ComponentName', () => {
|
|||||||
- `packages/*/build/` - Built output
|
- `packages/*/build/` - Built output
|
||||||
- `packages/desktop-client/playwright-report/` - Test reports
|
- `packages/desktop-client/playwright-report/` - Test reports
|
||||||
- `packages/desktop-client/test-results/` - Test results
|
- `packages/desktop-client/test-results/` - Test results
|
||||||
- `.lage/` - Lage task runner cache (improves test performance)
|
|
||||||
|
|
||||||
### Key Source Directories
|
### Key Source Directories
|
||||||
|
|
||||||
@@ -409,11 +366,8 @@ describe('ComponentName', () => {
|
|||||||
### Running Specific Tests
|
### Running Specific Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests across all packages (recommended)
|
# Unit test for a specific file in loot-core
|
||||||
yarn test
|
yarn workspace loot-core run test src/path/to/file.test.ts --watch=false
|
||||||
|
|
||||||
# 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
|
# E2E test for a specific file
|
||||||
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
|
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`
|
2. For Vitest: check `vitest.config.ts` or `vitest.web.config.ts`
|
||||||
3. For Playwright: check `playwright.config.ts`
|
3. For Playwright: check `playwright.config.ts`
|
||||||
4. Ensure mock minimization - prefer real implementations
|
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
|
### Import Resolution Issues
|
||||||
|
|
||||||
@@ -514,7 +466,8 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
|||||||
|
|
||||||
### Visual Regression Tests (VRT)
|
### 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`
|
- Use Docker for consistent environment: `yarn vrt:docker`
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# you are doing.
|
# 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
|
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["sh", "./bin/docker-start"]
|
CMD ["sh", "./bin/docker-start"]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
|
||||||
import { defineConfig } from 'eslint/config';
|
|
||||||
import pluginImport from 'eslint-plugin-import';
|
import pluginImport from 'eslint-plugin-import';
|
||||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||||
import pluginReact from 'eslint-plugin-react';
|
import pluginReact from 'eslint-plugin-react';
|
||||||
@@ -72,7 +71,7 @@ const confusingBrowserGlobals = [
|
|||||||
'top',
|
'top',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig(
|
export default pluginTypescript.config(
|
||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
'packages/api/app/bundle.api.js',
|
'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",
|
"build:api": "yarn workspace @actual-app/api build",
|
||||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||||
"test": "lage test --continue",
|
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||||
"test:debug": "lage test --no-cache --continue",
|
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||||
"e2e": "yarn workspace @actual-app/web run e2e",
|
"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",
|
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
|
||||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
"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",
|
"vrt:docker": "./bin/run-vrt",
|
||||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@types/node": "^22.18.11",
|
"@types/node": "^22.18.8",
|
||||||
"@types/prompts": "^2.4.9",
|
"@types/prompts": "^2.4.9",
|
||||||
"@typescript-eslint/parser": "^8.46.0",
|
"@typescript-eslint/parser": "^8.46.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
@@ -68,12 +68,11 @@
|
|||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"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",
|
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lage": "^2.14.14",
|
|
||||||
"lint-staged": "^16.2.3",
|
"lint-staged": "^16.2.3",
|
||||||
"minimatch": "^10.0.3",
|
"minimatch": "^10.0.3",
|
||||||
"node-jq": "^6.3.1",
|
"node-jq": "^6.3.1",
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
"socks": ">=2.8.3"
|
"socks": ">=2.8.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22",
|
"node": ">=20",
|
||||||
"yarn": "^4.9.1"
|
"yarn": "^4.9.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||||
"build:default-db": "cp default-db.sqlite dist/",
|
"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",
|
"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"
|
"clean": "rm -rf dist @types"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,11 +5,5 @@ export default {
|
|||||||
// print only console.error
|
// print only console.error
|
||||||
return type === 'stderr';
|
return type === 'stderr';
|
||||||
},
|
},
|
||||||
poolOptions: {
|
|
||||||
threads: {
|
|
||||||
maxThreads: 2,
|
|
||||||
minThreads: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest --run"
|
"test": "vitest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,5 @@ export default defineConfig({
|
|||||||
globals: true,
|
globals: true,
|
||||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
poolOptions: {
|
|
||||||
threads: {
|
|
||||||
singleThread: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
"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": "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',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||||
poolOptions: {
|
|
||||||
threads: {
|
|
||||||
maxThreads: 2,
|
|
||||||
minThreads: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [
|
alias: [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"build:node": "tsc --p tsconfig.dist.json",
|
"build:node": "tsc --p tsconfig.dist.json",
|
||||||
"proto:generate": "./bin/generate-proto",
|
"proto:generate": "./bin/generate-proto",
|
||||||
"build": "rm -rf dist && yarn run build:node",
|
"build": "rm -rf dist && yarn run build:node",
|
||||||
"test": "vitest --run --globals"
|
"test": "vitest --globals"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"google-protobuf": "^3.21.4",
|
"google-protobuf": "^3.21.4",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ rm -fr build
|
|||||||
|
|
||||||
export IS_GENERIC_BROWSER=1
|
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_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
|
yarn build
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ cd "$ROOT/.."
|
|||||||
export IS_GENERIC_BROWSER=1
|
export IS_GENERIC_BROWSER=1
|
||||||
export PORT=3001
|
export PORT=3001
|
||||||
export REACT_APP_BACKEND_WORKER_HASH="dev"
|
export REACT_APP_BACKEND_WORKER_HASH="dev"
|
||||||
|
export REACT_APP_PLUGIN_SERVICE_WORKER_HASH="dev"
|
||||||
|
|
||||||
yarn start
|
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;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
||||||
|
|
||||||
|
if (screenshot) await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
const importButton = accountPage.page.getByRole('button', {
|
const importButton = accountPage.page.getByRole('button', {
|
||||||
name: /Import \d+ transactions/,
|
name: /Import \d+ transactions/,
|
||||||
});
|
});
|
||||||
|
|
||||||
await importButton.waitFor({ state: 'visible' });
|
|
||||||
|
|
||||||
if (screenshot) await expect(page).toMatchThemeScreenshots();
|
|
||||||
|
|
||||||
await importButton.click();
|
await importButton.click();
|
||||||
|
|
||||||
await expect(importButton).not.toBeVisible();
|
await expect(importButton).not.toBeVisible();
|
||||||
@@ -149,14 +146,12 @@ test.describe('Accounts', () => {
|
|||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
||||||
|
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
const importButton = accountPage.page.getByRole('button', {
|
const importButton = accountPage.page.getByRole('button', {
|
||||||
name: /Import \d+ transactions/,
|
name: /Import \d+ transactions/,
|
||||||
});
|
});
|
||||||
|
|
||||||
await importButton.waitFor({ state: 'visible' });
|
|
||||||
|
|
||||||
await expect(page).toMatchThemeScreenshots();
|
|
||||||
|
|
||||||
await expect(importButton).toBeDisabled();
|
await expect(importButton).toBeDisabled();
|
||||||
await expect(await importButton.innerText()).toMatch(
|
await expect(await importButton.innerText()).toMatch(
|
||||||
/Import 0 transactions/,
|
/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') {
|
async selectMode(mode: 'total' | 'time') {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'total':
|
case 'total':
|
||||||
await this.pageContent
|
await this.pageContent.getByRole('button', { name: 'Total' }).click();
|
||||||
.getByRole('button', { name: 'Total', exact: true })
|
|
||||||
.click();
|
|
||||||
break;
|
break;
|
||||||
case 'time':
|
case 'time':
|
||||||
await this.pageContent
|
await this.pageContent.getByRole('button', { name: 'Time' }).click();
|
||||||
.getByRole('button', { name: 'Time', exact: true })
|
|
||||||
.click();
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unrecognized mode: ${mode}`);
|
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 { MobileAccountPage } from './mobile-account-page';
|
||||||
import { MobileAccountsPage } from './mobile-accounts-page';
|
import { MobileAccountsPage } from './mobile-accounts-page';
|
||||||
import { MobileBankSyncPage } from './mobile-bank-sync-page';
|
|
||||||
import { MobileBudgetPage } from './mobile-budget-page';
|
import { MobileBudgetPage } from './mobile-budget-page';
|
||||||
import { MobilePayeesPage } from './mobile-payees-page';
|
import { MobilePayeesPage } from './mobile-payees-page';
|
||||||
import { MobileReportsPage } from './mobile-reports-page';
|
import { MobileReportsPage } from './mobile-reports-page';
|
||||||
@@ -16,7 +15,6 @@ const NAV_LINKS_HIDDEN_BY_DEFAULT = [
|
|||||||
'Schedules',
|
'Schedules',
|
||||||
'Payees',
|
'Payees',
|
||||||
'Rules',
|
'Rules',
|
||||||
'Bank Sync',
|
|
||||||
'Settings',
|
'Settings',
|
||||||
];
|
];
|
||||||
const ROUTES_BY_PAGE = {
|
const ROUTES_BY_PAGE = {
|
||||||
@@ -26,7 +24,6 @@ const ROUTES_BY_PAGE = {
|
|||||||
Reports: '/reports',
|
Reports: '/reports',
|
||||||
Payees: '/payees',
|
Payees: '/payees',
|
||||||
Rules: '/rules',
|
Rules: '/rules',
|
||||||
'Bank Sync': '/bank-sync',
|
|
||||||
Settings: '/settings',
|
Settings: '/settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,13 +182,6 @@ export class MobileNavigation {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async goToBankSyncPage() {
|
|
||||||
return await this.navigateToPage(
|
|
||||||
'Bank Sync',
|
|
||||||
() => new MobileBankSyncPage(this.page),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async goToSettingsPage() {
|
async goToSettingsPage() {
|
||||||
return await this.navigateToPage(
|
return await this.navigateToPage(
|
||||||
'Settings',
|
'Settings',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type Page } from '@playwright/test';
|
import { type Page } from '@playwright/test';
|
||||||
|
|
||||||
import { AccountPage } from './account-page';
|
import { AccountPage } from './account-page';
|
||||||
import { BankSyncPage } from './bank-sync-page';
|
|
||||||
import { PayeesPage } from './payees-page';
|
import { PayeesPage } from './payees-page';
|
||||||
import { ReportsPage } from './reports-page';
|
import { ReportsPage } from './reports-page';
|
||||||
import { RulesPage } from './rules-page';
|
import { RulesPage } from './rules-page';
|
||||||
@@ -67,19 +66,6 @@ export class Navigation {
|
|||||||
return new PayeesPage(this.page);
|
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() {
|
async goToSettingsPage() {
|
||||||
const settingsLink = this.page.getByRole('link', { name: 'Settings' });
|
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 () => {
|
test('clicking on a rule opens edit form', async () => {
|
||||||
await expect(async () => {
|
const ruleCount = await rulesPage.getRuleCount();
|
||||||
const ruleCount = await rulesPage.getRuleCount();
|
expect(ruleCount).toBeGreaterThan(0);
|
||||||
expect(ruleCount).toBeGreaterThan(0);
|
|
||||||
}).toPass();
|
|
||||||
|
|
||||||
await rulesPage.clickRule(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 |