@camunda/e2e-test-suite 0.0.1249 → 0.0.1250

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.
@@ -15,8 +15,12 @@ declare class AppsPage {
15
15
  readonly consoleLink: Locator;
16
16
  readonly camundaComponentsButton: Locator;
17
17
  readonly camundaAppsButton: Locator;
18
+ readonly hubSidebar: Locator;
18
19
  constructor(page: Page);
19
20
  clickCluster(component: Locator, name: string): Promise<void>;
21
+ private expandHubClusterGroup;
22
+ private clickHubClusterApp;
23
+ isHubClusterAppPresent(clusterName: string, appName: string): Promise<boolean>;
20
24
  clickModeler(): Promise<void>;
21
25
  clickTasklist(clusterName: string): Promise<void>;
22
26
  private doClickClusterInOperate;
@@ -26,8 +30,8 @@ declare class AppsPage {
26
30
  clickOptimize(clusterName: string): Promise<void>;
27
31
  clickCamundaApps(maxRetries?: number): Promise<void>;
28
32
  clickConsoleLink(): Promise<void>;
29
- assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
30
- assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
31
- assertOptimizePresent(shouldBeVisible?: boolean): Promise<void>;
33
+ assertOperatePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
34
+ assertTasklistPresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
35
+ assertOptimizePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
32
36
  }
33
37
  export { AppsPage };
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AppsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
+ function escapeRegExp(value) {
7
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ }
9
+ function isHubUrl(url) {
10
+ return new URL(url).hostname.startsWith('hub.');
11
+ }
6
12
  class AppsPage {
7
13
  page;
8
14
  modelerLink;
@@ -19,12 +25,17 @@ class AppsPage {
19
25
  consoleLink;
20
26
  camundaComponentsButton;
21
27
  camundaAppsButton;
28
+ hubSidebar;
22
29
  constructor(page) {
23
30
  this.page = page;
24
31
  this.modelerLink = page
25
32
  .getByRole('link', { name: 'Modeler', exact: true })
26
33
  .or(page.getByLabel('App Panel').getByLabel('Camunda Modeler'))
27
34
  .first();
35
+ // Camunda Hub's persistent "Main navigation" sidebar lists each cluster
36
+ // with its own apps (Operate/Tasklist/Optimize/...) as child links --
37
+ // there is no per-app switcher panel to open on Hub (see clickCamundaApps).
38
+ this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
28
39
  this.appSwitcherButton = page.getByLabel('App Switcher');
29
40
  this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
30
41
  this.operateLink = page.getByRole('link', { name: 'Operate', exact: true });
@@ -59,7 +70,130 @@ class AppsPage {
59
70
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
60
71
  await cluster.click();
61
72
  }
73
+ // Camunda Hub's sidebar groups by cluster, not by app: each cluster has its
74
+ // own row with per-app child links (Operate/Tasklist/Optimize/...), the
75
+ // opposite axis from the old Console app-switcher (which grouped by app,
76
+ // with a cluster picked underneath it -- see doClickClusterInOperate and
77
+ // friends below). Expand the cluster's row if its children aren't already
78
+ // showing, then click the named app link inside it.
79
+ // Expands the requested cluster's Hub sidebar group and collapses every
80
+ // other expanded group, so a caller resolving an app link sidebar-wide
81
+ // (see clickHubClusterApp and isHubClusterAppPresent below) gets at most
82
+ // one match, scoped to this cluster.
83
+ async expandHubClusterGroup(clusterName) {
84
+ // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
85
+ // shape: "the design system splits the group row into a label control
86
+ // and a chevron control, both labelled by the cluster name; either one
87
+ // opens it" -- there is no separate "Toggle <name>" control, and the
88
+ // child links do not exist in the DOM at all until expanded (not merely
89
+ // hidden), nor are they nested under any container matchable by cluster
90
+ // name once expanded. So: find the toggle by the cluster's own name
91
+ // (`.first()` because both controls share that name), click it, then
92
+ // look up the app link sidebar-wide rather than scoped to a "group"
93
+ // container that doesn't reliably exist.
94
+ // Check THIS cluster's own toggle state, not "is any app link with this
95
+ // name visible anywhere in the sidebar" -- with more than one cluster in
96
+ // the org, a DIFFERENT cluster's group can already be expanded (left over
97
+ // from an earlier action on the same page), and that cluster's own
98
+ // Operate/Tasklist/Optimize link would satisfy an unscoped visibility
99
+ // check, skipping expansion of the requested cluster entirely and then
100
+ // clicking the wrong cluster's link. The child links are not nested under
101
+ // any container matchable by cluster name once expanded (confirmed
102
+ // against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
103
+ // 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();
107
+ const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
108
+ if (!alreadyExpanded) {
109
+ // This is the only expansion action in the Hub path -- swallowing its
110
+ // failure made a broken expansion look successful and left the caller
111
+ // to burn the full 30s below waiting on an app link that could never
112
+ // appear, hiding the actionable error (e.g. a toggle that doesn't
113
+ // exist or isn't clickable) behind an unrelated timeout.
114
+ await toggle.click({ timeout: 10000 });
115
+ }
116
+ // An app link resolved after this is sidebar-wide (see callers below), so
117
+ // if an EARLIER call in the same test left a different cluster's group
118
+ // expanded too, two "Operate" (say) links now exist and that becomes a
119
+ // strict-mode violation. Collapse every other expanded group before
120
+ // resolving it -- there is no container to scope the link to instead,
121
+ // since the child links aren't nested under anything matchable by
122
+ // cluster name once expanded.
123
+ // hasNotText filters DOM text content, not the accessible name -- the
124
+ // expanded chevron can be labelled via aria-label with no text content
125
+ // of its own, in which case hasNotText would fail to exclude the toggle
126
+ // we just expanded and this loop would immediately collapse it again.
127
+ // Match by accessible name (same algorithm getByRole's own name option
128
+ // uses) instead, excluding an exact match on clusterName.
129
+ const otherExpandedToggles = this.hubSidebar.getByRole('button', {
130
+ expanded: true,
131
+ name: new RegExp(`^(?!${escapeRegExp(clusterName)}$).+$`),
132
+ });
133
+ for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
134
+ // Let a failed collapse throw rather than swallowing it: the app link
135
+ // is intentionally sidebar-wide, so a group left expanded here produces
136
+ // a strict-mode violation (or the wrong cluster's link) below anyway --
137
+ // failing at the actual cause is more useful than at that symptom.
138
+ await otherExpandedToggles.first().click({ timeout: 10000 });
139
+ }
140
+ }
141
+ async clickHubClusterApp(clusterName, appName) {
142
+ await this.expandHubClusterGroup(clusterName);
143
+ // Not exact: camunda-hub's own nav-shell-ds.spec.saas.tsx asserts this same
144
+ // link with a substring/regex match (`{name: /Operate/}`), not an exact
145
+ // string -- and CI traces confirm why: even after the toggle click
146
+ // succeeds (no error thrown), an exact-match lookup here still times out
147
+ // with "element(s) not found", meaning the rendered link's accessible
148
+ // name is not byte-for-byte "Operate"/"Tasklist"/"Optimize" (likely
149
+ // additional content from the design-system sidebar item, e.g. an icon
150
+ // or badge). The five app names are distinct enough that a substring
151
+ // match carries no ambiguity risk.
152
+ const appLink = this.hubSidebar.getByRole('link', {
153
+ name: appName,
154
+ });
155
+ await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
156
+ await appLink.click({ timeout: 10000 });
157
+ }
158
+ // Checks whether an app link is present inside the requested cluster's Hub
159
+ // sidebar group, without clicking it. Unlike clickHubClusterApp, absence is
160
+ // an expected, valid outcome here -- this is how callers assert per-cluster
161
+ // app authorization (e.g. an Analyst-role user should not see Operate),
162
+ // which on Hub renders as the link never appearing in the expanded group
163
+ // rather than a disabled control -- so this never throws on a miss.
164
+ async isHubClusterAppPresent(clusterName, appName) {
165
+ await this.expandHubClusterGroup(clusterName);
166
+ const appLink = this.hubSidebar.getByRole('link', { name: appName });
167
+ return appLink.isVisible({ timeout: 10000 }).catch(() => false);
168
+ }
62
169
  async clickModeler() {
170
+ // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
171
+ // app switcher. Its home is the Modeler/Workspaces home, but a hostname
172
+ // check alone can't tell whether we're already there -- Hub's cluster
173
+ // pages are on the same hub.* hostname, and a no-op here would leave a
174
+ // caller like console-user-flows.spec.ts (which reaches clickModeler()
175
+ // from a cluster page) stranded with the next locator running against
176
+ // the wrong page. Navigate home through the header logo link instead,
177
+ // same as ModelerHomePage.clickHomeBreadcrumb -- clicking it when
178
+ // already on the Modeler home is a harmless in-SPA no-op.
179
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
180
+ const header = this.page.locator('header[data-slot="app-header"]');
181
+ // The href stays literally "/login" until the client's auth state
182
+ // catches up just after login (camunda-hub NavShellDs.common.tsx) --
183
+ // clicking it in that window would log the test out instead of
184
+ // navigating home, so exclude that href and wait for the authenticated
185
+ // one rather than racing it.
186
+ const homeLink = header
187
+ .getByRole('link', { name: 'Camunda Modeler home' })
188
+ .and(this.page.locator(':not([href$="/login"])'))
189
+ .or(header
190
+ .getByRole('link', { name: 'Camunda Hub home' })
191
+ .and(this.page.locator(':not([href$="/login"])')))
192
+ .first();
193
+ await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
194
+ await homeLink.click({ timeout: 30000 });
195
+ return;
196
+ }
63
197
  const maxRetries = 5;
64
198
  for (let retries = 0; retries < maxRetries; retries++) {
65
199
  try {
@@ -82,6 +216,10 @@ class AppsPage {
82
216
  throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
83
217
  }
84
218
  async clickTasklist(clusterName) {
219
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
220
+ await this.clickHubClusterApp(clusterName, 'Tasklist');
221
+ return;
222
+ }
85
223
  const maxRetries = 5;
86
224
  for (let retries = 0; retries < maxRetries; retries++) {
87
225
  try {
@@ -148,6 +286,10 @@ class AppsPage {
148
286
  await this.clickCluster(this.optimizeListItem, clusterName);
149
287
  }
150
288
  async clickOperate(clusterName) {
289
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
290
+ await this.clickHubClusterApp(clusterName, 'Operate');
291
+ return;
292
+ }
151
293
  const maxRetries = 5;
152
294
  for (let retries = 0; retries < maxRetries; retries++) {
153
295
  try {
@@ -184,6 +326,10 @@ class AppsPage {
184
326
  throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
185
327
  }
186
328
  async clickOptimize(clusterName) {
329
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
330
+ await this.clickHubClusterApp(clusterName, 'Optimize');
331
+ return;
332
+ }
187
333
  const maxRetries = 3;
188
334
  for (let retries = 0; retries < maxRetries; retries++) {
189
335
  try {
@@ -216,6 +362,14 @@ class AppsPage {
216
362
  throw new Error(`Failed to click the optimize link after ${maxRetries} attempts.`);
217
363
  }
218
364
  async clickCamundaApps(maxRetries = 3) {
365
+ // Camunda Hub's rewritten shells (NavShell / NavShellDs) have no
366
+ // app-switcher button to open -- Modeler is already Hub's home area and
367
+ // the other apps are reached per-cluster via the persistent sidebar, not
368
+ // through a global switcher panel. Mirrors the same hostname guard in
369
+ // clickModeler() below.
370
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
371
+ return;
372
+ }
219
373
  await (0, sleep_1.sleep)(2000);
220
374
  const appButtons = [
221
375
  this.camundaComponentsButton,
@@ -248,6 +402,15 @@ class AppsPage {
248
402
  }
249
403
  }
250
404
  async clickConsoleLink() {
405
+ // Camunda Hub folded Console into the same persistent shell as
406
+ // Modeler/home: Console's Dashboard/Clusters/Organization links are just
407
+ // another section of the same "Main navigation" sidebar, not a separate
408
+ // app to switch into. Its header banner (homePage.consoleBanner) is
409
+ // already visible from any Hub page, so there is no navigation action
410
+ // needed here -- mirrors the same hostname guard in clickModeler().
411
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
412
+ return;
413
+ }
251
414
  const maxRetries = 3;
252
415
  for (let retries = 0; retries < maxRetries; retries++) {
253
416
  try {
@@ -274,7 +437,16 @@ class AppsPage {
274
437
  }
275
438
  throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
276
439
  }
277
- async assertOperatePresent(shouldBeVisible = true) {
440
+ // Asserts whether the current user's role grants them Operate access.
441
+ // clusterName is required on Hub (see isHubClusterAppPresent) but unused on
442
+ // the legacy Console app-switcher, which has no per-cluster concept here --
443
+ // the caller's single active cluster is implicit in operateLink/operateButton.
444
+ async assertOperatePresent(clusterName, shouldBeVisible = true) {
445
+ if (isHubUrl(this.page.url())) {
446
+ const present = await this.isHubClusterAppPresent(clusterName, 'Operate');
447
+ (0, test_1.expect)(present).toBe(shouldBeVisible);
448
+ return;
449
+ }
278
450
  const maxRetries = 3;
279
451
  for (let retries = 0; retries < maxRetries; retries++) {
280
452
  try {
@@ -303,7 +475,13 @@ class AppsPage {
303
475
  }
304
476
  throw new Error(`Failed to assert the operate link visibility after ${maxRetries} attempts.`);
305
477
  }
306
- async assertTasklistPresent(shouldBeVisible = true) {
478
+ // See assertOperatePresent for the clusterName/Hub note.
479
+ async assertTasklistPresent(clusterName, shouldBeVisible = true) {
480
+ if (isHubUrl(this.page.url())) {
481
+ const present = await this.isHubClusterAppPresent(clusterName, 'Tasklist');
482
+ (0, test_1.expect)(present).toBe(shouldBeVisible);
483
+ return;
484
+ }
307
485
  const maxRetries = 3;
308
486
  for (let retries = 0; retries < maxRetries; retries++) {
309
487
  try {
@@ -332,7 +510,13 @@ class AppsPage {
332
510
  }
333
511
  throw new Error(`Failed to assert the tasklist link visibility after ${maxRetries} attempts.`);
334
512
  }
335
- async assertOptimizePresent(shouldBeVisible = true) {
513
+ // See assertOperatePresent for the clusterName/Hub note.
514
+ async assertOptimizePresent(clusterName, shouldBeVisible = true) {
515
+ if (isHubUrl(this.page.url())) {
516
+ const present = await this.isHubClusterAppPresent(clusterName, 'Optimize');
517
+ (0, test_1.expect)(present).toBe(shouldBeVisible);
518
+ return;
519
+ }
336
520
  const maxRetries = 3;
337
521
  for (let retries = 0; retries < maxRetries; retries++) {
338
522
  try {
@@ -118,10 +118,30 @@ class ClusterPage {
118
118
  });
119
119
  }
120
120
  async clickClusterBanner() {
121
- await this.page
121
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
122
+ // persistent "Main navigation" sidebar, and once the org has environments
123
+ // enabled that link is further renamed "Environments" (same rollout
124
+ // HomePage.clusterTab handles).
125
+ const clusterLink = this.page
122
126
  .getByRole('banner')
123
127
  .getByRole('link', { name: 'Clusters' })
124
- .click({ timeout: 60000 });
128
+ .or(this.page
129
+ .getByRole('navigation', { name: 'Main navigation' })
130
+ .getByRole('link', { name: 'Clusters', exact: true }))
131
+ .or(this.page
132
+ .getByRole('navigation', { name: 'Main navigation' })
133
+ .getByRole('link', { name: 'Environments', exact: true }));
134
+ await clusterLink.click({ timeout: 60000 });
135
+ // Landing on Hub's Environments overview (see above) renders clusters as
136
+ // cards, not the getByRole('row') table this page's own locators expect
137
+ // -- route through the page's own "Clusters" tab to reach the classic
138
+ // route. Same fix as HomePage.clickClusters().
139
+ const clustersPageTab = this.page
140
+ .getByRole('navigation', { name: 'Environments views' })
141
+ .getByRole('link', { name: 'Clusters', exact: true });
142
+ if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
143
+ await clustersPageTab.click({ timeout: 15000 });
144
+ }
125
145
  }
126
146
  async deleteCluster(name) {
127
147
  const clusterInstance = this.cluster(name).first();
@@ -49,9 +49,19 @@ class ClusterSecretsPage {
49
49
  .getByRole('dialog')
50
50
  .getByRole('button', { name: 'Delete', exact: true });
51
51
  this.dialog = page.getByRole('dialog');
52
+ // Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
53
+ // persistent "Main navigation" sidebar (same fix as HomePage.clusterTab).
54
+ // Once the org has environments enabled, that sidebar link is further
55
+ // renamed "Environments" -- see HomePage.clusterTab for the full context.
52
56
  this.clusterBanner = this.page
53
57
  .getByRole('banner')
54
- .getByRole('link', { name: 'Clusters' });
58
+ .getByRole('link', { name: 'Clusters' })
59
+ .or(this.page
60
+ .getByRole('navigation', { name: 'Main navigation' })
61
+ .getByRole('link', { name: 'Clusters', exact: true }))
62
+ .or(this.page
63
+ .getByRole('navigation', { name: 'Main navigation' })
64
+ .getByRole('link', { name: 'Environments', exact: true }));
55
65
  // exact: true because console-user-flows.spec.ts creates a second cluster
56
66
  // called "UI Test Cluster" in the same org -- a substring match on
57
67
  // "Test Cluster" then resolves to both links and fails on strict mode.
@@ -158,6 +168,17 @@ class ClusterSecretsPage {
158
168
  await this.dismissOpenModal();
159
169
  await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
160
170
  await this.clusterBanner.click({ timeout: 60000 });
171
+ // Landing on Hub's Environments overview (see clusterBanner above) renders
172
+ // clusters as cards, not the getByRole('link') the clusterLink() locator
173
+ // below expects to find in a row -- route through the page's own
174
+ // "Clusters" tab to reach the classic route. Same fix as
175
+ // HomePage.clickClusters().
176
+ const clustersPageTab = this.page
177
+ .getByRole('navigation', { name: 'Environments views' })
178
+ .getByRole('link', { name: 'Clusters', exact: true });
179
+ if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
180
+ await clustersPageTab.click({ timeout: 15000 });
181
+ }
161
182
  }
162
183
  async clickCluster(name) {
163
184
  await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
@@ -15,6 +15,8 @@ declare class HomePage {
15
15
  readonly modelerBanner: Locator;
16
16
  readonly gettingStartedHeading: Locator;
17
17
  readonly buttonSkipCustomization: Locator;
18
+ readonly hubSidebar: Locator;
19
+ readonly hubHeader: Locator;
18
20
  constructor(page: Page);
19
21
  clickClusters(): Promise<void>;
20
22
  clickDashboard(): Promise<void>;
@@ -18,12 +18,39 @@ class HomePage {
18
18
  modelerBanner;
19
19
  gettingStartedHeading;
20
20
  buttonSkipCustomization;
21
+ hubSidebar;
22
+ hubHeader;
21
23
  constructor(page) {
22
24
  this.page = page;
23
25
  this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
26
+ // Camunda Hub (which replaced Console on SaaS) moved Clusters from the
27
+ // header into its "Main navigation" sidebar.
28
+ this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
29
+ // Structural signal that Hub's authenticated shell has mounted, used as a
30
+ // fallback below: the header logo link's accessible name AND href both
31
+ // depend on client state (aria-label toggles on the workspacesEnabled
32
+ // flag; href is literally `/login` until `isAuthenticated` flips true --
33
+ // camunda-hub NavShellDs.common.tsx) that can lag just after login,
34
+ // making a name+href-exclusion match miss the timing window. The header
35
+ // only renders the user menu once authenticated, so this never races.
36
+ this.hubHeader = page
37
+ .locator('header[data-slot="app-header"]')
38
+ .filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
39
+ // camunda-hub's use-ds-sidebar-items.tsx: "Environments supersede the
40
+ // older Clusters surface: once the environments feature is on, the
41
+ // workspace nav drops Clusters entirely for every role." The sidebar
42
+ // link is renamed "Environments" (not merely relabelled -- "Clusters"
43
+ // stops rendering at all), but it still lists the same clusters by name
44
+ // underneath, so callers of clickClusters() need no other change.
24
45
  this.clusterTab = page
25
46
  .getByRole('banner')
26
- .getByRole('link', { name: 'Clusters' });
47
+ .getByRole('link', { name: 'Clusters' })
48
+ .or(this.hubSidebar.getByRole('link', { name: 'Clusters', exact: true }))
49
+ .or(this.hubSidebar.getByRole('link', {
50
+ name: 'Environments',
51
+ exact: true,
52
+ }))
53
+ .first();
27
54
  this.organizationTab = page.getByRole('link', { name: 'Organization' });
28
55
  this.organizationViewTitle = page.getByText('Organization Management');
29
56
  this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
@@ -32,17 +59,43 @@ class HomePage {
32
59
  });
33
60
  this.camundaAppsButton = page.getByLabel('Camunda apps');
34
61
  this.appSwitcher = page.getByLabel('App Switcher');
62
+ // Camunda Hub's rewritten shells (NavShell / NavShellDs) dropped the
63
+ // app-switcher dropdown entirely in favor of a persistent
64
+ // "Main navigation" sidebar (see hubSidebar above) -- there is no
65
+ // "Camunda components"/"Camunda apps"/"App Switcher" element to find on
66
+ // those shells. Accept the sidebar too so this locator resolves whichever
67
+ // shell the org's live rollout has reached.
35
68
  this.camundaComponentsButton = page
36
69
  .getByLabel('Camunda components')
37
70
  .or(this.camundaAppsButton)
38
- .or(this.appSwitcher);
71
+ .or(this.appSwitcher)
72
+ .or(this.hubSidebar);
39
73
  this.openOrganizationButton = page.getByLabel('Open Organizations');
40
- this.consoleBanner = page.getByRole('link', {
74
+ // Camunda Hub replaced the Console shell on SaaS; its header logo link is
75
+ // "Camunda Modeler home" on every page, Console pages included -- and, as
76
+ // of camunda-hub PR#29349, "Camunda Hub home" once workspaces are enabled
77
+ // for the org. Accept either name so login doesn't depend on which one
78
+ // this org's live rollout has reached.
79
+ this.consoleBanner = page
80
+ .getByRole('link', {
41
81
  name: 'Camunda logo Console',
42
- });
43
- this.modelerBanner = page.getByRole('banner', {
82
+ })
83
+ .or(page
84
+ .getByRole('link', { name: 'Camunda Modeler home' })
85
+ .and(page.locator(':not([href$="/login"])')))
86
+ .or(page
87
+ .getByRole('link', { name: 'Camunda Hub home' })
88
+ .and(page.locator(':not([href$="/login"])')))
89
+ .or(this.hubHeader)
90
+ .first();
91
+ this.modelerBanner = page
92
+ .getByRole('banner', {
44
93
  name: 'Camunda Modeler',
45
- });
94
+ })
95
+ .or(page
96
+ .getByRole('link', { name: 'Camunda Hub home' })
97
+ .and(page.locator(':not([href$="/login"])')))
98
+ .or(this.hubHeader);
46
99
  this.gettingStartedHeading = page.getByText('start by creating a project');
47
100
  this.buttonSkipCustomization = page.getByRole('button', {
48
101
  name: 'Skip customization',
@@ -51,6 +104,17 @@ class HomePage {
51
104
  async clickClusters() {
52
105
  await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 15000 });
53
106
  await this.clusterTab.click({ timeout: 15000 });
107
+ // Landing on Hub's Environments overview (see clusterTab above) doesn't
108
+ // put us on the classic row-based cluster list ClusterPage.ts expects --
109
+ // that page renders clusters as cards. Its own header renders a separate
110
+ // "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
111
+ // that does.
112
+ const clustersPageTab = this.page
113
+ .getByRole('navigation', { name: 'Environments views' })
114
+ .getByRole('link', { name: 'Clusters', exact: true });
115
+ if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
116
+ await clustersPageTab.click({ timeout: 15000 });
117
+ }
54
118
  }
55
119
  async clickDashboard() {
56
120
  await this.dashboardTab.click({ timeout: 90000 });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
6
7
  class LoginPage {
7
8
  page;
8
9
  usernameInput;
@@ -52,6 +53,10 @@ class LoginPage {
52
53
  }
53
54
  async login(credentials = {}) {
54
55
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
56
+ // See loginWithTestUser below: this re-authenticates the same page and
57
+ // must clear the Hub bearer-token cache too, or getUserToken() can
58
+ // return whatever user was previously logged in on it.
59
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
55
60
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 60000 });
56
61
  await this.fillUsername(username);
57
62
  await this.clickContinueButton();
@@ -63,6 +68,12 @@ class LoginPage {
63
68
  }
64
69
  async loginWithTestUser(credentials = {}) {
65
70
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
71
+ // See loginWithRetry in UtilitiesPage.ts: the Hub bearer-token cache in
72
+ // consoleApiHelpers.ts is keyed by Page for the tab's whole lifetime, so
73
+ // this re-authentication (a different user than whatever was logged in
74
+ // before) must clear it too, or getUserToken() can return the previous
75
+ // user's cached token.
76
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
66
77
  // Navigate to app root to start the auth flow.
67
78
  await this.page.goto('/');
68
79
  await this.page
@@ -112,6 +123,10 @@ class LoginPage {
112
123
  }
113
124
  async loginWithoutOrgAssertion(credentials = {}) {
114
125
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
126
+ // See loginWithTestUser above: this re-authenticates the same page and
127
+ // must clear the Hub bearer-token cache too, or getUserToken() can
128
+ // return whatever user was previously logged in on it.
129
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
115
130
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
116
131
  await this.fillUsername(username);
117
132
  await this.clickContinueButton();
@@ -14,8 +14,12 @@ declare class ModelerCreatePage {
14
14
  readonly autosaveIndicator: Locator;
15
15
  readonly nameInput: Locator;
16
16
  readonly diagramBreadcrumb: Locator;
17
+ readonly diagramActionsButton: Locator;
17
18
  readonly renameDiagramNameButton: Locator;
18
19
  readonly diagramNameInput: Locator;
20
+ readonly renameDialog: Locator;
21
+ readonly renameDialogInput: Locator;
22
+ readonly renameDialogSubmitButton: Locator;
19
23
  readonly variableInput: Locator;
20
24
  readonly embedFormButton: Locator;
21
25
  readonly embedButton: Locator;
@@ -20,8 +20,12 @@ class ModelerCreatePage {
20
20
  autosaveIndicator;
21
21
  nameInput;
22
22
  diagramBreadcrumb;
23
+ diagramActionsButton;
23
24
  renameDiagramNameButton;
24
25
  diagramNameInput;
26
+ renameDialog;
27
+ renameDialogInput;
28
+ renameDialogSubmitButton;
25
29
  variableInput;
26
30
  embedFormButton;
27
31
  embedButton;
@@ -154,8 +158,26 @@ class ModelerCreatePage {
154
158
  this.autosaveIndicator = page.locator('[data-test="autosave"]');
155
159
  this.nameInput = page.getByLabel('Name', { exact: true });
156
160
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
161
+ // Hub's newer nav (isNavV2: the design-system shell or C3NavigationV2)
162
+ // makes the legacy TopBar breadcrumb dropdown above a no-op and moves
163
+ // Rename/Duplicate/Delete behind a separate kebab button next to it,
164
+ // labeled "<entity name> actions" (use-file-page-actions.ts). Its accessible
165
+ // name always ends in " actions", regardless of the current diagram name.
166
+ this.diagramActionsButton = page
167
+ .getByRole('button', { name: / actions$/ })
168
+ .first();
157
169
  this.renameDiagramNameButton = page.getByText('Rename');
158
170
  this.diagramNameInput = page.locator('[data-test="editable-input"]');
171
+ // The design-system nav's Rename action opens a modal dialog
172
+ // (RenameDialogDs.tsx) instead of the legacy inline breadcrumb editor.
173
+ this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
174
+ this.renameDialogInput = this.renameDialog.getByLabel('Name', {
175
+ exact: true,
176
+ });
177
+ this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
178
+ name: 'Rename',
179
+ exact: true,
180
+ });
159
181
  this.variableInput = page.locator('textarea#variables-json');
160
182
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
161
183
  this.embedButton = page.locator('[data-test="confirm-move"]');
@@ -673,7 +695,19 @@ class ModelerCreatePage {
673
695
  return;
674
696
  }
675
697
  try {
676
- await this.diagramBreadcrumb.click({ timeout: 20000 });
698
+ // Hub's newer nav (isNavV2) makes this legacy breadcrumb click a
699
+ // no-op and moves the dropdown behind a separate "<name> actions"
700
+ // kebab instead -- prefer it when present, and only fall back to the
701
+ // legacy breadcrumb when it is not (see diagramActionsButton).
702
+ const kebabVisible = await this.diagramActionsButton
703
+ .isVisible({ timeout: 5000 })
704
+ .catch(() => false);
705
+ if (kebabVisible) {
706
+ await this.diagramActionsButton.click({ timeout: 20000 });
707
+ }
708
+ else {
709
+ await this.diagramBreadcrumb.click({ timeout: 20000 });
710
+ }
677
711
  await this.renameDiagramNameButton.waitFor({
678
712
  state: 'visible',
679
713
  timeout: 20000,
@@ -726,7 +760,9 @@ class ModelerCreatePage {
726
760
  // failure: the assertion below is what decides whether this worked.
727
761
  await this.renameDiagramNameButton.dispatchEvent('click');
728
762
  }
729
- await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
763
+ // The design-system nav's "Rename" opens a modal dialog
764
+ // (RenameDialogDs.tsx) instead of the legacy inline breadcrumb editor.
765
+ await (0, test_1.expect)(this.diagramNameInput.or(this.renameDialogInput)).toBeVisible({ timeout: 10000 });
730
766
  }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
731
767
  }
732
768
  async enterDiagramName(name) {
@@ -740,9 +776,27 @@ class ModelerCreatePage {
740
776
  const maxAttempts = 3;
741
777
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
742
778
  try {
743
- await this.diagramNameInput.fill(name, { timeout: 45000 });
744
- await this.diagramNameInput.press('Enter', { timeout: 45000 });
745
- await this.diagramNameBreadcrumb(name).waitFor({
779
+ // The design-system nav's Rename opens a modal dialog
780
+ // (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
781
+ // editor -- fill and submit whichever is actually open.
782
+ const modalOpen = await this.renameDialog
783
+ .isVisible()
784
+ .catch(() => false);
785
+ if (modalOpen) {
786
+ await this.renameDialogInput.fill(name, { timeout: 45000 });
787
+ await this.renameDialogSubmitButton.click({ timeout: 10000 });
788
+ await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
789
+ }
790
+ else {
791
+ await this.diagramNameInput.fill(name, { timeout: 45000 });
792
+ await this.diagramNameInput.press('Enter', { timeout: 45000 });
793
+ }
794
+ // The legacy breadcrumb shows the new name directly; the
795
+ // design-system kebab instead embeds it in its own accessible name
796
+ // ("<name> actions"). Accept either as confirmation the rename took.
797
+ await this.diagramNameBreadcrumb(name)
798
+ .or(this.page.getByRole('button', { name: `${name} actions` }))
799
+ .waitFor({
746
800
  state: 'visible',
747
801
  timeout: 30000,
748
802
  });
@@ -2,12 +2,18 @@ import { Page, Locator } from '@playwright/test';
2
2
  import { LoginPage } from './LoginPage';
3
3
  declare class ModelerHomePage {
4
4
  private defaultFolderName;
5
+ private defaultClusterName;
5
6
  private page;
6
7
  private processApplicationPage;
7
8
  private processApplicationUrl?;
8
9
  private processApplicationName?;
9
10
  readonly modelerPageBanner: Locator;
10
11
  readonly createNewProjectButton: Locator;
12
+ readonly createWorkspaceButton: Locator;
13
+ readonly workspaceWizardNameInput: Locator;
14
+ readonly workspaceWizardSubmitButton: Locator;
15
+ readonly workspaceWizardMembersStep: Locator;
16
+ readonly workspaceWizardEnvironmentsHeading: Locator;
11
17
  readonly projectNameInput: Locator;
12
18
  readonly formNameInput: Locator;
13
19
  readonly chooseBpmnTemplateButton: Locator;
@@ -31,7 +37,11 @@ declare class ModelerHomePage {
31
37
  readonly closeButton: Locator;
32
38
  readonly formNameBreadcrumb: (formName: string) => Locator;
33
39
  readonly formBreadcrumbButton: Locator;
40
+ readonly formActionsButton: Locator;
34
41
  readonly renameFormMenuItem: Locator;
42
+ readonly renameDialog: Locator;
43
+ readonly renameDialogInput: Locator;
44
+ readonly renameDialogSubmitButton: Locator;
35
45
  constructor(page: Page);
36
46
  dismissOverlays(): Promise<void>;
37
47
  clickCreateNewProjectButton(): Promise<void>;
@@ -63,6 +73,7 @@ declare class ModelerHomePage {
63
73
  clickHomeBreadcrumb(): Promise<void>;
64
74
  clickOpenOrganizationsButton(): Promise<void>;
65
75
  clickCrossComponentProjectFolder(): Promise<void>;
76
+ private createWorkspaceViaWizard;
66
77
  createCrossComponentProjectFolder(): Promise<void>;
67
78
  /**
68
79
  * Asserts the Cross Component project folder is listed on the Modeler home
@@ -9,6 +9,11 @@ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
10
10
  class ModelerHomePage {
11
11
  defaultFolderName = 'Cross Component Test Project';
12
+ // The default cluster test-setup.spec.ts's "Create Default Cluster" test
13
+ // provisions for the whole suite -- assigned to the shared workspace's
14
+ // Environments step so downstream specs can deploy to it without a manual
15
+ // assignment step first.
16
+ defaultClusterName = 'Test Cluster';
12
17
  page;
13
18
  // Since camunda/camunda-hub#25831 files and folders cannot sit at a
14
19
  // workspace ("project" in today's UI) root -- only process applications and
@@ -25,6 +30,11 @@ class ModelerHomePage {
25
30
  processApplicationName;
26
31
  modelerPageBanner;
27
32
  createNewProjectButton;
33
+ createWorkspaceButton;
34
+ workspaceWizardNameInput;
35
+ workspaceWizardSubmitButton;
36
+ workspaceWizardMembersStep;
37
+ workspaceWizardEnvironmentsHeading;
28
38
  projectNameInput;
29
39
  formNameInput;
30
40
  chooseBpmnTemplateButton;
@@ -48,13 +58,31 @@ class ModelerHomePage {
48
58
  closeButton;
49
59
  formNameBreadcrumb;
50
60
  formBreadcrumbButton;
61
+ formActionsButton;
51
62
  renameFormMenuItem;
63
+ renameDialog;
64
+ renameDialogInput;
65
+ renameDialogSubmitButton;
52
66
  constructor(page) {
53
67
  this.page = page;
54
68
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
55
- this.modelerPageBanner = page.getByRole('banner', {
69
+ // Hub's design-system shell renders the app header as a role=banner with no
70
+ // accessible name after the Camunda Hub rebrand, so the classic
71
+ // name-matched banner no longer resolves; the Modeler logo link inside it
72
+ // carries "Camunda Modeler home" or, once camunda-hub PR#29349's rename
73
+ // has rolled out to an org, "Camunda Hub home". Keep the legacy
74
+ // named-banner branch for shells where the rename has not shipped yet.
75
+ this.modelerPageBanner = page
76
+ .getByRole('banner', {
56
77
  name: 'Camunda Modeler',
57
- });
78
+ })
79
+ .or(page
80
+ .getByRole('link', { name: 'Camunda Modeler home' })
81
+ .and(page.locator(':not([href$="/login"])')))
82
+ .or(page
83
+ .getByRole('link', { name: 'Camunda Hub home' })
84
+ .and(page.locator(':not([href$="/login"])')))
85
+ .first();
58
86
  // Hub renamed this button to "Create project" on both the classic and the
59
87
  // design-system shell. The legacy label is kept so the flow still works
60
88
  // against Modeler builds where the rename has not shipped yet. exact:true
@@ -67,6 +95,24 @@ class ModelerHomePage {
67
95
  name: 'Create project',
68
96
  exact: true,
69
97
  }));
98
+ // Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
99
+ // hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
100
+ // wizard (General -> Members) that names the workspace up front. Same
101
+ // hooks as pages/8.10/ModelerHomePage.ts. exact:true keeps the split
102
+ // button's options chevron out of the match.
103
+ this.createWorkspaceButton = page.getByRole('button', {
104
+ name: 'Create workspace',
105
+ exact: true,
106
+ });
107
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
108
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
109
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
110
+ // When the org has environments enabled, an "Assign environments" step is
111
+ // inserted after Members -- camunda-hub's own
112
+ // e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
113
+ this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
114
+ name: 'Assign environments',
115
+ });
70
116
  this.projectNameInput = page.locator('[data-test="editable-input"]');
71
117
  this.formNameInput = page.locator('[data-test="editable-input"]');
72
118
  this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
@@ -98,10 +144,13 @@ class ModelerHomePage {
98
144
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
99
145
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
100
146
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
147
+ // On the Camunda Hub Workspaces overview the folder is a workspace tile
148
+ // whose "Open workspace <name>" button navigates into it.
101
149
  this.crossComponentProjectFolder = page
102
- .getByTitle(this.defaultFolderName, {
150
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
151
+ .or(page.getByTitle(this.defaultFolderName, {
103
152
  exact: true,
104
- })
153
+ }))
105
154
  .first();
106
155
  this.rows = page.getByRole('row');
107
156
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
@@ -116,7 +165,26 @@ class ModelerHomePage {
116
165
  hasText: formName,
117
166
  });
118
167
  this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
119
- this.renameFormMenuItem = page.locator('[data-test="rename-form"]');
168
+ // Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
169
+ // Rename/Duplicate/Delete behind a separate "<name> actions" kebab
170
+ // instead (use-file-page-actions.ts); its own menu item carries no
171
+ // data-test, just the text "Rename".
172
+ this.formActionsButton = page
173
+ .getByRole('button', { name: / actions$/ })
174
+ .first();
175
+ this.renameFormMenuItem = page
176
+ .locator('[data-test="rename-form"]')
177
+ .or(page.getByRole('menuitem', { name: 'Rename', exact: true }));
178
+ // The design-system nav's Rename opens a modal dialog (RenameDialogDs.tsx)
179
+ // instead of the legacy inline breadcrumb editor.
180
+ this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
181
+ this.renameDialogInput = this.renameDialog.getByLabel('Name', {
182
+ exact: true,
183
+ });
184
+ this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
185
+ name: 'Rename',
186
+ exact: true,
187
+ });
120
188
  }
121
189
  // The release-announcement dialog ("Check out our new release") re-mounts
122
190
  // after the projects list re-renders, so one dismissal pass is not enough.
@@ -300,10 +368,28 @@ class ModelerHomePage {
300
368
  if (!editorOpen) {
301
369
  await this.openFormRenameEditor();
302
370
  }
303
- await this.formNameInput.click({ timeout: 30000 });
304
- await this.formNameInput.fill(name);
305
- await this.formNameInput.press('Enter');
306
- await this.formNameBreadcrumb(name).waitFor({
371
+ // The design-system nav's Rename opens a modal dialog
372
+ // (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
373
+ // editor -- fill and submit whichever is actually open.
374
+ const modalOpen = await this.renameDialog
375
+ .isVisible()
376
+ .catch(() => false);
377
+ if (modalOpen) {
378
+ await this.renameDialogInput.fill(name, { timeout: 45000 });
379
+ await this.renameDialogSubmitButton.click({ timeout: 10000 });
380
+ await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
381
+ }
382
+ else {
383
+ await this.formNameInput.click({ timeout: 30000 });
384
+ await this.formNameInput.fill(name);
385
+ await this.formNameInput.press('Enter');
386
+ }
387
+ // The legacy breadcrumb shows the new name directly; the
388
+ // design-system kebab instead embeds it in its own accessible name
389
+ // ("<name> actions"). Accept either as confirmation the rename took.
390
+ await this.formNameBreadcrumb(name)
391
+ .or(this.page.getByRole('button', { name: `${name} actions` }))
392
+ .waitFor({
307
393
  state: 'visible',
308
394
  timeout: 30000,
309
395
  });
@@ -319,7 +405,18 @@ class ModelerHomePage {
319
405
  }
320
406
  }
321
407
  async openFormRenameEditor() {
322
- await this.formBreadcrumbButton.click({ timeout: 60000 });
408
+ // Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
409
+ // the dropdown behind a separate "<name> actions" kebab instead -- prefer
410
+ // it when present, and only fall back to the legacy breadcrumb otherwise.
411
+ const kebabVisible = await this.formActionsButton
412
+ .isVisible({ timeout: 5000 })
413
+ .catch(() => false);
414
+ if (kebabVisible) {
415
+ await this.formActionsButton.click({ timeout: 60000 });
416
+ }
417
+ else {
418
+ await this.formBreadcrumbButton.click({ timeout: 60000 });
419
+ }
323
420
  await this.renameFormMenuItem.click({ timeout: 30000 });
324
421
  }
325
422
  // Kept under its original name because it is what the existing call sites
@@ -432,6 +529,28 @@ class ModelerHomePage {
432
529
  return this.becameVisible(this.modelerPageBanner, timeout);
433
530
  }
434
531
  async clickHomeBreadcrumb() {
532
+ // Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
533
+ // overview, reached through the header logo link so the SPA keeps its
534
+ // in-memory session (a full navigation re-runs the whole Auth0 hand-off).
535
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
536
+ const header = this.page.locator('header[data-slot="app-header"]');
537
+ // The href stays literally "/login" until the client's auth state
538
+ // catches up just after login (camunda-hub NavShellDs.common.tsx) --
539
+ // clicking it in that window would log the test out instead of
540
+ // navigating home, so exclude that href and wait for the authenticated
541
+ // one rather than racing it (same hazard AppsPage.clickModeler()
542
+ // guards against).
543
+ const homeLink = header
544
+ .getByRole('link', { name: 'Camunda Modeler home' })
545
+ .and(this.page.locator(':not([href$="/login"])'))
546
+ .or(header
547
+ .getByRole('link', { name: 'Camunda Hub home' })
548
+ .and(this.page.locator(':not([href$="/login"])')))
549
+ .first();
550
+ await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
551
+ await homeLink.click({ timeout: 30000 });
552
+ return;
553
+ }
435
554
  try {
436
555
  await this.homeBreadcrumb.click();
437
556
  }
@@ -468,6 +587,57 @@ class ModelerHomePage {
468
587
  },
469
588
  });
470
589
  }
590
+ // Drives Camunda Hub's workspace-creation wizard. The name is entered on
591
+ // the General step, which enables "Next"; the same submit button then
592
+ // relabels itself through the remaining steps and finally reads "Create
593
+ // workspace" once it creates the workspace and routes the creator into its
594
+ // projects listing. Gate the Members-step click on that step becoming
595
+ // active so the shared submit button is not clicked twice while still on
596
+ // General.
597
+ async createWorkspaceViaWizard(name) {
598
+ await this.dismissOverlays();
599
+ await this.createWorkspaceButton.click({ timeout: 60000 });
600
+ await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
601
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
602
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
603
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
604
+ // When the org has environments enabled, this only advances the wizard
605
+ // into an "Assign environments" step instead of submitting. Assign the
606
+ // suite's default cluster here rather than submitting with an empty
607
+ // selection -- otherwise this shared workspace starts with no
608
+ // environments and every downstream spec that deploys to it needs its
609
+ // own manual assignment first. Each card's accessible name is
610
+ // "Select <cluster name>" before it is picked (camunda-hub's own
611
+ // create-workspace-environments.spec.tsx asserts the same pattern).
612
+ // Sampled immediately (isVisible() doesn't wait) since the step change
613
+ // is a synchronous client-side re-render.
614
+ if (await this.workspaceWizardEnvironmentsHeading
615
+ .isVisible()
616
+ .catch(() => false)) {
617
+ // "Create Default Cluster" runs in parallel with this flow
618
+ // (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
619
+ // lists may still be provisioning -- wait for it a generous 90s rather
620
+ // than sampling once. If it genuinely never appears (the cluster
621
+ // failed rather than just being slow), don't fail the whole shared
622
+ // workspace's setup over it: Hub's own recovery copy for this step
623
+ // ("Could not load environments. You can finish creating the
624
+ // workspace and assign them later.") confirms submitting unassigned
625
+ // is a supported, recoverable path, not a broken one.
626
+ const clusterOption = this.page.getByRole('button', {
627
+ name: new RegExp(`^Select ${this.defaultClusterName}`),
628
+ });
629
+ if (await this.becameVisible(clusterOption, 90000)) {
630
+ await clusterOption.click({ timeout: 10000 });
631
+ }
632
+ else {
633
+ console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
634
+ }
635
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
636
+ }
637
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
638
+ timeout: 120000,
639
+ });
640
+ }
471
641
  async createCrossComponentProjectFolder() {
472
642
  await this.dismissOverlays();
473
643
  // The project list can take well over a minute to render after a session
@@ -483,6 +653,10 @@ class ModelerHomePage {
483
653
  await this.clickCrossComponentProjectFolder();
484
654
  return;
485
655
  }
656
+ if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
657
+ await this.createWorkspaceViaWizard(this.defaultFolderName);
658
+ return;
659
+ }
486
660
  await this.clickCreateNewProjectButton();
487
661
  await this.enterNewProjectName(this.defaultFolderName);
488
662
  }
@@ -48,21 +48,36 @@ class ProcessApplicationPage {
48
48
  // .first() defensively only: hub renders exactly one of these today, in the
49
49
  // workspace-root empty state. A future second placement should not turn
50
50
  // every PA-creating test into a strict-mode violation.
51
+ //
52
+ // camunda-hub#25831 renamed the concept from "process application" to
53
+ // "project" (workspace root's CreateSplitButton renders literal text
54
+ // "Create project", opening a dialog titled "Create project"). Accept
55
+ // both names so this resolves whichever the org's live rollout has
56
+ // reached.
51
57
  this.createProcessApplicationButton = page
52
58
  .getByRole('button', { name: 'Create process application' })
59
+ .or(page.getByRole('button', { name: 'Create project' }))
53
60
  .first();
54
61
  this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
55
- this.creationDialog = page.getByRole('dialog', {
56
- name: 'Create a process application',
57
- });
62
+ this.creationDialog = page
63
+ .getByRole('dialog', { name: 'Create a process application' })
64
+ .or(page.getByRole('dialog', { name: 'Create project' }));
58
65
  // Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
59
66
  // getByLabel('Name', {exact: true}) for the properties panel, which would
60
67
  // resolve to two elements while the creation modal is open.
61
- this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
62
- this.submitButton = this.creationDialog.getByRole('button', {
63
- name: 'Create',
68
+ // CreateProjectModal.tsx labels its field "Project name", not "Name" --
69
+ // accept both so this resolves under either dialog variant.
70
+ this.nameInput = this.creationDialog
71
+ .getByLabel('Name', { exact: true })
72
+ .or(this.creationDialog.getByLabel('Project name', { exact: true }));
73
+ // CreateProjectModal.tsx's submit button reads "Create project", not the
74
+ // exact "Create" the legacy dialog used.
75
+ this.submitButton = this.creationDialog
76
+ .getByRole('button', { name: 'Create', exact: true })
77
+ .or(this.creationDialog.getByRole('button', {
78
+ name: 'Create project',
64
79
  exact: true,
65
- });
80
+ }));
66
81
  this.cancelButton = this.creationDialog.getByRole('button', {
67
82
  name: 'Cancel',
68
83
  exact: true,
@@ -72,8 +87,13 @@ class ProcessApplicationPage {
72
87
  // The process application page renders more than one diagram-dropdown
73
88
  // (hub's own unit tests reach for it with getAllByTestId), so the dropdown
74
89
  // has to be resolved inside the folder explorer rather than page-wide.
90
+ // camunda-hub#25831's workspace-projects flow (WorkspaceProjectDetail.tsx)
91
+ // renders this control as CreateActionDs with test-id
92
+ // "create-action-dropdown" instead of the legacy "diagram-dropdown" --
93
+ // accept both.
75
94
  this.createNewDropdown = this.folderExplorer
76
95
  .locator('[data-test="diagram-dropdown"]')
96
+ .or(this.folderExplorer.locator('[data-test="create-action-dropdown"]'))
77
97
  .first();
78
98
  this.inlineNameInput = page.locator('[data-test="editable-input"]');
79
99
  this.defineStagesDialog = page
@@ -7,11 +7,34 @@ const HomePage_1 = require("./HomePage");
7
7
  const sleep_1 = require("../../utils/sleep");
8
8
  const fileUpload_1 = require("../../utils/fileUpload");
9
9
  const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
10
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
11
  // Tracks which worker-derived `timeout` values have already paid the
11
12
  // pre-flight stagger below, so a worker only pays it once per process
12
13
  // lifetime rather than on every call -- see the comment at the call sites.
13
14
  const staggeredWorkerTimeouts = new Set();
15
+ const onboardingTourSuppressed = new WeakSet();
16
+ // Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
17
+ // (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
18
+ // "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
19
+ // hides the post-login banner link from the accessibility tree until dismissed, which times
20
+ // out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
21
+ // script on every navigation/reload, so there's no login-response race to lose; the flag is
22
+ // matched by prefix since the user id isn't known ahead of login.
23
+ async function suppressOnboardingTour(page) {
24
+ if (onboardingTourSuppressed.has(page)) {
25
+ return;
26
+ }
27
+ onboardingTourSuppressed.add(page);
28
+ await page.addInitScript((prefixPattern) => {
29
+ const pattern = new RegExp(prefixPattern);
30
+ const originalGetItem = Storage.prototype.getItem;
31
+ Storage.prototype.getItem = function (key) {
32
+ return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
33
+ };
34
+ }, '^hub\\.onboarding\\..*\\.seen\\.');
35
+ }
14
36
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5, skipOrgAssertion = false) {
37
+ await suppressOnboardingTour(page);
15
38
  // Callers derive `timeout` as (workerIndex + 1) * 1000, so it's a stable
16
39
  // per-worker identifier: a Playwright worker is a long-lived process that
17
40
  // calls loginWithRetry once per test/repeat it runs, and this stagger only
@@ -32,6 +55,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
32
55
  for (let attempt = 0; attempt < maxRetries; attempt++) {
33
56
  try {
34
57
  await page.context().clearCookies();
58
+ // The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
59
+ // for the tab's whole lifetime; without clearing it here alongside
60
+ // cookies/storage, a flow that logs a different user into the same
61
+ // page could have getUserToken() return the PREVIOUS user's token.
62
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
35
63
  await page.goto('about:blank');
36
64
  await page
37
65
  .evaluate(() => {
@@ -429,8 +457,15 @@ async function assertTestUsesCorrectOrganization(page) {
429
457
  const homePage = new HomePage_1.HomePage(page);
430
458
  if (page.url().includes('?code=') || page.url().includes('&state=')) {
431
459
  console.log('OAuth callback detected, waiting for redirect...');
460
+ // Camunda Hub (which replaced Console on SaaS) only exchanges the code
461
+ // once its SPA has booted, and on CI runners that boot alone was measured
462
+ // at ~45s past /login-callback -- a 30s wait aborted the exchange on every
463
+ // attempt. The code is single-use, so falling back to page.goto('/') on
464
+ // timeout (as this used to) doesn't recover a slow-but-valid exchange, it
465
+ // just turns it into a guaranteed failure. Wait longer instead, matching
466
+ // the sibling 8.10 fix.
432
467
  await page.waitForURL((url) => !url.toString().includes('?code='), {
433
- timeout: 30000,
468
+ timeout: 120000,
434
469
  });
435
470
  await (0, sleep_1.sleep)(5000);
436
471
  }
@@ -442,6 +477,24 @@ async function assertTestUsesCorrectOrganization(page) {
442
477
  else {
443
478
  await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 30000 });
444
479
  }
480
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on
481
+ // /workspaces (no org id in the URL) and has no "Open Organizations"
482
+ // button. Its sidebar links are org-scoped, so assert on those instead and
483
+ // switch via the same /org/<id> route Hub's org breadcrumb dropdown uses.
484
+ const isShown = (locator, timeout) => locator
485
+ .waitFor({ state: 'visible', timeout })
486
+ .then(() => true)
487
+ .catch(() => false);
488
+ if (await isShown(homePage.hubSidebar, 10000)) {
489
+ const orgScopedLink = homePage.hubSidebar
490
+ .locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
491
+ .first();
492
+ if (!(await isShown(orgScopedLink, 30000))) {
493
+ await page.goto(`/org/${process.env.ORG_ID}`);
494
+ }
495
+ await (0, test_1.expect)(orgScopedLink).toBeVisible({ timeout: 60000 });
496
+ return;
497
+ }
445
498
  await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
446
499
  try {
447
500
  await (0, test_1.expect)(page.url()).toContain(process.env.ORG_ID);
@@ -12,7 +12,8 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
12
12
  await (0, _setup_1.captureScreenshot)(page, testInfo);
13
13
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
14
14
  });
15
- (0, _8_7_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
15
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
16
+ _8_7_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
16
17
  _8_7_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
17
18
  _8_7_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
18
19
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
@@ -60,10 +61,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
60
61
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
61
62
  });
62
63
  await _8_7_1.test.step('Navigate to Web Modeler', async () => {
63
- await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
64
- timeout: 120000,
65
- });
66
- await appsPage.clickCamundaApps();
64
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
65
+ // Modeler home directly and has no app switcher to open.
66
+ if (!new URL(page.url()).hostname.startsWith('hub.')) {
67
+ await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
68
+ timeout: 120000,
69
+ });
70
+ await appsPage.clickCamundaApps();
71
+ }
67
72
  await appsPage.clickModeler();
68
73
  await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
69
74
  timeout: 180000,
@@ -71,6 +76,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
71
76
  });
72
77
  await _8_7_1.test.step('Create Cross Component Project Folder', async () => {
73
78
  await homePage.clickSkipCustomization();
79
+ // Do not attempt a "only User 1 creates, everyone else waits" fix for
80
+ // the parallel race here: the 8.10 sibling already tried exactly this
81
+ // and reverted it (see tests/8.10/test-setup.spec.ts) after finding
82
+ // the created workspace never became visible to the other users'
83
+ // sessions at all -- not a wait-budget problem, so no amount of
84
+ // retrying fixes it. Every user independently checking-and-creating
85
+ // still races occasionally, but that is the smaller, known failure
86
+ // mode; see camunda/c8-cross-component-e2e-tests#3649.
74
87
  await modelerHomePage.createCrossComponentProjectFolder();
75
88
  await modelerHomePage.clickHomeBreadcrumb();
76
89
  await modelerHomePage.assertCrossComponentProjectFolderVisible();
@@ -134,13 +134,15 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
134
134
  });
135
135
  await (0, sleep_1.sleep)(30000);
136
136
  await appsPage.clickCamundaApps();
137
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible({
138
- timeout: 30000,
139
- });
137
+ // clickCamundaApps() is a no-op on Hub (no global app-switcher panel to
138
+ // open -- see its own comment), so modelerLink has nothing to assert
139
+ // there: Hub's authenticated shell already proves Modeler access by
140
+ // having been reached at all. consoleLink's accessible name ("Console")
141
+ // matches Hub's persistent sidebar link unchanged, so that assertion
142
+ // still applies on both shells.
140
143
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible();
141
- await (0, test_1.expect)(appsPage.operateLink).not.toBeVisible();
142
- await (0, test_1.expect)(appsPage.tasklistLink).not.toBeVisible();
143
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
144
+ await appsPage.assertOperatePresent(clusterName, false);
145
+ await appsPage.assertTasklistPresent(clusterName, false);
144
146
  await appsPage.clickModeler();
145
147
  await homePage.clickSkipCustomization();
146
148
  await modelerHomePage.createCrossComponentProjectFolder();
@@ -206,10 +208,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
206
208
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
207
209
  timeout: 30000,
208
210
  });
209
- await appsPage.assertOperatePresent();
210
- await appsPage.assertTasklistPresent();
211
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
212
- await appsPage.assertOptimizePresent(false);
211
+ await appsPage.assertOperatePresent(clusterName);
212
+ await appsPage.assertTasklistPresent(clusterName);
213
+ await appsPage.assertOptimizePresent(clusterName, false);
213
214
  await appsPage.clickModeler();
214
215
  await modelerHomePage.clickCrossComponentProjectFolder();
215
216
  await modelerHomePage.clickProcessDiagram(processName);
@@ -266,10 +267,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
266
267
  await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
267
268
  timeout: 20000,
268
269
  });
269
- await appsPage.assertOptimizePresent();
270
- await appsPage.assertOperatePresent();
271
- await appsPage.assertTasklistPresent();
272
- await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
270
+ await appsPage.assertOptimizePresent(clusterName);
271
+ await appsPage.assertOperatePresent(clusterName);
272
+ await appsPage.assertTasklistPresent(clusterName);
273
273
  await appsPage.clickModeler();
274
274
  await modelerHomePage.clickCrossComponentProjectFolder();
275
275
  await modelerHomePage.clickProcessDiagram(processName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1249",
3
+ "version": "0.0.1250",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",