@camunda/e2e-test-suite 0.0.1254 → 0.0.1255

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.
@@ -81,6 +81,40 @@ class AppsPage {
81
81
  // (see clickHubClusterApp and isHubClusterAppPresent below) gets at most
82
82
  // one match, scoped to this cluster.
83
83
  async expandHubClusterGroup(clusterName) {
84
+ const toggle = this.hubSidebar
85
+ .getByRole('button', { name: clusterName })
86
+ .first();
87
+ // The diagram editor (and other full-screen views reached from it, e.g.
88
+ // right after runProcessInstance/deployDiagram) has no "Main navigation"
89
+ // sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
90
+ // page's only <nav> is aria-label="Context navigation" (the breadcrumb),
91
+ // and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
92
+ // appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
93
+ // no switcher panel to open), so a caller reaching clickOperate/
94
+ // clickTasklist/clickOptimize straight from a diagram was left on a page
95
+ // this sidebar could never resolve on, no matter how the toggle itself
96
+ // is matched. Navigate home first, the same way clickModeler() does,
97
+ // whenever the sidebar isn't already there.
98
+ //
99
+ // A freshly re-authenticated page can also land on a workspace-agnostic
100
+ // route (e.g. bare /workspaces right after loginWithTestUser(), which
101
+ // clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
102
+ // sidebar renders but has not resolved this cluster's own group yet.
103
+ // Confirmed from a nightly trace: both the getAttribute and click below
104
+ // timed out (10s each) against this exact toggle while the URL sat on
105
+ // /workspaces, right after a mid-test re-login. Route through the same
106
+ // clickModeler() recovery whenever the toggle itself hasn't shown up
107
+ // either, not only when the whole sidebar is missing.
108
+ const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
109
+ (await toggle
110
+ .waitFor({ state: 'visible', timeout: 10000 })
111
+ .then(() => true)
112
+ .catch(() => false));
113
+ if (!sidebarReady) {
114
+ await this.clickModeler();
115
+ await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
116
+ await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
117
+ }
84
118
  // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
85
119
  // shape: "the design system splits the group row into a label control
86
120
  // and a chevron control, both labelled by the cluster name; either one
@@ -101,9 +135,13 @@ class AppsPage {
101
135
  // any container matchable by cluster name once expanded (confirmed
102
136
  // against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
103
137
  // own aria-expanded is the only reliable per-cluster signal available.
104
- const toggle = this.hubSidebar
105
- .getByRole('button', { name: clusterName, exact: true })
106
- .first();
138
+ // Not exact: the same suite asserts this row with a substring/regex
139
+ // match (`{name: /Production/}`), not an exact string -- and nightly
140
+ // traces confirm why: the rendered toggle's accessible name is not
141
+ // byte-for-byte the bare cluster name (it carries additional content,
142
+ // e.g. a trailing tag summary), so an exact-match lookup here finds
143
+ // nothing and the click below times out waiting on a locator that can
144
+ // never resolve.
107
145
  const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
108
146
  if (!alreadyExpanded) {
109
147
  // This is the only expansion action in the Hub path -- swallowing its
@@ -125,10 +163,14 @@ class AppsPage {
125
163
  // of its own, in which case hasNotText would fail to exclude the toggle
126
164
  // we just expanded and this loop would immediately collapse it again.
127
165
  // Match by accessible name (same algorithm getByRole's own name option
128
- // uses) instead, excluding an exact match on clusterName.
166
+ // uses) instead, excluding any name CONTAINING clusterName -- not just
167
+ // one exactly equal to it, since (per the toggle locator above) the
168
+ // current cluster's own toggle name is not exactly clusterName either,
169
+ // and an exact-equality exclusion would wrongly treat it as "other" and
170
+ // immediately re-collapse the group this method just expanded.
129
171
  const otherExpandedToggles = this.hubSidebar.getByRole('button', {
130
172
  expanded: true,
131
- name: new RegExp(`^(?!${escapeRegExp(clusterName)}$).+$`),
173
+ name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
132
174
  });
133
175
  for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
134
176
  // Let a failed collapse throw rather than swallowing it: the app link
@@ -151,12 +193,15 @@ class AppsPage {
151
193
  // match carries no ambiguity risk.
152
194
  const appLink = this.hubAppLink(clusterName, appName);
153
195
  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.
196
+ // Hub's cluster-app links carry target="_blank", so clicking them opens
197
+ // the app in a new tab while every caller keeps asserting on this.page.
198
+ // Follow the href in the current tab instead, matching the same-tab
199
+ // navigation the Console app switcher gave these callers.
157
200
  const href = await appLink.getAttribute('href');
158
201
  if (href && (await appLink.getAttribute('target')) === '_blank') {
159
- await this.page.goto(href, { waitUntil: 'domcontentloaded' });
202
+ await this.page.goto(new URL(href, this.page.url()).toString(), {
203
+ waitUntil: 'domcontentloaded',
204
+ });
160
205
  return;
161
206
  }
162
207
  await appLink.click({ timeout: 10000 });
@@ -164,11 +209,18 @@ class AppsPage {
164
209
  // Inline (expanded rail) the app link carries the app's name. The rail can
165
210
  // also start collapsed (camunda-hub seeds SidebarProvider's defaultExpanded
166
211
  // 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>".
212
+ // opens a Radix popover -- a role="dialog" labelled with the cluster name
213
+ // and portalled OUTSIDE "Main navigation" -- whose links are plainly named.
214
+ // With Hub's Environments feature on, the sidebar instead lists
215
+ // environments whose app links render inline but are named
216
+ // "<app> in <cluster>" (opens in a new tab). Try the expanded-sidebar
217
+ // layout first, then each of these fallbacks.
169
218
  hubAppLink(clusterName, appName) {
170
219
  return this.hubSidebar
171
220
  .getByRole('link', { name: appName })
221
+ .or(this.page
222
+ .getByRole('dialog', { name: clusterName, exact: true })
223
+ .getByRole('link', { name: appName }))
172
224
  .or(this.page.getByRole('link', {
173
225
  name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
174
226
  }))
@@ -182,6 +234,8 @@ class AppsPage {
182
234
  // rather than a disabled control -- so this never throws on a miss.
183
235
  async isHubClusterAppPresent(clusterName, appName) {
184
236
  await this.expandHubClusterGroup(clusterName);
237
+ // Same collapsed-sidebar popover / environment-row fallbacks as
238
+ // clickHubClusterApp -- see hubAppLink.
185
239
  const appLink = this.hubAppLink(clusterName, appName);
186
240
  return appLink.isVisible({ timeout: 10000 }).catch(() => false);
187
241
  }
@@ -79,28 +79,27 @@ class ClusterPage {
79
79
  exact: true,
80
80
  });
81
81
  this.generationSearchInput = page.getByPlaceholder('Search generations');
82
- this.g3DevClusterType = page.locator('label').filter({
83
- hasText: this.defaultClusterType,
84
- });
85
- this.alphaTab = page.getByRole('tab', { name: 'Alpha' });
86
- this.devTab = page.getByRole('tab', { name: 'Internal Dev' });
87
- this.stableTab = page.getByRole('tab', { name: 'Stable' });
88
- this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
89
- this.gcpRegion = page.getByRole('tab', {
90
- name: 'GCP',
91
- });
92
- this.belgiumRegion = page.locator('label').filter({
82
+ // Camunda Hub's create-cluster form replaced the Carbon tiles (a <label>
83
+ // per option) and tabs with design-system choice cards and toggle
84
+ // groups, all exposed as role="radio" named by their title. Each locator
85
+ // below accepts either shape via this helper.
86
+ const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
87
+ this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
88
+ this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
89
+ this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
90
+ this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
91
+ this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
92
+ this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
93
+ this.belgiumRegion = choice(page.locator('label').filter({
93
94
  hasText: 'Chaos, Belgium, Europe (europe-west1)',
94
- });
95
- this.finlandRegion = page.locator('label').filter({
95
+ }), 'Chaos, Belgium, Europe (europe-west1)');
96
+ this.finlandRegion = choice(page.locator('label').filter({
96
97
  hasText: 'Finland, Europe (europe-north1)',
97
- });
98
- this.londonRegion = page.locator('label').filter({
99
- hasText: 'London, Europe (europe-west2)',
100
- });
101
- this.ohioRegion = page.locator('label').filter({
98
+ }), 'Finland, Europe (europe-north1)');
99
+ this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
100
+ this.ohioRegion = choice(page.locator('label').filter({
102
101
  hasText: 'Ohio, North America (us-east-2)',
103
- });
102
+ }), 'Ohio, North America (us-east-2)');
104
103
  this.clustersList = page
105
104
  .getByRole('row')
106
105
  .filter({ hasNotText: 'Generation' }); //Filter out header row
@@ -239,7 +238,10 @@ class ClusterPage {
239
238
  await this.clickClusterNameInput();
240
239
  await this.fillClusterNameInput(name);
241
240
  await this.clickClusterType();
242
- await this.page.getByRole('tab', { name: region }).click();
241
+ await this.page
242
+ .getByRole('tab', { name: region })
243
+ .or(this.page.getByRole('radio', { name: region, exact: true }))
244
+ .click();
243
245
  await this.selectSubRegion(region);
244
246
  await this.clickClusterOption(process.env.CLUSTER_VERSION);
245
247
  await this.clickCreateClusterButton();
@@ -4,6 +4,7 @@ exports.ConsoleOrganizationPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
7
8
  class ConsoleOrganizationPage {
8
9
  page;
9
10
  usersTab;
@@ -137,10 +138,26 @@ class ConsoleOrganizationPage {
137
138
  name: 'assign',
138
139
  })
139
140
  .last();
140
- this.assignMembersSearchbox = page.getByRole('searchbox', {
141
+ // The "Assign members" dialog now renders through cmdk (camunda-hub's
142
+ // Command component: data-slot="command"/"command-input") instead of the
143
+ // legacy Carbon combobox. The input keeps the same placeholder text, but
144
+ // its role is "combobox", not "searchbox" -- and the wrapping
145
+ // <label cmdk-label> that aria-labelledby points at is rendered empty, so
146
+ // the accessible-name lookup itself is unreliable; match by placeholder
147
+ // instead, which cmdk always sets from the same string. Nightly trace:
148
+ // getByRole('searchbox', {name: '...'}) found nothing and the click
149
+ // timed out even though the input was on screen.
150
+ this.assignMembersSearchbox = page
151
+ .getByPlaceholder('Search by full name or email address')
152
+ .or(page.getByRole('searchbox', {
141
153
  name: 'search by full name or email address',
142
- });
143
- this.assignMembersSearchResult = page.locator('.cds--list-box__menu-item');
154
+ }))
155
+ .first();
156
+ // cmdk's CommandItem renders role="option" (cmdk-item); keep the legacy
157
+ // Carbon class as a fallback.
158
+ this.assignMembersSearchResult = page
159
+ .getByRole('option')
160
+ .or(page.locator('.cds--list-box__menu-item'));
144
161
  this.organizationManagementLink = page.getByRole('link', {
145
162
  name: 'organization management',
146
163
  });
@@ -148,9 +165,15 @@ class ConsoleOrganizationPage {
148
165
  this.deleteSubButton = page
149
166
  .getByRole('dialog')
150
167
  .getByRole('button', { name: 'delete' });
151
- this.filterTableSearchbox = page.getByRole('searchbox', {
152
- name: 'Filter table',
153
- });
168
+ // Hub's org-management "Users" tab (hub.ultrawombat.com/org/.../
169
+ // management/users) renders its own table filter as a design-system
170
+ // input labelled plainly "Search", not "Filter table" -- confirmed from
171
+ // the nightly trace's DOM snapshot (an <input type="search"
172
+ // aria-label="Search"> directly inside the "Users" tab panel). Keep the
173
+ // legacy name as a fallback.
174
+ this.filterTableSearchbox = page
175
+ .getByRole('searchbox', { name: 'Search', exact: true })
176
+ .or(page.getByRole('searchbox', { name: 'Filter table' }));
154
177
  }
155
178
  async clickCreateAuthorizedResourceButton() {
156
179
  await (0, test_1.expect)(this.createButton).toBeVisible({ timeout: 60000 });
@@ -433,6 +456,14 @@ class ConsoleOrganizationPage {
433
456
  async clickGroupsTab() {
434
457
  for (let attempt = 0; attempt < 3; attempt++) {
435
458
  try {
459
+ // Hub can drop the SPA session mid-test and bounce this tab to
460
+ // /login-callback then its own hung /login?returnUrl=... instead of
461
+ // rendering Organization management -- the same session loss
462
+ // utils/sessionRecovery.ts already recovers from elsewhere. Recover it
463
+ // before clicking, or every retry below just reloads the dead login
464
+ // page and re-fails the same way.
465
+ await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
466
+ await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
436
467
  await this.groupsTab.click({ timeout: 60000 });
437
468
  return;
438
469
  }
@@ -122,11 +122,21 @@ class FormJsPage {
122
122
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
123
123
  }
124
124
  // Hub's environment-based dialog also renders a <label> per row, which
125
- // the legacy toggle detection below would mistake for its own.
125
+ // the legacy toggle detection below would mistake for its own. A bare
126
+ // isVisible() here does not wait -- it can race the dialog's own content
127
+ // rendering (this.dialog itself is visible a beat before its rows are)
128
+ // and report false even though this IS the environment-based dialog,
129
+ // falling through to the legacy branch below and failing on a
130
+ // [title="<cluster>"] locator that dialog never had. Give it a real
131
+ // window to render before deciding.
126
132
  const environmentRow = this.dialog
127
133
  .locator('[data-test^="environment-row-"]')
128
134
  .first();
129
- if (await environmentRow.isVisible().catch(() => false)) {
135
+ const hasEnvironmentRow = await environmentRow
136
+ .waitFor({ state: 'visible', timeout: 10000 })
137
+ .then(() => true)
138
+ .catch(() => false);
139
+ if (hasEnvironmentRow) {
130
140
  try {
131
141
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
132
142
  }
@@ -163,10 +173,12 @@ class FormJsPage {
163
173
  // Hub's environment-based dialog lists every Environment assigned to
164
174
  // the workspace and preselects the first, which need not be this one.
165
175
  const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
166
- if (await environmentRows
176
+ const hasEnvironmentRows = await environmentRows
167
177
  .first()
168
- .isVisible()
169
- .catch(() => false)) {
178
+ .waitFor({ state: 'visible', timeout: 10000 })
179
+ .then(() => true)
180
+ .catch(() => false);
181
+ if (hasEnvironmentRows) {
170
182
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
171
183
  }
172
184
  return;
@@ -175,10 +187,16 @@ class FormJsPage {
175
187
  const cluster = this.dialog.locator(`[title="${clusterName}"]`);
176
188
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
177
189
  await cluster.click();
178
- await (0, test_1.expect)(this.dialog
190
+ // ClustersListDs.tsx (camunda-hub) replaced the legacy Carbon toggle's
191
+ // role="switch" with a Radix RadioGroupItem (role="radio") -- the
192
+ // wrapping <label> is unchanged, but a role="switch" lookup now finds
193
+ // nothing. Accept either role.
194
+ const selectedClusterControl = this.dialog
179
195
  .locator('label')
180
- .filter({ hasText: new RegExp(`^${clusterName}`) })
181
- .getByRole('switch')).toBeChecked({ timeout: 30000 });
196
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
197
+ await (0, test_1.expect)(selectedClusterControl
198
+ .getByRole('switch')
199
+ .or(selectedClusterControl.getByRole('radio'))).toBeChecked({ timeout: 30000 });
182
200
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
183
201
  timeout: healthCheckTimeout,
184
202
  });
@@ -208,8 +226,17 @@ class FormJsPage {
208
226
  return;
209
227
  }
210
228
  await scopeSelect.click();
211
- await this.dialog
229
+ // camunda-hub's DeployScopeDs.tsx renders this combobox through the
230
+ // design system's Select, whose SelectContent mounts inside a Radix
231
+ // Portal (see @camunda/c4-components select.tsx) -- the option list
232
+ // renders as a direct child of <body>, OUTSIDE the dialog's own DOM
233
+ // subtree. A dialog-scoped lookup for the option therefore times out
234
+ // even though the click above opened it. Look page-wide first; keep the
235
+ // dialog-scoped lookup for the legacy Carbon Dropdown (DeployScope.tsx),
236
+ // which renders its menu inline.
237
+ await this.page
212
238
  .getByRole('option', { name: 'Only this resource' })
239
+ .or(this.dialog.getByRole('option', { name: 'Only this resource' }))
213
240
  .or(this.dialog.getByText('Only this resource', { exact: true }))
214
241
  .first()
215
242
  .click({ timeout: 30000 });
@@ -4,6 +4,7 @@ exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
6
  const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
7
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
7
8
  class LoginPage {
8
9
  page;
9
10
  usernameInput;
@@ -53,6 +54,7 @@ class LoginPage {
53
54
  }
54
55
  async login(credentials = {}) {
55
56
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
57
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
56
58
  // See loginWithTestUser below: this re-authenticates the same page and
57
59
  // must clear the Hub bearer-token cache too, or getUserToken() can
58
60
  // return whatever user was previously logged in on it.
@@ -68,6 +70,7 @@ class LoginPage {
68
70
  }
69
71
  async loginWithTestUser(credentials = {}) {
70
72
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
73
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
71
74
  // See loginWithRetry in UtilitiesPage.ts: the Hub bearer-token cache in
72
75
  // consoleApiHelpers.ts is keyed by Page for the tab's whole lifetime, so
73
76
  // this re-authentication (a different user than whatever was logged in
@@ -123,6 +126,7 @@ class LoginPage {
123
126
  }
124
127
  async loginWithoutOrgAssertion(credentials = {}) {
125
128
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
129
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
126
130
  // See loginWithTestUser above: this re-authenticates the same page and
127
131
  // must clear the Hub bearer-token cache too, or getUserToken() can
128
132
  // return whatever user was previously logged in on it.
@@ -477,14 +477,19 @@ class ModelerCreatePage {
477
477
  await this.userTaskOption.click();
478
478
  }
479
479
  async clickForm(name) {
480
+ // The "Link a form" picker renders each entry as a button keyed by the
481
+ // form's resource id (data-test="link-target-file-<uuid>"), not by name --
482
+ // `item-${name}` never matches any element the picker renders. The name is
483
+ // only exposed as this accessible name, so match on that instead.
484
+ //
480
485
  // Baseline upgrade runs use fixed form names, so a Playwright retry re-creates
481
486
  // a form with the same name and the picker then lists two identical entries.
482
487
  // Scope to the first matching item so the link step stays deterministic on retry
483
488
  // instead of failing with a strict-mode violation.
484
489
  await this.page
485
- .locator(`[data-test="item-${name}"]`)
490
+ .locator('[data-test="link-target-list"]')
491
+ .getByRole('button', { name: `Select ${name}` })
486
492
  .first()
487
- .getByText(name)
488
493
  .click({ timeout: 90000 });
489
494
  }
490
495
  async clickUserTask(id) {
@@ -525,20 +530,49 @@ class ModelerCreatePage {
525
530
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
526
531
  timeout: 30000,
527
532
  });
528
- //Select the correct cluster if multiple exist
529
- const clusterLabel = this.deployDiagramDialog
530
- .locator('label')
531
- .filter({ hasText: new RegExp(`^${clusterName}`) });
532
- await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
533
- await clusterLabel.click();
534
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
535
- timeout: 30000,
536
- });
537
- //Check healthy status of the selected cluster
538
- const regex = new RegExp(`${clusterName}.*?HealthydevManage`);
539
- await (0, test_1.expect)(this.deployDiagramDialog).toHaveText(regex, {
540
- timeout: 120000,
541
- });
533
+ // Hub's environment-based deploy dialog (camunda-hub#29210) lists the
534
+ // Environments assigned to the workspace as a role="radiogroup" under
535
+ // #environment-selection-heading, one row per
536
+ // [data-test="environment-row-*"] -- confirmed from a nightly trace's DOM
537
+ // snapshot: the radio button and its <label> are SIBLINGS inside the row
538
+ // (the label is no longer a wrapping parent), so a label-scoped
539
+ // getByRole('switch'|'radio') lookup finds nothing, and the row carries
540
+ // no "Manage" link at all, so the old concatenated-text health regex
541
+ // could never match either. Prefer this dialog; fall back to the legacy
542
+ // per-deploy cluster toggle list otherwise.
543
+ // A bare isVisible() does not wait -- it can race the dialog's own
544
+ // content rendering (the dialog itself becomes visible a beat before its
545
+ // rows do) and report false even though this IS the environment-based
546
+ // dialog, falling through to the legacy branch below and failing on a
547
+ // locator that dialog never had. Give it a real window to render before
548
+ // deciding.
549
+ const environmentRows = this.deployDiagramDialog.locator('[data-test^="environment-row-"]');
550
+ const hasEnvironmentRows = await environmentRows
551
+ .first()
552
+ .waitFor({ state: 'visible', timeout: 10000 })
553
+ .then(() => true)
554
+ .catch(() => false);
555
+ if (hasEnvironmentRows) {
556
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.deployDiagramDialog, clusterName);
557
+ const targetRow = environmentRows.filter({ hasText: clusterName }).first();
558
+ await (0, test_1.expect)(targetRow.getByText('Healthy', { exact: true })).toBeVisible({
559
+ timeout: 120000,
560
+ });
561
+ }
562
+ else {
563
+ //Select the correct cluster if multiple exist
564
+ const clusterLabel = this.deployDiagramDialog
565
+ .locator('label')
566
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
567
+ await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
568
+ await clusterLabel.click();
569
+ await (0, test_1.expect)(clusterLabel.getByRole('switch').or(clusterLabel.getByRole('radio'))).toBeChecked({ timeout: 30000 });
570
+ //Check healthy status of the selected cluster
571
+ const regex = new RegExp(`${clusterName}.*?HealthydevManage`);
572
+ await (0, test_1.expect)(this.deployDiagramDialog).toHaveText(regex, {
573
+ timeout: 120000,
574
+ });
575
+ }
542
576
  await (0, test_1.expect)(this.deploySubButton).toBeVisible({
543
577
  timeout: 120000,
544
578
  });
@@ -1647,11 +1681,21 @@ class ModelerCreatePage {
1647
1681
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
1648
1682
  }
1649
1683
  // Hub's environment-based dialog also renders a <label> per row, which
1650
- // the legacy toggle detection below would mistake for its own.
1684
+ // the legacy toggle detection below would mistake for its own. A bare
1685
+ // isVisible() here does not wait -- it can race the dialog's own content
1686
+ // rendering (this.dialog itself is visible a beat before its rows are)
1687
+ // and report false even though this IS the environment-based dialog,
1688
+ // falling through to the legacy branch below and failing on a
1689
+ // [title="<cluster>"] locator that dialog never had. Give it a real
1690
+ // window to render before deciding.
1651
1691
  const environmentRow = this.dialog
1652
1692
  .locator('[data-test^="environment-row-"]')
1653
1693
  .first();
1654
- if (await environmentRow.isVisible().catch(() => false)) {
1694
+ const hasEnvironmentRow = await environmentRow
1695
+ .waitFor({ state: 'visible', timeout: 10000 })
1696
+ .then(() => true)
1697
+ .catch(() => false);
1698
+ if (hasEnvironmentRow) {
1655
1699
  try {
1656
1700
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1657
1701
  }
@@ -1688,10 +1732,12 @@ class ModelerCreatePage {
1688
1732
  // Hub's environment-based dialog lists every Environment assigned to
1689
1733
  // the workspace and preselects the first, which need not be this one.
1690
1734
  const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
1691
- if (await environmentRows
1735
+ const hasEnvironmentRows = await environmentRows
1692
1736
  .first()
1693
- .isVisible()
1694
- .catch(() => false)) {
1737
+ .waitFor({ state: 'visible', timeout: 10000 })
1738
+ .then(() => true)
1739
+ .catch(() => false);
1740
+ if (hasEnvironmentRows) {
1695
1741
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1696
1742
  }
1697
1743
  return;
@@ -1702,9 +1748,11 @@ class ModelerCreatePage {
1702
1748
  .filter({ hasText: new RegExp(`^${clusterName}`) });
1703
1749
  await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
1704
1750
  await clusterLabel.click();
1705
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
1706
- timeout: 30000,
1707
- });
1751
+ // ClustersListDs.tsx (camunda-hub) replaced the legacy Carbon toggle's
1752
+ // role="switch" with a Radix RadioGroupItem (role="radio") -- the
1753
+ // wrapping <label> is unchanged, but a role="switch" lookup now finds
1754
+ // nothing. Accept either role.
1755
+ await (0, test_1.expect)(clusterLabel.getByRole('switch').or(clusterLabel.getByRole('radio'))).toBeChecked({ timeout: 30000 });
1708
1756
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
1709
1757
  timeout: healthCheckTimeout,
1710
1758
  });
@@ -58,6 +58,22 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
58
58
  for (const [index, user] of (0, users_1.getTestUsers)().entries()) {
59
59
  (0, _8_7_1.test)(`Create Project Folder for User ${index + 1}`, async ({ page, loginPage, homePage, appsPage, modelerHomePage, }, testInfo) => {
60
60
  await _8_7_1.test.step('Log in', async () => {
61
+ // getTestUsers() resolves straight from process.env.C8_USERNAME_TEST_N /
62
+ // C8_PASSWORD_TEST_N. When this SaaS org was created without one of
63
+ // those slots the password half comes back '', and loginWithRetry then
64
+ // spends its full backoff budget (~13 minutes across both Playwright
65
+ // retries) re-submitting a login Auth0 rejects with "Password is
66
+ // required" every single attempt, before surfacing the generic "Login
67
+ // failed after 5 attempts" -- no mention of which slot or that a
68
+ // password was even missing. Fail immediately instead, naming the
69
+ // slot, the same way requireTestUser() already does for specs that
70
+ // reach a numbered credential slot directly.
71
+ if (!user.username || !user.password) {
72
+ throw new Error(`User ${index + 1} (${user.username || 'username also missing'}) has no ` +
73
+ 'password configured -- this SaaS org was created without that ' +
74
+ 'user slot. See scripts/flakiness-saas-create-org.sh, which sizes ' +
75
+ 'the org from the slots each spec names.');
76
+ }
61
77
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
62
78
  });
63
79
  await _8_7_1.test.step('Navigate to Web Modeler', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1254",
3
+ "version": "0.0.1255",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",