E2E Locators
How the waldur-integration-testing Playwright suite finds HomePort's elements: by roles, accessible names, data-testid and data-slot, never by CSS classes. The suite keeps these selectors in tests/components/homeport_hooks.py. For when to add which attribute, see class-names-and-data-attributes.md.
Removing or renaming a hook below breaks that suite without failing anything in HomePort. Move one in three steps, as the "Before you remove or rename" section of class-names-and-data-attributes.md describes.
Hooks
| Element | Hook |
|---|---|
| Page tabs row, with its actions | [data-testid="page-toolbar"] |
| Page tab row (navigation) | get_by_role("navigation", name="Page tabs") |
| A page tab | get_by_role("link", name=…) or get_by_role("button", name=…) inside the page toolbar |
| Tab with a submenu | its button has aria-haspopup="menu" |
| Tab submenu | get_by_role("menu") |
| Filter bar under the page tabs | [data-testid="filter-bar"] |
| Clear button on a filter badge | [data-testid="badge-right-icon"] button |
| App footer | [data-testid="app-footer"] |
| A footer link | get_by_role("link", name=…) inside the app footer |
| Open action menu | [data-testid="actions-menu"], or get_by_role("menu") |
| Action menu row | [data-testid="action-item"], or get_by_role("menuitem") (every row has the role, inside a menu or not) |
| Action menu toggle | [data-testid="actions-toggle"]; the icon-only kebab is also get_by_role("button", name="Actions") |
| Table filter toggle | get_by_role("button", name="Set filters") |
| "Add filter" menu | get_by_role("dialog", name="Add filter") |
| A filter in that menu | get_by_role("button", name=<filter title>) inside the dialog |
| A filter's field | [data-slot="filter-field"] |
| Router tab bar (page hero, table sub-tabs) | nav[aria-label="Tabs"] (TAB_BAR) |
| A router tab | get_by_role("link", name=…), or "button" for a disabled tab (tab_item()) |
| In-page section tabs | [role="tablist"] with get_by_role("tab", name=…) (SECTION_TABS) |
| A dialog | get_by_role("dialog"), filtered by name or text; its parts are [data-slot="modal-body"] and so on |
| A dialog's close button | get_by_role("button", name="Close") inside the dialog |
| Invite dialog's role picker (one row) | [data-testid="role-project-select"], then get_by_role("combobox", name="Role for <email>") |
| A role picker option | get_by_role("option", name=…), on the page, not scoped to the row |
| Sign-in callback error | the "Login failed" heading and its parent |
| Provider offering page body | [data-testid="provider-offering"] |
| Marketplace dialog: category listing | [data-testid="category-listing"] |
| Marketplace dialog: category item | [data-testid="category-item"] |
| Marketplace dialog: category group | [data-testid="category-group"] |
| Marketplace dialog: offering listing | [data-testid="offering-listing"] |
| Marketplace dialog: offering item | [data-testid="offering-item"] |
| Team chat: the open conversation | [data-testid="team-chat-conversation"] |
| A toast | [data-slot="toast"][data-variant="{status}"] |
| A date field | [data-slot="date-picker-input"] (DATE_INPUT) |
| A select's parts (react-select) | .metronic-select__*, allow-listed in UNKNOWN_CLASS_HOOKS in eslint.config.js |
Notes
A router tab bar's aria-label defaults to "Tabs"; keep that default (or update TAB_BAR) when labelling a bar.
The role picker is two selects: Role, then Project for a project-level role (name="Project for <email>"). Each is named after its row's email, or Role for row <n> while the email is empty. Their menus are portalled to <body>, so look up options on the page, not inside the row.