taler-typescript-core

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

commit 577ebe51348eaae035a5928520eabc59e34a2693
parent 26d9884255678297948a8be2f8f6ba5007f5bb96
Author: Florian Dold <dold@taler.net>
Date:   Wed, 26 Aug 2026 00:04:22 +0200

taler-harness: update mytops onboarding for current UI

Diffstat:
Mpackages/taler-harness/src/stagefright/merchant-mytops.ts | 127+++++++++++++++++++++++++++++++++++++------------------------------------------
1 file changed, 59 insertions(+), 68 deletions(-)

diff --git a/packages/taler-harness/src/stagefright/merchant-mytops.ts b/packages/taler-harness/src/stagefright/merchant-mytops.ts @@ -101,32 +101,26 @@ function normalizeBaseUrl(url: string): string { return url.endsWith("/") ? url : `${url}/`; } -type ChallengeDialog = "solve" | "choose" | "logged-in"; +type RegistrationState = "email" | "sms" | "choose" | "logged-in"; /** - * Wait for whatever comes after submitting a form that needs MFA: a dialog - * asking for a code, a dialog asking which channel to use, or the portal - * itself once no challenge is left. - * - * The channel dialog is only expected right after submitting the form. Once - * a challenge has been solved it shows up again while the original request is - * retried, and answering it there would wait for a code that is never sent. + * Wait for whatever comes after submitting the registration form: a code + * field, a choice of MFA channel, or the portal once onboarding is complete. */ -async function waitForChallengeDialog( +async function waitForRegistrationState( page: Page, timeoutMs: number, - allowChannelChoice: boolean, -): Promise<ChallengeDialog> { +): Promise<RegistrationState> { const deadline = Date.now() + timeoutMs; let lastComplaint: string | undefined = undefined; for (;;) { - if ((await page.locator('input[name="code"]').count()) > 0) { - return "solve"; + if ((await page.locator("#signup-email-code").count()) > 0) { + return "email"; } - if ( - allowChannelChoice && - (await page.locator('input[name="challenge_id"]').count()) > 0 - ) { + if ((await page.locator("#signup-sms-code").count()) > 0) { + return "sms"; + } + if ((await page.locator('input[name="signup_2fa_channel"]').count()) > 0) { return "choose"; } // The navigation sidebar only exists for a logged-in instance. @@ -135,7 +129,7 @@ async function waitForChallengeDialog( } // Error notifications dismiss themselves after a while, so remember what // the last one said instead of only reporting a timeout. - const complaint = page.locator(".message.is-danger"); + const complaint = page.locator("[data-error-banner]"); if ((await complaint.count()) > 0) { lastComplaint = (await complaint.first().innerText()) .replace(/\s+/g, " ") @@ -146,7 +140,7 @@ async function waitForChallengeDialog( throw Error(`the deployment refused the request: ${lastComplaint}`); } throw Error( - `neither an MFA challenge nor the merchant portal showed up within ${timeoutMs}ms`, + `neither a registration challenge nor the merchant portal showed up within ${timeoutMs}ms`, ); } await page.waitForTimeout(250); @@ -154,36 +148,34 @@ async function waitForChallengeDialog( } /** - * Type the code into the dialog and wait for the dialog to disappear. The - * last digit submits the form, so there is no button to press. + * Submit one registration challenge and wait until the UI advances. Waiting + * for the input to be cleared as well as removed covers consecutive + * challenges that use the same channel and therefore reuse the same element. */ -async function enterChallengeCode( +async function submitChallengeCode( page: Page, + selector: string, code: string, timeoutMs: number, ): Promise<void> { - const digits = page.locator('input[name="code"]'); - const numDigits = await digits.count(); - if (numDigits !== code.length) { - throw Error( - `the dialog asks for ${numDigits} digits, but the code has ${code.length}`, - ); - } - // Hold on to the first input, so that we can tell this dialog apart from - // the one of the next challenge, which looks exactly the same. - const dialog = await digits.first().elementHandle(); - for (let i = 0; i < numDigits; i++) { - await digits.nth(i).fill(code[i]); - } - if (!dialog) { + const input = page.locator(selector); + const oldInput = await input.elementHandle(); + await input.fill(code); + await page.click('button[type="submit"]'); + if (!oldInput) { return; } try { - await page.waitForFunction((el) => !el.isConnected, dialog, { - timeout: timeoutMs, - }); + await page.waitForFunction( + (el) => + !el.isConnected || + ((el as { value: string; disabled: boolean }).value === "" && + !(el as { value: string; disabled: boolean }).disabled), + oldInput, + { timeout: timeoutMs }, + ); } finally { - await dialog.dispose(); + await oldInput.dispose(); } } @@ -210,56 +202,51 @@ export async function runStagefrightMerchantMytops( await stage.run(async () => { await stage.step("open the merchant portal", async (page) => { await page.goto(new URL("webui/", baseUrl).href); - await page.waitForSelector('input[name="username"]'); + await page.waitForSelector("#signin-account"); }); await stage.step("start the onboarding", async (page) => { - await page.click('a[href="#/account/new"]'); - await page.waitForSelector('input[name="id"]'); + await page.click('a[href="#/signup"]'); + await page.waitForSelector("#signup-business"); }); await stage.step("fill in the account details", async (page) => { - await page.fill('input[name="id"]', instanceId); - await page.fill('input[name="name"]', businessName); - await page.fill('input[name="password"]', password); - await page.fill('input[name="repeat"]', password); + await page.fill("#signup-business", businessName); + await page.fill("#signup-username", instanceId); + await page.fill("#signup-password", password); + await page.fill("#signup-confirm-password", password); // Which of the two the deployment asks for depends on its configured // mandatory TAN channels. - if ((await page.locator('input[name="email"]').count()) > 0) { - await page.fill('input[name="email"]', email); + if ((await page.locator("#signup-email").count()) > 0) { + await page.fill("#signup-email", email); } - if ((await page.locator('input[name="phone"]').count()) > 0) { - await page.fill('input[name="phone"]', phone); + if ((await page.locator("#signup-phone").count()) > 0) { + await page.fill("#signup-phone", phone); } - // The toggle for the terms of service is styled away, so it can't be - // clicked the way a user would. - await page.locator('input[name="tos"]').dispatchEvent("click"); + await page.locator('input[type="checkbox"]').check(); }); await stage.step("request the account", async (page) => { - await page.click("footer.modal-card-foot button.is-success"); + await page.click('button[type="submit"]'); }); // A deployment can require more than one channel, in which case the next // challenge is sent as soon as the previous one is solved. for (let round = 1; ; round++) { - const dialog = await waitForChallengeDialog( - stage.page, - timeoutMs, - round === 1, - ); - if (dialog === "logged-in") { + const state = await waitForRegistrationState(stage.page, timeoutMs); + if (state === "logged-in") { break; } - if (dialog === "choose") { + if (state === "choose") { await stage.step( `pick the channel for challenge ${round}`, async (page) => { - await page.locator('input[name="challenge_id"]').first().check(); - await page.click( - ".modal.is-active .modal-card-foot button.is-success", - ); - await page.waitForSelector('input[name="code"]'); + const channel = page.locator('input[name="signup_2fa_channel"]'); + if (!(await channel.first().isChecked())) { + await channel.first().check(); + } + await page.click('button[type="submit"]'); + await page.waitForSelector("#signup-email-code, #signup-sms-code"); }, ); } @@ -276,7 +263,11 @@ export async function runStagefrightMerchantMytops( await stage.step( `enter the code for challenge ${round}`, async (page) => { - await enterChallengeCode(page, code, timeoutMs); + const selector = + (await page.locator("#signup-email-code").count()) > 0 + ? "#signup-email-code" + : "#signup-sms-code"; + await submitChallengeCode(page, selector, code, timeoutMs); }, ); }