@camunda/e2e-test-suite 0.0.1250 → 0.0.1252
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 +24 -12
- package/dist/pages/8.9/ClusterDetailsPage.js +8 -1
- package/dist/pages/8.9/ClusterPage.js +22 -20
- package/dist/pages/8.9/HomePage.js +6 -1
- package/dist/pages/8.9/ProcessApplicationPage.js +30 -10
- package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +17 -0
- package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
- package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
- package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
- package/dist/pages/SM-8.10/ClusterPage.js +3 -1
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
- package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
- package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
- package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
- package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
- package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
- package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
- package/dist/pages/SM-8.7/NavigationPage.js +2 -1
- package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.10/smoke-tests.spec.js +2 -1
- package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
- package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.7/smoke-tests.spec.js +2 -1
- package/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.8/smoke-tests.spec.js +2 -1
- package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.9/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
- package/dist/utils/modelerCluster.d.ts +4 -6
- package/dist/utils/modelerCluster.js +55 -32
- package/package.json +1 -1
- package/playwright.config.ts +18 -5
|
@@ -9,6 +9,8 @@ const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
|
9
9
|
class ModelerCreatePage {
|
|
10
10
|
page;
|
|
11
11
|
processApplicationPage;
|
|
12
|
+
pendingFileSaves = 0;
|
|
13
|
+
lastFileSaveActivity = 0;
|
|
12
14
|
generalPanel;
|
|
13
15
|
taskDefinitionPanel;
|
|
14
16
|
processIdInput;
|
|
@@ -43,7 +45,6 @@ class ModelerCreatePage {
|
|
|
43
45
|
restConnectorOption;
|
|
44
46
|
changeElementSearchInput;
|
|
45
47
|
changeElementResults;
|
|
46
|
-
changeElementTabStrip;
|
|
47
48
|
reusableAssetsTab;
|
|
48
49
|
marketPlaceButton;
|
|
49
50
|
clientIdTextbox;
|
|
@@ -108,6 +109,23 @@ class ModelerCreatePage {
|
|
|
108
109
|
constructor(page) {
|
|
109
110
|
this.page = page;
|
|
110
111
|
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
112
|
+
// Track the modeler's debounced autosave (PATCH /files/{id}) so a deploy
|
|
113
|
+
// can wait until the server holds the latest edits. See
|
|
114
|
+
// waitForFileSavesToSettle().
|
|
115
|
+
page.on('request', (request) => {
|
|
116
|
+
if (ModelerCreatePage.isFileSave(request)) {
|
|
117
|
+
this.pendingFileSaves++;
|
|
118
|
+
this.lastFileSaveActivity = Date.now();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const settleFileSave = (request) => {
|
|
122
|
+
if (ModelerCreatePage.isFileSave(request)) {
|
|
123
|
+
this.pendingFileSaves = Math.max(0, this.pendingFileSaves - 1);
|
|
124
|
+
this.lastFileSaveActivity = Date.now();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
page.on('requestfinished', settleFileSave);
|
|
128
|
+
page.on('requestfailed', settleFileSave);
|
|
111
129
|
this.generalPanel = page.locator('[data-group-id="group-general"]');
|
|
112
130
|
this.taskDefinitionPanel = page
|
|
113
131
|
.locator('div')
|
|
@@ -211,17 +229,17 @@ class ModelerCreatePage {
|
|
|
211
229
|
.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
|
|
212
230
|
this.changeElementSearchInput = page.locator('.djs-popup-search input');
|
|
213
231
|
this.changeElementResults = page.locator('.djs-popup-results');
|
|
214
|
-
this.changeElementTabStrip = page.locator('.djs-popup-tabs-container');
|
|
215
232
|
// The tab renders its label as text content, and the product source
|
|
216
233
|
// spells it both "Reusable Assets" and "Reusable assets".
|
|
217
234
|
this.reusableAssetsTab = page.getByRole('tab', {
|
|
218
235
|
name: /reusable assets/i,
|
|
219
236
|
});
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
237
|
+
// Browse All replaces the legacy Marketplace entry only in large menus
|
|
238
|
+
// when the Design System is enabled; small or DS-off menus keep the
|
|
239
|
+
// original entry.
|
|
240
|
+
this.marketPlaceButton = page
|
|
241
|
+
.locator('[data-id="hub-browse-all"]')
|
|
242
|
+
.or(page.locator('[data-id="browseConnectors"]'));
|
|
225
243
|
this.clientIdTextbox = page.getByLabel('Client ID');
|
|
226
244
|
this.clientSecretTextbox = page.getByLabel('Client secret');
|
|
227
245
|
this.rememberCredentialsCheckbox = page.getByText('Remember credentials');
|
|
@@ -455,9 +473,13 @@ class ModelerCreatePage {
|
|
|
455
473
|
await legacyItem.click({ timeout: 90000 });
|
|
456
474
|
}
|
|
457
475
|
async runProcessInstance(variables = '', tenant = '', businessId = '') {
|
|
476
|
+
// ensureStageCluster may navigate away from the diagram, which would drop
|
|
477
|
+
// a save that is still debounced, so settle before and after it.
|
|
478
|
+
await this.waitForFileSavesToSettle();
|
|
458
479
|
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
459
|
-
await this.
|
|
480
|
+
await this.waitForFileSavesToSettle();
|
|
460
481
|
const maxRetries = 3;
|
|
482
|
+
let executeSucceeded = false;
|
|
461
483
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
462
484
|
try {
|
|
463
485
|
await this.clickDeployAndRunMainButton();
|
|
@@ -482,9 +504,16 @@ class ModelerCreatePage {
|
|
|
482
504
|
});
|
|
483
505
|
await this.businessIdInput.fill(businessId);
|
|
484
506
|
}
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
507
|
+
const [executeResponse] = await Promise.all([
|
|
508
|
+
this.page.waitForResponse((response) => response.request().method() === 'POST' &&
|
|
509
|
+
// Workspace-era Hub builds execute via /v2/projects/{id}; older
|
|
510
|
+
// builds used /process-applications/{id}.
|
|
511
|
+
/\/(process-applications|projects)\/[^/]+\/execute$/.test(new URL(response.url()).pathname), { timeout: 30000 }),
|
|
512
|
+
this.clickDeployAndRunSubButton(),
|
|
513
|
+
]);
|
|
514
|
+
(0, test_1.expect)(executeResponse.ok(), await executeResponse.text()).toBeTruthy();
|
|
515
|
+
executeSucceeded = true;
|
|
516
|
+
break;
|
|
488
517
|
}
|
|
489
518
|
catch (error) {
|
|
490
519
|
if (attempt < maxRetries - 1) {
|
|
@@ -497,12 +526,33 @@ class ModelerCreatePage {
|
|
|
497
526
|
}
|
|
498
527
|
}
|
|
499
528
|
}
|
|
529
|
+
if (executeSucceeded) {
|
|
530
|
+
await this.instanceStartedAssertion();
|
|
531
|
+
}
|
|
500
532
|
}
|
|
501
533
|
// Test mode mounts a one-time "A new way to test your processes" modal
|
|
502
534
|
// (camunda-hub TestAnnouncementModal) that covers the setup steps behind it.
|
|
503
535
|
// It renders unless localStorage already carries the seen flag, which it
|
|
504
536
|
// reads on mount; a fresh Playwright context always starts empty, so set the
|
|
505
537
|
// flag before entering Test mode rather than clicking the modal away.
|
|
538
|
+
static isFileSave(request) {
|
|
539
|
+
return (request.method() === 'PATCH' &&
|
|
540
|
+
/\/api\/internal\/files\/[^/]+$/.test(new URL(request.url()).pathname));
|
|
541
|
+
}
|
|
542
|
+
// Deploy & run deploys the diagram the server holds, not what is on screen.
|
|
543
|
+
// Hub debounces edits for roughly a second before it sends
|
|
544
|
+
// PATCH /files/{id}, and the autosave label keeps showing the previous
|
|
545
|
+
// "Autosaved at ..." time during that window, so the label cannot prove the
|
|
546
|
+
// latest edits are stored. Wait until no file save is in flight and none
|
|
547
|
+
// has started for longer than the debounce, measured from this call, so a
|
|
548
|
+
// save queued by edits made just before it is observed and awaited.
|
|
549
|
+
async waitForFileSavesToSettle(quietMs = 2000, timeout = 60000) {
|
|
550
|
+
const start = Date.now();
|
|
551
|
+
await test_1.expect
|
|
552
|
+
.poll(() => this.pendingFileSaves === 0 &&
|
|
553
|
+
Date.now() - Math.max(start, this.lastFileSaveActivity) >= quietMs, { timeout, intervals: [250] })
|
|
554
|
+
.toBe(true);
|
|
555
|
+
}
|
|
506
556
|
async suppressTestModeAnnouncement() {
|
|
507
557
|
await this.page.evaluate(() => localStorage.setItem('play-testAnnouncementSeen', 'true'));
|
|
508
558
|
}
|
|
@@ -996,15 +1046,13 @@ class ModelerCreatePage {
|
|
|
996
1046
|
// before bpmn-js attaches its search handler during the popup re-render.
|
|
997
1047
|
async searchAndClickRestConnector() {
|
|
998
1048
|
await (0, test_1.expect)(this.changeElementSearchInput).toBeVisible({ timeout: 30000 });
|
|
999
|
-
await this.changeElementSearchInput.click();
|
|
1000
|
-
await this.changeElementSearchInput.fill('');
|
|
1001
1049
|
// 'Send REST Request' is the template's current name -- connectors #7917
|
|
1002
1050
|
// renamed it from 'REST Outbound Connector' on 2026-07-15, and "Outbound"
|
|
1003
1051
|
// now appears in no searchable field, so the old filter matched nothing.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout:
|
|
1052
|
+
// Templates live under the popup's "Reusable assets" tab, so searching the
|
|
1053
|
+
// BPMN tab alone never lists them.
|
|
1054
|
+
await this.revealChangeElementTemplate(this.restConnectorOption, 'Send REST Request');
|
|
1055
|
+
await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 30000 });
|
|
1008
1056
|
await this.restConnectorOption.click({ timeout: 90000 });
|
|
1009
1057
|
}
|
|
1010
1058
|
async clickRestConnectorOption(recoverSession) {
|
|
@@ -1106,38 +1154,10 @@ class ModelerCreatePage {
|
|
|
1106
1154
|
name: /^Configure (test case|scenario)$/,
|
|
1107
1155
|
}))
|
|
1108
1156
|
.first();
|
|
1109
|
-
// Play settles into exactly ONE of three states, and which one is not
|
|
1110
|
-
// knowable up front, so race them instead of timing out on each in turn:
|
|
1111
|
-
// the setup overlay, the legacy Continue button, or the configure-test
|
|
1112
|
-
// panel (meaning setup is already done for this diagram).
|
|
1113
|
-
//
|
|
1114
|
-
// Waiting only for the first two and reading "neither" as "already ready"
|
|
1115
|
-
// was wrong once the diagram moved inside a process application.
|
|
1116
|
-
// ConfigurationOverlay renders null until isDeploymentStateValidated, and
|
|
1117
|
-
// it sets that flag synchronously ONLY when processApplicationId is
|
|
1118
|
-
// undefined -- exactly what a project-root diagram used to give it. With
|
|
1119
|
-
// the diagram in a process application the id is defined, so the overlay
|
|
1120
|
-
// must first await checkExistingDeployment(). The old 15s probe raced that
|
|
1121
|
-
// async call, concluded "already ready" and returned while the overlay was
|
|
1122
|
-
// still about to appear. ConfigureTestPanel returns null for as long as the
|
|
1123
|
-
// overlay is up, so the Start button never existed and
|
|
1124
|
-
// clickStartInstanceButton burned its entire retry budget looking for it.
|
|
1125
|
-
// camunda-hub's "Play" -> "Test Studio" rebrand renamed this container's
|
|
1126
|
-
// data-test from `play-configuration-overlay` to
|
|
1127
|
-
// `test-studio-configuration-overlay`, and a later build dropped both
|
|
1128
|
-
// container data-test attributes entirely (neither is present in the
|
|
1129
|
-
// failing run's trace DOM), so those two gates now match nothing. The
|
|
1130
|
-
// always-present anchor is the panel's heading, which the current build
|
|
1131
|
-
// renders as "Set up environment" (verb, two words) -- the old
|
|
1132
|
-
// `getByText('Setup environment')` (one word) never matched it and the
|
|
1133
|
-
// whole chain timed out. Match the heading with a spelling-tolerant regex
|
|
1134
|
-
// so a future flip between the two forms cannot re-break the gate, and keep
|
|
1135
|
-
// the legacy data-test attributes for older modeler builds. This locator
|
|
1136
|
-
// means "the setup panel is still on screen".
|
|
1137
1157
|
const configurationOverlay = this.page
|
|
1138
1158
|
.locator('[data-test="test-studio-configuration-overlay"]')
|
|
1139
1159
|
.or(this.page.locator('[data-test="play-configuration-overlay"]'))
|
|
1140
|
-
.or(this.page.getByText(/set
|
|
1160
|
+
.or(this.page.getByText(/set up test run/i))
|
|
1141
1161
|
.first();
|
|
1142
1162
|
const configureTestPanel = this.page.getByTestId('configure-test-panel');
|
|
1143
1163
|
await (0, test_1.expect)(configurationOverlay
|
|
@@ -1156,18 +1176,23 @@ class ModelerCreatePage {
|
|
|
1156
1176
|
if (isNewFlow) {
|
|
1157
1177
|
// Step 1: connect cluster if not yet connected (modal with cluster dropdown)
|
|
1158
1178
|
const connectClusterButton = this.page.getByRole('button', {
|
|
1159
|
-
name: '
|
|
1179
|
+
name: 'Select environment',
|
|
1160
1180
|
});
|
|
1161
1181
|
const needsConnect = await connectClusterButton
|
|
1162
1182
|
.isVisible({ timeout: 3000 })
|
|
1163
1183
|
.catch(() => false);
|
|
1164
1184
|
if (needsConnect) {
|
|
1165
1185
|
await connectClusterButton.click({ timeout });
|
|
1186
|
+
// camunda-hub's TestModeDeployEndpointConfigModal replaced the
|
|
1187
|
+
// "Configure environment" combobox dialog with a "Choose where to
|
|
1188
|
+
// run" dialog: environments now render as a radio group (one radio
|
|
1189
|
+
// per environment), not a dropdown. The first environment is
|
|
1190
|
+
// auto-selected once the list loads, but click it explicitly rather
|
|
1191
|
+
// than relying on that default.
|
|
1166
1192
|
const configureEnvDialog = this.page
|
|
1167
1193
|
.getByRole('dialog')
|
|
1168
|
-
.filter({ hasText: '
|
|
1169
|
-
await configureEnvDialog.getByRole('
|
|
1170
|
-
await this.page.getByRole('option').first().click({ timeout });
|
|
1194
|
+
.filter({ hasText: 'Choose where to run' });
|
|
1195
|
+
await configureEnvDialog.getByRole('radio').first().click({ timeout });
|
|
1171
1196
|
await configureEnvDialog
|
|
1172
1197
|
.getByRole('button', { name: 'Save' })
|
|
1173
1198
|
.click({ timeout });
|
|
@@ -1255,7 +1280,9 @@ class ModelerCreatePage {
|
|
|
1255
1280
|
if (await this.isVisibleWithin(option, 5000)) {
|
|
1256
1281
|
return true;
|
|
1257
1282
|
}
|
|
1258
|
-
|
|
1283
|
+
// The tab strip's container class changed across Hub builds; probe the
|
|
1284
|
+
// "Reusable assets" tab itself instead of its wrapper.
|
|
1285
|
+
if (await this.isVisibleWithin(this.reusableAssetsTab, 5000)) {
|
|
1259
1286
|
// A failed tab click leaves the search path intact, so do not throw.
|
|
1260
1287
|
await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
|
|
1261
1288
|
if (await this.isVisibleWithin(option, 15000)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Page, Locator } from '@playwright/test';
|
|
1
|
+
import { Browser, Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ModelerHomePage {
|
|
3
3
|
private page;
|
|
4
4
|
private defaultFolderName;
|
|
@@ -57,6 +57,7 @@ declare class ModelerHomePage {
|
|
|
57
57
|
username: string;
|
|
58
58
|
password: string;
|
|
59
59
|
}): Promise<void>;
|
|
60
|
+
assignWorkspaceEnvironmentAsOrgAdmin(browser: Browser): Promise<void>;
|
|
60
61
|
clickCrossComponentProjectFolder(credentials?: {
|
|
61
62
|
username: string;
|
|
62
63
|
password: string;
|
|
@@ -349,6 +349,30 @@ class ModelerHomePage {
|
|
|
349
349
|
// under flag-on that folder maps to a Workspace.
|
|
350
350
|
await this.ensureWorkspaceOpen(credentials);
|
|
351
351
|
}
|
|
352
|
+
// Only an org admin can assign Environments to a workspace (Hub 8.10), so a
|
|
353
|
+
// workspace created by a plain Member has nothing to deploy to until one does.
|
|
354
|
+
// Call while this page is inside that workspace: assigns an Environment as the
|
|
355
|
+
// default Identity first user (an org admin) in a separate browser context,
|
|
356
|
+
// leaving this page's session untouched.
|
|
357
|
+
async assignWorkspaceEnvironmentAsOrgAdmin(browser) {
|
|
358
|
+
const workspaceUrl = this.page.url();
|
|
359
|
+
const context = await browser.newContext({
|
|
360
|
+
baseURL: new URL(workspaceUrl).origin,
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
const adminPage = await context.newPage();
|
|
364
|
+
await new NavigationPage_1.NavigationPage(adminPage).goToModeler();
|
|
365
|
+
await new ProcessApplicationPage_1.ProcessApplicationPage(adminPage).assignEnvironmentToWorkspace(workspaceUrl, process.env.MODELER_CLUSTER_NAME);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await context.close();
|
|
369
|
+
}
|
|
370
|
+
// This page loaded the workspace before the admin assigned the
|
|
371
|
+
// Environment in the other session, so Hub still holds the empty
|
|
372
|
+
// environment list and Deploy & run has no target until it refetches.
|
|
373
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
374
|
+
this.processApplicationPage.markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl);
|
|
375
|
+
}
|
|
352
376
|
async clickCrossComponentProjectFolder(credentials) {
|
|
353
377
|
// Kept name: callers use this to (re-)enter the shared Workspace's projects
|
|
354
378
|
// listing after leaving Modeler. Creates the Workspace if it is somehow
|
|
@@ -119,7 +119,14 @@ class OperateHomePage {
|
|
|
119
119
|
// yields `"updatedValue""` -- invalid JSON, and Apply rejects it exactly
|
|
120
120
|
// like the per-character version did. Same recipe as
|
|
121
121
|
// pages/c8Run-8.10/OperateHomePage.ts.
|
|
122
|
-
|
|
122
|
+
//
|
|
123
|
+
// Select with Home + Shift+End rather than ControlOrMeta+A: headless
|
|
124
|
+
// Chromium on macOS does not apply Meta+A as select-all inside Monaco's
|
|
125
|
+
// native edit context, so the insert landed mid-string. The value is a
|
|
126
|
+
// single-line JSON string, so selecting the line selects the document on
|
|
127
|
+
// every platform.
|
|
128
|
+
await this.page.keyboard.press('Home');
|
|
129
|
+
await this.page.keyboard.press('Shift+End');
|
|
123
130
|
await this.page.keyboard.insertText(`"${newValue}`);
|
|
124
131
|
}
|
|
125
132
|
async clickSaveVariableButton() {
|
|
@@ -52,10 +52,19 @@ class OptimizeHomePage {
|
|
|
52
52
|
}
|
|
53
53
|
async assertProcessHasBeenImported(processId) {
|
|
54
54
|
await this.clickDashboardLink();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// The process list is paginated, so on a cluster with more than one page
|
|
56
|
+
// of processes the new one is not rendered until the list is filtered by
|
|
57
|
+
// name. Import is asynchronous, so reload and re-filter until it appears.
|
|
58
|
+
const search = this.page
|
|
59
|
+
.getByPlaceholder(/Search for name/i)
|
|
60
|
+
.or(this.page.getByPlaceholder('Filter table'))
|
|
61
|
+
.first();
|
|
62
|
+
const processLink = this.page.getByRole('link', { name: processId });
|
|
63
|
+
await (0, test_1.expect)(async () => {
|
|
64
|
+
await this.page.reload();
|
|
65
|
+
await search.fill(processId, { timeout: 30000 });
|
|
66
|
+
await (0, test_1.expect)(processLink).toBeVisible({ timeout: 15000 });
|
|
67
|
+
}).toPass({ timeout: 180000, intervals: [5000, 10000, 15000] });
|
|
59
68
|
}
|
|
60
69
|
async clickOpenUserSettingsButton() {
|
|
61
70
|
await this.openUserSettingsButton.click();
|
|
@@ -12,19 +12,24 @@ declare class ProcessApplicationPage {
|
|
|
12
12
|
readonly titleBar: Locator;
|
|
13
13
|
readonly createNewDropdown: Locator;
|
|
14
14
|
readonly inlineNameInput: Locator;
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
15
|
+
readonly manageEnvironmentsHeading: Locator;
|
|
16
|
+
readonly assignedEnvironmentCards: Locator;
|
|
17
|
+
readonly editEnvironmentsButton: Locator;
|
|
18
|
+
readonly environmentEditorRegion: Locator;
|
|
19
|
+
readonly saveEnvironmentsButton: Locator;
|
|
18
20
|
constructor(page: Page);
|
|
19
21
|
private pageReady;
|
|
20
22
|
waitForProcessApplication(timeout?: number): Promise<void>;
|
|
21
23
|
clickCreateProcessApplicationButton(): Promise<void>;
|
|
22
24
|
clickCreateProcessApplicationOption(): Promise<void>;
|
|
23
25
|
nameNewProcessApplication(name: string): Promise<void>;
|
|
24
|
-
private defineDevelopmentStage;
|
|
25
26
|
rememberResourceContainer(url: string): void;
|
|
27
|
+
private workspaceEnvironmentsUrl;
|
|
26
28
|
ensureStageCluster(clusterName?: string): Promise<void>;
|
|
27
|
-
|
|
29
|
+
assignEnvironmentToWorkspace(workspaceUrl: string, clusterName?: string): Promise<void>;
|
|
30
|
+
markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl: string): void;
|
|
31
|
+
private workspaceId;
|
|
32
|
+
private assignWorkspaceEnvironment;
|
|
28
33
|
openByName(name: string): Promise<void>;
|
|
29
34
|
}
|
|
30
35
|
export { ProcessApplicationPage };
|
|
@@ -6,6 +6,10 @@ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModal
|
|
|
6
6
|
// ModelerHomePage creates the container, while ModelerCreatePage later binds
|
|
7
7
|
// its stage. Both page objects share the same Playwright Page.
|
|
8
8
|
const resourceContainers = new WeakMap();
|
|
9
|
+
// Workspace ids whose Environment an org admin assigned from a separate session
|
|
10
|
+
// (see assignEnvironmentToWorkspace), keyed by the non-admin user's Page. That
|
|
11
|
+
// user cannot open the workspace Environments tab, so ensureStageCluster skips it.
|
|
12
|
+
const adminAssignedWorkspaces = new WeakMap();
|
|
9
13
|
// Flag-on Self-Managed 8.10 (camunda-hub#27903: workspaces-enabled=true) renamed
|
|
10
14
|
// the entity hierarchy: the legacy "project folder" is now a Workspace and the
|
|
11
15
|
// legacy "process application" is now a Project (camunda-hub#28005). A Project is
|
|
@@ -14,11 +18,19 @@ const resourceContainers = new WeakMap();
|
|
|
14
18
|
// from the workspace's projects listing via the "Create project" split button +
|
|
15
19
|
// dialog, and the app lands on `/workspace/{wsId}/projects/{projectId}`.
|
|
16
20
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
21
|
+
// camunda-hub#29210 ("Environments phase1") then moved cluster assignment up a
|
|
22
|
+
// level again: a Project no longer has its own "Define stages" dialog or
|
|
23
|
+
// "Connected clusters" card at all. Assignment is now an Environment (backed by
|
|
24
|
+
// a registered cluster) assigned to the whole Workspace via its Environments
|
|
25
|
+
// settings tab (`/workspaces/{wsId}/settings?tab=environments`), shared by every
|
|
26
|
+
// Project inside that Workspace.
|
|
27
|
+
//
|
|
28
|
+
// This page object owns that container's surface (creating a Project) plus the
|
|
29
|
+
// Workspace-level Environment assignment Deploy/Play need. Entering and leaving
|
|
30
|
+
// it is driven from ModelerHomePage so the ~150 existing spec call sites keep
|
|
31
|
+
// working unchanged. Method names are kept from the flag-off era
|
|
32
|
+
// ("...ProcessApplication...", "...StageCluster...") so the callers do not have
|
|
33
|
+
// to change.
|
|
22
34
|
class ProcessApplicationPage {
|
|
23
35
|
page;
|
|
24
36
|
// The workspace projects listing's "Create project" split button. exact:true
|
|
@@ -40,9 +52,15 @@ class ProcessApplicationPage {
|
|
|
40
52
|
titleBar;
|
|
41
53
|
createNewDropdown;
|
|
42
54
|
inlineNameInput;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
// Workspace management -> Environments tab (camunda-hub#29210). Page-scoped,
|
|
56
|
+
// not container-scoped: this tab lives on a different page
|
|
57
|
+
// (`/workspaces/{wsId}/settings`) than the Project detail these other
|
|
58
|
+
// locators target.
|
|
59
|
+
manageEnvironmentsHeading;
|
|
60
|
+
assignedEnvironmentCards;
|
|
61
|
+
editEnvironmentsButton;
|
|
62
|
+
environmentEditorRegion;
|
|
63
|
+
saveEnvironmentsButton;
|
|
46
64
|
constructor(page) {
|
|
47
65
|
this.page = page;
|
|
48
66
|
this.createProcessApplicationButton = page
|
|
@@ -95,11 +113,20 @@ class ProcessApplicationPage {
|
|
|
95
113
|
.or(page.getByRole('button', { name: 'Create new' }))
|
|
96
114
|
.first();
|
|
97
115
|
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
116
|
+
// "Assigned environments" heading: WorkspaceEnvironmentsTab's ready signal
|
|
117
|
+
// (it only renders once the workspace-scoped read has resolved).
|
|
118
|
+
this.manageEnvironmentsHeading = page.locator('[data-test="manage-environments-heading"]');
|
|
119
|
+
this.assignedEnvironmentCards = page.locator('[data-test="environment-card"]');
|
|
120
|
+
// Reads "Add environments" with none assigned yet, "Edit environments"
|
|
121
|
+
// otherwise -- same data-test either way.
|
|
122
|
+
this.editEnvironmentsButton = page.locator('[data-test="edit-environments-button"]');
|
|
123
|
+
this.environmentEditorRegion = page.getByRole('group', {
|
|
124
|
+
name: 'Edit assigned environments',
|
|
125
|
+
});
|
|
126
|
+
// Page-scoped, not environmentEditorRegion-scoped: Save/Cancel are
|
|
127
|
+
// rendered in the tab's header row, a SIBLING of the "Edit assigned
|
|
128
|
+
// environments" group, not a descendant of it.
|
|
129
|
+
this.saveEnvironmentsButton = page.getByRole('button', {
|
|
103
130
|
name: 'Save',
|
|
104
131
|
exact: true,
|
|
105
132
|
});
|
|
@@ -152,82 +179,150 @@ class ProcessApplicationPage {
|
|
|
152
179
|
await this.inlineNameInput.press('Enter');
|
|
153
180
|
await this.waitForProcessApplication();
|
|
154
181
|
}
|
|
155
|
-
async defineDevelopmentStage(clusterName) {
|
|
156
|
-
await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 60000 });
|
|
157
|
-
await this.developmentClusterSelect.click({ timeout: 30000 });
|
|
158
|
-
const escapedClusterName = clusterName?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
159
|
-
// The listbox leads with a "No cluster" entry that unassigns the stage; the
|
|
160
|
-
// placeholder "Select a development cluster" is only the trigger's text, not an
|
|
161
|
-
// option. Excluding just the placeholder leaves "No cluster" first, so an
|
|
162
|
-
// unnamed pick binds no cluster and saves defaultDevClusterId=null -- which
|
|
163
|
-
// makes the later Deploy dialog report "Clusters not configured for stages".
|
|
164
|
-
// Exclude "No cluster" too so the fallback lands on a real cluster.
|
|
165
|
-
const option = clusterName
|
|
166
|
-
? this.page
|
|
167
|
-
.getByRole('option', { name: new RegExp(`^${escapedClusterName}`) })
|
|
168
|
-
.first()
|
|
169
|
-
: this.page
|
|
170
|
-
.getByRole('option')
|
|
171
|
-
.filter({ hasNotText: 'Select a development cluster' })
|
|
172
|
-
.filter({ hasNotText: 'No cluster' })
|
|
173
|
-
.first();
|
|
174
|
-
await option.click({ timeout: 30000 });
|
|
175
|
-
await this.defineStagesSaveButton.click({ timeout: 30000 });
|
|
176
|
-
await (0, test_1.expect)(this.defineStagesDialog).toBeHidden({ timeout: 60000 });
|
|
177
|
-
}
|
|
178
182
|
rememberResourceContainer(url) {
|
|
179
183
|
resourceContainers.set(this.page, { url });
|
|
180
184
|
}
|
|
185
|
+
// Derives the shared Workspace's Environments settings URL from a Project
|
|
186
|
+
// detail URL (".../workspaces/<wsId>/projects/<projectId>[?...]"): the
|
|
187
|
+
// origin/app-root plus "/workspaces/<wsId>/settings?tab=environments" -- the
|
|
188
|
+
// same deep link camunda-hub's own useManageEnvironmentsLink builds for the
|
|
189
|
+
// deploy dialog's "Manage environments" link. Always emits the plural
|
|
190
|
+
// "workspaces" regardless of which spelling the container URL used
|
|
191
|
+
// (ModelerHomePage.openWorkspaceTile still tolerates a singular
|
|
192
|
+
// "/workspace/<id>/projects" from camunda-hub#28928 not yet being merged
|
|
193
|
+
// everywhere) -- the settings route itself (App.tsx) is plural-only, with no
|
|
194
|
+
// singular alias, so reusing a singular prefix verbatim would 404.
|
|
195
|
+
workspaceEnvironmentsUrl(containerUrl) {
|
|
196
|
+
const match = containerUrl.match(/^(.*)\/workspaces?\/([^/?#]+)\//);
|
|
197
|
+
if (!match) {
|
|
198
|
+
throw new Error(`Could not derive the workspace Environments URL from "${containerUrl}": ` +
|
|
199
|
+
'expected a "/workspaces/<id>/..." segment.');
|
|
200
|
+
}
|
|
201
|
+
const [, origin, workspaceId] = match;
|
|
202
|
+
return `${origin}/workspaces/${workspaceId}/settings?tab=environments`;
|
|
203
|
+
}
|
|
204
|
+
// Assigns an Environment to the shared Workspace via its Environments
|
|
205
|
+
// settings tab, so Deploy/Play (and Play's "Configure environment") find a
|
|
206
|
+
// usable target instead of rendering "No environments assigned" and staying
|
|
207
|
+
// disabled -- camunda-hub#29210 moved this out of the Project detail's own
|
|
208
|
+
// "Define stages" dialog and up to the Workspace, shared by every Project
|
|
209
|
+
// inside it.
|
|
210
|
+
//
|
|
211
|
+
// Checks the read-only "Assigned environments" list before entering edit
|
|
212
|
+
// mode, so a Workspace another (parallel) test already assigned is left
|
|
213
|
+
// alone regardless of this Page's own memo flag below -- required because
|
|
214
|
+
// every test's Project lives in the one shared Workspace. This is a
|
|
215
|
+
// best-effort check, not a lock: two tests racing to assign at once each
|
|
216
|
+
// still write their own snapshot of the selected set (Save persists the
|
|
217
|
+
// whole set, it does not merge), so a genuine concurrent race could still
|
|
218
|
+
// trigger two writes. It converges correctly here only because every test
|
|
219
|
+
// targets the one MODELER_CLUSTER_NAME (or, when unset, is happy with
|
|
220
|
+
// whichever single Environment ends up assigned).
|
|
181
221
|
async ensureStageCluster(clusterName) {
|
|
182
222
|
const container = resourceContainers.get(this.page);
|
|
183
223
|
if (!container ||
|
|
184
224
|
(container.stageBound && container.developmentCluster === clusterName)) {
|
|
185
225
|
return;
|
|
186
226
|
}
|
|
227
|
+
if (adminAssignedWorkspaces
|
|
228
|
+
.get(this.page)
|
|
229
|
+
?.has(this.workspaceId(container.url))) {
|
|
230
|
+
container.stageBound = true;
|
|
231
|
+
container.developmentCluster = clusterName;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
187
234
|
const returnTo = this.page.url();
|
|
188
|
-
|
|
189
|
-
// "Connected clusters" card lives in the Project detail's sidebar and its
|
|
190
|
-
// "Configure" button opens the Define stages modal (see camunda-hub
|
|
191
|
-
// ConnectedClustersDs). So navigate to the Project detail itself and let
|
|
192
|
-
// openStageDefinition() click that button.
|
|
193
|
-
const processApplicationUrl = container.url
|
|
194
|
-
.replace(/[?#].*$/, '')
|
|
195
|
-
.replace(/\/$/, '');
|
|
196
|
-
await (0, test_1.expect)(async () => {
|
|
197
|
-
await this.page.goto(processApplicationUrl, {
|
|
198
|
-
waitUntil: 'domcontentloaded',
|
|
199
|
-
});
|
|
200
|
-
await this.waitForProcessApplication();
|
|
201
|
-
await this.openStageDefinition();
|
|
202
|
-
await this.defineDevelopmentStage(clusterName);
|
|
203
|
-
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
235
|
+
await this.assignEnvironmentToWorkspace(container.url, clusterName);
|
|
204
236
|
container.stageBound = true;
|
|
205
237
|
container.developmentCluster = clusterName;
|
|
206
238
|
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
207
239
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
// Assigns an Environment to the workspace that `workspaceUrl` (any
|
|
241
|
+
// "/workspaces/<id>/..." URL) belongs to, driving this Page's own session.
|
|
242
|
+
async assignEnvironmentToWorkspace(workspaceUrl, clusterName) {
|
|
243
|
+
const environmentsUrl = this.workspaceEnvironmentsUrl(workspaceUrl);
|
|
244
|
+
await (0, test_1.expect)(async () => {
|
|
245
|
+
await this.page.goto(environmentsUrl, { waitUntil: 'domcontentloaded' });
|
|
246
|
+
await this.assignWorkspaceEnvironment(clusterName);
|
|
247
|
+
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
248
|
+
}
|
|
249
|
+
markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl) {
|
|
250
|
+
const assigned = adminAssignedWorkspaces.get(this.page) ?? new Set();
|
|
251
|
+
assigned.add(this.workspaceId(workspaceUrl));
|
|
252
|
+
adminAssignedWorkspaces.set(this.page, assigned);
|
|
253
|
+
}
|
|
254
|
+
workspaceId(workspaceUrl) {
|
|
255
|
+
const match = workspaceUrl.match(/\/workspaces?\/([^/?#]+)\//);
|
|
256
|
+
if (!match) {
|
|
257
|
+
throw new Error(`Could not derive the workspace id from "${workspaceUrl}": ` +
|
|
258
|
+
'expected a "/workspaces/<id>/..." segment.');
|
|
259
|
+
}
|
|
260
|
+
return match[1];
|
|
261
|
+
}
|
|
262
|
+
async assignWorkspaceEnvironment(clusterName) {
|
|
263
|
+
const tabReady = await this.manageEnvironmentsHeading
|
|
264
|
+
.or(this.editEnvironmentsButton)
|
|
265
|
+
.first()
|
|
266
|
+
.waitFor({ state: 'visible', timeout: 90000 })
|
|
211
267
|
.then(() => true)
|
|
212
268
|
.catch(() => false);
|
|
213
|
-
if (
|
|
269
|
+
if (!tabReady) {
|
|
270
|
+
// WorkspaceManageBody silently normalises an unrecognised/unusable
|
|
271
|
+
// "?tab=environments" back to Members -- e.g. isEnvironmentsEnabled()
|
|
272
|
+
// off, or the current user failing canManageWorkspaceEnvironments
|
|
273
|
+
// (assigning is stricter than viewing: a plain Editor with no
|
|
274
|
+
// elevated/DevOps org role cannot assign at all). Name the likely
|
|
275
|
+
// cause rather than timing out with an opaque "not visible".
|
|
276
|
+
const onMembersTab = await this.page
|
|
277
|
+
.locator('[data-test="tab-members"][data-state="active"]')
|
|
278
|
+
.isVisible()
|
|
279
|
+
.catch(() => false);
|
|
280
|
+
throw new Error('The workspace Environments settings tab never rendered' +
|
|
281
|
+
(onMembersTab ? ' -- landed on the Members tab instead' : '') +
|
|
282
|
+
'. Either the Environments feature is off for this deployment, or the ' +
|
|
283
|
+
"current user lacks permission to manage this workspace's environments.");
|
|
284
|
+
}
|
|
285
|
+
const alreadyAssigned = clusterName
|
|
286
|
+
? this.assignedEnvironmentCards.filter({ hasText: clusterName })
|
|
287
|
+
: this.assignedEnvironmentCards;
|
|
288
|
+
if (await alreadyAssigned
|
|
289
|
+
.first()
|
|
290
|
+
.isVisible()
|
|
291
|
+
.catch(() => false)) {
|
|
214
292
|
return;
|
|
215
293
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
294
|
+
await this.editEnvironmentsButton.click({ timeout: 30000 });
|
|
295
|
+
await (0, test_1.expect)(this.environmentEditorRegion).toBeVisible({ timeout: 30000 });
|
|
296
|
+
const escapedClusterName = clusterName?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
297
|
+
// A picker card's toggle button is "Select <name> (<clusterName or
|
|
298
|
+
// clusterId>)" (see camunda-hub EnvironmentPickerCard) -- match the
|
|
299
|
+
// cluster name in that parenthetical rather than the environment's own
|
|
300
|
+
// display name, which is a separate, admin-chosen field that need not
|
|
301
|
+
// match the cluster at all.
|
|
302
|
+
const targetOption = clusterName
|
|
303
|
+
? this.environmentEditorRegion
|
|
304
|
+
.getByRole('button', {
|
|
305
|
+
name: new RegExp(`^Select .*\\(${escapedClusterName}\\)`),
|
|
306
|
+
})
|
|
307
|
+
.first()
|
|
308
|
+
: this.environmentEditorRegion
|
|
309
|
+
.getByRole('button', { name: /^Select / })
|
|
310
|
+
.first();
|
|
311
|
+
const optionAppeared = await targetOption
|
|
312
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
223
313
|
.then(() => true)
|
|
224
314
|
.catch(() => false);
|
|
225
|
-
if (!
|
|
226
|
-
throw new Error(
|
|
227
|
-
|
|
228
|
-
|
|
315
|
+
if (!optionAppeared) {
|
|
316
|
+
throw new Error(clusterName
|
|
317
|
+
? `No Environment backed by cluster "${clusterName}" is available to assign to this ` +
|
|
318
|
+
'workspace. Register the cluster (or its Environment) first.'
|
|
319
|
+
: 'No Environment is available to assign to this workspace: the picker shows "No ' +
|
|
320
|
+
'environments available".');
|
|
229
321
|
}
|
|
230
|
-
await
|
|
322
|
+
await targetOption.click({ timeout: 30000 });
|
|
323
|
+
await (0, test_1.expect)(this.saveEnvironmentsButton).toBeEnabled({ timeout: 30000 });
|
|
324
|
+
await this.saveEnvironmentsButton.click({ timeout: 30000 });
|
|
325
|
+
await (0, test_1.expect)(this.environmentEditorRegion).toBeHidden({ timeout: 60000 });
|
|
231
326
|
}
|
|
232
327
|
async openByName(name) {
|
|
233
328
|
// Workspace-mode projects table: the whole row is the click target (see the
|