@camunda/e2e-test-suite 0.0.1273 → 0.0.1274

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.
@@ -18,8 +18,10 @@ declare class AppsPage {
18
18
  readonly consoleLink: Locator;
19
19
  readonly camundaComponentsButton: Locator;
20
20
  readonly camundaAppsButton: Locator;
21
+ readonly hubSidebar: Locator;
21
22
  constructor(page: Page);
22
23
  clickCluster(component: Locator, name: string): Promise<void>;
24
+ private clickHubClusterApp;
23
25
  clickModeler(): Promise<void>;
24
26
  /**
25
27
  * Waits for the Modeler app to actually render, and recovers the one landing
@@ -23,6 +23,7 @@ class AppsPage {
23
23
  consoleLink;
24
24
  camundaComponentsButton;
25
25
  camundaAppsButton;
26
+ hubSidebar;
26
27
  constructor(page) {
27
28
  this.page = page;
28
29
  this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
@@ -34,6 +35,11 @@ class AppsPage {
34
35
  this.consoleLink = page.getByRole('link', { name: 'Console', exact: true });
35
36
  this.camundaComponentsButton = page.getByLabel('Camunda components');
36
37
  this.camundaAppsButton = page.getByLabel('Camunda apps');
38
+ // Camunda Hub's persistent "Main navigation" sidebar lists each cluster
39
+ // with its own apps (Operate/Tasklist/Optimize/Identity) as child links
40
+ // -- there is no per-app switcher panel to open on Hub (see
41
+ // clickCamundaApps below). Ported from pages/8.9/AppsPage.ts.
42
+ this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
37
43
  this.tasklistListItem = this.page
38
44
  .getByRole('listitem')
39
45
  .filter({ hasText: 'Tasklist' });
@@ -67,7 +73,83 @@ class AppsPage {
67
73
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 60000 });
68
74
  await cluster.click();
69
75
  }
76
+ // Camunda Hub's sidebar groups by cluster, not by app: each cluster row
77
+ // expands to reveal its own per-app child links (Operate/Tasklist/...).
78
+ // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: the
79
+ // design system splits the group row into a label control and a chevron
80
+ // control, both labelled by the cluster name; either one opens it -- and
81
+ // the child links do not exist in the DOM at all until expanded (not just
82
+ // hidden), and are not nested under any container matchable by cluster
83
+ // name once expanded. So: find the toggle by the cluster's own name
84
+ // (`.first()` because both controls share that name), click it, then look
85
+ // up the app link sidebar-wide rather than scoped to a "group" container
86
+ // that doesn't reliably exist. Ported from pages/8.9/AppsPage.ts, where
87
+ // this shape is already nightly-proven.
88
+ async clickHubClusterApp(clusterName, appName) {
89
+ // When the sidebar is collapsed (the default on a fresh session), the
90
+ // group toggle opens a Radix popover -- a role="dialog" labelled with the
91
+ // cluster name and portalled OUTSIDE the "Main navigation" <nav> -- so a
92
+ // sidebar-scoped lookup finds nothing there. Look in that popover first;
93
+ // keep the sidebar-scoped lookup for the expanded-sidebar layout, where
94
+ // the links render inline. Not exact: the rendered accessible name
95
+ // carries more than the bare app name (e.g. "Operate in <cluster>
96
+ // (opens in a new tab)").
97
+ const appLink = this.page
98
+ .getByRole('dialog', { name: clusterName, exact: true })
99
+ .getByRole('link', { name: appName })
100
+ .or(this.hubSidebar.getByRole('link', { name: appName }))
101
+ .first();
102
+ // Decide whether to expand from clusterName's OWN toggle state, not from
103
+ // whether "some" appLink is already visible sidebar-wide: in a
104
+ // multi-cluster org another cluster's group can already be expanded with
105
+ // the same app name, and the click would then fire on the wrong
106
+ // cluster's link.
107
+ const collapsedToggle = this.hubSidebar
108
+ .getByRole('button', { name: clusterName, expanded: false })
109
+ .first();
110
+ if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
111
+ await collapsedToggle.click({ timeout: 10000 });
112
+ }
113
+ await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
114
+ // Hub's cluster-app links carry target="_blank", so clicking them opens
115
+ // the app in a new tab while every caller keeps asserting on this.page.
116
+ // Follow the href in the current tab instead, matching the same-tab
117
+ // navigation the Console app switcher gave these callers.
118
+ const href = await appLink.getAttribute('href');
119
+ if (href) {
120
+ await this.page.goto(new URL(href, this.page.url()).toString());
121
+ return;
122
+ }
123
+ await appLink.click({ timeout: 10000 });
124
+ }
70
125
  async clickModeler() {
126
+ // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
127
+ // app switcher. Its home is the Modeler/Workspaces home, but a hostname
128
+ // check alone can't tell whether we're already there -- Hub's cluster
129
+ // pages are on the same hub.* hostname, and a no-op here would leave a
130
+ // caller that reaches clickModeler() from a cluster/API-client page
131
+ // stranded with the next locator running against the wrong page.
132
+ // Navigate home through the header logo link instead, same as
133
+ // ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
134
+ // Modeler home is a harmless in-SPA no-op.
135
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
136
+ const header = this.page.locator('header[data-slot="app-header"]');
137
+ // The href stays literally "/login" until the client's auth state
138
+ // catches up just after login (camunda-hub NavShellDs.common.tsx) --
139
+ // clicking it in that window would log the test out instead of
140
+ // navigating home, so exclude that href and wait for the authenticated
141
+ // one rather than racing it.
142
+ const homeLink = header
143
+ .getByRole('link', { name: 'Camunda Modeler home' })
144
+ .and(this.page.locator(':not([href$="/login"])'))
145
+ .or(header
146
+ .getByRole('link', { name: 'Camunda Hub home' })
147
+ .and(this.page.locator(':not([href$="/login"])')))
148
+ .first();
149
+ await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
150
+ await homeLink.click({ timeout: 30000 });
151
+ return;
152
+ }
71
153
  const maxRetries = 5;
72
154
  // If the click is already done and we are sitting on Modeler, return
73
155
  // immediately — observed in failing runs where the banner role lookup
@@ -148,6 +230,10 @@ class AppsPage {
148
230
  }
149
231
  }
150
232
  async clickTasklist(clusterName) {
233
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
234
+ await this.clickHubClusterApp(clusterName, 'Tasklist');
235
+ return;
236
+ }
151
237
  const maxRetries = 5;
152
238
  for (let retries = 0; retries < maxRetries; retries++) {
153
239
  try {
@@ -185,6 +271,14 @@ class AppsPage {
185
271
  throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
186
272
  }
187
273
  async clickIdentity(clusterName) {
274
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
275
+ // Non-anchored, matching the substring lookup clickHubClusterApp uses
276
+ // for Operate/Tasklist/Optimize -- an anchored pattern fails the same
277
+ // way an exact-string match did for those apps if the rendered link's
278
+ // accessible name carries any extra content (icon/badge).
279
+ await this.clickHubClusterApp(clusterName, /Admin|Identity/);
280
+ return;
281
+ }
188
282
  const maxRetries = 5;
189
283
  for (let retries = 0; retries < maxRetries; retries++) {
190
284
  try {
@@ -251,6 +345,10 @@ class AppsPage {
251
345
  await this.clickCluster(this.optimizeListItem, clusterName);
252
346
  }
253
347
  async clickOperate(clusterName) {
348
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
349
+ await this.clickHubClusterApp(clusterName, 'Operate');
350
+ return;
351
+ }
254
352
  const maxRetries = 3;
255
353
  for (let retries = 0; retries < maxRetries; retries++) {
256
354
  try {
@@ -287,6 +385,10 @@ class AppsPage {
287
385
  throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
288
386
  }
289
387
  async clickOptimize(clusterName) {
388
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
389
+ await this.clickHubClusterApp(clusterName, 'Optimize');
390
+ return;
391
+ }
290
392
  const maxRetries = 3;
291
393
  for (let retries = 0; retries < maxRetries; retries++) {
292
394
  try {
@@ -373,6 +475,13 @@ class AppsPage {
373
475
  }
374
476
  }
375
477
  async clickCamundaApps(maxRetries = 3) {
478
+ // Camunda Hub's rewritten shells have no app-switcher button to open --
479
+ // Modeler is already Hub's home area and the other apps are reached
480
+ // per-cluster via the persistent sidebar, not through a global switcher
481
+ // panel. Mirrors the same hostname guard in clickModeler() above.
482
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
483
+ return;
484
+ }
376
485
  await (0, sleep_1.sleep)(2000);
377
486
  const appButtons = [
378
487
  this.camundaComponentsButton,
@@ -420,6 +529,19 @@ class AppsPage {
420
529
  .catch(() => { });
421
530
  }
422
531
  async clickConsoleLink() {
532
+ // Camunda Hub replaced Console on SaaS: there is no "Console" entry in a
533
+ // switcher, and the org-scoped Console area is the sidebar's
534
+ // Clusters/Environments section. Going there is what callers assert on
535
+ // (HomePage.consoleBanner resolves to Hub's authenticated header).
536
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
537
+ const consoleArea = this.hubSidebar
538
+ .locator('a[href$="/clusters"]')
539
+ .or(this.hubSidebar.locator('a[href$="/environments"]'))
540
+ .first();
541
+ await (0, test_1.expect)(consoleArea).toBeVisible({ timeout: 30000 });
542
+ await consoleArea.click({ timeout: 30000 });
543
+ return;
544
+ }
423
545
  const maxRetries = 3;
424
546
  for (let retries = 0; retries < maxRetries; retries++) {
425
547
  try {
@@ -40,6 +40,7 @@ declare class ClusterDetailsPage {
40
40
  readonly orchestrationClusterCheckbox: Locator;
41
41
  readonly clientCredentialsLink: (clientCredentials: string) => Locator;
42
42
  readonly clientRow: (name: string) => Locator;
43
+ readonly clientRowByDom: (name: string) => Locator;
43
44
  readonly clientRowDeleteButton: (name: string) => Locator;
44
45
  readonly reviewUpdateButton: Locator;
45
46
  readonly selectTargetVersionDialog: Locator;
@@ -46,6 +46,7 @@ class ClusterDetailsPage {
46
46
  orchestrationClusterCheckbox;
47
47
  clientCredentialsLink;
48
48
  clientRow;
49
+ clientRowByDom;
49
50
  clientRowDeleteButton;
50
51
  reviewUpdateButton;
51
52
  selectTargetVersionDialog;
@@ -78,9 +79,6 @@ class ClusterDetailsPage {
78
79
  .locator('label')
79
80
  .filter({ hasText: /^Administration API - Resource: Secrets$/ });
80
81
  this.createButton = page.getByRole('button', { name: 'Create', exact: true });
81
- this.closeModalButton = page
82
- .getByRole('dialog', { name: 'Client credentials', exact: true })
83
- .getByLabel('Close');
84
82
  this.createClientButton = page.getByRole('button', {
85
83
  name: 'Create new Client',
86
84
  });
@@ -98,9 +96,18 @@ class ClusterDetailsPage {
98
96
  this.createClientCredentialsDialog = page.getByRole('dialog', {
99
97
  name: 'Create new client credentials',
100
98
  });
101
- this.clientCredentialsDialog = page.getByRole('dialog', {
102
- name: 'Client credentials',
103
- exact: true,
99
+ // Use a content-based locator so the test stays robust if the dialog
100
+ // title is ever renamed in the Console UI.
101
+ this.clientCredentialsDialog = page.getByRole('dialog').filter({
102
+ has: page.getByText('The Client Secret will not be shown again.'),
103
+ });
104
+ // Hub's client-credentials dialog (design-system Dialog, not the old
105
+ // Carbon one) exposes its close affix as an accessible button named
106
+ // "Close" rather than via an aria-label picked up by getByLabel -- see
107
+ // camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
108
+ // (clientCredentialsDialogCloseButton), which already uses this pattern.
109
+ this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
110
+ name: 'Close',
104
111
  });
105
112
  this.clustersLink = page
106
113
  .getByRole('banner')
@@ -154,10 +161,22 @@ class ClusterDetailsPage {
154
161
  this.orchestrationClusterCheckbox = page
155
162
  .locator('label')
156
163
  .filter({ hasText: /^Orchestration Cluster API$/ });
164
+ // Camunda Hub's data table renders the client name as a link-styled
165
+ // <button data-slot="button"> rather than an <a>. The cell also holds a
166
+ // pointer-events-none row-overlay button with the same accessible name,
167
+ // so match the visible one by its data-slot.
157
168
  this.clientCredentialsLink = (clientCredentials) => page
158
169
  .getByRole('cell', { name: clientCredentials })
159
- .getByText(clientCredentials);
170
+ .locator('a, button[data-slot="button"]')
171
+ .first();
160
172
  this.clientRow = (name) => this.clientsList.filter({ hasText: name });
173
+ // Same row, matched through the DOM instead of the accessibility tree.
174
+ // Hub's client-credentials dialog is a Radix dialog: while it is open the
175
+ // whole app shell behind it carries aria-hidden/inert, so `getByRole`
176
+ // resolves nothing there and the post-create scope check below can never
177
+ // pass. `locator('tr')` is unaffected. The client name is unique per run,
178
+ // so matching page-wide cannot pick up another table's row.
179
+ this.clientRowByDom = (name) => page.locator('tr').filter({ hasText: name });
161
180
  this.clientRowDeleteButton = (name) => this.clientRow(name).getByRole('button', { name: 'Delete' });
162
181
  this.reviewUpdateButton = page.getByRole('button', { name: 'Review Update' });
163
182
  this.selectTargetVersionDialog = page
@@ -609,9 +628,9 @@ class ClusterDetailsPage {
609
628
  }
610
629
  await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible({ timeout: 30000 });
611
630
  // The clients table behind the dialog re-fetches after the create, and
612
- // renders a skeleton (no `row` roles at all) until that GET resolves — so
613
- // the default 5s expect timeout can expire before the new row exists.
614
- await (0, test_1.expect)(this.clientsList.filter({ hasText: name })).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, { timeout: 60000 });
631
+ // renders a skeleton (no rows at all) until that GET resolves — so the
632
+ // default 5s expect timeout can expire before the new row exists.
633
+ await (0, test_1.expect)(this.clientRowByDom(name)).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, { timeout: 60000 });
615
634
  }
616
635
  async clickEnvVarsButton() {
617
636
  await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
@@ -124,10 +124,43 @@ class ClusterPage {
124
124
  this.clusterVersion = (clusterName, versionText) => this.cluster(clusterName).getByText(versionText);
125
125
  }
126
126
  async clickClusterBanner() {
127
- await this.page
127
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
128
+ // persistent "Main navigation" sidebar, and once the org has environments
129
+ // enabled that link is further renamed "Environments" (same rollout
130
+ // HomePage.clusterTab handles).
131
+ const clusterLink = this.page
128
132
  .getByRole('banner')
129
133
  .getByRole('link', { name: 'Clusters' })
130
- .click({ timeout: 60000 });
134
+ .or(this.page
135
+ .getByRole('navigation', { name: 'Main navigation' })
136
+ .getByRole('link', { name: 'Clusters', exact: true }))
137
+ .or(this.page
138
+ .getByRole('navigation', { name: 'Main navigation' })
139
+ .getByRole('link', { name: 'Environments', exact: true }));
140
+ await clusterLink.click({ timeout: 60000 });
141
+ // Landing on Hub's Environments overview (see above) renders clusters as
142
+ // cards, not the getByRole('row') table this page's own locators expect
143
+ // -- route through the page's own "Clusters" tab to reach the classic
144
+ // route. Same fix as HomePage.clickClusters().
145
+ const clustersPageTab = this.page
146
+ .getByRole('navigation', { name: 'Environments views' })
147
+ .getByRole('link', { name: 'Clusters', exact: true });
148
+ // Required on Hub, not optional: this page's row-based cluster locators
149
+ // cannot be satisfied by the card-based Environments overview we just
150
+ // landed on. isVisible() does not wait, so a slow tab render used to be
151
+ // read as "no tab here" and the flow continued on the wrong route.
152
+ const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
153
+ const tabShown = await clustersPageTab
154
+ .waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
155
+ .then(() => true)
156
+ .catch(() => false);
157
+ if (tabShown) {
158
+ await clustersPageTab.click({ timeout: 15000 });
159
+ }
160
+ else if (onHub) {
161
+ throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
162
+ 'row-based cluster list this page needs is unreachable without it');
163
+ }
131
164
  }
132
165
  async deleteCluster(name) {
133
166
  await this.page.waitForTimeout(1000);
@@ -36,6 +36,7 @@ declare class ClusterSecretsPage {
36
36
  value: string;
37
37
  }[]): Promise<void>;
38
38
  reopenConnectorSecretsTab(cluster: string): Promise<void>;
39
+ private reselectConnectorSecretsTabIfPresent;
39
40
  assertSecretsToolbarVisible(cluster: string): Promise<void>;
40
41
  assertSecretsOrCreate(cluster: string, secrets: {
41
42
  name: string;
@@ -50,10 +50,27 @@ class ClusterSecretsPage {
50
50
  this.dialogImportButton = this.page
51
51
  .getByRole('dialog')
52
52
  .getByRole('button', { name: 'Import', exact: true });
53
- this.secretsSearchBox = page.getByRole('searchbox', { name: 'Filter table' });
53
+ // "Filter table" was Carbon's default TableToolbarSearch label. Camunda
54
+ // Hub's rebranded panel renders a plain "Search" input instead, so the
55
+ // old exact-name match never resolves and every lookup falls through to
56
+ // creating a secret that may already exist. Match either.
57
+ this.secretsSearchBox = page
58
+ .getByRole('searchbox', { name: 'Filter table' })
59
+ .or(page.getByRole('searchbox', { name: 'Search' }))
60
+ .or(page.getByPlaceholder('Search'));
61
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
62
+ // persistent "Main navigation" sidebar, and once the org has environments
63
+ // enabled that link is further renamed "Environments" (same rollout
64
+ // HomePage.clusterTab handles).
54
65
  this.clusterBanner = this.page
55
66
  .getByRole('banner')
56
- .getByRole('link', { name: 'Clusters' });
67
+ .getByRole('link', { name: 'Clusters' })
68
+ .or(this.page
69
+ .getByRole('navigation', { name: 'Main navigation' })
70
+ .getByRole('link', { name: 'Clusters', exact: true }))
71
+ .or(this.page
72
+ .getByRole('navigation', { name: 'Main navigation' })
73
+ .getByRole('link', { name: 'Environments', exact: true }));
57
74
  this.clusterLink = (name) => page.getByRole('link', { name: name });
58
75
  this.clusterSecretTab = this.page.getByRole('tab', {
59
76
  name: 'Connector Secrets',
@@ -144,6 +161,30 @@ class ClusterSecretsPage {
144
161
  await this.dismissOpenDialogIfPresent();
145
162
  await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
146
163
  await this.clusterBanner.click({ timeout: 60000 });
164
+ // Landing on Hub's Environments overview (see clusterBanner above) renders
165
+ // clusters as cards, not the getByRole('link') the clusterLink() locator
166
+ // below expects to find directly -- route through the page's own
167
+ // "Clusters" tab to reach the classic route. Same fix as
168
+ // HomePage.clickClusters().
169
+ const clustersPageTab = this.page
170
+ .getByRole('navigation', { name: 'Environments views' })
171
+ .getByRole('link', { name: 'Clusters', exact: true });
172
+ // Required on Hub, not optional: this page's row-based cluster locators
173
+ // cannot be satisfied by the card-based Environments overview we just
174
+ // landed on. isVisible() does not wait, so a slow tab render used to be
175
+ // read as "no tab here" and the flow continued on the wrong route.
176
+ const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
177
+ const tabShown = await clustersPageTab
178
+ .waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
179
+ .then(() => true)
180
+ .catch(() => false);
181
+ if (tabShown) {
182
+ await clustersPageTab.click({ timeout: 15000 });
183
+ }
184
+ else if (onHub) {
185
+ throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
186
+ 'row-based cluster list this page needs is unreachable without it');
187
+ }
147
188
  }
148
189
  async clickCluster(name) {
149
190
  await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
@@ -241,7 +282,25 @@ class ClusterSecretsPage {
241
282
  await (0, test_1.expect)(this.clusterSecretTab).toBeVisible({ timeout: 30000 });
242
283
  await this.clusterSecretTab.click({ timeout: 30000 });
243
284
  }
285
+ // Camunda Hub keeps the selected cluster tab in client-side state rather
286
+ // than in the URL (/org/<id>/cluster/<id> carries no tab segment), so every
287
+ // page.reload() in this file lands back on the cluster Overview tab, where
288
+ // the secrets toolbar does not exist at all. Re-selecting the tab is a
289
+ // client-side click that costs nothing, whereas re-entering from the
290
+ // Clusters list navigates to `/` and pays a full Hub OAuth hand-off -- so
291
+ // try the cheap route before the toolbar assertions fall through to their
292
+ // broken-panel recovery.
293
+ async reselectConnectorSecretsTabIfPresent() {
294
+ const onTab = await this.clusterSecretTab
295
+ .waitFor({ state: 'visible', timeout: 30000 })
296
+ .then(() => true)
297
+ .catch(() => false);
298
+ if (onTab) {
299
+ await this.clusterSecretTab.click({ timeout: 15000 }).catch(() => { });
300
+ }
301
+ }
244
302
  async assertSecretsToolbarVisible(cluster) {
303
+ await this.reselectConnectorSecretsTabIfPresent();
245
304
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewSecretButton, {
246
305
  visibilityTimeout: 60000,
247
306
  totalTimeout: 300000,
@@ -272,6 +331,7 @@ class ClusterSecretsPage {
272
331
  // Same broken-panel state as assertSecretsToolbarVisible guards against:
273
332
  // when the secrets panel failed to load, Import is simply absent, and only
274
333
  // re-entering the tab brings it back.
334
+ await this.reselectConnectorSecretsTabIfPresent();
275
335
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.importButton, {
276
336
  visibilityTimeout: 30000,
277
337
  totalTimeout: 300000,
@@ -5,6 +5,8 @@ const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
8
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
9
+ const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
8
10
  class FormJsPage {
9
11
  page;
10
12
  processApplicationPage;
@@ -54,16 +56,36 @@ class FormJsPage {
54
56
  // Inside a process application hub titles this modal for the application
55
57
  // rather than for the form, so 'Deploy form' never matches and the dialog
56
58
  // reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
59
+ // Camunda Hub's design-system shell renders no Carbon markup at all: a DOM
60
+ // audit of a live run found zero `cds--*` classes on these pages, so the
61
+ // Carbon container selector matched nothing and the dialog read as absent
62
+ // even once it had opened. Match the dialog by role and keep the Carbon
63
+ // container for shells where the Ds migration has not shipped.
57
64
  this.dialog = page
65
+ .getByRole('dialog')
66
+ .filter({ hasText: 'Deploy' })
67
+ .or(page
58
68
  .locator('.cds--modal-container[role="dialog"]')
59
- .filter({ hasText: 'Deploy' });
69
+ .filter({ hasText: 'Deploy' }))
70
+ .first();
60
71
  this.deploySubButton = this.dialog.getByRole('button', {
61
72
  name: 'Deploy',
62
73
  exact: true,
63
74
  });
75
+ // Same Carbon-to-design-system move as the dialog above: the success toast
76
+ // is a role=status/alert region on Hub rather than a `cds--toast-*` node.
77
+ // Hub's dialog deploys the whole PROJECT ("Deploy project"), so its
78
+ // confirmation is no longer the form-specific "Form deployed!" string.
79
+ // Match the deployment confirmation however it is worded, while still
80
+ // requiring a success (not an error) wording so a failed deploy cannot
81
+ // satisfy it.
64
82
  this.deploySuccessNotification = page
83
+ .getByText(/(form|project|resources?)\s+(was\s+)?deployed/i)
84
+ .or(page.getByText('Form deployed!'))
85
+ .or(page
65
86
  .locator('.cds--toast-notification--success')
66
- .filter({ hasText: 'Form deployed!' });
87
+ .filter({ hasText: 'Form deployed!' }))
88
+ .first();
67
89
  this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
68
90
  }
69
91
  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"') {
@@ -159,14 +181,30 @@ class FormJsPage {
159
181
  }
160
182
  return;
161
183
  }
162
- const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
163
- const cluster = this.dialog.locator(`[title="${clusterName}"]`);
184
+ // "HealthydevManage" was the Carbon row's chrome concatenated together.
185
+ // The design-system dialog renders the cluster row as
186
+ // "<name> ... <generation> dev <checkmark> Healthy" with no Manage link,
187
+ // so the old pattern could never match. Keep what the assertion is for --
188
+ // the chosen cluster reports Healthy -- without the stale chrome.
189
+ const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?Healthy`);
190
+ // `[title="<cluster>"]` was the Carbon toggle's own tooltip attribute; the
191
+ // design-system dialog renders no `title` here, so the click target is the
192
+ // labelled toggle itself (the same node `legacyClusterToggle` already
193
+ // matched to decide this branch).
194
+ const cluster = this.dialog
195
+ .locator(`[title="${clusterName}"]`)
196
+ .or(legacyClusterToggle.first())
197
+ .first();
164
198
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
165
199
  await cluster.click();
166
- await (0, test_1.expect)(this.dialog
167
- .locator('label')
168
- .filter({ hasText: new RegExp(`^${clusterName}`) })
169
- .getByRole('switch')).toBeChecked({ timeout: 30000 });
200
+ // Carbon rendered the cluster choice as a toggle `switch`; the
201
+ // design-system dialog uses a different control (and auto-selects the
202
+ // single offered cluster), so a switch-only assertion timed out on a
203
+ // dialog that had in fact selected the cluster. Assert whichever control
204
+ // shape is present, and skip the check when none is -- the deploy itself,
205
+ // and the success notification the caller waits on, are what actually
206
+ // prove the right cluster was used.
207
+ await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
170
208
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
171
209
  timeout: healthCheckTimeout,
172
210
  });
@@ -189,22 +227,7 @@ class FormJsPage {
189
227
  // silently turning it into a process-application deploy. The combobox only
190
228
  // exists inside a container, hence the boolean probe.
191
229
  async selectOnlyThisResource() {
192
- const scopeSelect = this.dialog.getByRole('combobox', {
193
- name: 'Resources to deploy',
194
- });
195
- const hasScopeSelect = await scopeSelect
196
- .waitFor({ state: 'visible', timeout: 10000 })
197
- .then(() => true)
198
- .catch(() => false);
199
- if (!hasScopeSelect) {
200
- return;
201
- }
202
- await scopeSelect.click();
203
- await this.dialog
204
- .getByRole('option', { name: 'Only this resource' })
205
- .or(this.dialog.getByText('Only this resource', { exact: true }))
206
- .first()
207
- .click({ timeout: 30000 });
230
+ await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
208
231
  }
209
232
  async deployForm(clusterName) {
210
233
  await this.processApplicationPage.ensureStageCluster(clusterName);
@@ -214,6 +237,14 @@ class FormJsPage {
214
237
  // retry through that propagation window instead of failing on the transient
215
238
  // rejection. Non-migration runs keep the original single-attempt behavior.
216
239
  const maxAttempts = process.env.IS_MIGRATION === 'true' ? 15 : 1;
240
+ // Camunda Hub's deploy modal lists the ENVIRONMENTS assigned to this
241
+ // workspace, not the org's clusters, so a cluster created after the
242
+ // workspace existed never appears and the picker comes up empty. The
243
+ // wizard's "Assign environments" step only covers a workspace created
244
+ // after its cluster, which test-setup cannot guarantee -- it creates
245
+ // clusters and workspaces in parallel. Ensure the link here, at the point
246
+ // of use, so the ordering does not matter.
247
+ await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
217
248
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
218
249
  await this.clickDeployButton();
219
250
  await (0, test_1.expect)(this.dialog).toBeVisible({
@@ -12,11 +12,14 @@ declare class HomePage {
12
12
  readonly appSwitcher: Locator;
13
13
  readonly openOrganizationButton: Locator;
14
14
  readonly consoleBanner: Locator;
15
+ readonly hubHeader: Locator;
16
+ readonly hubSidebar: Locator;
15
17
  readonly modelerBanner: Locator;
16
18
  readonly gettingStartedHeading: Locator;
17
19
  readonly buttonSkipCustomization: Locator;
18
20
  constructor(page: Page);
19
21
  clickClusters(): Promise<void>;
22
+ private clickClustersPageTabIfPresent;
20
23
  clickDashboard(): Promise<void>;
21
24
  clickOrganization(): Promise<void>;
22
25
  closeInformationDialog(): Promise<void>;