@camunda/e2e-test-suite 0.0.1272 → 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.
@@ -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
  }
@@ -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>;
@@ -7,6 +7,7 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
7
7
  const randomName_1 = require("../../utils/randomName");
8
8
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
+ const modelerCluster_1 = require("../../utils/modelerCluster");
10
11
  class ModelerHomePage {
11
12
  defaultFolderName = 'Cross Component Test Project';
12
13
  // The default cluster test-setup.spec.ts's "Create Default Cluster" test
@@ -52,6 +53,8 @@ class ModelerHomePage {
52
53
  messageBanner;
53
54
  idpTemplateOption;
54
55
  idpEmptyStateButton;
56
+ idpProjectsTab;
57
+ createIdpProjectButton;
55
58
  idpApplicationNameInput;
56
59
  createIdpApplicationButton;
57
60
  dialog;
@@ -60,6 +63,11 @@ class ModelerHomePage {
60
63
  blockingDialogDismiss;
61
64
  optionsButton;
62
65
  formNameBreadcrumb;
66
+ formBreadcrumbButton;
67
+ formActionsButton;
68
+ renameFormMenuItem;
69
+ renameInput;
70
+ renameSubmitButton;
63
71
  constructor(page) {
64
72
  this.page = page;
65
73
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
@@ -113,7 +121,12 @@ class ModelerHomePage {
113
121
  });
114
122
  this.projectNameInput = page.locator('[data-test="editable-input"]');
115
123
  this.formNameInput = page.locator('[data-test="editable-input"]');
116
- this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
124
+ // Hub's workspace projects page renamed IDP applications to "IDP projects"
125
+ // (camunda-hub CreateIdpApplicationModal.common.tsx).
126
+ this.idpApplicationNameInput = page
127
+ .getByPlaceholder('New IDP Application')
128
+ .or(page.getByPlaceholder('New IDP project'))
129
+ .first();
117
130
  this.createIdpApplicationButton = page.getByRole('button', {
118
131
  name: 'Create',
119
132
  exact: true,
@@ -130,6 +143,13 @@ class ModelerHomePage {
130
143
  this.idpEmptyStateButton = page
131
144
  .getByRole('button', { name: 'Create IDP application' })
132
145
  .first();
146
+ // Hub's workspace projects page (camunda-hub WorkspaceProjects.tsx) has no
147
+ // create menu at the root: IDP projects live on their own tab, created
148
+ // through its "Create IDP project" split button (header or empty state).
149
+ this.idpProjectsTab = page.locator('[data-test="tab-idp-applications"]');
150
+ this.createIdpProjectButton = page
151
+ .getByRole('button', { name: 'Create IDP project', exact: true })
152
+ .first();
133
153
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
134
154
  // Matched on the test id alone: hub's design-system variant of the create
135
155
  // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
@@ -192,9 +212,31 @@ class ModelerHomePage {
192
212
  .getByRole('button', { name: /^(Got it|Close|Dismiss)/i })
193
213
  .first();
194
214
  this.optionsButton = page.getByRole('button', { name: 'Options' });
195
- this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
215
+ // The legacy breadcrumb shows the name directly; Hub's newer nav
216
+ // (isNavV2) embeds it in the current crumb's kebab ("<name> actions").
217
+ this.formNameBreadcrumb = (formName) => page
218
+ .locator('button[data-test="breadcrumb-form"]', {
196
219
  hasText: formName,
197
- });
220
+ })
221
+ .or(page.getByRole('button', { name: `${formName} actions` }))
222
+ .first();
223
+ this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
224
+ // Nav-v2 moves Rename/Duplicate/Delete behind a kebab next to the current
225
+ // (last) crumb (camunda-hub use-file-page-actions.ts); same locator as
226
+ // camunda-hub's own e2e BasePage.breadcrumbActionsButton.
227
+ this.formActionsButton = page
228
+ .getByRole('navigation', { name: 'Context navigation' })
229
+ .or(page.getByRole('navigation', { name: 'Breadcrumb', exact: true }))
230
+ .getByRole('button', { name: / actions$/ })
231
+ .last();
232
+ this.renameFormMenuItem = page
233
+ .locator('[data-test="rename-form"]')
234
+ .or(page.getByRole('menuitem', { name: 'Rename', exact: true }))
235
+ .first();
236
+ // Nav-v2's Rename opens a modal dialog (camunda-hub RenameDialogDs.tsx)
237
+ // instead of the legacy inline breadcrumb editor.
238
+ this.renameInput = page.locator('[data-test="rename-input"]');
239
+ this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
198
240
  }
199
241
  // An announcement dialog renders over the projects list and swallows every
200
242
  // click on it: Playwright reports the project link as visible, enabled and
@@ -361,10 +403,19 @@ class ModelerHomePage {
361
403
  // clickCreateProcessApplication, which already covers this for the sibling
362
404
  // resource type.
363
405
  async startIdpApplicationCreation() {
364
- const rootLoaded = await this.becameVisible(this.idpEmptyStateButton.or(this.diagramTypeDropdown).first(), 60000);
406
+ const rootLoaded = await this.becameVisible(this.idpEmptyStateButton
407
+ .or(this.diagramTypeDropdown)
408
+ .or(this.idpProjectsTab)
409
+ .first(), 60000);
365
410
  if (!rootLoaded) {
366
411
  await this.page.reload();
367
412
  }
413
+ if (await this.becameVisible(this.idpProjectsTab, 5000)) {
414
+ await this.idpProjectsTab.click({ timeout: 30000 });
415
+ await (0, test_1.expect)(this.createIdpProjectButton).toBeVisible({ timeout: 60000 });
416
+ await this.createIdpProjectButton.click({ timeout: 30000 });
417
+ return;
418
+ }
368
419
  if (await this.becameVisible(this.idpEmptyStateButton, 5000)) {
369
420
  await this.idpEmptyStateButton.click({ timeout: 60000 });
370
421
  return;
@@ -381,10 +432,61 @@ class ModelerHomePage {
381
432
  async clickFormOption() {
382
433
  await this.formTemplateOption.click();
383
434
  }
435
+ // The legacy shell opens a fresh form with its breadcrumb name in inline-edit
436
+ // mode. Nav-v2 has no inline editing: the form stays "New form" until renamed
437
+ // through the crumb's "<name> actions" kebab and the shared rename dialog.
438
+ // Retried as a unit, and confirmed on the crumb, because a re-render
439
+ // (post-create refetch, form store load) can drop the editor mid-fill.
384
440
  async enterFormName(name) {
385
- await this.formNameInput.click({ timeout: 60000 });
386
- await this.formNameInput.fill(name);
387
- await this.formNameInput.press('Enter');
441
+ const maxAttempts = 3;
442
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
443
+ try {
444
+ if (await this.formNameBreadcrumb(name).isVisible()) {
445
+ return;
446
+ }
447
+ const editorOpen = await this.becameVisible(this.formNameInput.or(this.renameInput).first(), 15000);
448
+ if (!editorOpen) {
449
+ await this.openFormRenameEditor();
450
+ }
451
+ if (await this.renameInput.isVisible().catch(() => false)) {
452
+ await this.renameInput.fill(name, { timeout: 30000 });
453
+ await (0, test_1.expect)(this.renameInput).toHaveValue(name, { timeout: 5000 });
454
+ await this.renameSubmitButton.click({ timeout: 10000 });
455
+ await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 30000 });
456
+ }
457
+ else {
458
+ await this.formNameInput.click({ timeout: 30000 });
459
+ await this.formNameInput.fill(name);
460
+ await this.formNameInput.press('Enter');
461
+ }
462
+ await (0, test_1.expect)(this.formNameBreadcrumb(name)).toBeVisible({
463
+ timeout: 30000,
464
+ });
465
+ return;
466
+ }
467
+ catch (error) {
468
+ if (attempt === maxAttempts) {
469
+ throw error;
470
+ }
471
+ console.warn(`Form rename to "${name}" did not commit ` +
472
+ `(attempt ${attempt}/${maxAttempts}): ${error}`);
473
+ }
474
+ }
475
+ }
476
+ async openFormRenameEditor() {
477
+ await (0, test_1.expect)(this.formBreadcrumbButton.or(this.formActionsButton).first()).toBeVisible({ timeout: 60000 });
478
+ if (await this.formActionsButton.isVisible().catch(() => false)) {
479
+ // Until the new form has loaded, nav-v2 keeps showing the previous
480
+ // page's crumbs (camunda-hub NavProvider), so the last kebab can still
481
+ // belong to the parent container. A fresh form is named "New form";
482
+ // wait for its own crumb before renaming anything.
483
+ await (0, test_1.expect)(this.formActionsButton).toHaveAccessibleName('New form actions', { timeout: 60000 });
484
+ await this.formActionsButton.click({ timeout: 30000 });
485
+ }
486
+ else {
487
+ await this.formBreadcrumbButton.click({ timeout: 60000 });
488
+ }
489
+ await this.renameFormMenuItem.click({ timeout: 30000 });
388
490
  }
389
491
  // Kept under its original name because it is what the existing call sites
390
492
  // already mean: "go back to where my resources live". That is now the
@@ -711,6 +813,13 @@ class ModelerHomePage {
711
813
  async selectCluster(clusterName) {
712
814
  //Select the correct cluster if multiple exist
713
815
  const baseClusterName = clusterName.split('(')[0].trim();
816
+ // With Hub's Environments feature on (camunda-hub#29210) the IDP create
817
+ // dialog picks an Environment row instead of a cluster toggle.
818
+ const hasEnvironmentRow = await this.becameVisible(this.dialog.locator('[data-test^="environment-row-"]').first(), 10000);
819
+ if (hasEnvironmentRow) {
820
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, baseClusterName);
821
+ return;
822
+ }
714
823
  const cluster = this.dialog.getByText(new RegExp(`^${baseClusterName}`));
715
824
  await (0, test_1.expect)(cluster).toBeVisible({
716
825
  timeout: 30000,
@@ -24,6 +24,7 @@ declare class ProcessApplicationPage {
24
24
  rememberResourceContainer(url: string): void;
25
25
  forgetStageCluster(): void;
26
26
  ensureStageCluster(clusterName: string): Promise<boolean>;
27
+ private ensureWorkspaceEnvironment;
27
28
  private openStageDefinition;
28
29
  openByName(name: string): Promise<void>;
29
30
  }
@@ -266,7 +266,19 @@ class ProcessApplicationPage {
266
266
  // suite is. Re-navigating is the recovery; waiting longer on a page that
267
267
  // has already gone wrong is not, which is why the inner timeouts stay
268
268
  // short. A genuinely unbindable cluster still fails, with the same error.
269
+ //
270
+ // Under Hub workspaces (/workspaces/<ws>/projects/<id>) there is no
271
+ // /connected-clusters route -- it renders "No match for the current
272
+ // location". With Hub's Environments feature on (camunda-hub#29210),
273
+ // WorkspaceProjectDetail hides the Connected clusters card altogether and
274
+ // the deploy dialog offers the environments assigned to the parent
275
+ // workspace instead, so the binding is made there.
276
+ const workspaceId = paUrl.match(/\/workspaces\/([^/]+)\/projects\//)?.[1];
269
277
  await (0, test_1.expect)(async () => {
278
+ if (workspaceId) {
279
+ await this.ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName);
280
+ return;
281
+ }
270
282
  await this.page.goto(`${paUrl}/connected-clusters`, {
271
283
  waitUntil: 'domcontentloaded',
272
284
  });
@@ -277,6 +289,90 @@ class ProcessApplicationPage {
277
289
  await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
278
290
  return true;
279
291
  }
292
+ // Assigns the Environment backed by clusterName to the workspace, from
293
+ // workspace management's Environments tab (camunda-hub
294
+ // WorkspaceEnvironmentsTab). Without the Environments feature the project
295
+ // page still carries the Connected clusters card, whose "Configure" button
296
+ // opens the Define stages dialog -- that path is taken instead.
297
+ //
298
+ // Save replaces the workspace's whole assignment with the picker's
299
+ // selection, and parallel workers share this workspace, so only this
300
+ // cluster's toggle is touched and the result re-read afterwards. A
301
+ // concurrent save that drops it anyway surfaces as a deploy dialog without
302
+ // the cluster, which forgets the binding and comes back here (see
303
+ // forgetStageCluster).
304
+ async ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName) {
305
+ await this.page.goto(paUrl, { waitUntil: 'domcontentloaded' });
306
+ await this.waitForProcessApplication();
307
+ const configure = this.page.getByRole('button', {
308
+ name: 'Configure',
309
+ exact: true,
310
+ });
311
+ // isVisible() is a point-in-time check: the Connected clusters card can
312
+ // still be mounting right after waitForProcessApplication resolves, so an
313
+ // instant read here races the card and can wrongly fall through to the
314
+ // (possibly unavailable) Environments tab below. Give it the same short
315
+ // wait the analogous hasEnvironmentRow checks elsewhere in this PR use.
316
+ const configureAppeared = await configure
317
+ .waitFor({ state: 'visible', timeout: 10000 })
318
+ .then(() => true)
319
+ .catch(() => false);
320
+ if (configureAppeared) {
321
+ await configure.click({ timeout: 30000 });
322
+ await this.defineDevelopmentStage(clusterName);
323
+ return;
324
+ }
325
+ const manageUrl = `${new URL(paUrl).origin}/workspaces/${workspaceId}/manage?tab=environments`;
326
+ await this.page.goto(manageUrl, { waitUntil: 'domcontentloaded' });
327
+ const heading = this.page.locator('[data-test="manage-environments-heading"]');
328
+ if (!(await heading
329
+ .waitFor({ state: 'visible', timeout: 60000 })
330
+ .then(() => true)
331
+ .catch(() => false))) {
332
+ throw new Error(`Could not bind "${clusterName}" as a deploy target: the project page ` +
333
+ 'has no Connected clusters "Configure" control, and the workspace ' +
334
+ `Environments tab did not render at ${manageUrl}.`);
335
+ }
336
+ const editButton = this.page.locator('[data-test="edit-environments-button"]');
337
+ await (0, test_1.expect)(editButton).toBeEnabled({ timeout: 60000 });
338
+ await editButton.click({ timeout: 30000 });
339
+ const editor = this.page.getByRole('group', {
340
+ name: 'Edit assigned environments',
341
+ });
342
+ await (0, test_1.expect)(editor).toBeVisible({ timeout: 30000 });
343
+ const search = editor.locator('[data-test="picker-search"]');
344
+ if (await search.isVisible().catch(() => false)) {
345
+ await search.fill(clusterName);
346
+ }
347
+ // Picker card toggles are named "Select <environment> (<cluster>)" and
348
+ // flip to "Selected ..." with aria-pressed once picked.
349
+ const escapedClusterName = clusterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
350
+ const toggle = editor
351
+ .locator('[data-test="picker-environment"]')
352
+ .getByRole('button', {
353
+ name: new RegExp(`^Select(ed)? .*\\(${escapedClusterName}\\)$`),
354
+ })
355
+ .first();
356
+ await (0, test_1.expect)(toggle).toBeVisible({ timeout: 60000 });
357
+ if ((await toggle.getAttribute('aria-pressed')) === 'true') {
358
+ await this.page
359
+ .getByRole('button', { name: 'Cancel', exact: true })
360
+ .click({ timeout: 30000 });
361
+ await (0, test_1.expect)(editor).toBeHidden({ timeout: 30000 });
362
+ return;
363
+ }
364
+ await toggle.click({ timeout: 30000 });
365
+ await (0, test_1.expect)(toggle).toHaveAttribute('aria-pressed', 'true');
366
+ await this.page
367
+ .getByRole('button', { name: 'Save', exact: true })
368
+ .click({ timeout: 30000 });
369
+ await (0, test_1.expect)(editor).toBeHidden({ timeout: 60000 });
370
+ await (0, test_1.expect)(this.page
371
+ .getByRole('list', { name: 'Assigned environments' })
372
+ .getByRole('listitem')
373
+ .filter({ hasText: clusterName })
374
+ .first()).toBeVisible({ timeout: 60000 });
375
+ }
280
376
  // The /connected-clusters deep link opens the Define stages dialog by itself
281
377
  // while the application has no stage bound. Once one is bound -- a test that
282
378
  // deploys to a second cluster -- it has to be reopened from the page, so try
@@ -7,8 +7,18 @@ class SettingsPage {
7
7
  logoutButton;
8
8
  constructor(page) {
9
9
  this.page = page;
10
- this.openSettingsButton = page.getByLabel('Open Settings');
11
- this.logoutButton = page.getByRole('button', { name: 'Log out' });
10
+ // The Camunda Hub shell replaces Console's "Open Settings" header button
11
+ // with the design system's UserMenu, whose trigger carries
12
+ // data-slot="user-menu-trigger" and whose logout entry is a "Log out"
13
+ // menuitem (camunda-hub NavShellDs.common.tsx, nav-shell-ds.spec.saas.tsx).
14
+ this.openSettingsButton = page
15
+ .getByLabel('Open Settings')
16
+ .or(page.locator('[data-slot="user-menu-trigger"]'))
17
+ .first();
18
+ this.logoutButton = page
19
+ .getByRole('button', { name: 'Log out' })
20
+ .or(page.getByRole('menuitem', { name: 'Log out', exact: true }))
21
+ .first();
12
22
  }
13
23
  async clickOpenSettingsButton(retries = 3) {
14
24
  for (let i = 0; i < retries; i++) {
@@ -106,6 +106,7 @@ _8_9_1.test.describe('Console User Flow Tests @tasklistV2', () => {
106
106
  await _8_9_1.test.step('Create First Email Alert', async () => {
107
107
  await clusterDetailsPage.createAlert(true);
108
108
  await (0, test_1.expect)(async () => {
109
+ await clusterDetailsPage.expandAlerts();
109
110
  (0, test_1.expect)(await clusterDetailsPage.alertsList.count()).toBeGreaterThan(1);
110
111
  await (0, test_1.expect)(clusterDetailsPage.alertsList.nth(0)).toContainText(`Email`);
111
112
  await (0, test_1.expect)(clusterDetailsPage.alertsList.nth(1)).toContainText(testUser.username);
@@ -157,6 +158,7 @@ _8_9_1.test.describe('Console User Flow Tests @tasklistV2', () => {
157
158
  await (0, test_1.expect)(clusterDetailsPage.alertsList.first()).toBeVisible({
158
159
  timeout: 20000,
159
160
  });
161
+ await clusterDetailsPage.expandAlerts();
160
162
  (0, test_1.expect)(await clusterDetailsPage.alertsList.count()).toBeGreaterThan(1);
161
163
  await (0, test_1.expect)(clusterDetailsPage.alertsList.nth(0)).toContainText(`Email`);
162
164
  await clusterDetailsPage.assertAlertText('Amount triggered1');
@@ -196,6 +198,7 @@ _8_9_1.test.describe('Console User Flow Tests @tasklistV2', () => {
196
198
  await (0, test_1.expect)(clusterDetailsPage.alertsList.first()).toBeVisible({
197
199
  timeout: 20000,
198
200
  });
201
+ await clusterDetailsPage.expandAlerts();
199
202
  (0, test_1.expect)(await clusterDetailsPage.alertsList.count()).toBeGreaterThan(1);
200
203
  }).toPass({ timeout: 60000 });
201
204
  await clusterDetailsPage.assertAlertText('Amount triggered3');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1272",
3
+ "version": "0.0.1273",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",