Refactor test execution to use lage task runner (#5964)

This commit is contained in:
Matiss Janis Aboltins
2025-10-21 08:58:26 +02:00
committed by GitHub
parent 8019d9f61b
commit 57d01467ca
29 changed files with 275 additions and 36 deletions

3
.cursor/worktrees.json Normal file
View File

@@ -0,0 +1,3 @@
{
"setup-worktree": ["yarn"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -37,6 +37,8 @@ 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:
@@ -58,6 +60,8 @@ 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

3
.gitignore vendored
View File

@@ -62,3 +62,6 @@ build/
# .d.ts files aren't type-checked with skipLibCheck set to true
*.d.ts
# Lage cache
.lage/

View File

@@ -40,7 +40,28 @@ 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
- Include `--watch=false` flag when running unit tests to prevent watch mode
- 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
@@ -54,8 +75,13 @@ The core application logic that runs on any platform.
- Platform-agnostic code
- Exports for both browser and node environments
- Test commands:
```bash
yarn workspace loot-core run test --watch=false
# 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`)
@@ -95,9 +121,16 @@ Public API for programmatic access to Actual.
- Node.js API
- Designed for integrations and automation
- Commands:
```bash
# 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`)
@@ -157,24 +190,29 @@ When implementing changes:
**Unit Tests (Vitest)**
The project uses **lage** for running tests across all workspaces efficiently.
```bash
# All tests
# Run all tests across all packages (using lage)
yarn test
# Specific package
yarn workspace loot-core run test --watch=false
# Run tests without cache (for debugging)
yarn test:debug
# Specific test file
yarn workspace loot-core run test path/to/test.test.ts --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
```
**E2E Tests (Playwright)**
```bash
# Desktop client E2E
yarn workspace @actual-app/web e2e
# Run E2E tests for web
yarn e2e
# Desktop Electron E2E
# Desktop Electron E2E (includes full build)
yarn e2e:desktop
# Visual regression tests
@@ -182,6 +220,9 @@ 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:**
@@ -329,6 +370,7 @@ 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
@@ -348,6 +390,7 @@ 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
@@ -366,8 +409,11 @@ describe('ComponentName', () => {
### Running Specific Tests
```bash
# Unit test for a specific file in loot-core
yarn workspace loot-core run test src/path/to/file.test.ts --watch=false
# 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
@@ -432,6 +478,8 @@ 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
@@ -466,8 +514,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
### Visual Regression Tests (VRT)
- Run with `VRT=true` environment variable
- Snapshots stored per test file
- Snapshots stored per test file in `*-snapshots/` directories
- Use Docker for consistent environment: `yarn vrt:docker`
## Additional Resources

30
lage.config.js Normal file
View 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,
};

View File

@@ -40,8 +40,8 @@
"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",
"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",
@@ -73,6 +73,7 @@
"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",

View File

@@ -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",
"test": "yarn run build:app && yarn run build:crdt && vitest --run",
"clean": "rm -rf dist @types"
},
"dependencies": {

View File

@@ -5,5 +5,11 @@ export default {
// print only console.error
return type === 'stderr';
},
poolOptions: {
threads: {
maxThreads: 2,
minThreads: 1,
},
},
},
};

View File

@@ -6,6 +6,6 @@
"vitest": "^3.2.4"
},
"scripts": {
"test": "vitest"
"test": "vitest --run"
}
}

View File

@@ -5,5 +5,10 @@ export default defineConfig({
globals: true,
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
environment: 'node',
poolOptions: {
threads: {
singleThread: true,
},
},
},
});

View File

@@ -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"
}
}

View File

@@ -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: [

View File

@@ -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 --globals"
"test": "vitest --run --globals"
},
"dependencies": {
"google-protobuf": "^3.21.4",

View File

@@ -84,7 +84,7 @@
"build": "vite build",
"build:browser": "cross-env ./bin/build-browser",
"generate:i18n": "i18next",
"test": "vitest",
"test": "vitest --run",
"e2e": "npx playwright test --browser=chromium",
"vrt": "cross-env VRT=true npx playwright test --browser=chromium",
"playwright": "playwright"

View File

@@ -13,6 +13,14 @@ vi.mock('i18next', () => {
};
});
vi.mock('./languages', () => ({
languages: {
'/locale/en.json': vi.fn(),
'/locale/uk.json': vi.fn(),
'/locale/pt-BR.json': vi.fn(),
},
}));
vi.hoisted(vi.resetModules);
describe('setI18NextLanguage', () => {

View File

@@ -5,7 +5,7 @@ import resourcesToBackend from 'i18next-resources-to-backend';
import * as Platform from 'loot-core/shared/platform';
const languages = import.meta.glob(['/locale/*.json', '!/locale/*_old.json']);
import { languages } from './languages';
export const availableLanguages = Object.keys(languages).map(
path => path.split('/')[2].split('.')[0],

View File

@@ -0,0 +1,4 @@
export const languages = import.meta.glob([
'/locale/*.json',
'!/locale/*_old.json',
]);

View File

@@ -219,6 +219,12 @@ export default defineConfig(async ({ mode }) => {
// print only console.error
return type === 'stderr';
},
poolOptions: {
threads: {
maxThreads: 2,
minThreads: 1,
},
},
},
};
});

View File

