@camunda/e2e-test-suite 0.0.1273 → 0.0.1275

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.
Files changed (33) hide show
  1. package/dist/pages/8.7/AppsPage.js +11 -3
  2. package/dist/pages/8.7/ClusterDetailsPage.d.ts +1 -0
  3. package/dist/pages/8.7/ClusterDetailsPage.js +22 -64
  4. package/dist/pages/8.7/ModelerCreatePage.js +56 -14
  5. package/dist/pages/8.7/PlayPage.js +12 -1
  6. package/dist/pages/8.7/SettingsPage.js +15 -2
  7. package/dist/pages/8.7/UtilitiesPage.js +22 -0
  8. package/dist/pages/8.8/AppsPage.d.ts +2 -0
  9. package/dist/pages/8.8/AppsPage.js +122 -0
  10. package/dist/pages/8.8/ClusterDetailsPage.d.ts +1 -0
  11. package/dist/pages/8.8/ClusterDetailsPage.js +29 -10
  12. package/dist/pages/8.8/ClusterPage.js +35 -2
  13. package/dist/pages/8.8/ClusterSecretsPage.d.ts +1 -0
  14. package/dist/pages/8.8/ClusterSecretsPage.js +62 -2
  15. package/dist/pages/8.8/FormJsPage.js +55 -24
  16. package/dist/pages/8.8/HomePage.d.ts +3 -0
  17. package/dist/pages/8.8/HomePage.js +62 -7
  18. package/dist/pages/8.8/LoginPage.js +32 -1
  19. package/dist/pages/8.8/ModelerCreatePage.d.ts +1 -0
  20. package/dist/pages/8.8/ModelerCreatePage.js +78 -16
  21. package/dist/pages/8.8/ModelerHomePage.d.ts +10 -0
  22. package/dist/pages/8.8/ModelerHomePage.js +339 -14
  23. package/dist/pages/8.8/UtilitiesPage.js +47 -2
  24. package/dist/tests/8.7/rba-enabled-user-flows.spec.js +5 -1
  25. package/dist/tests/8.7/web-modeler-user-flows.spec.js +7 -4
  26. package/dist/tests/8.8/test-setup.spec.js +29 -4
  27. package/dist/utils/consoleApiHelpers.d.ts +5 -0
  28. package/dist/utils/consoleApiHelpers.js +177 -2
  29. package/dist/utils/hubDeployDialog.d.ts +3 -0
  30. package/dist/utils/hubDeployDialog.js +74 -0
  31. package/dist/utils/hubResourceRename.d.ts +4 -0
  32. package/dist/utils/hubResourceRename.js +74 -0
  33. package/package.json +1 -1
