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:
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);