@camunda/e2e-test-suite 0.0.1272 → 0.0.1274
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.8/AppsPage.d.ts +2 -0
- package/dist/pages/8.8/AppsPage.js +122 -0
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +29 -10
- package/dist/pages/8.8/ClusterPage.js +35 -2
- package/dist/pages/8.8/ClusterSecretsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterSecretsPage.js +62 -2
- package/dist/pages/8.8/FormJsPage.js +55 -24
- package/dist/pages/8.8/HomePage.d.ts +3 -0
- package/dist/pages/8.8/HomePage.js +62 -7
- package/dist/pages/8.8/LoginPage.js +32 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.8/ModelerCreatePage.js +78 -16
- package/dist/pages/8.8/ModelerHomePage.d.ts +10 -0
- package/dist/pages/8.8/ModelerHomePage.js +339 -14
- package/dist/pages/8.8/UtilitiesPage.js +47 -2
- package/dist/pages/8.9/AppsPage.js +10 -1
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +66 -1
- package/dist/pages/8.9/ConsoleOrganizationPage.js +50 -17
- package/dist/pages/8.9/FormJsPage.js +23 -0
- package/dist/pages/8.9/ModelerCreatePage.d.ts +3 -0
- package/dist/pages/8.9/ModelerCreatePage.js +108 -7
- package/dist/pages/8.9/ModelerHomePage.d.ts +8 -0
- package/dist/pages/8.9/ModelerHomePage.js +116 -7
- package/dist/pages/8.9/ProcessApplicationPage.d.ts +1 -0
- package/dist/pages/8.9/ProcessApplicationPage.js +96 -0
- package/dist/pages/8.9/SettingsPage.js +12 -2
- package/dist/tests/8.8/test-setup.spec.js +29 -4
- package/dist/tests/8.9/console-user-flows.spec.js +3 -0
- package/dist/utils/consoleApiHelpers.d.ts +5 -0
- package/dist/utils/consoleApiHelpers.js +177 -2
- package/dist/utils/hubDeployDialog.d.ts +3 -0
- package/dist/utils/hubDeployDialog.js +74 -0
- package/dist/utils/hubResourceRename.d.ts +4 -0
- package/dist/utils/hubResourceRename.js +74 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
}
|