@@ -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>;
@@ -15,16 +15,35 @@ class HomePage {
15
15
  appSwitcher;
16
16
  openOrganizationButton;
17
17
  consoleBanner;
18
+ hubHeader;
19
+ hubSidebar;
18
20
  modelerBanner;
19
21
  gettingStartedHeading;
20
22
  buttonSkipCustomization;
21
23
  constructor(page) {
22
24
  this.page = page;
25
+ // Camunda Hub replaces Console on SaaS: an app header (only carrying the
26
+ // user menu once authenticated) plus a sidebar whose Console section
27
+ // links to /org/<id>/clusters and /org/<id>/management.
28
+ this.hubHeader = page
29
+ .locator('header[data-slot="app-header"]')
30
+ .filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
31
+ this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
23
32
  this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
33
+ // Once an org has environmentsEnabled, camunda-hub's
34
+ // use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
35
+ // labelled "Environments" whose href is '/environments', not
36
+ // '/clusters' -- accept both hrefs so this resolves either way.
24
37
  this.clusterTab = page
25
38
  .getByRole('banner')
26
- .getByRole('link', { name: 'Clusters' });
27
- this.organizationTab = page.getByRole('link', { name: 'Organization' });
39
+ .getByRole('link', { name: 'Clusters' })
40
+ .or(this.hubSidebar.locator('a[href$="/clusters"]'))
41
+ .or(this.hubSidebar.locator('a[href$="/environments"]'))
42
+ .first();
43
+ this.organizationTab = page
44
+ .getByRole('link', { name: 'Organization' })
45
+ .or(this.hubSidebar.locator('a[href$="/management"]'))
46
+ .first();
28
47
  this.organizationViewTitle = page.getByText('Organization Management');
29
48
  this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
30
49
  this.informationDialog = page.getByRole('button', {
@@ -32,14 +51,23 @@ class HomePage {
32
51
  });
33
52
  this.camundaAppsButton = page.getByLabel('Camunda apps');
34
53
  this.appSwitcher = page.getByLabel('App Switcher');
54
+ // Specs assert this as "the authenticated app shell has rendered" before
55
+ // navigating. Camunda Hub has no app-switcher button at all (apps are
56
+ // reached from the sidebar), so on Hub its authenticated header is the
57
+ // equivalent readiness signal.
35
58
  this.camundaComponentsButton = page
36
59
  .getByLabel('Camunda components')
37
60
  .or(this.camundaAppsButton)
38
- .or(this.appSwitcher);
61
+ .or(this.appSwitcher)
62
+ .or(this.hubHeader);
39
63
  this.openOrganizationButton = page.getByLabel('Open Organizations');
40
- this.consoleBanner = page.getByRole('link', {
41
- name: 'Camunda logo Console',
42
- });
64
+ // "Camunda logo Console" was the old Carbon header's logo link. Camunda
65
+ // Hub has no equivalent link -- its post-login landing is `hubHeader`
66
+ // (see above) -- so callers that assert this banner as a login/Console
67
+ // arrival signal need the Hub fallback too.
68
+ this.consoleBanner = page
69
+ .getByRole('link', { name: 'Camunda logo Console' })
70
+ .or(this.hubHeader);
43
71
  this.gettingStartedHeading = page.getByText('start by creating a project');
44
72
  this.buttonSkipCustomization = page.getByRole('button', {
45
73
  name: 'Skip customization',
@@ -49,8 +77,35 @@ class HomePage {
49
77
  });
50
78
  }
51
79
  async clickClusters() {
52
- await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 15000 });
80
+ await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 30000 });
53
81
  await this.clusterTab.click({ timeout: 15000 });
82
+ await this.clickClustersPageTabIfPresent();
83
+ }
84
+ // Landing on Hub's Environments overview (see clusterTab above) doesn't put
85
+ // us on the classic row-based cluster list ClusterPage.ts expects -- that
86
+ // page renders clusters as cards. Its own header renders a separate
87
+ // "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
88
+ // that does.
89
+ async clickClustersPageTabIfPresent() {
90
+ const clustersPageTab = this.page
91
+ .getByRole('navigation', { name: 'Environments views' })
92
+ .getByRole('link', { name: 'Clusters', exact: true });
93
+ // Required on Hub, not optional: this page's row-based cluster locators
94
+ // cannot be satisfied by the card-based Environments overview we just
95
+ // landed on. isVisible() does not wait, so a slow tab render used to be
96
+ // read as "no tab here" and the flow continued on the wrong route.
97
+ const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
98
+ const tabShown = await clustersPageTab
99
+ .waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
100
+ .then(() => true)
101
+ .catch(() => false);
102
+ if (tabShown) {
103
+ await clustersPageTab.click({ timeout: 15000 });
104
+ }
105
+ else if (onHub) {
106
+ throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
107
+ 'row-based cluster list this page needs is unreachable without it');
108
+ }
54
109
  }
