@camunda/e2e-test-suite 0.0.1275 → 0.0.1277

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.
@@ -28,8 +28,13 @@ class ClusterDetailsPage {
28
28
  clientCredentialsDialog;
29
29
  clustersLink;
30
30
  alertsTab;
31
+ alertsPanel;
31
32
  activeTabPanel;
32
33
  alertsList;
34
+ alertExpandRowButtons;
35
+ alertRowActionsButtons;
36
+ deleteAlertMenuItem;
37
+ deleteAlertConfirmButton;
33
38
  createFirstAlertButton;
34
39
  createNewAlertButton;
35
40
  emailRadioButton;
@@ -43,6 +48,7 @@ class ClusterDetailsPage {
43
48
  expandButton;
44
49
  rbaEnabledMessage;
45
50
  authorizationsHeading;
51
+ authorizationsSwitch;
46
52
  orchestrationClusterCheckbox;
47
53
  clientCredentialsLink;
48
54
  clientRow;
@@ -123,12 +129,39 @@ class ClusterDetailsPage {
123
129
  this.alertsList = this.activeTabPanel
124
130
  .getByRole('row')
125
131
  .filter({ hasNotText: 'Delivery method' }); //Filter out header row
132
+ // Hub's Alerts tab is a design-system data table: one summary row per
133
+ // alert, its detail fields in a sibling expansion row that only exists
134
+ // once the row's "Expand row" toggle is clicked, and Delete behind the
135
+ // row's "Row actions" kebab -> "Delete" -> "Delete alert" dialog. A
136
+ // nightly trace's DOM carries aria-label="Row actions" and no row-level
137
+ // "Delete" button at all, which is why doDelete() below saw "no items"
138
+ // and silently deleted nothing. Same shape as pages/8.9, scoped to the
139
+ // visible tab panel.
140
+ this.alertExpandRowButtons = this.activeTabPanel.getByRole('button', {
141
+ name: 'Expand row',
142
+ exact: true,
143
+ });
144
+ this.alertRowActionsButtons = this.activeTabPanel.getByRole('button', {
145
+ name: 'Row actions',
146
+ exact: true,
147
+ });
148
+ this.deleteAlertMenuItem = page.getByRole('menuitem', {
149
+ name: 'Delete',
150
+ exact: true,
151
+ });
152
+ this.deleteAlertConfirmButton = page
153
+ .getByRole('dialog', { name: 'Delete alert' })
154
+ .getByRole('button', { name: 'Delete', exact: true });
126
155
  this.createFirstAlertButton = page.getByRole('button', {
127
156
  name: 'Create an alert',
128
157
  });
129
158
  this.createNewAlertButton = page.getByRole('button', {
130
159
  name: 'Create new alert',
131
160
  });
161
+ // The Alerts panel offers "Create an alert" while empty and "Create new
162
+ // alert" once it holds one, so either proves the panel itself mounted.
163
+ // Ported from pages/8.9.
164
+ this.alertsPanel = this.createFirstAlertButton.or(this.createNewAlertButton);
132
165
  this.emailRadioButton = page.getByRole('radio', {
133
166
  name: 'Email',
134
167
  });
@@ -155,6 +188,14 @@ class ClusterDetailsPage {
155
188
  });
156
189
  this.expandButton = page.locator('button[class="cds--table-expand__button"]');
157
190
  this.rbaEnabledMessage = page.getByText('Authorizations enabled');
191
+ // Hub's design-system Settings tab renders this as
192
+ // <button role="switch" data-slot="switch" aria-label="Switch Enable
193
+ // authorizations" aria-checked="true|false">, confirmed from a live
194
+ // trace. The Carbon `.cds--toggle__*` nodes the old code indexed into do
195
+ // not exist there at all.
196
+ this.authorizationsSwitch = this.page.getByRole('switch', {
197
+ name: /Enable authorizations/i,
198
+ });
158
199
  this.authorizationsHeading = this.page.getByRole('heading', {
159
200
  name: 'Authorizations',
160
201
  });
@@ -280,9 +321,34 @@ class ClusterDetailsPage {
280
321
  },
281
322
  });
282
323
  }
