import { Page, expect } from '@playwright/test'; import { logOffset, waitForOtp } from './api-log'; /** * A new applicant, unique to this run. * * Every spec makes its own. Sharing one account between specs means the second * run of the suite starts from a state the first run left behind — an * applicant who already declared their operations, already holds a licence — * and the assertions quietly stop meaning what they say. */ export interface Applicant { email: string; username: string; phoneNumber: string; password: string; name: string; } export function newApplicant(label: string): Applicant { const stamp = `${Date.now()}${Math.floor(Math.random() * 1000)}`; return { email: `e2e.${label}.${stamp}@example.test`, username: `e2e${label}${stamp}`.slice(0, 28), // Ethiopian mobile format; the last digits vary so two runs never collide. phoneNumber: `+2519${stamp.slice(-8)}`, password: 'E2ePassw0rd!', name: `E2E ${label} ${stamp.slice(-4)}`, }; } /** * Fills and submits the signup form. * * Returns the log offset taken immediately before submitting, so the caller can * pick up the one-time code that submission triggers without matching a stale * one from an earlier test. */ export async function signUp(page: Page, applicant: Applicant): Promise { await page.goto('/signup'); await page.getByLabel('Name (English)').fill(applicant.name); await page.getByLabel('Email address').fill(applicant.email); await page.getByLabel('Username').fill(applicant.username); await page.getByLabel('Phone number').fill(applicant.phoneNumber); await page.getByLabel('Password', { exact: true }).fill(applicant.password); await page.getByLabel('Confirm password').fill(applicant.password); // The terms checkbox gates the submit button. await page.getByRole('checkbox').check(); const offset = logOffset(); await page.getByRole('button', { name: /create account|sign up/i }).click(); return offset; } /** * Completes phone verification by typing the real code. * * No-op when signup did not land on the OTP screen — whether it does depends * on how the account was created, and a test that asserts the flow rather than * the screen should not care. */ export async function verifyOtpIfPrompted( page: Page, sinceOffset: number, ): Promise { await page.waitForURL(/\/(otp-verify|onboarding|dashboard)/, { timeout: 30_000 }); if (!page.url().includes('/otp-verify')) return false; const code = await waitForOtp(sinceOffset); // Mantine's PinInput is one input per character and moves focus itself, so // the code is typed rather than filled — filling each box individually // fights the component's own focus handling. const boxes = page.locator('input[inputmode], input[type="text"]'); await boxes.first().click(); await page.keyboard.type(code, { delay: 30 }); // `onComplete` submits on the last character; the button is the fallback for // when it does not fire. const submit = page.getByRole('button', { name: /^verify$/i }); await Promise.race([ page.waitForURL(/\/(onboarding|dashboard)/, { timeout: 10_000 }).catch(() => null), submit.click({ timeout: 5_000 }).catch(() => null), ]); return true; } /** Signs in an existing applicant through the login screen. */ export async function logIn(page: Page, applicant: Applicant): Promise { await page.goto('/login'); // The API takes an email here, not a username — sending a username answers // "email should not be empty". await page.getByLabel(/email/i).fill(applicant.email); await page.getByLabel(/password/i).fill(applicant.password); await page.getByRole('button', { name: /sign in|log in|login/i }).click(); await expect(page).toHaveURL(/\/(onboarding|dashboard|profile)/, { timeout: 30_000 }); }