@camunda/e2e-test-suite 0.0.627 → 0.0.628
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.9/LoginPage.d.ts +1 -0
- package/dist/pages/8.9/LoginPage.js +46 -5
- package/dist/pages/8.9/ModelerCreatePage.js +13 -2
- package/dist/pages/8.9/OCIdentityClusterVariablesPage.js +6 -1
- package/dist/pages/8.9/OperateProcessInstancePage.js +2 -2
- package/dist/pages/8.9/PlayPage.d.ts +4 -0
- package/dist/pages/8.9/PlayPage.js +45 -12
- package/dist/pages/8.9/UtilitiesPage.js +22 -2
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ declare class LoginPage {
|
|
|
20
20
|
readonly loginMessage: Locator;
|
|
21
21
|
readonly errorMessage: Locator;
|
|
22
22
|
readonly passwordHeading: Locator;
|
|
23
|
+
readonly rateLimitError: Locator;
|
|
23
24
|
constructor(page: Page);
|
|
24
25
|
fillUsername(username: string): Promise<void>;
|
|
25
26
|
clickContinueButton(): Promise<void>;
|
|
@@ -19,13 +19,19 @@ class LoginPage {
|
|
|
19
19
|
loginMessage;
|
|
20
20
|
errorMessage;
|
|
21
21
|
passwordHeading;
|
|
22
|
+
rateLimitError;
|
|
22
23
|
constructor(page) {
|
|
23
24
|
this.page = page;
|
|
24
25
|
this.usernameInput = {
|
|
25
|
-
locator: page
|
|
26
|
+
locator: page
|
|
27
|
+
.getByLabel(/Email address/i)
|
|
28
|
+
.and(page.locator(':not([id="c4-invite-email"])')),
|
|
26
29
|
schema: userNameInputValidInputSchema,
|
|
27
30
|
};
|
|
28
|
-
this.passwordInput = page
|
|
31
|
+
this.passwordInput = page
|
|
32
|
+
.getByLabel(/^Password\s*\*?$/i)
|
|
33
|
+
.or(page.locator('input[type="password"]'))
|
|
34
|
+
.first();
|
|
29
35
|
this.continueButton = page.getByRole('button', {
|
|
30
36
|
name: 'Continue',
|
|
31
37
|
exact: true,
|
|
@@ -39,14 +45,20 @@ class LoginPage {
|
|
|
39
45
|
exact: true,
|
|
40
46
|
});
|
|
41
47
|
this.errorMessage = page.getByText('Wrong email or password');
|
|
48
|
+
this.rateLimitError = page
|
|
49
|
+
.getByText(/rate limit has been reached/i)
|
|
50
|
+
.or(page.getByText(/We are sorry, an error occurred/i));
|
|
42
51
|
}
|
|
43
52
|
async fillUsername(username) {
|
|
53
|
+
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 120000 });
|
|
44
54
|
await this.usernameInput.locator.fill(username);
|
|
45
55
|
}
|
|
46
56
|
async clickContinueButton() {
|
|
57
|
+
await (0, test_1.expect)(this.continueButton).toBeEnabled({ timeout: 30000 });
|
|
47
58
|
await this.continueButton.click({ timeout: 30000 });
|
|
48
59
|
}
|
|
49
60
|
async fillPassword(password) {
|
|
61
|
+
await (0, test_1.expect)(this.passwordInput).toBeVisible({ timeout: 120000 });
|
|
50
62
|
await this.passwordInput.fill(password);
|
|
51
63
|
}
|
|
52
64
|
async clickLoginButton() {
|
|
@@ -73,9 +85,38 @@ class LoginPage {
|
|
|
73
85
|
await this.page
|
|
74
86
|
.waitForURL((url) => !url.hostname.includes('console'), { timeout: 60000 })
|
|
75
87
|
.catch(() => { });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
let authStep = 'unknown';
|
|
89
|
+
const deadline = Date.now() + 120000;
|
|
90
|
+
while (Date.now() < deadline) {
|
|
91
|
+
const [passwordHeadingVisible, passwordInputVisible, usernameVisible, rateLimitVisible,] = await Promise.all([
|
|
92
|
+
this.passwordHeading.isVisible().catch(() => false),
|
|
93
|
+
this.passwordInput.isVisible().catch(() => false),
|
|
94
|
+
this.usernameInput.locator.isVisible().catch(() => false),
|
|
95
|
+
this.rateLimitError
|
|
96
|
+
.first()
|
|
97
|
+
.isVisible()
|
|
98
|
+
.catch(() => false),
|
|
99
|
+
]);
|
|
100
|
+
if (rateLimitVisible) {
|
|
101
|
+
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
102
|
+
}
|
|
103
|
+
if (passwordHeadingVisible || passwordInputVisible) {
|
|
104
|
+
authStep = 'password';
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (usernameVisible) {
|
|
108
|
+
authStep = 'username';
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
await this.page.waitForTimeout(1000);
|
|
112
|
+
}
|
|
113
|
+
await (0, test_1.expect)(authStep).not.toBe('unknown');
|
|
114
|
+
if (authStep === 'username') {
|
|
115
|
+
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 120000 });
|
|
116
|
+
await this.fillUsername(username);
|
|
117
|
+
await this.clickContinueButton();
|
|
118
|
+
await (0, test_1.expect)(this.passwordInput).toBeVisible({ timeout: 120000 });
|
|
119
|
+
}
|
|
79
120
|
await this.fillPassword(password);
|
|
80
121
|
await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
|
|
81
122
|
await this.clickLoginButton();
|
|
@@ -768,7 +768,14 @@ class ModelerCreatePage {
|
|
|
768
768
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
769
769
|
try {
|
|
770
770
|
if (retries <= 2) {
|
|
771
|
-
|
|
771
|
+
// Wait explicitly for the option to render in the change-type
|
|
772
|
+
// panel before clicking. Without this we burn an attempt every
|
|
773
|
+
// time the panel is still loading after download.
|
|
774
|
+
await this.publicHolidayConnectorOption.waitFor({
|
|
775
|
+
state: 'visible',
|
|
776
|
+
timeout: 60000,
|
|
777
|
+
});
|
|
778
|
+
await this.publicHolidayConnectorOption.click({ timeout: 30000 });
|
|
772
779
|
}
|
|
773
780
|
else {
|
|
774
781
|
await this.clickMarketPlaceButton();
|
|
@@ -777,7 +784,11 @@ class ModelerCreatePage {
|
|
|
777
784
|
await connectorMarketplacePage.fillSearchForConnectorTextbox('Public Holiday Connector');
|
|
778
785
|
await (0, sleep_1.sleep)(10000);
|
|
779
786
|
await connectorMarketplacePage.downloadConnectorToProject();
|
|
780
|
-
await this.publicHolidayConnectorOption.
|
|
787
|
+
await this.publicHolidayConnectorOption.waitFor({
|
|
788
|
+
state: 'visible',
|
|
789
|
+
timeout: 120000,
|
|
790
|
+
});
|
|
791
|
+
await this.publicHolidayConnectorOption.click({ timeout: 30000 });
|
|
781
792
|
}
|
|
782
793
|
return;
|
|
783
794
|
}
|
|
@@ -53,7 +53,12 @@ class OCIdentityClusterVariablesPage {
|
|
|
53
53
|
await this.monacoEditor.click();
|
|
54
54
|
await this.monacoEditorTextArea.clear();
|
|
55
55
|
await this.monacoEditorTextArea.fill(newValue);
|
|
56
|
-
await
|
|
56
|
+
await this.monacoEditorTextArea.press('Tab');
|
|
57
|
+
await test_1.expect
|
|
58
|
+
.poll(async () => await this.saveVariableButton.isEnabled(), {
|
|
59
|
+
timeout: 30000,
|
|
60
|
+
})
|
|
61
|
+
.toBe(true);
|
|
57
62
|
await this.saveVariableButton.click();
|
|
58
63
|
await (0, test_1.expect)(this.successMessage).toBeVisible();
|
|
59
64
|
await (0, test_1.expect)(this.editVariableModal).toBeHidden();
|
|
@@ -77,10 +77,10 @@ class OperateProcessInstancePage {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
async assertProcessVariableContainsText(variableName, text) {
|
|
80
|
-
const maxRetries =
|
|
80
|
+
const maxRetries = 5;
|
|
81
81
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
82
82
|
try {
|
|
83
|
-
await (0, test_1.expect)(this.page.getByTestId(`variable-${variableName}`)).toContainText(text, { timeout:
|
|
83
|
+
await (0, test_1.expect)(this.page.getByTestId(`variable-${variableName}`)).toContainText(text, { timeout: 60000 });
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
catch (error) {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class PlayPage {
|
|
3
3
|
private page;
|
|
4
|
+
readonly diagram: Locator;
|
|
4
5
|
readonly completeJobButton: Locator;
|
|
6
|
+
readonly configureTestPanel: Locator;
|
|
7
|
+
readonly configureTestPanelStartButton: Locator;
|
|
8
|
+
readonly startInstanceOverlayButton: Locator;
|
|
5
9
|
constructor(page: Page);
|
|
6
10
|
waitForCompleteJobButtonToBeAvailable(): Promise<void>;
|
|
7
11
|
clickCompleteJobButton(): Promise<void>;
|
|
@@ -2,15 +2,33 @@
|
|
|
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;
|
|
9
|
+
diagram;
|
|
8
10
|
completeJobButton;
|
|
11
|
+
configureTestPanel;
|
|
12
|
+
configureTestPanelStartButton;
|
|
13
|
+
startInstanceOverlayButton;
|
|
9
14
|
constructor(page) {
|
|
10
15
|
this.page = page;
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
this.diagram = page.getByTestId('diagram');
|
|
17
|
+
this.completeJobButton = this.diagram.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 = this.diagram.getByLabel('Start instance', { exact: false });
|
|
14
32
|
}
|
|
15
33
|
async waitForCompleteJobButtonToBeAvailable() {
|
|
16
34
|
await (0, test_1.expect)(this.completeJobButton).toBeVisible({
|
|
@@ -21,17 +39,32 @@ class PlayPage {
|
|
|
21
39
|
await this.completeJobButton.click();
|
|
22
40
|
}
|
|
23
41
|
async clickStartInstanceButton() {
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
|
|
42
|
+
const startTrigger = this.configureTestPanelStartButton
|
|
43
|
+
.or(this.startInstanceOverlayButton)
|
|
44
|
+
.first();
|
|
45
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, startTrigger, {
|
|
46
|
+
totalTimeout: 240000,
|
|
47
|
+
visibilityTimeout: 60000,
|
|
48
|
+
maxRetries: 8,
|
|
49
|
+
retryDelayMs: 5000,
|
|
50
|
+
});
|
|
28
51
|
}
|
|
29
52
|
async dismissStartModal() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
// The intro dialog cycles labels across Modeler versions/states.
|
|
54
|
+
// Try the known variations in order; first match wins.
|
|
55
|
+
const buttonVariations = [
|
|
56
|
+
'Start a process instance',
|
|
57
|
+
'Start another instance',
|
|
58
|
+
'Start new instance',
|
|
59
|
+
'Start instance',
|
|
60
|
+
];
|
|
61
|
+
for (const buttonName of buttonVariations) {
|
|
62
|
+
const button = this.page.getByRole('button', { name: buttonName });
|
|
63
|
+
if ((await button.count()) > 0) {
|
|
64
|
+
await button.first().click();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
35
68
|
}
|
|
36
69
|
async waitForInstanceDetailsToBeLoaded() {
|
|
37
70
|
const maxRetries = 2;
|
|
@@ -8,7 +8,7 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
8
8
|
const randomSleep_1 = require("../../utils/randomSleep");
|
|
9
9
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
10
10
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
11
|
-
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries =
|
|
11
|
+
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
|
|
12
12
|
let lastError;
|
|
13
13
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
14
14
|
try {
|
|
@@ -35,14 +35,34 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3
|
|
|
35
35
|
.waitForLoadState('networkidle', { timeout: 30000 })
|
|
36
36
|
.catch(() => { });
|
|
37
37
|
await (0, sleep_1.sleep)(timeout);
|
|
38
|
+
// The identity provider occasionally returns a generic error / rate
|
|
39
|
+
// limit page that has none of the usual auth controls. Bail out early
|
|
40
|
+
// when that page is visible so the outer retry can back off.
|
|
41
|
+
if (await loginPage.rateLimitError
|
|
42
|
+
.first()
|
|
43
|
+
.isVisible()
|
|
44
|
+
.catch(() => false)) {
|
|
45
|
+
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
46
|
+
}
|
|
47
|
+
// Auth0 may restore directly on password entry depending on session
|
|
48
|
+
// history, so allow either identifier screen or password screen.
|
|
49
|
+
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
38
50
|
await loginPage.loginWithTestUser(testUser);
|
|
39
51
|
return;
|
|
40
52
|
}
|
|
41
53
|
catch (error) {
|
|
42
54
|
lastError = error;
|
|
43
55
|
if (attempt < maxRetries - 1) {
|
|
56
|
+
const rateLimited = String(error).includes('AUTH0_RATE_LIMIT');
|
|
44
57
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
45
|
-
|
|
58
|
+
if (rateLimited) {
|
|
59
|
+
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
60
|
+
// enough to outlast the window before the next attempt.
|
|
61
|
+
await (0, randomSleep_1.randomSleep)(60000, 90000);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await (0, randomSleep_1.randomSleep)(10000, 20000);
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
else {
|
|
48
68
|
console.error(lastError);
|