324
+ // Drives the design-system Authorizations switch. Returns false when this
325
+ // shell does not render one, so the caller can fall through to the legacy
326
+ // Carbon toggle.
327
+ async setAuthorizationsViaSwitch(enabled) {
328
+ if (!(await this.authorizationsSwitch.isVisible().catch(() => false))) {
329
+ return false;
330
+ }
331
+ const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
332
+ if (isOn === enabled) {
333
+ return true;
334
+ }
335
+ await this.authorizationsSwitch.click({ timeout: 30000 });
336
+ if (await this.dialog.isVisible({ timeout: 15000 }).catch(() => false)) {
337
+ const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
338
+ if (await confirm.isVisible().catch(() => false)) {
339
+ await confirm.click({ timeout: 30000 });
340
+ }
341
+ await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
342
+ }
343
+ await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
344
+ return true;
345
+ }
283
346
  async enableAuthorizations() {
284
347
  await (0, sleep_1.sleep)(5000);
285
348
  await this.waitForAuthorizationsSection();
349
+ if (await this.setAuthorizationsViaSwitch(true)) {
350
+ return;
351
+ }
286
352
  // Locate all elements with class .cds--toggle__text
287
353
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
288
354
  // Check if toggleTextElements has at least 2 elements
@@ -316,11 +382,22 @@ class ClusterDetailsPage {
316
382
  }
317
383
  }
318
384
  else {
319
- console.log('Toggle text elements not found or less than 2.');
385
+ // Previously a console.log that returned as though the work were done.
386
+ // Under Hub neither `.cds--toggle__text` nor `.cds--toggle__switch`
387
+ // exists, so this branch was taken on every run: authorizations were
388
+ // never enabled, the cluster kept its "Authorizations disabled" badge,
389
+ // and nothing downstream noticed until a permission assertion failed
390
+ // somewhere far away.
391
+ throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
392
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
393
+ 'toggle pair.');
320
394
  }
321
395
  }
322
396
  async disableAuthorizations() {
323
397
  await this.waitForAuthorizationsSection();
398
+ if (await this.setAuthorizationsViaSwitch(false)) {
399
+ return;
400
+ }
324
401
  // Locate all elements with class .cds--toggle__text
325
402
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
326
403
  // Check if toggleTextElements has at least 2 elements
@@ -354,7 +431,11 @@ class ClusterDetailsPage {
354
431
  }
355
432
  }
356
433
  else {
357
- console.log('Toggle text elements not found or less than 2.');
434
+ // Same silent no-op as enableAuthorizations had: under Hub no Carbon
435
+ // toggle exists, so this returned as though it had disabled them.
436
+ throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
437
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
438
+ 'toggle pair.');
358
439
  }
359
440
  }
360
441
  async searchAndClickClientCredentialsLink(name) {
@@ -448,7 +529,65 @@ class ClusterDetailsPage {
448
529
  });
449
530
  }
