@camunda/e2e-test-suite 0.0.1048 → 0.0.1050

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.
@@ -18,6 +18,11 @@ export declare function loginWithRetry(page: Page, loginPage: LoginPage, testUse
18
18
  username: string;
19
19
  password: string;
20
20
  }, timeout: number, maxRetries?: number): Promise<void>;
21
+ export declare function isSignedOut(page: Page): boolean;
22
+ export declare function withSessionRecovery(page: Page, loginPage: LoginPage, appsPage: AppsPage, modelerBanner: Locator, testUser: {
23
+ username: string;
24
+ password: string;
25
+ }, loginStagger: number, action: () => Promise<void>): Promise<void>;
21
26
  export declare function modelDiagramFromFile(page: Page, modelerHomePage: ModelerHomePage, modelerCreatePage: ModelerCreatePage, processName: string, fileName?: string, nrOfRenamedUserTasks?: number, taskName?: string): Promise<void>;
22
27
  export declare function modelRestConnector(modelerCreatePage: ModelerCreatePage, connectorSettingsPage: ConnectorSettingsPage, processName: string, url: string, auth: string, resultExpression: string, resultVariable?: string, basicAuthCredentials?: {
23
28
  username: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.openAppTabWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
3
+ exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.openAppTabWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.withSessionRecovery = exports.isSignedOut = exports.loginWithRetry = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const ModelerHomePage_1 = require("./ModelerHomePage");
6
6
  const HomePage_1 = require("./HomePage");
@@ -191,6 +191,49 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
191
191
  }
192
192
  }
193
193
  exports.loginWithRetry = loginWithRetry;
