Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .github/workflows/e2e-vrt-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,57 @@ jobs:
echo "VRT tests passed or skipped for PR #$PR_NUMBER"
fi

- name: Comment on PR with VRT report link
- name: Check if VRT Update - Generate is already running
id: vrt_update_generate
if: steps.metadata.outputs.should_comment == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const prNumber = parseInt('${{ steps.metadata.outputs.pr_number }}', 10);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const headSha = pr.data.head.sha;

let runs = (
await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'vrt-update-generate.yml',
head_sha: headSha,
per_page: 20,
})
).data.workflow_runs;

// issue_comment-triggered runs may not match head_sha filter; fall back to branch + SHA.
if (runs.length === 0) {
runs = (
await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'vrt-update-generate.yml',
branch: pr.data.head.ref,
per_page: 20,
})
).data.workflow_runs.filter((run) => run.head_sha === headSha);
}

const activeRun = runs.find(
(run) => run.status === 'queued' || run.status === 'in_progress',
);

core.setOutput('active', activeRun ? 'true' : 'false');
if (activeRun) {
core.setOutput('run_url', activeRun.html_url);
core.info(
`VRT Update - Generate is ${activeRun.status} for PR head ${headSha}: ${activeRun.html_url}`,
);
}

- name: Comment on PR with VRT report link
if: steps.metadata.outputs.should_comment == 'true' && steps.vrt_update_generate.outputs.active != 'true'
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
number: ${{ steps.metadata.outputs.pr_number }}
Expand All @@ -64,3 +113,17 @@ jobs:
VRT tests ❌ failed. [View the test report](${{ steps.metadata.outputs.artifact_url }}).

To update the VRT screenshots, comment `/update-vrt` on this PR. The VRT update operation takes about 50 minutes.

- name: Comment on PR with VRT report link (update already running)
if: steps.metadata.outputs.should_comment == 'true' && steps.vrt_update_generate.outputs.active == 'true'
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
number: ${{ steps.metadata.outputs.pr_number }}
header: vrt-comment
hide_and_recreate: true
hide_classify: OUTDATED
message: |
<!-- vrt-comment -->
VRT tests ❌ failed. [View the test report](${{ steps.metadata.outputs.artifact_url }}).

[**VRT Update - Generate**](${{ steps.vrt_update_generate.outputs.run_url }}) is already running for this PR's latest commit. Open that workflow run to watch progress.
27 changes: 24 additions & 3 deletions .github/workflows/vrt-update-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ jobs:
with:
download-translations: 'false'

# Web VRT: manifest under e2e/.vrt-manifest records expected PNGs; prune step removes orphans only.
- name: Prepare VRT snapshot manifest
run: rm -rf packages/desktop-client/e2e/.vrt-manifest

- name: Clear Electron VRT baselines
run: |
if [ -d packages/desktop-electron/e2e/__screenshots__ ]; then
find packages/desktop-electron/e2e/__screenshots__ -type f -name '*.png' -delete
fi

- name: Run VRT Tests on Desktop app
continue-on-error: true
run: |
Expand All @@ -78,6 +88,9 @@ jobs:
continue-on-error: true
run: yarn vrt --update-snapshots

- name: Prune orphaned web VRT snapshots
run: yarn vrt:prune

- name: Create patch with PNG changes only
id: create-patch
run: |
Expand All @@ -87,10 +100,18 @@ jobs:
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

# Stage only PNG files
git add "**/*.png"
# Stage VRT snapshot changes
git add --no-ignore-removal -- packages/desktop-client/e2e packages/desktop-electron/e2e

# Unstage non-PNG files (not included in VRT patch)
mapfile -t non_png < <(git diff --cached --name-only | grep -vE '\.png$' || true)
if [ "${#non_png[@]}" -gt 0 ]; then
echo "Unstaging non-PNG files (not included in VRT patch):"
printf ' %s\n' "${non_png[@]}"
git reset HEAD -- "${non_png[@]}"
fi

# Check if there are any changes
# Check if there are any real changes
if git diff --staged --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No VRT changes to commit"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ packages/desktop-client/public/kcab
packages/desktop-client/locale
packages/desktop-client/playwright-report
packages/desktop-client/test-results
packages/desktop-client/e2e/.vrt-manifest
packages/desktop-electron/client-build
packages/desktop-electron/build
packages/desktop-electron/.electron-symbols
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",
"playwright": "yarn workspace @actual-app/web run playwright",
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:prune": "yarn workspace @actual-app/web run prune-vrt-snapshots",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-node": "yarn workspace @actual-app/core rebuild",
Expand Down
3 changes: 3 additions & 0 deletions packages/desktop-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ yarn vrt:docker

# To update snapshots, use the following command:
yarn vrt:docker --e2e-start-url https://ip:port --update-snapshots