@@ -5,7 +5,7 @@
"exports": "./lib/index.js",
"type": "commonjs",
"scripts": {
"test": "vitest"
"test": "vitest --run"
},
"dependencies": {
"requireindex": "^1.2.0"

View File

@@ -11,8 +11,8 @@
"watch:browser": "cross-env NODE_ENV=development ./bin/build-browser",
"generate:i18n": "i18next",
"test": "npm-run-all -cp 'test:*'",
"test:node": "ENV=node vitest",
"test:web": "ENV=web vitest -c vitest.web.config.ts"
"test:node": "ENV=node vitest --run",
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
},
"author": "",
"license": "ISC",

View File

@@ -24,6 +24,12 @@ export default defineConfig({
// print only console.error
return type === 'stderr';
},
poolOptions: {
threads: {
maxThreads: 2,
minThreads: 1,
},
},
},
resolve: {
alias: [

View File

@@ -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: [

View File

@@ -17,7 +17,7 @@
"start-monitor": "nodemon --exec 'tsc && node build/app' --ignore './build/**/*' --ext 'ts,js' build/app",
"build": "tsc && yarn copy-static-assets",
"copy-static-assets": "rm -rf build/src/sql && cp -r src/sql build/src/sql",
"test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' vitest",
"test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' vitest --run",
"db:migrate": "yarn build && cross-env NODE_ENV=development node build/src/scripts/run-migrations.js up",
"db:downgrade": "yarn build && cross-env NODE_ENV=development node build/src/scripts/run-migrations.js down",
"db:test-migrate": "yarn build && cross-env NODE_ENV=test node build/src/scripts/run-migrations.js up",

View File

@@ -4,15 +4,13 @@ export default {
globalSetup: ['./vitest.globalSetup.js'],
globals: true,
coverage: {
enabled: true,
include: ['**/*.{js,ts,tsx}'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/coverage/**',
],
reporter: ['html', 'lcov', 'text', 'text-summary'],
enabled: false,
},
poolOptions: {
threads: {
maxThreads: 2,
minThreads: 1,
},
},
},
};

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Refactor test execution to use lage task runner for improved monorepo test orchestration with parallel execution, smart caching, and better CI performance.

View File

@@ -6966,6 +6966,7 @@ __metadata:
globals: "npm:^16.4.0"
html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7"
lage: "npm:^2.14.14"
lint-staged: "npm:^16.2.3"
minimatch: "npm:^10.0.3"
node-jq: "npm:^6.3.1"
@@ -11405,6 +11406,65 @@ __metadata:
languageName: node
linkType: hard
"glob-hasher-darwin-arm64@npm:1.4.2":
version: 1.4.2
resolution: "glob-hasher-darwin-arm64@npm:1.4.2"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"glob-hasher-darwin-x64@npm:1.4.2":
version: 1.4.2
resolution: "glob-hasher-darwin-x64@npm:1.4.2"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"glob-hasher-linux-x64-gnu@npm:1.4.2":
version: 1.4.2
resolution: "glob-hasher-linux-x64-gnu@npm:1.4.2"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"glob-hasher-win32-arm64-msvc@npm:1.4.2":
version: 1.4.2
resolution: "glob-hasher-win32-arm64-msvc@npm:1.4.2"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"glob-hasher-win32-x64-msvc@npm:1.4.2":
version: 1.4.2
resolution: "glob-hasher-win32-x64-msvc@npm:1.4.2"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"glob-hasher@npm:^1.4.2":
version: 1.4.2
resolution: "glob-hasher@npm:1.4.2"
dependencies:
glob-hasher-darwin-arm64: "npm:1.4.2"
glob-hasher-darwin-x64: "npm:1.4.2"
glob-hasher-linux-x64-gnu: "npm:1.4.2"
glob-hasher-win32-arm64-msvc: "npm:1.4.2"
glob-hasher-win32-x64-msvc: "npm:1.4.2"
dependenciesMeta:
glob-hasher-darwin-arm64:
optional: true
glob-hasher-darwin-x64:
optional: true
glob-hasher-linux-x64-gnu:
optional: true
glob-hasher-win32-arm64-msvc:
optional: true
glob-hasher-win32-x64-msvc:
optional: true
checksum: 10/7d21697e63cc43f6edcec52b88ed0cc877011edaca1446c0fb1fb2efb3b7bd26f9900dfde7a7ab62ee425c6bd8da359c0a3d34997c9916de6b887029ca2995d8
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
@@ -13213,6 +13273,22 @@ __metadata:
languageName: node
linkType: hard
"lage@npm:^2.14.14":
version: 2.14.14
resolution: "lage@npm:2.14.14"
dependencies:
fsevents: "npm:~2.3.2"
glob-hasher: "npm:^1.4.2"
dependenciesMeta:
fsevents:
optional: true
bin:
lage: dist/lage.js
lage-server: dist/lage-server.js
checksum: 10/e8539b2740ccdfb0862070b5752912b20be3e5b1f3729e75cfdf62a7b3d8f60edd547c1d4901ae9b441dbf3faa6940e6597c2c019ba943f5cdbc41f4779b41aa
languageName: node
linkType: hard
"language-subtag-registry@npm:^0.3.20":
version: 0.3.23
resolution: "language-subtag-registry@npm:0.3.23"