From 3653b95b3cbb9c6611ae47de3d7134525cec572f Mon Sep 17 00:00:00 2001 From: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:44:43 +0200 Subject: [PATCH 1/4] test(account-merge): cover empty otp, 404, DeadLetter and Failed jobs The confirmation page already handles these API states; the suite now opens each one the way a mail link would. --- tests/behavior.spec.mjs | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs index 6248c0f..9ee6bbb 100644 --- a/tests/behavior.spec.mjs +++ b/tests/behavior.spec.mjs @@ -557,6 +557,83 @@ test.describe('account-merge flow', () => { await expect(page.locator('#state-invalid h1')).toHaveText(expected); }); + test('an empty otp shows the invalid state and makes no confirm request', async ({ page }) => { + const confirmCalls = []; + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => { + confirmCalls.push(route.request().url()); + route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); + }); + await page.goto('/account-merge/?otp='); + await expect(page.locator('#state-invalid')).toBeVisible(); + expect(confirmCalls).toEqual([]); + }); + + test('a 404 response shows the invalid state', async ({ page }) => { + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + route.fulfill({ status: 404, contentType: 'application/json', body: '{}' }), + ); + await page.goto('/account-merge/?otp=abc'); + await expect(page.locator('#state-invalid')).toBeVisible(); + }); + + test('a 202 ticket already DeadLetter skips job polling and shows unavailable', async ({ page }) => { + let jobCalls = 0; + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + route.fulfill({ + status: 202, + contentType: 'application/json', + body: JSON.stringify({ uid: 'job-1', status: 'DeadLetter', expectedSeconds: 2 }), + }), + ); + await page.route(MERGE_JOB_ENDPOINT, (route) => { + jobCalls += 1; + route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); + }); + await page.goto('/account-merge/?otp=abc'); + await expect(page.locator('#state-unavailable')).toBeVisible({ timeout: 10000 }); + expect(jobCalls).toBe(0); + }); + + test('a job that becomes Failed after polling shows unavailable', async ({ page }) => { + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + route.fulfill({ + status: 202, + contentType: 'application/json', + body: JSON.stringify({ uid: 'job-1', status: 'Pending', expectedSeconds: 2 }), + }), + ); + await page.route(MERGE_JOB_ENDPOINT, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ uid: 'job-1', status: 'Failed' }), + }), + ); + await page.goto('/account-merge/?otp=abc'); + await expect(page.locator('#state-unavailable')).toBeVisible({ timeout: 10000 }); + }); + + test('confirmed state shows the RealUnit logo', async ({ page }) => { + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ kycHash: 'x' }), + }), + ); + await page.goto('/account-merge/?otp=abc'); + await expect(page.locator('#state-confirmed')).toBeVisible(); + await expect(page.locator('img.logo')).toBeVisible(); + await expect(page.locator('img.logo')).toHaveAttribute('src', '/assets/realunit-logo.png'); + }); + + test('?lang=de renders German copy and sets ', async ({ page }) => { + await page.goto('/account-merge/?mock=invalid&lang=de'); + await expect(page.locator('html')).toHaveAttribute('lang', 'de'); + const expected = await page.evaluate(() => window.RealUnitMerge.I18N.de['invalid.title']); + await expect(page.locator('#state-invalid h1')).toHaveText(expected); + }); + test('an ?api= override sends the confirmation to that API base', async ({ page }) => { let requestedUrl = null; await page.route(MERGE_CONFIRM_ENDPOINT, (route) => { From 752b745f571a64c9e8688260ab0287d9f1ae32ad Mon Sep 17 00:00:00 2001 From: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:38:53 +0200 Subject: [PATCH 2/4] test(account-merge): match sibling assertions and printWidth Empty OTP also asserts the loading state is hidden. DeadLetter counts the confirm request like the existing Failed case. --- tests/behavior.spec.mjs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs index 9ee6bbb..8843fb5 100644 --- a/tests/behavior.spec.mjs +++ b/tests/behavior.spec.mjs @@ -565,6 +565,7 @@ test.describe('account-merge flow', () => { }); await page.goto('/account-merge/?otp='); await expect(page.locator('#state-invalid')).toBeVisible(); + await expect(page.locator('#state-loading')).toBeHidden(); expect(confirmCalls).toEqual([]); }); @@ -576,21 +577,30 @@ test.describe('account-merge flow', () => { await expect(page.locator('#state-invalid')).toBeVisible(); }); - test('a 202 ticket already DeadLetter skips job polling and shows unavailable', async ({ page }) => { + test('a 202 ticket already DeadLetter skips job polling and shows unavailable', async ({ + page, + }) => { + let confirmCalls = 0; let jobCalls = 0; - await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => { + confirmCalls += 1; route.fulfill({ status: 202, contentType: 'application/json', - body: JSON.stringify({ uid: 'job-1', status: 'DeadLetter', expectedSeconds: 2 }), - }), - ); + body: JSON.stringify({ + uid: 'job-1', + status: 'DeadLetter', + expectedSeconds: 2, + }), + }); + }); await page.route(MERGE_JOB_ENDPOINT, (route) => { jobCalls += 1; route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); }); await page.goto('/account-merge/?otp=abc'); await expect(page.locator('#state-unavailable')).toBeVisible({ timeout: 10000 }); + expect(confirmCalls).toBe(1); expect(jobCalls).toBe(0); }); From 1a8d178f63c6963b8916c2aab29b4091122ff35d Mon Sep 17 00:00:00 2001 From: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com> Date: Sat, 29 Aug 2026 17:06:50 +0200 Subject: [PATCH 3/4] test(account-merge): count the job GET on a Failed poll A regression that skipped polling would still show unavailable. The test now requires the confirm and the job request. --- tests/behavior.spec.mjs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs index 8843fb5..c9366d5 100644 --- a/tests/behavior.spec.mjs +++ b/tests/behavior.spec.mjs @@ -605,22 +605,28 @@ test.describe('account-merge flow', () => { }); test('a job that becomes Failed after polling shows unavailable', async ({ page }) => { - await page.route(MERGE_CONFIRM_ENDPOINT, (route) => + let confirmCalls = 0; + let jobCalls = 0; + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => { + confirmCalls += 1; route.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ uid: 'job-1', status: 'Pending', expectedSeconds: 2 }), - }), - ); - await page.route(MERGE_JOB_ENDPOINT, (route) => + }); + }); + await page.route(MERGE_JOB_ENDPOINT, (route) => { + jobCalls += 1; route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ uid: 'job-1', status: 'Failed' }), - }), - ); + }); + }); await page.goto('/account-merge/?otp=abc'); await expect(page.locator('#state-unavailable')).toBeVisible({ timeout: 10000 }); + expect(confirmCalls).toBe(1); + expect(jobCalls).toBe(1); }); test('confirmed state shows the RealUnit logo', async ({ page }) => { From 87654609762cd0812dad37c2435471a4f7e34a11 Mon Sep 17 00:00:00 2001 From: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:16:15 +0200 Subject: [PATCH 4/4] test(account-merge): cover DeadLetter after a pending job poll A ticket that is Pending on confirm and DeadLetter on the first job GET must show unavailable and must have issued the job request. --- tests/behavior.spec.mjs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs index c9366d5..a258f14 100644 --- a/tests/behavior.spec.mjs +++ b/tests/behavior.spec.mjs @@ -604,6 +604,31 @@ test.describe('account-merge flow', () => { expect(jobCalls).toBe(0); }); + test('a job that becomes DeadLetter after polling shows unavailable', async ({ page }) => { + let confirmCalls = 0; + let jobCalls = 0; + await page.route(MERGE_CONFIRM_ENDPOINT, (route) => { + confirmCalls += 1; + route.fulfill({ + status: 202, + contentType: 'application/json', + body: JSON.stringify({ uid: 'job-1', status: 'Pending', expectedSeconds: 2 }), + }); + }); + await page.route(MERGE_JOB_ENDPOINT, (route) => { + jobCalls += 1; + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ uid: 'job-1', status: 'DeadLetter' }), + }); + }); + await page.goto('/account-merge/?otp=abc'); + await expect(page.locator('#state-unavailable')).toBeVisible({ timeout: 10000 }); + expect(confirmCalls).toBe(1); + expect(jobCalls).toBe(1); + }); + test('a job that becomes Failed after polling shows unavailable', async ({ page }) => { let confirmCalls = 0; let jobCalls = 0;