@camunda/e2e-test-suite 0.0.636 → 0.0.637
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.8/AppsPage.d.ts +1 -0
- package/dist/pages/8.8/AppsPage.js +76 -15
- package/dist/pages/8.8/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/8.8/ConnectorMarketplacePage.js +44 -7
- package/dist/pages/8.8/ConsoleOrganizationPage.js +11 -3
- package/dist/pages/8.8/HomePage.js +15 -14
- package/dist/pages/8.8/LoginPage.js +21 -4
- package/dist/pages/8.8/ModelerCreatePage.js +72 -21
- package/dist/pages/8.8/ModelerHomePage.js +60 -13
- package/dist/pages/8.8/OCIdentityRolesPage.js +10 -1
- package/dist/pages/8.8/PlayPage.d.ts +3 -0
- package/dist/pages/8.8/PlayPage.js +43 -9
- package/dist/pages/8.8/UtilitiesPage.js +28 -4
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ declare class AppsPage {
|
|
|
30
30
|
clickOperate(clusterName: string): Promise<void>;
|
|
31
31
|
clickOptimize(clusterName: string): Promise<void>;
|
|
32
32
|
clickCamundaApps(maxRetries?: number): Promise<void>;
|
|
33
|
+
private dismissBlockingModal;
|
|
33
34
|
clickConsoleLink(): Promise<void>;
|
|
34
35
|
assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
|
|
35
36
|
assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
|
|
@@ -68,23 +68,53 @@ class AppsPage {
|
|
|
68
68
|
await cluster.click();
|
|
69
69
|
}
|
|
70
70
|
async clickModeler() {
|
|
71
|
-
const maxRetries =
|
|
71
|
+
const maxRetries = 5;
|
|
72
|
+
const modelerBanner = this.page.getByRole('banner', {
|
|
73
|
+
name: 'Camunda Modeler',
|
|
74
|
+
});
|
|
75
|
+
const modelerDiagramDropdown = this.page.locator('[data-test="diagram-dropdown"]');
|
|
76
|
+
// If the click is already done and we are sitting on Modeler, return
|
|
77
|
+
// immediately — observed in failing runs where the banner role lookup
|
|
78
|
+
// never resolves because partial app.js 429s prevent the accessible
|
|
79
|
+
// name from being set, even though the project list rendered.
|
|
80
|
+
if (this.page.url().includes('modeler.')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
72
83
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
73
84
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
await this.clickCamundaApps();
|
|
86
|
+
await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
|
|
87
|
+
await this.modelerLink.click({ timeout: 30000 });
|
|
88
|
+
// Primary success signal: the browser actually landed on the
|
|
89
|
+
// Modeler hostname. Some Modeler renders never expose the banner
|
|
90
|
+
// accessible name when modeler asset requests get 429-throttled
|
|
91
|
+
// by SaaS infra, but the URL transition is reliable.
|
|
92
|
+
await this.page.waitForURL((url) => url.hostname.includes('modeler.'), {
|
|
93
|
+
timeout: 90000,
|
|
94
|
+
});
|
|
95
|
+
// Best-effort: also wait briefly for the banner or the project
|
|
96
|
+
// dropdown so the caller's next interaction has a stable DOM.
|
|
97
|
+
// Tolerate this timing out — the URL is already correct.
|
|
98
|
+
await Promise.race([
|
|
99
|
+
modelerBanner.waitFor({ state: 'visible', timeout: 30000 }),
|
|
100
|
+
modelerDiagramDropdown.waitFor({ state: 'visible', timeout: 30000 }),
|
|
101
|
+
]).catch(() => { });
|
|
83
102
|
return;
|
|
84
103
|
}
|
|
85
104
|
catch (error) {
|
|
86
105
|
console.warn(`Click attempt ${retries + 1} failed: ${error}`);
|
|
87
|
-
|
|
106
|
+
if (this.page.url().includes('modeler.')) {
|
|
107
|
+
// Click registered and navigation actually happened — accept it
|
|
108
|
+
// even though banner/dropdown took too long to settle.
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!this.page.isClosed()) {
|
|
112
|
+
await this.page
|
|
113
|
+
.waitForLoadState('domcontentloaded', { timeout: 20000 })
|
|
114
|
+
.catch(() => { });
|
|
115
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
116
|
+
}
|
|
117
|
+
await (0, sleep_1.sleep)(5000);
|
|
88
118
|
}
|
|
89
119
|
}
|
|
90
120
|
throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
|
|
@@ -272,13 +302,44 @@ class AppsPage {
|
|
|
272
302
|
this.appSwitcherButton,
|
|
273
303
|
];
|
|
274
304
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
305
|
+
try {
|
|
306
|
+
await this.dismissBlockingModal();
|
|
307
|
+
for (const appButton of appButtons) {
|
|
308
|
+
if (await appButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
309
|
+
await appButton.click({ timeout: 30000 });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
279
312
|
}
|
|
280
313
|
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
console.warn(`Click attempt ${retries + 1} failed while opening app switcher: ${error}`);
|
|
316
|
+
}
|
|
317
|
+
await this.dismissBlockingModal();
|
|
318
|
+
await (0, sleep_1.sleep)(2000);
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Failed to open app switcher after ${maxRetries} attempts.`);
|
|
321
|
+
}
|
|
322
|
+
async dismissBlockingModal() {
|
|
323
|
+
const visibleModal = this.page.locator('.cds--modal.is-visible').first();
|
|
324
|
+
if (!(await visibleModal.isVisible().catch(() => false))) {
|
|
325
|
+
return;
|
|
281
326
|
}
|
|
327
|
+
await this.page.keyboard.press('Escape').catch(() => { });
|
|
328
|
+
await this.page
|
|
329
|
+
.getByRole('button', { name: 'Close' })
|
|
330
|
+
.last()
|
|
331
|
+
.click({ timeout: 3000, force: true })
|
|
332
|
+
.catch(() => { });
|
|
333
|
+
await this.page
|
|
334
|
+
.getByRole('button', { name: 'Got it - Dismiss' })
|
|
335
|
+
.last()
|
|
336
|
+
.click({ timeout: 3000, force: true })
|
|
337
|
+
.catch(() => { });
|
|
338
|
+
await this.page
|
|
339
|
+
.getByRole('button', { name: 'Skip customization' })
|
|
340
|
+
.last()
|
|
341
|
+
.click({ timeout: 3000, force: true })
|
|
342
|
+
.catch(() => { });
|
|
282
343
|
}
|
|
283
344
|
async clickConsoleLink() {
|
|
284
345
|
const maxRetries = 3;
|
|
@@ -6,6 +6,8 @@ declare class ConnectorMarketplacePage {
|
|
|
6
6
|
readonly snackbar: Locator;
|
|
7
7
|
readonly closeButton: Locator;
|
|
8
8
|
readonly replaceResourceButton: Locator;
|
|
9
|
+
readonly addToProjectButton: Locator;
|
|
10
|
+
readonly saveAsCopyButton: Locator;
|
|
9
11
|
readonly cancelButton: Locator;
|
|
10
12
|
constructor(page: Page);
|
|
11
13
|
clickSearchForConnectorTextbox(): Promise<void>;
|
|
@@ -8,6 +8,8 @@ class ConnectorMarketplacePage {
|
|
|
8
8
|
snackbar;
|
|
9
9
|
closeButton;
|
|
10
10
|
replaceResourceButton;
|
|
11
|
+
addToProjectButton;
|
|
12
|
+
saveAsCopyButton;
|
|
11
13
|
cancelButton;
|
|
12
14
|
constructor(page) {
|
|
13
15
|
this.page = page;
|
|
@@ -20,6 +22,21 @@ class ConnectorMarketplacePage {
|
|
|
20
22
|
this.replaceResourceButton = page.getByRole('button', {
|
|
21
23
|
name: 'Replace resource',
|
|
22
24
|
});
|
|
25
|
+
// Camunda Hub's ImportModal renders one of three primary buttons depending
|
|
26
|
+
// on state. Source: camunda-hub
|
|
27
|
+
// frontend/apps/hub/src/components/ImportModal/get-action-buttons-props.js
|
|
28
|
+
// - "Add to project" for a brand-new resource (showPublish)
|
|
29
|
+
// - "Replace resource" when a duplicate exists and replace is allowed
|
|
30
|
+
// - "Save as copy" when a duplicate exists but replace is NOT
|
|
31
|
+
// allowed (e.g. an existing project template
|
|
32
|
+
// conflict — user must save the import under a
|
|
33
|
+
// new ID to keep both)
|
|
34
|
+
this.addToProjectButton = page.getByRole('button', {
|
|
35
|
+
name: 'Add to project',
|
|
36
|
+
});
|
|
37
|
+
this.saveAsCopyButton = page.getByRole('button', {
|
|
38
|
+
name: 'Save as copy',
|
|
39
|
+
});
|
|
23
40
|
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
24
41
|
}
|
|
25
42
|
async clickSearchForConnectorTextbox() {
|
|
@@ -42,15 +59,35 @@ class ConnectorMarketplacePage {
|
|
|
42
59
|
}
|
|
43
60
|
async downloadConnectorToProject() {
|
|
44
61
|
await this.clickDownloadToProjectButton();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
// After "Download to project", the Hub ImportModal opens. Per
|
|
63
|
+
// camunda-hub
|
|
64
|
+
// frontend/apps/hub/src/components/ImportModal/get-action-buttons-props.js
|
|
65
|
+
// the primary button is one of:
|
|
66
|
+
// - "Add to project" fresh import — we want to confirm
|
|
67
|
+
// - "Replace resource" duplicate exists, replace allowed
|
|
68
|
+
// - "Save as copy" duplicate exists, replace NOT allowed
|
|
69
|
+
//
|
|
70
|
+
// Strategy:
|
|
71
|
+
// - If "Add to project" is visible, click it: the connector wasn't
|
|
72
|
+
// in the project, this commits the fresh import.
|
|
73
|
+
// - Otherwise the connector is already in the project (conflict).
|
|
74
|
+
// Cancel the modal: Save as copy would create a duplicate under a
|
|
75
|
+
// different name (and the test then can't find "Worldwide Public
|
|
76
|
+
// Holiday"), and Replace risks other tests' state. The connector
|
|
77
|
+
// already exists — clickPublicHolidayConnectorOption just needs
|
|
78
|
+
// to find it in the change-element popup, which it does via a
|
|
79
|
+
// state reset.
|
|
80
|
+
if (await this.addToProjectButton
|
|
81
|
+
.isVisible({ timeout: 20000 })
|
|
82
|
+
.catch(() => false)) {
|
|
83
|
+
await this.addToProjectButton.click({ timeout: 30000 });
|
|
52
84
|
return;
|
|
53
85
|
}
|
|
86
|
+
if (await this.cancelButton.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
87
|
+
await this.cancelButton.click({ timeout: 30000 });
|
|
88
|
+
}
|
|
89
|
+
// If neither button is visible the import dialog never opened; nothing
|
|
90
|
+
// to dismiss and downstream state-reset will recover.
|
|
54
91
|
}
|
|
55
92
|
}
|
|
56
93
|
exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
|
|
@@ -266,9 +266,17 @@ class ConsoleOrganizationPage {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
//Enable Alpha Feature
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
const toggleTextLocator = alphaFeature.locator('[class= "cds--toggle__text"]');
|
|
270
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, toggleTextLocator, {
|
|
271
|
+
visibilityTimeout: 15000,
|
|
272
|
+
totalTimeout: 90000,
|
|
273
|
+
maxRetries: 3,
|
|
274
|
+
postAction: async () => {
|
|
275
|
+
await this.page.reload();
|
|
276
|
+
await this.clickSettingsTab();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const toggleText = await toggleTextLocator.textContent();
|
|
272
280
|
console.info(`Previous feature(${name}) setting is ${toggleText}.`);
|
|
273
281
|
if (toggleText == 'Enabled') {
|
|
274
282
|
console.log(`Feature ${name} is already enabled.`);
|
|
@@ -74,7 +74,13 @@ class HomePage {
|
|
|
74
74
|
if (!(await next.isVisible({ timeout: visibleTimeout }))) {
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
try {
|
|
78
|
+
await next.click({ timeout: 30000, force: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Dialogs are often re-rendered during fade animations; if the current
|
|
82
|
+
// close button becomes stale or hidden, continue and try the latest one.
|
|
83
|
+
}
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
organizationUuid() {
|
|
@@ -89,20 +95,15 @@ class HomePage {
|
|
|
89
95
|
await this.openOrganizationButton.click({ timeout: 60000 });
|
|
90
96
|
}
|
|
91
97
|
async clickSkipCustomization() {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
await this.buttonSkipCustomization.click();
|
|
100
|
-
await this.closeInformationDialog();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
console.error(error);
|
|
98
|
+
// The onboarding modal is not consistently shown for every session; this
|
|
99
|
+
// step must remain non-blocking for the rest of test setup.
|
|
100
|
+
const hasCustomizationModal = await this.buttonSkipCustomization
|
|
101
|
+
.isVisible({ timeout: 20000 })
|
|
102
|
+
.catch(() => false);
|
|
103
|
+
if (hasCustomizationModal) {
|
|
104
|
+
await this.buttonSkipCustomization.click({ timeout: 30000, force: true });
|
|
105
105
|
}
|
|
106
|
+
await this.closeInformationDialog();
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
exports.HomePage = HomePage;
|
|
@@ -17,7 +17,10 @@ class LoginPage {
|
|
|
17
17
|
this.usernameInput = page
|
|
18
18
|
.getByLabel('Email address')
|
|
19
19
|
.and(page.locator(':not([id="c4-invite-email"])'));
|
|
20
|
-
this.passwordInput = page
|
|
20
|
+
this.passwordInput = page
|
|
21
|
+
.getByLabel(/^Password\s*\*?$/i)
|
|
22
|
+
.or(page.locator('input[type="password"]'))
|
|
23
|
+
.first();
|
|
21
24
|
this.continueButton = page.getByRole('button', {
|
|
22
25
|
name: 'Continue',
|
|
23
26
|
exact: true,
|
|
@@ -75,9 +78,23 @@ class LoginPage {
|
|
|
75
78
|
await this.page.waitForLoadState('domcontentloaded', { timeout: 30000 });
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
await
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const onPasswordStep = await this.passwordHeading
|
|
82
|
+
.isVisible({ timeout: 5000 })
|
|
83
|
+
.catch(() => false);
|
|
84
|
+
if (!onPasswordStep) {
|
|
85
|
+
// After logout the page can stall on a blank/intermediate Console
|
|
86
|
+
// state and never redirect to Auth0. Wait a shorter window first and
|
|
87
|
+
// reload once if the email field still isn't there.
|
|
88
|
+
try {
|
|
89
|
+
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 60000 });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
await this.page.reload();
|
|
93
|
+
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 120000 });
|
|
94
|
+
}
|
|
95
|
+
await this.fillUsername(username);
|
|
96
|
+
await this.clickContinueButton();
|
|
97
|
+
}
|
|
81
98
|
await this.fillPassword(password);
|
|
82
99
|
await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
|
|
83
100
|
await this.clickLoginButton();
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ModelerCreatePage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
|
-
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
7
6
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
8
7
|
class ModelerCreatePage {
|
|
9
8
|
page;
|
|
@@ -187,9 +186,17 @@ class ModelerCreatePage {
|
|
|
187
186
|
this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
|
|
188
187
|
this.payloadInput = page.locator('[class="fjs-input"]');
|
|
189
188
|
this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
// Match the listitem rendered by bpmn-js's change-element popup. The
|
|
190
|
+
// previous getByText-inside-[data-test="modeler"] resolver finds the
|
|
191
|
+
// text node anywhere in the modeler, including off-screen positions in
|
|
192
|
+
// the virtualized list — which never satisfies toBeVisible. Targeting
|
|
193
|
+
// the listitem by accessible name mirrors the SM-8.8 pattern
|
|
194
|
+
// (pages/SM-8.8/ModelerCreatePage.ts:165) and works whether the
|
|
195
|
+
// connector is at the top of the list or filtered via search.
|
|
196
|
+
this.publicHolidayConnectorOption = page.getByRole('listitem', {
|
|
197
|
+
name: 'Worldwide Public Holiday',
|
|
198
|
+
exact: false,
|
|
199
|
+
});
|
|
193
200
|
this.publicHolidayYearOption = page.getByLabel('Year');
|
|
194
201
|
this.publicHolidayCountryCodeOption = page.getByLabel('Countrycode');
|
|
195
202
|
this.implementationSection = page.locator('[data-group-id="group-userTaskImplementation"]');
|
|
@@ -777,33 +784,77 @@ class ModelerCreatePage {
|
|
|
777
784
|
throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
|
|
778
785
|
}
|
|
779
786
|
async clickPublicHolidayConnectorOption() {
|
|
787
|
+
// Port of the 8.7 state-reset approach
|
|
788
|
+
// (pages/8.7/ModelerCreatePage.ts). After the test's marketplace flow
|
|
789
|
+
// completes, the connector is on the project (whether freshly added or
|
|
790
|
+
// already there from a prior run) but the modeler may be in any of
|
|
791
|
+
// several states: change-element popup closed, residual import modal
|
|
792
|
+
// up, virtualized list with the connector off-screen. Instead of
|
|
793
|
+
// trying to manage those states, dismiss any overlay, reload the page,
|
|
794
|
+
// then re-open the change-element popup from a clean state. The
|
|
795
|
+
// downloaded connector persists across reloads (server-side project
|
|
796
|
+
// state). Filter the popup so the listitem comes into view —
|
|
797
|
+
// `.djs-popup-search input` is the popup's own search, same selector
|
|
798
|
+
// the camunda-hub e2e tests use under
|
|
799
|
+
// e2e/cypress/e2e/bpmn/browse-market-place/.
|
|
780
800
|
const maxRetries = 4;
|
|
801
|
+
const setupModelerState = async () => {
|
|
802
|
+
const onModeler = await this.page
|
|
803
|
+
.locator('[data-test="modeler"]')
|
|
804
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
805
|
+
.then(() => true)
|
|
806
|
+
.catch(() => false);
|
|
807
|
+
if (!onModeler) {
|
|
808
|
+
await this.page.keyboard.press('Escape');
|
|
809
|
+
await this.page.waitForTimeout(2000);
|
|
810
|
+
const afterEscape = await this.page
|
|
811
|
+
.locator('[data-test="modeler"]')
|
|
812
|
+
.waitFor({ state: 'visible', timeout: 3000 })
|
|
813
|
+
.then(() => true)
|
|
814
|
+
.catch(() => false);
|
|
815
|
+
if (!afterEscape) {
|
|
816
|
+
await this.page.goBack();
|
|
817
|
+
await this.page.waitForTimeout(5000);
|
|
818
|
+
}
|
|
819
|
+
await this.page.reload();
|
|
820
|
+
await this.page.waitForTimeout(5000);
|
|
821
|
+
}
|
|
822
|
+
await this.clickCanvas();
|
|
823
|
+
await this.secondElement.click({ timeout: 60000 });
|
|
824
|
+
await this.clickChangeTypeButton();
|
|
825
|
+
// Wait for the change-element popup to be fully loaded.
|
|
826
|
+
await (0, test_1.expect)(this.marketPlaceButton).toBeVisible({ timeout: 60000 });
|
|
827
|
+
// Filter the change-element popup so the connector listitem comes
|
|
828
|
+
// into the rendered slice of the virtualized list.
|
|
829
|
+
const changeElementSearch = this.page.locator('.djs-popup-search input');
|
|
830
|
+
if (await changeElementSearch.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
831
|
+
await changeElementSearch.fill('Worldwide Public Holiday');
|
|
832
|
+
}
|
|
833
|
+
};
|
|
781
834
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
782
835
|
try {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
await this.clickMarketPlaceButton();
|
|
788
|
-
const connectorMarketplacePage = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
|
|
789
|
-
await connectorMarketplacePage.clickSearchForConnectorTextbox();
|
|
790
|
-
await connectorMarketplacePage.fillSearchForConnectorTextbox('Worldwide Public Holiday');
|
|
791
|
-
await (0, sleep_1.sleep)(10000);
|
|
792
|
-
await connectorMarketplacePage.downloadConnectorToProject();
|
|
793
|
-
await this.publicHolidayConnectorOption.click({ timeout: 120000 });
|
|
794
|
-
}
|
|
836
|
+
await setupModelerState();
|
|
837
|
+
await this.publicHolidayConnectorOption.click({ timeout: 120000 });
|
|
795
838
|
return;
|
|
796
839
|
}
|
|
797
840
|
catch (error) {
|
|
798
841
|
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
842
|
+
if (retries >= maxRetries - 1)
|
|
843
|
+
break;
|
|
844
|
+
const onModeler = await this.page
|
|
845
|
+
.locator('[data-test="modeler"]')
|
|
846
|
+
.waitFor({ state: 'visible', timeout: 3000 })
|
|
847
|
+
.then(() => true)
|
|
848
|
+
.catch(() => false);
|
|
849
|
+
if (!onModeler) {
|
|
850
|
+
await this.page.goBack();
|
|
851
|
+
await this.page.waitForTimeout(3000);
|
|
852
|
+
}
|
|
799
853
|
await this.page.reload();
|
|
800
|
-
await
|
|
801
|
-
await this.clickCanvas();
|
|
802
|
-
await this.secondElement.click({ timeout: 60000 });
|
|
803
|
-
await this.clickChangeTypeButton();
|
|
854
|
+
await this.page.waitForTimeout(5000);
|
|
804
855
|
}
|
|
805
856
|
}
|
|
806
|
-
throw new Error(`Failed to click the
|
|
857
|
+
throw new Error(`Failed to click the public holiday connector after ${maxRetries} attempts.`);
|
|
807
858
|
}
|
|
808
859
|
async clickPublicHolidayYearOption() {
|
|
809
860
|
await this.publicHolidayYearOption.click({ timeout: 60000 });
|
|
@@ -103,9 +103,28 @@ class ModelerHomePage {
|
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
async enterNewProjectName(name) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
await (0, test_1.expect)(this.projectNameInput.first()).toBeVisible({
|
|
109
|
+
timeout: 90000,
|
|
110
|
+
});
|
|
111
|
+
await this.projectNameInput
|
|
112
|
+
.first()
|
|
113
|
+
.click({ timeout: 60000, force: true });
|
|
114
|
+
await this.projectNameInput.first().fill(name);
|
|
115
|
+
await this.projectNameInput.first().press('Enter');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (attempt >= 2) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
await this.dismissOverlays();
|
|
123
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
124
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
125
|
+
await this.clickCreateNewProjectButton();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
async enterIdpApplicationName(name) {
|
|
111
130
|
await this.idpApplicationNameInput.click({ timeout: 60000 });
|
|
@@ -131,10 +150,24 @@ class ModelerHomePage {
|
|
|
131
150
|
await process.click();
|
|
132
151
|
}
|
|
133
152
|
async clickDiagramTypeDropdown() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
await this.dismissOverlays();
|
|
156
|
+
await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
|
|
157
|
+
timeout: 45000,
|
|
158
|
+
});
|
|
159
|
+
await this.diagramTypeDropdown.click({ timeout: 30000 });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (attempt >= 3) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
167
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
168
|
+
await this.clickCrossComponentProjectFolder();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
138
171
|
}
|
|
139
172
|
async clickBpmnTemplateOption() {
|
|
140
173
|
await this.bpmnTemplateOption.click({ timeout: 120000 });
|
|
@@ -143,8 +176,9 @@ class ModelerHomePage {
|
|
|
143
176
|
await this.formTemplateOption.click();
|
|
144
177
|
}
|
|
145
178
|
async enterFormName(name) {
|
|
146
|
-
await this.formNameInput.
|
|
147
|
-
await this.formNameInput.
|
|
179
|
+
await (0, test_1.expect)(this.formNameInput).toBeVisible({ timeout: 120000 });
|
|
180
|
+
await this.formNameInput.click({ timeout: 60000, force: true });
|
|
181
|
+
await this.formNameInput.fill(name, { timeout: 60000 });
|
|
148
182
|
await this.formNameInput.press('Enter');
|
|
149
183
|
}
|
|
150
184
|
async clickProjectBreadcrumb() {
|
|
@@ -166,10 +200,23 @@ class ModelerHomePage {
|
|
|
166
200
|
await this.openOrganizationsButton.click({ timeout: 30000 });
|
|
167
201
|
}
|
|
168
202
|
async createForm(formName) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
const maxRetries = 3;
|
|
204
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
205
|
+
try {
|
|
206
|
+
await this.clickDiagramTypeDropdown();
|
|
207
|
+
await this.clickFormOption();
|
|
208
|
+
await this.enterFormName(formName);
|
|
209
|
+
await (0, sleep_1.sleep)(10000);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (attempt >= maxRetries - 1) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
217
|
+
await this.clickCrossComponentProjectFolder();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
173
220
|
}
|
|
174
221
|
async clickCreateIdpApplicationButton() {
|
|
175
222
|
await this.createIdpApplicationButton.click();
|
|
@@ -120,7 +120,16 @@ class OCIdentityRolesPage {
|
|
|
120
120
|
await (0, test_1.expect)(this.assignUserModal).toBeVisible({ timeout: 30000 });
|
|
121
121
|
await this.assignUserModalInputField.fill(email);
|
|
122
122
|
await this.assignUserModalAssignButton.click({ timeout: 30000 });
|
|
123
|
-
|
|
123
|
+
try {
|
|
124
|
+
await (0, test_1.expect)(this.assignUserModal).not.toBeVisible({ timeout: 30000 });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The Assign click occasionally lands while the modal is mid-render
|
|
128
|
+
// and the submission is silently dropped — the modal stays open with
|
|
129
|
+
// no error surfaced. Re-click Assign once before failing the test.
|
|
130
|
+
await this.assignUserModalAssignButton.click({ timeout: 30000 });
|
|
131
|
+
await (0, test_1.expect)(this.assignUserModal).not.toBeVisible({ timeout: 30000 });
|
|
132
|
+
}
|
|
124
133
|
}
|
|
125
134
|
async createRole(role) {
|
|
126
135
|
await (0, test_1.expect)(this.createRoleButton).toBeVisible({ timeout: 30000 });
|
|
@@ -2,6 +2,9 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class PlayPage {
|
|
3
3
|
private page;
|
|
4
4
|
readonly completeJobButton: Locator;
|
|
5
|
+
readonly configureTestPanel: Locator;
|
|
6
|
+
readonly configureTestPanelStartButton: Locator;
|
|
7
|
+
readonly startInstanceOverlayButton: Locator;
|
|
5
8
|
constructor(page: Page);
|
|
6
9
|
waitForCompleteJobButtonToBeAvailable(): Promise<void>;
|
|
7
10
|
clickCompleteJobButton(): Promise<void>;
|
|
@@ -2,15 +2,35 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PlayPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
5
6
|
const maxWaitTimeSeconds = 120000;
|
|
6
7
|
class PlayPage {
|
|
7
8
|
page;
|
|
8
9
|
completeJobButton;
|
|
10
|
+
configureTestPanel;
|
|
11
|
+
configureTestPanelStartButton;
|
|
12
|
+
startInstanceOverlayButton;
|
|
9
13
|
constructor(page) {
|
|
10
14
|
this.page = page;
|
|
11
15
|
this.completeJobButton = page
|
|
12
16
|
.getByTestId('diagram')
|
|
13
17
|
.getByLabel('Complete job');
|
|
18
|
+
// Newer Modeler Play UI: the "Configure Scenario" floating card hosts a
|
|
19
|
+
// primary "Start" Carbon button (variants: "Start", "Start with
|
|
20
|
+
// variables", "Start with a form"). Source:
|
|
21
|
+
// camunda-hub frontend/packages/modeler/play/src/Definition/
|
|
22
|
+
// ConfigureTestPanel/index.tsx — renders <FloatingCard
|
|
23
|
+
// data-testid="configure-test-panel"> with a <Button> whose visible
|
|
24
|
+
// text starts with "Start".
|
|
25
|
+
this.configureTestPanel = page.getByTestId('configure-test-panel');
|
|
26
|
+
this.configureTestPanelStartButton = this.configureTestPanel.getByRole('button', { name: /^Start( with .*)?$/, exact: false });
|
|
27
|
+
// Legacy bpmn-js canvas overlay button (still emitted by older Modeler
|
|
28
|
+
// builds, also relabeled with cached/example-data suffixes). Kept as a
|
|
29
|
+
// fallback for environments that haven't shipped the ConfigureTestPanel
|
|
30
|
+
// yet.
|
|
31
|
+
this.startInstanceOverlayButton = page
|
|
32
|
+
.getByTestId('diagram')
|
|
33
|
+
.getByLabel('Start instance', { exact: false });
|
|
14
34
|
}
|
|
15
35
|
async waitForCompleteJobButtonToBeAvailable() {
|
|
16
36
|
await (0, test_1.expect)(this.completeJobButton).toBeVisible({
|
|
@@ -21,17 +41,31 @@ class PlayPage {
|
|
|
21
41
|
await this.completeJobButton.click();
|
|
22
42
|
}
|
|
23
43
|
async clickStartInstanceButton() {
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
|
|
44
|
+
const startTrigger = this.configureTestPanelStartButton
|
|
45
|
+
.or(this.startInstanceOverlayButton)
|
|
46
|
+
.first();
|
|
47
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, startTrigger, {
|
|
48
|
+
totalTimeout: 240000,
|
|
49
|
+
visibilityTimeout: 60000,
|
|
50
|
+
maxRetries: 8,
|
|
51
|
+
retryDelayMs: 5000,
|
|
52
|
+
});
|
|
28
53
|
}
|
|
29
54
|
async dismissStartModal() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
// The intro dialog cycles labels across Modeler versions/states.
|
|
56
|
+
const buttonVariations = [
|
|
57
|
+
'Start a process instance',
|
|
58
|
+
'Start another instance',
|
|
59
|
+
'Start new instance',
|
|
60
|
+
'Start instance',
|
|
61
|
+
];
|
|
62
|
+
for (const buttonName of buttonVariations) {
|
|
63
|
+
const button = this.page.getByRole('button', { name: buttonName });
|
|
64
|
+
if ((await button.count()) > 0) {
|
|
65
|
+
await button.first().click();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
35
69
|
}
|
|
36
70
|
async waitForInstanceDetailsToBeLoaded() {
|
|
37
71
|
const maxRetries = 2;
|
|
@@ -33,21 +33,45 @@ async function forceLogoutIfNeeded(page) {
|
|
|
33
33
|
}
|
|
34
34
|
exports.forceLogoutIfNeeded = forceLogoutIfNeeded;
|
|
35
35
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3) {
|
|
36
|
+
let lastError;
|
|
36
37
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
37
38
|
try {
|
|
38
|
-
await page.
|
|
39
|
+
await page.context().clearCookies();
|
|
40
|
+
await page.goto('about:blank');
|
|
41
|
+
await page
|
|
42
|
+
.evaluate(() => {
|
|
43
|
+
try {
|
|
44
|
+
localStorage.clear();
|
|
45
|
+
}
|
|
46
|
+
catch (_) {
|
|
47
|
+
// storage may be unavailable in some contexts
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
sessionStorage.clear();
|
|
51
|
+
}
|
|
52
|
+
catch (_) {
|
|
53
|
+
// storage may be unavailable in some contexts
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.catch(() => { });
|
|
57
|
+
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
58
|
+
await page
|
|
59
|
+
.waitForLoadState('networkidle', { timeout: 30000 })
|
|
60
|
+
.catch(() => { });
|
|
39
61
|
await (0, sleep_1.sleep)(timeout);
|
|
62
|
+
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
40
63
|
await loginPage.loginWithTestUser(testUser);
|
|
41
64
|
return;
|
|
42
65
|
}
|
|
43
66
|
catch (error) {
|
|
67
|
+
lastError = error;
|
|
44
68
|
if (attempt < maxRetries - 1) {
|
|
45
|
-
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying...`);
|
|
69
|
+
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`);
|
|
46
70
|
await (0, randomSleep_1.randomSleep)(10000, 20000);
|
|
47
71
|
}
|
|
48
72
|
else {
|
|
49
|
-
console.error(
|
|
50
|
-
throw new Error(`Login failed after ${maxRetries} attempts`);
|
|
73
|
+
console.error(lastError);
|
|
74
|
+
throw new Error(`Login failed after ${maxRetries} attempts: ${String(lastError)}`);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
}
|