import { test, expect } from '@playwright/test'; // The SSR manual transaction import accepts the exact Yodlee positional-column // TSV format from the master branch. Column order (14 columns), per // auto-ap.import.manual/columns: // 0:status 1:raw-date 2:description-original 3:high-level-category // 4,5:(unused) 6:amount 7..11:(unused) 12:bank-account-code 13:client-code // // The test server (auto-ap.test-server) seeds client "TEST" with a bank // account whose code is the deterministic "TEST-CHK" (see seed-test-data). const IMPORT_PATH = '/transaction2/external-import-new'; function yodleeRow(opts: { status?: string; date?: string; description?: string; category?: string; amount?: string; bankAccountCode?: string; clientCode?: string; }): string { const cols = new Array(14).fill(''); cols[0] = opts.status ?? 'POSTED'; cols[1] = opts.date ?? ''; cols[2] = opts.description ?? ''; cols[3] = opts.category ?? ''; cols[6] = opts.amount ?? ''; cols[12] = opts.bankAccountCode ?? ''; cols[13] = opts.clientCode ?? ''; return cols.join('\t'); } function yodleeTsv(rows: string[]): string { // First line is a header that the importer drops. const header = new Array(14).fill(''); header[0] = 'Status'; header[1] = 'Date'; header[2] = 'Description'; header[6] = 'Amount'; header[12] = 'Bank Account'; header[13] = 'Client'; return [header.join('\t'), ...rows].join('\n'); } async function gotoImport(page: any) { await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' }); await page.goto(IMPORT_PATH); } async function pasteAndParse(page: any, tsv: string) { const textarea = page.locator('#parse-form textarea').first(); await textarea.fill(tsv); // A visible "Parse" button submits the paste form (htmx swaps in the grid). await page.getByRole('button', { name: /parse/i }).click(); await page.waitForTimeout(800); } test.describe('Manual Transaction Import (SSR)', () => { test('renders the import page with a paste box', async ({ page }) => { await gotoImport(page); await expect(page.locator('#parse-form textarea').first()).toBeVisible(); }); test('paste -> parse -> review grid -> import a valid transaction', async ({ page }) => { await gotoImport(page); const description = 'E2E Imported Coffee'; const tsv = yodleeTsv([ yodleeRow({ date: '01/15/2024', description, category: 'Food', amount: '12.50', bankAccountCode: 'TEST-CHK', clientCode: 'TEST', }), ]); await pasteAndParse(page, tsv); // The review grid renders the parsed row as editable inputs (the // description lives in an input value, so assert on the input, not text). await expect(page.locator('input[value="TEST-CHK"]').first()).toBeVisible(); await expect(page.locator(`input[value="${description}"]`).first()).toBeVisible(); // Import the clean batch. await page.getByRole('button', { name: /^import$/i }).click(); await page.waitForTimeout(1000); // The imported transaction shows up on the transactions list. await page.goto('/transaction2?date-range=all'); await page.waitForSelector('table tbody tr'); await expect(page.getByText(description)).toBeVisible(); }); test('blocks the whole batch when a row has an unknown bank-account code', async ({ page }) => { await gotoImport(page); const description = 'E2E Blocked Row'; const tsv = yodleeTsv([ yodleeRow({ date: '01/16/2024', description, amount: '20.00', bankAccountCode: 'NOPE-DOES-NOT-EXIST', clientCode: 'TEST', }), ]); await pasteAndParse(page, tsv); // The grid surfaces a blocking error for the bad row. The importer reuses // the master-branch message wording ("Cannot find bank account by code …"). await expect(page.getByText(/cannot find bank account/i).first()).toBeVisible(); // Importing does not create the transaction (batch blocked). await page.getByRole('button', { name: /^import$/i }).click(); await page.waitForTimeout(800); await page.goto('/transaction2?date-range=all'); await page.waitForSelector('table tbody tr'); await expect(page.getByText(description)).toHaveCount(0); }); });