@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,7 +21,12 @@ declare class AppsPage {
21
21
  constructor(page: Page);
22
22
  clickCluster(component: Locator, name: string): Promise<void>;
23
23
  clickModeler(): Promise<void>;
24
+ private isModelerAuthTransit;
25
+ openModelerWithRetry(banner: Locator, timeout?: number, maxRetries?: number): Promise<void>;
26
+ openOperateWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
24
27
  clickTasklist(clusterName: string): Promise<void>;
28
+ openTasklistWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
29
+ openOptimizeWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
25
30
  clickAdmin(clusterName: string): Promise<void>;
26
31
  private doClickClusterInOperate;
27
32
  private doClickClusterInTasklist;
@@ -113,6 +113,99 @@ class AppsPage {
113
113
  }
114
114
  throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
115
115
  }
116
+ // True while the tab is still inside the Modeler's OAuth hand-off rather
117
+ // than on a Modeler page: `modeler.<host>/login` bounces through the IdP and
118
+ // commits `/login-callback?code=...` before the app itself renders.
119
+ isModelerAuthTransit() {
120
+ try {
121
+ const { hostname, pathname } = new URL(this.page.url());
122
+ return (hostname.startsWith('modeler.') &&
123
+ /^\/login(-callback)?\/?$/.test(pathname));
124
+ }
125
+ catch {
126
+ // Not a parsable URL yet (e.g. a tab still on about:blank).
127
+ return false;
128
+ }
129
+ }
130
+ // The Modeler OAuth handshake occasionally stalls on
131
+ // `modeler.<host>/login-callback?code=...`: the tab stays on the callback
132
+ // URL and the Modeler banner never renders. Reloading cannot recover it —
133
+ // the `code` is single use — so re-enter through the Camunda apps menu,
134
+ // which starts a fresh handshake.
135
+ async openModelerWithRetry(banner, timeout = 60000, maxRetries = 3) {
136
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
137
+ await this.clickCamundaApps();
138
+ await this.clickModeler();
139
+ try {
140
+ await (0, test_1.expect)(banner).toBeVisible({ timeout });
141
+ return;
142
+ }
143
+ catch (error) {
144
+ // Never tear down a hand-off that is still in flight. `modeler
145
+ // .<host>/login` runs an invisible reCAPTCHA before it even reaches
146
+ // the IdP, so on SaaS the tab routinely leaves `/login` for
147
+ // `/login-callback?code=...` only just AFTER this window lapses.
148
+ // Re-entering through the apps menu at that moment aborts an exchange
149
+ // that was about to finish and starts an identical one, which is then
150
+ // cut at the very same mark — three attempts, three restarts, and the
151
+ // banner never renders even though nothing was actually wrong. The
152
+ // trace of such a run shows `/login` committing `/login-callback`
153
+ // 60.3s into a 60s wait, three times over. Give a hand-off that is
154
+ // still in transit one more window to settle before treating the
155
+ // attempt as failed; a tab that is genuinely wedged spends it and
156
+ // falls through to the re-entry below exactly as before.
157
+ if (this.isModelerAuthTransit()) {
158
+ try {
159
+ await (0, test_1.expect)(banner).toBeVisible({ timeout });
160
+ return;
161
+ }
162
+ catch (settleError) {
163
+ console.warn(`Modeler hand-off did not settle within the extra window: ` +
164
+ `${settleError}`);
165
+ }
166
+ }
167
+ if (attempt === maxRetries - 1) {
168
+ throw error;
169
+ }
170
+ console.warn(`Modeler banner not visible on attempt ${attempt + 1}; ` +
171
+ 're-entering through the apps menu.');
172
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
173
+ }
174
+ }
175
+ }
176
+ // The hand-off into Operate can resolve without the app ever coming up:
177
+ // `clickOperate` returns as soon as its click resolves, so a click that
178
+ // landed while the apps menu was being torn down — or a cluster hand-off
179
+ // that stalls on the proxy — leaves the tab on whatever app it started from.
180
+ // The caller then looks for Operate's nav on a page that is not Operate, and
181
+ // because Tasklist ships a "Processes" link of its own that lookup can even
182
+ // SUCCEED: the click goes to Tasklist's Processes tab, and the wait for
183
+ // Operate's own Processes view (`expanded-panel` > "Process") afterwards can
184
+ // never resolve. That is what reported "Failed to click locator
185
+ // getByRole('link', { name: 'Processes', exact: true })" on one hand and
186
+ // "Failed to assert visibility locator getByTestId('expanded-panel')
187
+ // .getByText('Process', { exact: true })" on the other. Gate on Operate's
188
+ // banner and re-enter through the apps menu when it never renders — the same
189
+ // recovery `openModelerWithRetry` uses for the identical Modeler
190
+ // `login-callback` stall.
191
+ async openOperateWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3) {
192
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
193
+ await this.clickCamundaApps();
194
+ await this.clickOperate(clusterName);
195
+ try {
196
+ await (0, test_1.expect)(banner).toBeVisible({ timeout });
197
+ return;
198
+ }
199
+ catch (error) {
200
+ if (attempt === maxRetries - 1) {
201
+ throw error;
202
+ }
203
+ console.warn(`Operate banner not visible on attempt ${attempt + 1}; ` +
204
+ 're-entering through the apps menu.');
205
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
206
+ }
207
+ }
208
+ }
116
209
  async clickTasklist(clusterName) {
117
210
  const maxRetries = 3;
118
211
  for (let retries = 0; retries < maxRetries; retries++) {
@@ -157,6 +250,65 @@ class AppsPage {
157
250
  }
158
251
  throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
159
252
  }
253
+ // Same hand-off failure `openOperateWithRetry` above exists to catch, on the
254
+ // other app: `clickTasklist` returns as soon as its click resolves, so a
255
+ // click that landed while the apps menu was being torn down — or a cluster
256
+ // hand-off that stalls on the proxy — leaves the tab on the app it started
257
+ // from. The caller then polls THAT page for a task row which can never be
258
+ // there, and the reload each polling round performs cannot recover a
259
+ // hand-off that never happened — it re-renders the same wrong app. That is
260
+ // what reported `Task <name> did not open from "Available tasks"` on a run
261
+ // whose instance was already confirmed ACTIVE in Operate one step earlier.
262
+ // Gate on Tasklist's own banner and re-enter through the apps menu when it
263
+ // never renders.
264
+ async openTasklistWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3) {
265
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
266
+ await this.clickCamundaApps();
267
+ await this.clickTasklist(clusterName);
268
+ try {
269
+ await (0, test_1.expect)(banner).toBeVisible({ timeout });
270
+ return;
271
+ }
272
+ catch (error) {
273
+ if (attempt === maxRetries - 1) {
274
+ throw error;
275
+ }
276
+ console.warn(`Tasklist banner not visible on attempt ${attempt + 1}; ` +
277
+ 're-entering through the apps menu.');
278
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
279
+ }
280
+ }
281
+ }
282
+ // The third instance of the hand-off failure `openOperateWithRetry` and
283
+ // `openTasklistWithRetry` above exist to catch — and the only app hop in the
284
+ // smoke flow that was still ungated. `clickOptimize` returns as soon as its
285
+ // click resolves, so a hand-off that stalled (or that landed on Optimize's
286
+ // `/login` SSO bounce rather than the app) leaves the tab somewhere that can
287
+ // never render a process definition. The caller's import poll then spends
288
+ // its ENTIRE budget — the longest single wait in the flow, 20 minutes —
289
+ // reloading that page, and a reload cannot recover a hand-off that never
290
+ // happened. Gating here fails in 3 x 60s with a diagnostic naming the hop
291
+ // instead. Every other 8.10 spec already asserts `optimizeBanner` right
292
+ // after `clickOptimize` for exactly this reason; only the smoke flow did
293
+ // not.
294
+ async openOptimizeWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3) {
295
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
296
+ await this.clickCamundaApps();
297
+ await this.clickOptimize(clusterName);
298
+ try {
299
+ await (0, test_1.expect)(banner).toBeVisible({ timeout });
300
+ return;
301
+ }
302
+ catch (error) {
303
+ if (attempt === maxRetries - 1) {
304
+ throw error;
305
+ }
306
+ console.warn(`Optimize banner not visible on attempt ${attempt + 1}; ` +
307
+ 're-entering through the apps menu.');
308
+ await this.page.goto('/', { waitUntil: 'domcontentloaded' });
309
+ }
310
+ }
311
+ }
160
312
  async clickAdmin(clusterName) {
161
313
  const maxRetries = 5;
162
314
  for (let retries = 0; retries < maxRetries; retries++) {
@@ -38,6 +38,7 @@ declare class ModelerHomePage {
38
38
  clickBpmnTemplateOption(): Promise<void>;
39
39
  clickFormOption(): Promise<void>;
40
40
  enterFormName(name: string): Promise<void>;
41
+ enterFormNameWithRetry(name: string): Promise<void>;
41
42
  clickProjectBreadcrumb(): Promise<void>;
42
43
  clickHomeBreadcrumb(): Promise<void>;
43
44
  clickOpenOrganizationsButton(): Promise<void>;
@@ -62,9 +62,15 @@ class ModelerHomePage {
62
62
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
63
63
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
64
64
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
65
- this.crossComponentProjectFolder = page.getByTitle(this.defaultFolderName, {
65
+ // `.first()` keeps this usable in an org that already accumulated more
66
+ // than one project with this name — an unscoped by-title match throws a
67
+ // strict-mode violation the moment a duplicate exists, which fails every
68
+ // later run in that org rather than just the run that created it.
69
+ this.crossComponentProjectFolder = page
70
+ .getByTitle(this.defaultFolderName, {
66
71
  exact: true,
67
- });
72
+ })
73
+ .first();
68
74
  this.rows = page.getByRole('row');
69
75
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
70
76
  this.messageBanner = page.locator('[data-test="close-top-banner"]');
@@ -157,8 +163,13 @@ class ModelerHomePage {
157
163
  await process.click();
158
164
  }
159
165
  async clickDiagramTypeDropdown() {
166
+ // Navigation-class wait: this is the first control on the project page, so
167
+ // it is reached right after navigating into the project (or back via the
168
+ // breadcrumb) and its render waits on the project's resource list. 15s was
169
+ // short enough that a slow project load failed with
170
+ // `locator('[data-test="diagram-dropdown"]') ... element(s) not found`.
160
171
  await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
161
- timeout: 15000,
172
+ timeout: 60000,
162
173
  });
163
174
  await this.diagramTypeDropdown.click();
164
175
  }
@@ -176,6 +187,33 @@ class ModelerHomePage {
176
187
  await this.formNameInput.fill(name);
177
188
  await this.formNameInput.press('Enter');
178
189
  }
190
+ // Re-enters the name until the breadcrumb reflects it. Web Modeler mounts
191
+ // the inline name field as soon as the form editor opens, before it is wired
192
+ // to the rename mutation, so a fill that lands in that window is silently
193
+ // discarded: the form keeps its default "New form" name and the breadcrumb
194
+ // never shows the requested one. Nothing retried that, so a single lost
195
+ // keystroke made the caller's `assertFormBreadcrumbVisible` wait out its
196
+ // whole 120s on a form that was never going to be renamed
197
+ // (`button[data-test="breadcrumb-form"] ... element(s) not found`). Verifying
198
+ // with a short per-attempt budget turns that into one cheap retry. The
199
+ // caller's assertion stays the authority: this returns after the last
200
+ // attempt rather than throwing, so the failure is still reported by the
201
+ // assertion and not by a helper.
202
+ async enterFormNameWithRetry(name) {
203
+ const maxRetries = 3;
204
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
205
+ await this.enterFormName(name);
206
+ const renamed = await this.formNameBreadcrumb(name)
207
+ .waitFor({ state: 'visible', timeout: 30000 })
208
+ .then(() => true)
209
+ .catch(() => false);
210
+ if (renamed) {
211
+ return;
212
+ }
213
+ console.warn(`Form rename to "${name}" has not reached the breadcrumb ` +
214
+ `(attempt ${attempt}/${maxRetries}). Re-entering the name.`);
215
+ }
216
+ }
179
217
  async clickProjectBreadcrumb() {
180
218
  await this.projectBreadcrumb.click({ timeout: 60000 });
181
219
  }
@@ -233,9 +271,18 @@ class ModelerHomePage {
233
271
  });
