test(freight-e2e): fix stale onboarding selectors, tighten Fayda assertions

The onboarding journey had been failing at the company step for a while:
that step was restructured into StepSection cards, so its TIN and VAT
fields no longer have <label> elements and the label-based fill() helper
could not find them. Match on aria-label instead, which also sidesteps the
"0012345678" placeholder both fields share.

Two further staleness bugs were hiding behind that one, both in fill()
itself. It chained clear() into type() on a subject captured beforehand, so
a step-persist PATCH resolving between the two detached it; and re-querying
by the captured id was no better, because the fields remount rather than
re-render and Mantine mints a fresh generated id when they do. Resolve
label -> for -> element afresh for each action. fillPhone gets the same
treatment.

Tighten what the journey proves about Fayda. It verified out-of-band and
then never checked the result reached the UI, so assert the panel renders
fayda-mock's own payload — name, phone and email — which only holds if it
travelled Fayda -> API -> UI, and cross-check ownerFaydaSub on the company,
since that sub is what locks the owner's fields server-side. The PoA step
now asserts no file input exists while the PoA is unverified, covering the
DARS gating; asserted structurally so rewording the document setting cannot
turn a regression into a passing test.
This commit is contained in:
Nathnael
2026-08-04 10:58:37 +00:00
parent 53accbc57b
commit b356433886

View File

@@ -42,21 +42,44 @@ function latestOnboardJourney() {
});
}
/** Fill a labelled Mantine input (label[for] → input id). */
/**
* Fill a labelled Mantine input (label[for] → input id).
*
* The input is resolved fresh for every action rather than captured once.
* Each wizard step persists and re-seeds asynchronously, and when a field
* remounts Mantine mints a NEW generated id — so both a subject and an id
* captured a command earlier can be stale by the time the next command runs.
* Going label → for → element each time always addresses what's on the page
* now.
*/
function fill(label: string | RegExp, value: string) {
cy.contains("label", label)
.invoke("attr", "for")
.then((id) => {
cy.get(`[id="${id}"]`).clear({ force: true }).type(value, { force: true });
});
const input = () =>
cy
.contains("label", label)
.invoke("attr", "for")
.then((id) => cy.get(`[id="${id}"]`));
input().clear({ force: true });
input().type(value, { force: true });
}
/**
* Fill an input that has no <label> — the wizard's company step renders its
* fields inside StepSection cards (the heading is the card's title, not a
* label), so they're reachable only by aria-label. Both TIN and VAT share the
* "0012345678" placeholder, which is why this matches on aria-label instead.
*/
function fillAria(ariaSelector: string, value: string) {
const selector = `.mantine-Modal-content ${ariaSelector}`;
cy.get(selector).clear({ force: true });
cy.get(selector).type(value, { force: true });
}
/** The wizard's phone inputs (react-phone-number-input, type=tel). */
function fillPhone(index: number, national: string) {
cy.get('.mantine-Modal-content input[type="tel"]')
.eq(index)
.clear({ force: true })
.type(national, { force: true });
const selector = '.mantine-Modal-content input[type="tel"]';
cy.get(selector).eq(index).clear({ force: true });
cy.get(selector).eq(index).type(national, { force: true });
}
describe("customer onboarding journey", { retries: 0 }, () => {
@@ -86,20 +109,48 @@ describe("customer onboarding journey", { retries: 0 }, () => {
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
// Ethiopian companies gate the "Owner identity" step on Fayda
// verification — a real popup + SMS OTP flow that can't run in e2e.
// Complete it via the API against fayda-mock-e2e (the profile this
// verification — a real eSignet redirect + SMS OTP flow that can't run in
// e2e. Complete it via the API against fayda-mock-e2e (the profile this
// attaches to was just created by the nationality/role step above).
// The wizard already fetched `identity` once when this step mounted
// completing verification out-of-band (no popup, so no onVerified
// callback fires) leaves that fetch stale, so reload to force a fresh
// one. Wizard progress resumes server-side, so this doesn't lose the
// nationality/role step just completed.
// The wizard already fetched `identity` once when this step mounted, and
// completing verification out-of-band skips the redirect that would
// normally remount everything — so reload to force a fresh fetch. Wizard
// progress resumes server-side, so this doesn't lose the nationality/role
// step just completed.
completeFaydaVerification("owner");
cy.reload();
cy.contains("Confirm your VAT number", { timeout: 20000 }).should(
"be.visible",
);
// The owner's identity is the verification's output, never typed: the
// panel must show it verified and render the name/phone/email that came
// back from fayda-mock-e2e. Asserting the mock's own values is the only
// way to prove the payload travelled Fayda → API → UI rather than the
// panel simply flipping a "verified" flag.
cy.get(".mantine-Modal-content").within(() => {
cy.contains("Fayda verified").should("be.visible");
cy.contains("Abebe Bekele").should("be.visible");
cy.contains("+251911223344").should("be.visible");
cy.contains("abebe.bekele@example.com").should("be.visible");
});
// The verified sub is what locks the owner's fields server-side, so check
// it actually landed on the company rather than trusting the panel alone.
cy.task<{ rows: Array<{ owner_fayda_sub: string | null }> }>("db:query", {
sql: `SELECT c.attributes->>'ownerFaydaSub' AS owner_fayda_sub
FROM freight.companies c
JOIN freight.external_profiles ep ON ep.company_id = c.id
JOIN iam.users u ON u.id = ep.user_id
WHERE u.email = $1`,
params: [email],
}).then(({ rows }) => {
expect(rows, "company row").to.have.length(1);
expect(rows[0].owner_fayda_sub, "owner Fayda sub").to.eq(
"e2e-fayda-sub-0001",
);
});
// Company step. TIN auto-triggers the eTrade lookup once it's a full 10
// digits (mocked in e2e — see docker-compose.e2e.yaml's etrade-mock-e2e).
// A successful lookup locks Company Name/Region/Zone/Woreda/Kebele/House
@@ -108,7 +159,7 @@ describe("customer onboarding journey", { retries: 0 }, () => {
// Fayda-verified owner supplies contact details now). By label, not
// placeholder: the VAT Number field on this same step shares the TIN
// field's "0012345678" placeholder, so a placeholder selector matches 2.
fill(/^TIN Number/, tin);
fillAria('[aria-label^="TIN Number"]', tin);
cy.contains("Verified with eTrade", { timeout: 15000 }).should(
"be.visible",
);
@@ -117,7 +168,7 @@ describe("customer onboarding journey", { retries: 0 }, () => {
// after the badge appears. Typing into VAT immediately raced one of
// those and detached mid-type; let it finish before touching the form.
cy.wait(500);
fill(/^VAT Number/, vat);
fillAria('[aria-label="VAT Number"]', vat);
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
// Personnel (general manager).
@@ -131,7 +182,16 @@ describe("customer onboarding journey", { retries: 0 }, () => {
fillPhone(0, "911234569");
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
// PoA — optional for an importer.
// PoA — optional for an importer, and left unverified here. The DARS
// delegation paper authorises the representative the verification names,
// so with no verified PoA there is nothing for it to authorise: the
// upload must not be offered, and the step must not block on it. Asserted
// as "no file input on this step" rather than by label, so a reworded
// document setting doesn't turn a real regression into a passing test.
cy.get(".mantine-Modal-content")
.contains("Power of Attorney")
.should("be.visible");
cy.get('.mantine-Modal-content input[type="file"]').should("not.exist");
cy.get(".mantine-Modal-content").contains("button", "Continue").click();
// Documents: no company docs are configured in e2e, but every role needs