194
+ // True once the tab has been thrown out of the app and back to the IdP: the
195
+ // Modeler hand-off can commit `/login-callback?code=...` and then bounce
196
+ // straight back to `modeler.<host>/login` and on to `weblogin.<host>`, leaving
197
+ // an Auth0 form where the app used to be. Distinct from
198
+ // `AppsPage.isModelerAuthTransit`, which covers a hand-off still in flight.
199
+ function isSignedOut(page) {
200
+ try {
201
+ const { hostname, pathname } = new URL(page.url());
202
+ return (hostname.startsWith('weblogin.') ||
203
+ (hostname.startsWith('modeler.') && /^\/login\/?$/.test(pathname)));
204
+ }
205
+ catch {
206
+ // Not a parsable URL yet (e.g. a tab still on about:blank).
207
+ return false;
208
+ }
209
+ }
210
+ exports.isSignedOut = isSignedOut;
211
+ // Runs `action`, and if it fails because the session was dropped mid-flow,
212
+ // signs back in, returns to the Modeler projects list and runs it once more.
213
+ //
214
+ // Without this the caller's own locator retries keep hunting for app controls
215
+ // on the Auth0 form and expire against a page that is not the app at all —
216
+ // reported as a misleading "Failed to click locator ... New project" rather
217
+ // than as the sign-out it actually was. Only a confirmed sign-out is retried;
218
+ // any other failure propagates untouched, so a genuinely broken control still
219
+ // fails the test.
220
+ async function withSessionRecovery(page, loginPage, appsPage, modelerBanner, testUser, loginStagger, action) {
221
+ try {
222
+ await action();
223
+ return;
224
+ }
225
+ catch (error) {
226
+ if (!isSignedOut(page)) {
227
+ throw error;
228
+ }
229
+ console.warn(`Session was dropped mid-flow (tab landed on ${page.url()}); ` +
230
+ 'signing back in and retrying once.');
231
+ }
232
+ await loginWithRetry(page, loginPage, testUser, loginStagger);
233
+ await appsPage.openModelerWithRetry(modelerBanner);
234
+ await action();
235
+ }
236
+ exports.withSessionRecovery = withSessionRecovery;
194
237
  async function modelDiagramFromFile(page, modelerHomePage, modelerCreatePage, processName, fileName = '', nrOfRenamedUserTasks = 0, taskName = '') {
195
238
  await modelerHomePage.clickDiagramTypeDropdown();
196
239
  await modelerHomePage.clickUploadFilesButton();
@@ -47,6 +47,7 @@ declare class ModelerHomePage {
47
47
  clickFormOption(): Promise<void>;
48
48
  enterFormName(name: string): Promise<void>;
49
49
  private renameViaMoreActions;
50
+ private waitForEntityCrumb;
50
51
  clickProjectBreadcrumb(): Promise<void>;
51
52
  clickWorkspaceBreadcrumb(): Promise<void>;
52
53
  enterResourceContainer(name?: string): Promise<void>;
@@ -356,6 +356,11 @@ class ModelerHomePage {
356
356
  // canvas mounts together with the entity crumb, so gate on it rather than
357
357
  // on the header, which is present either way.
358
358
  await this.page.waitForURL(/\/forms\//, { timeout: 60000 });
359
+ // The URL flips before the editor mounts, and the app header re-renders as
360
+ // it does. Renaming in that window finds the kebab but loses the menu it
361
+ // opens, so wait for the editor canvas first. Soft: on a shell that labels
362
+ // the canvas differently the retry in renameViaMoreActions still covers us.
363
+ await this.becameVisible(this.formEditorCanvas, 60000);
359
364
  await this.formNameInput
360
365
  .or(this.formEditorCanvas)
361
366
  .first()
@@ -377,6 +382,12 @@ class ModelerHomePage {
377
382
  // menu from the keyboard -- same control, reached the way a keyboard user
378
383
  // reaches it, and unaffected by what paints over it.
379
384
  //
385
+ // The header keeps re-rendering for a while after the editor mounts (the
386
+ // breadcrumb swap, then the license tags arriving in that ml-auto sibling),
387
+ // and each re-render unmounts the open menu: the nightly trace shows "Rename"
388
+ // resolving, reporting "element is not stable", then "element was detached
389
+ // from the DOM". Reopen the menu and try again rather than racing a single
390
+ // open; the last attempt throws so a genuinely missing menu still fails.
380
391
  // A freshly created entity's file data lands after the editor has already
381
392
  // painted, and the header re-renders when it does -- which unmounts the open
382
393
  // Radix menu mid-click ("element was detached from the DOM, retrying") and
@@ -385,6 +396,7 @@ class ModelerHomePage {
385
396
  // test. Escape first: the trigger toggles, so an attempt that left the menu
386
397
  // open would otherwise have the next Enter close it again.
387
398
  async renameViaMoreActions(name) {
399
+ await this.waitForEntityCrumb();
388
400
  const entityActions = this.moreActionsButton.last();
389
401
  // The header keeps settling after the editor paints: the license tags
390
402
  // mount into the ml-auto sibling and the breadcrumb label swaps from its
@@ -403,17 +415,31 @@ class ModelerHomePage {
403
415
  await this.renameMenuItem.click({ timeout: 15000 });
404
416
  await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
405
417
  }).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
406
- await (0, test_1.expect)(async () => {
407
- await this.page.keyboard.press('Escape');
408
- await entityActions.focus();
409
- await entityActions.press('Enter');
410
- await this.renameMenuItem.click({ timeout: 5000 });
411
- await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
412
- }).toPass({ timeout: 120000, intervals: [2000] });
413
418
  await this.renameInput.fill(name);
414
419
  await this.renameSubmitButton.click();
415
420
  await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
416
421
  }
422
+ // The breadcrumb is built up progressively: on a freshly created resource the
423
+ // container is briefly the deepest crumb and owns the only kebab, and the
424
+ // entity crumb is appended a render later. Reopening the menu is not enough
425
+ // on its own -- an attempt that lands inside that window opens the
426
+ // CONTAINER's menu, so "Rename" there renames the process application
427
+ // instead of the entity, and the entity crumb's arrival re-renders the
428
+ // breadcrumb and unmounts the menu mid-click. Wait for the deepest kebab to
429
+ // stop being the container's. Shells that label the kebab generically
430
+ // ("More actions") never match a container label, so this is a no-op there.
431
+ async waitForEntityCrumb() {
432
+ if (!this.processApplicationName) {
433
+ return;
434
+ }
435
+ const containerActions = `${this.processApplicationName} actions`;
436
+ await test_1.expect
437
+ .poll(async () => (await this.moreActionsButton
438
+ .last()
439
+ .getAttribute('aria-label')
440
+ .catch(() => null)) ?? '', { timeout: 30000 })
441
+ .not.toBe(containerActions);
442
+ }
417
443
  // Kept under its original name because it is what ~10 call sites already
418
444
  // mean: "go back to where my resources live". That is now the process
419
445
  // application, not the workspace root.
@@ -27,7 +27,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
27
27
  await (0, _setup_1.captureScreenshot)(page, testInfo);
28
28
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
29
29
  });
30
- (0, _8_10_1.test)('Most Common Flow User Flow With All Apps', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
30
+ (0, _8_10_1.test)('Most Common Flow User Flow With All Apps', async ({ page, loginPage, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }, testInfo) => {
31
31
  _8_10_1.test.slow();
32
32
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
33
33
  const formName = 'New form' + randomString;
@@ -60,7 +60,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
60
60
  // absent/flaky the folder is missing and clicking it hangs forever.
61
61
  // createCrossComponentProjectFolder clicks into the folder when present
62
62
  // (the normal path) and creates it only when it is genuinely missing.
63
- await modelerHomePage.createCrossComponentProjectFolder();
63
+ await (0, UtilitiesPage_1.withSessionRecovery)(page, loginPage, appsPage, modelerHomePage.modelerPageBanner, testUser, (testInfo.workerIndex + 1) * 1000, () => modelerHomePage.createCrossComponentProjectFolder());
64
64
  await modelerHomePage.clickDiagramTypeDropdown();
65
65
  await modelerHomePage.clickFormOption();
66
66
  await modelerHomePage.enterFormNameWithRetry(formName);
@@ -292,7 +292,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
292
292
  await optimizeReportPage.assertUserTaskInstanceCountWithRetry(2, appTab, countRetries, Math.floor(flowBudget(countRetries * constants_1.TIMEOUT.navigation) / countRetries));
293
293
  });
294
294
  });
295
- (0, _8_10_1.test)('Most Common REST Connector User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }) => {
295
+ (0, _8_10_1.test)('Most Common REST Connector User Flow', async ({ page, loginPage, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }, testInfo) => {
296
296
  _8_10_1.test.slow();
297
297
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
298
298
  const processName = 'REST_Connector_Basic_Auth_Process' + randomString;
@@ -305,7 +305,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
305
305
  await _8_10_1.test.step('Open Cross Component Test Project and create a BPMN diagram template', async () => {
306
306
  // See note in the flow above: ensure the per-user project exists rather
307
307
  // than hanging on a click when the shared setup did not create it.
308
- await modelerHomePage.createCrossComponentProjectFolder();
308
+ await (0, UtilitiesPage_1.withSessionRecovery)(page, loginPage, appsPage, modelerHomePage.modelerPageBanner, testUser, (testInfo.workerIndex + 1) * 1000, () => modelerHomePage.createCrossComponentProjectFolder());
309
309
  await modelerHomePage.clickDiagramTypeDropdown();
310
310
  await modelerHomePage.clickBpmnTemplateOption();
311
311
  });
@@ -120,18 +120,14 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
120
120
  await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
121
121
  timeout: 120000,
122
122
  });
123
- await appsPage.clickCamundaApps();
124
- await appsPage.clickModeler();
125
- await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
126
- timeout: 180000,
127
- });
123
+ await appsPage.openModelerWithRetry(modelerHomePage.modelerPageBanner, 180000);
128
124
  });
129
125
  await _8_10_1.test.step('Create Cross Component Project Folder', async () => {
130
- await homePage.clickSkipCustomization();
131
- await modelerHomePage.createCrossComponentProjectFolder();
132
- await modelerHomePage.clickHomeBreadcrumb();
133
- await (0, test_1.expect)(modelerHomePage.crossComponentProjectFolder).toBeVisible({
134
- timeout: 60000,
126
+ await (0, UtilitiesPage_1.withSessionRecovery)(page, loginPage, appsPage, modelerHomePage.modelerPageBanner, user, (testInfo.workerIndex + 1) * 1000, async () => {
127
+ await homePage.clickSkipCustomization();
128
+ await modelerHomePage.createCrossComponentProjectFolder();
129
+ await modelerHomePage.clickHomeBreadcrumb();
130
+ await (0, test_1.expect)(modelerHomePage.crossComponentProjectFolder).toBeVisible({ timeout: 60000 });
135
131
  });
136
132
  });
137
133
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1048",
3
+ "version": "0.0.1050",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",