@camunda/e2e-test-suite 0.0.944 → 0.0.945

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.
Files changed (38) hide show
  1. package/dist/pages/8.10/AppsPage.d.ts +5 -0
  2. package/dist/pages/8.10/AppsPage.js +152 -0
  3. package/dist/pages/8.10/ModelerHomePage.d.ts +1 -0
  4. package/dist/pages/8.10/ModelerHomePage.js +52 -5
  5. package/dist/pages/8.10/OperateHomePage.js +23 -2
  6. package/dist/pages/8.10/OperateProcessInstancePage.d.ts +17 -3
  7. package/dist/pages/8.10/OperateProcessInstancePage.js +718 -16
  8. package/dist/pages/8.10/OperateProcessesPage.d.ts +1 -0
  9. package/dist/pages/8.10/OperateProcessesPage.js +38 -2
  10. package/dist/pages/8.10/OptimizeHomePage.d.ts +5 -0
  11. package/dist/pages/8.10/OptimizeHomePage.js +138 -0
  12. package/dist/pages/8.10/OptimizeReportPage.d.ts +1 -0
  13. package/dist/pages/8.10/OptimizeReportPage.js +25 -0
  14. package/dist/pages/8.10/TaskPanelPage.d.ts +2 -1
  15. package/dist/pages/8.10/TaskPanelPage.js +37 -9
  16. package/dist/pages/8.10/UtilitiesPage.d.ts +2 -1
  17. package/dist/pages/8.10/UtilitiesPage.js +155 -23
  18. package/dist/pages/SM-8.10/KeycloakAdminPage.js +1 -1
  19. package/dist/pages/SM-8.10/KeycloakLoginPage.d.ts +1 -0
  20. package/dist/pages/SM-8.10/KeycloakLoginPage.js +8 -1
  21. package/dist/pages/SM-8.10/LoginPage.d.ts +1 -1
  22. package/dist/pages/SM-8.10/LoginPage.js +11 -2
  23. package/dist/pages/SM-8.10/NavigationPage.d.ts +1 -0
  24. package/dist/pages/SM-8.10/NavigationPage.js +86 -8
  25. package/dist/pages/SM-8.10/OCIdentityHomePage.js +12 -2
  26. package/dist/pages/SM-8.10/OCIdentityRolesPage.d.ts +3 -0
  27. package/dist/pages/SM-8.10/OCIdentityRolesPage.js +110 -4
  28. package/dist/pages/SM-8.10/OperateHomePage.js +13 -1
  29. package/dist/pages/SM-8.10/OperateProcessesPage.d.ts +1 -0
  30. package/dist/pages/SM-8.10/OperateProcessesPage.js +37 -18
  31. package/dist/pages/SM-8.10/OptimizeReportPage.js +21 -1
  32. package/dist/pages/SM-8.10/TaskDetailsPage.js +7 -0
  33. package/dist/pages/SM-8.10/UtilitiesPage.js +15 -4
  34. package/dist/pages/SM-8.10/optimizeReportUtils.js +30 -4
  35. package/dist/tests/8.10/smoke-tests.spec.js +182 -44
  36. package/dist/utils/constants.d.ts +1 -0
  37. package/dist/utils/constants.js +4 -0
  38. package/package.json +1 -1
@@ -21,6 +21,7 @@ declare class OperateProcessesPage {
21
21
  clickProcessIncidentsCheckbox(): Promise<void>;
22
22
  clickRunningProcessInstancesCheckbox(): Promise<void>;
23
23
  clickFinishedProcessInstancesCheckbox(): Promise<void>;
24
+ private returnToProcessesList;
24
25
  clickProcessInstanceLink(processName: string): Promise<void>;
25
26
  applyMoreFilters(filter: string, value: string): Promise<void>;
26
27
  }