450
531
  async deleteAlerts() {
451
- await this.doDelete(this.dialog, 'Alerts');
532
+ const hasRowActions = await this.alertRowActionsButtons
533
+ .first()
534
+ .waitFor({ state: 'visible', timeout: 10000 })
535
+ .then(() => true)
536
+ .catch(() => false);
537
+ if (!hasRowActions) {
538
+ await this.doDelete(this.dialog, 'Alerts');
539
+ return;
540
+ }
541
+ let remaining = await this.alertRowActionsButtons.count();
542
+ while (remaining > 0) {
543
+ // A re-render can detach the menu between opening it and the click, so
544
+ // open-menu-and-choose-Delete is retried as one unit.
545
+ await (0, test_1.expect)(async () => {
546
+ await this.alertRowActionsButtons.first().click({ timeout: 10000 });
547
+ await this.deleteAlertMenuItem.click({ timeout: 10000 });
548
+ await (0, test_1.expect)(this.deleteAlertConfirmButton).toBeVisible({
549
+ timeout: 10000,
550
+ });
551
+ }).toPass({ timeout: 60000 });
552
+ await this.deleteAlertConfirmButton.click({ timeout: 30000 });
553
+ await (0, test_1.expect)(this.alertRowActionsButtons).toHaveCount(remaining - 1, {
554
+ timeout: 60000,
555
+ });
556
+ remaining = await this.alertRowActionsButtons.count();
557
+ }
558
+ }
559
+ // Reveals each alert's detail row, where the design-system table keeps the
560
+ // recipient, creation date and trigger count the callers assert on. A no-op
561
+ // on the legacy table, whose detail rows are always rendered.
562
+ // Re-selects the Alerts tab when it is no longer the showing one. A
563
+ // page.reload() drops the tab selection and the cluster page comes back on
564
+ // Overview, whose Environments table supplies the very first row -- which
565
+ // is how `alertsList.nth(0)` came to read "NameAlert Flow Cluster" instead
566
+ // of "Email" (console-user-flows.spec.ts reloads and then reads the list
567
+ // straight away). `alertsList` is scoped to whichever tabpanel is visible,
568
+ // so restoring the tab is what makes those reads address the alerts table.
569
+ async ensureAlertsTabSelected() {
570
+ const selected = await this.alertsTab
571
+ .getAttribute('aria-selected')
572
+ .catch(() => null);
573
+ if (selected === 'true') {
574
+ return;
575
+ }
576
+ await this.clickAlertsTab();
577
+ }
578
+ async expandAlerts() {
579
+ await this.ensureAlertsTabSelected();
580
+ for (let i = 0; i < 10; i++) {
581
+ const toggle = this.alertExpandRowButtons.first();
582
+ if (!(await toggle.isVisible().catch(() => false))) {
583
+ return;
584
+ }
585
+ const before = await this.alertExpandRowButtons.count();
586
+ await toggle.click({ timeout: 10000 });
587
+ await (0, test_1.expect)(this.alertExpandRowButtons).toHaveCount(before - 1, {
588
+ timeout: 10000,
589
+ });
590
+ }
452
591
  }
453
592
  // The confirmation modal only closes once the DELETE request resolves. When
454
593
  // that call fails the modal stays open on an inline "We're having trouble
@@ -701,6 +840,18 @@ class ClusterDetailsPage {
701
840
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
702
841
  await this.page.reload();
703
842
  await (0, sleep_1.sleep)(20000);
843
+ // A reload drops the tab selection, so the Alerts panel is no
844
+ // longer the visible one -- `alertsList` is scoped to whichever
845
+ // tabpanel is showing, which after a reload is Overview, whose
846
+ // first non-header row is the cluster-details "Name" row. That is
847
+ // exactly what the nightly reported: `Received string:
848
+ // "NameAlert Flow Cluster"`, nine times over, while the Email alert
849
+ // sat unread on a tab nobody had re-opened. Re-select the tab, wait
850
+ // for the panel to mount, and re-expand the detail rows the
851
+ // assertions read from.
852
+ await this.alertsTab.click({ timeout: 60000 });
853
+ await (0, test_1.expect)(this.alertsPanel).toBeVisible({ timeout: 60000 });
854
+ await this.expandAlerts();
704
855
  }