# After updating, remove PNGs that are no longer referenced by tests:
yarn vrt:prune
```

Run manually:
Expand Down
61 changes: 61 additions & 0 deletions packages/desktop-client/bin/prune-vrt-snapshots.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node
/**
* Remove PNGs under e2e/*-snapshots that are not listed in the VRT manifest
* (written during the last `yarn vrt` run). Safe to no-op when the manifest is empty.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgRoot = path.join(__dirname, '..');
const manifestDir =
process.env.VRT_SNAPSHOT_MANIFEST_DIR ??
path.join(pkgRoot, 'e2e', '.vrt-manifest');

const expected = new Set();
if (fs.existsSync(manifestDir)) {
for (const name of fs.readdirSync(manifestDir)) {
if (!name.startsWith('parallel-') || !name.endsWith('.txt')) continue;
const text = fs.readFileSync(path.join(manifestDir, name), 'utf8');
for (const line of text.split('\n')) {
const t = line.trim();
if (t) expected.add(t);
}
}
}

if (expected.size === 0) {
console.log('No VRT snapshot manifest entries; skipping orphan prune.');
process.exit(0);
}

const e2eRoot = path.join(pkgRoot, 'e2e');
let removed = 0;

function considerRemove(absPath) {
const rel = path.relative(pkgRoot, absPath).split(path.sep).join('/');
if (!expected.has(rel)) {
fs.unlinkSync(absPath);
console.log('Removed orphan snapshot:', rel);
removed += 1;
}
}

function walkPngUnderSnapshotDir(dir) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walkPngUnderSnapshotDir(p);
else if (ent.name.endsWith('.png')) considerRemove(p);
}
}

for (const ent of fs.readdirSync(e2eRoot, { withFileTypes: true })) {
if (ent.isDirectory() && ent.name.endsWith('-snapshots')) {
walkPngUnderSnapshotDir(path.join(e2eRoot, ent.name));
}
}

console.log(
`VRT orphan prune done (${removed} removed, ${expected.size} expected).`,
);
61 changes: 47 additions & 14 deletions packages/desktop-client/e2e/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { join } from 'path';

import type { Page } from '@playwright/test';

import {
currencyPrecisionTestData,
setDefaultCurrency,
} from './currency-precision';
import { expect, test } from './fixtures';
import type { AccountPage } from './page-models/account-page';
import { ConfigurationPage } from './page-models/configuration-page';
Expand All @@ -26,23 +30,52 @@ test.describe('Accounts', () => {
await page?.close();
});

test('creates a new account and views the initial balance transaction', async () => {
accountPage = await navigation.createAccount({
name: 'New Account',
offBudget: false,
balance: 100,
});
test.describe('Currency Precision', () => {
for (const {
code,
balance,
expectedDisplay,
} of currencyPrecisionTestData) {
const codeLabel = code === '' ? 'Default' : code;
test(`starting balance displayed correctly for ${codeLabel}`, async () => {
await setDefaultCurrency(page, navigation, code);
accountPage = await navigation.createAccount({
name: `${codeLabel} Local Account`,
offBudget: false,
balance,
});
await accountPage.waitFor();
await expect(accountPage.accountBalance).toContainText(expectedDisplay);

const transaction = accountPage.getNthTransaction(0);
await expect(transaction.payee).toHaveText('Starting Balance');
await expect(transaction.notes).toHaveText('');
await expect(transaction.category).toHaveText('Starting Balances');
await expect(transaction.debit).toHaveText('');
await expect(transaction.credit).toHaveText(expectedDisplay);
await expect(page).toMatchThemeScreenshots();
});

const transaction = accountPage.getNthTransaction(0);
await expect(transaction.payee).toHaveText('Starting Balance');
await expect(transaction.notes).toHaveText('');
await expect(transaction.category).toHaveText('Starting Balances');
await expect(transaction.debit).toHaveText('');
await expect(transaction.credit).toHaveText('100.00');
await expect(page).toMatchThemeScreenshots();
test(`close account shows correct balance for ${codeLabel}`, async () => {
await setDefaultCurrency(page, navigation, code);
accountPage = await navigation.createAccount({
name: `${codeLabel} Close Test`,
offBudget: false,
balance,
});
await accountPage.waitFor();
await expect(accountPage.accountBalance).toContainText(expectedDisplay);

const modal = await accountPage.clickCloseAccount();
await expect(modal.locator).toContainText('balance');
await expect(modal.locator).toContainText(expectedDisplay);
await expect(modal.locator).toMatchThemeScreenshots();
await page.reload();
});
}
});

test('closes an account', async () => {
test('closes an account with transfer', async () => {
accountPage = await navigation.goToAccountPage('Roth IRA');

await expect(accountPage.accountName).toHaveText('Roth IRA');
Expand Down
25 changes: 25 additions & 0 deletions packages/desktop-client/e2e/budget.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { Page } from '@playwright/test';

import {
currencyPrecisionTestData,
setDefaultCurrency,
} from './currency-precision';
import { expect, test } from './fixtures';
import type { BudgetPage } from './page-models/budget-page';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';

test.describe('Budget', () => {
let page: Page;
Expand Down Expand Up @@ -66,4 +71,24 @@ test.describe('Budget', () => {
expect(await accountPage.accountName.textContent()).toMatch('All Accounts');
await page.getByRole('button', { name: 'Back' }).click();
});

test.describe('Currency Precision', () => {
for (const { code } of currencyPrecisionTestData) {
const codeLabel = code === '' ? 'Default' : code;
test(`budget page displays for ${codeLabel}`, async () => {
const navigation = new Navigation(page);
await setDefaultCurrency(page, navigation, code);

await page.goto('/budget', { waitUntil: 'load' });
await page.waitForLoadState('networkidle');
await expect(budgetPage.budgetSummary.first()).toBeVisible({
timeout: 30000,
});
await expect(budgetPage.budgetTable).toBeVisible();

await page.mouse.move(0, 0);
await expect(page).toMatchThemeScreenshots();
});
}
});
});
Loading
Loading