234
272
  }
235
273
  async createCrossComponentProjectFolder() {
236
- await (0, sleep_1.sleep)(15000);
237
274
  await this.dismissOverlays();
238
- if (await this.crossComponentProjectFolder.isVisible()) {
275
+ // The projects list renders asynchronously, so an instantaneous
276
+ // isVisible() reports "missing" whenever the list is still loading — and
277
+ // this method then creates a SECOND project with the same name. Those
278
+ // duplicates are permanent and break every subsequent run for that user,
279
+ // so wait for the folder to actually show up before concluding it is
280
+ // absent (this also replaces the blind 15s sleep the check relied on).
281
+ const folderExists = await this.crossComponentProjectFolder
282
+ .waitFor({ state: 'visible', timeout: 60000 })
283
+ .then(() => true)
284
+ .catch(() => false);
285
+ if (folderExists) {
239
286
  console.log('Cross Component Project folder already exists. Clicking into it');
240
287
  await this.clickCrossComponentProjectFolder();
241
288
  return;
@@ -54,12 +54,20 @@ class OperateHomePage {
54
54
  this.messageBanner = page.getByRole('button', { name: 'Close' });
55
55
  }
56
56
  async clickProcessesTab() {
57
+ // `clickLocatorWithRetry` stops at whichever of `maxRetries` and
58
+ // `totalTimeout` is reached first. One failing attempt here costs up to
59
+ // ~47s (the banner pre-action plus the 30s visibility waits), so a 120s
60
+ // budget cut the 5 configured retries down to 3 — this method is reached
61
+ // right after switching apps, while the cluster SSO hand-off into Operate
62
+ // is often still in flight, and it reported "Failed to click locator
63
+ // getByRole('link', { name: 'Processes', exact: true }) after 3 attempts"
64
+ // after barely two minutes. Give the configured retries room to run.
57
65
  await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.processesTab, {
58
66
  preAction: async () => {
59
67
  await this.clickMessageBanner();
60
68
  },
61
69
  maxRetries: 5,
62
- totalTimeout: 120000,
70
+ totalTimeout: 300000,
63
71
  visibilityTimeout: 30000,
64
72
  });
65
73
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.processPageHeading, {
@@ -67,8 +75,21 @@ class OperateHomePage {
67
75
  await (0, test_1.expect)(this.processesTab).toBeVisible({ timeout: 10000 });
68
76
  await this.processesTab.click({ timeout: 10000 });
69
77
  },
78
+ // The Processes view routinely needs longer than this helper's default
79
+ // 10s per attempt to render on a loaded cluster — the same 30s the tab
80
+ // click above already assumes for this nav, and what SM-8.10 uses for
81
+ // this very assertion. With the default, all 5 attempts could expire
82
+ // inside ~1 min of the 5 min budget and report "Failed to assert
83
+ // visibility locator getByTestId('expanded-panel').getByText('Process',
84
+ // { exact: true }) after 5 attempts" while the view was still on its way.
85
+ visibilityTimeout: 30000,
86
+ // Re-clicking the tab cannot help a view that never rendered, so reload
87
+ // between attempts and let each one re-ask Operate for the page.
88
+ postAction: async () => {
89
+ await this.page.reload();
90
+ },
70
91
  maxRetries: 5,
71
- totalTimeout: 120000,
92
+ totalTimeout: 300000,
72
93
  });
73
94
  }
74
95
  async closeInformationDialog() {
@@ -1,11 +1,15 @@
1
- import { Page, Locator } from '@playwright/test';
1
+ import { Page, Locator, BrowserContext } from '@playwright/test';
2
+ type InstanceUrlWatcher = () => string | undefined;
3
+ declare function watchOperateInstanceUrl(context: BrowserContext): InstanceUrlWatcher;
2
4
  declare class OperateProcessInstancePage {
3
5
  private page;
6
+ private instanceUrl;
4
7
  readonly diagram: Locator;
5
8
  readonly completedIcon: Locator;
6
9
  readonly diagramSpinner: Locator;
7
10
  readonly activeIcon: Locator;
8
11
  readonly incidentIcon: Locator;
12
+ readonly incidentsBanner: Locator;
9
13
  readonly userTaskOpenTasklistLink: Locator;
10
14
  readonly taskDetailsPopOver: Locator;
11
15
  readonly connectorResultVariableName: (name: string) => Locator;
@@ -15,8 +19,18 @@ declare class OperateProcessInstancePage {
15
19
  readonly gotItButton: Locator;
16
20
  constructor(page: Page);
17
21
  connectorResultVariableValue(variableName: string): Promise<Locator>;
22
+ rememberInstanceUrl(timeout?: number, watchedUrl?: InstanceUrlWatcher): Promise<void>;
23
+ private hasBouncedToList;
24
+ private waitForAuthToSettle;
25
+ private isStateIconWait;
26
+ private describeIncident;
27
+ private describeRenderedState;
28
+ waitForInstanceLocator(locator: Locator, description: string, totalTimeout?: number, pollTimeout?: number): Promise<void>;
29
+ dismissWhatsNewIfVisible(): Promise<void>;
18
30
  closePopOverIfVisible(): Promise<void>;
19
- reload(): Promise<void>;
31
+ private clusterHost;
32
+ private reenterViaConsole;
33
+ reload(): Promise<boolean>;
20
34
  assertProcessCompleteStatusWithRetry(timeout?: number, maxRetries?: number): Promise<void>;
21
35
  assertProcessVariableContainsText(variableName: string, text: string): Promise<void>;
22
36
  assertProcessVariableHasDocumentCount(variableName: string, count: number, softFail?: boolean): Promise<void>;
@@ -25,4 +39,4 @@ declare class OperateProcessInstancePage {
25
39
  assertDetailsPopoverIsVisible(): Promise<void>;
26
40
  assertBusinessIdVisible(businessId: string): Promise<void>;
27
41
  }
28
- export { OperateProcessInstancePage };
42
+ export { OperateProcessInstancePage, watchOperateInstanceUrl };