@camunda/e2e-test-suite 0.0.1011 → 0.0.1013
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/FormJsPage.d.ts +2 -0
- package/dist/pages/8.9/FormJsPage.js +74 -1
- package/dist/pages/8.9/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.9/ModelerCreatePage.js +157 -20
- package/dist/pages/8.9/ModelerHomePage.d.ts +12 -0
- package/dist/pages/8.9/ModelerHomePage.js +152 -7
- package/dist/pages/8.9/ProcessApplicationPage.d.ts +30 -0
- package/dist/pages/8.9/ProcessApplicationPage.js +260 -0
- package/dist/pages/8.9/UtilitiesPage.js +8 -0
- package/dist/pages/SM-8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/SM-8.9/ModelerHomePage.js +97 -2
- package/dist/pages/SM-8.9/ProcessApplicationPage.d.ts +22 -0
- package/dist/pages/SM-8.9/ProcessApplicationPage.js +121 -0
- package/dist/tests/8.9/access-user-role-management.spec.js +3 -3
- package/dist/tests/8.9/idp-user-flows.spec.js +9 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class FormJsPage {
|
|
3
3
|
private page;
|
|
4
|
+
private processApplicationPage;
|
|
4
5
|
readonly aiFormGeneratorButton: Locator;
|
|
5
6
|
readonly formRequestInput: Locator;
|
|
6
7
|
readonly generateFormButton: Locator;
|
|
@@ -35,6 +36,7 @@ declare class FormJsPage {
|
|
|
35
36
|
selectCluster(clusterName: string): Promise<void>;
|
|
36
37
|
clickDeployButton(): Promise<void>;
|
|
37
38
|
clickDeploySubButton(): Promise<void>;
|
|
39
|
+
selectOnlyThisResource(): Promise<void>;
|
|
38
40
|
deployForm(clusterName: string): Promise<void>;
|
|
39
41
|
}
|
|
40
42
|
export { FormJsPage };
|
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FormJsPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
6
7
|
class FormJsPage {
|
|
7
8
|
page;
|
|
9
|
+
processApplicationPage;
|
|
8
10
|
aiFormGeneratorButton;
|
|
9
11
|
formRequestInput;
|
|
10
12
|
generateFormButton;
|
|
@@ -25,6 +27,7 @@ class FormJsPage {
|
|
|
25
27
|
deploySuccessNotification;
|
|
26
28
|
constructor(page) {
|
|
27
29
|
this.page = page;
|
|
30
|
+
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
28
31
|
this.aiFormGeneratorButton = page.getByRole('button', {
|
|
29
32
|
name: 'AI Form Generator',
|
|
30
33
|
});
|
|
@@ -50,9 +53,12 @@ class FormJsPage {
|
|
|
50
53
|
this.deployButton = page
|
|
51
54
|
.locator('[data-test="action-bar"]')
|
|
52
55
|
.getByRole('button', { name: 'Deploy', exact: true });
|
|
56
|
+
// Inside a process application hub titles this modal for the application
|
|
57
|
+
// rather than for the form, so 'Deploy form' never matches and the dialog
|
|
58
|
+
// reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
|
|
53
59
|
this.dialog = page
|
|
54
60
|
.locator('.cds--modal-container[role="dialog"]')
|
|
55
|
-
.filter({ hasText: 'Deploy
|
|
61
|
+
.filter({ hasText: 'Deploy' });
|
|
56
62
|
this.deploySubButton = this.dialog.getByRole('button', {
|
|
57
63
|
name: 'Deploy',
|
|
58
64
|
exact: true,
|
|
@@ -124,6 +130,46 @@ class FormJsPage {
|
|
|
124
130
|
async selectCluster(clusterName) {
|
|
125
131
|
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
|
|
126
132
|
const healthCheckTimeout = 30000;
|
|
133
|
+
// Inside a process application the deploy dialog renders
|
|
134
|
+
// ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
|
|
135
|
+
// list: it shows only clusters assigned to one of the application's stages
|
|
136
|
+
// and auto-selects the first, so there is no label to click. Confirm the
|
|
137
|
+
// intended cluster is the one on offer instead.
|
|
138
|
+
//
|
|
139
|
+
// The unconfigured case is called out explicitly: without this the symptom
|
|
140
|
+
// is the cluster toggle never appearing, retried until the test times out,
|
|
141
|
+
// which says nothing about the actual cause.
|
|
142
|
+
const notConfigured = this.dialog.getByText('Clusters not configured for stages');
|
|
143
|
+
if (await notConfigured.isVisible().catch(() => false)) {
|
|
144
|
+
throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
|
|
145
|
+
'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
|
|
146
|
+
}
|
|
147
|
+
const legacyClusterToggle = this.dialog.locator('label').filter({
|
|
148
|
+
hasText: new RegExp(`^${clusterName}`),
|
|
149
|
+
});
|
|
150
|
+
const isLegacyDialog = await legacyClusterToggle
|
|
151
|
+
.first()
|
|
152
|
+
.isVisible()
|
|
153
|
+
.catch(() => false);
|
|
154
|
+
if (!isLegacyDialog) {
|
|
155
|
+
const offered = await this.dialog
|
|
156
|
+
.getByText(clusterName, { exact: false })
|
|
157
|
+
.first()
|
|
158
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
159
|
+
.then(() => true)
|
|
160
|
+
.catch(() => false);
|
|
161
|
+
if (!offered) {
|
|
162
|
+
// Forget the binding before failing. The dialog lists exactly the
|
|
163
|
+
// clusters this application's stages point at, so a missing cluster
|
|
164
|
+
// means the binding did not take -- and no amount of reopening the
|
|
165
|
+
// dialog will make it appear. Clearing the record lets the caller's
|
|
166
|
+
// next attempt rebind instead of re-checking an unchanged dialog.
|
|
167
|
+
this.processApplicationPage.forgetStageCluster();
|
|
168
|
+
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
169
|
+
'binding did not take. It will be rebound on the next attempt.');
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
127
173
|
const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
|
|
128
174
|
const cluster = this.dialog.locator(`[title="${clusterName}"]`);
|
|
129
175
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
@@ -142,12 +188,39 @@ class FormJsPage {
|
|
|
142
188
|
async clickDeploySubButton() {
|
|
143
189
|
await this.deploySubButton.click({ timeout: 60000 });
|
|
144
190
|
}
|
|
191
|
+
// Since camunda/camunda-hub#25831 a form lives inside a process application,
|
|
192
|
+
// and Hub then scopes Deploy to the WHOLE application by default -- it calls
|
|
193
|
+
// deployProcessApplication, not deployFile, so the "Form deployed!" toast
|
|
194
|
+
// never fires (hub's own form-deploy-action.spec.sm.tsx asserts exactly that
|
|
195
|
+
// default). Narrowing the scope keeps this a form-deploy test instead of
|
|
196
|
+
// silently turning it into a process-application deploy. The combobox only
|
|
197
|
+
// exists inside a container, hence the boolean probe.
|
|
198
|
+
async selectOnlyThisResource() {
|
|
199
|
+
const scopeSelect = this.dialog.getByRole('combobox', {
|
|
200
|
+
name: 'Resources to deploy',
|
|
201
|
+
});
|
|
202
|
+
const hasScopeSelect = await scopeSelect
|
|
203
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
204
|
+
.then(() => true)
|
|
205
|
+
.catch(() => false);
|
|
206
|
+
if (!hasScopeSelect) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await scopeSelect.click();
|
|
210
|
+
await this.dialog
|
|
211
|
+
.getByRole('option', { name: 'Only this resource' })
|
|
212
|
+
.or(this.dialog.getByText('Only this resource', { exact: true }))
|
|
213
|
+
.first()
|
|
214
|
+
.click({ timeout: 30000 });
|
|
215
|
+
}
|
|
145
216
|
async deployForm(clusterName) {
|
|
217
|
+
await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
146
218
|
await this.clickDeployButton();
|
|
147
219
|
await (0, test_1.expect)(this.dialog).toBeVisible({
|
|
148
220
|
timeout: 30000,
|
|
149
221
|
});
|
|
150
222
|
await this.selectCluster(clusterName);
|
|
223
|
+
await this.selectOnlyThisResource();
|
|
151
224
|
await this.clickDeploySubButton();
|
|
152
225
|
await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 60000 });
|
|
153
226
|
}
|
|
@@ -5,8 +5,10 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
7
7
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
8
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
8
9
|
class ModelerCreatePage {
|
|
9
10
|
page;
|
|
11
|
+
processApplicationPage;
|
|
10
12
|
generalPanel;
|
|
11
13
|
deployDiagramDialog;
|
|
12
14
|
processIdInput;
|
|
@@ -115,6 +117,7 @@ class ModelerCreatePage {
|
|
|
115
117
|
diagramNameBreadcrumb;
|
|
116
118
|
constructor(page) {
|
|
117
119
|
this.page = page;
|
|
120
|
+
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
118
121
|
this.generalPanel = page.locator('[data-group-id="group-general"]');
|
|
119
122
|
this.processIdInput = page.getByLabel('ID', { exact: true });
|
|
120
123
|
this.startEventElement = page.locator('.djs-hit').first();
|
|
@@ -250,8 +253,12 @@ class ModelerCreatePage {
|
|
|
250
253
|
this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
|
|
251
254
|
name: 'Process successfully deployed',
|
|
252
255
|
});
|
|
256
|
+
// Since camunda/camunda-hub#25831 a diagram deploys as part of its process
|
|
257
|
+
// application and hub titles the modal for the application, not for the
|
|
258
|
+
// single resource, so an exact 'Deploy diagram' name no longer matches.
|
|
259
|
+
// Match on the verb -- pages/SM-8.10 already leaves this unfiltered.
|
|
253
260
|
this.deployDiagramDialog = page.getByRole('dialog', {
|
|
254
|
-
name:
|
|
261
|
+
name: /^Deploy/,
|
|
255
262
|
});
|
|
256
263
|
this.appendGatewayButton = page.getByRole('button', {
|
|
257
264
|
name: 'Append gateway',
|
|
@@ -567,6 +574,7 @@ class ModelerCreatePage {
|
|
|
567
574
|
await this.deployMenuItem.click({ timeout: 60000 });
|
|
568
575
|
}
|
|
569
576
|
async deployDiagram(clusterName) {
|
|
577
|
+
await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
570
578
|
await this.clickDeployButtonWithRetry();
|
|
571
579
|
await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
|
|
572
580
|
timeout: 30000,
|
|
@@ -587,6 +595,10 @@ class ModelerCreatePage {
|
|
|
587
595
|
const retryDelayMs = 20000;
|
|
588
596
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
589
597
|
try {
|
|
598
|
+
// Inside the loop, not before it: a no-op once bound, but selectCluster
|
|
599
|
+
// clears the binding when the dialog does not offer the cluster, so the
|
|
600
|
+
// next attempt rebinds rather than reopening an unchanged dialog.
|
|
601
|
+
await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
590
602
|
await this.clickStartInstanceMainButton();
|
|
591
603
|
await this.selectCluster(clusterName);
|
|
592
604
|
if (variables.length > 0) {
|
|
@@ -642,6 +654,46 @@ class ModelerCreatePage {
|
|
|
642
654
|
async selectCluster(clusterName) {
|
|
643
655
|
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
|
|
644
656
|
const healthCheckTimeout = 30000;
|
|
657
|
+
// Inside a process application the deploy dialog renders
|
|
658
|
+
// ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
|
|
659
|
+
// list: it shows only clusters assigned to one of the application's stages
|
|
660
|
+
// and auto-selects the first, so there is no label to click. Confirm the
|
|
661
|
+
// intended cluster is the one on offer instead.
|
|
662
|
+
//
|
|
663
|
+
// The unconfigured case is called out explicitly: without this the symptom
|
|
664
|
+
// is the cluster toggle never appearing, retried until the test times out,
|
|
665
|
+
// which says nothing about the actual cause.
|
|
666
|
+
const notConfigured = this.dialog.getByText('Clusters not configured for stages');
|
|
667
|
+
if (await notConfigured.isVisible().catch(() => false)) {
|
|
668
|
+
throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
|
|
669
|
+
'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
|
|
670
|
+
}
|
|
671
|
+
const legacyClusterToggle = this.dialog.locator('label').filter({
|
|
672
|
+
hasText: new RegExp(`^${clusterName}`),
|
|
673
|
+
});
|
|
674
|
+
const isLegacyDialog = await legacyClusterToggle
|
|
675
|
+
.first()
|
|
676
|
+
.isVisible()
|
|
677
|
+
.catch(() => false);
|
|
678
|
+
if (!isLegacyDialog) {
|
|
679
|
+
const offered = await this.dialog
|
|
680
|
+
.getByText(clusterName, { exact: false })
|
|
681
|
+
.first()
|
|
682
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
683
|
+
.then(() => true)
|
|
684
|
+
.catch(() => false);
|
|
685
|
+
if (!offered) {
|
|
686
|
+
// Forget the binding before failing. The dialog lists exactly the
|
|
687
|
+
// clusters this application's stages point at, so a missing cluster
|
|
688
|
+
// means the binding did not take -- and no amount of reopening the
|
|
689
|
+
// dialog will make it appear. Clearing the record lets the caller's
|
|
690
|
+
// next attempt rebind instead of re-checking an unchanged dialog.
|
|
691
|
+
this.processApplicationPage.forgetStageCluster();
|
|
692
|
+
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
693
|
+
'binding did not take. It will be rebound on the next attempt.');
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
645
697
|
const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
|
|
646
698
|
const clusterLabel = this.dialog
|
|
647
699
|
.locator('label')
|
|
@@ -667,8 +719,45 @@ class ModelerCreatePage {
|
|
|
667
719
|
async clickDiagramBreadcrumb() {
|
|
668
720
|
await this.diagramBreadcrumb.click({ timeout: 10000 });
|
|
669
721
|
}
|
|
722
|
+
// Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
|
|
723
|
+
// and hub renders that button's truncated-name tooltip on top of the menu,
|
|
724
|
+
// where it swallows the click on "Rename" -- playwright reports
|
|
725
|
+
// `<div role="tooltip">…</div> intercepts pointer events`. Since
|
|
726
|
+
// camunda/camunda-hub#25831 the breadcrumb carries an extra
|
|
727
|
+
// process-application crumb, so the name truncates, and the tooltip renders,
|
|
728
|
+
// as the rule rather than the exception. Park the pointer away from the
|
|
729
|
+
// breadcrumb and let the tooltip unmount before clicking; reopen the menu if
|
|
730
|
+
// a previous attempt closed it.
|
|
670
731
|
async clickEditDiagramNameButton() {
|
|
671
|
-
|
|
732
|
+
const openTooltip = this.page.locator('[role="tooltip"][aria-hidden="false"]');
|
|
733
|
+
await (0, test_1.expect)(async () => {
|
|
734
|
+
const menuOpen = await this.renameDiagramNameButton
|
|
735
|
+
.isVisible()
|
|
736
|
+
.catch(() => false);
|
|
737
|
+
if (!menuOpen) {
|
|
738
|
+
await this.clickDiagramBreadcrumb();
|
|
739
|
+
}
|
|
740
|
+
// Park the pointer so the breadcrumb's own tooltip gets a chance to
|
|
741
|
+
// unmount -- best effort, because it is not reliably hover-driven, and
|
|
742
|
+
// requiring it to go turned an interceptable click into a hard failure.
|
|
743
|
+
await this.page.mouse.move(0, 0);
|
|
744
|
+
await openTooltip
|
|
745
|
+
.first()
|
|
746
|
+
.waitFor({ state: 'hidden', timeout: 2000 })
|
|
747
|
+
.catch(() => false);
|
|
748
|
+
try {
|
|
749
|
+
await this.renameDiagramNameButton.click({ timeout: 10000 });
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// The tooltip renders over the menu and swallows the click (playwright
|
|
753
|
+
// reports `<div role="tooltip">…</div> intercepts pointer events`).
|
|
754
|
+
// Dispatching on the element bypasses hit-testing, which no amount of
|
|
755
|
+
// waiting achieves when the tooltip never unmounts. Not a swallowed
|
|
756
|
+
// failure: the assertion below is what decides whether this worked.
|
|
757
|
+
await this.renameDiagramNameButton.dispatchEvent('click');
|
|
758
|
+
}
|
|
759
|
+
await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
|
|
760
|
+
}).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
|
|
672
761
|
}
|
|
673
762
|
async enterDiagramName(name) {
|
|
674
763
|
await this.diagramNameInput.fill(name, { timeout: 90000 });
|
|
@@ -1224,6 +1313,16 @@ class ModelerCreatePage {
|
|
|
1224
1313
|
await this.playTab.click();
|
|
1225
1314
|
}
|
|
1226
1315
|
async completePlayConfiguration(clusterName) {
|
|
1316
|
+
// Play is a tab on the diagram page, not its own URL, so the stage-cluster
|
|
1317
|
+
// detour's goto() lands back on the Modeler tab and every Play locator
|
|
1318
|
+
// below would then look for a panel that is no longer rendered. Re-enter
|
|
1319
|
+
// Play when the detour actually navigated. Binding cannot simply move
|
|
1320
|
+
// before switchToPlay(): this is the first point in the flow that knows
|
|
1321
|
+
// which cluster the test plays on.
|
|
1322
|
+
const reboundStageCluster = await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
1323
|
+
if (reboundStageCluster) {
|
|
1324
|
+
await this.switchToPlay();
|
|
1325
|
+
}
|
|
1227
1326
|
const timeout = 30000;
|
|
1228
1327
|
// New flow (8.10+): "Setup environment" panel with three steps:
|
|
1229
1328
|
// 1. Connect cluster → "Configure environment" modal → select cluster → Save
|
|
@@ -1247,19 +1346,37 @@ class ModelerCreatePage {
|
|
|
1247
1346
|
name: /^Configure (test case|scenario)$/,
|
|
1248
1347
|
}))
|
|
1249
1348
|
.first();
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1349
|
+
// Play settles into exactly ONE of three states, and which one is not
|
|
1350
|
+
// knowable up front, so race them instead of timing out on each in turn:
|
|
1351
|
+
// the setup overlay, the legacy Continue button, or the configure-test
|
|
1352
|
+
// panel (meaning setup is already done for this diagram).
|
|
1353
|
+
//
|
|
1354
|
+
// Waiting only for the first two and reading "neither" as "already ready"
|
|
1355
|
+
// was wrong once the diagram moved inside a process application.
|
|
1356
|
+
// ConfigurationOverlay renders null until isDeploymentStateValidated, and
|
|
1357
|
+
// it sets that flag synchronously ONLY when processApplicationId is
|
|
1358
|
+
// undefined -- exactly what a project-root diagram used to give it. With
|
|
1359
|
+
// the diagram in a process application the id is defined, so the overlay
|
|
1360
|
+
// must first await checkExistingDeployment(). The old 15s probe raced that
|
|
1361
|
+
// async call, concluded "already ready" and returned while the overlay was
|
|
1362
|
+
// still about to appear. ConfigureTestPanel returns null for as long as the
|
|
1363
|
+
// overlay is up, so the Start button never existed and
|
|
1364
|
+
// clickStartInstanceButton burned its entire retry budget looking for it.
|
|
1365
|
+
const configurationOverlay = this.page.locator('[data-test="play-configuration-overlay"]');
|
|
1366
|
+
const configureTestPanel = this.page.getByTestId('configure-test-panel');
|
|
1367
|
+
await (0, test_1.expect)(configurationOverlay
|
|
1368
|
+
.or(this.continueToPlayButton)
|
|
1369
|
+
.or(configureTestPanel)
|
|
1370
|
+
.first()).toBeVisible({ timeout: 90000 });
|
|
1371
|
+
if (await configureTestPanel.isVisible()) {
|
|
1372
|
+
// Already past setup for this diagram -- nothing to drive.
|
|
1260
1373
|
return;
|
|
1261
1374
|
}
|
|
1262
|
-
|
|
1375
|
+
// Keyed on the overlay itself, not on its Deploy button: when
|
|
1376
|
+
// checkExistingDeployment finds a deployment the overlay opens with the
|
|
1377
|
+
// deploy step already complete, so there is no Deploy button to see and
|
|
1378
|
+
// keying on it would fall through to the legacy Continue flow.
|
|
1379
|
+
const isNewFlow = await configurationOverlay.isVisible();
|
|
1263
1380
|
if (isNewFlow) {
|
|
1264
1381
|
// Step 1: connect cluster if not yet connected (modal with cluster dropdown)
|
|
1265
1382
|
const connectClusterButton = this.page.getByRole('button', {
|
|
@@ -1298,13 +1415,33 @@ class ModelerCreatePage {
|
|
|
1298
1415
|
.getByRole('button', { name: 'Save' })
|
|
1299
1416
|
.click({ timeout });
|
|
1300
1417
|
}
|
|
1301
|
-
// Step 2: deploy
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1418
|
+
// Step 2: deploy, unless the overlay already opened with the
|
|
1419
|
+
// deploy step complete. checkExistingDeployment only runs when
|
|
1420
|
+
// processApplicationId is defined, which it now is, so re-entering
|
|
1421
|
+
// Play on an already-deployed process application legitimately
|
|
1422
|
+
// has nothing to deploy and renders no Deploy button at all.
|
|
1423
|
+
const alreadyDeployed = await configureScenarioButton
|
|
1424
|
+
.isVisible({ timeout: 3000 })
|
|
1425
|
+
.catch(() => false);
|
|
1426
|
+
if (!alreadyDeployed) {
|
|
1427
|
+
// Step 2: deploy — wait for enabled; button stays disabled until the cluster
|
|
1428
|
+
// connection is confirmed by the backend after the Save in step 1.
|
|
1429
|
+
await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
|
|
1430
|
+
await setupDeployButton.click({ timeout });
|
|
1431
|
+
await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
|
|
1432
|
+
}
|
|
1433
|
+
// Step 3: leave the overlay, and confirm we actually left it. The
|
|
1434
|
+
// button only calls hideConfigurationOverlay(); the overlay's own
|
|
1435
|
+
// cluster-change effect can then fire resetDeployment and bring it
|
|
1436
|
+
// straight back, and ConfigureTestPanel stays null while it is up.
|
|
1437
|
+
// Assert the panel really rendered, and re-drive the click if the
|
|
1438
|
+
// overlay returned.
|
|
1439
|
+
await (0, test_1.expect)(async () => {
|
|
1440
|
+
if (await configurationOverlay.isVisible()) {
|
|
1441
|
+
await configureScenarioButton.click({ timeout });
|
|
1442
|
+
}
|
|
1443
|
+
await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
|
|
1444
|
+
}).toPass({ timeout: 90000 });
|
|
1308
1445
|
return;
|
|
1309
1446
|
}
|
|
1310
1447
|
// Legacy flow: select cluster via dialog, then click Continue.
|
|
@@ -2,6 +2,9 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class ModelerHomePage {
|
|
3
3
|
private defaultFolderName;
|
|
4
4
|
private page;
|
|
5
|
+
private processApplicationPage;
|
|
6
|
+
private processApplicationUrl?;
|
|
7
|
+
private processApplicationName?;
|
|
5
8
|
readonly modelerPageBanner: Locator;
|
|
6
9
|
readonly createNewProjectButton: Locator;
|
|
7
10
|
readonly projectNameInput: Locator;
|
|
@@ -11,6 +14,7 @@ declare class ModelerHomePage {
|
|
|
11
14
|
readonly bpmnTemplateOption: Locator;
|
|
12
15
|
readonly formTemplateOption: Locator;
|
|
13
16
|
readonly projectBreadcrumb: Locator;
|
|
17
|
+
readonly workspaceBreadcrumb: Locator;
|
|
14
18
|
readonly openOrganizationsButton: Locator;
|
|
15
19
|
readonly manageButton: Locator;
|
|
16
20
|
readonly crossComponentProjectFolder: Locator;
|
|
@@ -35,10 +39,18 @@ declare class ModelerHomePage {
|
|
|
35
39
|
getProjectNames(name: string): Promise<string[]>;
|
|
36
40
|
clickProcessDiagram(name: string): Promise<void>;
|
|
37
41
|
clickDiagramTypeDropdown(): Promise<void>;
|
|
42
|
+
clickWorkspaceDiagramTypeDropdown(): Promise<void>;
|
|
38
43
|
clickBpmnTemplateOption(): Promise<void>;
|
|
39
44
|
clickFormOption(): Promise<void>;
|
|
40
45
|
enterFormName(name: string): Promise<void>;
|
|
41
46
|
clickProjectBreadcrumb(): Promise<void>;
|
|
47
|
+
clickWorkspaceBreadcrumb(): Promise<void>;
|
|
48
|
+
enterResourceContainer(name?: string): Promise<void>;
|
|
49
|
+
private clickCreateProcessApplication;
|
|
50
|
+
private ensureInResourceContainer;
|
|
51
|
+
returnToResourceContainer(): Promise<void>;
|
|
52
|
+
startBlankBpmnDiagram(): Promise<void>;
|
|
53
|
+
private becameVisible;
|
|
42
54
|
clickHomeBreadcrumb(): Promise<void>;
|
|
43
55
|
clickOpenOrganizationsButton(): Promise<void>;
|
|
44
56
|
createForm(formName: string): Promise<void>;
|
|
@@ -4,9 +4,24 @@ exports.ModelerHomePage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
7
|
+
const randomName_1 = require("../../utils/randomName");
|
|
8
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
9
|
class ModelerHomePage {
|
|
8
10
|
defaultFolderName = 'Cross Component Test Project';
|
|
9
11
|
page;
|
|
12
|
+
// Since camunda/camunda-hub#25831 files and folders cannot sit at a
|
|
13
|
+
// workspace ("project" in today's UI) root -- only process applications and
|
|
14
|
+
// IDP applications can. The container is therefore entered lazily, at the
|
|
15
|
+
// point a resource is created, and remembered for the lifetime of this page
|
|
16
|
+
// object (one instance per test, see fixtures/8.9.ts). Specs that only
|
|
17
|
+
// need the workspace itself -- collaborators, IDP applications -- never
|
|
18
|
+
// trigger the hop.
|
|
19
|
+
processApplicationPage;
|
|
20
|
+
// Load-bearing: hub renders no process-application breadcrumb on a diagram
|
|
21
|
+
// page at container root, so the remembered URL -- not a breadcrumb click --
|
|
22
|
+
// is how we get back. Hub's own Playwright suite does the same.
|
|
23
|
+
processApplicationUrl;
|
|
24
|
+
processApplicationName;
|
|
10
25
|
modelerPageBanner;
|
|
11
26
|
createNewProjectButton;
|
|
12
27
|
projectNameInput;
|
|
@@ -16,6 +31,7 @@ class ModelerHomePage {
|
|
|
16
31
|
bpmnTemplateOption;
|
|
17
32
|
formTemplateOption;
|
|
18
33
|
projectBreadcrumb;
|
|
34
|
+
workspaceBreadcrumb;
|
|
19
35
|
openOrganizationsButton;
|
|
20
36
|
manageButton;
|
|
21
37
|
crossComponentProjectFolder;
|
|
@@ -32,6 +48,7 @@ class ModelerHomePage {
|
|
|
32
48
|
formNameBreadcrumb;
|
|
33
49
|
constructor(page) {
|
|
34
50
|
this.page = page;
|
|
51
|
+
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
35
52
|
this.modelerPageBanner = page.getByRole('banner', {
|
|
36
53
|
name: 'Camunda Modeler',
|
|
37
54
|
});
|
|
@@ -52,13 +69,20 @@ class ModelerHomePage {
|
|
|
52
69
|
.locator('[data-test="create-idp-application"]')
|
|
53
70
|
.getByText('IDP Application');
|
|
54
71
|
this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
.getByText('Form');
|
|
72
|
+
// Matched on the test id alone: hub's design-system variant of the create
|
|
73
|
+
// menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
|
|
74
|
+
// day that variant ships.
|
|
75
|
+
this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
|
|
76
|
+
this.formTemplateOption = page.locator('[data-test="create-form"]');
|
|
61
77
|
this.projectBreadcrumb = page.locator('[data-test="breadcrumb-project"]');
|
|
78
|
+
// Kept separate from projectBreadcrumb, which now means "the container my
|
|
79
|
+
// resources live in". When hub's workspaces terminology flag flips,
|
|
80
|
+
// breadcrumb-project comes to denote the process application, so the
|
|
81
|
+
// workspace level needs its own name from the start.
|
|
82
|
+
this.workspaceBreadcrumb = page
|
|
83
|
+
.locator('[data-test="breadcrumb-project"]')
|
|
84
|
+
.or(page.locator('[data-test="breadcrumb-workspace"]'))
|
|
85
|
+
.first();
|
|
62
86
|
this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
|
|
63
87
|
this.openOrganizationsButton = page.getByLabel('Open Organizations');
|
|
64
88
|
this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
|
|
@@ -134,12 +158,32 @@ class ModelerHomePage {
|
|
|
134
158
|
.map((el) => el.textContent || '')
|
|
135
159
|
.filter((text) => text !== ''));
|
|
136
160
|
}
|
|
161
|
+
// Opens a resource where this test put it. Several specs leave Modeler
|
|
162
|
+
// entirely (to Operate, to Optimize) and come back via
|
|
163
|
+
// clickCrossComponentProjectFolder(), which lands at workspace root -- the
|
|
164
|
+
// resource is one level down, so re-enter the container first.
|
|
137
165
|
async clickProcessDiagram(name) {
|
|
166
|
+
await this.ensureInResourceContainer();
|
|
138
167
|
const process = this.rows.filter({ hasText: name }).getByTitle(name).first();
|
|
139
168
|
await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
|
|
140
169
|
await process.click();
|
|
141
170
|
}
|
|
171
|
+
// Opens the create menu inside this test's process application. The menu at
|
|
172
|
+
// workspace root no longer offers BPMN, Form or Upload -- those resources
|
|
173
|
+
// cannot live there -- so the container is entered first. Callers are
|
|
174
|
+
// unchanged, which is why the existing spec call sites need no edit.
|
|
142
175
|
async clickDiagramTypeDropdown() {
|
|
176
|
+
await this.enterResourceContainer();
|
|
177
|
+
const dropdown = this.processApplicationPage.createNewDropdown;
|
|
178
|
+
await (0, test_1.expect)(dropdown).toBeVisible({
|
|
179
|
+
timeout: 60000,
|
|
180
|
+
});
|
|
181
|
+
await dropdown.click();
|
|
182
|
+
}
|
|
183
|
+
// Opens the create menu at workspace root, which is the only place process
|
|
184
|
+
// applications and IDP applications can be created. Everything else has to
|
|
185
|
+
// go through clickDiagramTypeDropdown().
|
|
186
|
+
async clickWorkspaceDiagramTypeDropdown() {
|
|
143
187
|
await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
|
|
144
188
|
timeout: 15000,
|
|
145
189
|
});
|
|
@@ -159,8 +203,109 @@ class ModelerHomePage {
|
|
|
159
203
|
await this.formNameInput.fill(name);
|
|
160
204
|
await this.formNameInput.press('Enter');
|
|
161
205
|
}
|
|
206
|
+
// Kept under its original name because it is what the existing call sites
|
|
207
|
+
// already mean: "go back to where my resources live". That is now the
|
|
208
|
+
// process application, not the workspace root.
|
|
162
209
|
async clickProjectBreadcrumb() {
|
|
163
|
-
await this.
|
|
210
|
+
await this.returnToResourceContainer();
|
|
211
|
+
}
|
|
212
|
+
// The workspace ("project") level itself, for the surfaces that genuinely
|
|
213
|
+
// live there -- collaborators, IDP applications, published element
|
|
214
|
+
// templates.
|
|
215
|
+
async clickWorkspaceBreadcrumb() {
|
|
216
|
+
await this.workspaceBreadcrumb.click({ timeout: 60000 });
|
|
217
|
+
}
|
|
218
|
+
// Creates, once per test, the process application that holds this test's
|
|
219
|
+
// resources, and leaves the browser inside it. Idempotent: called again while
|
|
220
|
+
// already in the container it is a no-op, so a spec that creates several
|
|
221
|
+
// resources in a row pays for the container once.
|
|
222
|
+
//
|
|
223
|
+
// One container per test rather than one shared with the seeded workspace:
|
|
224
|
+
// deploying is process-application-wide, so a shared container would let one
|
|
225
|
+
// spec's deploy pick up every other spec's half-modelled diagram.
|
|
226
|
+
async enterResourceContainer(name) {
|
|
227
|
+
if (this.processApplicationUrl) {
|
|
228
|
+
const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
|
|
229
|
+
if (!stillInside) {
|
|
230
|
+
await this.returnToResourceContainer();
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.processApplicationName =
|
|
235
|
+
name ??
|
|
236
|
+
(await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
|
|
237
|
+
console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
|
|
238
|
+
await this.clickCreateProcessApplication();
|
|
239
|
+
await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
|
|
240
|
+
this.processApplicationUrl = this.page.url();
|
|
241
|
+
// Remember the container; its stage cluster is bound on the first deploy,
|
|
242
|
+
// by ProcessApplicationPage.ensureStageCluster, which -- unlike this point
|
|
243
|
+
// in the flow -- knows which cluster the test is going to deploy to.
|
|
244
|
+
this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
|
|
245
|
+
}
|
|
246
|
+
// Which control creates a process application depends on the hub version
|
|
247
|
+
// under test. Since camunda/camunda-hub#25831 it is a plain primary button on
|
|
248
|
+
// the workspace root, and the "Create new" menu explicitly hides the item
|
|
249
|
+
// (hideProcessApplication) because a process application is no longer just
|
|
250
|
+
// another resource. Before that change the menu item was the only way in.
|
|
251
|
+
// Race both entry points so neither version pays a full timeout, then use
|
|
252
|
+
// whichever one rendered.
|
|
253
|
+
async clickCreateProcessApplication() {
|
|
254
|
+
const button = this.processApplicationPage.createProcessApplicationButton;
|
|
255
|
+
const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
|
|
256
|
+
if (!rootLoaded) {
|
|
257
|
+
await this.page.reload();
|
|
258
|
+
}
|
|
259
|
+
// Short: the joint probe above already settled which branch rendered, and
|
|
260
|
+
// hub renders exactly one of them (EntityTable renders the toolbar's create
|
|
261
|
+
// menu OR the empty state, never both), so this only confirms which.
|
|
262
|
+
if (await this.becameVisible(button, 5000)) {
|
|
263
|
+
await this.processApplicationPage.clickCreateProcessApplicationButton();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await this.clickWorkspaceDiagramTypeDropdown();
|
|
267
|
+
await this.processApplicationPage.clickCreateProcessApplicationOption();
|
|
268
|
+
}
|
|
269
|
+
// Re-enters this test's container if it already has one, without creating
|
|
270
|
+
// one: opening an existing resource must not have the side effect of
|
|
271
|
+
// creating a container for a spec that never asked for it.
|
|
272
|
+
async ensureInResourceContainer() {
|
|
273
|
+
if (!this.processApplicationUrl) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
await this.enterResourceContainer();
|
|
277
|
+
}
|
|
278
|
+
// Navigates back to this test's process application by the remembered URL
|
|
279
|
+
// rather than by a breadcrumb click: hub renders no process-application
|
|
280
|
+
// breadcrumb on a diagram page at container root, and the breadcrumb that
|
|
281
|
+
// does exist on the container page doubles as its rename control. Hub's own
|
|
282
|
+
// Playwright suite navigates back the same way.
|
|
283
|
+
async returnToResourceContainer() {
|
|
284
|
+
if (!this.processApplicationUrl) {
|
|
285
|
+
await this.clickWorkspaceBreadcrumb();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await this.page.goto(this.processApplicationUrl, {
|
|
289
|
+
waitUntil: 'domcontentloaded',
|
|
290
|
+
});
|
|
291
|
+
await this.processApplicationPage.waitForProcessApplication();
|
|
292
|
+
await this.dismissOverlays();
|
|
293
|
+
}
|
|
294
|
+
// Single seam for "give me an empty BPMN diagram in this test's container".
|
|
295
|
+
// A process application always ships an auto-created main process; this adds
|
|
296
|
+
// a new diagram rather than adopting that one, so every existing modelling
|
|
297
|
+
// assertion keeps its current subject.
|
|
298
|
+
async startBlankBpmnDiagram() {
|
|
299
|
+
await this.clickDiagramTypeDropdown();
|
|
300
|
+
await this.clickBpmnTemplateOption();
|
|
301
|
+
}
|
|
302
|
+
// Boolean visibility probe. waitFor rather than isVisible, which resolves
|
|
303
|
+
// immediately and would report a false negative on a still-rendering page.
|
|
304
|
+
async becameVisible(locator, timeout) {
|
|
305
|
+
return locator
|
|
306
|
+
.waitFor({ state: 'visible', timeout })
|
|
307
|
+
.then(() => true)
|
|
308
|
+
.catch(() => false);
|
|
164
309
|
}
|
|
165
310
|
async clickHomeBreadcrumb() {
|
|
166
311
|
try {
|