From 6469c7fa52842e63fbf7eff48027a9020b0176ce Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 18 Aug 2026 11:38:37 +0000 Subject: [PATCH] test(e2e): cover every onboarding route across portal and backoffice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five journeys, one file each, every one of them crossing from the portal into the backoffice and cross-checking the database rather than the screen: - ethiopian eTrade verified, Fayda, approved, contract wizard reachable — including the dead end a TIN with no trade licence is for an ordinary company - investor foreign investment licence: nothing on file at eTrade, typed registration, passport identity, per-role licence still owed, and the backoffice's manual-entry badge and banner - cooperative no foreign option, no freight-forwarder role, the co-operative document set, no licence cards, its own badge and banner - switch_back settings → switch to eTrade → registration cleared, company pending, wizard reopened on the company step → re-run through eTrade → the flag is gone from the backoffice - guards the refused combinations, and the mid-wizard un-tick that has to clear the typed registration Replaces the old onboarding.cy.ts (removed a commit earlier by accident of a staged deletion): it drove a wizard shape that no longer exists — Fayda before the company step, a "Personnel" step — so it could only ever have been red. Notes for whoever edits these next. Attach files to the FIRST empty dropzone, never by index — SmartFileInput removes the input once a file is on it. Resolve the company from the database after any cross-origin hop, never from module state: Cypress re-evaluates the spec bundle and Date.now() with it, which is what latestJourney's run-stamp cutoff is for. And the deliberate eTrade 400 is ignored as an uncaught exception — the portal handles that outcome on screen but leaves the rejected request unhandled at the promise level. --- .../cypress/e2e/flows/onboarding-utils.ts | 385 ++++++++++++++++++ .../e2e/flows/onboarding_cooperative.cy.ts | 161 ++++++++ .../e2e/flows/onboarding_ethiopian.cy.ts | 180 ++++++++ .../cypress/e2e/flows/onboarding_guards.cy.ts | 171 ++++++++ .../e2e/flows/onboarding_investor.cy.ts | 123 ++++++ .../e2e/flows/onboarding_switch_back.cy.ts | 158 +++++++ 6 files changed, 1178 insertions(+) create mode 100644 e2e/freight/cypress/e2e/flows/onboarding-utils.ts create mode 100644 e2e/freight/cypress/e2e/flows/onboarding_cooperative.cy.ts create mode 100644 e2e/freight/cypress/e2e/flows/onboarding_ethiopian.cy.ts create mode 100644 e2e/freight/cypress/e2e/flows/onboarding_guards.cy.ts create mode 100644 e2e/freight/cypress/e2e/flows/onboarding_investor.cy.ts create mode 100644 e2e/freight/cypress/e2e/flows/onboarding_switch_back.cy.ts diff --git a/e2e/freight/cypress/e2e/flows/onboarding-utils.ts b/e2e/freight/cypress/e2e/flows/onboarding-utils.ts new file mode 100644 index 000000000..93d193ac7 --- /dev/null +++ b/e2e/freight/cypress/e2e/flows/onboarding-utils.ts @@ -0,0 +1,385 @@ +/** + * Shared machinery for the onboarding journeys (portal → backoffice). + * + * The wizard's five form steps are `company → owner → representation → + * contact → documents`, preceded by the nationality/role phase. Three routes + * run through them: + * + * ordinary eTrade answers for the TIN; the registration is read-only. + * investor a foreign company on an Investment Commission licence — eTrade + * holds nothing, the registration is typed, the per-role business + * licence is still owed. + * co-op a union or farm — eTrade holds nothing, the registration is + * typed, and no business licence exists to ask for. + * + * The eTrade mock picks its answer from the TIN's leading digit (see + * etrade-mock/server.js), so a spec chooses "verified" or "nothing on file" by + * choosing its number — `etradeTin()` / `noLicenceTin()` / `unknownTin()`. + */ + +/** + * Every manual-route journey deliberately looks up a TIN eTrade holds nothing + * for, and the API answers 400. The portal handles that outcome on screen (the + * "nothing on file" alert is the whole point) but leaves the rejected request + * unhandled at the promise level, and Cypress fails a test on any unhandled + * rejection from the app. Ignored here rather than per spec: it is the expected + * response to a request these journeys make on purpose, in every one of them. + * + * Narrow on purpose — only the 400. A 500, or anything else the app throws, + * still fails the test. + */ +Cypress.on("uncaught:exception", (err) => { + if (/Request failed with status code 400/.test(err.message)) return false; + return true; +}); + +const apiUrl = () => Cypress.env("apiUrl") as string; + +export const portalUrl = () => Cypress.env("portalUrl") as string; + +/* ------------------------------------------------------------------ */ +/* Field helpers */ +/* ------------------------------------------------------------------ */ + +/** + * 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 a subject captured a command + * earlier can already be stale. Going label → for → element each time always + * addresses what is on the page now. + */ +export function fill(label: string | RegExp, value: string) { + 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