import { fileURLToPath } from 'node:url'; import { es } from '@impuestos/i18n'; import { expect, test, type Page } from '@playwright/test'; /** * Golden paths 2 and 3 (SPEC.md section 13). Both run through the real UI: the browser * decodes the fixture's QR itself, uploads the image, and the document comes back through * the same pipeline a phone would use. */ // Serial: these tests confirm and reject out of one account's bandeja, so running two of // them at once has each pulling cards out from under the other. test.describe.configure({ mode: 'serial' }); const fixture = (name: string) => fileURLToPath(new URL(`../fixtures/${name}.png`, import.meta.url)); async function signIn(page: Page): Promise { await page.goto('/es/login'); await page.getByLabel(es['auth.register.email']).fill('maria@demo.local'); await page.getByLabel(es['auth.login.password']).fill('demo-maria-1'); await page.getByRole('button', { name: es['auth.login.submit'] }).click(); await expect(page).toHaveURL(/\/es\/inicio$/); } /** * These confirm and reject documents out of one shared demo account, so running them in * two projects at once has the two workers racing for the same card. They run on desktop * only; bandeja rendering on a phone viewport is covered by bandeja.spec.ts, which reads * and never mutates. */ test.describe('golden path 2: scan a QR comprobante and confirm it', () => { test.beforeEach(() => { test.skip(test.info().project.name !== 'desktop', 'mutates shared seed data'); }); test('reads the fixture, then confirms it out of the bandeja', async ({ page }) => { await signIn(page); await page.goto('/es/escanear'); // "Subir archivo" is the same code path as the camera: decode, then upload. await page.locator('input[type="file"]').setInputFiles(fixture('factura-qr-supermercado')); // Whether this is the first scan of the fixture or a repeat depends on what the // stack has seen before, and path 2 is about the data, not about which of the two // headings appears. The duplicate case has its own test below. await expect(page.getByText(es['scan.registered'])).toBeVisible({ timeout: 20_000 }); // A QR carries the emitter's RUC but not its name, so that is what is shown. await expect(page.getByText('RUC 80011223')).toBeVisible(); await expect(page.getByText('Gs. 385.000')).toBeVisible(); // Exact: the duplicate note also names the date, in a sentence. await expect(page.getByText('14 de agosto', { exact: true })).toBeVisible(); await page.getByRole('link', { name: es['scan.goToBandeja'] }).click(); await expect(page).toHaveURL(/\/es\/bandeja$/); // Confirming empties the bandeja, so a suite run consumes what it needs. The reset in // the README is the precondition; this says so rather than timing out on a locator. const card = page.getByTestId('bandeja-card'); await expect( card, 'the bandeja is empty: run pnpm db:reset with the stack stopped before the suite', ).toBeVisible(); const emitter = (await card.locator('p').first().textContent()) ?? ''; await page.getByRole('button', { name: es['bandeja.confirm'] }).click(); // The confirmed card leaves the stack. await expect(async () => { const next = await page.getByTestId('bandeja-card').locator('p').first().textContent(); expect(next).not.toBe(emitter); }).toPass({ timeout: 10_000 }); }); // CONTRACTS.md 5.1, through the UI this time. Scanning twice inside the test makes the // second outcome deterministic whatever the stack has seen before. test('scanning the same fixture twice says so instead of duplicating it', async ({ page }) => { await signIn(page); await page.goto('/es/escanear'); await page.locator('input[type="file"]').setInputFiles(fixture('factura-qr-farmacia')); await expect(page.getByText(es['scan.registered'])).toBeVisible({ timeout: 20_000 }); await page.goto('/es/escanear'); await page.locator('input[type="file"]').setInputFiles(fixture('factura-qr-farmacia')); await expect(page.getByRole('heading', { name: es['scan.duplicate.title'] })).toBeVisible({ timeout: 20_000, }); await expect(page.getByText(es['scan.duplicate.body'].split('{')[0] as string)).toBeVisible(); }); test('a photo with no QR sends you to the manual form with the file attached', async ({ page }) => { await signIn(page); await page.goto('/es/escanear'); await page.locator('input[type="file"]').setInputFiles(fixture('factura-sin-qr-ferreteria')); await expect(page).toHaveURL(/\/es\/comprobantes\/nuevo\?fileId=/, { timeout: 20_000 }); await expect(page.getByText(es['scan.needsManual'])).toBeVisible(); }); }); test.describe('golden path 3: enter a factura by hand and change its category', () => { test.beforeEach(() => { test.skip(test.info().project.name !== 'desktop', 'mutates shared seed data'); }); test('saves it, classifies it, and takes a correction', async ({ page }) => { await signIn(page); await page.goto('/es/comprobantes/nuevo'); // The dedupe key is the issuer, the date and the total, not the name, so the total // has to differ per run or this correctly merges into a previous run's document. const emitter = `PANADERIA PRUEBA ${Date.now()}`; const total = 170_000 + (Date.now() % 9_000); await page.getByLabel(es['manual.emitterRuc']).fill('80022114-1'); await page.getByLabel(es['manual.emitterName']).fill(emitter); await page.getByLabel(es['manual.total']).fill(String(total)); await page.getByRole('button', { name: es['manual.submit'] }).click(); await expect(page).toHaveURL(/\/es\/bandeja$/, { timeout: 15_000 }); // PANADERIA is an alimentacion keyword, so that is what the rules suggested. const card = page.getByTestId('bandeja-card'); await expect(card).toContainText(emitter); await expect(card).toContainText(es['categories.alimentacion']); // Correct it to Salud, which is the user overriding the classifier. await page.getByRole('button', { name: es['scan.result.changeCat'] }).click(); await page.getByRole('button', { name: es['categories.salud'] }).click(); await expect(card).toContainText(es['categories.salud']); await page.getByRole('button', { name: es['bandeja.confirm'] }).click(); // The correction survives into the document detail. await page.goto('/es/comprobantes'); await page.getByRole('link').filter({ hasText: emitter }).first().click(); await expect(page.getByTestId('document-category')).toHaveText(es['categories.salud']); }); test('refuses a RUC whose check digit does not match', async ({ page }) => { await signIn(page); await page.goto('/es/comprobantes/nuevo'); await page.getByLabel(es['manual.emitterRuc']).fill('4123456-9'); await expect(page.getByText(es['landing.invalidDoc'])).toBeVisible(); await expect(page.getByRole('button', { name: es['manual.submit'] })).toBeDisabled(); }); });