@@ -78,19 +78,55 @@ class OperateProcessesPage {
78
78
  async clickFinishedProcessInstancesCheckbox() {
79
79
  await this.processFinishedInstancesCheckbox.click();
80
80
  }
81
+ // Puts the tab back on the processes list before the next attempt of
82
+ // `clickProcessInstanceLink`. A click that DID navigate to the instance page
83
+ // but whose "Instance History" panel was slow to render leaves the tab on
84
+ // `/processes/<key>`, and the instance row the loop looks for can never
85
+ // appear there: reloading that page spent every remaining attempt waiting
86
+ // 60s for a row that is not on it, which is what reported "Failed to click
87
+ // the link after 5 attempts" while the instance was perfectly fine.
88
+ // Re-opening the list URL also restores the filters the caller applied
89
+ // (they live in the query string), which a `goBack()` would not guarantee.
90
+ //
91
+ // Navigation errors are logged and retried rather than thrown: a reload that
92
+ // races the click's still-in-flight navigation fails with
93
+ // `net::ERR_ABORTED; maybe frame was detached?` and a DNS blip with
94
+ // `net::ERR_NAME_NOT_RESOLVED`. Both used to escape this loop's catch block
95
+ // and fail the test outright instead of being retried like every other
96
+ // recovery step here.
97
+ async returnToProcessesList(listUrl) {
98
+ try {
99
+ if (this.page.url() === listUrl) {
100
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
101
+ return;
102
+ }
103
+ await this.page.goto(listUrl, { waitUntil: 'domcontentloaded' });
104
+ }
105
+ catch (error) {
106
+ console.warn(`Could not return to the processes list: ${error}`);
107
+ }
108
+ }
81
109
  async clickProcessInstanceLink(processName) {
82
110
  // Increased from 3 to 5 retries: process-instance indexing in Operate can take
83
111
  // longer than 3 × (60 s visible + 10 s reload) on busy SaaS environments,
84
112
  // causing intermittent failures in the webhook connector nightly tests.
85
113
  const maxRetries = 5;
114
+ // Captured before the first click so a retry can re-open the list with the
115
+ // caller's filters instead of reloading whatever page the click landed on.
116
+ const listUrl = this.page.url();
86
117
  for (let retries = 0; retries < maxRetries; retries++) {
87
118
  try {
88
119
  await (0, test_1.expect)(this.processInstanceLink(processName)).toBeVisible({
89
120
  timeout: 60000,
90
121
  });
91
122
  await this.processInstanceLink(processName).click({ timeout: 60000 });
123
+ // The instance page is rendered from the same Zeebe -> Operate import
124
+ // this list was filtered on, so on a busy cluster the panel regularly
125
+ // needs longer than 10s to paint. That short budget turned a slow
126
+ // render into a full re-attempt — and every re-attempt then looked for
127
+ // the row on the instance page it had just opened.
92
128
  await (0, test_1.expect)(this.page.getByText('Instance History').first()).toBeVisible({
93
- timeout: 10000,
129
+ timeout: 60000,
94
130
  });
95
131
  if (await this.whatsNewPopUp.isVisible()) {
96
132
  await this.gotItButton.click();
@@ -99,7 +135,7 @@ class OperateProcessesPage {
99
135
  }
100
136
  catch (error) {
101
137
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
102
- await this.page.reload();
138
+ await this.returnToProcessesList(listUrl);
103
139
  await (0, sleep_1.sleep)(10000);
104
140
  }
105
141
  }
@@ -6,7 +6,12 @@ declare class OptimizeHomePage {
6
6
  readonly modalCloseButton: Locator;
7
7
  readonly optimizeBanner: Locator;
8
8
  readonly noPermissionsMessage: Locator;
9
+ readonly filterTableInput: Locator;
9
10
  constructor(page: Page);
11
+ private waitForOptimizeToRender;
12
+ private filterTableToProcess;
13
+ closeModalIfVisible(): Promise<void>;
14
+ assertProcessImportedWithRetry(processName: string, timeout: number, maxRetries?: number): Promise<void>;
10
15
  clickCollectionsLink(): Promise<void>;
11
16
  clickDashboardLink(): Promise<void>;
12
17
  }
@@ -3,6 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OptimizeHomePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
+ // Optimize's SSO hand-off can fail on the way into the app and leave the tab on
7
+ // `/login?error`, which is not an Optimize page at all: nothing on it ever
8
+ // renders a process definition, and `page.reload()` only re-requests the very
9
+ // same static error page. A polling loop that reloads is therefore stuck there
10
+ // for its whole budget -- that is what reported "Assertion of process <name> in
11
+ // Optimize failed after 1200000ms of polling
12
+ // (url=<region>.optimize.ultrawombat.com/login?error)". Re-entering the app
13
+ // root instead restarts the hand-off, so the poll can get back onto the app.
14
+ const OPTIMIZE_LOGIN_PATH = /^\/login\/?$/;
15
+ function isOptimizeLoginPage(rawUrl) {
16
+ try {
17
+ return OPTIMIZE_LOGIN_PATH.test(new URL(rawUrl).pathname);
18
+ }
19
+ catch {
20
+ // Not a parsable URL yet (e.g. a tab still on about:blank).
21
+ return false;
22
+ }
23
+ }
6
24
  class OptimizeHomePage {
7
25
  page;
8
26
  collectionsLink;
@@ -10,8 +28,10 @@ class OptimizeHomePage {
10
28
  modalCloseButton;
11
29
  optimizeBanner;
12
30
  noPermissionsMessage;
31
+ filterTableInput;
13
32
  constructor(page) {
14
33
  this.page = page;
34
+ this.filterTableInput = page.getByPlaceholder('Filter table');
15
35
  this.collectionsLink = page
16
36
  .getByRole('banner')
17
37
  .getByRole('link', { name: 'Collections' });
@@ -22,6 +42,124 @@ class OptimizeHomePage {
22
42
  });
23
43
  this.noPermissionsMessage = page.getByText('default backend - 404');
24
44
  }
45
+ // The Optimize process overview renders a bounded slice of the definition
46
+ // table, so on an org that already holds the definitions every sibling spec
47
+ // deployed that night a freshly imported one can sit outside the rendered
48
+ // rows entirely: `getByRole('link', {name})` then never becomes visible no
49
+ // matter how long the loop below waits, which is exactly what reported
50
+ // "Assertion of process <name> in Optimize failed after 1200000ms of
51
+ // polling" from the app root — a full 20 minutes of reload-polling for a
52
+ // definition that had long since been imported. Narrowing the table to the
53
+ // name being waited for is what `OptimizeDashboardPage.processLinkAssertion`
54
+ // already does before this identical link assertion.
55
+ // Best effort by design: when the input is not rendered (the app is still
56
+ // coming up, or a round landed on a page without the table) the round falls
57
+ // through and asserts against the unfiltered table exactly as before.
58
+ // Every probe a polling round makes runs immediately after that round's
59
+ // `reload()` + `waitForLoadState('domcontentloaded')`, which returns while
60
+ // the Optimize SPA is still mounting and the document is effectively empty.
61
+ // `isVisible()` answers instantly, so both probes below — the onboarding
62
+ // modal and the table filter — answered "not there" on essentially every
63
+ // round: the modal was never dismissed and the table was never narrowed, and
64
+ // the round then spent its whole slice asserting against a list that had not
65
+ // rendered. Waiting for the app shell first is what makes the rest of the
66
+ // round mean anything. It costs nothing once Optimize is up (the wait
67
+ // resolves in milliseconds) and is bounded, so a round that lands on a page
68
+ // which never renders Optimize still falls through and reloads as before.
69
+ async waitForOptimizeToRender(timeout) {
70
+ await this.filterTableInput
71
+ .or(this.optimizeBanner)
72
+ .first()
73
+ .waitFor({ state: 'visible', timeout })
74
+ .catch(() => { });
75
+ }
76
+ async filterTableToProcess(processName) {
77
+ const hasFilter = await this.filterTableInput
78
+ .isVisible()
79
+ .catch(() => false);
80
+ if (!hasFilter) {
81
+ return;
82
+ }
83
+ try {
84
+ await this.filterTableInput.fill(processName, { timeout: 15000 });
85
+ await this.filterTableInput.press('Enter');
86
+ }
87
+ catch (error) {
88
+ console.warn(`Could not filter the Optimize process table: ${error}`);
89
+ }
90
+ }
91
+ async closeModalIfVisible() {
92
+ if (await this.modalCloseButton.isVisible()) {
93
+ await this.modalCloseButton.click({ timeout: 60000 });
94
+ }
95
+ }
96
+ // Optimize pulls process definitions on its own import cycle, so the only
97
+ // lever is patience — but the patience has to be spent re-asking, not idling.
98
+ // This list does not live-update: a definition that lands mid-wait is only
99
+ // rendered by the next reload, so a long `toBeVisible` per round just sits on
100
+ // a page that already answered. `timeout` x `maxRetries` is kept as the total
101
+ // budget and spent in short slices that each reload, which turns 10 re-asks
102
+ // over 20 minutes into one every ~20s. That is the same "re-ask instead of
103
+ // idling" change already made for the Operate import waits, and it is what
104
+ // ran out as "Assertion of process <name> in Optimize failed after 10
105
+ // attempts" whenever the import lagged behind the exporter.
106
+ // Each round also dismisses the onboarding modal Optimize shows on this
107
+ // page — it is re-rendered by every reload and covers the process list, so
108
+ // without closing it the remaining rounds all assert against a modal.
109
+ async assertProcessImportedWithRetry(processName, timeout, maxRetries = 8) {
110
+ const processLink = this.page.getByRole('link', { name: processName });
111
+ const deadline = Date.now() + timeout * maxRetries;
112
+ // Short enough that a definition imported mid-wait is picked up by the next
113
+ // reload within seconds, long enough to keep the reload rate modest.
114
+ const checkSlice = 20000;
115
+ let lastError;
116
+ while (Date.now() < deadline) {
117
+ const remaining = deadline - Date.now();
118
+ if (remaining <= 0) {
119
+ break;
120
+ }
121
+ try {
122
+ await this.waitForOptimizeToRender(Math.min(checkSlice, remaining));
123
+ await this.closeModalIfVisible();
124
+ await this.filterTableToProcess(processName);
125
+ await (0, test_1.expect)(processLink).toBeVisible({
126
+ timeout: Math.min(checkSlice, remaining),
127
+ });
128
+ return;
129
+ }
130
+ catch (error) {
131
+ lastError = error;
132
+ }
133
+ if (Date.now() >= deadline) {
134
+ break;
135
+ }
136
+ // A reload that races an in-flight navigation fails with
137
+ // `net::ERR_ABORTED; maybe frame was detached?`, a DNS blip with
138
+ // `net::ERR_NAME_NOT_RESOLVED`. Both used to escape this polling loop
139
+ // and fail the test outright instead of being retried on the next round;
140
+ // an import that genuinely never lands still fails below.
141
+ try {
142
+ if (isOptimizeLoginPage(this.page.url())) {
143
+ // Off the app entirely (see the note above the class): re-enter the
144
+ // app root so the SSO hand-off is retried. Reloading the login page
145
+ // can only ever render the login page again.
146
+ await this.page.goto(new URL('/', this.page.url()).toString(), {
147
+ waitUntil: 'domcontentloaded',
148
+ });
149
+ }
150
+ else {
151
+ await this.page.reload();
152
+ await this.page.waitForLoadState('domcontentloaded');
153
+ }
154
+ }
155
+ catch (error) {
156
+ console.warn(`Could not reload the Optimize process list: ${error}`);
157
+ }
158
+ }
159
+ throw new Error(`Assertion of process ${processName} in Optimize failed after ` +
160
+ `${timeout * maxRetries}ms of polling (url=${this.page.url()}): ` +
161
+ `${lastError}`);
162
+ }
25
163
  async clickCollectionsLink() {
26
164
  if (await this.modalCloseButton.isVisible({ timeout: 60000 })) {
27
165
  await this.modalCloseButton.click();
@@ -40,6 +40,7 @@ declare class OptimizeReportPage {
40
40
  clearReportName(): Promise<void>;
41
41
  fillReportName(name: string): Promise<void>;
42
42
  clickSaveButton(): Promise<void>;
43
+ assertUserTaskInstanceCountWithRetry(expectedCount: number, tab: Page, maxRetries?: number, timeout?: number): Promise<void>;
43
44
  waitUntilLocatorIsVisible(locator: Locator, tab: Page): Promise<void>;
44
45
  waitUntilProcessIsVisible(locator: Locator, tab: Page): Promise<void>;
45
46
  clickBlankReportButton(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OptimizeReportPage = void 0;
4
+ const test_1 = require("@playwright/test");
4
5
  class OptimizeReportPage {
5
6
  page;
6
7
  processSelectionButton;
@@ -117,6 +118,30 @@ class OptimizeReportPage {
117
118
  async clickSaveButton() {
118
119
  await this.saveButton.click();
119
120
  }
121
+ // Optimize imports user-task *instance* data on a later cycle than the
122
+ // process definition, so a report saved right after the definition lands can
123
+ // render with a table that is still empty or only partly filled. Poll the
124
+ // expected row count across reloads instead of probing visibility once and
125
+ // then asserting the count against whatever the first render contained --
126
+ // that ordering reports `toHaveCount` received 0 while the import is simply
127
+ // still in flight.
128
+ async assertUserTaskInstanceCountWithRetry(expectedCount, tab, maxRetries = 4, timeout = 60000) {
129
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
130
+ try {
131
+ await (0, test_1.expect)(this.oneUserTaskInstance).toHaveCount(expectedCount, {
132
+ timeout: timeout,
133
+ });
134
+ return;
135
+ }
136
+ catch (error) {
137
+ if (attempt === maxRetries - 1) {
138
+ throw error;
139
+ }
140
+ await tab.reload();
141
+ await tab.waitForLoadState('domcontentloaded');
142
+ }
143
+ }
144
+ }
120
145
  async waitUntilLocatorIsVisible(locator, tab) {
121
146
  let elapsedTime = 0;
122
147
  const maxWaitTimeSeconds = 120000;
@@ -10,7 +10,8 @@ declare class TaskPanelPage {
10
10
  readonly expandButton: Locator;
11
11
  readonly processesInfoText: Locator;
12
12
  constructor(page: Page);
13
- openTask(name: string): Promise<void>;
13
+ taskInList(name: string): Locator;
14
+ openTask(name: string, totalTimeout?: number): Promise<void>;
14
15
  filterBy(option: 'All open' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
15
16
  taskCount(name: string): Promise<number>;
16
17
  clickTasksTab(): Promise<void>;
@@ -30,24 +30,52 @@ class TaskPanelPage {
30
30
  });
31
31
  this.processesInfoText = page.getByText('Browse and run processes published by your organization');
32
32
  }
33
- async openTask(name) {
34
- let attempts = 0;
35
- const maxAttempts = 3;
36
- while (attempts < maxAttempts) {
33
+ // Tasklist's importer can lag well behind Zeebe under heavy concurrent load
34
+ // from the sibling chromium-v1/chromium-v2 projects sharing one cluster --
35
+ // trace evidence from failing runs showed a smoke-test user task still
36
+ // absent from "Available tasks" more than 5 minutes after the instance was
37
+ // already ACTIVE in Operate. The budget is therefore generous, but it is a
38
+ // *total* deadline rather than a fixed attempt count: an attempt count
39
+ // multiplies with any outer retry loop (`completeTaskWithRetry` re-enters
40
+ // this method 3 times), which gave one step a worst case larger than the
41
+ // whole test timeout, so a lagging importer killed the test on the harness
42
+ // clock instead of failing here with a usable error.
43
+ // The row for a named task inside the task list. Exposed so callers that
44
+ // assert on the details panel can re-select the row when a click did not
45
+ // take effect, rather than re-deriving the locator.
46
+ taskInList(name) {
47
+ return this.availableTasks.getByText(name, { exact: true }).first();
48
+ }
49
+ async openTask(name, totalTimeout = 1200000) {
50
+ const deadline = Date.now() + totalTimeout;
51
+ let lastError;
52
+ while (Date.now() < deadline) {
37
53
  try {
38
- const task = this.availableTasks.getByText(name, { exact: true }).first();
39
- await task.waitFor({ state: 'visible', timeout: 120000 });
54
+ const task = this.taskInList(name);
55
+ await task.waitFor({
56
+ state: 'visible',
57
+ // Clamped away from 0: Playwright reads `timeout: 0` as "no timeout".
58
+ timeout: Math.max(1000, Math.min(120000, deadline - Date.now())),
59
+ });
40
60
  await task.click({ timeout: 120000 });
41
61
  return;
42
62
  }
43
63
  catch (error) {
44
- attempts++;
45
- if (attempts >= maxAttempts) {
46
- throw error;
64
+ lastError = error;
65
+ if (Date.now() >= deadline) {
66
+ break;
47
67
  }
48
68
  await this.page.reload();
69
+ // Tasklist polls continuously for new tasks, so `networkidle` never
70
+ // fires and the wait times out at 60s -- burning the retry budget
71
+ // without ever re-attempting the task click. `domcontentloaded`
72
+ // resolves once the reloaded HTML has parsed, which is the actual
73
+ // signal we need before trying to find the task again.
74
+ await this.page.waitForLoadState('domcontentloaded', { timeout: 60000 });
49
75
  }
50
76
  }
77
+ throw new Error(`Task ${name} did not open from "Available tasks" within ` +
78
+ `${totalTimeout}ms: ${lastError}`);
51
79
  }
52
80
  async filterBy(option) {
53
81
  try {
@@ -24,8 +24,9 @@ export declare function modelRestConnector(modelerCreatePage: ModelerCreatePage,
24
24
  password: string;
25
25
  }): Promise<void>;
26
26
  export declare function assertLocatorVisibleWithRetry(page: Page | OperateProcessInstancePage | ModelerHomePage, locator: Locator, text: string, timeout?: number, notVisible?: boolean, maxRetries?: number, clickLocator?: Locator): Promise<void>;
27
+ export declare function openAppTabWithRetry(page: Page, link: Locator, timeout?: number, maxRetries?: number, clickTimeout?: number): Promise<Page>;
27
28
  export declare function assertPageTextWithRetry(page: Page, text: string, notVisible?: boolean, timeout?: number, maxRetries?: number): Promise<void>;
28
- export declare function completeTaskWithRetry(taskPanelPage: TaskPanelPage, taskDetailsPage: TaskDetailsPage, taskName: string, taskPriority: string, maxRetries?: number): Promise<void>;
29
+ export declare function completeTaskWithRetry(taskPanelPage: TaskPanelPage, taskDetailsPage: TaskDetailsPage, taskName: string, taskPriority: string, maxRetries?: number, totalTimeout?: number): Promise<void>;
29
30
  export declare function runProcess(clusterName: string, page: Page, modelerHomePage: ModelerHomePage, modelerCreatePage: ModelerCreatePage, processName: string, processId?: string, uploadFromFile?: string): Promise<void>;
30
31
  export declare function runMultipleProcesses(clusterName: string, page: Page, modelerHomePage: ModelerHomePage, modelerCreatePage: ModelerCreatePage, options?: {
31
32
  numberOfProcesses?: number;
@@ -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.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.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");
@@ -8,6 +8,85 @@ 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 clearWebStorage(page) {
12
+ await page
13
+ .evaluate(() => {
14
+ try {
15
+ localStorage.clear();
16
+ }
17
+ catch (_) {
18
+ // ignore — storage may be unavailable in some contexts
19
+ }
20
+ try {
21
+ sessionStorage.clear();
22
+ }
23
+ catch (_) {
24
+ // ignore — storage may be unavailable in some contexts
25
+ }
26
+ })
27
+ .catch(() => { });
28
+ }
29
+ // Waits for the auth0 login form, re-asking Console for the auth redirect
30
+ // between checks instead of idling on whatever page the browser landed on.
31
+ //
32
+ // A single passive `expect.poll` cannot recover the two ways this hand-off
33
+ // fails: Console swallowing the redirect (the tab stays on a page that will
34
+ // never render a login form), or a session that outlived the cookie wipe (the
35
+ // authenticated shell renders instead). In both cases nothing changes on its
36
+ // own, so the poll burns its whole budget and the login attempt is lost --
37
+ // three of those in a row is exactly the reported "Login failed after 3
38
+ // attempts: ... Timeout 60000ms exceeded while waiting on the predicate".
39
+ // Re-navigating to '/' re-triggers the redirect, and dropping cookies +
40
+ // storage first evicts a surviving session, so the hand-off self-heals inside
41
+ // the attempt rather than costing one.
42
+ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTimeout = 20000) {
43
+ const deadline = Date.now() + totalTimeout;
44
+ const homePage = new HomePage_1.HomePage(page);
45
+ let lastState = 'no login form';
46
+ while (Date.now() < deadline) {
47
+ const round = Math.min(roundTimeout, deadline - Date.now());
48
+ try {
49
+ // Confirm we reached the login page before filling credentials.
50
+ // Without this check the locator for 'Email address' can match a hidden
51
+ // invite-user input on the Console page when the logout redirect has not
52
+ // yet completed, causing a 200 s wait before failing.
53
+ await test_1.expect
54
+ .poll(async () => {
55
+ const [loginMessageVisible, passwordHeadingVisible, usernameVisible, passwordVisible,] = await Promise.all([
56
+ loginPage.loginMessage.isVisible().catch(() => false),
57
+ loginPage.passwordHeading.isVisible().catch(() => false),
58
+ loginPage.usernameInput.locator.isVisible().catch(() => false),
59
+ loginPage.passwordInput.isVisible().catch(() => false),
60
+ ]);
61
+ return (loginMessageVisible ||
62
+ passwordHeadingVisible ||
63
+ usernameVisible ||
64
+ passwordVisible);
65
+ }, { timeout: round })
66
+ .toBe(true);
67
+ return;
68
+ }
69
+ catch {
70
+ // No login form this round — establish why, then re-ask for the redirect.
71
+ lastState = `no login form on ${page.url()}`;
72
+ }
73
+ const authenticated = await homePage.camundaComponentsButton
74
+ .isVisible()
75
+ .catch(() => false);
76
+ if (authenticated) {
77
+ lastState = 'an authenticated Console shell — no auth0 hand-off happened';
78
+ // The session survived the cookie wipe done before this wait; drop it
79
+ // again so the navigation below has to go through auth0.
80
+ await page.context().clearCookies();
81
+ await clearWebStorage(page);
82
+ }
83
+ if (Date.now() >= deadline) {
84
+ break;
85
+ }
86
+ await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
87
+ }
88
+ throw new Error(`Auth0 login form did not render within ${totalTimeout}ms: ${lastState}`);
89
+ }
11
90
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3) {
12
91
  let lastError;
13
92
  for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -59,24 +138,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3
59
138
  .waitForLoadState('networkidle', { timeout: 30000 })
60
139
  .catch(() => { });
61
140
  await (0, sleep_1.sleep)(timeout);
62
- // Confirm we reached the login page before filling credentials.
63
- // Without this check the locator for 'Email address' can match a hidden
64
- // invite-user input on the Console page when the logout redirect has not
65
- // yet completed, causing a 200 s wait before failing.
66
- await test_1.expect
67
- .poll(async () => {
68
- const [loginMessageVisible, passwordHeadingVisible, usernameVisible, passwordVisible,] = await Promise.all([
69
- loginPage.loginMessage.isVisible().catch(() => false),
70
- loginPage.passwordHeading.isVisible().catch(() => false),
71
- loginPage.usernameInput.locator.isVisible().catch(() => false),
72
- loginPage.passwordInput.isVisible().catch(() => false),
73
- ]);
74
- return (loginMessageVisible ||
75
- passwordHeadingVisible ||
76
- usernameVisible ||
77
- passwordVisible);
78
- }, { timeout: 60000 })
79
- .toBe(true);
141
+ await waitForLoginForm(page, loginPage);
80
142
  await loginPage.loginWithTestUser(testUser);
81
143
  return;
82
144
  }
@@ -125,8 +187,14 @@ async function modelRestConnector(modelerCreatePage, connectorSettingsPage, proc
125
187
  username: '',
126
188
  password: '',
127
189
  }) {
190
+ // Same gate as `modelJobWorkerDiagram` (120s) and
191
+ // `modelCamundaUserTaskDiagram` (180s): the properties panel renders once the
192
+ // new BPMN template has finished opening. 15s was the outlier here, and a
193
+ // slow template load failed with
194
+ // `locator('[data-group-id="group-general"]') ... element(s) not found`
195
+ // before any modelling had started.
128
196
  await (0, test_1.expect)(modelerCreatePage.generalPanel).toBeVisible({
129
- timeout: 15000,
197
+ timeout: 120000,
130
198
  });
131
199
  await modelerCreatePage.enterDiagramName(processName);
132
200
  await (0, sleep_1.sleep)(10000);
@@ -209,7 +277,16 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 6000
209
277
  catch (error) {
210
278
  if (attempt < maxRetries - 1) {
211
279
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
212
- await page.reload();
280
+ // The reload is only the recovery step: when it races an in-flight
281
+ // navigation (`net::ERR_ABORTED; maybe frame was detached?`) or hits a
282
+ // DNS blip (`net::ERR_NAME_NOT_RESOLVED`) it used to escape this catch
283
+ // and fail the test instead of letting the next attempt re-assert.
284
+ try {
285
+ await page.reload();
286
+ }
287
+ catch (reloadError) {
288
+ console.warn(`Could not reload while asserting ${text}: ${reloadError}`);
289
+ }
213
290
  await (0, sleep_1.sleep)(10000);
214
291
  }
215
292
  else {
@@ -219,6 +296,53 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 6000
219
296
  }
220
297
  }
221
298
  exports.assertLocatorVisibleWithRetry = assertLocatorVisibleWithRetry;
299
+ // Clicks a link/button that opens another Camunda app in a new tab and returns
300
+ // that tab, retrying the click when no tab appears.
301
+ //
302
+ // The wait is registered BEFORE each click because Playwright only reports
303
+ // events raised after it is called and the tab opens while the click is still
304
+ // resolving. That alone is not enough though: the "View process instance"
305
+ // control lives in Modeler's post-deployment notification, and a click that
306
+ // lands while that notification is being torn down resolves without ever
307
+ // running its handler — no tab is opened, and the wait then sits out its full
308
+ // budget on a click that silently did nothing ("page.waitForEvent: Timeout
309
+ // 60000ms exceeded while waiting for event \"popup\""). Re-clicking recovers
310
+ // that, so long as the control is still on screen; when it is gone the loop
311
+ // stops immediately rather than burning another click timeout on it.
312
+ async function openAppTabWithRetry(page, link, timeout = 60000, maxRetries = 3, clickTimeout = 180000) {
313
+ const context = page.context();
314
+ const knownPages = new Set(context.pages());
315
+ let lastError;
316
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
317
+ if (attempt > 1 && !(await link.isVisible().catch(() => false))) {
318
+ break;
319
+ }
320
+ const appTabPromise = page.waitForEvent('popup', { timeout });
321
+ let clickError;
322
+ try {
323
+ await link.click({ timeout: clickTimeout });
324
+ }
325
+ catch (error) {
326
+ clickError = error;
327
+ }
328
+ try {
329
+ // Always awaited, even when the click threw, so the wait can never
330
+ // reject unobserved.
331
+ return await appTabPromise;
332
+ }
333
+ catch (error) {
334
+ lastError = clickError ?? error;
335
+ // The click may still have opened the tab without the wait observing it.
336
+ const opened = context.pages().find((tab) => !knownPages.has(tab));
337
+ if (opened !== undefined) {
338
+ return opened;
339
+ }
340
+ console.warn(`Attempt ${attempt} to open the app tab failed: ${error}`);
341
+ }
342
+ }
343
+ throw new Error(`Failed to open the app tab after ${maxRetries} attempts: ${lastError}`);
344
+ }
345
+ exports.openAppTabWithRetry = openAppTabWithRetry;
222
346
  async function assertPageTextWithRetry(page, text, notVisible = false, timeout = 60000, maxRetries = 10) {
223
347
  for (let attempt = 0; attempt < maxRetries; attempt++) {
224
348
  try {
@@ -248,14 +372,22 @@ async function assertPageTextWithRetry(page, text, notVisible = false, timeout =
248
372
  }
249
373
  }
250
374
  exports.assertPageTextWithRetry = assertPageTextWithRetry;
251
- async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, taskPriority, maxRetries = 3) {
375
+ // `totalTimeout` bounds the whole "complete this task" hop, including the wait
376
+ // for Tasklist to import the task. It is shared across the attempts below
377
+ // rather than granted to each of them: `openTask` polls for minutes on its
378
+ // own, so a per-attempt budget multiplied out to a worst case longer than the
379
+ // test timeout and the harness killed the test mid-wait, with no assertion
380
+ // error to diagnose. One deadline keeps the retries (which recover a lost
381
+ // click or a stale assign button) without inflating the ceiling.
382
+ async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, taskPriority, maxRetries = 3, totalTimeout = 1200000) {
383
+ const deadline = Date.now() + totalTimeout;
252
384
  const openTask = taskPanelPage.availableTasks
253
385
  .getByText(taskName, { exact: true })
254
386
  .first();
255
387
  for (let attempt = 0; attempt < maxRetries; attempt++) {
256
388
  let completionRequested = false;
257
389
  try {
258
- await taskPanelPage.openTask(taskName);
390
+ await taskPanelPage.openTask(taskName, Math.max(30000, deadline - Date.now()));
259
391
  await (0, test_1.expect)(taskDetailsPage.detailsInfo.getByText(taskName, { exact: true })).toBeVisible();
260
392
  if (!(await taskDetailsPage.assignedToMeText.isVisible())) {
261
393
  await taskDetailsPage.clickAssignToMeButton();
@@ -36,7 +36,7 @@ class KeycloakAdminPage {
36
36
  await (0, test_1.expect)(realmResult).toBeVisible({ timeout: 30000 });
37
37
  await realmResult.click();
38
38
  const mainLocator = this.page.locator('#kc-main-content-page-container');
39
- await (0, test_1.expect)(mainLocator).toBeVisible();
39
+ await (0, test_1.expect)(mainLocator).toBeVisible({ timeout: 60000 });
40
40
  }
41
41
  async clickUsersTab() {
42
42
  await this.page.click('a[id="nav-item-users"]');
@@ -6,6 +6,7 @@ declare class KeycloakLoginPage {
6
6
  toBeVisible(): Promise<void>;
7
7
  fillUsername(username: string): Promise<void>;
8
8
  fillPassword(password: string): Promise<void>;
9
+ clearSession(): Promise<void>;
9
10
  clickLogin(): Promise<void>;
10
11
  }
11
12
  export { KeycloakLoginPage };
@@ -18,8 +18,15 @@ class KeycloakLoginPage {
18
18
  async fillPassword(password) {
19
19
  await this.page.fill('#password', password);
20
20
  }
21
+ async clearSession() {
22
+ await this.page.context().clearCookies();
23
+ }
21
24
  async clickLogin() {
22
- await this.page.click('button[type="submit"]');
25
+ // Submitting the Keycloak form starts a redirect chain that page.click
26
+ // implicitly waits on ("waiting for scheduled navigations to finish"), so
27
+ // the action inherits the 10s actionTimeout to cover a navigation. Give it
28
+ // the navigation budget the rest of the suite uses instead.
29
+ await this.page.click('button[type="submit"]', { timeout: 60000 });
23
30
  }
24
31
  }
25
32
  exports.KeycloakLoginPage = KeycloakLoginPage;