@camunda/e2e-test-suite 0.0.1271 → 0.0.1273

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 (61) hide show
  1. package/dist/pages/8.10/ClusterSecretsPage.js +17 -1
  2. package/dist/pages/8.9/AppsPage.js +10 -1
  3. package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
  4. package/dist/pages/8.9/ClusterDetailsPage.js +66 -1
  5. package/dist/pages/8.9/ClusterPage.js +11 -2
  6. package/dist/pages/8.9/ClusterSecretsPage.js +19 -4
  7. package/dist/pages/8.9/ConsoleOrganizationPage.js +50 -17
  8. package/dist/pages/8.9/FormJsPage.js +23 -0
  9. package/dist/pages/8.9/ModelerCreatePage.d.ts +3 -0
  10. package/dist/pages/8.9/ModelerCreatePage.js +108 -7
  11. package/dist/pages/8.9/ModelerHomePage.d.ts +8 -0
  12. package/dist/pages/8.9/ModelerHomePage.js +116 -7
  13. package/dist/pages/8.9/ProcessApplicationPage.d.ts +1 -0
  14. package/dist/pages/8.9/ProcessApplicationPage.js +96 -0
  15. package/dist/pages/8.9/SettingsPage.js +12 -2
  16. package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +1 -0
  17. package/dist/pages/BrowseAllConnectorMarketplacePage.js +26 -14
  18. package/dist/pages/SM-8.9/ClusterPage.js +5 -1
  19. package/dist/resources/Basic_Auth_REST_Connector.bpmn +2 -2
  20. package/dist/resources/ConnectorsBasicTest.bpmn +6 -6
  21. package/dist/resources/REST_Connector_Bearer_Auth_Process.bpmn +2 -2
  22. package/dist/resources/REST_Connector_No_Auth_Process.bpmn +1 -1
  23. package/dist/test-setup.d.ts +0 -1
  24. package/dist/test-setup.js +7 -29
  25. package/dist/tests/8.10/connectors-user-flows.spec.js +4 -7
  26. package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +1 -2
  27. package/dist/tests/8.10/smoke-tests.spec.js +2 -3
  28. package/dist/tests/8.7/connectors-user-flows.spec.js +10 -14
  29. package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +1 -2
  30. package/dist/tests/8.7/smoke-tests.spec.js +3 -3
  31. package/dist/tests/8.8/connectors-user-flows.spec.js +5 -7
  32. package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +1 -2
  33. package/dist/tests/8.8/smoke-tests.spec.js +3 -3
  34. package/dist/tests/8.9/connectors-user-flows.spec.js +5 -7
  35. package/dist/tests/8.9/console-user-flows.spec.js +3 -0
  36. package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +1 -2
  37. package/dist/tests/8.9/smoke-tests.spec.js +3 -3
  38. package/dist/tests/SM-8.10/connectors-user-flows.spec.js +6 -9
  39. package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +6 -6
  40. package/dist/tests/SM-8.10/smoke-tests.spec.js +2 -1
  41. package/dist/tests/SM-8.7/connectors-user-flows.spec.js +4 -4
  42. package/dist/tests/SM-8.7/mt-enabled-user-flows.spec.js +5 -4
  43. package/dist/tests/SM-8.7/smoke-tests.spec.js +3 -2
  44. package/dist/tests/SM-8.8/connectors-user-flows.spec.js +4 -4
  45. package/dist/tests/SM-8.8/mt-enabled-user-flows.spec.js +5 -4
  46. package/dist/tests/SM-8.8/smoke-tests.spec.js +2 -1
  47. package/dist/tests/SM-8.9/connectors-user-flows.spec.js +6 -6
  48. package/dist/tests/SM-8.9/mt-enabled-user-flows.spec.js +5 -4
  49. package/dist/tests/SM-8.9/smoke-tests.spec.js +2 -1
  50. package/dist/tests/c8Run-8.10/connectors-user-flows.spec.js +2 -2
  51. package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +5 -8
  52. package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +5 -8
  53. package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +5 -8
  54. package/dist/utils/connectorSecrets.js +2 -1
  55. package/dist/utils/constants.d.ts +2 -0
  56. package/dist/utils/constants.js +20 -1
  57. package/package.json +1 -1
  58. package/resources/Basic_Auth_REST_Connector.bpmn +2 -2
  59. package/resources/ConnectorsBasicTest.bpmn +6 -6
  60. package/resources/REST_Connector_Bearer_Auth_Process.bpmn +2 -2
  61. package/resources/REST_Connector_No_Auth_Process.bpmn +1 -1
