|
| 1 | +import { test, expect, Page } from '@playwright/test'; |
| 2 | + |
| 3 | +/** |
| 4 | + * E2E Tests: Accounting Page - T-Account Balance Sheet |
| 5 | + * |
| 6 | + * Tests use the REAL API - no mocking allowed! |
| 7 | + * |
| 8 | + * Visual documentation of the accounting balance flow: |
| 9 | + * 1. Open accounting page |
| 10 | + * 2. Select year 2025 |
| 11 | + * 3. Select bank "Maerki Baumann (CHF)" |
| 12 | + * 4. Verify T-account display with: |
| 13 | + * - Anfangsbestand (Opening Balance) |
| 14 | + * - Alle Einnahmen (Total Income) |
| 15 | + * - Alle Ausgaben (Total Expenses) |
| 16 | + * - Total row |
| 17 | + * - Saldo (Closing Balance) |
| 18 | + * - Validation message |
| 19 | + * |
| 20 | + * Each step produces a baseline screenshot for visual regression testing. |
| 21 | + * |
| 22 | + * Required env vars: ADMIN_ADDRESS, ADMIN_SIGNATURE |
| 23 | + */ |
| 24 | + |
| 25 | +const ADMIN_ADDRESS = process.env.ADMIN_ADDRESS || ''; |
| 26 | +const ADMIN_SIGNATURE = process.env.ADMIN_SIGNATURE || ''; |
| 27 | + |
| 28 | +// Bank IBANs for selection |
| 29 | +const MAERKI_CHF_IBAN = 'CH3408573177975200001'; |
| 30 | +const MAERKI_EUR_IBAN = 'CH6808573177975201814'; |
| 31 | +const RAIFFEISEN_CHF_IBAN = 'CH4880808002186504370'; |
| 32 | + |
| 33 | +async function removeErrorOverlay(page: Page): Promise<void> { |
| 34 | + await page.evaluate(() => { |
| 35 | + const overlay = document.getElementById('webpack-dev-server-client-overlay'); |
| 36 | + if (overlay) overlay.remove(); |
| 37 | + }); |
| 38 | +} |
| 39 | + |
| 40 | +async function waitForAppLoaded(page: Page): Promise<void> { |
| 41 | + await page.waitForTimeout(3000); |
| 42 | +} |
| 43 | + |
| 44 | +async function waitForBalanceSheet(page: Page): Promise<void> { |
| 45 | + await page.waitForSelector('[data-testid="balance-sheet"]', { state: 'visible', timeout: 10000 }); |
| 46 | +} |
| 47 | + |
| 48 | +test.describe('Accounting Page - T-Account Balance Sheet (Real API)', () => { |
| 49 | + test.beforeAll(() => { |
| 50 | + if (!ADMIN_ADDRESS || !ADMIN_SIGNATURE) { |
| 51 | + throw new Error('ADMIN_ADDRESS and ADMIN_SIGNATURE must be set in .env'); |
| 52 | + } |
| 53 | + }); |
| 54 | + |
| 55 | + test.describe('Step-by-Step Flow: Maerki Baumann CHF 2025', () => { |
| 56 | + /** |
| 57 | + * Step 1: Open the accounting page |
| 58 | + * - Page loads with admin authentication |
| 59 | + * - Shows title "DFX Accounting Report" |
| 60 | + * - First bank is auto-selected |
| 61 | + * - Balance sheet loads automatically |
| 62 | + */ |
| 63 | + test('Step 1: Open accounting page - auto-loads first bank', async ({ page }) => { |
| 64 | + await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`); |
| 65 | + await waitForAppLoaded(page); |
| 66 | + await removeErrorOverlay(page); |
| 67 | + |
| 68 | + // Verify page title is visible |
| 69 | + await expect(page.locator('h1')).toContainText('DFX Accounting Report'); |
| 70 | + |
| 71 | + // Verify dropdowns are visible |
| 72 | + const yearSelect = page.locator('select').first(); |
| 73 | + const bankSelect = page.locator('select').nth(1); |
| 74 | + await expect(yearSelect).toBeVisible(); |
| 75 | + await expect(bankSelect).toBeVisible(); |
| 76 | + |
| 77 | + // Wait for balance sheet to load (first bank is auto-selected) |
| 78 | + await waitForBalanceSheet(page); |
| 79 | + |
| 80 | + // Balance sheet should be visible (first bank auto-selected) |
| 81 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 82 | + await expect(balanceSheet).toBeVisible(); |
| 83 | + |
| 84 | + // Capture baseline screenshot |
| 85 | + await expect(page).toHaveScreenshot('01-accounting-page-opened.png', { |
| 86 | + fullPage: true, |
| 87 | + animations: 'disabled', |
| 88 | + }); |
| 89 | + }); |
| 90 | + |
| 91 | + /** |
| 92 | + * Step 2: Select Maerki Baumann CHF and year 2025 |
| 93 | + * - T-Account table appears with real data from API |
| 94 | + */ |
| 95 | + test('Step 2: Select Maerki Baumann CHF 2025 - shows T-account', async ({ page }) => { |
| 96 | + await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`); |
| 97 | + await waitForAppLoaded(page); |
| 98 | + await removeErrorOverlay(page); |
| 99 | + |
| 100 | + // Select year 2025 |
| 101 | + const yearSelect = page.locator('select').first(); |
| 102 | + await yearSelect.selectOption('2025'); |
| 103 | + await page.waitForTimeout(300); |
| 104 | + |
| 105 | + // Select Maerki Baumann (CHF) by IBAN |
| 106 | + const bankSelect = page.locator('select').nth(1); |
| 107 | + await bankSelect.selectOption(MAERKI_CHF_IBAN); |
| 108 | + |
| 109 | + // Wait for balance sheet to load from real API |
| 110 | + await waitForBalanceSheet(page); |
| 111 | + |
| 112 | + // Verify bank is selected |
| 113 | + await expect(bankSelect).toHaveValue(MAERKI_CHF_IBAN); |
| 114 | + |
| 115 | + // Verify T-account is visible |
| 116 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 117 | + await expect(balanceSheet).toBeVisible(); |
| 118 | + |
| 119 | + // Verify IBAN is displayed |
| 120 | + await expect(balanceSheet).toContainText(MAERKI_CHF_IBAN); |
| 121 | + |
| 122 | + // Verify opening balance is displayed (value from yearlyBalances in DB) |
| 123 | + const openingBalance = page.locator('[data-testid="opening-balance"]'); |
| 124 | + await expect(openingBalance).toBeVisible(); |
| 125 | + |
| 126 | + // Verify total income is displayed |
| 127 | + const totalIncome = page.locator('[data-testid="total-income"]'); |
| 128 | + await expect(totalIncome).toBeVisible(); |
| 129 | + |
| 130 | + // Verify total expenses is displayed |
| 131 | + const totalExpenses = page.locator('[data-testid="total-expenses"]'); |
| 132 | + await expect(totalExpenses).toBeVisible(); |
| 133 | + |
| 134 | + // Verify closing balance (Saldo) is displayed |
| 135 | + const closingBalance = page.locator('[data-testid="closing-balance"]'); |
| 136 | + await expect(closingBalance).toBeVisible(); |
| 137 | + |
| 138 | + // Capture baseline screenshot with T-account data |
| 139 | + await expect(page).toHaveScreenshot('02-maerki-chf-t-account-displayed.png', { |
| 140 | + fullPage: true, |
| 141 | + animations: 'disabled', |
| 142 | + }); |
| 143 | + }); |
| 144 | + }); |
| 145 | + |
| 146 | + test.describe('Additional Balance Scenarios', () => { |
| 147 | + /** |
| 148 | + * Maerki Baumann EUR 2025 |
| 149 | + */ |
| 150 | + test('Maerki Baumann EUR 2025 T-account', async ({ page }) => { |
| 151 | + await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`); |
| 152 | + await waitForAppLoaded(page); |
| 153 | + await removeErrorOverlay(page); |
| 154 | + |
| 155 | + const yearSelect = page.locator('select').first(); |
| 156 | + await yearSelect.selectOption('2025'); |
| 157 | + |
| 158 | + const bankSelect = page.locator('select').nth(1); |
| 159 | + await bankSelect.selectOption(MAERKI_EUR_IBAN); |
| 160 | + |
| 161 | + await waitForBalanceSheet(page); |
| 162 | + |
| 163 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 164 | + await expect(balanceSheet).toBeVisible(); |
| 165 | + |
| 166 | + // Verify IBAN is displayed |
| 167 | + await expect(balanceSheet).toContainText(MAERKI_EUR_IBAN); |
| 168 | + |
| 169 | + // Verify opening balance is displayed |
| 170 | + const openingBalance = page.locator('[data-testid="opening-balance"]'); |
| 171 | + await expect(openingBalance).toBeVisible(); |
| 172 | + |
| 173 | + // Verify closing balance is displayed |
| 174 | + const closingBalance = page.locator('[data-testid="closing-balance"]'); |
| 175 | + await expect(closingBalance).toBeVisible(); |
| 176 | + |
| 177 | + await expect(page).toHaveScreenshot('03-maerki-eur-t-account-displayed.png', { |
| 178 | + fullPage: true, |
| 179 | + animations: 'disabled', |
| 180 | + }); |
| 181 | + }); |
| 182 | + |
| 183 | + /** |
| 184 | + * Raiffeisen CHF 2025 |
| 185 | + */ |
| 186 | + test('Raiffeisen CHF 2025 T-account', async ({ page }) => { |
| 187 | + await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`); |
| 188 | + await waitForAppLoaded(page); |
| 189 | + await removeErrorOverlay(page); |
| 190 | + |
| 191 | + const yearSelect = page.locator('select').first(); |
| 192 | + await yearSelect.selectOption('2025'); |
| 193 | + |
| 194 | + const bankSelect = page.locator('select').nth(1); |
| 195 | + await bankSelect.selectOption(RAIFFEISEN_CHF_IBAN); |
| 196 | + |
| 197 | + await waitForBalanceSheet(page); |
| 198 | + |
| 199 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 200 | + await expect(balanceSheet).toBeVisible(); |
| 201 | + |
| 202 | + // Verify IBAN is displayed |
| 203 | + await expect(balanceSheet).toContainText(RAIFFEISEN_CHF_IBAN); |
| 204 | + |
| 205 | + // Verify opening balance is displayed |
| 206 | + const openingBalance = page.locator('[data-testid="opening-balance"]'); |
| 207 | + await expect(openingBalance).toBeVisible(); |
| 208 | + |
| 209 | + // Verify closing balance is displayed |
| 210 | + const closingBalance = page.locator('[data-testid="closing-balance"]'); |
| 211 | + await expect(closingBalance).toBeVisible(); |
| 212 | + |
| 213 | + await expect(page).toHaveScreenshot('04-raiffeisen-chf-t-account-displayed.png', { |
| 214 | + fullPage: true, |
| 215 | + animations: 'disabled', |
| 216 | + }); |
| 217 | + }); |
| 218 | + |
| 219 | + /** |
| 220 | + * Year 2024 - test with different year |
| 221 | + */ |
| 222 | + test('Year 2024 - balance sheet loads', async ({ page }) => { |
| 223 | + await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`); |
| 224 | + await waitForAppLoaded(page); |
| 225 | + await removeErrorOverlay(page); |
| 226 | + |
| 227 | + // Select year 2024 |
| 228 | + const yearSelect = page.locator('select').first(); |
| 229 | + await yearSelect.selectOption('2024'); |
| 230 | + |
| 231 | + // Select Raiffeisen |
| 232 | + const bankSelect = page.locator('select').nth(1); |
| 233 | + await bankSelect.selectOption(RAIFFEISEN_CHF_IBAN); |
| 234 | + |
| 235 | + await waitForBalanceSheet(page); |
| 236 | + |
| 237 | + // Balance sheet should be visible |
| 238 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 239 | + await expect(balanceSheet).toBeVisible(); |
| 240 | + |
| 241 | + // Verify opening and closing balance are displayed |
| 242 | + const openingBalance = page.locator('[data-testid="opening-balance"]'); |
| 243 | + await expect(openingBalance).toBeVisible(); |
| 244 | + |
| 245 | + const closingBalance = page.locator('[data-testid="closing-balance"]'); |
| 246 | + await expect(closingBalance).toBeVisible(); |
| 247 | + |
| 248 | + await expect(page).toHaveScreenshot('05-year-2024-balance-sheet.png', { |
| 249 | + fullPage: true, |
| 250 | + animations: 'disabled', |
| 251 | + }); |
| 252 | + }); |
| 253 | + }); |
| 254 | + |
| 255 | + test.describe('Direct URL with Preselected Parameters', () => { |
| 256 | + /** |
| 257 | + * Test direct URL access with year and bank preselected via URL params |
| 258 | + * URL format: /accounting?year=2025&bank=CH3408573177975200001&address=...&signature=... |
| 259 | + * |
| 260 | + * Expected behavior: |
| 261 | + * - Year dropdown shows 2025 |
| 262 | + * - Bank dropdown shows "Maerki Baumann (CHF)" |
| 263 | + * - T-account balance sheet loads automatically from real API |
| 264 | + */ |
| 265 | + test('Direct link with year=2025 and bank=Maerki Baumann CHF', async ({ page }) => { |
| 266 | + // Navigate using direct URL with all parameters preselected |
| 267 | + const directUrl = `/accounting?year=2025&bank=${MAERKI_CHF_IBAN}&address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`; |
| 268 | + await page.goto(directUrl); |
| 269 | + await waitForAppLoaded(page); |
| 270 | + await removeErrorOverlay(page); |
| 271 | + |
| 272 | + // Verify page title |
| 273 | + await expect(page.locator('h1')).toContainText('DFX Accounting Report'); |
| 274 | + |
| 275 | + // Verify year is preselected to 2025 |
| 276 | + const yearSelect = page.locator('select').first(); |
| 277 | + await expect(yearSelect).toHaveValue('2025'); |
| 278 | + |
| 279 | + // Verify bank is preselected to Maerki Baumann CHF |
| 280 | + const bankSelect = page.locator('select').nth(1); |
| 281 | + await expect(bankSelect).toHaveValue(MAERKI_CHF_IBAN); |
| 282 | + |
| 283 | + // Wait for balance sheet to load from real API |
| 284 | + await waitForBalanceSheet(page); |
| 285 | + |
| 286 | + // Verify T-account balance sheet is visible (auto-loaded) |
| 287 | + const balanceSheet = page.locator('[data-testid="balance-sheet"]'); |
| 288 | + await expect(balanceSheet).toBeVisible(); |
| 289 | + |
| 290 | + // Verify IBAN is displayed in balance sheet |
| 291 | + await expect(balanceSheet).toContainText(MAERKI_CHF_IBAN); |
| 292 | + |
| 293 | + // Verify opening balance is displayed |
| 294 | + const openingBalance = page.locator('[data-testid="opening-balance"]'); |
| 295 | + await expect(openingBalance).toBeVisible(); |
| 296 | + |
| 297 | + // Verify closing balance (Saldo) is displayed |
| 298 | + const closingBalance = page.locator('[data-testid="closing-balance"]'); |
| 299 | + await expect(closingBalance).toBeVisible(); |
| 300 | + |
| 301 | + // Capture screenshot of direct link result |
| 302 | + await expect(page).toHaveScreenshot('06-direct-link-maerki-chf-2025.png', { |
| 303 | + fullPage: true, |
| 304 | + animations: 'disabled', |
| 305 | + }); |
| 306 | + }); |
| 307 | + }); |
| 308 | +}); |
0 commit comments