@camunda/e2e-test-suite 0.0.1198 → 0.0.1199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,8 +22,12 @@ declare class AppsPage {
22
22
  readonly tasklistMenuItem: Locator;
23
23
  readonly optimizeMenuItem: Locator;
24
24
  readonly adminMenuItem: Locator;
25
+ readonly operateMenuItem: Locator;
26
+ readonly consoleMenuItem: Locator;
27
+ readonly modelerMenuItem: Locator;
25
28
  constructor(page: Page);
26
29
  private reloadForRecovery;
30
+ private entryAppeared;
27
31
  clickCluster(component: Locator, name: string): Promise<void>;
28
32
  clickModeler(): Promise<void>;
29
33
  private isModelerAuthTransit;
@@ -27,6 +27,9 @@ class AppsPage {
27
27
  tasklistMenuItem;
28
28
  optimizeMenuItem;
29
29
  adminMenuItem;
30
+ operateMenuItem;
31
+ consoleMenuItem;
32
+ modelerMenuItem;
30
33
  constructor(page) {
31
34
  this.page = page;
32
35
  this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
@@ -52,6 +55,15 @@ class AppsPage {
52
55
  this.adminMenuItem = page.getByRole('menuitemradio', {
53
56
  name: /^(Admin|Identity)$/, // Identity Should be removed after https://github.com/camunda/camunda/issues/46447 is fixed
54
57
  });
58
+ // Admin is on nav V2 too now, so the switcher is also the way OUT of an
59
+ // app and not only the way in. Console and Modeler are org-level entries
60
+ // of the same dropdown (`CamundaAppKey` in @camunda/design-system covers
61
+ // operate | tasklist | optimize | admin | identity | modeler | console),
62
+ // so all three need the `menuitemradio` shape their link-role
63
+ // counterparts above no longer have.
64
+ this.operateMenuItem = page.getByRole('menuitemradio', { name: 'Operate' });
65
+ this.consoleMenuItem = page.getByRole('menuitemradio', { name: 'Console' });
66
+ this.modelerMenuItem = page.getByRole('menuitemradio', { name: 'Modeler' });
55
67
  this.tasklistListItem = this.page
56
68
  .getByRole('listitem')
57
69
  .filter({ hasText: 'Tasklist' });
@@ -115,6 +127,16 @@ class AppsPage {
115
127
  console.warn(`Could not reload while ${whileDoing}: ${error}`);
116
128
  }
117
129
  }
130
+ // Boolean probe for "did an apps-menu entry render?", with the same window
131
+ // the `expect(...).toBeVisible({timeout: 30000})` calls it replaces used —
132
+ // so a slow legacy flyout still gets its full 30s before any caller decides
133
+ // the entry is absent rather than merely late.
134
+ async entryAppeared(entry) {
135
+ return entry
136
+ .waitFor({ state: 'visible', timeout: 30000 })
137
+ .then(() => true)
138
+ .catch(() => false);
139
+ }
118
140
  async clickCluster(component, name) {
119
141
  // Cluster names are not unique in an org, so this app's sub-menu can list
120
142
  // the same name twice; pick the first entry (same row the Console cluster
@@ -131,17 +153,25 @@ class AppsPage {
131
153
  .getByText(/local_rate_limited/i)
132
154
  .or(this.page.getByText(/Well, that's awkward/i))
133
155
  .first();
156
+ // nav V2 breadcrumb switcher: Modeler is a `menuitemradio`, the legacy
157
+ // apps flyout renders it as a link. Resolve whichever this build shows.
158
+ const modelerEntry = this.modelerMenuItem.or(this.modelerLink).first();
134
159
  for (let retries = 0; retries < maxRetries; retries++) {
135
160
  try {
136
- if (retries === 0) {
137
- await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
138
- await this.modelerLink.click();
161
+ if (retries > 0) {
162
+ await this.clickCamundaApps();
139
163
  }
140
- else {
164
+ if (!(await this.entryAppeared(modelerEntry))) {
165
+ // Nothing to click, because the nav V2 breadcrumb switcher lists
166
+ // only the cluster's OWN webapps. Modeler is org-level, so unlike
167
+ // Operate it has no entry there at all. Re-enter through Console,
168
+ // whose apps flyout does list it — the same recovery the
169
+ // `open*WithRetry` helpers below use.
170
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
141
171
  await this.clickCamundaApps();
142
- await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
143
- await this.modelerLink.click();
172
+ await (0, test_1.expect)(modelerEntry).toBeVisible({ timeout: 30000 });
144
173
  }
174
+ await modelerEntry.click();
145
175
  // After clicking, the browser is redirected through
146
176
  // modeler.ultrawombat.com/login which can return 429
147
177
  // (`local_rate_limited`) or a generic "Well, that's awkward" error
@@ -473,24 +503,21 @@ class AppsPage {
473
503
  const maxRetries = 3;
474
504
  for (let retries = 0; retries < maxRetries; retries++) {
475
505
  try {
476
- if (retries === 0) {
477
- if (await this.operateLink.isVisible({ timeout: 5000 })) {
478
- //Single Cluster
479
- await this.operateLink.click({ timeout: 10000 });
480
- }
481
- else {
482
- //Multiple Cluster
483
- await this.doClickClusterInOperate(clusterName);
484
- }
506
+ if (retries > 0) {
507
+ await this.clickCamundaApps();
508
+ }
509
+ if (await this.operateMenuItem.isVisible({ timeout: 5000 })) {
510
+ // nav V2 breadcrumb switcher: apps are menuitemradio entries that
511
+ // hop to the same cluster's app directly, no cluster sub-select.
512
+ await this.operateMenuItem.click({ timeout: 10000 });
513
+ }
514
+ else if (await this.operateLink.isVisible({ timeout: 5000 })) {
515
+ //Single Cluster
516
+ await this.operateLink.click({ timeout: 10000 });
485
517
  }
486
518
  else {
487
- await this.clickCamundaApps();
488
- if (await this.operateLink.isVisible({ timeout: 5000 })) {
489
- await this.operateLink.click({ timeout: 10000 });
490
- }
491
- else {
492
- await this.doClickClusterInOperate(clusterName);
493
- }
519
+ //Multiple Cluster
520
+ await this.doClickClusterInOperate(clusterName);
494
521
  }
495
522
  return;
496
523
  }
@@ -562,17 +589,25 @@ class AppsPage {
562
589
  }
563
590
  async clickConsoleLink() {
564
591
  const maxRetries = 3;
592
+ // nav V2 breadcrumb switcher: Console is a `menuitemradio`, the legacy
593
+ // apps flyout renders it as a link. Resolve whichever this build shows.
594
+ const consoleEntry = this.consoleMenuItem.or(this.consoleLink).first();
565
595
  for (let retries = 0; retries < maxRetries; retries++) {
566
596
  try {
567
- if (retries === 0) {
568
- await (0, test_1.expect)(this.consoleLink).toBeVisible({ timeout: 30000 });
569
- await this.consoleLink.click();
570
- }
571
- else {
597
+ if (retries > 0) {
572
598
  await this.clickCamundaApps();
573
- await (0, test_1.expect)(this.consoleLink).toBeVisible({ timeout: 30000 });
574
- await this.consoleLink.click();
575
599
  }
600
+ if (await this.entryAppeared(consoleEntry)) {
601
+ await consoleEntry.click();
602
+ return;
603
+ }
604
+ // Nothing to click, because the nav V2 breadcrumb switcher lists only
605
+ // the cluster's OWN webapps (Operate, Tasklist, Optimize, Admin) —
606
+ // which is why `clickOperate` resolves from Admin through
607
+ // `operateMenuItem` and this does not. Console is org-level and has no
608
+ // entry there, but it IS what `baseURL` points at, so navigate to it
609
+ // directly rather than re-opening a menu that will never list it.
610
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
576
611
  return;
577
612
  }
578
613
  catch (error) {
@@ -2,13 +2,16 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class Authorization {
3
3
  readonly page: Page;
4
4
  protected readonly selectResourceTypeTab: (name: string) => Locator;
5
+ protected readonly resourceTypeFilterCombobox: Locator;
5
6
  protected readonly createAuthorizationModal: Locator;
6
7
  protected readonly createAuthorizationButton: Locator;
7
8
  protected readonly createAuthorizationOwnerTypeCombobox: Locator;
8
9
  protected readonly createAuthorizationOwnerTypeOption: (name: string) => Locator;
9
10
  protected readonly createAuthorizationOwnerComboBox: Locator;
10
11
  protected readonly createAuthorizationOwnerSearchInput: Locator;
11
- protected readonly createAuthorizationOwnerLegacyComboBox: Locator;
12
+ protected readonly createAuthorizationOwnerSearchOption: (name: string) => Locator;
13
+ protected readonly createAuthorizationOwnerCommandInput: Locator;
14
+ protected readonly createAuthorizationOwnerCommandOptions: Locator;
12
15
  protected readonly createAuthorizationOwnerOption: (name: string) => Locator;
13
16
  protected readonly createAuthorizationResourceTypeDropdown: Locator;
14
17
  protected readonly createAuthorizationResourceTypeOption: (name: string) => Locator;
@@ -19,8 +22,11 @@ declare class Authorization {
19
22
  protected readonly deleteAuthorizationButton: (name: string) => Locator;
20
23
  protected readonly deleteAuthorizationModal: Locator;
21
24
  protected readonly deleteAuthorizationModalDeleteButton: Locator;
25
+ protected readonly tableFilter: Locator;
22
26
  constructor(page: Page);
27
+ protected selectResourceType(resourceType: string, timeout?: number): Promise<void>;
23
28
  assertAuthorization(auth: string): Promise<void>;
29
+ private selectAuthorizationOwner;
24
30
  createAuthorization(authorization: {
25
31
  ownerType: string;
26
32
  ownerId: string;
@@ -29,6 +35,10 @@ declare class Authorization {
29
35
  accessPermissions: string[];
30
36
  }): Promise<void>;
31
37
  selectResourceTypeFromDialog(resourceType: string): Promise<void>;
38
+ private checkAccessPermission;
39
+ private authorizationRow;
40
+ private expectAuthorizationListed;
41
+ private filterAuthorizationsByOwner;
32
42
  deleteAuthorization(resourceId: string, resourceType: string): Promise<void>;
33
43
  }
34
44
  export { Authorization };
@@ -2,18 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Authorization = void 0;
4
4
  const test_1 = require("@playwright/test");
5
- const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
6
5
  const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
6
+ const findLocatorWithPagination_1 = require("../../utils/assertionHelpers/findLocatorWithPagination");
7
+ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
7
8
  class Authorization {
8
9
  page;
9
10
  selectResourceTypeTab;
11
+ resourceTypeFilterCombobox;
10
12
  createAuthorizationModal;
11
13
  createAuthorizationButton;
12
14
  createAuthorizationOwnerTypeCombobox;
13
15
  createAuthorizationOwnerTypeOption;
14
16
  createAuthorizationOwnerComboBox;
15
17
  createAuthorizationOwnerSearchInput;
16
- createAuthorizationOwnerLegacyComboBox;
18
+ createAuthorizationOwnerSearchOption;
19
+ createAuthorizationOwnerCommandInput;
20
+ createAuthorizationOwnerCommandOptions;
17
21
  createAuthorizationOwnerOption;
18
22
  createAuthorizationResourceTypeDropdown;
19
23
  createAuthorizationResourceTypeOption;
@@ -24,9 +28,20 @@ class Authorization {
24
28
  deleteAuthorizationButton;
25
29
  deleteAuthorizationModal;
26
30
  deleteAuthorizationModalDeleteButton;
31
+ tableFilter;
27
32
  constructor(page) {
28
33
  this.page = page;
29
34
  this.selectResourceTypeTab = (resourceType) => page.getByRole('tab', { name: resourceType });
35
+ // The Admin's Authorizations page moved onto the new design-system shell,
36
+ // which replaces the per-resource-type Carbon tabs with a single filter
37
+ // combobox in a left "Filter" card (identity/client ListV2.tsx). The
38
+ // combobox's accessible name was renamed from "Authorization type" to
39
+ // "Resource type" in camunda/camunda commit 377b0e3; match either so both
40
+ // image versions resolve.
41
+ this.resourceTypeFilterCombobox = page
42
+ .getByRole('combobox', { name: 'Resource type' })
43
+ .or(page.getByRole('combobox', { name: 'Authorization type' }))
44
+ .first();
30
45
  this.authorizationsList = page.getByRole('table');
31
46
  this.createAuthorizationButton = page.getByRole('button', {
32
47
  name: /create authorization/i,
@@ -38,63 +53,84 @@ class Authorization {
38
53
  this.createAuthorizationModal.getByRole('combobox', {
39
54
  name: 'Owner type',
40
55
  });
41
- this.createAuthorizationOwnerTypeOption = (name) => this.createAuthorizationModal.getByRole('option', {
56
+ // The design-system Select portals its listbox to the end of <body>, so
57
+ // the options are siblings of the dialog rather than descendants, and
58
+ // while the listbox is open Radix marks everything outside it aria-hidden
59
+ // — which takes the dialog itself out of the accessibility tree. Query the
60
+ // options from the page root so both placements resolve.
61
+ this.createAuthorizationOwnerTypeOption = (name) => this.page
62
+ .getByRole('option', {
42
63
  name,
43
64
  exact: true,
44
- });
45
- // The Owner field has two shapes depending on the build under test.
46
- //
47
- // Since camunda/camunda#57931 Identity renders an entity-search dropdown
48
- // (identity/client/src/components/form/EntitySearchSingleSelect.tsx ->
49
- // EntitySearchDropdown -> DropdownSearch): a bare `FormLabel` plus a
50
- // Carbon `Search` input carrying only the "Search by owner ID"
51
- // placeholder, with nothing exposing the accessible name "Owner".
52
- //
53
- // Builds that predate that change — notably released alpha generations
54
- // such as 8.10.0-alpha5, tagged 2026-08-31 before the stable/8.10
55
- // backport (#61570) landed on 2026-09-01 — still render the older Carbon
56
- // `ComboBox` (owner-selection/OwnerSelection.tsx) whose `titleText` is
57
- // "Owner". Match either so the suite runs against both.
58
- this.createAuthorizationOwnerSearchInput =
59
- this.createAuthorizationModal.getByPlaceholder('Search by owner ID');
60
- this.createAuthorizationOwnerLegacyComboBox =
61
- this.createAuthorizationModal.getByRole('combobox', {
62
- name: 'Owner',
63
- exact: true,
64
- });
65
- this.createAuthorizationOwnerComboBox =
66
- this.createAuthorizationOwnerSearchInput
67
- .or(this.createAuthorizationOwnerLegacyComboBox)
68
- .first();
65
+ })
66
+ .first();
67
+ // On the design-system build the closed Owner control is a combobox-role
68
+ // trigger rendered by EntitySearchSingleSelect. Its <Label htmlFor> does
69
+ // not always name the focusable trigger, so the product team's own suite
70
+ // identifies it by the placeholder it paints as static text instead
71
+ // (camunda/camunda qa/c8-orchestration-cluster-e2e-test-suite/pages/
72
+ // IdentityAuthorizationsPage.ts). Match either, and keep `exact` on the
73
+ // label form so it cannot collide with "Owner type".
74
+ this.createAuthorizationOwnerComboBox = this.createAuthorizationModal
75
+ .getByRole('combobox', {
76
+ name: 'Owner',
77
+ exact: true,
78
+ })
79
+ .or(this.createAuthorizationModal
80
+ .getByRole('combobox')
81
+ .filter({ hasText: 'Search by owner ID' }))
82
+ .first();
69
83
  this.createAuthorizationOwnerOption = (name) => this.createAuthorizationModal.getByRole('option', {
70
84
  name,
71
85
  exact: true,
72
86
  });
87
+ // Every owner type now renders a server-backed search dropdown instead of
88
+ // the combobox that listed all owners up front. On the entity-search build
89
+ // that dropdown is a Carbon Search inside a ListBox, so it has no combobox
90
+ // role and its results are list-box menu items. The design-system build
91
+ // renders the same field as a cmdk command palette whose results are
92
+ // role=option items in a popover portalled out of the dialog.
93
+ this.createAuthorizationOwnerSearchInput =
94
+ this.createAuthorizationModal.getByPlaceholder('Search by owner ID');
95
+ this.createAuthorizationOwnerSearchOption = (name) => this.page.getByRole('option').filter({ hasText: name }).first();
96
+ // In the design-system build the closed Owner control is a combobox-role
97
+ // trigger that paints "Search by owner ID" as static text (no placeholder
98
+ // attribute), and the cmdk search box plus its result listbox only mount
99
+ // in a popover once the trigger is opened. That popover is portalled out
100
+ // of the dialog, and while it is open Radix marks the dialog aria-hidden,
101
+ // so both must be queried from the page root.
102
+ this.createAuthorizationOwnerCommandInput = this.page
103
+ .locator('[data-slot="command-input"]')
104
+ .first();
105
+ this.createAuthorizationOwnerCommandOptions = this.page
106
+ .getByRole('listbox')
107
+ .getByRole('option');
73
108
  this.createAuthorizationResourceTypeDropdown =
74
109
  this.createAuthorizationModal.getByRole('combobox', {
75
110
  name: 'Resource type',
76
111
  });
77
- this.createAuthorizationResourceTypeOption = (name) => this.createAuthorizationModal.getByRole('option', {
112
+ this.createAuthorizationResourceTypeOption = (name) => this.page
113
+ .getByRole('option', {
78
114
  name,
79
115
  exact: true,
80
- });
116
+ })
117
+ .first();
81
118
  this.createAuthorizationResourceIdField =
82
119
  this.createAuthorizationModal.getByRole('textbox', {
83
120
  name: 'Resource ID',
84
121
  });
85
- this.createAuthorizationAccessPermission = (name) => this.createAuthorizationModal.getByRole('checkbox', {
86
- name,
87
- });
122
+ // Carbon wired its checkbox input and label together the same way the
123
+ // design-system Checkbox does (`id={permission}` / `htmlFor={permission}`),
124
+ // and on both builds the input itself is not the click target — the label
125
+ // is.
126
+ this.createAuthorizationAccessPermission = (name) => this.createAuthorizationModal.locator(`label[for="${name}"]`);
88
127
  this.createAuthorizationSubmitButton =
89
128
  this.createAuthorizationModal.getByRole('button', {
90
129
  name: 'Create authorization',
91
130
  });
92
- this.authorizationsList = page.getByRole('table');
93
131
  this.deleteAuthorizationButton = (name) => this.authorizationsList
94
- .getByRole('row')
95
- .filter({ hasText: name })
96
- .first()
97
- .getByRole('button', { name: 'danger Delete' });
132
+ .getByRole('row', { name })
133
+ .getByRole('button', { name: /^delete/i });
98
134
  this.deleteAuthorizationModal = page.getByRole('dialog', {
99
135
  name: /delete authorization/i,
100
136
  });
@@ -102,6 +138,30 @@ class Authorization {
102
138
  this.deleteAuthorizationModal.getByRole('button', {
103
139
  name: /delete authorization/i,
104
140
  });
141
+ // Carbon labelled the table toolbar search "Filter table"; the
142
+ // design-system EntityList exposes the placeholder as the accessible name
143
+ // instead. Match either.
144
+ this.tableFilter = page
145
+ .getByRole('searchbox', { name: 'Filter table' })
146
+ .or(page.getByRole('searchbox', { name: 'Search by owner ID' }))
147
+ .first();
148
+ }
149
+ // Select the resource type through whichever control this build renders:
150
+ // click the Carbon tab, or open the design-system filter combobox and pick
151
+ // the option.
152
+ async selectResourceType(resourceType, timeout = 60000) {
153
+ const tab = this.selectResourceTypeTab(resourceType);
154
+ await (0, test_1.expect)(tab.or(this.resourceTypeFilterCombobox)).toBeVisible({
155
+ timeout,
156
+ });
157
+ if (await tab.isVisible()) {
158
+ await tab.click();
159
+ return;
160
+ }
161
+ await this.resourceTypeFilterCombobox.click();
162
+ await this.page
163
+ .getByRole('option', { name: resourceType, exact: true })
164
+ .click({ timeout: Math.min(timeout, 30000) });
105
165
  }
106
166
  // The authorizations table paginates, and the RBA clusters these flows run
107
167
  // against are long-lived: every night's run leaves more process-definition
@@ -114,100 +174,236 @@ class Authorization {
114
174
  async assertAuthorization(auth) {
115
175
  await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, this.authorizationsList.getByRole('row').filter({ hasText: auth }).first());
116
176
  }
177
+ // Released 8.10 alpha images still render the Carbon combobox that lists
178
+ // every owner, so accept whichever of the three owner fields this build
179
+ // shows: the Carbon search dropdown, the Carbon combobox that lists every
180
+ // owner inline, or the design-system combobox whose cmdk popover only loads
181
+ // results once the owner ID is typed.
182
+ async selectAuthorizationOwner(ownerId) {
183
+ await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput.or(this.createAuthorizationOwnerComboBox)).toBeVisible({ timeout: 60000 });
184
+ if (await this.createAuthorizationOwnerSearchInput.isVisible()) {
185
+ await this.createAuthorizationOwnerSearchInput.fill(ownerId);
186
+ const ownerOption = this.createAuthorizationOwnerSearchOption(ownerId);
187
+ // The search is debounced and served by the API, so a just-created
188
+ // owner can take a while to surface.
189
+ await (0, test_1.expect)(ownerOption).toBeVisible({ timeout: 60000 });
190
+ await ownerOption.click({ timeout: 30000 });
191
+ // Picking an owner replaces the search input with a tag holding the
192
+ // selection, which is how we know the choice was committed.
193
+ await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput).toBeHidden({
194
+ timeout: 30000,
195
+ });
196
+ return;
197
+ }
198
+ await this.createAuthorizationOwnerComboBox.click();
199
+ const commandInput = this.createAuthorizationOwnerCommandInput;
200
+ const inlineOwnerOption = this.createAuthorizationOwnerOption(ownerId);
201
+ await (0, test_1.expect)(commandInput.or(inlineOwnerOption).first()).toBeVisible({
202
+ timeout: 30000,
203
+ });
204
+ if (await commandInput.isVisible()) {
205
+ await commandInput.fill(ownerId);
206
+ // The result list is served by the API and debounced, and each option is
207
+ // labelled "<owner id> — <name>" rather than the bare ID
208
+ // (identity/client RoleSelection.tsx `itemLabel`), so narrow the search
209
+ // to the single row this unique owner ID resolves to instead of matching
210
+ // on the label text.
211
+ const ownerOptions = this.createAuthorizationOwnerCommandOptions;
212
+ await (0, test_1.expect)(ownerOptions).toHaveCount(1, { timeout: 60000 });
213
+ await ownerOptions.first().click({ timeout: 30000, force: true });
214
+ // Committing the choice closes the popover, which is what hands the
215
+ // dialog back to the accessibility tree for the fields that follow.
216
+ await (0, test_1.expect)(commandInput).toBeHidden({ timeout: 30000 });
217
+ return;
218
+ }
219
+ await (0, test_1.expect)(inlineOwnerOption).toBeVisible({ timeout: 30000 });
220
+ await inlineOwnerOption.click({ timeout: 30000 });
221
+ }
117
222
  async createAuthorization(authorization) {
118
223
  await this.createAuthorizationButton.click();
119
224
  await (0, test_1.expect)(this.createAuthorizationModal).toBeVisible();
120
225
  await this.createAuthorizationOwnerTypeCombobox.click();
121
226
  await this.createAuthorizationOwnerTypeOption(authorization.ownerType).click();
122
- // Owner type selection re-renders the owner field, and on the
123
- // entity-search build that swap is query-driven, so it can lag well past
124
- // 30s under CI load — the product team's own e2e suite raised an
125
- // analogous wait on the same widget from 20s to 60s (camunda/camunda#61353).
126
- await (0, test_1.expect)(this.createAuthorizationOwnerComboBox).toBeVisible({
127
- timeout: 60000,
128
- });
129
- const usesEntitySearch = await this.createAuthorizationOwnerSearchInput.isVisible();
130
- const ownerField = usesEntitySearch
131
- ? this.createAuthorizationOwnerSearchInput
132
- : this.createAuthorizationOwnerLegacyComboBox;
133
- if (!usesEntitySearch) {
134
- // The legacy Carbon ComboBox renders its menu only once opened; the
135
- // entity-search input renders results off the typed value alone.
136
- await ownerField.click();
137
- }
138
- await ownerField.fill(authorization.ownerId);
139
- // The dropdown renders results only while the search box holds text, and
140
- // the list is fetched from the server on a debounce rather than filtered
141
- // client-side, so the matching entry appears a beat later. Clicking the
142
- // Carbon menu item is what commits the selection: DropdownSearch wires
143
- // `onSelect` to `ListBox.MenuItem`'s onClick
144
- // (identity/client/src/components/form/DropdownSearch.tsx), and that is
145
- // the only path that promotes an entry into the form's owner value.
146
- const ownerMenuItem = this.createAuthorizationModal
147
- .locator('.cds--list-box__menu-item')
148
- .filter({ hasText: authorization.ownerId })
149
- .first();
150
- await (0, test_1.expect)(ownerMenuItem).toBeVisible({ timeout: 60000 });
151
- await ownerMenuItem.click({ timeout: 20000 });
152
- // On the entity-search build a committed selection swaps the search box
153
- // for a filter Tag (EntitySearchSingleSelect renders one or the other), so
154
- // the box going away is how we know the owner actually landed on the form
155
- // rather than the click being swallowed. The legacy ComboBox keeps its
156
- // input and shows the selection as the input value, so this check only
157
- // applies to the entity-search shape.
158
- if (usesEntitySearch) {
159
- await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput).toBeHidden({
160
- timeout: 15000,
161
- });
162
- }
227
+ await this.selectAuthorizationOwner(authorization.ownerId);
163
228
  await this.selectResourceTypeFromDialog(authorization.resourceType);
164
229
  await this.createAuthorizationResourceIdField.fill(authorization.resourceId);
165
230
  for (const permission of authorization.accessPermissions) {
166
- await this.createAuthorizationAccessPermission(permission).click({
167
- force: true,
168
- });
231
+ await this.checkAccessPermission(permission);
169
232
  }
170
- await this.createAuthorizationSubmitButton.click();
171
- await (0, test_1.expect)(this.createAuthorizationModal).not.toBeVisible({
233
+ await (0, submitModalWithRetry_1.submitModalWithRetry)(this.createAuthorizationSubmitButton, this.createAuthorizationModal);
234
+ await this.selectResourceType(authorization.resourceType);
235
+ await this.expectAuthorizationListed(authorization, this.authorizationRow(authorization));
236
+ }
237
+ // The Resource type field is pre-selected from the list's current resource
238
+ // type and rendered `disabled` on the design-system build
239
+ // (identity/client/src/pages/authorizations/modalsV2/add-modal/AddModal.tsx
240
+ // passes a literal `disabled`), so there is nothing to pick. Only open the
241
+ // dropdown when the field is still editable.
242
+ async selectResourceTypeFromDialog(resourceType) {
243
+ await (0, test_1.expect)(this.createAuthorizationResourceTypeDropdown).toBeVisible({
172
244
  timeout: 30000,
173
245
  });
174
- await this.selectResourceTypeTab(authorization.resourceType).click();
175
- const item = this.page.getByRole('row').filter({
176
- hasText: `${authorization.ownerId}${authorization.resourceId}`,
177
- });
178
- await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, item, {
179
- clickLocator: this.selectResourceTypeTab(authorization.resourceType),
246
+ if (!(await this.createAuthorizationResourceTypeDropdown.isEnabled())) {
247
+ return;
248
+ }
249
+ await this.createAuthorizationResourceTypeDropdown.click();
250
+ await this.createAuthorizationResourceTypeOption(resourceType).click({
251
+ timeout: 30000,
180
252
  });
181
253
  }
182
- async selectResourceTypeFromDialog(resourceType) {
183
- const isDisabled = await this.createAuthorizationResourceTypeDropdown.isDisabled();
184
- if (isDisabled) {
254
+ // The permission control is a label + hidden input pair on both builds, so
255
+ // the label is the click target while the input carries the checked state.
256
+ // The design-system Checkbox re-renders on every toggle, which can swallow a
257
+ // click that lands mid-render, so confirm the state and re-click if needed.
258
+ async checkAccessPermission(permission) {
259
+ const permissionLabel = this.createAuthorizationAccessPermission(permission);
260
+ const permissionInput = this.createAuthorizationModal.locator(`input[id="${permission}"], [role="checkbox"][id="${permission}"]`);
261
+ await (0, test_1.expect)(permissionLabel).toBeVisible({ timeout: 30000 });
262
+ await permissionLabel.scrollIntoViewIfNeeded();
263
+ const maxAttempts = 3;
264
+ for (let attempt = 1; attempt < maxAttempts; attempt++) {
265
+ if (await permissionInput.isChecked()) {
266
+ return;
267
+ }
268
+ await permissionLabel.click({ force: true });
269
+ try {
270
+ await (0, test_1.expect)(permissionInput).toBeChecked({ timeout: 5000 });
271
+ return;
272
+ }
273
+ catch {
274
+ // A click that landed mid-rerender is dropped: try again. The
275
+ // assertion after the loop is the one that reports a permission that
276
+ // never took.
277
+ }
278
+ }
279
+ if (!(await permissionInput.isChecked())) {
280
+ await permissionLabel.click({ force: true });
281
+ }
282
+ await (0, test_1.expect)(permissionInput).toBeChecked({ timeout: 10000 });
283
+ }
284
+ // The authorizations list caps every cell at 25 characters
285
+ // (`maxDisplayCellLength={25}` in camunda/camunda
286
+ // identity/client/src/pages/authorizations/AuthorizationsListV2.tsx), and on
287
+ // the design-system build `truncateCell` renders an over-long value as
288
+ // `<button aria-label="<full value>">{first 25 chars}…</button>` with the
289
+ // tooltip carrying the rest portalled out of the row. An owner ID longer
290
+ // than 25 characters - the RBA specs' 33-character
291
+ // `rbaSelectedProcessesPermission<xxx>` roles - is therefore absent from the
292
+ // row's text, so filtering rows on `<ownerId><resourceId>` cannot match it
293
+ // however long we wait. The full value survives in the cell's accessible
294
+ // name, so also accept a row that HAS a cell named exactly the owner ID.
295
+ // Carbon truncates at 25 as well, but renders its tooltip inline, which
296
+ // keeps the full value in the row's text - the `hasText` branch covers that
297
+ // build, and short owner IDs resolve on both.
298
+ authorizationRow(authorization) {
299
+ const rows = this.page.getByRole('row');
300
+ return rows
301
+ .filter({ hasText: `${authorization.ownerId}${authorization.resourceId}` })
302
+ .or(rows
303
+ .filter({
304
+ has: this.page.getByRole('cell', {
305
+ name: authorization.ownerId,
306
+ exact: true,
307
+ }),
308
+ })
309
+ .filter({ hasText: authorization.resourceId }))
310
+ .first();
311
+ }
312
+ // The toolbar search filters by owner ID, so filling it with the resource
313
+ // type narrows nothing, and the retry helper it fed reloads before it
314
+ // asserts, which dropped the filter entirely. That left the new row to be
315
+ // found on page 1 of the unfiltered table - and the authorizations table
316
+ // paginates, so with the RBA specs creating owners in parallel it is
317
+ // regularly on a later page. Filter by owner, then page through what is
318
+ // left; fall back to the unfiltered table for builds whose search has not
319
+ // caught up with a just-created owner yet.
320
+ async expectAuthorizationListed(authorization, item) {
321
+ const deadline = Date.now() + 180000;
322
+ const budget = (max) => Math.max(5000, Math.min(max, deadline - Date.now()));
323
+ const maxAttempts = 5;
324
+ let lastError;
325
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
326
+ if (attempt > 0 && Date.now() >= deadline) {
327
+ break;
328
+ }
329
+ try {
330
+ await this.selectResourceType(authorization.resourceType, budget(30000));
331
+ for (const filterValue of [authorization.ownerId, '']) {
332
+ await this.filterAuthorizationsByOwner(filterValue);
333
+ const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, item, {
334
+ timeout: budget(filterValue === '' ? 45000 : 15000),
335
+ });
336
+ if (found) {
337
+ await (0, test_1.expect)(item).toBeVisible({ timeout: 5000 });
338
+ return;
339
+ }
340
+ }
341
+ throw new Error(`No row for owner "${authorization.ownerId}" and resource ` +
342
+ `"${authorization.resourceId}" in the ` +
343
+ `${authorization.resourceType} table`);
344
+ }
345
+ catch (error) {
346
+ lastError = error;
347
+ if (attempt < maxAttempts - 1) {
348
+ console.warn(`Attempt ${attempt + 1} did not list the ` +
349
+ `${authorization.resourceType} authorization for ` +
350
+ `"${authorization.ownerId}". Retrying...`);
351
+ await this.page.reload();
352
+ await this.page.waitForLoadState('domcontentloaded');
353
+ }
354
+ }
355
+ }
356
+ throw new Error(`Authorization for "${authorization.ownerId}" on ` +
357
+ `"${authorization.resourceId}" was not listed: ${String(lastError)}`);
358
+ }
359
+ async filterAuthorizationsByOwner(ownerId) {
360
+ const hasFilter = await this.tableFilter
361
+ .isVisible({ timeout: 5000 })
362
+ .catch(() => false);
363
+ if (!hasFilter) {
185
364
  return;
186
365
  }
187
- await this.createAuthorizationResourceTypeDropdown.click();
188
- await this.createAuthorizationResourceTypeOption(resourceType).click();
366
+ await this.tableFilter.fill(ownerId);
367
+ // The search is debounced and served by the API; the paginated lookup this
368
+ // feeds waits for the row itself, so there is nothing to wait on here
369
+ // beyond the value having landed in the box.
370
+ await (0, test_1.expect)(this.tableFilter).toHaveValue(ownerId);
189
371
  }
190
372
  async deleteAuthorization(resourceId, resourceType) {
191
- const locator = this.deleteAuthorizationButton(resourceId);
192
- await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, locator, {
193
- clickLocator: this.selectResourceTypeTab(resourceType),
194
- });
195
- await locator.click();
373
+ // Same paginated table as createAuthorization: the row being deleted is
374
+ // not necessarily on page 1, and the toolbar search cannot narrow by
375
+ // resource ID.
376
+ const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, this.deleteAuthorizationButton(resourceId), { timeout: 60000 });
377
+ if (!found) {
378
+ throw new Error(`No ${resourceType} authorization row for resource "${resourceId}"`);
379
+ }
380
+ await this.deleteAuthorizationButton(resourceId).click();
196
381
  await (0, test_1.expect)(this.deleteAuthorizationModal).toBeVisible();
197
382
  await this.deleteAuthorizationModalDeleteButton.click();
198
383
  await (0, test_1.expect)(this.deleteAuthorizationModal).not.toBeVisible();
199
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.authorizationsList
200
- .getByRole('cell', {
201
- name: resourceId,
202
- })
203
- .first(), {
204
- preAction: async () => {
205
- await this.selectResourceTypeTab(resourceType);
206
- await this.page.reload();
207
- await this.page.waitForTimeout(1000);
208
- },
209
- shouldBeVisible: false,
210
- });
384
+ // Re-selecting the resource type takes two interactions on the
385
+ // design-system build, so this cannot go through the shared
386
+ // reload-click-assert helper, which only accepts a single click locator.
387
+ const deletedCell = this.authorizationsList
388
+ .getByRole('cell', { name: resourceId })
389
+ .first();
390
+ let lastError;
391
+ for (let attempt = 0; attempt < 3; attempt++) {
392
+ await this.page.reload();
393
+ await this.page.waitForLoadState('domcontentloaded');
394
+ await this.selectResourceType(resourceType);
395
+ try {
396
+ await (0, test_1.expect)(deletedCell).not.toBeVisible({ timeout: 5000 });
397
+ return;
398
+ }
399
+ catch (error) {
400
+ lastError = error;
401
+ console.warn(`Attempt ${attempt + 1} still listed the ${resourceType} ` +
402
+ `authorization for "${resourceId}". Retrying...`);
403
+ }
404
+ }
405
+ throw new Error(`Authorization for resource "${resourceId}" is still listed: ` +
406
+ String(lastError));
211
407
  }
212
408
  }
213
409
  exports.Authorization = Authorization;
@@ -2,31 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OCIdentityAuthorizationsPage = void 0;
4
4
  const Authorization_1 = require("../8.10/Authorization");
5
- const test_1 = require("@playwright/test");
6
- const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
5
  class OCIdentityAuthorizationsPage extends Authorization_1.Authorization {
8
6
  constructor(page) {
9
7
  super(page);
10
8
  }
11
9
  async clickProcessDefinitionTab() {
12
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.selectResourceTypeTab('PROCESS_DEFINITION'));
13
- await this.selectResourceTypeTab('PROCESS_DEFINITION').click();
10
+ await this.selectResourceType('PROCESS_DEFINITION');
14
11
  }
15
12
  async clickAuthorizationTab() {
16
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.selectResourceTypeTab('AUTHORIZATION'));
17
- await this.selectResourceTypeTab('AUTHORIZATION').click();
13
+ await this.selectResourceType('AUTHORIZATION');
18
14
  }
19
15
  async clickRoleTab() {
20
- await (0, test_1.expect)(this.selectResourceTypeTab('Role')).toBeVisible();
21
- await this.selectResourceTypeTab('Role').click();
16
+ await this.selectResourceType('ROLE');
22
17
  }
23
18
  async clickComponentTab() {
24
- await (0, test_1.expect)(this.selectResourceTypeTab('COMPONENT')).toBeVisible();
25
- await this.selectResourceTypeTab('COMPONENT').click();
19
+ await this.selectResourceType('COMPONENT');
26
20
  }
27
21
  async clickResourceTab() {
28
- await (0, test_1.expect)(this.selectResourceTypeTab('RESOURCE')).toBeVisible();
29
- await this.selectResourceTypeTab('RESOURCE').click();
22
+ await this.selectResourceType('RESOURCE');
30
23
  }
31
24
  }
32
25
  exports.OCIdentityAuthorizationsPage = OCIdentityAuthorizationsPage;
@@ -13,6 +13,7 @@ export declare class OCIdentityClusterVariablesPage {
13
13
  readonly monacoEditorTextArea: Locator;
14
14
  readonly successMessage: Locator;
15
15
  readonly editMenuItem: Locator;
16
+ readonly editModalHeading: Locator;
16
17
  constructor(page: Page);
17
18
  editVariable(variableName: string, newValue: string): Promise<void>;
18
19
  assertVariableExists(variableName: string): Promise<void>;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OCIdentityClusterVariablesPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const sleep_1 = require("../../utils/sleep");
6
+ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
5
7
  class OCIdentityClusterVariablesPage {
6
8
  page;
7
9
  variablesList;
@@ -16,21 +18,36 @@ class OCIdentityClusterVariablesPage {
16
18
  monacoEditorTextArea;
17
19
  successMessage;
18
20
  editMenuItem;
21
+ editModalHeading;
19
22
  constructor(page) {
20
23
  this.page = page;
21
- this.variablesList = page.locator('table.cds--data-table');
22
- this.variableRow = (variableName) => this.variablesList
23
- .locator('tbody')
24
- .getByRole('row')
25
- .filter({ hasText: variableName });
26
- this.editVariableButton = (variableName) => this.variableRow(variableName).getByRole('button', { name: 'Options' });
24
+ // The Admin UI moved off Carbon onto the Camunda design system:
25
+ // `identity/client/src/pages/cluster-variables/index.tsx` renders ListV2
26
+ // (design-system `DataTable`, Radix dialog/menu) when the
27
+ // `isNewDesignSystemEnabled` client config is on, which SaaS clusters now
28
+ // ship with, so the Carbon `table.cds--data-table` this was scoped to no
29
+ // longer exists. The table role resolves both DOMs, and filtering the rows
30
+ // by the variable name already excludes the header row, so the removed
31
+ // tbody scoping is not needed. Mirrors
32
+ // pages/SM-8.10/OCIdentityClusterVariablesPage.ts.
33
+ this.variablesList = page.getByRole('table');
34
+ this.variableRow = (variableName) => this.variablesList.getByRole('row').filter({ hasText: variableName });
35
+ // Carbon's OverflowMenu labels its trigger "Options"; the design-system
36
+ // DataTable labels it "Row actions".
37
+ this.editVariableButton = (variableName) => this.variableRow(variableName).getByRole('button', {
38
+ name: /^(Options|Row actions)$/,
39
+ });
27
40
  this.editVariableModal = page.getByRole('dialog', {
28
41
  name: /Edit.*variable/i,
29
42
  });
30
43
  this.variableValueField = this.editVariableModal.getByRole('textbox', {
31
44
  name: 'Editor content',
32
45
  });
33
- this.saveVariableButton = this.editVariableModal.locator('.cds--modal-footer button.cds--btn--primary');
46
+ // Both modals pass `confirmLabel={t("save")}`, so the accessible name is
47
+ // stable across the two footers.
48
+ this.saveVariableButton = this.editVariableModal.getByRole('button', {
49
+ name: 'Save',
50
+ });
34
51
  this.closeEditVariableModal = this.editVariableModal.getByRole('button', {
35
52
  name: 'Close',
36
53
  });
@@ -40,9 +57,15 @@ class OCIdentityClusterVariablesPage {
40
57
  this.monacoEditor = this.editVariableModal.locator('.monaco-editor');
41
58
  this.monacoEditorTextArea = this.monacoEditor.locator('textarea.inputarea');
42
59
  this.successMessage = this.page.getByText('Cluster variable updated');
43
- this.editMenuItem = this.page
44
- .locator('button[role="menuitem"]')
45
- .filter({ hasText: /^Edit$/ });
60
+ // Carbon renders the modal title as an h3, the design-system dialog as an
61
+ // h2; the heading role covers both.
62
+ this.editModalHeading = this.editVariableModal.getByRole('heading').first();
63
+ // Carbon menu items are buttons, Radix menu items are divs — match on the
64
+ // role instead of the tag.
65
+ this.editMenuItem = this.page.getByRole('menuitem', {
66
+ name: 'Edit',
67
+ exact: true,
68
+ });
46
69
  }
47
70
  async editVariable(variableName, newValue) {
48
71
  await this.editVariableButton(variableName).click();
@@ -50,21 +73,50 @@ class OCIdentityClusterVariablesPage {
50
73
  await this.editMenuItem.click();
51
74
  await (0, test_1.expect)(this.variableValueField).toBeVisible();
52
75
  await (0, test_1.expect)(this.monacoEditor).toBeVisible();
53
- // Monaco's hidden <textarea> only forwards keyboard events to the
54
- // editor model — `fill()` writes the value but doesn't trigger
55
- // Monaco's onDidChangeContent, so the modal's JSON validator never
56
- // re-runs and Save stays disabled. Use real keyboard input instead.
57
- await this.monacoEditor.click();
58
- await this.page.keyboard.press('ControlOrMeta+A');
59
- await this.page.keyboard.press('Delete');
60
- await this.page.keyboard.type(newValue);
61
- await (0, test_1.expect)(this.saveVariableButton).toBeEnabled();
76
+ // Monaco's hidden <textarea> only forwards keyboard events to the editor
77
+ // model — `fill()` writes the value but doesn't trigger Monaco's
78
+ // onDidChangeContent, so the modal's JSON validator never re-runs and Save
79
+ // stays disabled. Use real keyboard input instead, and retry the
80
+ // clear-type-enable cycle because Monaco's auto-closing brackets/quotes can
81
+ // corrupt the JSON on the first attempt.
82
+ const MAX_TYPE_ATTEMPTS = 3;
83
+ for (let attempt = 1; attempt <= MAX_TYPE_ATTEMPTS; attempt++) {
84
+ await this.monacoEditor.click();
85
+ await this.monacoEditorTextArea.press('ControlOrMeta+A');
86
+ await this.monacoEditorTextArea.press('Delete');
87
+ // Let Monaco settle after clearing content
88
+ await (0, sleep_1.sleep)(500);
89
+ await this.monacoEditorTextArea.pressSequentially(newValue, { delay: 20 });
90
+ // Dismiss any auto-complete popup by clicking the modal heading.
91
+ // Do NOT use press('Escape') on the textarea — when no autocomplete
92
+ // is showing, the Escape event propagates to the dialog and closes it.
93
+ await this.editModalHeading.click();
94
+ // Let Monaco settle and validate the new content
95
+ await (0, sleep_1.sleep)(500);
96
+ try {
97
+ await (0, test_1.expect)(this.saveVariableButton).toBeEnabled({ timeout: 10000 });
98
+ break;
99
+ }
100
+ catch {
101
+ if (attempt === MAX_TYPE_ATTEMPTS) {
102
+ throw new Error(`Save button did not become enabled after ${MAX_TYPE_ATTEMPTS} typing attempts`);
103
+ }
104
+ }
105
+ }
62
106
  await this.saveVariableButton.click();
63
107
  await (0, test_1.expect)(this.successMessage).toBeVisible();
64
108
  await (0, test_1.expect)(this.editVariableModal).toBeHidden();
65
109
  }
66
110
  async assertVariableExists(variableName) {
67
- await (0, test_1.expect)(this.variableRow(variableName)).toBeVisible();
111
+ // ListV2 fetches the variables with react-query after the Cluster
112
+ // variables nav link is followed, so the table can still be empty when
113
+ // this runs; reload between attempts rather than racing a single wait.
114
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.variableRow(variableName), {
115
+ visibilityTimeout: 30000,
116
+ postAction: async () => {
117
+ await this.page.reload();
118
+ },
119
+ });
68
120
  }
69
121
  async assertVariableValue(variableName, expectedValue) {
70
122
  const row = this.variableRow(variableName);
@@ -10,6 +10,7 @@ declare class OCIdentityRolesPage {
10
10
  readonly rolesList: Locator;
11
11
  readonly roleCell: (name: string) => Locator;
12
12
  readonly rowValue: (name: string) => Locator;
13
+ readonly memberCell: (name: string) => Locator;
13
14
  readonly createRoleButton: Locator;
14
15
  readonly createRoleModal: Locator;
15
16
  readonly createNameField: Locator;
@@ -15,6 +15,7 @@ class OCIdentityRolesPage {
15
15
  rolesList;
16
16
  roleCell;
17
17
  rowValue;
18
+ memberCell;
18
19
  createRoleButton;
19
20
  createRoleModal;
20
21
  createNameField;
@@ -52,6 +53,18 @@ class OCIdentityRolesPage {
52
53
  });
53
54
  this.roleCell = (roleID) => this.rolesList.getByRole('cell', { name: roleID, exact: true });
54
55
  this.rowValue = (name) => page.getByRole('row').filter({ hasText: name });
56
+ // The role detail page's members table is the design-system EntityList,
57
+ // which truncates any cell longer than 50 characters: `truncateCell` in
58
+ // camunda/camunda identity/client/src/components/entityListV2/EntityList.tsx
59
+ // replaces the text with
60
+ // `<button aria-label="<full value>">{first 50 chars}…</button>`, and the
61
+ // tooltip that carries the rest is portalled out of the row. The QA
62
+ // usernames are 60-character addresses, so the row's text no longer
63
+ // contains the address at all and `hasText` cannot see it — the full value
64
+ // now only survives in the accessible name, which a `cell` derives from its
65
+ // contents. Matching on the cell name resolves the untruncated (Carbon)
66
+ // rendering just as well, so this covers both builds.
67
+ this.memberCell = (name) => this.rolesList.getByRole('cell', { name });
55
68
  this.createRoleModal = page.getByRole('dialog', {
56
69
  name: 'Create role',
57
70
  });
@@ -64,7 +77,13 @@ class OCIdentityRolesPage {
64
77
  exact: true,
65
78
  });
66
79
  this.createRoleModalCreateButton = this.createRoleModal.getByRole('button', { name: 'Create role' });
67
- this.deleteRoleButton = (rowName) => this.rolesList.getByRole('row', { name: rowName }).getByLabel('Delete');
80
+ // Carbon rendered the row's delete action as an icon-only button carrying
81
+ // an aria-label; the design-system EntityList renders the (dangerous) row
82
+ // action inline as a button with visible text instead, so there is no label
83
+ // to match. The accessible name covers both shapes.
84
+ this.deleteRoleButton = (rowName) => this.rolesList
85
+ .getByRole('row', { name: rowName })
86
+ .getByRole('button', { name: /^delete/i });
68
87
  this.deleteRoleModal = page.getByRole('dialog', { name: /delete role/i });
69
88
  this.closeDeleteRoleModal = this.deleteRoleModal.getByRole('button', {
70
89
  name: 'Close',
@@ -159,13 +178,12 @@ class OCIdentityRolesPage {
159
178
  await this.createRole({ id: role, name: role });
160
179
  }
161
180
  await this.doAssignUserToRole(role, email);
162
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.rowValue(email).first(), {
181
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.memberCell(email).first(), {
163
182
  preAction: async () => {
164
183
  await this.page.reload();
165
184
  await (0, test_1.expect)(this.rolesList.first()).toBeVisible({ timeout: 20000 });
166
185
  },
167
186
  });
168
- await (0, test_1.expect)(this.rowValue(email)).toContainText(`${email}`);
169
187
  }
170
188
  async deleteRole(name) {
171
189
  const locator = this.deleteRoleButton(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1198",
3
+ "version": "0.0.1199",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",