@camunda/e2e-test-suite 0.0.1017 → 0.0.1019

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.
@@ -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 = 3) {
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
- await (0, randomSleep_1.randomSleep)(10000, 20000);
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);
@@ -23,6 +23,7 @@ declare class ModelerHomePage {
23
23
  readonly uploadFilesButton: Locator;
24
24
  readonly messageBanner: Locator;
25
25
  readonly idpTemplateOption: Locator;
26
+ readonly idpEmptyStateButton: Locator;
26
27
  readonly idpApplicationNameInput: Locator;
27
28
  readonly createIdpApplicationButton: Locator;
28
29
  readonly dialog: Locator;
@@ -40,6 +41,7 @@ declare class ModelerHomePage {
40
41
  clickProcessDiagram(name: string): Promise<void>;
41
42
  clickDiagramTypeDropdown(): Promise<void>;
42
43
  clickWorkspaceDiagramTypeDropdown(): Promise<void>;
44
+ startIdpApplicationCreation(): Promise<void>;
43
45
  clickBpmnTemplateOption(): Promise<void>;
44
46
  clickFormOption(): Promise<void>;
45
47
  enterFormName(name: string): Promise<void>;
@@ -40,6 +40,7 @@ class ModelerHomePage {
40
40
  uploadFilesButton;
41
41
  messageBanner;
42
42
  idpTemplateOption;
43
+ idpEmptyStateButton;
43
44
  idpApplicationNameInput;
44
45
  createIdpApplicationButton;
45
46
  dialog;
@@ -68,6 +69,12 @@ class ModelerHomePage {
68
69
  this.idpTemplateOption = page
69
70
  .locator('[data-test="create-idp-application"]')
70
71
  .getByText('IDP Application');
72
+ // Empty workspace root: a plain button in hub's EmptyStateActionButtons,
73
+ // next to the "Create process application" CTA that ProcessApplicationPage
74
+ // already reaches for. .first() defensively only, matching that locator.
75
+ this.idpEmptyStateButton = page
76
+ .getByRole('button', { name: 'Create IDP application' })
77
+ .first();
71
78
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
72
79
  // Matched on the test id alone: hub's design-system variant of the create
73
80
  // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
@@ -189,6 +196,26 @@ class ModelerHomePage {
189
196
  });
190
197
  await this.diagramTypeDropdown.click();
191
198
  }
199
+ // Starts an IDP application from the workspace root. Which control exists
200
+ // there depends on whether the workspace holds anything: hub's EntityTable
201
+ // renders either the toolbar -- whose "Create new" menu carries the IDP item
202
+ // -- or the empty state, whose CTA is a plain button, never both. A fresh
203
+ // org lands on the empty state, so waiting on the menu alone finds no
204
+ // element at all. Race both and use whichever rendered; same shape as
205
+ // clickCreateProcessApplication, which already covers this for the sibling
206
+ // resource type.
207
+ async startIdpApplicationCreation() {
208
+ const rootLoaded = await this.becameVisible(this.idpEmptyStateButton.or(this.diagramTypeDropdown).first(), 60000);
209
+ if (!rootLoaded) {
210
+ await this.page.reload();
211
+ }
212
+ if (await this.becameVisible(this.idpEmptyStateButton, 5000)) {
213
+ await this.idpEmptyStateButton.click({ timeout: 60000 });
214
+ return;
215
+ }
216
+ await this.clickWorkspaceDiagramTypeDropdown();
217
+ await this.clickIdpApplicationTemplateOption();
218
+ }
192
219
  async clickBpmnTemplateOption() {
193
220
  await (0, test_1.expect)(this.bpmnTemplateOption).toBeVisible({
194
221
  timeout: 15000,
@@ -4,6 +4,7 @@ exports.OCIdentityRolesPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
6
6
  const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
7
+ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
7
8
  class OCIdentityRolesPage {
8
9
  page;
9
10
  selectResourceTypeTab;
@@ -128,8 +129,12 @@ class OCIdentityRolesPage {
128
129
  await (0, test_1.expect)(this.createRoleModal).toBeVisible({ timeout: 30000 });
129
130
  await this.createIdField.fill(role.id);
130
131
  await this.createNameField.fill(role.name);
131
- await this.createRoleModalCreateButton.click();
132
- await (0, test_1.expect)(this.createRoleModal).not.toBeVisible({ timeout: 30000 });
132
+ // POST /v2/roles intermittently answers 503 UNAVAILABLE ("connection error
133
+ // with one of the brokers") while a freshly created org's orchestration
134
+ // cluster settles. The modal then stays open with the form still valid, so
135
+ // re-submitting recovers; the close assertion itself is unchanged, and a
136
+ // persistent failure still fails the test.
137
+ await (0, submitModalWithRetry_1.submitModalWithRetry)(this.createRoleModalCreateButton, this.createRoleModal, { closeTimeout: 30000 });
133
138
  await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, this.rolesList
134
139
  .getByRole('cell', {
135
140
  name: role.name,
@@ -94,10 +94,11 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
94
94
  await modelerHomePage.clickCrossComponentProjectFolder();
95
95
  });
96
96
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
97
- // Workspace-level menu: an IDP application can only be created at
98
- // workspace root, never inside a process application.
99
- await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
100
- await modelerHomePage.clickIdpApplicationTemplateOption();
97
+ // Workspace level: an IDP application can only be created at
98
+ // workspace root, never inside a process application. The control
99
+ // there is the empty-state CTA or the "Create new" menu item,
100
+ // depending on whether the workspace already holds anything.
101
+ await modelerHomePage.startIdpApplicationCreation();
101
102
  });
102
103
  await _8_9_1.test.step('Creates a document extraction template', async () => {
103
104
  await modelerHomePage.enterIdpApplicationName(idpApplicationName);
@@ -168,10 +169,11 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
168
169
  await modelerHomePage.clickCrossComponentProjectFolder();
169
170
  });
170
171
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
171
- // Workspace-level menu: an IDP application can only be created at
172
- // workspace root, never inside a process application.
173
- await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
174
- await modelerHomePage.clickIdpApplicationTemplateOption();
172
+ // Workspace level: an IDP application can only be created at
173
+ // workspace root, never inside a process application. The control
174
+ // there is the empty-state CTA or the "Create new" menu item,
175
+ // depending on whether the workspace already holds anything.
176
+ await modelerHomePage.startIdpApplicationCreation();
175
177
  });
176
178
  await _8_9_1.test.step('Creates a document extraction template', async () => {
177
179
  await modelerHomePage.enterIdpApplicationName(idpApplicationName);
@@ -250,10 +252,11 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
250
252
  await modelerHomePage.clickCrossComponentProjectFolder();
251
253
  });
252
254
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
253
- // Workspace-level menu: an IDP application can only be created at
254
- // workspace root, never inside a process application.
255
- await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
256
- await modelerHomePage.clickIdpApplicationTemplateOption();
255
+ // Workspace level: an IDP application can only be created at
256
+ // workspace root, never inside a process application. The control
257
+ // there is the empty-state CTA or the "Create new" menu item,
258
+ // depending on whether the workspace already holds anything.
259
+ await modelerHomePage.startIdpApplicationCreation();
257
260
  });
258
261
  await _8_9_1.test.step('Creates a classification template', async () => {
259
262
  await modelerHomePage.enterIdpApplicationName(idpApplicationName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1017",
3
+ "version": "0.0.1019",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",