Files
emaui/test-results/seafarer-registration-seaf-b6d60--registration-with-a-reason-chromium/error-context.md
Fistum 1931e69594 Add error context and media files for seafarer registration tests
- Created error context markdown files for failed tests in seafarer registration, detailing validation errors related to profile details.
- Added binary files (PNG, ZIP, WEBM) for test results, including snapshots and traces for debugging purposes.
- Updated test cases to include new error context and media files for better analysis of failures.
2026-08-18 09:20:36 +00:00

16 KiB

Instructions

  • Following Playwright test failed.
  • Explain why, be concise, respect Playwright best practices.
  • Provide a snippet of code with the fix, if possible.

Test info

  • Name: seafarer-registration.spec.ts >> seafarer registration >> an officer can reject a registration with a reason
  • Location: apps/e2e/src/seafarer-registration.spec.ts:285:7

Error details

Error: Save did not submit — validation errors: Profile details are needed for seafarer registration.

Page snapshot

- generic [ref=f1e3]:
  - banner [ref=f1e4]:
    - generic [ref=f1e5]:
      - generic [ref=f1e6]:
        - button "Toggle navigation" [ref=f1e8] [cursor=pointer]
        - generic [ref=f1e10]:
          - generic [ref=f1e11]: Dashboard
          - generic [ref=f1e13]: Profile
      - generic [ref=f1e17]:
        - button "Language" [ref=f1e18] [cursor=pointer]
        - button "Toggle light / dark mode" [ref=f1e23] [cursor=pointer]
        - button "Notifications" [ref=f1e26] [cursor=pointer]:
          - generic [ref=f1e27]: "1"
        - button "ES" [ref=f1e32] [cursor=pointer]
  - navigation [ref=f1e34]:
    - generic [ref=f1e35]:
      - img "EMA" [ref=f1e36]
      - generic [ref=f1e37]:
        - paragraph [ref=f1e38]: EMA Portal
        - paragraph [ref=f1e39]: Ethiopian Maritime Authority
    - generic [ref=f1e43]:
      - generic [ref=f1e44]:
        - generic [ref=f1e45] [cursor=pointer]: Dashboard
        - generic [ref=f1e52] [cursor=pointer]:
          - generic [ref=f1e57]: Notifications
          - generic "1 pending" [ref=f1e59]: "1"
      - generic [ref=f1e61]:
        - button [expanded] [ref=f1e62] [cursor=pointer]:
          - paragraph [ref=f1e63]: Licensing
        - generic [ref=f1e66] [cursor=pointer]: My Applications
      - generic [ref=f1e73]:
        - button [expanded] [ref=f1e74] [cursor=pointer]:
          - paragraph [ref=f1e75]: Seafarer Services
        - generic [ref=f1e78] [cursor=pointer]: Seafarer Registration
        - generic [ref=f1e82] [cursor=pointer]: My Sea Records
        - generic [ref=f1e86] [cursor=pointer]: Seaman Book
        - generic [ref=f1e92] [cursor=pointer]: Basic Training Certificate
        - generic [ref=f1e98] [cursor=pointer]: Certificates
        - generic [ref=f1e104] [cursor=pointer]: Examinations
        - generic [ref=f1e108] [cursor=pointer]: Endorsements
      - generic [ref=f1e113]:
        - button [expanded] [ref=f1e114] [cursor=pointer]:
          - paragraph [ref=f1e115]: Account
        - generic [ref=f1e118] [cursor=pointer]: My Documents
        - generic [ref=f1e123] [cursor=pointer]: Profile
        - generic [ref=f1e130] [cursor=pointer]: Help & Support
    - button "Collapse" [ref=f1e139] [cursor=pointer]
  - main [ref=f1e143]:
    - generic [ref=f1e145]:
      - generic [ref=f1e147]:
        - heading "My Profile" [level=2] [ref=f1e148]
        - paragraph [ref=f1e149]: Manage your account details and preferences.
      - alert [ref=f1e150]:
        - generic [ref=f1e151]: Profile details are needed for seafarer registration.
      - generic [ref=f1e159]:
        - paragraph [ref=f1e161]: ES
        - generic [ref=f1e162]:
          - generic [ref=f1e163]:
            - heading "E2E seafarer 9173" [level=4] [ref=f1e164]
            - generic [ref=f1e165]: Unverified
          - paragraph [ref=f1e171]: e2e.seafarer.1787042455309173@example.test
        - generic [ref=f1e172]: e2eseafarer1787042455309173
        - generic "0% complete" [ref=f1e178]:
          - paragraph [ref=f1e183]: 0%
      - generic [ref=f1e184]:
        - tablist [ref=f1e185]:
          - tab "Personal" [ref=f1e186] [cursor=pointer]
          - tab "Profile" [selected] [ref=f1e193] [cursor=pointer]
          - tab "Address" [ref=f1e199] [cursor=pointer]
          - tab "Operations" [ref=f1e205] [cursor=pointer]
          - tab "Security" [ref=f1e212] [cursor=pointer]
          - tab "Preferences" [ref=f1e218] [cursor=pointer]
        - tabpanel "Profile" [ref=f1e224]:
          - generic [ref=f1e227]:
            - generic [ref=f1e228]:
              - heading "Maritime Profile" [level=5] [ref=f1e229]
              - paragraph [ref=f1e230]: Your professional maritime details
              - generic [ref=f1e231]:
                - generic [ref=f1e232]:
                  - generic [ref=f1e233]: Profession *
                  - textbox "Profession" [ref=f1e235]:
                    - /placeholder: Select
                    - text: Master Mariner
                - generic [ref=f1e236]:
                  - generic [ref=f1e237]: First Name *
                  - textbox "First Name" [ref=f1e239]:
                    - /placeholder: Enter first name
                    - text: Dawit
                - generic [ref=f1e240]:
                  - generic [ref=f1e241]: Middle Name *
                  - textbox "Middle Name" [ref=f1e243]:
                    - /placeholder: Enter middle name
                    - text: Bekele
                - generic [ref=f1e244]:
                  - generic [ref=f1e245]: Last Name *
                  - textbox "Last Name" [ref=f1e247]:
                    - /placeholder: Enter last name
                    - text: Tesfaye
                - generic [ref=f1e248]:
                  - generic [ref=f1e249]: Gender *
                  - textbox "Gender" [ref=f1e251] [cursor=pointer]:
                    - /placeholder: Select
                    - text: MALE
                - generic [ref=f1e252]:
                  - generic [ref=f1e253]: Date of Birth *
                  - generic [ref=f1e254]:
                    - button "Switch calendar type" [ref=f1e256] [cursor=pointer]:
                      - generic [ref=f1e257]: EN
                    - textbox "Date of Birth" [ref=f1e259] [cursor=pointer]: Apr 12, 1995
                    - button [ref=f1e261] [cursor=pointer]
                - generic [ref=f1e266]:
                  - generic [ref=f1e267]: Place of Birth
                  - textbox "Place of Birth" [ref=f1e269]:
                    - /placeholder: City, Region
                - generic [ref=f1e270]:
                  - generic [ref=f1e271]: Marital Status *
                  - textbox "Marital Status" [ref=f1e273] [cursor=pointer]:
                    - /placeholder: Select
                    - text: SINGLE
            - button "Save Profile" [active] [ref=f1e275] [cursor=pointer]