55
110
  async clickDashboard() {
56
111
  await this.dashboardTab.click({ timeout: 90000 });
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
5
5
  const authErrorClassification_1 = require("../../utils/authErrorClassification");
6
6
  const UtilitiesPage_1 = require("./UtilitiesPage");
7
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
8
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
8
9
  class LoginPage {
9
10
  page;
10
11
  usernameInput;
@@ -50,7 +51,34 @@ class LoginPage {
50
51
  }
51
52
  async fillPassword(password) {
52
53
  await this.passwordInput.waitFor({ state: 'visible', timeout: 60000 });
53
- await this.passwordInput.fill(password);
54
+ // Auth0's password screen becomes visible before its own scripts finish
55
+ // wiring up the field, and that initialisation wipes whatever is already
56
+ // in it. A discarded value makes Continue post an empty password, Auth0
57
+ // re-renders the same screen with "Password is required", and a login can
58
+ // burn all five retries without ever reaching the app.
59
+ //
60
+ // Typed, not filled: a live trace of that failure shows six fill +
61
+ // read-back cycles in a row all coming back empty, so re-filling cannot
62
+ // help -- `fill()` sets the value in one shot and Auth0's own handlers
63
+ // never see it, leaving their state empty and the field cleared on the
64
+ // next render. pressSequentially dispatches real key events, which those
65
+ // handlers do process.
66
+ //
67
+ // Polled rather than slept on: the check settles as soon as the value is
68
+ // there, so a healthy login pays nothing.
69
+ //
70
+ // Only the LENGTH is ever read back or asserted on, so the secret cannot
71
+ // reach a log, a trace entry or an assertion-failure message.
72
+ await (0, test_1.expect)(async () => {
73
+ await this.passwordInput.click();
74
+ await this.passwordInput.fill('');
75
+ await this.passwordInput.pressSequentially(password, { delay: 20 });
76
+ await test_1.expect
77
+ .poll(async () => this.passwordInput
78
+ .evaluate((el) => el.value.length)
79
+ .catch(() => 0), { timeout: 5000, intervals: [200, 400, 800] })
80
+ .toBe(password.length);
81
+ }).toPass({ timeout: 60000, intervals: [500, 1000, 2000] });
54
82
  }
55
83
  async clickLoginButton() {
56
84
  await this.loginButton.click({ timeout: 60000 });
@@ -58,6 +86,7 @@ class LoginPage {
58
86
  async login(credentials = {}) {
59
87
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
60
88
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
89
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
61
90
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 200000 });
62
91
  await this.fillUsername(username);
63
92
  await this.clickContinueButton();
@@ -69,6 +98,7 @@ class LoginPage {
69
98
  async loginWithTestUser(credentials = {}) {
70
99
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
71
100
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
101
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
72
102
  // SSO re-auth may silently log us back in as the previous user after logout.
73
103
  // Detect Console (settings button visible) and retry logout until Auth0 form appears.
74
104
  const settingsBtn = this.page.getByLabel('Open Settings');
@@ -119,6 +149,7 @@ class LoginPage {
119
149
  async loginWithoutOrgAssertion(credentials = {}) {
120
150
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
121
151
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
152
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
122
153
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
123
154
  await this.fillUsername(username);
124
155
  await this.clickContinueButton();
@@ -145,6 +145,7 @@ declare class ModelerCreatePage {
145
145
  runProcessInstance(clusterName: string, variables?: string, formVariables?: {
146
146
  [key: string]: string;
147
147
  }, textToVisible?: string, expectRejection?: boolean): Promise<void>;
148
+ selectOnlyThisResource(): Promise<void>;
148
149
  selectCluster(clusterName: string): Promise<void>;
149
150
  clickViewProcessInstanceLink(): Promise<void>;
150
151
  clickNameInput(): Promise<void>;
@@ -7,6 +7,9 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
7
7
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
8
8
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
+ const hubResourceRename_1 = require("../../utils/hubResourceRename");
11
+ const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
13
  class ModelerCreatePage {
11
14
  page;
12
15
  processApplicationPage;
@@ -172,14 +175,38 @@ class ModelerCreatePage {
172
175
  this.nameInput = page.getByLabel('Name', { exact: true });
173
176
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
174
177
  this.renameDiagramNameButton = page.getByText('Rename');
175
- this.diagramNameInput = page.locator('[data-test="editable-input"]');
178
+ // Hub's rename dialog field, with the legacy inline editable kept for
179
+ // shells where the design-system migration has not shipped.
180
+ this.diagramNameInput = page
181
+ .locator('[data-test="rename-input"]')
182
+ .or(page.locator('[data-test="editable-input"]'))
183
+ .first();
176
184
  this.variableInput = page.locator('[id="variables-json"]');
177
185
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
178
- this.embedButton = page.locator('[data-test="confirm-move"]');
186
+ // Hub's link picker confirms with `link-target-confirm` ("Link");
187
+ // `confirm-move` was the old move/link dialog's id and exists only in
188
+ // hub's older cypress specs now. Keep it as a fallback.
189
+ this.embedButton = page
190
+ .locator('[data-test="link-target-confirm"]')
191
+ .or(page.locator('[data-test="confirm-move"]'))
192
+ .first();
193
+ // Same picker as clickForm(): entries are buttons labelled
194
+ // "Select <name>" inside `link-target-list`, not `item-<name>` nodes.
179
195
  this.newForm = page
180
- .locator('[data-test="item-New form"]')
181
- .getByText('New form');
182
- this.deploySubButton = page.locator('[data-test="deploy-action"]');
196
+ .locator('[data-test="link-target-list"]')
197
+ .getByRole('button', { name: /^Select New form/ })
198
+ .first()
199
+ .or(page.locator('[data-test="item-New form"]').getByText('New form'))
200
+ .first();
201
+ // `deploy-action` is absent from Hub's shell (a DOM audit of a live run
202
+ // found no such id); its deploy dialog confirms with a plain button named
203
+ // "Deploy" in the dialog footer.
204
+ this.deploySubButton = page
205
+ .locator('[data-test="deploy-action"]')
206
+ .or(page
207
+ .getByRole('dialog')
208
+ .getByRole('button', { name: 'Deploy', exact: true }))
209
+ .first();
183
210
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
184
211
  this.restConnectorOption = page.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
185
212
  this.changeElementSearchInput = page.locator('.djs-popup-search input');
@@ -337,12 +364,25 @@ class ModelerCreatePage {
337
364
  this.backupDeployButton = page.getByRole('button', {
338
365
  name: 'Deploy',
339
366
  });
367
+ // Hub's design-system shell emits no Carbon markup, so the `cds--toast-*`
368
+ // scope matched nothing and this assertion read as "not linked" even on a
369
+ // successful link. Assert the message itself and keep the Carbon scope for
370
+ // shells that still render it.
340
371
  this.formLinkedSuccessMessage = page
372
+ .getByText('Form has been successfully linked.')
373
+ .or(page
341
374
  .locator('.cds--toast-notification__subtitle')
342
- .getByText('Form has been successfully linked.');
343
- this.diagramNameBreadcrumb = (diagramName) => page.locator('button[data-test="breadcrumb-diagram"]', {
375
+ .getByText('Form has been successfully linked.'))
376
+ .first();
377
+ // Hub's breadcrumbs carry no data-test (DOM audit of a live run found none
378
+ // of the breadcrumb-* ids present), so address the diagram by its own name
379
+ // in the breadcrumb navigation and keep the legacy id as a fallback.
380
+ this.diagramNameBreadcrumb = (diagramName) => page
381
+ .locator('button[data-test="breadcrumb-diagram"]', {
344
382
  hasText: diagramName,
345
- });
383
+ })
384
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page).getByText(diagramName, { exact: true }))
385
+ .first();
346
386
  this.webhookActiveMessage = page.getByText('Webhook is active');
347
387
  }
348
388
  async clickSecondDeployButton() {
@@ -500,10 +540,16 @@ class ModelerCreatePage {
500
540
  // a form with the same name and the picker then lists two identical entries.
501
541
  // Scope to the first matching item so the link step stays deterministic on retry
502
542
  // instead of failing with a strict-mode violation.
543
+ // Hub's link picker is `ul[data-test="link-target-list"]` whose entries are
544
+ // buttons labelled "Select <name>" with a per-file
545
+ // `data-test="link-target-file-<uuid>"`; the old `item-<name>` id does not
546
+ // exist on it (a DOM audit of a live run found no `item-*` at all). Keep
547
+ // the legacy id as a fallback for pre-design-system shells.
503
548
  await this.page
504
- .locator(`[data-test="item-${name}"]`)
549
+ .locator('[data-test="link-target-list"]')
550
+ .getByRole('button', { name: `Select ${name}`, exact: true })
551
+ .or(this.page.locator(`[data-test="item-${name}"]`).first().getByText(name))
505
552
  .first()
506
- .getByText(name)
507
553
  .click({ timeout: 90000 });
508
554
  }
509
555
  async clickUserTask(id) {
@@ -620,6 +666,12 @@ class ModelerCreatePage {
620
666
  const retryDelayMs = 20000;
621
667
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
622
668
  try {
669
+ // Hub lists only the ENVIRONMENTS assigned to the workspace in this
670
+ // dialog, so a cluster the workspace does not carry is simply absent
671
+ // from the picker. deployForm() ensures that link for the form path;
672
+ // a spec that runs a diagram without deploying a form first would
673
+ // otherwise reach this dialog with nothing to select.
674
+ await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
623
675
  // Inside the loop, not before it: a no-op once bound, but selectCluster
624
676
  // clears the binding when the dialog does not offer the cluster, so the
625
677
  // next attempt rebinds rather than reopening an unchanged dialog.
@@ -677,8 +729,18 @@ class ModelerCreatePage {
677
729
  }
678
730
  }
679
731
  }
732
+ // Scopes Hub's deploy/run dialog to the single resource being deployed.
733
+ // Mirrors FormJsPage.selectOnlyThisResource(): the select trigger carries no
734
+ // aria-labelledby, so it is addressed by its id (`deployment-scope-selection`)
735
+ // -- a name-based lookup silently matches nothing -- and its options render
736
+ // in a portal attached to <body>, outside the dialog, so they are looked up
737
+ // page-scoped. A no-op on dialogs that offer no scope choice.
738
+ async selectOnlyThisResource() {
739
+ await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
740
+ }
680
741
  async selectCluster(clusterName) {
681
742
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
743
+ await this.selectOnlyThisResource();
682
744
  const healthCheckTimeout = 30000;
683
745
  // Inside a process application the deploy dialog renders
684
746
  // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
@@ -720,15 +782,13 @@ class ModelerCreatePage {
720
782
  }
721
783
  return;
722
784
  }
723
- const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
785
+ const healthyRegex = new RegExp(`${clusterName}.*?Healthy`);
724
786
  const clusterLabel = this.dialog
725
787
  .locator('label')
726
788
  .filter({ hasText: new RegExp(`^${clusterName}`) });
727
789
  await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
728
790
  await clusterLabel.click();
729
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
730
- timeout: 30000,
731
- });
791
+ await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
732
792
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
733
793
  timeout: healthCheckTimeout,
734
794
  });
@@ -809,8 +869,10 @@ class ModelerCreatePage {
809
869
  }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
810
870
  }
811
871
  async enterDiagramName(name) {
812
- await this.diagramNameInput.fill(name, { timeout: 90000 });
813
- await this.diagramNameInput.press('Enter', { timeout: 90000 });
872
+ // Hub renames through the current crumb's actions menu into a shared
873
+ // dialog; the inline editable this used to fill never mounts on its own.
874
+ // "New BPMN diagram" is the server default the diagram still carries here.
875
+ await (0, hubResourceRename_1.renameHubResource)(this.page, 'New BPMN diagram', name);
814
876
  }
815
877
  async clickVariableInput() {
816
878
  await this.variableInput.click({ timeout: 60000 });