@camunda/e2e-test-suite 0.0.1275 → 0.0.1277
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.10/ClusterPage.js +8 -1
- package/dist/pages/8.10/ConsoleOrganizationPage.js +10 -3
- package/dist/pages/8.10/ProcessApplicationPage.js +36 -8
- package/dist/pages/8.10/SettingsPage.js +12 -2
- package/dist/pages/8.8/AppsPage.d.ts +5 -0
- package/dist/pages/8.8/AppsPage.js +231 -22
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +9 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +154 -3
- package/dist/pages/8.8/ClusterPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterPage.js +49 -20
- package/dist/pages/8.8/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/8.8/ConnectorMarketplacePage.js +44 -2
- package/dist/pages/8.8/ConsoleOrganizationPage.js +63 -18
- package/dist/pages/8.8/FormJsPage.d.ts +4 -0
- package/dist/pages/8.8/FormJsPage.js +31 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +3 -0
- package/dist/pages/8.8/ModelerCreatePage.js +167 -18
- package/dist/pages/8.8/PlayPage.js +14 -3
- package/dist/pages/8.8/SettingsPage.js +19 -2
- package/dist/pages/8.8/UtilitiesPage.js +43 -3
- package/dist/tests/8.8/console-user-flows.spec.js +6 -0
- package/dist/tests/8.8/web-modeler-user-flows.spec.js +6 -2
- package/dist/utils/consoleApiHelpers.js +38 -11
- package/package.json +1 -1
|
@@ -28,8 +28,13 @@ class ClusterDetailsPage {
|
|
|
28
28
|
clientCredentialsDialog;
|
|
29
29
|
clustersLink;
|
|
30
30
|
alertsTab;
|
|
31
|
+
alertsPanel;
|
|
31
32
|
activeTabPanel;
|
|
32
33
|
alertsList;
|
|
34
|
+
alertExpandRowButtons;
|
|
35
|
+
alertRowActionsButtons;
|
|
36
|
+
deleteAlertMenuItem;
|
|
37
|
+
deleteAlertConfirmButton;
|
|
33
38
|
createFirstAlertButton;
|
|
34
39
|
createNewAlertButton;
|
|
35
40
|
emailRadioButton;
|
|
@@ -43,6 +48,7 @@ class ClusterDetailsPage {
|
|
|
43
48
|
expandButton;
|
|
44
49
|
rbaEnabledMessage;
|
|
45
50
|
authorizationsHeading;
|
|
51
|
+
authorizationsSwitch;
|
|
46
52
|
orchestrationClusterCheckbox;
|
|
47
53
|
clientCredentialsLink;
|
|
48
54
|
clientRow;
|
|
@@ -123,12 +129,39 @@ class ClusterDetailsPage {
|
|
|
123
129
|
this.alertsList = this.activeTabPanel
|
|
124
130
|
.getByRole('row')
|
|
125
131
|
.filter({ hasNotText: 'Delivery method' }); //Filter out header row
|
|
132
|
+
// Hub's Alerts tab is a design-system data table: one summary row per
|
|
133
|
+
// alert, its detail fields in a sibling expansion row that only exists
|
|
134
|
+
// once the row's "Expand row" toggle is clicked, and Delete behind the
|
|
135
|
+
// row's "Row actions" kebab -> "Delete" -> "Delete alert" dialog. A
|
|
136
|
+
// nightly trace's DOM carries aria-label="Row actions" and no row-level
|
|
137
|
+
// "Delete" button at all, which is why doDelete() below saw "no items"
|
|
138
|
+
// and silently deleted nothing. Same shape as pages/8.9, scoped to the
|
|
139
|
+
// visible tab panel.
|
|
140
|
+
this.alertExpandRowButtons = this.activeTabPanel.getByRole('button', {
|
|
141
|
+
name: 'Expand row',
|
|
142
|
+
exact: true,
|
|
143
|
+
});
|
|
144
|
+
this.alertRowActionsButtons = this.activeTabPanel.getByRole('button', {
|
|
145
|
+
name: 'Row actions',
|
|
146
|
+
exact: true,
|
|
147
|
+
});
|
|
148
|
+
this.deleteAlertMenuItem = page.getByRole('menuitem', {
|
|
149
|
+
name: 'Delete',
|
|
150
|
+
exact: true,
|
|
151
|
+
});
|
|
152
|
+
this.deleteAlertConfirmButton = page
|
|
153
|
+
.getByRole('dialog', { name: 'Delete alert' })
|
|
154
|
+
.getByRole('button', { name: 'Delete', exact: true });
|
|
126
155
|
this.createFirstAlertButton = page.getByRole('button', {
|
|
127
156
|
name: 'Create an alert',
|
|
128
157
|
});
|
|
129
158
|
this.createNewAlertButton = page.getByRole('button', {
|
|
130
159
|
name: 'Create new alert',
|
|
131
160
|
});
|
|
161
|
+
// The Alerts panel offers "Create an alert" while empty and "Create new
|
|
162
|
+
// alert" once it holds one, so either proves the panel itself mounted.
|
|
163
|
+
// Ported from pages/8.9.
|
|
164
|
+
this.alertsPanel = this.createFirstAlertButton.or(this.createNewAlertButton);
|
|
132
165
|
this.emailRadioButton = page.getByRole('radio', {
|
|
133
166
|
name: 'Email',
|
|
134
167
|
});
|
|
@@ -155,6 +188,14 @@ class ClusterDetailsPage {
|
|
|
155
188
|
});
|
|
156
189
|
this.expandButton = page.locator('button[class="cds--table-expand__button"]');
|
|
157
190
|
this.rbaEnabledMessage = page.getByText('Authorizations enabled');
|
|
191
|
+
// Hub's design-system Settings tab renders this as
|
|
192
|
+
// <button role="switch" data-slot="switch" aria-label="Switch Enable
|
|
193
|
+
// authorizations" aria-checked="true|false">, confirmed from a live
|
|
194
|
+
// trace. The Carbon `.cds--toggle__*` nodes the old code indexed into do
|
|
195
|
+
// not exist there at all.
|
|
196
|
+
this.authorizationsSwitch = this.page.getByRole('switch', {
|
|
197
|
+
name: /Enable authorizations/i,
|
|
198
|
+
});
|
|
158
199
|
this.authorizationsHeading = this.page.getByRole('heading', {
|
|
159
200
|
name: 'Authorizations',
|
|
160
201
|
});
|
|
@@ -280,9 +321,34 @@ class ClusterDetailsPage {
|
|
|
280
321
|
},
|
|
281
322
|
});
|
|
282
323
|
}
|
|
324
|
+
// Drives the design-system Authorizations switch. Returns false when this
|
|
325
|
+
// shell does not render one, so the caller can fall through to the legacy
|
|
326
|
+
// Carbon toggle.
|
|
327
|
+
async setAuthorizationsViaSwitch(enabled) {
|
|
328
|
+
if (!(await this.authorizationsSwitch.isVisible().catch(() => false))) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
|
|
332
|
+
if (isOn === enabled) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
await this.authorizationsSwitch.click({ timeout: 30000 });
|
|
336
|
+
if (await this.dialog.isVisible({ timeout: 15000 }).catch(() => false)) {
|
|
337
|
+
const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
|
|
338
|
+
if (await confirm.isVisible().catch(() => false)) {
|
|
339
|
+
await confirm.click({ timeout: 30000 });
|
|
340
|
+
}
|
|
341
|
+
await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
|
|
342
|
+
}
|
|
343
|
+
await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
283
346
|
async enableAuthorizations() {
|
|
284
347
|
await (0, sleep_1.sleep)(5000);
|
|
285
348
|
await this.waitForAuthorizationsSection();
|
|
349
|
+
if (await this.setAuthorizationsViaSwitch(true)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
286
352
|
// Locate all elements with class .cds--toggle__text
|
|
287
353
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
288
354
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -316,11 +382,22 @@ class ClusterDetailsPage {
|
|
|
316
382
|
}
|
|
317
383
|
}
|
|
318
384
|
else {
|
|
319
|
-
console.log
|
|
385
|
+
// Previously a console.log that returned as though the work were done.
|
|
386
|
+
// Under Hub neither `.cds--toggle__text` nor `.cds--toggle__switch`
|
|
387
|
+
// exists, so this branch was taken on every run: authorizations were
|
|
388
|
+
// never enabled, the cluster kept its "Authorizations disabled" badge,
|
|
389
|
+
// and nothing downstream noticed until a permission assertion failed
|
|
390
|
+
// somewhere far away.
|
|
391
|
+
throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
|
|
392
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
393
|
+
'toggle pair.');
|
|
320
394
|
}
|
|
321
395
|
}
|
|
322
396
|
async disableAuthorizations() {
|
|
323
397
|
await this.waitForAuthorizationsSection();
|
|
398
|
+
if (await this.setAuthorizationsViaSwitch(false)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
324
401
|
// Locate all elements with class .cds--toggle__text
|
|
325
402
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
326
403
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -354,7 +431,11 @@ class ClusterDetailsPage {
|
|
|
354
431
|
}
|
|
355
432
|
}
|
|
356
433
|
else {
|
|
357
|
-
|
|
434
|
+
// Same silent no-op as enableAuthorizations had: under Hub no Carbon
|
|
435
|
+
// toggle exists, so this returned as though it had disabled them.
|
|
436
|
+
throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
|
|
437
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
438
|
+
'toggle pair.');
|
|
358
439
|
}
|
|
359
440
|
}
|
|
360
441
|
async searchAndClickClientCredentialsLink(name) {
|
|
@@ -448,7 +529,65 @@ class ClusterDetailsPage {
|
|
|
448
529
|
});
|
|
449
530
|
}
|
|
450
531
|
async deleteAlerts() {
|
|
451
|
-
await this.
|
|
532
|
+
const hasRowActions = await this.alertRowActionsButtons
|
|
533
|
+
.first()
|
|
534
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
535
|
+
.then(() => true)
|
|
536
|
+
.catch(() => false);
|
|
537
|
+
if (!hasRowActions) {
|
|
538
|
+
await this.doDelete(this.dialog, 'Alerts');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
let remaining = await this.alertRowActionsButtons.count();
|
|
542
|
+
while (remaining > 0) {
|
|
543
|
+
// A re-render can detach the menu between opening it and the click, so
|
|
544
|
+
// open-menu-and-choose-Delete is retried as one unit.
|
|
545
|
+
await (0, test_1.expect)(async () => {
|
|
546
|
+
await this.alertRowActionsButtons.first().click({ timeout: 10000 });
|
|
547
|
+
await this.deleteAlertMenuItem.click({ timeout: 10000 });
|
|
548
|
+
await (0, test_1.expect)(this.deleteAlertConfirmButton).toBeVisible({
|
|
549
|
+
timeout: 10000,
|
|
550
|
+
});
|
|
551
|
+
}).toPass({ timeout: 60000 });
|
|
552
|
+
await this.deleteAlertConfirmButton.click({ timeout: 30000 });
|
|
553
|
+
await (0, test_1.expect)(this.alertRowActionsButtons).toHaveCount(remaining - 1, {
|
|
554
|
+
timeout: 60000,
|
|
555
|
+
});
|
|
556
|
+
remaining = await this.alertRowActionsButtons.count();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Reveals each alert's detail row, where the design-system table keeps the
|
|
560
|
+
// recipient, creation date and trigger count the callers assert on. A no-op
|
|
561
|
+
// on the legacy table, whose detail rows are always rendered.
|
|
562
|
+
// Re-selects the Alerts tab when it is no longer the showing one. A
|
|
563
|
+
// page.reload() drops the tab selection and the cluster page comes back on
|
|
564
|
+
// Overview, whose Environments table supplies the very first row -- which
|
|
565
|
+
// is how `alertsList.nth(0)` came to read "NameAlert Flow Cluster" instead
|
|
566
|
+
// of "Email" (console-user-flows.spec.ts reloads and then reads the list
|
|
567
|
+
// straight away). `alertsList` is scoped to whichever tabpanel is visible,
|
|
568
|
+
// so restoring the tab is what makes those reads address the alerts table.
|
|
569
|
+
async ensureAlertsTabSelected() {
|
|
570
|
+
const selected = await this.alertsTab
|
|
571
|
+
.getAttribute('aria-selected')
|
|
572
|
+
.catch(() => null);
|
|
573
|
+
if (selected === 'true') {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
await this.clickAlertsTab();
|
|
577
|
+
}
|
|
578
|
+
async expandAlerts() {
|
|
579
|
+
await this.ensureAlertsTabSelected();
|
|
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
|
+
}
|
|
452
591
|
}
|
|
453
592
|
// The confirmation modal only closes once the DELETE request resolves. When
|
|
454
593
|
// that call fails the modal stays open on an inline "We're having trouble
|
|
@@ -701,6 +840,18 @@ class ClusterDetailsPage {
|
|
|
701
840
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
702
841
|
await this.page.reload();
|
|
703
842
|
await (0, sleep_1.sleep)(20000);
|
|
843
|
+
// A reload drops the tab selection, so the Alerts panel is no
|
|
844
|
+
// longer the visible one -- `alertsList` is scoped to whichever
|
|
845
|
+
// tabpanel is showing, which after a reload is Overview, whose
|
|
846
|
+
// first non-header row is the cluster-details "Name" row. That is
|
|
847
|
+
// exactly what the nightly reported: `Received string:
|
|
848
|
+
// "NameAlert Flow Cluster"`, nine times over, while the Email alert
|
|
849
|
+
// sat unread on a tab nobody had re-opened. Re-select the tab, wait
|
|
850
|
+
// for the panel to mount, and re-expand the detail rows the
|
|
851
|
+
// assertions read from.
|
|
852
|
+
await this.alertsTab.click({ timeout: 60000 });
|
|
853
|
+
await (0, test_1.expect)(this.alertsPanel).toBeVisible({ timeout: 60000 });
|
|
854
|
+
await this.expandAlerts();
|
|
704
855
|
}
|
|
705
856
|
else {
|
|
706
857
|
throw new Error(`Assertion failed after ${maxRetries} attempts`);
|
|
@@ -47,6 +47,7 @@ declare class ClusterPage {
|
|
|
47
47
|
private extractClusterIdFromUrl;
|
|
48
48
|
private logClusterCreationInfo;
|
|
49
49
|
createCluster(name: string, region?: string, tasklistV1Api?: boolean): Promise<void>;
|
|
50
|
+
clickProviderOption(region: string): Promise<void>;
|
|
50
51
|
selectSubRegion(region: string): Promise<void>;
|
|
51
52
|
determineTasklistAPI(tasklistV1Api?: boolean): Promise<void>;
|
|
52
53
|
clickClusterType(): Promise<void>;
|
|
@@ -82,28 +82,32 @@ class ClusterPage {
|
|
|
82
82
|
});
|
|
83
83
|
this.clusterVersionOption = page.getByText(`${process.env.CLUSTER_VERSION}`, { exact: true });
|
|
84
84
|
this.generationSearchInput = page.getByPlaceholder('Search generations');
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
});
|
|
91
|
-
this.
|
|
85
|
+
// Camunda Hub's create-cluster form replaced the Carbon tiles (a <label>
|
|
86
|
+
// per option) and tabs with design-system choice cards and toggle groups,
|
|
87
|
+
// all exposed as role="radio" named by their title. Each locator below
|
|
88
|
+
// accepts either shape via this helper. Ported from
|
|
89
|
+
// pages/8.10/ClusterPage.ts (#3730), where the same change was made.
|
|
90
|
+
const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
|
|
91
|
+
this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
|
|
92
|
+
this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
|
|
93
|
+
this.ohioRegion = choice(page.locator('label').filter({
|
|
92
94
|
hasText: 'Ohio, North America (us-east-2)',
|
|
93
|
-
});
|
|
94
|
-
this.belgiumRegion = page.locator('label').filter({
|
|
95
|
+
}), 'Ohio, North America (us-east-2)');
|
|
96
|
+
this.belgiumRegion = choice(page.locator('label').filter({
|
|
95
97
|
hasText: 'Chaos, Belgium, Europe (europe-west1)',
|
|
96
|
-
});
|
|
97
|
-
this.finlandRegion = page.locator('label').filter({
|
|
98
|
+
}), 'Chaos, Belgium, Europe (europe-west1)');
|
|
99
|
+
this.finlandRegion = choice(page.locator('label').filter({
|
|
98
100
|
hasText: 'Finland, Europe (europe-north1)',
|
|
99
|
-
});
|
|
100
|
-
this.alphaTab = page.getByRole('tab', { name: 'Alpha' });
|
|
101
|
-
this.devTab = page.getByRole('tab', { name: 'Internal Dev' });
|
|
102
|
-
this.stableTab = page.getByRole('tab', { name: 'Stable' });
|
|
103
|
-
this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
}), 'Finland, Europe (europe-north1)');
|
|
102
|
+
this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
|
|
103
|
+
this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
|
|
104
|
+
this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
|
|
105
|
+
this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
|
|
106
|
+
// The cloud-provider picker is a design-system toggle group, not tabs:
|
|
107
|
+
// a nightly trace's DOM has
|
|
108
|
+
// <button role="radio" aria-checked="true" data-slot="toggle-group-item">GCP</button>
|
|
109
|
+
// inside a role="radiogroup", and no role="tab" anywhere on the form.
|
|
110
|
+
this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
|
|
107
111
|
this.clustersList = page
|
|
108
112
|
.getByRole('row')
|
|
109
113
|
.filter({ hasNotText: 'Generation' }); //Filter out header row
|
|
@@ -224,6 +228,15 @@ class ClusterPage {
|
|
|
224
228
|
visibilityTimeout: 20000,
|
|
225
229
|
totalTimeout: 80000,
|
|
226
230
|
postAction: async () => {
|
|
231
|
+
// The clusters route intermittently answers with an Envoy gateway
|
|
232
|
+
// error ("upstream connect error or disconnect/reset before headers.
|
|
233
|
+
// reset reason: connection timeout"), which renders as a bare text
|
|
234
|
+
// document -- clicking the banner cannot recover that, so every
|
|
235
|
+
// remaining attempt retried against a dead page. Reload first, then
|
|
236
|
+
// fall back to the banner for the ordinary not-yet-navigated case.
|
|
237
|
+
await this.page
|
|
238
|
+
.reload({ waitUntil: 'domcontentloaded', timeout: 30000 })
|
|
239
|
+
.catch(() => { });
|
|
227
240
|
await this.clickClusterBanner();
|
|
228
241
|
},
|
|
229
242
|
});
|
|
@@ -276,7 +289,7 @@ class ClusterPage {
|
|
|
276
289
|
await this.clickClusterNameInput();
|
|
277
290
|
await this.fillClusterNameInput(name);
|
|
278
291
|
await this.clickClusterType();
|
|
279
|
-
await this.
|
|
292
|
+
await this.clickProviderOption(region);
|
|
280
293
|
await this.selectSubRegion(region);
|
|
281
294
|
await this.clickClusterOption(process.env.CLUSTER_VERSION);
|
|
282
295
|
await this.determineTasklistAPI(tasklistV1Api);
|
|
@@ -284,6 +297,22 @@ class ClusterPage {
|
|
|
284
297
|
await this.logClusterCreationInfo(name);
|
|
285
298
|
await this.clickClustersBreadcrumb(name);
|
|
286
299
|
}
|
|
300
|
+
// Selects the cloud provider ("GCP"/"AWS"). Hub renders the picker as a
|
|
301
|
+
// toggle group of role="radio" buttons rather than tabs, and it arrives with
|
|
302
|
+
// GCP already selected -- so a `role=tab` click both targeted nothing and
|
|
303
|
+
// was unnecessary. Accept either shape and skip the click when the option is
|
|
304
|
+
// already the selected one.
|
|
305
|
+
async clickProviderOption(region) {
|
|
306
|
+
const providerOption = this.page
|
|
307
|
+
.getByRole('tab', { name: region })
|
|
308
|
+
.or(this.page.getByRole('radio', { name: region, exact: true }))
|
|
309
|
+
.first();
|
|
310
|
+
await (0, test_1.expect)(providerOption).toBeVisible({ timeout: 40000 });
|
|
311
|
+
if ((await providerOption.getAttribute('aria-checked')) === 'true') {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await providerOption.click({ timeout: 30000 });
|
|
315
|
+
}
|
|
287
316
|
async selectSubRegion(region) {
|
|
288
317
|
if (region === 'GCP') {
|
|
289
318
|
if (process.env.IS_PROD == 'true') {
|
|
@@ -8,7 +8,9 @@ declare class ConnectorMarketplacePage {
|
|
|
8
8
|
readonly replaceResourceButton: Locator;
|
|
9
9
|
readonly addToProjectButton: Locator;
|
|
10
10
|
readonly saveAsCopyButton: Locator;
|
|
11
|
+
readonly cancelButton: Locator;
|
|
11
12
|
readonly importModal: Locator;
|
|
13
|
+
readonly browseAllMarketplaceDetailsButton: Locator;
|
|
12
14
|
constructor(page: Page);
|
|
13
15
|
clickSearchForConnectorTextbox(): Promise<void>;
|
|
14
16
|
fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorMarketplacePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
class ConnectorMarketplacePage {
|
|
5
6
|
page;
|
|
6
7
|
searchForConnectorTextbox;
|
|
@@ -10,10 +11,27 @@ class ConnectorMarketplacePage {
|
|
|
10
11
|
replaceResourceButton;
|
|
11
12
|
addToProjectButton;
|
|
12
13
|
saveAsCopyButton;
|
|
14
|
+
cancelButton;
|
|
13
15
|
importModal;
|
|
16
|
+
browseAllMarketplaceDetailsButton;
|
|
14
17
|
constructor(page) {
|
|
15
18
|
this.page = page;
|
|
16
|
-
|
|
19
|
+
// The legacy marketplace modal, or camunda-hub's "Browse all" dialog,
|
|
20
|
+
// which lists Marketplace connectors alongside the diagram's own elements
|
|
21
|
+
// and reusable assets. Same shape as pages/8.7.
|
|
22
|
+
this.searchForConnectorTextbox = page
|
|
23
|
+
.getByPlaceholder('Search for a connector')
|
|
24
|
+
.or(page.locator('#browse-all-search'))
|
|
25
|
+
.or(page.getByRole('searchbox', {
|
|
26
|
+
name: 'Search elements and reusable assets',
|
|
27
|
+
}))
|
|
28
|
+
.first();
|
|
29
|
+
// A Browse all Marketplace card opens its review step ("Add to project")
|
|
30
|
+
// through this button; self-hosted connectors only link to setup docs.
|
|
31
|
+
this.browseAllMarketplaceDetailsButton = page
|
|
32
|
+
.getByRole('list', { name: 'Marketplace connectors' })
|
|
33
|
+
.getByRole('button', { name: /^View details for .+ from Marketplace$/ })
|
|
34
|
+
.first();
|
|
17
35
|
this.downloadToProjectButton = page
|
|
18
36
|
.getByRole('button', { name: 'Download to project' })
|
|
19
37
|
.first();
|
|
@@ -26,6 +44,7 @@ class ConnectorMarketplacePage {
|
|
|
26
44
|
name: 'Add to project',
|
|
27
45
|
});
|
|
28
46
|
this.saveAsCopyButton = page.getByRole('button', { name: 'Save as copy' });
|
|
47
|
+
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
29
48
|
this.importModal = page.locator('[data-test="import-modal"]');
|
|
30
49
|
}
|
|
31
50
|
async clickSearchForConnectorTextbox() {
|
|
@@ -47,7 +66,30 @@ class ConnectorMarketplacePage {
|
|
|
47
66
|
await this.replaceResourceButton.click({ timeout: 60000 });
|
|
48
67
|
}
|
|
49
68
|
async downloadConnectorToProject() {
|
|
50
|
-
|
|
69
|
+
// Browse all has no "Download to project": a result is a card that opens
|
|
70
|
+
// its own review step, which is where the import action lives. Wait for
|
|
71
|
+
// whichever entry point this shell renders before deciding -- isVisible()
|
|
72
|
+
// never waits, so probing it straight away raced the dialog's own render
|
|
73
|
+
// and fell through both branches.
|
|
74
|
+
await (0, test_1.expect)(this.downloadToProjectButton
|
|
75
|
+
.or(this.browseAllMarketplaceDetailsButton)
|
|
76
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
77
|
+
if (await this.downloadToProjectButton.isVisible()) {
|
|
78
|
+
await this.clickDownloadToProjectButton();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
|
|
82
|
+
}
|
|
83
|
+
// The Browse all review step fetches the connector's details before it
|
|
84
|
+
// renders "Add to project". Let one of the outcomes render before the
|
|
85
|
+
// probes below, which would otherwise repeat that same race.
|
|
86
|
+
await this.addToProjectButton
|
|
87
|
+
.or(this.replaceResourceButton)
|
|
88
|
+
.or(this.saveAsCopyButton)
|
|
89
|
+
.or(this.cancelButton)
|
|
90
|
+
.first()
|
|
91
|
+
.waitFor({ state: 'visible', timeout: 60000 })
|
|
92
|
+
.catch(() => { });
|
|
51
93
|
// "Download to project" opens Web Modeler's "Import resource" modal, which
|
|
52
94
|
// can take ~20s to appear. The import-confirming button depends on whether
|
|
53
95
|
// the connector template already exists in the project: an existing one
|
|
@@ -43,20 +43,44 @@ 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
|
-
.getByRole('
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
// Hub's org-management Users table is a design-system data table whose
|
|
47
|
+
// per-row action affordance is a kebab named "Row actions", not Carbon's
|
|
48
|
+
// "Options" -- a nightly screenshot shows the kebab and the trace has no
|
|
49
|
+
// "Options" node at all. Scoped to the visible tab panel so another tab's
|
|
50
|
+
// rows cannot match. Same shape as pages/8.9.
|
|
51
|
+
this.optionsButton = page
|
|
52
|
+
.getByRole('button', { name: 'Options' })
|
|
53
|
+
.or(page.getByRole('tabpanel').getByRole('button', { name: 'Row actions' }))
|
|
54
|
+
.first();
|
|
55
|
+
this.editUserMenuItem = page.getByRole('menuitem', {
|
|
56
|
+
name: /^Edit user$/i,
|
|
57
|
+
});
|
|
58
|
+
// The "Edit user" dialog's role toggles are plain Carbon Checkboxes
|
|
59
|
+
// (@camunda/ccma-saas-frontend c4-user-details-page.tsx), each its own
|
|
60
|
+
// labelText-bound <input type="checkbox">, not a role="group" of
|
|
61
|
+
// <label>s -- there is no group wrapper around them at all. Scoped to
|
|
62
|
+
// whichever dialog is open rather than one named "Edit user": a modal
|
|
63
|
+
// whose heading isn't wired via aria-labelledby carries no accessible
|
|
64
|
+
// name at all, which silently zeroes out a name-matched dialog locator.
|
|
65
|
+
const openDialog = page.getByRole('dialog');
|
|
66
|
+
// Hub renamed the platform roles, and the "Edit user" dialog states each
|
|
67
|
+
// mapping in the role's own description (confirmed live against the
|
|
68
|
+
// nightly org): "Member ... Formerly the Modeler role.",
|
|
69
|
+
// "Organization Admin ... Formerly the Admin role.", "DevOps ... Formerly
|
|
70
|
+
// the Operations Engineer role."; Analyst is unchanged. There is no
|
|
71
|
+
// "Modeler" checkbox at all any more, which is why the exact-name lookup
|
|
72
|
+
// resolved to nothing.
|
|
73
|
+
//
|
|
74
|
+
// Anchored regex rather than `exact`: each checkbox's accessible name
|
|
75
|
+
// carries that description after the label, so an exact match on the bare
|
|
76
|
+
// role name would miss it.
|
|
77
|
+
const roleCheckbox = (current, legacy) => openDialog
|
|
78
|
+
.getByRole('checkbox', { name: current })
|
|
79
|
+
.or(openDialog.getByRole('checkbox', { name: legacy, exact: true }))
|
|
80
|
+
.first();
|
|
81
|
+
this.modelerCheckbox = roleCheckbox(/^Member\b/, 'Modeler');
|
|
82
|
+
this.adminCheckbox = roleCheckbox(/^Organization Admin\b/, 'Admin');
|
|
83
|
+
this.analystCheckbox = roleCheckbox(/^Analyst\b/, 'Analyst');
|
|
60
84
|
this.confirmButton = page.getByRole('button', { name: 'Confirm' });
|
|
61
85
|
this.settingsTab = page.getByRole('tab', { name: 'Settings' });
|
|
62
86
|
this.optInButton = page.getByRole('button', { name: 'Opt-in', exact: true });
|
|
@@ -118,9 +142,16 @@ class ConsoleOrganizationPage {
|
|
|
118
142
|
name: 'danger',
|
|
119
143
|
})
|
|
120
144
|
.last();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
// Hub's org-management "Users" tab renders its table filter as a
|
|
146
|
+
// design-system <input type="search" placeholder="Search"
|
|
147
|
+
// aria-label="Search">, not Carbon's "Filter table" -- confirmed from a
|
|
148
|
+
// nightly trace's DOM snapshot, where "Filter table" occurs only inside
|
|
149
|
+
// our own call log. Keep the legacy name as a fallback. Same shape as
|
|
150
|
+
// pages/8.9.
|
|
151
|
+
this.filterTableSearchbox = page
|
|
152
|
+
.getByRole('searchbox', { name: 'Search', exact: true })
|
|
153
|
+
.or(page.getByRole('searchbox', { name: 'Filter table' }))
|
|
154
|
+
.first();
|
|
124
155
|
}
|
|
125
156
|
async filterTable(names) {
|
|
126
157
|
for (const name of names) {
|
|
@@ -189,7 +220,21 @@ class ConsoleOrganizationPage {
|
|
|
189
220
|
await this.optionsButton.click({ timeout: 60000 });
|
|
190
221
|
}
|
|
191
222
|
async clickEditUserMenuItem() {
|
|
192
|
-
|
|
223
|
+
// Hub's row menu is a Radix dropdown that animates in and re-mounts its
|
|
224
|
+
// items while opening. A live run against the nightly org resolved this
|
|
225
|
+
// item correctly -- <div role="menuitem" data-slot="dropdown-menu-item">
|
|
226
|
+
// Edit user</div> -- and still failed the click: "element is not stable"
|
|
227
|
+
// twice, then "element was detached from the DOM, retrying" until the 60s
|
|
228
|
+
// budget ran out. A detached item means the menu itself went away, so
|
|
229
|
+
// clicking harder cannot help; re-open it and retry the pair as one unit,
|
|
230
|
+
// gated on the dialog actually opening.
|
|
231
|
+
await (0, test_1.expect)(async () => {
|
|
232
|
+
if (!(await this.editUserMenuItem.isVisible().catch(() => false))) {
|
|
233
|
+
await this.clickOptionsButton();
|
|
234
|
+
}
|
|
235
|
+
await this.editUserMenuItem.click({ timeout: 10000 });
|
|
236
|
+
await (0, test_1.expect)(this.dialog.first()).toBeVisible({ timeout: 10000 });
|
|
237
|
+
}).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
|
|
193
238
|
}
|
|
194
239
|
async checkModelerCheckbox() {
|
|
195
240
|
if (!(await this.modelerCheckbox.isChecked({ timeout: 60000 }))) {
|
|
@@ -3,6 +3,9 @@ declare class FormJsPage {
|
|
|
3
3
|
private page;
|
|
4
4
|
private processApplicationPage;
|
|
5
5
|
readonly aiFormGeneratorButton: Locator;
|
|
6
|
+
readonly copilotCtaButton: Locator;
|
|
7
|
+
readonly copilotChatInput: Locator;
|
|
8
|
+
readonly copilotSendButton: Locator;
|
|
6
9
|
readonly formRequestInput: Locator;
|
|
7
10
|
readonly generateFormButton: Locator;
|
|
8
11
|
readonly formEditor: Locator;
|
|
@@ -22,6 +25,7 @@ declare class FormJsPage {
|
|
|
22
25
|
readonly deployErrorMessage: Locator;
|
|
23
26
|
constructor(page: Page);
|
|
24
27
|
generateAIForm(request?: string): Promise<void>;
|
|
28
|
+
private generateFormWithCopilot;
|
|
25
29
|
clickAIFormGeneratorButton(): Promise<void>;
|
|
26
30
|
clickFormRequestInput(): Promise<void>;
|
|
27
31
|
fillFormRequestInput(formRequest: string): Promise<void>;
|
|
@@ -11,6 +11,9 @@ class FormJsPage {
|
|
|
11
11
|
page;
|
|
12
12
|
processApplicationPage;
|
|
13
13
|
aiFormGeneratorButton;
|
|
14
|
+
copilotCtaButton;
|
|
15
|
+
copilotChatInput;
|
|
16
|
+
copilotSendButton;
|
|
14
17
|
formRequestInput;
|
|
15
18
|
generateFormButton;
|
|
16
19
|
formEditor;
|
|
@@ -34,6 +37,16 @@ class FormJsPage {
|
|
|
34
37
|
this.aiFormGeneratorButton = page.getByRole('button', {
|
|
35
38
|
name: 'AI Form Generator',
|
|
36
39
|
});
|
|
40
|
+
// camunda-hub #21398 retired the form page's AI Form Generator in favour
|
|
41
|
+
// of the Camunda Copilot sidecar: with One Copilot on, the empty form
|
|
42
|
+
// offers "Try Camunda Copilot" instead (FormDs.tsx / CopilotCTA.tsx), and
|
|
43
|
+
// the sidecar's chat input and send button come from
|
|
44
|
+
// camunda-copilot-client's ChatInput. Same shape as pages/8.7.
|
|
45
|
+
this.copilotCtaButton = page.getByRole('button', {
|
|
46
|
+
name: 'Try Camunda Copilot',
|
|
47
|
+
});
|
|
48
|
+
this.copilotChatInput = page.getByPlaceholder('Type a message...');
|
|
49
|
+
this.copilotSendButton = page.getByRole('button', { name: 'Send message' });
|
|
37
50
|
this.formRequestInput = page.getByPlaceholder('A form for an american standard loan request');
|
|
38
51
|
this.generateFormButton = page.getByRole('button', { name: 'Generate form' });
|
|
39
52
|
this.formEditor = page.getByLabel('Form Definition');
|
|
@@ -89,9 +102,13 @@ class FormJsPage {
|
|
|
89
102
|
this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
|
|
90
103
|
}
|
|
91
104
|
async generateAIForm(request = 'Create a form with the following fields: 1. A text field with the label "Full Name" 2. A number with the label "Count" 3. A date input with the label "Date of birth"4. A Checkbox with the label "Agree"') {
|
|
92
|
-
await (0, test_1.expect)(this.aiFormGeneratorButton).toBeVisible({
|
|
105
|
+
await (0, test_1.expect)(this.aiFormGeneratorButton.or(this.copilotCtaButton).first()).toBeVisible({
|
|
93
106
|
timeout: 90000,
|
|
94
107
|
});
|
|
108
|
+
if (!(await this.aiFormGeneratorButton.isVisible())) {
|
|
109
|
+
await this.generateFormWithCopilot(request);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
95
112
|
await this.clickAIFormGeneratorButton();
|
|
96
113
|
await this.clickFormRequestInput();
|
|
97
114
|
await this.fillFormRequestInput(request);
|
|
@@ -100,6 +117,19 @@ class FormJsPage {
|
|
|
100
117
|
await (0, test_1.expect)(this.page.getByText('Successfully generated smart form.')).toBeVisible({ timeout: 300000 });
|
|
101
118
|
await (0, sleep_1.sleep)(60000);
|
|
102
119
|
}
|
|
120
|
+
async generateFormWithCopilot(request) {
|
|
121
|
+
await this.copilotCtaButton.click({ timeout: 30000 });
|
|
122
|
+
await (0, test_1.expect)(this.copilotChatInput).toBeVisible({ timeout: 60000 });
|
|
123
|
+
await this.copilotChatInput.fill(request);
|
|
124
|
+
await this.copilotSendButton.click({ timeout: 30000 });
|
|
125
|
+
// Copilot edits the open form in place; the generated form is ready once
|
|
126
|
+
// every requested field shows up on the canvas.
|
|
127
|
+
for (const label of ['Full Name', 'Count', 'Date of birth', 'Agree']) {
|
|
128
|
+
await (0, test_1.expect)(this.formEditor.getByText(label).first()).toBeVisible({
|
|
129
|
+
timeout: 300000,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
103
133
|
async clickAIFormGeneratorButton() {
|
|
104
134
|
await this.aiFormGeneratorButton.click();
|
|
105
135
|
}
|
|
@@ -71,6 +71,7 @@ declare class ModelerCreatePage {
|
|
|
71
71
|
readonly publicAccessToggle: Locator;
|
|
72
72
|
readonly publicationTab: Locator;
|
|
73
73
|
readonly copyPublicLinkButton: Locator;
|
|
74
|
+
readonly publicLinkCodeBlock: Locator;
|
|
74
75
|
readonly closeButton: Locator;
|
|
75
76
|
readonly rbaEnabledDeployedDialog: Locator;
|
|
76
77
|
readonly viewProcessLink: Locator;
|
|
@@ -214,9 +215,11 @@ declare class ModelerCreatePage {
|
|
|
214
215
|
fillJobTypeInput(name: string): Promise<void>;
|
|
215
216
|
private suppressTestModeAnnouncement;
|
|
216
217
|
switchToPlay(): Promise<void>;
|
|
218
|
+
private selectPlayDeploySingleResource;
|
|
217
219
|
completePlayConfiguration(clusterName: string): Promise<void>;
|
|
218
220
|
embedForm(formName: string): Promise<void>;
|
|
219
221
|
fillStartEventVariablesForDocHandling(zeebeUrl: string, zeebeClientId: string, zeebeClientSecret: string): Promise<void>;
|
|
222
|
+
private openOutputMappingEntry;
|
|
220
223
|
fillZeebeClientId(clientId: string): Promise<void>;
|
|
221
224
|
fillZeebeSecretValue(zeebeSecret: string): Promise<void>;
|
|
222
225
|
fillZeebeRestValue(zeebeUrl: string): Promise<void>;
|