@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.
- 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.9/console-user-flows.spec.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
-
|
|
11
|
-
|
|
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');
|