@camunda/e2e-test-suite 0.0.1252 → 0.0.1253

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.
@@ -20,6 +20,7 @@ declare class AppsPage {
20
20
  clickCluster(component: Locator, name: string): Promise<void>;
21
21
  private expandHubClusterGroup;
22
22
  private clickHubClusterApp;
23
+ private hubAppLink;
23
24
  isHubClusterAppPresent(clusterName: string, appName: string): Promise<boolean>;
24
25
  clickModeler(): Promise<void>;
25
26
  clickTasklist(clusterName: string): Promise<void>;
@@ -149,12 +149,31 @@ class AppsPage {
149
149
  // additional content from the design-system sidebar item, e.g. an icon
150
150
  // or badge). The five app names are distinct enough that a substring
151
151
  // match carries no ambiguity risk.
152
- const appLink = this.hubSidebar.getByRole('link', {
153
- name: appName,
154
- });
152
+ const appLink = this.hubAppLink(clusterName, appName);
155
153
  await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
154
+ // With Hub environments enabled the sidebar lists environments, whose
155
+ // app links open in a new tab ("Operate in <env> (opens in a new tab)").
156
+ // Callers assert the app on this page, so follow the link here instead.
157
+ const href = await appLink.getAttribute('href');
158
+ if (href && (await appLink.getAttribute('target')) === '_blank') {
159
+ await this.page.goto(href, { waitUntil: 'domcontentloaded' });
160
+ return;
161
+ }
156
162
  await appLink.click({ timeout: 10000 });
157
163
  }
164
+ // Inline (expanded rail) the app link carries the app's name. The rail can
165
+ // also start collapsed (camunda-hub seeds SidebarProvider's defaultExpanded
166
+ // from whether its items had loaded at first render); the group toggle then
167
+ // opens a flyout portalled outside "Main navigation", whose links -- like
168
+ // environment rows' -- are named "<app> in <cluster>".
169
+ hubAppLink(clusterName, appName) {
170
+ return this.hubSidebar
171
+ .getByRole('link', { name: appName })
172
+ .or(this.page.getByRole('link', {
173
+ name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
174
+ }))
175
+ .first();
176
+ }
158
177
  // Checks whether an app link is present inside the requested cluster's Hub
159
178
  // sidebar group, without clicking it. Unlike clickHubClusterApp, absence is
160
179
  // an expected, valid outcome here -- this is how callers assert per-cluster
@@ -163,7 +182,7 @@ class AppsPage {
163
182
  // rather than a disabled control -- so this never throws on a miss.
164
183
  async isHubClusterAppPresent(clusterName, appName) {
165
184
  await this.expandHubClusterGroup(clusterName);
166
- const appLink = this.hubSidebar.getByRole('link', { name: appName });
185
+ const appLink = this.hubAppLink(clusterName, appName);
167
186
  return appLink.isVisible({ timeout: 10000 }).catch(() => false);
168
187
  }
169
188
  async clickModeler() {
@@ -78,9 +78,15 @@ class ClusterDetailsPage {
78
78
  .locator('label')
79
79
  .filter({ hasText: /^Secrets$/ });
80
80
  this.createButton = page.getByRole('button', { name: 'Create', exact: true });
81
+ // The Hub shell's dialog names its close button through visually hidden
82
+ // text rather than an aria-label, which getByLabel does not see.
81
83
  this.closeModalButton = page
82
84
  .getByRole('dialog', { name: 'Client credentials', exact: true })
83
- .getByLabel('Close');
85
+ .getByRole('button', { name: 'Close', exact: true })
86
+ .or(page
87
+ .getByRole('dialog', { name: 'Client credentials', exact: true })
88
+ .getByLabel('Close'))
89
+ .first();
84
90
  this.createClientButton = page.getByRole('button', {
85
91
  name: 'Create new Client',
86
92
  });
@@ -446,7 +452,13 @@ class ClusterDetailsPage {
446
452
  timeout: 20000,
447
453
  });
448
454
  await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible();
449
- await (0, test_1.expect)(this.clientRow(name)).toContainText('Zeebe, Tasklist, Operate, Optimize, and Secrets');
455
+ // The Hub shell's credentials dialog is modal and marks the page behind it
456
+ // aria-hidden, so the new client's row is out of the accessibility tree
457
+ // until it closes -- include hidden rows to check its scopes meanwhile.
458
+ await (0, test_1.expect)(this.page
459
+ .getByRole('row', { includeHidden: true })
460
+ .filter({ hasNotText: 'Scopes' })
461
+ .filter({ hasText: name })).toContainText('Zeebe, Tasklist, Operate, Optimize, and Secrets');
450
462
  }
451
463
  async clickEnvVarsButton() {
452
464
  await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
@@ -144,6 +144,14 @@ class ClusterPage {
144
144
  }
145
145
  }
146
146
  async deleteCluster(name) {
147
+ // A full reload in the Hub shell bounces through /login, the identity
148
+ // provider and /login-callback (~35s) before the SPA pushes its returnUrl
149
+ // back to /clusters. Counting rows mid-bounce throws "Execution context
150
+ // was destroyed", so wait for the clusters page to have settled first.
151
+ await this.page.waitForURL((url) => /\/clusters\/?$/.test(url.pathname), {
152
+ timeout: 120000,
153
+ });
154
+ await (0, test_1.expect)(this.createNewClusterButton).toBeVisible({ timeout: 60000 });
147
155
  const clusterInstance = this.cluster(name).first();
148
156
  if ((await clusterInstance.count()) <= 0) {
149
157
  console.log(`No clusters found with name ${name}, skipping deletion.`);
@@ -8,6 +8,7 @@ declare class ConnectorMarketplacePage {
8
8
  readonly addToProjectButton: Locator;
9
9
  readonly cancelButton: Locator;
10
10
  readonly closeButton: Locator;
11
+ readonly browseAllMarketplaceDetailsButton: Locator;
11
12
  constructor(page: Page);
12
13
  clickSearchForConnectorTextbox(): Promise<void>;
13
14
  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,9 +11,22 @@ class ConnectorMarketplacePage {
10
11
  addToProjectButton;
11
12
  cancelButton;
12
13
  closeButton;
14
+ browseAllMarketplaceDetailsButton;
13
15
  constructor(page) {
14
16
  this.page = page;
15
- this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
17
+ // The legacy marketplace modal, or camunda-hub's "Browse all" dialog
18
+ // (2026-09-23), which lists Marketplace connectors alongside the
19
+ // diagram's own elements and reusable assets.
20
+ this.searchForConnectorTextbox = page
21
+ .getByPlaceholder('Search for a connector')
22
+ .or(page.locator('#browse-all-search'))
23
+ .first();
24
+ // A Browse all Marketplace card opens its review step ("Add to project")
25
+ // through this button; self-hosted connectors only link to setup docs.
26
+ this.browseAllMarketplaceDetailsButton = page
27
+ .getByRole('list', { name: 'Marketplace connectors' })
28
+ .getByRole('button', { name: /^View details for .+ from Marketplace$/ })
29
+ .first();
16
30
  this.downloadToProjectButton = page
17
31
  .getByRole('button', { name: 'Download to project' })
18
32
  .first();
@@ -42,7 +56,15 @@ class ConnectorMarketplacePage {
42
56
  await this.replaceResourceButton.click({ timeout: 60000 });
43
57
  }
44
58
  async downloadConnectorToProject() {
45
- await this.clickDownloadToProjectButton();
59
+ await (0, test_1.expect)(this.downloadToProjectButton
60
+ .or(this.browseAllMarketplaceDetailsButton)
61
+ .first()).toBeVisible({ timeout: 60000 });
62
+ if (await this.downloadToProjectButton.isVisible()) {
63
+ await this.clickDownloadToProjectButton();
64
+ }
65
+ else {
66
+ await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
67
+ }
46
68
  let imported = false;
47
69
  // A fresh download shows "Add to project"; an already-present resource
48
70
  // shows "Replace resource". Pick whichever the ImportModal rendered —
@@ -515,6 +515,17 @@ class ConsoleOrganizationPage {
515
515
  // to actually drop before targeting the next row: clicking a delete button
516
516
  // while its row is being removed detaches the element from the DOM and the
517
517
  // click never lands (retries against a continuously re-rendering table).
518
+ //
519
+ // The table fetches its rows after the tab renders, so sampling the count
520
+ // straight away read 0 while the previous run's groups were still loading
521
+ // -- they were left in place and re-creating them failed "name:duplicate".
522
+ // Give existing rows the chance to render before counting.
523
+ await (0, test_1.expect)(this.createGroupButton).toBeVisible({ timeout: 60000 });
524
+ await this.deleteButton
525
+ .first()
526
+ .waitFor({ state: 'visible', timeout: 20000 })
527
+ .then(() => true)
528
+ .catch(() => false);
518
529
  let remaining = await this.deleteButton.count();
519
530
  while (remaining > 0) {
520
531
  await this.deleteButton.first().click({ timeout: 60000 });
@@ -4,6 +4,7 @@ exports.FormJsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
+ const modelerCluster_1 = require("../../utils/modelerCluster");
7
8
  class FormJsPage {
8
9
  page;
9
10
  processApplicationPage;
@@ -50,15 +51,16 @@ class FormJsPage {
50
51
  // Inside a process application hub titles this modal for the application
51
52
  // rather than for the form, so 'Deploy form' never matches and the dialog
52
53
  // reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
53
- this.dialog = page
54
- .locator('.cds--modal-container[role="dialog"]')
55
- .filter({ hasText: 'Deploy' });
54
+ // Role-based rather than Carbon's modal container: under the Hub shell the
55
+ // deploy dialog is a design-system dialog with no .cds--modal-container.
56
+ this.dialog = page.getByRole('dialog').filter({ hasText: 'Deploy' });
56
57
  this.deploySubButton = this.dialog.getByRole('button', {
57
58
  name: 'Deploy',
58
59
  exact: true,
59
60
  });
61
+ // Carbon toast, or the design system's (sonner) Toaster under the Hub shell.
60
62
  this.deploySuccessNotification = page
61
- .locator('.cds--toast-notification--success')
63
+ .locator('.cds--toast-notification--success, [data-sonner-toast][data-type="success"]')
62
64
  .filter({ hasText: 'Form deployed!' });
63
65
  }
64
66
  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"') {
@@ -119,6 +121,21 @@ class FormJsPage {
119
121
  throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
120
122
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
121
123
  }
124
+ // Hub's environment-based dialog also renders a <label> per row, which
125
+ // the legacy toggle detection below would mistake for its own.
126
+ const environmentRow = this.dialog
127
+ .locator('[data-test^="environment-row-"]')
128
+ .first();
129
+ if (await environmentRow.isVisible().catch(() => false)) {
130
+ try {
131
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
132
+ }
133
+ catch (error) {
134
+ this.processApplicationPage.forgetStageCluster();
135
+ throw error;
136
+ }
137
+ return;
138
+ }
122
139
  const legacyClusterToggle = this.dialog.locator('label').filter({
123
140
  hasText: new RegExp(`^${clusterName}`),
124
141
  });
@@ -143,6 +160,15 @@ class FormJsPage {
143
160
  throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
144
161
  'binding did not take. It will be rebound on the next attempt.');
145
162
  }
163
+ // Hub's environment-based dialog lists every Environment assigned to
164
+ // the workspace and preselects the first, which need not be this one.
165
+ const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
166
+ if (await environmentRows
167
+ .first()
168
+ .isVisible()
169
+ .catch(() => false)) {
170
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
171
+ }
146
172
  return;
147
173
  }
148
174
  const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
7
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
8
+ const modelerCluster_1 = require("../../utils/modelerCluster");
8
9
  class ModelerCreatePage {
9
10
  page;
10
11
  processApplicationPage;
@@ -212,7 +213,14 @@ class ModelerCreatePage {
212
213
  this.firstPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
213
214
  this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
214
215
  this.payloadInput = page.locator('[class="fjs-input"]');
215
- this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
216
+ // Since camunda-hub's "Browse all" (2026-09-23) a change-element menu with
217
+ // enough entries swaps its header's marketplace icon for a "Browse all"
218
+ // entry, whose dialog carries the Marketplace search and import itself
219
+ // (see ConnectorMarketplacePage).
220
+ this.marketPlaceButton = page
221
+ .getByTitle('Browse Marketplace for more Connectors')
222
+ .or(page.getByTitle('Browse all available elements and assets'))
223
+ .first();
216
224
  this.publicHolidayConnectorOption = page
217
225
  .locator('[data-test="modeler"]')
218
226
  .getByText('Worldwide Public Holiday');
@@ -1638,6 +1646,21 @@ class ModelerCreatePage {
1638
1646
  throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
1639
1647
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
1640
1648
  }
1649
+ // Hub's environment-based dialog also renders a <label> per row, which
1650
+ // the legacy toggle detection below would mistake for its own.
1651
+ const environmentRow = this.dialog
1652
+ .locator('[data-test^="environment-row-"]')
1653
+ .first();
1654
+ if (await environmentRow.isVisible().catch(() => false)) {
1655
+ try {
1656
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1657
+ }
1658
+ catch (error) {
1659
+ this.processApplicationPage.forgetStageCluster();
1660
+ throw error;
1661
+ }
1662
+ return;
1663
+ }
1641
1664
  const legacyClusterToggle = this.dialog.locator('label').filter({
1642
1665
  hasText: new RegExp(`^${clusterName}`),
1643
1666
  });
@@ -1662,6 +1685,15 @@ class ModelerCreatePage {
1662
1685
  throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
1663
1686
  'binding did not take. It will be rebound on the next attempt.');
1664
1687
  }
1688
+ // Hub's environment-based dialog lists every Environment assigned to
1689
+ // the workspace and preselects the first, which need not be this one.
1690
+ const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
1691
+ if (await environmentRows
1692
+ .first()
1693
+ .isVisible()
1694
+ .catch(() => false)) {
1695
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1696
+ }
1665
1697
  return;
1666
1698
  }
1667
1699
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
@@ -26,6 +26,7 @@ declare class ModelerHomePage {
26
26
  readonly projectBreadcrumb: Locator;
27
27
  readonly workspaceBreadcrumb: Locator;
28
28
  readonly openOrganizationsButton: Locator;
29
+ readonly hubOrganizationLink: Locator;
29
30
  readonly optionsButton: Locator;
30
31
  readonly manageButton: Locator;
31
32
  readonly crossComponentProjectFolder: Locator;
@@ -71,6 +72,7 @@ declare class ModelerHomePage {
71
72
  private becameVisible;
72
73
  isModelerPageBannerVisible(timeout: number): Promise<boolean>;
73
74
  clickHomeBreadcrumb(): Promise<void>;
75
+ openOrganizationManagement(timeout: number): Promise<void>;
74
76
  clickOpenOrganizationsButton(): Promise<void>;
75
77
  clickCrossComponentProjectFolder(): Promise<void>;
76
78
  private createWorkspaceViaWizard;
@@ -47,6 +47,7 @@ class ModelerHomePage {
47
47
  projectBreadcrumb;
48
48
  workspaceBreadcrumb;
49
49
  openOrganizationsButton;
50
+ hubOrganizationLink;
50
51
  optionsButton;
51
52
  manageButton;
52
53
  crossComponentProjectFolder;
@@ -143,6 +144,10 @@ class ModelerHomePage {
143
144
  .first();
144
145
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
145
146
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
147
+ this.hubOrganizationLink = page
148
+ .getByRole('navigation', { name: 'Main navigation' })
149
+ .getByRole('link', { name: 'Organization' })
150
+ .first();
146
151
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
147
152
  // On the Camunda Hub Workspaces overview the folder is a workspace tile
148
153
  // whose "Open workspace <name>" button navigates into it.
@@ -561,6 +566,19 @@ class ModelerHomePage {
561
566
  // An announcement dialog covering the Modeler header makes every click on the
562
567
  // org button bounce off the modal layer ("intercepts pointer events") until
563
568
  // the timeout, so dismiss overlays before each attempt.
569
+ // Opens organization management. The Camunda Hub shell has no "Open
570
+ // Organizations" menu -- its org breadcrumb only switches organizations --
571
+ // and reaches organization management from the sidebar's "Organization"
572
+ // link instead.
573
+ async openOrganizationManagement(timeout) {
574
+ await (0, test_1.expect)(this.openOrganizationsButton.or(this.hubOrganizationLink).first()).toBeVisible({ timeout });
575
+ if (await this.hubOrganizationLink.isVisible()) {
576
+ await this.hubOrganizationLink.click({ timeout: 30000 });
577
+ return;
578
+ }
579
+ await this.clickOpenOrganizationsButton();
580
+ await this.clickManageButton();
581
+ }
564
582
  async clickOpenOrganizationsButton() {
565
583
  await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
566
584
  preAction: async () => {
@@ -24,6 +24,7 @@ declare class ProcessApplicationPage {
24
24
  rememberResourceContainer(url: string): void;
25
25
  forgetStageCluster(): void;
26
26
  ensureStageCluster(clusterName: string): Promise<boolean>;
27
+ private ensureWorkspaceEnvironment;
27
28
  private openStageDefinition;
28
29
  openByName(name: string): Promise<void>;
29
30
  }
@@ -237,7 +237,19 @@ class ProcessApplicationPage {
237
237
  // suite is. Re-navigating is the recovery; waiting longer on a page that
238
238
  // has already gone wrong is not, which is why the inner timeouts stay
239
239
  // short. A genuinely unbindable cluster still fails, with the same error.
240
+ //
241
+ // Under Hub workspaces (/workspaces/<ws>/projects/<id>) there is no
242
+ // /connected-clusters route -- it renders "No match for the current
243
+ // location". With Hub's Environments feature on (camunda-hub#29210),
244
+ // WorkspaceProjectDetail hides the Connected clusters card altogether and
245
+ // the deploy dialog offers the environments assigned to the parent
246
+ // workspace instead, so the binding is made there.
247
+ const workspaceId = paUrl.match(/\/workspaces\/([^/]+)\/projects\//)?.[1];
240
248
  await (0, test_1.expect)(async () => {
249
+ if (workspaceId) {
250
+ await this.ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName);
251
+ return;
252
+ }
241
253
  await this.page.goto(`${paUrl}/connected-clusters`, {
242
254
  waitUntil: 'domcontentloaded',
243
255
  });
@@ -248,6 +260,81 @@ class ProcessApplicationPage {
248
260
  await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
249
261
  return true;
250
262
  }
263
+ // Assigns the Environment backed by clusterName to the workspace, from
264
+ // workspace management's Environments tab (camunda-hub
265
+ // WorkspaceEnvironmentsTab). Without the Environments feature the project
266
+ // page still carries the Connected clusters card, whose "Configure" button
267
+ // opens the Define stages dialog -- that path is taken instead.
268
+ //
269
+ // Save replaces the workspace's whole assignment with the picker's
270
+ // selection, and parallel workers share this workspace, so only this
271
+ // cluster's toggle is touched and the result re-read afterwards. A
272
+ // concurrent save that drops it anyway surfaces as a deploy dialog without
273
+ // the cluster, which forgets the binding and comes back here (see
274
+ // forgetStageCluster).
275
+ async ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName) {
276
+ await this.page.goto(paUrl, { waitUntil: 'domcontentloaded' });
277
+ await this.waitForProcessApplication();
278
+ const configure = this.page.getByRole('button', {
279
+ name: 'Configure',
280
+ exact: true,
281
+ });
282
+ if (await configure.isVisible().catch(() => false)) {
283
+ await configure.click({ timeout: 30000 });
284
+ await this.defineDevelopmentStage(clusterName);
285
+ return;
286
+ }
287
+ const manageUrl = `${new URL(paUrl).origin}/workspaces/${workspaceId}/manage?tab=environments`;
288
+ await this.page.goto(manageUrl, { waitUntil: 'domcontentloaded' });
289
+ const heading = this.page.locator('[data-test="manage-environments-heading"]');
290
+ if (!(await heading
291
+ .waitFor({ state: 'visible', timeout: 60000 })
292
+ .then(() => true)
293
+ .catch(() => false))) {
294
+ throw new Error(`Could not bind "${clusterName}" as a deploy target: the project page ` +
295
+ 'has no Connected clusters "Configure" control, and the workspace ' +
296
+ `Environments tab did not render at ${manageUrl}.`);
297
+ }
298
+ const editButton = this.page.locator('[data-test="edit-environments-button"]');
299
+ await (0, test_1.expect)(editButton).toBeEnabled({ timeout: 60000 });
300
+ await editButton.click({ timeout: 30000 });
301
+ const editor = this.page.getByRole('group', {
302
+ name: 'Edit assigned environments',
303
+ });
304
+ await (0, test_1.expect)(editor).toBeVisible({ timeout: 30000 });
305
+ const search = editor.locator('[data-test="picker-search"]');
306
+ if (await search.isVisible().catch(() => false)) {
307
+ await search.fill(clusterName);
308
+ }
309
+ // Picker card toggles are named "Select <environment> (<cluster>)" and
310
+ // flip to "Selected ..." with aria-pressed once picked.
311
+ const escapedClusterName = clusterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
312
+ const toggle = editor
313
+ .locator('[data-test="picker-environment"]')
314
+ .getByRole('button', {
315
+ name: new RegExp(`^Select(ed)? .*\\(${escapedClusterName}\\)$`),
316
+ })
317
+ .first();
318
+ await (0, test_1.expect)(toggle).toBeVisible({ timeout: 60000 });
319
+ if ((await toggle.getAttribute('aria-pressed')) === 'true') {
320
+ await this.page
321
+ .getByRole('button', { name: 'Cancel', exact: true })
322
+ .click({ timeout: 30000 });
323
+ await (0, test_1.expect)(editor).toBeHidden({ timeout: 30000 });
324
+ return;
325
+ }
326
+ await toggle.click({ timeout: 30000 });
327
+ await (0, test_1.expect)(toggle).toHaveAttribute('aria-pressed', 'true');
328
+ await this.page
329
+ .getByRole('button', { name: 'Save', exact: true })
330
+ .click({ timeout: 30000 });
331
+ await (0, test_1.expect)(editor).toBeHidden({ timeout: 60000 });
332
+ await (0, test_1.expect)(this.page
333
+ .getByRole('list', { name: 'Assigned environments' })
334
+ .getByRole('listitem')
335
+ .filter({ hasText: clusterName })
336
+ .first()).toBeVisible({ timeout: 60000 });
337
+ }
251
338
  // The /connected-clusters deep link opens the Define stages dialog by itself
252
339
  // while the application has no stage bound. Once one is bound -- a test that
253
340
  // deploys to a second cluster -- it has to be reopened from the page, so try
@@ -556,7 +556,13 @@ async function modelAndRunConnectorsDocHandlingDiagram(modelerCreatePage, modele
556
556
  await modelerCreatePage.clickMarketPlaceButton();
557
557
  await connectorMarketplacePage.clickSearchForConnectorTextbox();
558
558
  await connectorMarketplacePage.fillSearchForConnectorTextbox('REST Connector');
559
- await (0, test_1.expect)(page.getByText('REST ConnectorBy CamundaConnect, interact, and synchronize processes')).toBeVisible({
559
+ // The legacy marketplace modal's card, or its Browse all dialog card.
560
+ await (0, test_1.expect)(page
561
+ .getByText('REST ConnectorBy CamundaConnect, interact, and synchronize processes')
562
+ .or(page
563
+ .getByRole('list', { name: 'Marketplace connectors' })
564
+ .getByText('REST Connector', { exact: true }))
565
+ .first()).toBeVisible({
560
566
  timeout: 60000,
561
567
  });
562
568
  await connectorMarketplacePage.downloadConnectorToProject();
@@ -106,11 +106,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
106
106
  await modelerHomePage.clickMessageBanner();
107
107
  });
108
108
  await _8_7_1.test.step('Change User Roles and Assert This Updates in Web Modeler', async () => {
109
- await (0, test_1.expect)(modelerHomePage.openOrganizationsButton).toBeVisible({
110
- timeout: 120000,
111
- });
112
- await modelerHomePage.clickOpenOrganizationsButton();
113
- await modelerHomePage.clickManageButton();
109
+ await modelerHomePage.openOrganizationManagement(120000);
114
110
  await consoleOrganizationsPage.clickUsersTab();
115
111
  await consoleOrganizationsPage.filterTable([
116
112
  process.env.C8_USERNAME_TEST_3,
@@ -174,11 +170,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
174
170
  timeout: 90000,
175
171
  });
176
172
  await modelerHomePage.clickMessageBanner();
177
- await (0, test_1.expect)(modelerHomePage.openOrganizationsButton).toBeVisible({
178
- timeout: 120000,
179
- });
180
- await modelerHomePage.clickOpenOrganizationsButton();
181
- await modelerHomePage.clickManageButton();
173
+ await modelerHomePage.openOrganizationManagement(120000);
182
174
  await consoleOrganizationsPage.clickUsersTab();
183
175
  await consoleOrganizationsPage.filterTable([
184
176
  process.env.C8_USERNAME_TEST_3,
@@ -234,11 +226,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
234
226
  timeout: 90000,
235
227
  });
236
228
  await modelerHomePage.clickMessageBanner();
237
- await (0, test_1.expect)(modelerHomePage.openOrganizationsButton).toBeVisible({
238
- timeout: 60000,
239
- });
240
- await modelerHomePage.clickOpenOrganizationsButton();
241
- await modelerHomePage.clickManageButton();
229
+ await modelerHomePage.openOrganizationManagement(60000);
242
230
  await consoleOrganizationsPage.clickUsersTab();
243
231
  await consoleOrganizationsPage.filterTable([
244
232
  process.env.C8_USERNAME_TEST_3,
@@ -15,3 +15,16 @@ import { Locator } from '@playwright/test';
15
15
  * preselected.
16
16
  */
17
17
  export declare function selectModelerCluster(dialog: Locator): Promise<void>;
18
+ /**
19
+ * Picks a row in the environment-based deploy dialog (camunda-hub#29210). Each row is
20
+ * `[data-test="environment-row-<id>"]`, one per Environment assigned to the deploy's workspace, and
21
+ * the dialog preselects the first one -- so with no cluster name to match, the preselection is left
22
+ * alone.
23
+ *
24
+ * This suite's single-cluster topology gives each cluster exactly one Environment, whose row shows
25
+ * no separate cluster subtext -- so the row's only visible identifying text is the Environment's
26
+ * own name, matched here on the assumption that it is named after its cluster (true for how this
27
+ * suite's clusters are registered). A row that does carry a distinct cluster subtext also matches,
28
+ * since `hasText` scans the whole row.
29
+ */
30
+ export declare function selectDeployEnvironment(dialog: Locator, clusterName: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.selectModelerCluster = void 0;
3
+ exports.selectDeployEnvironment = exports.selectModelerCluster = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  /**
6
6
  * Points a Web Modeler deploy dialog at the cluster named by MODELER_CLUSTER_NAME.
@@ -94,3 +94,4 @@ async function selectDeployEnvironment(dialog, clusterName) {
94
94
  await target.getByRole('radio').click({ timeout: 30000 });
95
95
  await (0, test_1.expect)(target).toContainText(clusterName);
96
96
  }
97
+ exports.selectDeployEnvironment = selectDeployEnvironment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1252",
3
+ "version": "0.0.1253",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",