@camunda/e2e-test-suite 0.0.1269 → 0.0.1271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.10/AppsPage.d.ts +4 -0
- package/dist/pages/8.10/AppsPage.js +182 -0
- package/dist/pages/8.10/ClusterPage.js +19 -19
- package/dist/pages/8.10/HomePage.js +16 -3
- package/dist/tests/8.10/web-modeler-user-flows.spec.js +2 -1
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +2 -8
- package/package.json +1 -1
- package/playwright.config.ts +80 -0
|
@@ -25,9 +25,13 @@ declare class AppsPage {
|
|
|
25
25
|
readonly operateMenuItem: Locator;
|
|
26
26
|
readonly consoleMenuItem: Locator;
|
|
27
27
|
readonly modelerMenuItem: Locator;
|
|
28
|
+
readonly hubSidebar: Locator;
|
|
28
29
|
constructor(page: Page);
|
|
29
30
|
private reloadForRecovery;
|
|
30
31
|
private entryAppeared;
|
|
32
|
+
private expandHubClusterGroup;
|
|
33
|
+
private hubAppLink;
|
|
34
|
+
private clickHubClusterApp;
|
|
31
35
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
32
36
|
clickModeler(): Promise<void>;
|
|
33
37
|
private isModelerAuthTransit;
|
|
@@ -4,6 +4,9 @@ exports.AppsPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const UtilitiesPage_1 = require("../8.10/UtilitiesPage");
|
|
7
|
+
function escapeRegExp(value) {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
}
|
|
7
10
|
class AppsPage {
|
|
8
11
|
page;
|
|
9
12
|
modelerLink;
|
|
@@ -30,8 +33,14 @@ class AppsPage {
|
|
|
30
33
|
operateMenuItem;
|
|
31
34
|
consoleMenuItem;
|
|
32
35
|
modelerMenuItem;
|
|
36
|
+
hubSidebar;
|
|
33
37
|
constructor(page) {
|
|
34
38
|
this.page = page;
|
|
39
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
40
|
+
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
41
|
+
// -- there is no per-app switcher panel to open on Hub (see
|
|
42
|
+
// clickCamundaApps below). Mirrors pages/8.9/AppsPage.ts.
|
|
43
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
35
44
|
this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
|
|
36
45
|
this.appSwitcherButton = page.getByLabel('App Switcher');
|
|
37
46
|
this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
|
|
@@ -137,6 +146,105 @@ class AppsPage {
|
|
|
137
146
|
.then(() => true)
|
|
138
147
|
.catch(() => false);
|
|
139
148
|
}
|
|
149
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
150
|
+
// expands to reveal its own per-app child links (Operate/Tasklist/...).
|
|
151
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: "the
|
|
152
|
+
// design system splits the group row into a label control and a chevron
|
|
153
|
+
// control, both labelled by the cluster name; either one opens it" -- and
|
|
154
|
+
// the child links do not exist in the DOM at all until expanded (not just
|
|
155
|
+
// hidden), and are not nested under any container matchable by cluster
|
|
156
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
157
|
+
// (`.first()` because both controls share that name), click it, then look
|
|
158
|
+
// up the app link sidebar-wide rather than scoped to a "group" container
|
|
159
|
+
// that doesn't reliably exist -- which is why every OTHER expanded group
|
|
160
|
+
// must be collapsed first, below. Ported from pages/8.7/AppsPage.ts
|
|
161
|
+
// (expandHubClusterGroup), which itself fixed the exact bug an unscoped
|
|
162
|
+
// sidebar-wide lookup has: with more than one cluster in the org, a
|
|
163
|
+
// DIFFERENT cluster's group can already be expanded (left over from an
|
|
164
|
+
// earlier action on the same page), and `.first()` would then silently
|
|
165
|
+
// resolve to THAT cluster's app link instead of throwing.
|
|
166
|
+
async expandHubClusterGroup(clusterName) {
|
|
167
|
+
const toggle = this.hubSidebar
|
|
168
|
+
.getByRole('button', { name: clusterName })
|
|
169
|
+
.first();
|
|
170
|
+
// Check THIS cluster's own toggle state via aria-expanded, not "is any
|
|
171
|
+
// app link with this name visible anywhere in the sidebar" -- the latter
|
|
172
|
+
// is exactly the unscoped check that let a different, already-expanded
|
|
173
|
+
// cluster's link satisfy it.
|
|
174
|
+
const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
|
|
175
|
+
if (!alreadyExpanded) {
|
|
176
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
177
|
+
// failure made a broken expansion look successful and left the caller
|
|
178
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
179
|
+
// appear, hiding the actionable error behind an unrelated timeout.
|
|
180
|
+
await toggle.click({ timeout: 10000 });
|
|
181
|
+
}
|
|
182
|
+
// An app link resolved after this is sidebar-wide (see clickHubClusterApp
|
|
183
|
+
// below), so if an EARLIER call in the same test left a different
|
|
184
|
+
// cluster's group expanded too, two links with this app name now exist --
|
|
185
|
+
// a strict-mode violation, or worse, `.first()` silently picking the
|
|
186
|
+
// wrong cluster's link. Collapse every other expanded group before
|
|
187
|
+
// resolving it -- there is no container to scope the link to instead,
|
|
188
|
+
// since the child links aren't nested under anything matchable by
|
|
189
|
+
// cluster name once expanded.
|
|
190
|
+
// hasNotText filters DOM text content, not the accessible name -- the
|
|
191
|
+
// expanded chevron can be labelled via aria-label with no text content of
|
|
192
|
+
// its own, in which case hasNotText would fail to exclude the toggle we
|
|
193
|
+
// just expanded and this loop would immediately collapse it again. Match
|
|
194
|
+
// by accessible name instead, excluding any name CONTAINING clusterName
|
|
195
|
+
// -- not just one exactly equal to it, since the rendered toggle name
|
|
196
|
+
// carries more than the bare cluster name.
|
|
197
|
+
const otherExpandedToggles = this.hubSidebar.getByRole('button', {
|
|
198
|
+
expanded: true,
|
|
199
|
+
name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
|
|
200
|
+
});
|
|
201
|
+
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
202
|
+
// Let a failed collapse throw rather than swallowing it: the app link
|
|
203
|
+
// is intentionally sidebar-wide, so a group left expanded here produces
|
|
204
|
+
// the wrong cluster's link below anyway -- failing at the actual cause
|
|
205
|
+
// is more useful than at that symptom.
|
|
206
|
+
await otherExpandedToggles.first().click({ timeout: 10000 });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// When the sidebar is collapsed (the default on a fresh session), the
|
|
210
|
+
// group toggle opens a Radix popover -- a role="dialog" labelled with the
|
|
211
|
+
// cluster name and portalled OUTSIDE the "Main navigation" <nav> -- so a
|
|
212
|
+
// sidebar-scoped lookup finds nothing there. Look in that popover first;
|
|
213
|
+
// keep the sidebar-scoped lookup for the expanded-sidebar layout, where the
|
|
214
|
+
// links render inline. Not exact: the rendered accessible name carries
|
|
215
|
+
// more than the bare app name (e.g. "Operate in <cluster> (opens in a new
|
|
216
|
+
// tab)"). Ported from pages/8.7/AppsPage.ts (hubAppLink).
|
|
217
|
+
hubAppLink(clusterName, appName) {
|
|
218
|
+
const candidates = [
|
|
219
|
+
this.hubSidebar.getByRole('link', { name: appName }),
|
|
220
|
+
this.page
|
|
221
|
+
.getByRole('dialog', { name: clusterName, exact: true })
|
|
222
|
+
.getByRole('link', { name: appName }),
|
|
223
|
+
];
|
|
224
|
+
// The "<app> in <cluster>" fallback name is only buildable for a literal
|
|
225
|
+
// app name, not the Admin/Identity alternation regex clickAdmin passes.
|
|
226
|
+
if (typeof appName === 'string') {
|
|
227
|
+
candidates.push(this.page.getByRole('link', {
|
|
228
|
+
name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
return candidates.reduce((acc, locator) => acc.or(locator)).first();
|
|
232
|
+
}
|
|
233
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
234
|
+
await this.expandHubClusterGroup(clusterName);
|
|
235
|
+
const appLink = this.hubAppLink(clusterName, appName);
|
|
236
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
237
|
+
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
238
|
+
// the app in a new tab while every caller keeps asserting on this.page.
|
|
239
|
+
// Follow the href in the current tab instead, matching the same-tab
|
|
240
|
+
// navigation the Console app switcher gave these callers.
|
|
241
|
+
const href = await appLink.getAttribute('href');
|
|
242
|
+
if (href) {
|
|
243
|
+
await this.page.goto(new URL(href, this.page.url()).toString());
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await appLink.click({ timeout: 10000 });
|
|
247
|
+
}
|
|
140
248
|
async clickCluster(component, name) {
|
|
141
249
|
// Cluster names are not unique in an org, so this app's sub-menu can list
|
|
142
250
|
// the same name twice; pick the first entry (same row the Console cluster
|
|
@@ -156,6 +264,34 @@ class AppsPage {
|
|
|
156
264
|
// nav V2 breadcrumb switcher: Modeler is a `menuitemradio`, the legacy
|
|
157
265
|
// apps flyout renders it as a link. Resolve whichever this build shows.
|
|
158
266
|
const modelerEntry = this.modelerMenuItem.or(this.modelerLink).first();
|
|
267
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
268
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
269
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
270
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
271
|
+
// caller that reaches clickModeler() from a cluster/API-client page
|
|
272
|
+
// stranded with the next locator running against the wrong page.
|
|
273
|
+
// Navigate home through the header logo link instead, same as
|
|
274
|
+
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
275
|
+
// Modeler home is a harmless in-SPA no-op. Ported from
|
|
276
|
+
// pages/8.9/AppsPage.ts.
|
|
277
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
278
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
279
|
+
// The href stays literally "/login" until the client's auth state
|
|
280
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
281
|
+
// clicking it in that window would log the test out instead of
|
|
282
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
283
|
+
// one rather than racing it.
|
|
284
|
+
const homeLink = header
|
|
285
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
286
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
287
|
+
.or(header
|
|
288
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
289
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
290
|
+
.first();
|
|
291
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
292
|
+
await homeLink.click({ timeout: 30000 });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
159
295
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
160
296
|
try {
|
|
161
297
|
if (retries > 0) {
|
|
@@ -338,6 +474,12 @@ class AppsPage {
|
|
|
338
474
|
}
|
|
339
475
|
}
|
|
340
476
|
async clickTasklist(clusterName) {
|
|
477
|
+
// Camunda Hub replaced the global app switcher with a per-cluster
|
|
478
|
+
// sidebar; ported from pages/8.9/AppsPage.ts.
|
|
479
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
480
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
341
483
|
const maxRetries = 3;
|
|
342
484
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
343
485
|
try {
|
|
@@ -461,6 +603,16 @@ class AppsPage {
|
|
|
461
603
|
}
|
|
462
604
|
}
|
|
463
605
|
async clickAdmin(clusterName) {
|
|
606
|
+
// Camunda Hub replaced the global app switcher with a per-cluster
|
|
607
|
+
// sidebar; ported from pages/8.9/AppsPage.ts. Non-anchored, matching the
|
|
608
|
+
// substring lookup clickHubClusterApp uses for Operate/Tasklist/Optimize
|
|
609
|
+
// -- an anchored pattern fails the same way an exact-string match did for
|
|
610
|
+
// those apps if the rendered link's accessible name carries any extra
|
|
611
|
+
// content (icon/badge).
|
|
612
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
613
|
+
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
464
616
|
const maxRetries = 5;
|
|
465
617
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
466
618
|
try {
|
|
@@ -543,6 +695,12 @@ class AppsPage {
|
|
|
543
695
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
544
696
|
}
|
|
545
697
|
async clickOperate(clusterName) {
|
|
698
|
+
// Camunda Hub replaced the global app switcher with a per-cluster
|
|
699
|
+
// sidebar; ported from pages/8.9/AppsPage.ts.
|
|
700
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
701
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
546
704
|
const maxRetries = 3;
|
|
547
705
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
548
706
|
try {
|
|
@@ -573,6 +731,12 @@ class AppsPage {
|
|
|
573
731
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
574
732
|
}
|
|
575
733
|
async clickOptimize(clusterName) {
|
|
734
|
+
// Camunda Hub replaced the global app switcher with a per-cluster
|
|
735
|
+
// sidebar; ported from pages/8.9/AppsPage.ts.
|
|
736
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
737
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
576
740
|
const maxRetries = 3;
|
|
577
741
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
578
742
|
try {
|
|
@@ -614,6 +778,14 @@ class AppsPage {
|
|
|
614
778
|
throw new Error(`Failed to click the optimize link after ${maxRetries} attempts.`);
|
|
615
779
|
}
|
|
616
780
|
async clickCamundaApps(maxRetries = 3) {
|
|
781
|
+
// Camunda Hub's rewritten shells have no app-switcher button to open --
|
|
782
|
+
// Modeler is already Hub's home area and the other apps are reached
|
|
783
|
+
// per-cluster via the persistent sidebar, not through a global switcher
|
|
784
|
+
// panel. Mirrors the same hostname guard in clickModeler() above. Ported
|
|
785
|
+
// from pages/8.9/AppsPage.ts.
|
|
786
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
617
789
|
await (0, sleep_1.sleep)(2000);
|
|
618
790
|
const appButtons = [
|
|
619
791
|
this.camundaComponentsButton,
|
|
@@ -631,6 +803,16 @@ class AppsPage {
|
|
|
631
803
|
}
|
|
632
804
|
}
|
|
633
805
|
async clickConsoleLink() {
|
|
806
|
+
// Camunda Hub folded Console into the same persistent shell as
|
|
807
|
+
// Modeler/home: Console's Dashboard/Clusters/Organization links are just
|
|
808
|
+
// another section of the same "Main navigation" sidebar, not a separate
|
|
809
|
+
// app to switch into. Its header banner (homePage.consoleBanner, via
|
|
810
|
+
// hubHeader) is already visible from any Hub page, so there is no
|
|
811
|
+
// navigation action needed here -- mirrors the same hostname guard in
|
|
812
|
+
// clickModeler(). Ported from pages/8.9/AppsPage.ts.
|
|
813
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
634
816
|
const maxRetries = 3;
|
|
635
817
|
// nav V2 breadcrumb switcher: Console is a `menuitemradio`, the legacy
|
|
636
818
|
// apps flyout renders it as a link. Resolve whichever this build shows.
|
|
@@ -79,33 +79,33 @@ class ClusterPage {
|
|
|
79
79
|
.or(page.getByRole('tab', {
|
|
80
80
|
name: 'Connector Secrets',
|
|
81
81
|
}));
|
|
82
|
-
|
|
82
|
+
// Camunda Hub's create-cluster form replaced the Carbon tiles (a
|
|
83
|
+
// <label> per option) and tabs with design-system choice cards and toggle
|
|
84
|
+
// groups, all exposed as role="radio" named by their title. Each locator
|
|
85
|
+
// below accepts either shape via this helper. Ported from
|
|
86
|
+
// pages/8.9/ClusterPage.ts.
|
|
87
|
+
const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
|
|
88
|
+
this.belgiumRegion = choice(page.locator('label').filter({
|
|
83
89
|
hasText: 'Chaos, Belgium, Europe (europe-west1)',
|
|
84
|
-
});
|
|
85
|
-
this.londonRegion = page.locator('label').filter({
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
this.ohioRegion = page.locator('label').filter({
|
|
90
|
+
}), 'Chaos, Belgium, Europe (europe-west1)');
|
|
91
|
+
this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
|
|
92
|
+
this.ohioRegion = choice(page.locator('label').filter({
|
|
89
93
|
hasText: 'Ohio, North America (us-east-2)',
|
|
90
|
-
});
|
|
91
|
-
this.finlandRegion = page.locator('label').filter({
|
|
94
|
+
}), 'Ohio, North America (us-east-2)');
|
|
95
|
+
this.finlandRegion = choice(page.locator('label').filter({
|
|
92
96
|
hasText: 'Finland, Europe (europe-north1)',
|
|
93
|
-
});
|
|
97
|
+
}), 'Finland, Europe (europe-north1)');
|
|
94
98
|
this.createAClusterButton = page.getByRole('button', {
|
|
95
99
|
name: 'Create a Cluster',
|
|
96
100
|
});
|
|
97
101
|
this.clusterVersionOption = page.getByText(`${process.env.CLUSTER_VERSION}`, { exact: true });
|
|
98
102
|
this.generationSearchInput = page.getByPlaceholder('Search generations');
|
|
99
|
-
this.g3DevClusterType = page.locator('label').filter({
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
|
|
106
|
-
this.gcpRegion = page.getByRole('tab', {
|
|
107
|
-
name: 'GCP',
|
|
108
|
-
});
|
|
103
|
+
this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
|
|
104
|
+
this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
|
|
105
|
+
this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
|
|
106
|
+
this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
|
|
107
|
+
this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
|
|
108
|
+
this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
|
|
109
109
|
this.clustersList = page
|
|
110
110
|
.getByRole('row')
|
|
111
111
|
.filter({ hasNotText: 'Generation' }); //Filter out header row
|
|
@@ -51,14 +51,27 @@ class HomePage {
|
|
|
51
51
|
});
|
|
52
52
|
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
53
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 -- see AppsPage.clickCamundaApps), so on Hub
|
|
57
|
+
// its authenticated header is the equivalent readiness signal. Ported
|
|
58
|
+
// from pages/8.9/HomePage.ts.
|
|
54
59
|
this.camundaComponentsButton = page
|
|
55
60
|
.getByLabel('Camunda components')
|
|
56
61
|
.or(this.camundaAppsButton)
|
|
57
|
-
.or(this.appSwitcher)
|
|
62
|
+
.or(this.appSwitcher)
|
|
63
|
+
.or(this.hubHeader);
|
|
58
64
|
this.openOrganizationButton = page.getByLabel('Open Organizations');
|
|
59
|
-
|
|
65
|
+
// "Camunda logo Console" was the old Carbon header's logo link. Camunda
|
|
66
|
+
// Hub has no equivalent link -- its post-login landing is `hubHeader`
|
|
67
|
+
// (see above) -- so callers that assert this banner as a login/Console
|
|
68
|
+
// arrival signal need the Hub fallback too. Ported from
|
|
69
|
+
// pages/8.9/HomePage.ts.
|
|
70
|
+
this.consoleBanner = page
|
|
71
|
+
.getByRole('link', {
|
|
60
72
|
name: 'Camunda logo Console',
|
|
61
|
-
})
|
|
73
|
+
})
|
|
74
|
+
.or(this.hubHeader);
|
|
62
75
|
this.gettingStartedHeading = page.getByText('start by creating a project');
|
|
63
76
|
this.buttonSkipCustomization = page.getByRole('button', {
|
|
64
77
|
name: 'Skip customization',
|
|
@@ -35,7 +35,8 @@ _8_10_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
35
35
|
await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
// Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
|
|
39
|
+
_8_10_1.test.skip('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, consoleOrganizationsPage, loginPage, }, testInfo) => {
|
|
39
40
|
_8_10_1.test.slow();
|
|
40
41
|
const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
|
|
41
42
|
const processName = 'User_Task_Process_With_AI_Form' + randomString;
|
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const test_1 = require("@playwright/test");
|
|
4
4
|
const SM_8_8_1 = require("../../fixtures/SM-8.8");
|
|
5
|
-
const ModelerCreatePage_1 = require("../../pages/SM-8.10/ModelerCreatePage");
|
|
6
|
-
const ModelerHomePage_1 = require("../../pages/SM-8.10/ModelerHomePage");
|
|
7
|
-
const NavigationPage_1 = require("../../pages/SM-8.10/NavigationPage");
|
|
8
5
|
const UtilitiesPage_1 = require("../../pages/SM-8.8/UtilitiesPage");
|
|
9
6
|
const _setup_1 = require("../../test-setup.js");
|
|
10
7
|
SM_8_8_1.test.afterEach(async ({ page }, testInfo) => {
|
|
11
8
|
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
12
9
|
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
13
10
|
});
|
|
14
|
-
(0, SM_8_8_1.test)('Topology orchestration golden path @tasklistV2', async ({ navigationPage, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskPanelPage, taskDetailsPage, optimizeHomePage, page, request, }) => {
|
|
11
|
+
(0, SM_8_8_1.test)('Topology orchestration golden path @tasklistV2', async ({ modelerHomePage, modelerCreatePage, navigationPage, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskPanelPage, taskDetailsPage, optimizeHomePage, page, request, }) => {
|
|
15
12
|
SM_8_8_1.test.slow();
|
|
16
13
|
const processName = `Topology_Orchestration_${await (0, _setup_1.generateRandomStringAsync)(5)}`;
|
|
17
|
-
const hubNavigationPage = new NavigationPage_1.NavigationPage(page);
|
|
18
|
-
const modelerHomePage = new ModelerHomePage_1.ModelerHomePage(page);
|
|
19
|
-
const modelerCreatePage = new ModelerCreatePage_1.ModelerCreatePage(page);
|
|
20
14
|
await SM_8_8_1.test.step('Create and start a process in Web Modeler', async () => {
|
|
21
|
-
await
|
|
15
|
+
await navigationPage.goToModeler();
|
|
22
16
|
await modelerHomePage.createCrossComponentProjectFolder();
|
|
23
17
|
await modelerHomePage.clickDiagramTypeDropdown();
|
|
24
18
|
await modelerHomePage.clickBpmnTemplateOption();
|
package/package.json
CHANGED
package/playwright.config.ts
CHANGED
|
@@ -224,6 +224,83 @@ const changedFolders =
|
|
|
224
224
|
? args[changedFoldersArgIndex + 1].split(',')
|
|
225
225
|
: [];
|
|
226
226
|
|
|
227
|
+
// Run-budget guards. See the "A hung test is not an infrastructure failure"
|
|
228
|
+
// note in docs/ci-failure-categories.md, and issue #3115.
|
|
229
|
+
const isSmoke = process.env.IS_SMOKE === 'true';
|
|
230
|
+
|
|
231
|
+
// Reads a numeric env override. An unset, empty, negative or unparseable value
|
|
232
|
+
// returns undefined so the guard falls back to its default rather than being
|
|
233
|
+
// silently switched off by a typo. An explicit 0 is honoured -- Playwright
|
|
234
|
+
// reads 0 as "no limit" for both globalTimeout and maxFailures.
|
|
235
|
+
const numericEnv = (name: string): number | undefined => {
|
|
236
|
+
const raw = process.env[name];
|
|
237
|
+
if (raw === undefined || raw.trim() === '') return undefined;
|
|
238
|
+
const value = Number(raw);
|
|
239
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Without globalTimeout Playwright never stops itself, so one hung test costs
|
|
243
|
+
// `timeout x (retries + 1)` -- 24 minutes at the 12-minute default -- and
|
|
244
|
+
// GitHub SIGKILLs the job at its `timeout-minutes` before any report is
|
|
245
|
+
// written. A run with no report has total_tests == 0, which
|
|
246
|
+
// docs/ci-failure-categories.md classifies as
|
|
247
|
+
// `infrastructure` -- a hung test gets filed as a platform problem and never
|
|
248
|
+
// reaches anyone who can fix it. When Playwright owns the deadline instead it
|
|
249
|
+
// runs its reporters first (runner/tasks.js finishTaskRun calls onEnd on every
|
|
250
|
+
// status), so the run fails with a report naming the stuck test.
|
|
251
|
+
//
|
|
252
|
+
// The budget belongs to the caller: globalTimeout is only correct below the
|
|
253
|
+
// job's `timeout-minutes`, and that differs per workflow. The smoke leg is the
|
|
254
|
+
// one context this file can infer -- the blocking gate with the tightest
|
|
255
|
+
// budget (25 minutes, camunda/camunda-platform-helm
|
|
256
|
+
// .github/workflows/test-integration-runner.yaml), which a single hang already
|
|
257
|
+
// exhausts. Every other caller opts in with C8E2E_GLOBAL_TIMEOUT_MIN. Jobs that
|
|
258
|
+
// declare no `timeout-minutes` (this repo's nightlies, the repeat-each
|
|
259
|
+
// validation loops that run for hours) have no budget to protect, so they keep
|
|
260
|
+
// the unbounded behaviour.
|
|
261
|
+
const SMOKE_GLOBAL_TIMEOUT_MINUTES = 20;
|
|
262
|
+
|
|
263
|
+
const globalTimeout = () => {
|
|
264
|
+
const override = numericEnv('C8E2E_GLOBAL_TIMEOUT_MIN');
|
|
265
|
+
if (override !== undefined) return override * 60 * 1000;
|
|
266
|
+
return isSmoke ? SMOKE_GLOBAL_TIMEOUT_MINUTES * 60 * 1000 : undefined;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// The missing fail-fast lever: once the platform is down there is no value in
|
|
270
|
+
// paying the per-test timeout for every remaining test. Smoke is a blocking
|
|
271
|
+
// gate over a single spec, so the first failure is already the answer.
|
|
272
|
+
//
|
|
273
|
+
// Everywhere else the default stays 0 (no limit) on purpose. The nightly triage
|
|
274
|
+
// dispatchers (.github/workflows/triage-nightly-failures*.yml) fan out one fix
|
|
275
|
+
// agent per failing test and docs/ci-failure-categories.md derives
|
|
276
|
+
// failure_category from the full counts, so truncating the failure list would
|
|
277
|
+
// hide work rather than save time. Callers with a real budget set
|
|
278
|
+
// C8E2E_MAX_FAILURES.
|
|
279
|
+
const maxFailures = () => {
|
|
280
|
+
const override = numericEnv('C8E2E_MAX_FAILURES');
|
|
281
|
+
if (override !== undefined) return override;
|
|
282
|
+
return isSmoke ? 1 : 0;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// @playwright/test sets the context navigation timeout to 0 -- no limit --
|
|
286
|
+
// whenever `navigationTimeout` is absent (playwright/lib/index.js
|
|
287
|
+
// `_setupContextOptions`: `navigationTimeout || 0`), and TimeoutSettings
|
|
288
|
+
// resolves that 0 before it ever falls through to `actionTimeout`. So today
|
|
289
|
+
// every `goto`, `waitForURL` and `waitForLoadState` call that does not pass its
|
|
290
|
+
// own `timeout` is bounded only by the 12-minute test timeout.
|
|
291
|
+
//
|
|
292
|
+
// That is the hang primitive: `waitForLoadState('networkidle')` waits for 500ms
|
|
293
|
+
// of network silence that never arrives while a backend restarts and clients
|
|
294
|
+
// retry, and the suite has 86 of those calls. Bounding navigation converts a
|
|
295
|
+
// 12-minute stall into a 2-minute failure with a stack trace, without touching
|
|
296
|
+
// the per-test budget that genuinely slow flows rely on.
|
|
297
|
+
//
|
|
298
|
+
// A call that passes its own `timeout` always wins, so nothing bounded today
|
|
299
|
+
// becomes stricter; 120s matches the most generous explicit navigation timeout
|
|
300
|
+
// the suite already uses.
|
|
301
|
+
const NAVIGATION_TIMEOUT_MS =
|
|
302
|
+
numericEnv('C8E2E_NAVIGATION_TIMEOUT_MS') ?? 120 * 1000;
|
|
303
|
+
|
|
227
304
|
const timeout = () => {
|
|
228
305
|
if (process.env.MINOR_VERSION?.includes('SM-8.7')) {
|
|
229
306
|
// OpenSearch indexes more slowly than Elasticsearch — give SM-8.7 the
|
|
@@ -268,6 +345,8 @@ export default defineConfig({
|
|
|
268
345
|
// API specs into a browser project that has none of their environment.
|
|
269
346
|
testIgnore: /[\\/]tests[\\/]api[\\/]/,
|
|
270
347
|
timeout: timeout(),
|
|
348
|
+
globalTimeout: globalTimeout(),
|
|
349
|
+
maxFailures: maxFailures(),
|
|
271
350
|
workers: workerCount(),
|
|
272
351
|
retries: 1,
|
|
273
352
|
fullyParallel: false,
|
|
@@ -275,6 +354,7 @@ export default defineConfig({
|
|
|
275
354
|
use: {
|
|
276
355
|
baseURL: getBaseURL(),
|
|
277
356
|
actionTimeout: 10000,
|
|
357
|
+
navigationTimeout: NAVIGATION_TIMEOUT_MS,
|
|
278
358
|
// Stage 5: trace on-first-retry. With retries=1, a passing-on-retry
|
|
279
359
|
// test (i.e., a flake) leaves a trace from the first attempt — exactly
|
|
280
360
|
// the cases worth debugging. Green runs pay no trace cost.
|