Test source

  46  |   await openTab(page, 'Address');
  47  |   await pick(page, 'ID Type', /^NID$/i);
  48  |   await page.getByLabel('ID Number').fill('FYD1234567890');
  49  |   // A country select, not a free-text field.
  50  |   await pick(page, 'Nationality', /ethiopia/i);
  51  |   // `addressSchema` requires this in Ethiopian format; without it the form
  52  |   // never submits and no request is made for `save` to wait on.
  53  |   await page
  54  |     .getByRole('textbox', { name: 'Primary Phone' })
  55  |     .fill('+251911234567');
  56  |   await save(page);
  57  | }
  58  | 
  59  | /** Selects a profile tab and waits for its panel to be the visible one. */
  60  | async function openTab(page: Page, name: string): Promise<void> {
  61  |   await page.getByRole('tab', { name, exact: true }).click();
  62  |   await expect(page.getByRole('tabpanel', { name })).toBeVisible({
  63  |     timeout: 15_000,
  64  |   });
  65  | }
  66  | 
  67  | /**
  68  |  * Picks a value from a Mantine select.
  69  |  *
  70  |  * The label is bound to both the input and the listbox it opens, so matching
  71  |  * by label alone is ambiguous once the dropdown is showing — the textbox role
  72  |  * names the control itself.
  73  |  */
  74  | async function pick(page: Page, label: string, option: RegExp): Promise<void> {
  75  |   await page.getByRole('textbox', { name: label }).click();
  76  |   await page.getByRole('option', { name: option }).first().click();
  77  | }
  78  | 
  79  | /**
  80  |  * Sets the date of birth through the picker's own UI.
  81  |  *
  82  |  * `AmharicDatePicker` is a controlled component: it reports changes through
  83  |  * `onChange`, which is what writes the value into react-hook-form. Setting the
  84  |  * input's `value` natively bypasses that entirely — the field stays empty as
  85  |  * far as zod is concerned, and the form silently refuses to submit.
  86  |  *
  87  |  * So the calendar is actually driven: open it, pick the year and month from
  88  |  * the caption dropdowns, then click the day.
  89  |  */
  90  | async function pickDate(page: Page, label: string, iso: string): Promise<void> {
  91  |   const [year, month, day] = iso.split('-').map(Number);
  92  | 
  93  |   await page.getByRole('textbox', { name: label }).click();
  94  |   const calendar = page.locator('.amharic-daypicker-dropdown');
  95  |   await expect(calendar).toBeVisible({ timeout: 10_000 });
  96  | 
  97  |   // `captionLayout="dropdown"` renders native selects for month and year.
  98  |   await calendar.locator('select').last().selectOption(String(year));
  99  |   await calendar
  100 |     .locator('select')
  101 |     .first()
  102 |     .selectOption({ index: month - 1 });
  103 | 
  104 |   // Each day is a button whose accessible name is the full date
  105 |   // ("Saturday, April 1st, 1995"), not the bare number — matching on the
  106 |   // number alone finds nothing. Anchored on the ordinal so 1 cannot match 11
  107 |   // or 21. Resolved after the dropdowns settle, since changing year or month
  108 |   // re-renders the grid.
  109 |   const cell = calendar
  110 |     .getByRole('button', { name: new RegExp(`\\b${day}(st|nd|rd|th),`) })
  111 |     .first();
  112 |   await expect(cell).toBeVisible({ timeout: 10_000 });
  113 |   await cell.click();
  114 | 
  115 |   await expect(calendar).toBeHidden({ timeout: 10_000 });
  116 | 
  117 |   // The picker writes through `onChange`; if that did not land, zod still sees
  118 |   // an empty field and the failure would surface later as a refused submit.
  119 |   await expect(page.getByRole('textbox', { name: label })).not.toHaveValue('', {
  120 |     timeout: 10_000,
  121 |   });
  122 | }
  123 | 
  124 | async function save(page: Page): Promise<void> {
  125 |   // Matched loosely on purpose: the personal tab PATCHes a user, the profile
  126 |   // tab a profile, and the address tab POSTs to `/addresss/profile/:id` — the
  127 |   // route's own spelling. Any successful write from this screen is the signal.
  128 |   const saved = page.waitForResponse(
  129 |     (r) =>
  130 |       r.request().method() !== 'GET' &&
  131 |       r.status() < 400 &&
  132 |       /(profile|address|user)/i.test(r.url()),
  133 |     { timeout: 20_000 },
  134 |   );
  135 |   await page.getByRole('button', { name: /save/i }).first().click();
  136 | 
  137 |   try {
  138 |     await saved;
  139 |   } catch (cause) {
  140 |     // A zod-blocked submit fires no request at all, so the bare timeout says
  141 |     // only "no response" — which reads as a backend fault rather than a form
  142 |     // that refused to submit. Surface the field errors instead.
  143 |     const messages = await page
  144 |       .locator('.mantine-InputWrapper-error, [role="alert"]')
  145 |       .allTextContents();
> 146 |     throw new Error(
      |           ^ Error: Save did not submit  validation errors: Profile details are needed for seafarer registration.
  147 |       messages.length
  148 |         ? `Save did not submit — validation errors: ${messages.join('; ')}`
  149 |         : 'Save produced no request and reported no validation error.',
  150 |       { cause },
  151 |     );
  152 |   }
  153 | }
  154 | 
  155 | /** Signs up, declares seafarer operations, and fills the gating profile. */
  156 | async function readyApplicant(page: Page, applicant: Applicant): Promise<void> {
  157 |   const offset = await signUp(page, applicant);
  158 |   await verifyOtpIfPrompted(page, offset);
  159 |   await expect(page).toHaveURL(/\/onboarding\/operations/, { timeout: 30_000 });
  160 |   await page
  161 |     .getByRole('checkbox', { name: /seafarer registration/i })
  162 |     .first()
  163 |     .check();
  164 |   await page.getByRole('button', { name: /save operations/i }).click();
  165 |   // A seafarer is taken to `/profile`, not the dashboard: registration is
  166 |   // built from the profile, and a fresh signup holds none of it yet.
  167 |   await expect(page).toHaveURL(/\/profile/, { timeout: 30_000 });
  168 |   await completeProfile(page);
  169 | }
  170 | 
  171 | test.describe('seafarer registration', () => {
  172 |   let applicant: Applicant;
  173 | 
  174 |   test.beforeEach(() => {
  175 |     applicant = newApplicant('seafarer');
  176 |   });
  177 | 
  178 |   test.afterEach(() => {
  179 |     deleteApplicant(applicant.email);
  180 |   });
  181 | 
  182 |   test('the wizard refuses to open until the profile it is built from is complete', async ({
  183 |     page,
  184 |   }) => {
  185 |     const offset = await signUp(page, applicant);
  186 |     await verifyOtpIfPrompted(page, offset);
  187 |     await expect(page).toHaveURL(/\/onboarding\/operations/, { timeout: 30_000 });
  188 |     await page
  189 |       .getByRole('checkbox', { name: /seafarer registration/i })
  190 |       .first()
  191 |       .check();
  192 |     await page.getByRole('button', { name: /save operations/i }).click();
  193 |     await expect(page).toHaveURL(/\/profile/, { timeout: 30_000 });
  194 | 
  195 |     // A new account holds none of the identity the registration is filled in
  196 |     // from, so the gate collects it rather than opening an uncompletable form.
  197 |     await page.goto('/seafarer-registration');
  198 |     await expect(page).toHaveURL(/\/profile/, { timeout: 30_000 });
  199 | 
  200 |     // The shared wizard route is gated identically — otherwise the gate is
  201 |     // decoration a deep link walks straight past.
  202 |     await page.goto('/licensing/SEAFARER_REGISTRATION/apply');
  203 |     await expect(page).toHaveURL(/\/profile/, { timeout: 30_000 });
  204 |   });
  205 | 
  206 |   test('opening the wizard creates the draft up front', async ({ page }) => {
  207 |     await readyApplicant(page, applicant);
  208 | 
  209 |     await page.goto('/licensing/SEAFARER_REGISTRATION/apply');
  210 |     await expect(page).not.toHaveURL(/\/profile/, { timeout: 30_000 });
  211 | 
  212 |     // The draft exists before anything is filled in, so uploads have an owner
  213 |     // and closing the browser mid-wizard loses nothing.
  214 |     const number = await waitForApplication(applicant.email);
  215 |     expect(number).toMatch(/^SFR/);
  216 |     expect(statusOf(number)).toBe('DRAFT');
  217 |   });
  218 | 
  219 |   test('a registration never reaches evaluation or inspection', async ({
  220 |     page,
  221 |   }) => {
  222 |     await readyApplicant(page, applicant);
  223 |     await page.goto('/licensing/SEAFARER_REGISTRATION/apply');
  224 |     const number = await waitForApplication(applicant.email);
  225 |     const id = idOf(number);
  226 | 
  227 |     await submit(id);
  228 |     await runWorkflow(id, [{ path: 'claim' }]);
  229 |     expect(statusOf(number)).toBe('UNDER_REVIEW');
  230 | 
  231 |     // The licence course's middle stages have nothing to hold in a
  232 |     // registration, and the transition table is the authority regardless of
  233 |     // which endpoint is called.
  234 |     const refused = await runWorkflow(id, [
  235 |       { path: 'complete-review', expectFailure: true },
  236 |       { path: 'approve-documents', expectFailure: true },
  237 |       { path: 'record-inspection', expectFailure: true },
  238 |     ]);
  239 |     expect(refused.every((code) => code >= 400)).toBe(true);
  240 |     expect(statusOf(number)).toBe('UNDER_REVIEW');
  241 |   });
  242 | 
  243 |   test('an officer can return a registration for correction and take it back', async ({
  244 |     page,
  245 |   }) => {
  246 |     await readyApplicant(page, applicant);