fix(e2e): get the stage Playwright suite green after the UI redesigns
PR
- Have you followed the contributing guidelines?
- Have you explained what your changes do, and why they add value?
What and why
Stage d365fbe9e6 (run 40364004456) was the first Playwright run in weeks to get past "Build API + Web". The four e2e shards then failed 46 of ~80 scenarios (10 + 12 + 10 + 14). This PR fixes the e2e suite so those scenarios test the same behaviour against today's UI.
Root causes, by each scenario's first failure:
| Root cause | Scenarios |
|---|---|
| Stale selector: the markup changed in a UI redesign since the suite was last green | 33 |
| Changed flow: the image-URL input became a file-upload avatar (#10336) | 11 |
Test data or order dependency: manage-user-invites assumed another shard's invite | 1 |
Catalog drift: roles-permissions toggle count and stale permission lists | 1 |
| App regression | 0 |
Each scenario failed at its first broken step, and the steps behind that one had never run against the new UI. For example, 14 scenarios stopped in addProject before reaching their own steps. So every step of each scenario was checked against the current templates on origin/develop, not only the first failure. Further stale selectors and changed flows turned up behind the first failure, and they are fixed too. The evidence is the stage trace DOM snapshots and screenshots, the templates, and the commit that changed the markup.
What is not changed:
- No scenario, step or assertion was deleted or skipped, and no
.skip/fixmewas added. The.featurefiles are untouched and every step file defines the same steps. - App code is untouched: the investigation found no app regression to fix.
Where a flow changed, the step now uses the equivalent control:
- Picture: the step uploads a PNG through
<ngx-image-uploader>instead of typing an image URL. - Type-to-confirm deletes: the step types the organization name or invitee email before confirming.
Several steps also got stricter, so a failed save now fails where it happens. Before, these could pass without saving anything:
- the project, contact, client and organization forms must close (or the row must exist) after save;
- the dialogs must close after send/create;
addOrganizationre-enables its existing-row check, which was commented out.
Failing scenario → root cause → fix
Shard numbers are from run 40364004456. Commits referenced are on develop.
| Scenario (shard) | First failure | Root cause (evidence) | Fix |
|---|---|---|---|
| add-organization (1), delete-organization (1), manage-organization (3), register (4) | ga-country ng-select never attached after the first "Next" | Stale selector. #10608 (ffc762ee8a) moved the step buttons out of nb-stepper into nb-card-footer.dialog-footer and relabelled them Continue / Skip / Create organization. The helper searched nb-stepper for Next/Add, found nothing and returned false, so the stepper never left step 1. Confirmed in the trace DOM. | New advanceStepForm: clicks the footer's primary button once it is enabled, then checks the step indicator moved or the dialog closed. Delete is type-to-confirm since #10616 (a19de169d1), so the step types the name. Grids are filtered to the new organization. manage-organization no longer renames whichever row is last. |
| organization-public-page (3) | button[type="submit"] (Next) | Stale selector (same #10608 footer). Also: the share route needs the organization id (/share/organization/:profileLink/:organizationId), and in ng-select 24 the language combobox role sits on the inner input. | Uses advanceStepForm. Navigates to the full share route, and uses ngx-language-selector ng-select for the language. |
organization-projects (3) and, via addProject: add-tasks (1), clients (1), contacts-leads (1), customers (1), manage-employees (3), estimates (2), expenses (2), invoices (2), payments (4), sales-estimates (4), sales-invoices (4), teams-tasks (4), timesheets (4) | span.tab-text .nth(3) (.nth(1)) | Stale selector. #11177 (954aac863c) replaced the form's nb-tabset with its own tablist, button[role=tab]#project-form-tab-<id>, and moved Save to div.form-footer > button[status=primary]. | clickTabButton opens a tab by id: budget, settings, and old indices map to the same tabs. It checks aria-selected. Save waits until enabled and for the redirect; verifyProjectFormClosed proves the project was saved. |
↳ downstream of addProject | (never reached on stage) | Stale selectors, one group per area (rows below) | (fixes in the rows below) |
| ↳ clients, contacts-leads, customers | Contact wizard #10308 (e9e6bfd9fa): members picker is now ng-select.members-select; step buttons are .wizard-actions button.green; the panel-closing click target became the avatar aside. | Members picked through the new picker; the wizard must close after Save. | |
| ↳ estimates, invoices, sales-estimates, sales-invoices | The View button became action primary (5536f7b711), and the old edit selector then hit View. The contact select is wrapped in div.form-group (#10182). The actions popover now closes only on a click inside it. | Component-scoped selectors; toolbar buttons found by icon; waits on the real outcome instead of fixed sleeps. | |
| ↳ manage-employees | Invite dialog #10761 (699b3c7187), including the type-to-confirm delete. Account Save and language select #10115. Manage Invites #10113. | Updated selectors; the step types the invitee email before confirming. | |
| ↳ add-tasks, teams-tasks, timesheets | Once a project exists, the header renders its own ga-project-selector, which the bare selector matched first. | Pickers scoped to their dialog or timer. addTask waits for POST /api/tasks. | |
| ↳ expenses, payments | Bare div.ng-option click could hit the loading placeholder. | Option picked through selectNgOption; the payments note is unique per run and rows are selected by text. | |
| appointments (1), approval-request (1), edit-employee (2), goals-kpi (2), human-resources (2), proposals (4), time-off (4) | #inputImageUrl input[type="text"] in addEmployee | Changed flow. #10336 (a73de1d121) replaced the image-URL input with the <ngx-image-uploader> avatar, and moved every dialog action into nb-card-footer.dialog-footer (Continue, Continue, Create employees) with a header step bar. | The step uploads the picture through the avatar (new uploadImageThroughUploader helper). Each step change is confirmed on the step bar, and Create must close the dialog. |
↳ downstream of addEmployee | Breakdown rows and identity edit button (a15f8536cd, 33e3c09f30). Membership tabs (17125da2e8). Appointments header and pick-employee footer (d58dd759cb). | Updated selectors. goals-kpi, proposals and time-off use names unique per run, filter the grid, and wait for the dialog to close. | |
| add-user (1), edit-user (2), remove-user (4) | [placeholder="Image"] | Changed flow (#10336, same basic-info form) | Avatar upload. Confirm button scoped to ga-user-mutation; the dialog must close. |
| manage-interviews (3) | ga-candidate-mutation #inputImageUrl … in addCandidate | Changed flow (#10336). Behind it, #10359 (75957d91aa) rebuilt the interview dialog: Next and Save are in the footer, and an interviewer is required. | Avatar upload; footer Next and Save; an interviewer is picked. The title is unique per run because a duplicate title disables Next. |
| login "Log out" (3), edit-profile (2), my-tasks-tracked-in-timesheets (3) | p[routerlink="/auth/logout"] | Stale selector. #10044 (01171bba2e) made Sign out an <a class="panel-row" routerlink="/auth/logout">. The stage DOM shows the panel open with that link rendered. | gauzy-user-menu a[routerlink="/auth/logout"]. In my-tasks, the pickers are scoped, and the timesheet check that swallowed its own failure is a real assertion again. |
| candidates (1), manage-candidates-invites (3), invite-user (2) | nb-card-footer … button[status="success"] | Stale selector. #10761 (699b3c7187) redesigned the invite dialog: the send button is nb-card-footer.invite-footer .actions button[status=primary]. At failure the form was valid and Send was enabled. | New selector, which waits until enabled; the dialog must close. Invite delete is type-to-confirm. |
| manage-user-invites (3) | tr.angular2-smart-row not visible; the grid said "You have not invited any users." | Test data / order. Each shard seeds a fresh DB with DEMO=false, which creates no invites, and invite-user runs in another shard. | The scenario creates its own invite, filters the grid by its email, and types the email into the type-to-confirm delete dialog. |
| manage-interviews-criterions (3) | span.col-2 > button[status="success"] | Stale selector. #10359 (75957d91aa) moved both panels to ga-candidate-criterion-card. | Rescoped selectors; the same assertions. |
| organization-vendors (4) | vendor row via div.float-left.text-truncate | Stale selector. .float-left was removed in #10160 (c53db3d7c0). The trace DOM shows the saved row as div.ga-name-cell "Dominos Pizza", so the save worked. | div.ga-name-cell; names unique per run; rows selected by name. |
| file-storage (2) | nb-card-header > h6 "S3 Configuration" | Stale selector (#10481, 367113adaf) | .provider-heading > h6; submit button in .form-actions |
| goals-general-settings (2) | .settings-title caption | Stale selector (#10227, 33d464dba1): five captions are span.settings-label | :is(.settings-title, .settings-label) |
| import-export (2) | div.info | Stale selector (#10538, b1ac7714ea) | Scoped to section.actions-panel |
| reports (4) | div.align-items-center > h6 | Stale selector (#10289, f0bce080a9) | nb-card.report-card … h6.report-name and its toggle |
| settings-button (4) | Themes nb-select .nth(2) | Stale selector. In #10044 Themes became a popover, so Quick Settings has two selects. | Layout select is index 1; theme toggle selector updated |
| settings-features (4) | nb-card-header > header "Manage Features for" | Stale selector (#10439, 418bc244c6). The index-based toggle checks also no longer matched the seed order. | Checks by feature and sub-feature label; all 57 checks converted one-for-one. |
| roles-permissions (4) | "GENERAL card rendered 306 toggles; expected 307" | Catalog drift. PermissionGroups.GENERAL lists ORG_TAGS_EDIT twice (role-permission.model.ts l.598 and l.671). #10526 (09ecd8e5aa) renders each permission once, regrouped by section, so indexes no longer name toggles. ENABLED_PERMISSIONS was stale too (e.g. SUPER_ADMIN listed 179, the seed grants 364). This is not an app bug: SUPER_ADMIN was mapped label by label on the stage page. | Toggles identified by label. Each card must render every catalog permission exactly once (DEMO variant accepted). ENABLED_PERMISSIONS regenerated from the seed defaults plus plugin grants for all seven roles. |
Validation
- Typecheck:
tsc -p apps/gauzy-e2e/tests/tsconfig.json --noEmitshows only the 5 errors that already exist inchange-language.steps.ts. - Run 1 (40421871684, head
c327d2ce86): 77 passed, 1 flaky, 2 failed, 3 skipped.- Shards 1, 3 and 4 were green.
- The 3 skips are the existing runtime skips in documents-hub, organization-documents and organization-help-center, unchanged here.
- Run 1 found two more problems, both fixed in
171219ca2a:
| Scenario | Failure in run 1 | Root cause (evidence) | Fix |
|---|---|---|---|
| estimates (failed 3/3), sales-invoices (flaky, passed on retry) | the email dialog never closed | CI environment, not the app. In the trace, PUT /api/invoices/email/<address> was still pending after 75 s on every attempt. The committed .env.local points the API at smtp.gmail.com:465 with no credentials, so mail could never be delivered. From the runner, that connect can hang until nodemailer's 2-minute connection timeout, and the dialog waits for the API's answer. The step's re-click also queued a second and third email. | The e2e API's mail goes to a closed local port (MAIL_HOST=127.0.0.1, MAIL_PORT=2525), so a send fails at once and the dialog closes; this is a workflow env change. The confirm step no longer re-clicks while its request is in flight. |
| human-resources (failed 3/3) | chart option "Bar" never visible | Test timing. The forced coordinate click landed at y=416.5, on the panel header, while the KPI tiles were still pushing the panel down; the select settled at ~433. | Click the select's button without force, so Playwright waits for it to be stable, and confirm the list opened. |
- Run 2 (40454347665, head
171219ca2a): in progress; results will be posted here.
Known risks / follow-ups (not changed here)
- organization-public-page
try/catch: the scenario already wraps four of its seven steps intry/catch, which can hide failures in those steps. This PR neither adds nor removes them. Making them strict needs test-data and assertion rework; that is a follow-up. - Duplicate permission:
PermissionsEnum.ORG_TAGS_EDITis listed twice inPermissionGroups.GENERAL. It has no effect on behaviour (the UI de-duplicates), so it is left as is. - roles-permissions, roles other than SUPER_ADMIN: their lists come from the seed and plugin
defaultForsources. Only SUPER_ADMIN could be checked against the stage page. - Time budget: the long CRUD chains (estimates, my-tasks) run close to the 240 s test timeout.
- Uploaded pictures: upload success is reported, not asserted, because the file-storage scenario points its shard's tenant at a placeholder S3 bucket.
🤖 Generated with Claude Code