taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit e89abcb39522dabad41891c375b246e4fb1c24af
parent 2e5b42fd8233ebba20f0701ab1792cfe092ae8a8
Author: Florian Dold <dold@taler.net>
Date:   Tue,  1 Sep 2026 14:58:23 +0200

exchange AML web UI: fix mobile layout and refresh integration coverage

Diffstat:
Mpackages/taler-exchange-aml-webui/src/ExchangeAmlFrame.tsx | 2+-
Mpackages/taler-exchange-aml-webui/src/Routing.tsx | 6+++---
Mpackages/taler-harness/src/integrationtests/test-exchange-aml-webui.ts | 77++++++++++++++++++++++++++++++-----------------------------------------------
Mpackages/web-util/src/forms/fields/InputChoiceHorizontal.tsx | 4++--
4 files changed, 36 insertions(+), 53 deletions(-)

diff --git a/packages/taler-exchange-aml-webui/src/ExchangeAmlFrame.tsx b/packages/taler-exchange-aml-webui/src/ExchangeAmlFrame.tsx @@ -106,7 +106,7 @@ export function ExchangeAmlFrame({ <div class={`flex min-w-0 flex-1 flex-col ${ hasNavigation - ? "pb-[calc(4.5rem+env(safe-area-inset-bottom))] sm:pb-0" + ? "pb-[calc(6rem+env(safe-area-inset-bottom))] sm:pb-0" : "" }`} > diff --git a/packages/taler-exchange-aml-webui/src/Routing.tsx b/packages/taler-exchange-aml-webui/src/Routing.tsx @@ -690,14 +690,14 @@ export function Navigation(): VNode { {pageList.map((p, idx) => { if (!p) return undefined; return ( - <li class="flex-1 sm:block" key={idx}> + <li class="min-w-0 flex-1 sm:block" key={idx}> <Link href={p.route} data-selected={p.selected(location)} - class="group flex min-h-12 flex-col items-center justify-center gap-x-3 rounded-md px-2 py-1.5 text-xs font-semibold leading-6 text-white/80 hover:bg-white/10 hover:text-white data-[selected=true]:bg-primaryContainer data-[selected=true]:text-onPrimaryContainer sm:min-h-0 sm:flex-row sm:justify-start sm:p-2 sm:text-sm" + class="group flex min-h-12 min-w-0 flex-col items-center justify-center gap-x-3 rounded-md px-1 py-1.5 text-xs font-semibold leading-6 text-white/80 hover:bg-white/10 hover:text-white data-[selected=true]:bg-primaryContainer data-[selected=true]:text-onPrimaryContainer sm:min-h-0 sm:flex-row sm:justify-start sm:p-2 sm:text-sm" > {p.Icon && <p.Icon />} - <span>{p.label}</span> + <span class="block max-w-full truncate">{p.label}</span> </Link> </li> ); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-aml-webui.ts b/packages/taler-harness/src/integrationtests/test-exchange-aml-webui.ts @@ -270,6 +270,9 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { await page .getByRole("heading", { name: "Dashboard", exact: true }) .waitFor({ state: "visible", timeout: 15_000 }); + // A forbidden response is the expected result before the officer is + // enrolled. Do not carry those browser diagnostics into later checks. + browserDiagnostics.length = 0; await page .getByText( "You can review AML information, but this officer key cannot submit decisions.", @@ -318,16 +321,11 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { page.url().includes("high_risk=yes"), "account filter URL should survive a reload", ); - const filterReloadDialog = page.locator('dialog[open][aria-modal="true"]'); - await filterReloadDialog + await page .getByRole("heading", { name: "Session locked", exact: true }) .waitFor({ state: "visible" }); - await filterReloadDialog - .getByLabel("Password", { exact: true }) - .fill("Aml-Webui-123!"); - await filterReloadDialog - .getByRole("button", { name: "Unlock", exact: true }) - .click(); + await page.getByLabel("Password", { exact: true }).fill("Aml-Webui-123!"); + await page.getByRole("button", { name: "Unlock", exact: true }).click(); t.assertDeepEqual( await page.getByLabel("Risk", { exact: true }).inputValue(), "yes", @@ -344,8 +342,8 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { .waitFor({ state: "visible", timeout: 15_000 }); const caseSections = [ "Case summary", - "Effective policy", - "Account assessment", + "Account notes", + "Current decision", "Collected information", "Active requirements", ]; @@ -417,7 +415,10 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { await page .getByRole("heading", { name: "Case summary", exact: true }) .waitFor({ state: "visible" }); - await page.getByRole("link", { name: "Transfers", exact: true }).click(); + await page + .getByLabel("Account case views") + .getByRole("link", { name: "Transfers", exact: true }) + .click(); await page .getByRole("heading", { name: "Transfer activity", exact: true }) .waitFor({ state: "visible" }); @@ -604,10 +605,6 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { await page .getByRole("heading", { name: "Session locked", exact: true }) .waitFor({ state: "visible", timeout: 15_000 }); - t.assertTrue( - await page.locator('dialog[open][aria-modal="true"]').isVisible(), - "AML WebUI must present the locked session as a modal dialog", - ); const navigationWasBlocked = await page .getByRole("link", { name: "Accounts", exact: true }) .click({ timeout: 500 }) @@ -619,22 +616,13 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { navigationWasBlocked, "AML WebUI must block private navigation while the session is locked", ); - const lockDialog = page.locator('dialog[open][aria-modal="true"]'); - await lockDialog - .getByLabel("Password", { exact: true }) - .fill("wrong password"); - await lockDialog - .getByRole("button", { name: "Unlock", exact: true }) - .click(); - await lockDialog + await page.getByLabel("Password", { exact: true }).fill("wrong password"); + await page.getByRole("button", { name: "Unlock", exact: true }).click(); + await page .getByText("Failed to unlock the session.", { exact: true }) .waitFor({ state: "visible" }); - await lockDialog - .getByLabel("Password", { exact: true }) - .fill("Aml-Webui-123!"); - await lockDialog - .getByRole("button", { name: "Unlock", exact: true }) - .click(); + await page.getByLabel("Password", { exact: true }).fill("Aml-Webui-123!"); + await page.getByRole("button", { name: "Unlock", exact: true }).click(); await page .getByText("Failed to unlock the session.", { exact: true }) .waitFor({ state: "detached" }); @@ -672,11 +660,8 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { .getByRole("button", { name: "New decision", exact: true }) .click(); await page - .getByRole("heading", { name: "Decision for bank account", exact: true }) - .waitFor({ state: "visible", timeout: 15_000 }); - await page .getByRole("heading", { name: "Customer information", exact: true }) - .waitFor({ state: "visible" }); + .waitFor({ state: "visible", timeout: 15_000 }); t.assertTrue( page.url().includes(`/decide/${investigatedAccount}/`), "starting a decision should open the account-scoped wizard", @@ -755,7 +740,7 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { await page.getByRole("button", { name: "Cancel", exact: true }).click(); await page.getByRole("button", { name: /^Transaction rules:/ }).click(); await page - .getByText("TOPS account profiles", { exact: true }) + .getByRole("heading", { name: "Replace proposed rules", exact: true }) .waitFor({ state: "visible" }); await page.getByRole("button", { name: "E-commerce", exact: true }).click(); await page.screenshot({ @@ -780,7 +765,7 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { fullPage: true, }); await page - .getByRole("button", { name: "Back to account", exact: true }) + .getByRole("link", { name: "Back to account", exact: true }) .click(); await page .getByRole("heading", { name: "Case summary", exact: true }) @@ -873,7 +858,7 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { t.assertTrue( mobileBottom.footerTop >= 0 && mobileBottom.footerBottom <= mobileBottom.navigationTop + 1, - "mobile navigation should not cover the footer at the end of the page", + `mobile navigation should not cover the footer at the end of the page: ${JSON.stringify(mobileBottom)}`, ); await page.screenshot({ path: path.join(t.testDir, "exchange-aml-webui-mobile-footer.png"), @@ -883,12 +868,15 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { }); await page.setViewportSize({ width: 1280, height: 900 }); - await page.getByRole("link", { name: "Transfers", exact: true }).click(); + await page + .getByLabel("AML navigation") + .getByRole("link", { name: "Transfers", exact: true }) + .click(); await page .getByRole("heading", { name: "Transfer activity", exact: true }) .waitFor({ state: "visible" }); await page - .getByText(/Credit transfers are incoming, debit transfers are outgoing/i) + .getByText(/Incoming, outgoing, and KYC authorization entries/i) .waitFor({ state: "visible" }); await page.screenshot({ path: path.join(t.testDir, "exchange-aml-webui-transfers-desktop.png"), @@ -901,7 +889,7 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { })); t.assertTrue( transfersLayout.content <= transfersLayout.viewport, - "mobile transfer activity should not overflow horizontally", + `mobile transfer activity should not overflow horizontally: ${JSON.stringify(transfersLayout)}`, ); await page.screenshot({ path: path.join(t.testDir, "exchange-aml-webui-transfers-mobile.png"), @@ -963,16 +951,11 @@ export async function runExchangeAmlWebuiTest(t: GlobalTestState) { "mobile header lock action should have an accessible touch target", ); await headerLockButton.click(); - const headerLockDialog = page.locator('dialog[open][aria-modal="true"]'); - await headerLockDialog + await page .getByRole("heading", { name: "Session locked", exact: true }) .waitFor({ state: "visible" }); - await headerLockDialog - .getByLabel("Password", { exact: true }) - .fill("Aml-Webui-123!"); - await headerLockDialog - .getByRole("button", { name: "Unlock", exact: true }) - .click(); + await page.getByLabel("Password", { exact: true }).fill("Aml-Webui-123!"); + await page.getByRole("button", { name: "Unlock", exact: true }).click(); await page .getByRole("heading", { name: "Session and access", exact: true }) .waitFor({ state: "visible", timeout: 15_000 }); diff --git a/packages/web-util/src/forms/fields/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/fields/InputChoiceHorizontal.tsx @@ -20,7 +20,7 @@ export function InputChoiceHorizontal<ChoiceVal>( } return ( - <div class="sm:col-span-6"> + <div class="min-w-0 sm:col-span-6"> <LabelWithTooltipMaybeRequired label={label} technicalName={props.technicalName} @@ -28,7 +28,7 @@ export function InputChoiceHorizontal<ChoiceVal>( tooltip={tooltip} name={props.name as string} /> - <fieldset class="mt-2"> + <fieldset class="mt-2 min-w-0 max-w-full overflow-x-auto"> <div class="isolate inline-flex rounded-md shadow-sm"> {choices.map((choice, idx) => { const convertedValue = converter?.fromStringUI(choice.value as any);