@camunda/e2e-test-suite 0.0.1016 → 0.0.1018
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/LoginPage.d.ts +1 -0
- package/dist/pages/8.10/LoginPage.js +11 -0
- package/dist/pages/8.10/UtilitiesPage.js +24 -2
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/SM-8.10/ModelerCreatePage.js +4 -0
- package/dist/pages/SM-8.10/ModelerHomePage.js +1 -0
- package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +7 -0
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +76 -0
- 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,6 +19,7 @@ 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 = {
|
|
@@ -39,6 +40,16 @@ class LoginPage {
|
|
|
39
40
|
this.passwordHeading = page.getByRole('heading', {
|
|
40
41
|
name: 'Enter Your Password',
|
|
41
42
|
});
|
|
43
|
+
// The identity provider occasionally returns a generic error / rate
|
|
44
|
+
// limit page under load ("...rate limit has been reached") that has
|
|
45
|
+
// none of the usual auth controls. Ported from 8.9's LoginPage.ts
|
|
46
|
+
// (fix(8.9): back off when Auth0 returns rate-limit page) -- 8.10 was
|
|
47
|
+
// missing this detection entirely, so a rate-limited login here fell
|
|
48
|
+
// through to the generic 10-20s retry backoff instead of the 60-90s
|
|
49
|
+
// needed to outlast a typical rate-limit window.
|
|
50
|
+
this.rateLimitError = page
|
|
51
|
+
.getByText(/rate limit has been reached/i)
|
|
52
|
+
.or(page.getByText(/We are sorry, an error occurred/i));
|
|
42
53
|
this.loginButton = page.getByRole('button', {
|
|
43
54
|
name: 'Continue',
|
|
44
55
|
exact: true,
|
|
@@ -45,6 +45,19 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
45
45
|
let lastState = 'no login form';
|
|
46
46
|
while (Date.now() < deadline) {
|
|
47
47
|
const round = Math.min(roundTimeout, deadline - Date.now());
|
|
48
|
+
// The identity provider occasionally returns a generic error / rate
|
|
49
|
+
// limit page that has none of the usual auth controls. Checked once per
|
|
50
|
+
// iteration, outside expect.poll: a throw inside a poll predicate is
|
|
51
|
+
// swallowed and retried by design, which would burn this function's
|
|
52
|
+
// whole budget waiting for a login form that will never appear. Bailing
|
|
53
|
+
// out immediately with a typed marker lets loginWithRetry's caller back
|
|
54
|
+
// off long enough to actually outlast the rate-limit window instead.
|
|
55
|
+
if (await loginPage.rateLimitError
|
|
56
|
+
.first()
|
|
57
|
+
.isVisible()
|
|
58
|
+
.catch(() => false)) {
|
|
59
|
+
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
60
|
+
}
|
|
48
61
|
try {
|
|
49
62
|
// Confirm we reached the login page before filling credentials.
|
|
50
63
|
// Without this check the locator for 'Email address' can match a hidden
|
|
@@ -87,7 +100,7 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
87
100
|
}
|
|
88
101
|
throw new Error(`Auth0 login form did not render within ${totalTimeout}ms: ${lastState}`);
|
|
89
102
|
}
|
|
90
|
-
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries =
|
|
103
|
+
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
|
|
91
104
|
let lastError;
|
|
92
105
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
93
106
|
try {
|
|
@@ -145,8 +158,17 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3
|
|
|
145
158
|
catch (error) {
|
|
146
159
|
lastError = error;
|
|
147
160
|
if (attempt < maxRetries - 1) {
|
|
161
|
+
const rateLimited = String(error).includes('AUTH0_RATE_LIMIT');
|
|
148
162
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
149
|
-
|
|
163
|
+
if (rateLimited) {
|
|
164
|
+
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
165
|
+
// enough to outlast the window before the next attempt, rather
|
|
166
|
+
// than retrying inside it and burning another attempt for free.
|
|
167
|
+
await (0, randomSleep_1.randomSleep)(60000, 90000);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await (0, randomSleep_1.randomSleep)(10000, 20000);
|
|
171
|
+
}
|
|
150
172
|
}
|
|
151
173
|
else {
|
|
152
174
|
console.error(lastError);
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ModelerCreatePage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
6
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
6
7
|
const sleep_1 = require("../../utils/sleep");
|
|
7
8
|
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
8
9
|
class ModelerCreatePage {
|
|
9
10
|
page;
|
|
11
|
+
processApplicationPage;
|
|
10
12
|
generalPanel;
|
|
11
13
|
taskDefinitionPanel;
|
|
12
14
|
processIdInput;
|
|
@@ -103,6 +105,7 @@ class ModelerCreatePage {
|
|
|
103
105
|
openDetailsPanel;
|
|
104
106
|
constructor(page) {
|
|
105
107
|
this.page = page;
|
|
108
|
+
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
106
109
|
this.generalPanel = page.locator('[data-group-id="group-general"]');
|
|
107
110
|
this.taskDefinitionPanel = page
|
|
108
111
|
.locator('div')
|
|
@@ -396,6 +399,7 @@ class ModelerCreatePage {
|
|
|
396
399
|
.click({ timeout: 90000 });
|
|
397
400
|
}
|
|
398
401
|
async runProcessInstance(variables = '', tenant = '', businessId = '') {
|
|
402
|
+
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
399
403
|
await this.page.waitForTimeout(2000); // the canvas autosaves. This can take a second to save.
|
|
400
404
|
const maxRetries = 3;
|
|
401
405
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -392,6 +392,7 @@ class ModelerHomePage {
|
|
|
392
392
|
await this.clickCreateProcessApplication();
|
|
393
393
|
await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
|
|
394
394
|
this.processApplicationUrl = this.page.url();
|
|
395
|
+
this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
|
|
395
396
|
}
|
|
396
397
|
// Which control creates a process application depends on the hub version
|
|
397
398
|
// under test. Since camunda/camunda-hub#25831 it is a plain primary button on
|
|
@@ -12,12 +12,19 @@ declare class ProcessApplicationPage {
|
|
|
12
12
|
readonly titleBar: Locator;
|
|
13
13
|
readonly createNewDropdown: Locator;
|
|
14
14
|
readonly inlineNameInput: Locator;
|
|
15
|
+
readonly defineStagesDialog: Locator;
|
|
16
|
+
readonly developmentClusterSelect: Locator;
|
|
17
|
+
readonly defineStagesSaveButton: Locator;
|
|
15
18
|
constructor(page: Page);
|
|
16
19
|
private pageReady;
|
|
17
20
|
waitForProcessApplication(timeout?: number): Promise<void>;
|
|
18
21
|
clickCreateProcessApplicationButton(): Promise<void>;
|
|
19
22
|
clickCreateProcessApplicationOption(): Promise<void>;
|
|
20
23
|
nameNewProcessApplication(name: string): Promise<void>;
|
|
24
|
+
private defineDevelopmentStage;
|
|
25
|
+
rememberResourceContainer(url: string): void;
|
|
26
|
+
ensureStageCluster(clusterName?: string): Promise<void>;
|
|
27
|
+
private openStageDefinition;
|
|
21
28
|
openByName(name: string): Promise<void>;
|
|
22
29
|
}
|
|
23
30
|
export { ProcessApplicationPage };
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ProcessApplicationPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
6
|
+
// ModelerHomePage creates the container, while ModelerCreatePage later binds
|
|
7
|
+
// its stage. Both page objects share the same Playwright Page.
|
|
8
|
+
const resourceContainers = new WeakMap();
|
|
6
9
|
// Since camunda/camunda-hub#25831 a process application is a first-class
|
|
7
10
|
// container rather than a folder, and a workspace root ("project" in today's
|
|
8
11
|
// UI) holds nothing but process applications and IDP applications. Every file
|
|
@@ -29,6 +32,9 @@ class ProcessApplicationPage {
|
|
|
29
32
|
titleBar;
|
|
30
33
|
createNewDropdown;
|
|
31
34
|
inlineNameInput;
|
|
35
|
+
defineStagesDialog;
|
|
36
|
+
developmentClusterSelect;
|
|
37
|
+
defineStagesSaveButton;
|
|
32
38
|
constructor(page) {
|
|
33
39
|
this.page = page;
|
|
34
40
|
// .first() defensively only: hub renders exactly one of these today, in the
|
|
@@ -72,6 +78,14 @@ class ProcessApplicationPage {
|
|
|
72
78
|
.locator('[data-test="diagram-dropdown"]')
|
|
73
79
|
.first();
|
|
74
80
|
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
81
|
+
this.defineStagesDialog = page
|
|
82
|
+
.getByRole('dialog')
|
|
83
|
+
.filter({ hasText: 'Define stages' });
|
|
84
|
+
this.developmentClusterSelect = this.defineStagesDialog.locator('#dropdown-development-cluster');
|
|
85
|
+
this.defineStagesSaveButton = this.defineStagesDialog.getByRole('button', {
|
|
86
|
+
name: 'Save',
|
|
87
|
+
exact: true,
|
|
88
|
+
});
|
|
75
89
|
}
|
|
76
90
|
// Any one of the title bar, the resource region or the breadcrumb proves we
|
|
77
91
|
// are on the process application page. Three branches because the Design
|
|
@@ -118,6 +132,68 @@ class ProcessApplicationPage {
|
|
|
118
132
|
await this.inlineNameInput.press('Enter');
|
|
119
133
|
await this.waitForProcessApplication();
|
|
120
134
|
}
|
|
135
|
+
async defineDevelopmentStage(clusterName) {
|
|
136
|
+
await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 60000 });
|
|
137
|
+
await this.developmentClusterSelect.click({ timeout: 30000 });
|
|
138
|
+
const escapedClusterName = clusterName?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
139
|
+
const option = clusterName
|
|
140
|
+
? this.page
|
|
141
|
+
.getByRole('option', { name: new RegExp(`^${escapedClusterName}`) })
|
|
142
|
+
.first()
|
|
143
|
+
: this.page
|
|
144
|
+
.getByRole('option')
|
|
145
|
+
.filter({ hasNotText: 'Select a development cluster' })
|
|
146
|
+
.first();
|
|
147
|
+
await option.click({ timeout: 30000 });
|
|
148
|
+
await this.defineStagesSaveButton.click({ timeout: 30000 });
|
|
149
|
+
await (0, test_1.expect)(this.defineStagesDialog).toBeHidden({ timeout: 60000 });
|
|
150
|
+
}
|
|
151
|
+
rememberResourceContainer(url) {
|
|
152
|
+
resourceContainers.set(this.page, { url });
|
|
153
|
+
}
|
|
154
|
+
async ensureStageCluster(clusterName) {
|
|
155
|
+
const container = resourceContainers.get(this.page);
|
|
156
|
+
if (!container ||
|
|
157
|
+
(container.stageBound && container.developmentCluster === clusterName)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const returnTo = this.page.url();
|
|
161
|
+
const processApplicationUrl = container.url
|
|
162
|
+
.replace(/[?#].*$/, '')
|
|
163
|
+
.replace(/\/$/, '');
|
|
164
|
+
await (0, test_1.expect)(async () => {
|
|
165
|
+
await this.page.goto(`${processApplicationUrl}/connected-clusters`, {
|
|
166
|
+
waitUntil: 'domcontentloaded',
|
|
167
|
+
});
|
|
168
|
+
await this.openStageDefinition();
|
|
169
|
+
await this.defineDevelopmentStage(clusterName);
|
|
170
|
+
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
171
|
+
container.stageBound = true;
|
|
172
|
+
container.developmentCluster = clusterName;
|
|
173
|
+
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
174
|
+
}
|
|
175
|
+
async openStageDefinition() {
|
|
176
|
+
const opened = await this.defineStagesDialog
|
|
177
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
178
|
+
.then(() => true)
|
|
179
|
+
.catch(() => false);
|
|
180
|
+
if (opened) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const reopen = this.page
|
|
184
|
+
.getByRole('button', { name: /define stages|edit stages|configure/i })
|
|
185
|
+
.first();
|
|
186
|
+
const canReopen = await reopen
|
|
187
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
188
|
+
.then(() => true)
|
|
189
|
+
.catch(() => false);
|
|
190
|
+
if (!canReopen) {
|
|
191
|
+
throw new Error('Could not open the "Define stages" dialog on /connected-clusters: it ' +
|
|
192
|
+
'did not open by itself and no "Define stages" / "Edit stages" / ' +
|
|
193
|
+
'"Configure" control was found.');
|
|
194
|
+
}
|
|
195
|
+
await reopen.click({ timeout: 30000 });
|
|
196
|
+
}
|
|
121
197
|
async openByName(name) {
|
|
122
198
|
const processApplication = this.page
|
|
123
199
|
.getByRole('row')
|