@@ -88,7 +88,23 @@ class ClusterSecretsPage {
88
88
  .or(this.page
89
89
  .getByRole('navigation', { name: 'Main navigation' })
90
90
  .getByRole('link', { name: 'Environments', exact: true }));
91
- this.clusterLink = (name) => page.getByRole('link', { name: name });
91
+ // Scope the cluster link to its row in the clusters table, the way
92
+ // ClusterPage.clusterLink already does (see the note on
93
+ // ClusterPage.clusterRow). A page-wide getByRole('link', {name}) matches
94
+ // two more things and turns an ordinary click into a strict-mode
95
+ // violation: Hub's sidebar entry for the same environment, and every
96
+ // extra row when the org holds several clusters under one name -- which
97
+ // it does as soon as a failed create is retried, because the Console
98
+ // enforces name uniqueness only per region. The comment on
99
+ // clickClusterBanner() below already describes this locator as
100
+ // row-based; now it is.
101
+ const clustersList = page
102
+ .getByRole('row')
103
+ .filter({ hasNotText: 'Generation' }); // Filter out header row
104
+ this.clusterLink = (name) => clustersList
105
+ .filter({ has: page.getByRole('link', { name: name, exact: true }) })
106
+ .first()
107
+ .getByRole('link', { name: name, exact: true });
92
108
  // Some environments render this tab as "Cluster secrets" rather than
93
109
  // "Connector Secrets" -- match either (see ClusterPage's identical
94
110
  // connectorSecretsTab fallback; this page object has its own separate
@@ -103,10 +103,19 @@ class AppsPage {
103
103
  // for the expanded-sidebar layout, where the links render inline.
104
104
  // Not exact: the rendered accessible name carries more than the bare app
105
105
  // name (e.g. "Operate in <cluster> (opens in a new tab)").
106
+ //
107
+ // The sidebar-scoped fallback must also carry clusterName in its name: the
108
+ // sidebar lists every Environment as a link named after its cluster, so a
109
+ // bare /Admin|Identity/ match resolved to the "Admin Role Cluster"
110
+ // environment link -- which precedes the portalled popover in DOM order --
111
+ // and navigated to that environment's page instead of Admin (nightly
112
+ // trace, UTR Candidate Group).
106
113
  const appLink = this.page
107
114
  .getByRole('dialog', { name: clusterName, exact: true })
108
115
  .getByRole('link', { name: appName })
109
- .or(this.hubSidebar.getByRole('link', { name: appName }))
116
+ .or(this.hubSidebar
117
+ .getByRole('link', { name: appName })
118
+ .and(this.hubSidebar.getByRole('link', { name: ` in ${clusterName}` })))
110
119
  .first();
111
120
  // Decide whether to expand from clusterName's OWN toggle state, not from
112
121
  // whether "some" appLink is already visible sidebar-wide: in a
@@ -25,6 +25,10 @@ declare class ClusterDetailsPage {
25
25
  readonly clustersLink: Locator;
26
26
  readonly alertsTab: Locator;
27
27
  readonly alertsList: Locator;
28
+ readonly alertExpandRowButtons: Locator;
29
+ readonly alertRowActionsButtons: Locator;
30
+ readonly deleteAlertMenuItem: Locator;
31
+ readonly deleteAlertConfirmButton: Locator;
28
32
  readonly createFirstAlertButton: Locator;
29
33
  readonly createNewAlertButton: Locator;
30
34
  readonly alertsPanel: Locator;
@@ -82,6 +86,7 @@ declare class ClusterDetailsPage {
82
86
  selectEmailRadioButton(): Promise<void>;
83
87
  clickCreateAlertButton(): Promise<void>;
84
88
  deleteAlerts(): Promise<void>;
89
+ expandAlerts(): Promise<void>;
85
90
  private doDelete;
86
91
  assertComponentsHealth(components?: string[]): Promise<void>;
87
92
  createAPIClientAndReturnVariables(name: string, setEnvVariables?: boolean, clusterType?: string): Promise<{
@@ -32,6 +32,10 @@ class ClusterDetailsPage {
32
32
  clustersLink;
33
33
  alertsTab;
34
34
  alertsList;
35
+ alertExpandRowButtons;
36
+ alertRowActionsButtons;
37
+ deleteAlertMenuItem;
38
+ deleteAlertConfirmButton;
35
39
  createFirstAlertButton;
36
40
  createNewAlertButton;
37
41
  alertsPanel;
@@ -133,6 +137,25 @@ class ClusterDetailsPage {
133
137
  this.alertsList = page
134
138
  .getByRole('row')
135
139
  .filter({ hasNotText: 'Delivery method' }); //Filter out header row
140
+ // Hub's Alerts tab is a design-system data table: one summary row per
141
+ // alert (Delivery method / Created at / Created by), with the detail
142
+ // fields in a sibling expansion row that only exists once the row's
143
+ // "Expand row" toggle is clicked, and Delete behind the row's "Row
144
+ // actions" kebab -> "Delete" -> "Delete alert" dialog (camunda-hub e2e
145
+ // ConsoleClusterDetailsPage). Scoped to the visible tab panel.
146
+ this.alertExpandRowButtons = page
147
+ .getByRole('tabpanel')
148
+ .getByRole('button', { name: 'Expand row', exact: true });
149
+ this.alertRowActionsButtons = page
150
+ .getByRole('tabpanel')
151
+ .getByRole('button', { name: 'Row actions', exact: true });
152
+ this.deleteAlertMenuItem = page.getByRole('menuitem', {
153
+ name: 'Delete',
154
+ exact: true,
155
+ });
156
+ this.deleteAlertConfirmButton = page
157
+ .getByRole('dialog', { name: 'Delete alert' })
158
+ .getByRole('button', { name: 'Delete', exact: true });
136
159
  this.createFirstAlertButton = page.getByRole('button', {
137
160
  name: 'Create an alert',
138
161
  });
@@ -524,7 +547,47 @@ class ClusterDetailsPage {
524
547
  });
525
548
  }
526
549
  async deleteAlerts() {
527
- await this.doDelete(this.dialog, 'Alerts');
550
+ const hasRowActions = await this.alertRowActionsButtons
551
+ .first()
552
+ .waitFor({ state: 'visible', timeout: 10000 })
553
+ .then(() => true)
554
+ .catch(() => false);
555
+ if (!hasRowActions) {
556
+ await this.doDelete(this.dialog, 'Alerts');
557
+ return;
558
+ }
559
+ let remaining = await this.alertRowActionsButtons.count();
560
+ while (remaining > 0) {
561
+ // A re-render can detach the menu between opening it and the click, so
562
+ // open-menu-and-choose-Delete is retried as one unit.
563
+ await (0, test_1.expect)(async () => {
564
+ await this.alertRowActionsButtons.first().click({ timeout: 10000 });
565
+ await this.deleteAlertMenuItem.click({ timeout: 10000 });
566
+ await (0, test_1.expect)(this.deleteAlertConfirmButton).toBeVisible({
567
+ timeout: 10000,
568
+ });
569
+ }).toPass({ timeout: 60000 });
570
+ await this.deleteAlertConfirmButton.click({ timeout: 30000 });
571
+ await (0, test_1.expect)(this.alertRowActionsButtons).toHaveCount(remaining - 1, {
572
+ timeout: 60000,
573
+ });
574
+ remaining = await this.alertRowActionsButtons.count();
575
+ }
576
+ }
577
+ // Reveals each alert's detail row. A no-op on the legacy table, whose
578
+ // detail rows are always rendered.
579
+ async expandAlerts() {
580
+ for (let i = 0; i < 10; i++) {
581
+ const toggle = this.alertExpandRowButtons.first();
582
+ if (!(await toggle.isVisible().catch(() => false))) {
583
+ return;
584
+ }
585
+ const before = await this.alertExpandRowButtons.count();
586
+ await toggle.click({ timeout: 10000 });
587
+ await (0, test_1.expect)(this.alertExpandRowButtons).toHaveCount(before - 1, {
588
+ timeout: 10000,
589
+ });
590
+ }
528
591
  }
529
592
  async doDelete(dialog, text) {
530
593
  try {
@@ -758,6 +821,8 @@ class ClusterDetailsPage {
758
821
  async assertAlertText(text, timeout = 60000, maxRetries = 10) {
759
822
  for (let attempt = 0; attempt < maxRetries; attempt++) {
760
823
  try {
824
+ await (0, test_1.expect)(this.alertsList.first()).toBeVisible({ timeout });
825
+ await this.expandAlerts();
761
826
  await (0, test_1.expect)(this.alertsList.nth(1)).toContainText(text, { timeout });
762
827
  return;
763
828
  }
@@ -73,9 +73,18 @@ class ClusterPage {
73
73
  .getByRole('dialog')
74
74
  .getByRole('button', { name: 'Delete' });
75
75
  this.testClusterLink = page.getByRole('link', { name: 'Test Cluster' });
76
- this.connectorSecretsTab = page.getByRole('tab', {
76
+ // Camunda Hub renames this tab to "Cluster secrets". Accept either name,
77
+ // the way 8.10's ClusterPage already does: the 8.9 SaaS org is on Hub
78
+ // (see the create-cluster note below, which adapts to the same shell),
79
+ // so the Carbon-era name alone never matches there and every setup step
80
+ // that opens this tab fails on all three clusters.
81
+ this.connectorSecretsTab = page
82
+ .getByRole('tab', {
83
+ name: 'Cluster secrets',
84
+ })
85
+ .or(page.getByRole('tab', {
77
86
  name: 'Connector Secrets',
78
- });
87
+ }));
79
88
  // Camunda Hub's create-cluster form replaced the Carbon tiles (a
80
89
  // <label> per option) and tabs with design-system choice cards and toggle
81
90
  // groups, all exposed as role="radio" named by their title. Each locator
@@ -77,10 +77,25 @@ class ClusterSecretsPage {
77
77
  .or(this.page
78
78
  .getByRole('navigation', { name: 'Main navigation' })
79
79
  .getByRole('link', { name: 'Environments', exact: true }));
80
- this.clusterLink = (name) => page.getByRole('link', { name: name });
81
- this.clusterSecretTab = this.page.getByRole('tab', {
82
- name: 'Connector Secrets',
83
- });
80
+ // Scope the cluster link to its row in the clusters table, as 8.10's
81
+ // ClusterSecretsPage does. The 8.9 org runs the Hub shell -- the tab
82
+ // rename handled above proves it -- and Hub's sidebar carries a link
83
+ // with the same name as the environment, so a page-wide
84
+ // getByRole('link', {name}) matches the sidebar entry and the table row
85
+ // together. Extra rows from a retried create collide the same way,
86
+ // because the Console enforces cluster-name uniqueness only per region.
87
+ const clustersList = page
88
+ .getByRole('row')
89
+ .filter({ hasNotText: 'Generation' }); // Filter out header row
90
+ this.clusterLink = (name) => clustersList
91
+ .filter({ has: page.getByRole('link', { name: name, exact: true }) })
92
+ .first()
93
+ .getByRole('link', { name: name, exact: true });
94
+ // Same Hub tab rename as ClusterPage.connectorSecretsTab above -- accept
95
+ // either name, as 8.10's ClusterSecretsPage does.
96
+ this.clusterSecretTab = this.page
97
+ .getByRole('tab', { name: 'Cluster secrets' })
98
+ .or(this.page.getByRole('tab', { name: 'Connector Secrets' }));
84
99
  this.confirmDeleteInput = page.getByRole('textbox', {
85
100
  name: 'Type the word DELETE to confirm',
86
101
  });
@@ -43,20 +43,39 @@ class ConsoleOrganizationPage {
43
43
  constructor(page) {
44
44
  this.page = page;
45
45
  this.usersTab = page.getByRole('tab', { name: 'Users' });
46
- this.optionsButton = page.getByRole('button', { name: 'Options' });
47
- this.editUserMenuItem = page.getByRole('menuitem', { name: 'Edit User' });
48
- this.modelerCheckbox = page
49
- .getByRole('group')
50
- .locator('label')
51
- .filter({ hasText: 'Modeler' });
52
- this.adminCheckbox = page
53
- .getByRole('group')
54
- .locator('label')
55
- .filter({ hasText: 'Admin' });
56
- this.analystCheckbox = page
57
- .getByRole('group')
58
- .locator('label')
59
- .filter({ hasText: 'Analyst' });
46
+ // Hub's org-management Users table is a design-system data table whose
47
+ // per-row kebab is named "Row actions" (camunda-hub e2e
48
+ // ConsoleOrgManagementPage), not Carbon's "Options". Scoped to the visible
49
+ // tab panel so another tab's rows cannot match.
50
+ this.optionsButton = page
51
+ .getByRole('button', { name: 'Options' })
52
+ .or(page.getByRole('tabpanel').getByRole('button', { name: 'Row actions' }))
53
+ .first();
54
+ this.editUserMenuItem = page.getByRole('menuitem', {
55
+ name: /^Edit user$/i,
56
+ });
57
+ // The "Edit user" dialog's role toggles are plain Carbon Checkboxes
58
+ // (@camunda/ccma-saas-frontend c4-user-details-page.tsx), each its own
59
+ // labelText-bound <input type="checkbox">, not a role="group" of
60
+ // <label>s -- there is no group wrapper around them at all. Scoped to
61
+ // whichever dialog is open rather than one named "Edit user": a modal
62
+ // whose heading isn't wired via aria-labelledby carries no accessible
63
+ // name at all, which silently zeroes out a name-matched dialog locator
64
+ // (confirmed live: the name-scoped version timed out the same as the
65
+ // original role="group" one).
66
+ const openDialog = page.getByRole('dialog');
67
+ this.modelerCheckbox = openDialog.getByRole('checkbox', {
68
+ name: 'Modeler',
69
+ exact: true,
70
+ });
71
+ this.adminCheckbox = openDialog.getByRole('checkbox', {
72
+ name: 'Admin',
73
+ exact: true,
74
+ });
75
+ this.analystCheckbox = openDialog.getByRole('checkbox', {
76
+ name: 'Analyst',
77
+ exact: true,
78
+ });
60
79
  this.confirmButton = page.getByRole('button', { name: 'Confirm' });
61
80
  this.settingsTab = page.getByRole('tab', { name: 'Settings' });
62
81
  this.optInButton = page.getByRole('button', { name: 'Opt-in', exact: true });
@@ -118,14 +137,28 @@ class ConsoleOrganizationPage {
118
137
  name: 'danger',
119
138
  })
120
139
  .last();
121
- this.filterTableSearchbox = page.getByRole('searchbox', {
122
- name: 'Filter table',
123
- });
140
+ // Hub's org-management "Users" tab renders its table filter as a
141
+ // design-system <input type="search" aria-label="Search">, not Carbon's
142
+ // "Filter table" (nightly trace DOM; camunda-hub e2e
143
+ // ConsoleOrgManagementPage.searchbox). Keep the legacy name as a fallback.
144
+ this.filterTableSearchbox = page
145
+ .getByRole('searchbox', { name: 'Search', exact: true })
146
+ .or(page.getByRole('searchbox', { name: 'Filter table' }))
147
+ .first();
124
148
  }
125
149
  async filterTable(names) {
126
150
  for (const name of names) {
127
151
  await this.filterTableSearchbox.click({ timeout: 60000 });
128
152
  await this.filterTableSearchbox.fill(name);
153
+ // The design-system filter applies after a debounce; callers act on the
154
+ // first row next, so wait for a data row containing this name. A
155
+ // "does every row match" assertion never resolves: one row (header or
156
+ // toolbar) always carries a role="cell" descendant without the name.
157
+ await (0, test_1.expect)(this.page
158
+ .getByRole('row')
159
+ .filter({ has: this.page.getByRole('cell') })
160
+ .filter({ hasText: name })
161
+ .first()).toBeVisible({ timeout: 30000 });
129
162
  }
130
163
  }
131
164
  async clickProcessIdInput() {
@@ -4,6 +4,7 @@ exports.FormJsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
+ const modelerCluster_1 = require("../../utils/modelerCluster");
7
8
  class FormJsPage {
8
9
  page;
9
10
  processApplicationPage;
@@ -144,6 +145,28 @@ class FormJsPage {
144
145
  throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
145
146
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
146
147
  }
148
+ // Hub's environment-based dialog (camunda-hub#29210) lists every
149
+ // Environment assigned to the workspace, preselects the first -- which
150
+ // need not be this one -- and renders a <label> per row that the legacy
151
+ // toggle detection below would mistake for its own. Same handling as
152
+ // ModelerCreatePage.selectCluster, so a form deploy targets the requested
153
+ // cluster instead of whichever environment the dialog preselected.
154
+ const hasEnvironmentRow = await this.dialog
155
+ .locator('[data-test^="environment-row-"]')
156
+ .first()
157
+ .waitFor({ state: 'visible', timeout: 10000 })
158
+ .then(() => true)
159
+ .catch(() => false);
160
+ if (hasEnvironmentRow) {
161
+ try {
162
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
163
+ }
164
+ catch (error) {
165
+ this.processApplicationPage.forgetStageCluster();
166
+ throw error;
167
+ }
168
+ return;
169
+ }
147
170
  const legacyClusterToggle = this.dialog.locator('label').filter({
148
171
  hasText: new RegExp(`^${clusterName}`),
149
172
  });
@@ -14,8 +14,11 @@ declare class ModelerCreatePage {
14
14
  readonly viewProcessInstanceLink: 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 renameInput: Locator;
21
+ readonly renameSubmitButton: Locator;
19
22
  readonly variableInput: Locator;
20
23
  readonly embedFormButton: Locator;
21
24
  readonly embedButton: Locator;
@@ -6,6 +6,7 @@ const sleep_1 = require("../../utils/sleep");
6
6
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
7
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
8
8
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
+ const modelerCluster_1 = require("../../utils/modelerCluster");
9
10
  class ModelerCreatePage {
10
11
  page;
11
12
  processApplicationPage;
@@ -21,8 +22,11 @@ class ModelerCreatePage {
21
22
  viewProcessInstanceLink;
22
23
  nameInput;
23
24
  diagramBreadcrumb;
25
+ diagramActionsButton;
24
26
  renameDiagramNameButton;
25
27
  diagramNameInput;
28
+ renameInput;
29
+ renameSubmitButton;
26
30
  variableInput;
27
31
  embedFormButton;
28
32
  embedButton;
@@ -165,8 +169,25 @@ class ModelerCreatePage {
165
169
  });
166
170
  this.nameInput = page.getByLabel('Name', { exact: true });
167
171
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
168
- this.renameDiagramNameButton = page.getByText('Rename');
172
+ // Hub's newer nav (isNavV2, camunda-hub use-file-page-actions.ts) renders
173
+ // no breadcrumb-diagram dropdown: Rename/Duplicate/Delete sit behind a
174
+ // kebab next to the current (last) crumb, named "<entity name> actions".
175
+ // Same locator as camunda-hub's own e2e BasePage.breadcrumbActionsButton.
176
+ this.diagramActionsButton = page
177
+ .getByRole('navigation', { name: 'Context navigation' })
178
+ .or(page.getByRole('navigation', { name: 'Breadcrumb', exact: true }))
179
+ .getByRole('button', { name: / actions$/ })
180
+ .last();
181
+ this.renameDiagramNameButton = page
182
+ .getByRole('menuitem', { name: 'Rename', exact: true })
183
+ .or(page.locator('[data-test="diagram-menu-item-rename-diagram"]'))
184
+ .first();
169
185
  this.diagramNameInput = page.locator('[data-test="editable-input"]');
186
+ // The nav-v2 Rename opens a modal dialog (camunda-hub RenameDialogDs.tsx)
187
+ // instead of the legacy inline breadcrumb editor, and a fresh diagram no
188
+ // longer opens with its name in edit mode.
189
+ this.renameInput = page.locator('[data-test="rename-input"]');
190
+ this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
170
191
  this.variableInput = page.locator('[id="variables-json"]');
171
192
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
172
193
  this.embedButton = page.locator('[data-test="confirm-move"]');
@@ -337,9 +358,14 @@ class ModelerCreatePage {
337
358
  this.formLinkedSuccessMessage = page
338
359
  .locator('.cds--toast-notification__subtitle')
339
360
  .getByText('Form has been successfully linked.');
340
- this.diagramNameBreadcrumb = (diagramName) => page.locator('button[data-test="breadcrumb-diagram"]', {
361
+ // The legacy breadcrumb shows the name directly; the nav-v2 kebab embeds
362
+ // it in its own accessible name ("<name> actions").
363
+ this.diagramNameBreadcrumb = (diagramName) => page
364
+ .locator('button[data-test="breadcrumb-diagram"]', {
341
365
  hasText: diagramName,
342
- });
366
+ })
367
+ .or(page.getByRole('button', { name: `${diagramName} actions` }))
368
+ .first();
343
369
  this.webhookActiveMessage = page.getByText('Webhook is active');
344
370
  }
345
371
  async clickSecondDeployButton() {
@@ -699,6 +725,27 @@ class ModelerCreatePage {
699
725
  throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
700
726
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
701
727
  }
728
+ // Hub's environment-based dialog (camunda-hub#29210) lists every
729
+ // Environment assigned to the workspace, preselects the first -- which
730
+ // need not be this one -- and renders a <label> per row that the legacy
731
+ // toggle detection below would mistake for its own. The dialog is visible
732
+ // a beat before its rows, so wait briefly rather than sampling once.
733
+ const hasEnvironmentRow = await this.dialog
734
+ .locator('[data-test^="environment-row-"]')
735
+ .first()
736
+ .waitFor({ state: 'visible', timeout: 10000 })
737
+ .then(() => true)
738
+ .catch(() => false);
739
+ if (hasEnvironmentRow) {
740
+ try {
741
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
742
+ }
743
+ catch (error) {
744
+ this.processApplicationPage.forgetStageCluster();
745
+ throw error;
746
+ }
747
+ return;
748
+ }
702
749
  const legacyClusterToggle = this.dialog.locator('label').filter({
703
750
  hasText: new RegExp(`^${clusterName}`),
704
751
  });
@@ -747,8 +794,23 @@ class ModelerCreatePage {
747
794
  async fillNamedInput(name) {
748
795
  await this.nameInput.fill(name, { timeout: 60000 });
749
796
  }
797
+ // Opens the current diagram's crumb menu: the legacy breadcrumb dropdown, or
798
+ // the nav-v2 "<name> actions" kebab. Waits for the properties panel first:
799
+ // until the diagram has loaded, nav-v2 keeps showing the previous page's
800
+ // crumbs (camunda-hub NavProvider), so the last kebab would belong to the
801
+ // parent container and Rename would rename that instead.
750
802
  async clickDiagramBreadcrumb() {
751
- await this.diagramBreadcrumb.click({ timeout: 10000 });
803
+ await (0, test_1.expect)(this.generalPanel).toBeVisible({ timeout: 120000 });
804
+ const crumbMenuTrigger = this.diagramBreadcrumb
805
+ .or(this.diagramActionsButton)
806
+ .first();
807
+ await (0, test_1.expect)(crumbMenuTrigger).toBeVisible({ timeout: 60000 });
808
+ if (await this.diagramActionsButton.isVisible().catch(() => false)) {
809
+ await this.diagramActionsButton.click({ timeout: 10000 });
810
+ }
811
+ else {
812
+ await this.diagramBreadcrumb.click({ timeout: 10000 });
813
+ }
752
814
  }
753
815
  // Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
754
816
  // and hub renders that button's truncated-name tooltip on top of the menu,
@@ -787,12 +849,51 @@ class ModelerCreatePage {
787
849
  // failure: the assertion below is what decides whether this worked.
788
850
  await this.renameDiagramNameButton.dispatchEvent('click');
789
851
  }
790
- await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
852
+ await (0, test_1.expect)(this.diagramNameInput.or(this.renameInput).first()).toBeVisible({ timeout: 10000 });
791
853
  }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
792
854
  }
855
+ // Callers either just opened the rename editor or just created the diagram,
856
+ // which the legacy shell opens with its name already in inline-edit mode.
857
+ // Nav-v2 does neither for a fresh diagram, so open the rename action when no
858
+ // editor is showing, fill whichever editor it is, and confirm the crumb
859
+ // actually took the name rather than trusting the fill. Retried as a unit:
860
+ // a re-render (autosave, post-create refetch) can drop the editor mid-fill.
793
861
  async enterDiagramName(name) {
794
- await this.diagramNameInput.fill(name, { timeout: 90000 });
795
- await this.diagramNameInput.press('Enter', { timeout: 90000 });
862
+ const maxAttempts = 3;
863
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
864
+ try {
865
+ const editorOpen = await this.diagramNameInput
866
+ .or(this.renameInput)
867
+ .first()
868
+ .waitFor({ state: 'visible', timeout: 5000 })
869
+ .then(() => true)
870
+ .catch(() => false);
871
+ if (!editorOpen) {
872
+ await this.clickEditDiagramNameButton();
873
+ }
874
+ if (await this.renameInput.isVisible().catch(() => false)) {
875
+ await this.renameInput.fill(name, { timeout: 30000 });
876
+ await (0, test_1.expect)(this.renameInput).toHaveValue(name, { timeout: 5000 });
877
+ await this.renameSubmitButton.click({ timeout: 10000 });
878
+ await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 30000 });
879
+ }
880
+ else {
881
+ await this.diagramNameInput.fill(name, { timeout: 45000 });
882
+ await this.diagramNameInput.press('Enter', { timeout: 45000 });
883
+ }
884
+ await (0, test_1.expect)(this.diagramNameBreadcrumb(name)).toBeVisible({
885
+ timeout: 30000,
886
+ });
887
+ return;
888
+ }
889
+ catch (error) {
890
+ if (attempt === maxAttempts) {
891
+ throw error;
892
+ }
893
+ console.warn(`Diagram rename to "${name}" did not commit ` +
894
+ `(attempt ${attempt}/${maxAttempts}): ${error}`);
895
+ }
896
+ }
796
897
  }
797
898
  async clickVariableInput() {
798
899
  await this.variableInput.click({ timeout: 60000 });
@@ -30,6 +30,8 @@ declare class ModelerHomePage {
30
30
  readonly messageBanner: Locator;
31
31
  readonly idpTemplateOption: Locator;
32
32
  readonly idpEmptyStateButton: Locator;
33
+ readonly idpProjectsTab: Locator;
34
+ readonly createIdpProjectButton: Locator;
33
35
  readonly idpApplicationNameInput: Locator;
34
36
  readonly createIdpApplicationButton: Locator;
35
37
  readonly dialog: Locator;
@@ -38,6 +40,11 @@ declare class ModelerHomePage {
38
40
  readonly blockingDialogDismiss: Locator;
39
41
  readonly optionsButton: Locator;
40
42
  readonly formNameBreadcrumb: (formName: string) => Locator;
43
+ readonly formBreadcrumbButton: Locator;
44
+ readonly formActionsButton: Locator;
45
+ readonly renameFormMenuItem: Locator;
46
+ readonly renameInput: Locator;
47
+ readonly renameSubmitButton: Locator;
41
48
  constructor(page: Page);
42
49
  dismissBlockingDialog(): Promise<void>;
43
50
  private dismissUncataloguedDialog;
@@ -55,6 +62,7 @@ declare class ModelerHomePage {
55
62
  clickBpmnTemplateOption(): Promise<void>;
56
63
  clickFormOption(): Promise<void>;
57
64
  enterFormName(name: string): Promise<void>;
65
+ openFormRenameEditor(): Promise<void>;
58
66
  clickProjectBreadcrumb(): Promise<void>;
59
67
  clickWorkspaceBreadcrumb(): Promise<void>;
60
68
  enterResourceContainer(name?: string): Promise<void>;