705
856
  else {
706
857
  throw new Error(`Assertion failed after ${maxRetries} attempts`);
@@ -47,6 +47,7 @@ declare class ClusterPage {
47
47
  private extractClusterIdFromUrl;
48
48
  private logClusterCreationInfo;
49
49
  createCluster(name: string, region?: string, tasklistV1Api?: boolean): Promise<void>;
50
+ clickProviderOption(region: string): Promise<void>;
50
51
  selectSubRegion(region: string): Promise<void>;
51
52
  determineTasklistAPI(tasklistV1Api?: boolean): Promise<void>;
52
53
  clickClusterType(): Promise<void>;
@@ -82,28 +82,32 @@ class ClusterPage {
82
82
  });
83
83
  this.clusterVersionOption = page.getByText(`${process.env.CLUSTER_VERSION}`, { exact: true });
84
84
  this.generationSearchInput = page.getByPlaceholder('Search generations');
85
- this.g3DevClusterType = page.locator('label').filter({
86
- hasText: this.defaultClusterType,
87
- });
88
- this.londonRegion = page.locator('label').filter({
89
- hasText: 'London, Europe (europe-west2)',
90
- });
91
- this.ohioRegion = page.locator('label').filter({
85
+ // Camunda Hub's create-cluster form replaced the Carbon tiles (a <label>
86
+ // per option) and tabs with design-system choice cards and toggle groups,
87
+ // all exposed as role="radio" named by their title. Each locator below
88
+ // accepts either shape via this helper. Ported from
89
+ // pages/8.10/ClusterPage.ts (#3730), where the same change was made.
90
+ const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
91
+ this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
92
+ this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
93
+ this.ohioRegion = choice(page.locator('label').filter({
92
94
  hasText: 'Ohio, North America (us-east-2)',
93
- });
94
- this.belgiumRegion = page.locator('label').filter({
95
+ }), 'Ohio, North America (us-east-2)');
96
+ this.belgiumRegion = choice(page.locator('label').filter({
95
97
  hasText: 'Chaos, Belgium, Europe (europe-west1)',
96
- });
97
- this.finlandRegion = page.locator('label').filter({
98
+ }), 'Chaos, Belgium, Europe (europe-west1)');
99
+ this.finlandRegion = choice(page.locator('label').filter({
98
100
  hasText: 'Finland, Europe (europe-north1)',
99
- });
100
- this.alphaTab = page.getByRole('tab', { name: 'Alpha' });
101
- this.devTab = page.getByRole('tab', { name: 'Internal Dev' });
102
- this.stableTab = page.getByRole('tab', { name: 'Stable' });
103
- this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
104
- this.gcpRegion = page.getByRole('tab', {
105
- name: 'GCP',
106
- });
101
+ }), 'Finland, Europe (europe-north1)');
102
+ this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
103
+ this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
104
+ this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
105
+ this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
106
+ // The cloud-provider picker is a design-system toggle group, not tabs:
107
+ // a nightly trace's DOM has
108
+ // <button role="radio" aria-checked="true" data-slot="toggle-group-item">GCP</button>
109
+ // inside a role="radiogroup", and no role="tab" anywhere on the form.
110
+ this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
107
111
  this.clustersList = page
108
112
  .getByRole('row')
109
113
  .filter({ hasNotText: 'Generation' }); //Filter out header row
@@ -224,6 +228,15 @@ class ClusterPage {
224
228
  visibilityTimeout: 20000,
225
229
  totalTimeout: 80000,
226
230
  postAction: async () => {
231
+ // The clusters route intermittently answers with an Envoy gateway
232
+ // error ("upstream connect error or disconnect/reset before headers.
233
+ // reset reason: connection timeout"), which renders as a bare text
234
+ // document -- clicking the banner cannot recover that, so every
235
+ // remaining attempt retried against a dead page. Reload first, then
236
+ // fall back to the banner for the ordinary not-yet-navigated case.
237
+ await this.page
238
+ .reload({ waitUntil: 'domcontentloaded', timeout: 30000 })
239
+ .catch(() => { });
227
240
  await this.clickClusterBanner();
228
241
  },
229
242
  });
