@camunda/e2e-test-suite 0.0.1273 → 0.0.1275
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.7/AppsPage.js +11 -3
- package/dist/pages/8.7/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.7/ClusterDetailsPage.js +22 -64
- package/dist/pages/8.7/ModelerCreatePage.js +56 -14
- package/dist/pages/8.7/PlayPage.js +12 -1
- package/dist/pages/8.7/SettingsPage.js +15 -2
- package/dist/pages/8.7/UtilitiesPage.js +22 -0
- package/dist/pages/8.8/AppsPage.d.ts +2 -0
- package/dist/pages/8.8/AppsPage.js +122 -0
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +29 -10
- package/dist/pages/8.8/ClusterPage.js +35 -2
- package/dist/pages/8.8/ClusterSecretsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterSecretsPage.js +62 -2
- package/dist/pages/8.8/FormJsPage.js +55 -24
- package/dist/pages/8.8/HomePage.d.ts +3 -0
- package/dist/pages/8.8/HomePage.js +62 -7
- package/dist/pages/8.8/LoginPage.js +32 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.8/ModelerCreatePage.js +78 -16
- package/dist/pages/8.8/ModelerHomePage.d.ts +10 -0
- package/dist/pages/8.8/ModelerHomePage.js +339 -14
- package/dist/pages/8.8/UtilitiesPage.js +47 -2
- package/dist/tests/8.7/rba-enabled-user-flows.spec.js +5 -1
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +7 -4
- package/dist/tests/8.8/test-setup.spec.js +29 -4
- package/dist/utils/consoleApiHelpers.d.ts +5 -0
- package/dist/utils/consoleApiHelpers.js +177 -2
- package/dist/utils/hubDeployDialog.d.ts +3 -0
- package/dist/utils/hubDeployDialog.js +74 -0
- package/dist/utils/hubResourceRename.d.ts +4 -0
- package/dist/utils/hubResourceRename.js +74 -0
- package/package.json +1 -1
|
@@ -124,10 +124,43 @@ class ClusterPage {
|
|
|
124
124
|
this.clusterVersion = (clusterName, versionText) => this.cluster(clusterName).getByText(versionText);
|
|
125
125
|
}
|
|
126
126
|
async clickClusterBanner() {
|
|
127
|
-
|
|
127
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
128
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
129
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
130
|
+
// HomePage.clusterTab handles).
|
|
131
|
+
const clusterLink = this.page
|
|
128
132
|
.getByRole('banner')
|
|
129
133
|
.getByRole('link', { name: 'Clusters' })
|
|
130
|
-
.
|
|
134
|
+
.or(this.page
|
|
135
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
136
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
137
|
+
.or(this.page
|
|
138
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
139
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
140
|
+
await clusterLink.click({ timeout: 60000 });
|
|
141
|
+
// Landing on Hub's Environments overview (see above) renders clusters as
|
|
142
|
+
// cards, not the getByRole('row') table this page's own locators expect
|
|
143
|
+
// -- route through the page's own "Clusters" tab to reach the classic
|
|
144
|
+
// route. Same fix as HomePage.clickClusters().
|
|
145
|
+
const clustersPageTab = this.page
|
|
146
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
147
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
148
|
+
// Required on Hub, not optional: this page's row-based cluster locators
|
|
149
|
+
// cannot be satisfied by the card-based Environments overview we just
|
|
150
|
+
// landed on. isVisible() does not wait, so a slow tab render used to be
|
|
151
|
+
// read as "no tab here" and the flow continued on the wrong route.
|
|
152
|
+
const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
|
|
153
|
+
const tabShown = await clustersPageTab
|
|
154
|
+
.waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
|
|
155
|
+
.then(() => true)
|
|
156
|
+
.catch(() => false);
|
|
157
|
+
if (tabShown) {
|
|
158
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
159
|
+
}
|
|
160
|
+
else if (onHub) {
|
|
161
|
+
throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
|
|
162
|
+
'row-based cluster list this page needs is unreachable without it');
|
|
163
|
+
}
|
|
131
164
|
}
|
|
132
165
|
async deleteCluster(name) {
|
|
133
166
|
await this.page.waitForTimeout(1000);
|
|
@@ -36,6 +36,7 @@ declare class ClusterSecretsPage {
|
|
|
36
36
|
value: string;
|
|
37
37
|
}[]): Promise<void>;
|
|
38
38
|
reopenConnectorSecretsTab(cluster: string): Promise<void>;
|
|
39
|
+
private reselectConnectorSecretsTabIfPresent;
|
|
39
40
|
assertSecretsToolbarVisible(cluster: string): Promise<void>;
|
|
40
41
|
assertSecretsOrCreate(cluster: string, secrets: {
|
|
41
42
|
name: string;
|
|
@@ -50,10 +50,27 @@ class ClusterSecretsPage {
|
|
|
50
50
|
this.dialogImportButton = this.page
|
|
51
51
|
.getByRole('dialog')
|
|
52
52
|
.getByRole('button', { name: 'Import', exact: true });
|
|
53
|
-
|
|
53
|
+
// "Filter table" was Carbon's default TableToolbarSearch label. Camunda
|
|
54
|
+
// Hub's rebranded panel renders a plain "Search" input instead, so the
|
|
55
|
+
// old exact-name match never resolves and every lookup falls through to
|
|
56
|
+
// creating a secret that may already exist. Match either.
|
|
57
|
+
this.secretsSearchBox = page
|
|
58
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
59
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
60
|
+
.or(page.getByPlaceholder('Search'));
|
|
61
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
62
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
63
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
64
|
+
// HomePage.clusterTab handles).
|
|
54
65
|
this.clusterBanner = this.page
|
|
55
66
|
.getByRole('banner')
|
|
56
|
-
.getByRole('link', { name: 'Clusters' })
|
|
67
|
+
.getByRole('link', { name: 'Clusters' })
|
|
68
|
+
.or(this.page
|
|
69
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
70
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
71
|
+
.or(this.page
|
|
72
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
73
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
57
74
|
this.clusterLink = (name) => page.getByRole('link', { name: name });
|
|
58
75
|
this.clusterSecretTab = this.page.getByRole('tab', {
|
|
59
76
|
name: 'Connector Secrets',
|
|
@@ -144,6 +161,30 @@ class ClusterSecretsPage {
|
|
|
144
161
|
await this.dismissOpenDialogIfPresent();
|
|
145
162
|
await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
|
|
146
163
|
await this.clusterBanner.click({ timeout: 60000 });
|
|
164
|
+
// Landing on Hub's Environments overview (see clusterBanner above) renders
|
|
165
|
+
// clusters as cards, not the getByRole('link') the clusterLink() locator
|
|
166
|
+
// below expects to find directly -- route through the page's own
|
|
167
|
+
// "Clusters" tab to reach the classic route. Same fix as
|
|
168
|
+
// HomePage.clickClusters().
|
|
169
|
+
const clustersPageTab = this.page
|
|
170
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
171
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
172
|
+
// Required on Hub, not optional: this page's row-based cluster locators
|
|
173
|
+
// cannot be satisfied by the card-based Environments overview we just
|
|
174
|
+
// landed on. isVisible() does not wait, so a slow tab render used to be
|
|
175
|
+
// read as "no tab here" and the flow continued on the wrong route.
|
|
176
|
+
const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
|
|
177
|
+
const tabShown = await clustersPageTab
|
|
178
|
+
.waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
|
|
179
|
+
.then(() => true)
|
|
180
|
+
.catch(() => false);
|
|
181
|
+
if (tabShown) {
|
|
182
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
183
|
+
}
|
|
184
|
+
else if (onHub) {
|
|
185
|
+
throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
|
|
186
|
+
'row-based cluster list this page needs is unreachable without it');
|
|
187
|
+
}
|
|
147
188
|
}
|
|
148
189
|
async clickCluster(name) {
|
|
149
190
|
await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
|
|
@@ -241,7 +282,25 @@ class ClusterSecretsPage {
|
|
|
241
282
|
await (0, test_1.expect)(this.clusterSecretTab).toBeVisible({ timeout: 30000 });
|
|
242
283
|
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
243
284
|
}
|
|
285
|
+
// Camunda Hub keeps the selected cluster tab in client-side state rather
|
|
286
|
+
// than in the URL (/org/<id>/cluster/<id> carries no tab segment), so every
|
|
287
|
+
// page.reload() in this file lands back on the cluster Overview tab, where
|
|
288
|
+
// the secrets toolbar does not exist at all. Re-selecting the tab is a
|
|
289
|
+
// client-side click that costs nothing, whereas re-entering from the
|
|
290
|
+
// Clusters list navigates to `/` and pays a full Hub OAuth hand-off -- so
|
|
291
|
+
// try the cheap route before the toolbar assertions fall through to their
|
|
292
|
+
// broken-panel recovery.
|
|
293
|
+
async reselectConnectorSecretsTabIfPresent() {
|
|
294
|
+
const onTab = await this.clusterSecretTab
|
|
295
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
296
|
+
.then(() => true)
|
|
297
|
+
.catch(() => false);
|
|
298
|
+
if (onTab) {
|
|
299
|
+
await this.clusterSecretTab.click({ timeout: 15000 }).catch(() => { });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
244
302
|
async assertSecretsToolbarVisible(cluster) {
|
|
303
|
+
await this.reselectConnectorSecretsTabIfPresent();
|
|
245
304
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewSecretButton, {
|
|
246
305
|
visibilityTimeout: 60000,
|
|
247
306
|
totalTimeout: 300000,
|
|
@@ -272,6 +331,7 @@ class ClusterSecretsPage {
|
|
|
272
331
|
// Same broken-panel state as assertSecretsToolbarVisible guards against:
|
|
273
332
|
// when the secrets panel failed to load, Import is simply absent, and only
|
|
274
333
|
// re-entering the tab brings it back.
|
|
334
|
+
await this.reselectConnectorSecretsTabIfPresent();
|
|
275
335
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.importButton, {
|
|
276
336
|
visibilityTimeout: 30000,
|
|
277
337
|
totalTimeout: 300000,
|
|
@@ -5,6 +5,8 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
7
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
8
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
9
|
+
const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
|
|
8
10
|
class FormJsPage {
|
|
9
11
|
page;
|
|
10
12
|
processApplicationPage;
|
|
@@ -54,16 +56,36 @@ class FormJsPage {
|
|
|
54
56
|
// Inside a process application hub titles this modal for the application
|
|
55
57
|
// rather than for the form, so 'Deploy form' never matches and the dialog
|
|
56
58
|
// reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
|
|
59
|
+
// Camunda Hub's design-system shell renders no Carbon markup at all: a DOM
|
|
60
|
+
// audit of a live run found zero `cds--*` classes on these pages, so the
|
|
61
|
+
// Carbon container selector matched nothing and the dialog read as absent
|
|
62
|
+
// even once it had opened. Match the dialog by role and keep the Carbon
|
|
63
|
+
// container for shells where the Ds migration has not shipped.
|
|
57
64
|
this.dialog = page
|
|
65
|
+
.getByRole('dialog')
|
|
66
|
+
.filter({ hasText: 'Deploy' })
|
|
67
|
+
.or(page
|
|
58
68
|
.locator('.cds--modal-container[role="dialog"]')
|
|
59
|
-
.filter({ hasText: 'Deploy' })
|
|
69
|
+
.filter({ hasText: 'Deploy' }))
|
|
70
|
+
.first();
|
|
60
71
|
this.deploySubButton = this.dialog.getByRole('button', {
|
|
61
72
|
name: 'Deploy',
|
|
62
73
|
exact: true,
|
|
63
74
|
});
|
|
75
|
+
// Same Carbon-to-design-system move as the dialog above: the success toast
|
|
76
|
+
// is a role=status/alert region on Hub rather than a `cds--toast-*` node.
|
|
77
|
+
// Hub's dialog deploys the whole PROJECT ("Deploy project"), so its
|
|
78
|
+
// confirmation is no longer the form-specific "Form deployed!" string.
|
|
79
|
+
// Match the deployment confirmation however it is worded, while still
|
|
80
|
+
// requiring a success (not an error) wording so a failed deploy cannot
|
|
81
|
+
// satisfy it.
|
|
64
82
|
this.deploySuccessNotification = page
|
|
83
|
+
.getByText(/(form|project|resources?)\s+(was\s+)?deployed/i)
|
|
84
|
+
.or(page.getByText('Form deployed!'))
|
|
85
|
+
.or(page
|
|
65
86
|
.locator('.cds--toast-notification--success')
|
|
66
|
-
.filter({ hasText: 'Form deployed!' })
|
|
87
|
+
.filter({ hasText: 'Form deployed!' }))
|
|
88
|
+
.first();
|
|
67
89
|
this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
|
|
68
90
|
}
|
|
69
91
|
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"') {
|
|
@@ -159,14 +181,30 @@ class FormJsPage {
|
|
|
159
181
|
}
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
// "HealthydevManage" was the Carbon row's chrome concatenated together.
|
|
185
|
+
// The design-system dialog renders the cluster row as
|
|
186
|
+
// "<name> ... <generation> dev <checkmark> Healthy" with no Manage link,
|
|
187
|
+
// so the old pattern could never match. Keep what the assertion is for --
|
|
188
|
+
// the chosen cluster reports Healthy -- without the stale chrome.
|
|
189
|
+
const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?Healthy`);
|
|
190
|
+
// `[title="<cluster>"]` was the Carbon toggle's own tooltip attribute; the
|
|
191
|
+
// design-system dialog renders no `title` here, so the click target is the
|
|
192
|
+
// labelled toggle itself (the same node `legacyClusterToggle` already
|
|
193
|
+
// matched to decide this branch).
|
|
194
|
+
const cluster = this.dialog
|
|
195
|
+
.locator(`[title="${clusterName}"]`)
|
|
196
|
+
.or(legacyClusterToggle.first())
|
|
197
|
+
.first();
|
|
164
198
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
165
199
|
await cluster.click();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
// Carbon rendered the cluster choice as a toggle `switch`; the
|
|
201
|
+
// design-system dialog uses a different control (and auto-selects the
|
|
202
|
+
// single offered cluster), so a switch-only assertion timed out on a
|
|
203
|
+
// dialog that had in fact selected the cluster. Assert whichever control
|
|
204
|
+
// shape is present, and skip the check when none is -- the deploy itself,
|
|
205
|
+
// and the success notification the caller waits on, are what actually
|
|
206
|
+
// prove the right cluster was used.
|
|
207
|
+
await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
|
|
170
208
|
await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
|
|
171
209
|
timeout: healthCheckTimeout,
|
|
172
210
|
});
|
|
@@ -189,22 +227,7 @@ class FormJsPage {
|
|
|
189
227
|
// silently turning it into a process-application deploy. The combobox only
|
|
190
228
|
// exists inside a container, hence the boolean probe.
|
|
191
229
|
async selectOnlyThisResource() {
|
|
192
|
-
|
|
193
|
-
name: 'Resources to deploy',
|
|
194
|
-
});
|
|
195
|
-
const hasScopeSelect = await scopeSelect
|
|
196
|
-
.waitFor({ state: 'visible', timeout: 10000 })
|
|
197
|
-
.then(() => true)
|
|
198
|
-
.catch(() => false);
|
|
199
|
-
if (!hasScopeSelect) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
await scopeSelect.click();
|
|
203
|
-
await this.dialog
|
|
204
|
-
.getByRole('option', { name: 'Only this resource' })
|
|
205
|
-
.or(this.dialog.getByText('Only this resource', { exact: true }))
|
|
206
|
-
.first()
|
|
207
|
-
.click({ timeout: 30000 });
|
|
230
|
+
await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
|
|
208
231
|
}
|
|
209
232
|
async deployForm(clusterName) {
|
|
210
233
|
await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
@@ -214,6 +237,14 @@ class FormJsPage {
|
|
|
214
237
|
// retry through that propagation window instead of failing on the transient
|
|
215
238
|
// rejection. Non-migration runs keep the original single-attempt behavior.
|
|
216
239
|
const maxAttempts = process.env.IS_MIGRATION === 'true' ? 15 : 1;
|
|
240
|
+
// Camunda Hub's deploy modal lists the ENVIRONMENTS assigned to this
|
|
241
|
+
// workspace, not the org's clusters, so a cluster created after the
|
|
242
|
+
// workspace existed never appears and the picker comes up empty. The
|
|
243
|
+
// wizard's "Assign environments" step only covers a workspace created
|
|
244
|
+
// after its cluster, which test-setup cannot guarantee -- it creates
|
|
245
|
+
// clusters and workspaces in parallel. Ensure the link here, at the point
|
|
246
|
+
// of use, so the ordering does not matter.
|
|
247
|
+
await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
|
|
217
248
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
218
249
|
await this.clickDeployButton();
|
|
219
250
|
await (0, test_1.expect)(this.dialog).toBeVisible({
|
|
@@ -12,11 +12,14 @@ declare class HomePage {
|
|
|
12
12
|
readonly appSwitcher: Locator;
|
|
13
13
|
readonly openOrganizationButton: Locator;
|
|
14
14
|
readonly consoleBanner: Locator;
|
|
15
|
+
readonly hubHeader: Locator;
|
|
16
|
+
readonly hubSidebar: Locator;
|
|
15
17
|
readonly modelerBanner: Locator;
|
|
16
18
|
readonly gettingStartedHeading: Locator;
|
|
17
19
|
readonly buttonSkipCustomization: Locator;
|
|
18
20
|
constructor(page: Page);
|
|
19
21
|
clickClusters(): Promise<void>;
|
|
22
|
+
private clickClustersPageTabIfPresent;
|
|
20
23
|
clickDashboard(): Promise<void>;
|
|
21
24
|
clickOrganization(): Promise<void>;
|
|
22
25
|
closeInformationDialog(): Promise<void>;
|
|
@@ -15,16 +15,35 @@ class HomePage {
|
|
|
15
15
|
appSwitcher;
|
|
16
16
|
openOrganizationButton;
|
|
17
17
|
consoleBanner;
|
|
18
|
+
hubHeader;
|
|
19
|
+
hubSidebar;
|
|
18
20
|
modelerBanner;
|
|
19
21
|
gettingStartedHeading;
|
|
20
22
|
buttonSkipCustomization;
|
|
21
23
|
constructor(page) {
|
|
22
24
|
this.page = page;
|
|
25
|
+
// Camunda Hub replaces Console on SaaS: an app header (only carrying the
|
|
26
|
+
// user menu once authenticated) plus a sidebar whose Console section
|
|
27
|
+
// links to /org/<id>/clusters and /org/<id>/management.
|
|
28
|
+
this.hubHeader = page
|
|
29
|
+
.locator('header[data-slot="app-header"]')
|
|
30
|
+
.filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
|
|
31
|
+
this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
|
|
23
32
|
this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
|
|
33
|
+
// Once an org has environmentsEnabled, camunda-hub's
|
|
34
|
+
// use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
|
|
35
|
+
// labelled "Environments" whose href is '/environments', not
|
|
36
|
+
// '/clusters' -- accept both hrefs so this resolves either way.
|
|
24
37
|
this.clusterTab = page
|
|
25
38
|
.getByRole('banner')
|
|
26
|
-
.getByRole('link', { name: 'Clusters' })
|
|
27
|
-
|
|
39
|
+
.getByRole('link', { name: 'Clusters' })
|
|
40
|
+
.or(this.hubSidebar.locator('a[href$="/clusters"]'))
|
|
41
|
+
.or(this.hubSidebar.locator('a[href$="/environments"]'))
|
|
42
|
+
.first();
|
|
43
|
+
this.organizationTab = page
|
|
44
|
+
.getByRole('link', { name: 'Organization' })
|
|
45
|
+
.or(this.hubSidebar.locator('a[href$="/management"]'))
|
|
46
|
+
.first();
|
|
28
47
|
this.organizationViewTitle = page.getByText('Organization Management');
|
|
29
48
|
this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
|
|
30
49
|
this.informationDialog = page.getByRole('button', {
|
|
@@ -32,14 +51,23 @@ class HomePage {
|
|
|
32
51
|
});
|
|
33
52
|
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
34
53
|
this.appSwitcher = page.getByLabel('App Switcher');
|
|
54
|
+
// Specs assert this as "the authenticated app shell has rendered" before
|
|
55
|
+
// navigating. Camunda Hub has no app-switcher button at all (apps are
|
|
56
|
+
// reached from the sidebar), so on Hub its authenticated header is the
|
|
57
|
+
// equivalent readiness signal.
|
|
35
58
|
this.camundaComponentsButton = page
|
|
36
59
|
.getByLabel('Camunda components')
|
|
37
60
|
.or(this.camundaAppsButton)
|
|
38
|
-
.or(this.appSwitcher)
|
|
61
|
+
.or(this.appSwitcher)
|
|
62
|
+
.or(this.hubHeader);
|
|
39
63
|
this.openOrganizationButton = page.getByLabel('Open Organizations');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
// "Camunda logo Console" was the old Carbon header's logo link. Camunda
|
|
65
|
+
// Hub has no equivalent link -- its post-login landing is `hubHeader`
|
|
66
|
+
// (see above) -- so callers that assert this banner as a login/Console
|
|
67
|
+
// arrival signal need the Hub fallback too.
|
|
68
|
+
this.consoleBanner = page
|
|
69
|
+
.getByRole('link', { name: 'Camunda logo Console' })
|
|
70
|
+
.or(this.hubHeader);
|
|
43
71
|
this.gettingStartedHeading = page.getByText('start by creating a project');
|
|
44
72
|
this.buttonSkipCustomization = page.getByRole('button', {
|
|
45
73
|
name: 'Skip customization',
|
|
@@ -49,8 +77,35 @@ class HomePage {
|
|
|
49
77
|
});
|
|
50
78
|
}
|
|
51
79
|
async clickClusters() {
|
|
52
|
-
await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout:
|
|
80
|
+
await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 30000 });
|
|
53
81
|
await this.clusterTab.click({ timeout: 15000 });
|
|
82
|
+
await this.clickClustersPageTabIfPresent();
|
|
83
|
+
}
|
|
84
|
+
// Landing on Hub's Environments overview (see clusterTab above) doesn't put
|
|
85
|
+
// us on the classic row-based cluster list ClusterPage.ts expects -- that
|
|
86
|
+
// page renders clusters as cards. Its own header renders a separate
|
|
87
|
+
// "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
|
|
88
|
+
// that does.
|
|
89
|
+
async clickClustersPageTabIfPresent() {
|
|
90
|
+
const clustersPageTab = this.page
|
|
91
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
92
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
93
|
+
// Required on Hub, not optional: this page's row-based cluster locators
|
|
94
|
+
// cannot be satisfied by the card-based Environments overview we just
|
|
95
|
+
// landed on. isVisible() does not wait, so a slow tab render used to be
|
|
96
|
+
// read as "no tab here" and the flow continued on the wrong route.
|
|
97
|
+
const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
|
|
98
|
+
const tabShown = await clustersPageTab
|
|
99
|
+
.waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
|
|
100
|
+
.then(() => true)
|
|
101
|
+
.catch(() => false);
|
|
102
|
+
if (tabShown) {
|
|
103
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
104
|
+
}
|
|
105
|
+
else if (onHub) {
|
|
106
|
+
throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
|
|
107
|
+
'row-based cluster list this page needs is unreachable without it');
|
|
108
|
+
}
|
|
54
109
|
}
|
|
55
110
|
async clickDashboard() {
|
|
56
111
|
await this.dashboardTab.click({ timeout: 90000 });
|
|
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
6
6
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
7
7
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
8
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
8
9
|
class LoginPage {
|
|
9
10
|
page;
|
|
10
11
|
usernameInput;
|
|
@@ -50,7 +51,34 @@ class LoginPage {
|
|
|
50
51
|
}
|
|
51
52
|
async fillPassword(password) {
|
|
52
53
|
await this.passwordInput.waitFor({ state: 'visible', timeout: 60000 });
|
|
53
|
-
|
|
54
|
+
// Auth0's password screen becomes visible before its own scripts finish
|
|
55
|
+
// wiring up the field, and that initialisation wipes whatever is already
|
|
56
|
+
// in it. A discarded value makes Continue post an empty password, Auth0
|
|
57
|
+
// re-renders the same screen with "Password is required", and a login can
|
|
58
|
+
// burn all five retries without ever reaching the app.
|
|
59
|
+
//
|
|
60
|
+
// Typed, not filled: a live trace of that failure shows six fill +
|
|
61
|
+
// read-back cycles in a row all coming back empty, so re-filling cannot
|
|
62
|
+
// help -- `fill()` sets the value in one shot and Auth0's own handlers
|
|
63
|
+
// never see it, leaving their state empty and the field cleared on the
|
|
64
|
+
// next render. pressSequentially dispatches real key events, which those
|
|
65
|
+
// handlers do process.
|
|
66
|
+
//
|
|
67
|
+
// Polled rather than slept on: the check settles as soon as the value is
|
|
68
|
+
// there, so a healthy login pays nothing.
|
|
69
|
+
//
|
|
70
|
+
// Only the LENGTH is ever read back or asserted on, so the secret cannot
|
|
71
|
+
// reach a log, a trace entry or an assertion-failure message.
|
|
72
|
+
await (0, test_1.expect)(async () => {
|
|
73
|
+
await this.passwordInput.click();
|
|
74
|
+
await this.passwordInput.fill('');
|
|
75
|
+
await this.passwordInput.pressSequentially(password, { delay: 20 });
|
|
76
|
+
await test_1.expect
|
|
77
|
+
.poll(async () => this.passwordInput
|
|
78
|
+
.evaluate((el) => el.value.length)
|
|
79
|
+
.catch(() => 0), { timeout: 5000, intervals: [200, 400, 800] })
|
|
80
|
+
.toBe(password.length);
|
|
81
|
+
}).toPass({ timeout: 60000, intervals: [500, 1000, 2000] });
|
|
54
82
|
}
|
|
55
83
|
async clickLoginButton() {
|
|
56
84
|
await this.loginButton.click({ timeout: 60000 });
|
|
@@ -58,6 +86,7 @@ class LoginPage {
|
|
|
58
86
|
async login(credentials = {}) {
|
|
59
87
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
60
88
|
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
89
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
61
90
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 200000 });
|
|
62
91
|
await this.fillUsername(username);
|
|
63
92
|
await this.clickContinueButton();
|
|
@@ -69,6 +98,7 @@ class LoginPage {
|
|
|
69
98
|
async loginWithTestUser(credentials = {}) {
|
|
70
99
|
const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
|
|
71
100
|
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
101
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
72
102
|
// SSO re-auth may silently log us back in as the previous user after logout.
|
|
73
103
|
// Detect Console (settings button visible) and retry logout until Auth0 form appears.
|
|
74
104
|
const settingsBtn = this.page.getByLabel('Open Settings');
|
|
@@ -119,6 +149,7 @@ class LoginPage {
|
|
|
119
149
|
async loginWithoutOrgAssertion(credentials = {}) {
|
|
120
150
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
121
151
|
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
152
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
122
153
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
|
|
123
154
|
await this.fillUsername(username);
|
|
124
155
|
await this.clickContinueButton();
|
|
@@ -145,6 +145,7 @@ declare class ModelerCreatePage {
|
|
|
145
145
|
runProcessInstance(clusterName: string, variables?: string, formVariables?: {
|
|
146
146
|
[key: string]: string;
|
|
147
147
|
}, textToVisible?: string, expectRejection?: boolean): Promise<void>;
|
|
148
|
+
selectOnlyThisResource(): Promise<void>;
|
|
148
149
|
selectCluster(clusterName: string): Promise<void>;
|
|
149
150
|
clickViewProcessInstanceLink(): Promise<void>;
|
|
150
151
|
clickNameInput(): Promise<void>;
|
|
@@ -7,6 +7,9 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
|
|
|
7
7
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
8
8
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
9
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
10
|
+
const hubResourceRename_1 = require("../../utils/hubResourceRename");
|
|
11
|
+
const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
|
|
12
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
10
13
|
class ModelerCreatePage {
|
|
11
14
|
page;
|
|
12
15
|
processApplicationPage;
|
|
@@ -172,14 +175,38 @@ class ModelerCreatePage {
|
|
|
172
175
|
this.nameInput = page.getByLabel('Name', { exact: true });
|
|
173
176
|
this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
|
|
174
177
|
this.renameDiagramNameButton = page.getByText('Rename');
|
|
175
|
-
|
|
178
|
+
// Hub's rename dialog field, with the legacy inline editable kept for
|
|
179
|
+
// shells where the design-system migration has not shipped.
|
|
180
|
+
this.diagramNameInput = page
|
|
181
|
+
.locator('[data-test="rename-input"]')
|
|
182
|
+
.or(page.locator('[data-test="editable-input"]'))
|
|
183
|
+
.first();
|
|
176
184
|
this.variableInput = page.locator('[id="variables-json"]');
|
|
177
185
|
this.embedFormButton = page.getByRole('button', { name: 'Link form' });
|
|
178
|
-
|
|
186
|
+
// Hub's link picker confirms with `link-target-confirm` ("Link");
|
|
187
|
+
// `confirm-move` was the old move/link dialog's id and exists only in
|
|
188
|
+
// hub's older cypress specs now. Keep it as a fallback.
|
|
189
|
+
this.embedButton = page
|
|
190
|
+
.locator('[data-test="link-target-confirm"]')
|
|
191
|
+
.or(page.locator('[data-test="confirm-move"]'))
|
|
192
|
+
.first();
|
|
193
|
+
// Same picker as clickForm(): entries are buttons labelled
|
|
194
|
+
// "Select <name>" inside `link-target-list`, not `item-<name>` nodes.
|
|
179
195
|
this.newForm = page
|
|
180
|
-
.locator('[data-test="
|
|
181
|
-
.
|
|
182
|
-
|
|
196
|
+
.locator('[data-test="link-target-list"]')
|
|
197
|
+
.getByRole('button', { name: /^Select New form/ })
|
|
198
|
+
.first()
|
|
199
|
+
.or(page.locator('[data-test="item-New form"]').getByText('New form'))
|
|
200
|
+
.first();
|
|
201
|
+
// `deploy-action` is absent from Hub's shell (a DOM audit of a live run
|
|
202
|
+
// found no such id); its deploy dialog confirms with a plain button named
|
|
203
|
+
// "Deploy" in the dialog footer.
|
|
204
|
+
this.deploySubButton = page
|
|
205
|
+
.locator('[data-test="deploy-action"]')
|
|
206
|
+
.or(page
|
|
207
|
+
.getByRole('dialog')
|
|
208
|
+
.getByRole('button', { name: 'Deploy', exact: true }))
|
|
209
|
+
.first();
|
|
183
210
|
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
184
211
|
this.restConnectorOption = page.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
|
|
185
212
|
this.changeElementSearchInput = page.locator('.djs-popup-search input');
|
|
@@ -337,12 +364,25 @@ class ModelerCreatePage {
|
|
|
337
364
|
this.backupDeployButton = page.getByRole('button', {
|
|
338
365
|
name: 'Deploy',
|
|
339
366
|
});
|
|
367
|
+
// Hub's design-system shell emits no Carbon markup, so the `cds--toast-*`
|
|
368
|
+
// scope matched nothing and this assertion read as "not linked" even on a
|
|
369
|
+
// successful link. Assert the message itself and keep the Carbon scope for
|
|
370
|
+
// shells that still render it.
|
|
340
371
|
this.formLinkedSuccessMessage = page
|
|
372
|
+
.getByText('Form has been successfully linked.')
|
|
373
|
+
.or(page
|
|
341
374
|
.locator('.cds--toast-notification__subtitle')
|
|
342
|
-
.getByText('Form has been successfully linked.')
|
|
343
|
-
|
|
375
|
+
.getByText('Form has been successfully linked.'))
|
|
376
|
+
.first();
|
|
377
|
+
// Hub's breadcrumbs carry no data-test (DOM audit of a live run found none
|
|
378
|
+
// of the breadcrumb-* ids present), so address the diagram by its own name
|
|
379
|
+
// in the breadcrumb navigation and keep the legacy id as a fallback.
|
|
380
|
+
this.diagramNameBreadcrumb = (diagramName) => page
|
|
381
|
+
.locator('button[data-test="breadcrumb-diagram"]', {
|
|
344
382
|
hasText: diagramName,
|
|
345
|
-
})
|
|
383
|
+
})
|
|
384
|
+
.or((0, hubResourceRename_1.hubBreadcrumbs)(page).getByText(diagramName, { exact: true }))
|
|
385
|
+
.first();
|
|
346
386
|
this.webhookActiveMessage = page.getByText('Webhook is active');
|
|
347
387
|
}
|
|
348
388
|
async clickSecondDeployButton() {
|
|
@@ -500,10 +540,16 @@ class ModelerCreatePage {
|
|
|
500
540
|
// a form with the same name and the picker then lists two identical entries.
|
|
501
541
|
// Scope to the first matching item so the link step stays deterministic on retry
|
|
502
542
|
// instead of failing with a strict-mode violation.
|
|
543
|
+
// Hub's link picker is `ul[data-test="link-target-list"]` whose entries are
|
|
544
|
+
// buttons labelled "Select <name>" with a per-file
|
|
545
|
+
// `data-test="link-target-file-<uuid>"`; the old `item-<name>` id does not
|
|
546
|
+
// exist on it (a DOM audit of a live run found no `item-*` at all). Keep
|
|
547
|
+
// the legacy id as a fallback for pre-design-system shells.
|
|
503
548
|
await this.page
|
|
504
|
-
.locator(
|
|
549
|
+
.locator('[data-test="link-target-list"]')
|
|
550
|
+
.getByRole('button', { name: `Select ${name}`, exact: true })
|
|
551
|
+
.or(this.page.locator(`[data-test="item-${name}"]`).first().getByText(name))
|
|
505
552
|
.first()
|
|
506
|
-
.getByText(name)
|
|
507
553
|
.click({ timeout: 90000 });
|
|
508
554
|
}
|
|
509
555
|
async clickUserTask(id) {
|
|
@@ -620,6 +666,12 @@ class ModelerCreatePage {
|
|
|
620
666
|
const retryDelayMs = 20000;
|
|
621
667
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
622
668
|
try {
|
|
669
|
+
// Hub lists only the ENVIRONMENTS assigned to the workspace in this
|
|
670
|
+
// dialog, so a cluster the workspace does not carry is simply absent
|
|
671
|
+
// from the picker. deployForm() ensures that link for the form path;
|
|
672
|
+
// a spec that runs a diagram without deploying a form first would
|
|
673
|
+
// otherwise reach this dialog with nothing to select.
|
|
674
|
+
await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
|
|
623
675
|
// Inside the loop, not before it: a no-op once bound, but selectCluster
|
|
624
676
|
// clears the binding when the dialog does not offer the cluster, so the
|
|
625
677
|
// next attempt rebinds rather than reopening an unchanged dialog.
|
|
@@ -677,8 +729,18 @@ class ModelerCreatePage {
|
|
|
677
729
|
}
|
|
678
730
|
}
|
|
679
731
|
}
|
|
732
|
+
// Scopes Hub's deploy/run dialog to the single resource being deployed.
|
|
733
|
+
// Mirrors FormJsPage.selectOnlyThisResource(): the select trigger carries no
|
|
734
|
+
// aria-labelledby, so it is addressed by its id (`deployment-scope-selection`)
|
|
735
|
+
// -- a name-based lookup silently matches nothing -- and its options render
|
|
736
|
+
// in a portal attached to <body>, outside the dialog, so they are looked up
|
|
737
|
+
// page-scoped. A no-op on dialogs that offer no scope choice.
|
|
738
|
+
async selectOnlyThisResource() {
|
|
739
|
+
await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
|
|
740
|
+
}
|
|
680
741
|
async selectCluster(clusterName) {
|
|
681
742
|
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
|
|
743
|
+
await this.selectOnlyThisResource();
|
|
682
744
|
const healthCheckTimeout = 30000;
|
|
683
745
|
// Inside a process application the deploy dialog renders
|
|
684
746
|
// ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
|
|
@@ -720,15 +782,13 @@ class ModelerCreatePage {
|
|
|
720
782
|
}
|
|
721
783
|
return;
|
|
722
784
|
}
|
|
723
|
-
const healthyRegex = new RegExp(`${clusterName}.*?
|
|
785
|
+
const healthyRegex = new RegExp(`${clusterName}.*?Healthy`);
|
|
724
786
|
const clusterLabel = this.dialog
|
|
725
787
|
.locator('label')
|
|
726
788
|
.filter({ hasText: new RegExp(`^${clusterName}`) });
|
|
727
789
|
await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
|
|
728
790
|
await clusterLabel.click();
|
|
729
|
-
await (0,
|
|
730
|
-
timeout: 30000,
|
|
731
|
-
});
|
|
791
|
+
await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
|
|
732
792
|
await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
|
|
733
793
|
timeout: healthCheckTimeout,
|
|
734
794
|
});
|
|
@@ -809,8 +869,10 @@ class ModelerCreatePage {
|
|
|
809
869
|
}).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
|
|
810
870
|
}
|
|
811
871
|
async enterDiagramName(name) {
|
|
812
|
-
|
|
813
|
-
|
|
872
|
+
// Hub renames through the current crumb's actions menu into a shared
|
|
873
|
+
// dialog; the inline editable this used to fill never mounts on its own.
|
|
874
|
+
// "New BPMN diagram" is the server default the diagram still carries here.
|
|
875
|
+
await (0, hubResourceRename_1.renameHubResource)(this.page, 'New BPMN diagram', name);
|
|
814
876
|
}
|
|
815
877
|
async clickVariableInput() {
|
|
816
878
|
await this.variableInput.click({ timeout: 60000 });
|