@@ -276,7 +289,7 @@ class ClusterPage {
276
289
  await this.clickClusterNameInput();
277
290
  await this.fillClusterNameInput(name);
278
291
  await this.clickClusterType();
279
- await this.page.getByRole('tab', { name: region }).click();
292
+ await this.clickProviderOption(region);
280
293
  await this.selectSubRegion(region);
281
294
  await this.clickClusterOption(process.env.CLUSTER_VERSION);
282
295
  await this.determineTasklistAPI(tasklistV1Api);
@@ -284,6 +297,22 @@ class ClusterPage {
284
297
  await this.logClusterCreationInfo(name);
285
298
  await this.clickClustersBreadcrumb(name);
286
299
  }
300
+ // Selects the cloud provider ("GCP"/"AWS"). Hub renders the picker as a
301
+ // toggle group of role="radio" buttons rather than tabs, and it arrives with
302
+ // GCP already selected -- so a `role=tab` click both targeted nothing and
303
+ // was unnecessary. Accept either shape and skip the click when the option is
304
+ // already the selected one.
305
+ async clickProviderOption(region) {
306
+ const providerOption = this.page
307
+ .getByRole('tab', { name: region })
308
+ .or(this.page.getByRole('radio', { name: region, exact: true }))
309
+ .first();
310
+ await (0, test_1.expect)(providerOption).toBeVisible({ timeout: 40000 });
311
+ if ((await providerOption.getAttribute('aria-checked')) === 'true') {
312
+ return;
313
+ }
314
+ await providerOption.click({ timeout: 30000 });
315
+ }
287
316
  async selectSubRegion(region) {
288
317
  if (region === 'GCP') {
289
318
  if (process.env.IS_PROD == 'true') {
@@ -8,7 +8,9 @@ declare class ConnectorMarketplacePage {
8
8
  readonly replaceResourceButton: Locator;
9
9
  readonly addToProjectButton: Locator;
10
10
  readonly saveAsCopyButton: Locator;
11
+ readonly cancelButton: Locator;
11
12
  readonly importModal: Locator;
13
+ readonly browseAllMarketplaceDetailsButton: Locator;
12
14
  constructor(page: Page);
13
15
  clickSearchForConnectorTextbox(): Promise<void>;
14
16
  fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorMarketplacePage = void 0;
4
+ const test_1 = require("@playwright/test");
4
5
  class ConnectorMarketplacePage {
5
6
  page;
6
7
  searchForConnectorTextbox;
@@ -10,10 +11,27 @@ class ConnectorMarketplacePage {
10
11
  replaceResourceButton;
11
12
  addToProjectButton;
12
13
  saveAsCopyButton;
14
+ cancelButton;
13
15
  importModal;
16
+ browseAllMarketplaceDetailsButton;
14
17
  constructor(page) {
15
18
  this.page = page;
16
- this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
19
+ // The legacy marketplace modal, or camunda-hub's "Browse all" dialog,
20
+ // which lists Marketplace connectors alongside the diagram's own elements
21
+ // and reusable assets. Same shape as pages/8.7.
22
+ this.searchForConnectorTextbox = page
23
+ .getByPlaceholder('Search for a connector')
24
+ .or(page.locator('#browse-all-search'))
25
+ .or(page.getByRole('searchbox', {
26
+ name: 'Search elements and reusable assets',
27
+ }))
28
+ .first();
29
+ // A Browse all Marketplace card opens its review step ("Add to project")
30
+ // through this button; self-hosted connectors only link to setup docs.
31
+ this.browseAllMarketplaceDetailsButton = page
32
+ .getByRole('list', { name: 'Marketplace connectors' })
33
+ .getByRole('button', { name: /^View details for .+ from Marketplace$/ })
34
+ .first();
17
35
  this.downloadToProjectButton = page
18
36
  .getByRole('button', { name: 'Download to project' })
19
37
  .first();
@@ -26,6 +44,7 @@ class ConnectorMarketplacePage {
26
44
  name: 'Add to project',
27
45
  });
28
46
  this.saveAsCopyButton = page.getByRole('button', { name: 'Save as copy' });
47
+ this.cancelButton = page.getByRole('button', { name: 'Cancel' });
29
48
  this.importModal = page.locator('[data-test="import-modal"]');
30
49
  }
31
50
  async clickSearchForConnectorTextbox() {
@@ -47,7 +66,30 @@ class ConnectorMarketplacePage {
47
66
  await this.replaceResourceButton.click({ timeout: 60000 });
48
67
  }
49
68
  async downloadConnectorToProject() {
50
- await this.clickDownloadToProjectButton();
69
+ // Browse all has no "Download to project": a result is a card that opens
70
+ // its own review step, which is where the import action lives. Wait for
71
+ // whichever entry point this shell renders before deciding -- isVisible()
72
+ // never waits, so probing it straight away raced the dialog's own render
73
+ // and fell through both branches.
74
+ await (0, test_1.expect)(this.downloadToProjectButton
75
+ .or(this.browseAllMarketplaceDetailsButton)
76
+ .first()).toBeVisible({ timeout: 60000 });
77
+ if (await this.downloadToProjectButton.isVisible()) {
78
+ await this.clickDownloadToProjectButton();
79
+ }
80
+ else {
81
+ await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
82
+ }
83
+ // The Browse all review step fetches the connector's details before it
84
+ // renders "Add to project". Let one of the outcomes render before the
85
+ // probes below, which would otherwise repeat that same race.
86
+ await this.addToProjectButton
87
+ .or(this.replaceResourceButton)
88
+ .or(this.saveAsCopyButton)
89
+ .or(this.cancelButton)
90
+ .first()
91
+ .waitFor({ state: 'visible', timeout: 60000 })
92
+ .catch(() => { });
51
93
  // "Download to project" opens Web Modeler's "Import resource" modal, which
52
94
  // can take ~20s to appear. The import-confirming button depends on whether
53
95
  // the connector template already exists in the project: an existing one
@@ -43,20 +43,44 @@ class ConsoleOrganizationPage {
43
43
  constructor(page) {
44
44
  this.page = page;
45
45
  this.usersTab = page.getByRole('tab', { name: 'Users' });
46
- this.optionsButton = page.getByRole('button', { name: 'Options' });
47
- this.editUserMenuItem = page.getByRole('menuitem', { name: 'Edit User' });
48
- this.modelerCheckbox = page
49
- .getByRole('group')
50
- .locator('label')
51
- .filter({ hasText: 'Modeler' });
52
- this.adminCheckbox = page
53
- .getByRole('group')
54
- .locator('label')
55
- .filter({ hasText: 'Admin' });
56
- this.analystCheckbox = page
57
- .getByRole('group')
58
- .locator('label')
59
- .filter({ hasText: 'Analyst' });
46
+ // Hub's org-management Users table is a design-system data table whose
47
+ // per-row action affordance is a kebab named "Row actions", not Carbon's
48
+ // "Options" -- a nightly screenshot shows the kebab and the trace has no
49
+ // "Options" node at all. Scoped to the visible tab panel so another tab's
50
+ // rows cannot match. Same shape as pages/8.9.
51
+ this.optionsButton = page
52
+ .getByRole('button', { name: 'Options' })
53
+ .or(page.getByRole('tabpanel').getByRole('button', { name: 'Row actions' }))
54
+ .first();
55
+ this.editUserMenuItem = page.getByRole('menuitem', {
56
+ name: /^Edit user$/i,
57
+ });
58
+ // The "Edit user" dialog's role toggles are plain Carbon Checkboxes
59
+ // (@camunda/ccma-saas-frontend c4-user-details-page.tsx), each its own
60
+ // labelText-bound <input type="checkbox">, not a role="group" of
61
+ // <label>s -- there is no group wrapper around them at all. Scoped to
62
+ // whichever dialog is open rather than one named "Edit user": a modal
63
+ // whose heading isn't wired via aria-labelledby carries no accessible
64
+ // name at all, which silently zeroes out a name-matched dialog locator.
65
+ const openDialog = page.getByRole('dialog');
66
+ // Hub renamed the platform roles, and the "Edit user" dialog states each
67
+ // mapping in the role's own description (confirmed live against the
68
+ // nightly org): "Member ... Formerly the Modeler role.",
69
+ // "Organization Admin ... Formerly the Admin role.", "DevOps ... Formerly
70
+ // the Operations Engineer role."; Analyst is unchanged. There is no
71
+ // "Modeler" checkbox at all any more, which is why the exact-name lookup
72
+ // resolved to nothing.
73
+ //
74
+ // Anchored regex rather than `exact`: each checkbox's accessible name
75
+ // carries that description after the label, so an exact match on the bare
76
+ // role name would miss it.
77
+ const roleCheckbox = (current, legacy) => openDialog
78
+ .getByRole('checkbox', { name: current })
79
+ .or(openDialog.getByRole('checkbox', { name: legacy, exact: true }))
80
+ .first();
81
+ this.modelerCheckbox = roleCheckbox(/^Member\b/, 'Modeler');
82
+ this.adminCheckbox = roleCheckbox(/^Organization Admin\b/, 'Admin');
83
+ this.analystCheckbox = roleCheckbox(/^Analyst\b/, 'Analyst');
60
84
  this.confirmButton = page.getByRole('button', { name: 'Confirm' });
61
85
  this.settingsTab = page.getByRole('tab', { name: 'Settings' });
62
86
  this.optInButton = page.getByRole('button', { name: 'Opt-in', exact: true });
@@ -118,9 +142,16 @@ class ConsoleOrganizationPage {
118
142
  name: 'danger',
119
143
  })
120
144
  .last();
121
- this.filterTableSearchbox = page.getByRole('searchbox', {
122
- name: 'Filter table',
123
- });
145
+ // Hub's org-management "Users" tab renders its table filter as a
146
+ // design-system <input type="search" placeholder="Search"
147
+ // aria-label="Search">, not Carbon's "Filter table" -- confirmed from a
148
+ // nightly trace's DOM snapshot, where "Filter table" occurs only inside
149
+ // our own call log. Keep the legacy name as a fallback. Same shape as
150
+ // pages/8.9.
151
+ this.filterTableSearchbox = page
152
+ .getByRole('searchbox', { name: 'Search', exact: true })
153
+ .or(page.getByRole('searchbox', { name: 'Filter table' }))
154
+ .first();
124
155
  }
125
156
  async filterTable(names) {
126
157
  for (const name of names) {
@@ -189,7 +220,21 @@ class ConsoleOrganizationPage {
189
220
  await this.optionsButton.click({ timeout: 60000 });
190
221
  }
191
222
  async clickEditUserMenuItem() {
192
- await this.editUserMenuItem.click({ timeout: 60000 });
223
+ // Hub's row menu is a Radix dropdown that animates in and re-mounts its
224
+ // items while opening. A live run against the nightly org resolved this
225
+ // item correctly -- <div role="menuitem" data-slot="dropdown-menu-item">
226
+ // Edit user</div> -- and still failed the click: "element is not stable"
227
+ // twice, then "element was detached from the DOM, retrying" until the 60s
228
+ // budget ran out. A detached item means the menu itself went away, so
229
+ // clicking harder cannot help; re-open it and retry the pair as one unit,
230
+ // gated on the dialog actually opening.
231
+ await (0, test_1.expect)(async () => {
232
+ if (!(await this.editUserMenuItem.isVisible().catch(() => false))) {
233
+ await this.clickOptionsButton();
234
+ }
235
+ await this.editUserMenuItem.click({ timeout: 10000 });
236
+ await (0, test_1.expect)(this.dialog.first()).toBeVisible({ timeout: 10000 });
237
+ }).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
193
238
  }
194
239
  async checkModelerCheckbox() {
195
240
  if (!(await this.modelerCheckbox.isChecked({ timeout: 60000 }))) {
@@ -3,6 +3,9 @@ declare class FormJsPage {
3
3
  private page;
4
4
  private processApplicationPage;
5
5
  readonly aiFormGeneratorButton: Locator;
6
+ readonly copilotCtaButton: Locator;
7
+ readonly copilotChatInput: Locator;
8
+ readonly copilotSendButton: Locator;
6
9
  readonly formRequestInput: Locator;
7
10
  readonly generateFormButton: Locator;
8
11
  readonly formEditor: Locator;
@@ -22,6 +25,7 @@ declare class FormJsPage {
22
25
  readonly deployErrorMessage: Locator;
23
26
  constructor(page: Page);
24
27
  generateAIForm(request?: string): Promise<void>;
28
+ private generateFormWithCopilot;
25
29
  clickAIFormGeneratorButton(): Promise<void>;
26
30
  clickFormRequestInput(): Promise<void>;
27
31
  fillFormRequestInput(formRequest: string): Promise<void>;
@@ -11,6 +11,9 @@ class FormJsPage {
11
11
  page;
12
12
  processApplicationPage;
13
13
  aiFormGeneratorButton;
14
+ copilotCtaButton;
15
+ copilotChatInput;
16
+ copilotSendButton;
14
17
  formRequestInput;
15
18
  generateFormButton;
16
19
  formEditor;
@@ -34,6 +37,16 @@ class FormJsPage {
34
37
  this.aiFormGeneratorButton = page.getByRole('button', {
35
38
  name: 'AI Form Generator',
36
39
  });
40
+ // camunda-hub #21398 retired the form page's AI Form Generator in favour
41
+ // of the Camunda Copilot sidecar: with One Copilot on, the empty form
42
+ // offers "Try Camunda Copilot" instead (FormDs.tsx / CopilotCTA.tsx), and
43
+ // the sidecar's chat input and send button come from
44
+ // camunda-copilot-client's ChatInput. Same shape as pages/8.7.
45
+ this.copilotCtaButton = page.getByRole('button', {
46
+ name: 'Try Camunda Copilot',
47
+ });
48
+ this.copilotChatInput = page.getByPlaceholder('Type a message...');
49
+ this.copilotSendButton = page.getByRole('button', { name: 'Send message' });
37
50
  this.formRequestInput = page.getByPlaceholder('A form for an american standard loan request');
38
51
  this.generateFormButton = page.getByRole('button', { name: 'Generate form' });
39
52
  this.formEditor = page.getByLabel('Form Definition');
@@ -89,9 +102,13 @@ class FormJsPage {
89
102
  this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
90
103
  }
91
104
  async generateAIForm(request = 'Create a form with the following fields: 1. A text field with the label "Full Name" 2. A number with the label "Count" 3. A date input with the label "Date of birth"4. A Checkbox with the label "Agree"') {
92
- await (0, test_1.expect)(this.aiFormGeneratorButton).toBeVisible({
105
+ await (0, test_1.expect)(this.aiFormGeneratorButton.or(this.copilotCtaButton).first()).toBeVisible({
93
106
  timeout: 90000,
94
107
  });
108
+ if (!(await this.aiFormGeneratorButton.isVisible())) {
109
+ await this.generateFormWithCopilot(request);
110
+ return;
111
+ }
95
112
  await this.clickAIFormGeneratorButton();
96
113
  await this.clickFormRequestInput();
97
114
  await this.fillFormRequestInput(request);
@@ -100,6 +117,19 @@ class FormJsPage {
100
117
  await (0, test_1.expect)(this.page.getByText('Successfully generated smart form.')).toBeVisible({ timeout: 300000 });
101
118
  await (0, sleep_1.sleep)(60000);
102
119
  }
120
+ async generateFormWithCopilot(request) {
121
+ await this.copilotCtaButton.click({ timeout: 30000 });
122
+ await (0, test_1.expect)(this.copilotChatInput).toBeVisible({ timeout: 60000 });
123
+ await this.copilotChatInput.fill(request);
124
+ await this.copilotSendButton.click({ timeout: 30000 });
125
+ // Copilot edits the open form in place; the generated form is ready once
126
+ // every requested field shows up on the canvas.
127
+ for (const label of ['Full Name', 'Count', 'Date of birth', 'Agree']) {
128
+ await (0, test_1.expect)(this.formEditor.getByText(label).first()).toBeVisible({
129
+ timeout: 300000,
130
+ });
131
+ }
132
+ }
103
133
  async clickAIFormGeneratorButton() {
104
134
  await this.aiFormGeneratorButton.click();
105
135
  }
@@ -71,6 +71,7 @@ declare class ModelerCreatePage {
71
71
  readonly publicAccessToggle: Locator;
72
72
  readonly publicationTab: Locator;
73
73
  readonly copyPublicLinkButton: Locator;
74
+ readonly publicLinkCodeBlock: Locator;
74
75
  readonly closeButton: Locator;
75
76
  readonly rbaEnabledDeployedDialog: Locator;
76
77
  readonly viewProcessLink: Locator;
@@ -214,9 +215,11 @@ declare class ModelerCreatePage {
214
215
  fillJobTypeInput(name: string): Promise<void>;
215
216
  private suppressTestModeAnnouncement;
216
217
  switchToPlay(): Promise<void>;
218
+ private selectPlayDeploySingleResource;
217
219
  completePlayConfiguration(clusterName: string): Promise<void>;
218
220
  embedForm(formName: string): Promise<void>;
219
221
  fillStartEventVariablesForDocHandling(zeebeUrl: string, zeebeClientId: string, zeebeClientSecret: string): Promise<void>;
222
+ private openOutputMappingEntry;
220
223
  fillZeebeClientId(clientId: string): Promise<void>;
221
224
  fillZeebeSecretValue(zeebeSecret: string): Promise<void>;
222
225
  fillZeebeRestValue(zeebeUrl: string): Promise<void>;