@camunda/e2e-test-suite 0.0.1168 → 0.0.1170

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.
@@ -217,7 +217,7 @@ declare class ModelerCreatePage {
217
217
  clickJobTypeInput(): Promise<void>;
218
218
  fillJobTypeInput(name: string): Promise<void>;
219
219
  private suppressTestModeAnnouncement;
220
- switchToPlay(): Promise<void>;
220
+ switchToPlay(afterNavigation?: boolean): Promise<void>;
221
221
  completePlayConfiguration(clusterName: string): Promise<void>;
222
222
  embedForm(formName: string): Promise<void>;
223
223
  openDetailsPanelIfCollapsed(): Promise<void>;
@@ -1420,7 +1420,11 @@ class ModelerCreatePage {
1420
1420
  localStorage.setItem('play-testAnnouncementSeen', 'true');
1421
1421
  });
1422
1422
  }
1423
- async switchToPlay() {
1423
+ // `afterNavigation` marks the call sites that land here straight off a
1424
+ // page load (ensureStageCluster's goto() detour, the legacy retry's
1425
+ // reload()) rather than off an already-rendered diagram. Only those recover
1426
+ // by reloading -- see below.
1427
+ async switchToPlay(afterNavigation = false) {
1424
1428
  await this.suppressTestModeAnnouncement();
1425
1429
  // completePlayConfiguration re-enters Play after ensureStageCluster's
1426
1430
  // goto() detour, and the diagram page keeps rendering well past the point
@@ -1428,7 +1432,34 @@ class ModelerCreatePage {
1428
1432
  // 10s after it returned, so the click burned the whole global
1429
1433
  // actionTimeout on a page that was merely still booting. Wait for the tab
1430
1434
  // on its own, longer budget first, then click within the normal one.
1431
- await this.playTab.waitFor({ state: 'visible', timeout: 90000 });
1435
+ //
1436
+ // Waiting longer is not enough on its own for the post-navigation call,
1437
+ // because the tabs' absence there is not always transient. Hub renders the
1438
+ // modes tablist only when `isCurrentDiagramValid && isBPMN &&
1439
+ // allowedViewModes.length > 0`, and `currentDiagramStore.isValid` stays
1440
+ // false until the diagram content has been fetched AND sanitised
1441
+ // (`healthState.isValid && hasBeenValidated`). A diagram fetch that fails
1442
+ // on that fresh load therefore renders an action bar with no mode tabs at
1443
+ // all, permanently -- so the single-shot 90s wait could only ever expire on
1444
+ // it. Re-navigate between attempts instead: the reload re-runs the fetch
1445
+ // that did not land. Gated on `afterNavigation` so the in-place call from
1446
+ // the spec, made on a diagram whose tabs have been on screen throughout
1447
+ // modelling, can never reload mid-edit.
1448
+ if (afterNavigation) {
1449
+ await (0, test_1.expect)(async () => {
1450
+ const visible = await this.playTab
1451
+ .waitFor({ state: 'visible', timeout: 45000 })
1452
+ .then(() => true)
1453
+ .catch(() => false);
1454
+ if (!visible) {
1455
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
1456
+ await this.playTab.waitFor({ state: 'visible', timeout: 45000 });
1457
+ }
1458
+ }).toPass({ timeout: 150000 });
1459
+ }
1460
+ else {
1461
+ await this.playTab.waitFor({ state: 'visible', timeout: 90000 });
1462
+ }
1432
1463
  await this.playTab.click();
1433
1464
  }
1434
1465
  async completePlayConfiguration(clusterName) {
@@ -1440,7 +1471,7 @@ class ModelerCreatePage {
1440
1471
  // which cluster the test plays on.
1441
1472
  const reboundStageCluster = await this.processApplicationPage.ensureStageCluster(clusterName);
1442
1473
  if (reboundStageCluster) {
1443
- await this.switchToPlay();
1474
+ await this.switchToPlay(true);
1444
1475
  }
1445
1476
  const timeout = 30000;
1446
1477
  // New flow (8.10+): "Setup environment" panel with three steps:
@@ -1458,17 +1489,32 @@ class ModelerCreatePage {
1458
1489
  // data-testid is unchanged between the old overlay and the new panel, so
1459
1490
  // anchor on it instead.
1460
1491
  const setupDeployButton = this.page.getByTestId('test-configuration-deploy-button');
1461
- // The same commit dropped the "Process has been successfully deployed."
1462
- // sentence in favour of a "Successfully deployed" badge. Match the badge's
1463
- // test id first, keep the old sentence for pre-8b42269 modeler builds.
1492
+ // "Deploy reported success" has moved twice. 8b42269 dropped the
1493
+ // "Process has been successfully deployed." sentence in favour of a
1494
+ // `deploy-success-badge` element, and camunda-hub d2fd90227c ("remove
1495
+ // non-process-application deploy logic from test studio", #28585,
1496
+ // 2026-09-11) then deleted that badge outright along with the rest of the
1497
+ // non-process-application deploy path -- so neither locator matches
1498
+ // anything on a current modeler build and this wait could only ever time
1499
+ // out, even though the deploy itself succeeds.
1500
+ //
1501
+ // The surviving indicator is the accordion step icon: DeployStepIcon
1502
+ // renders a check carrying `aria-label="Deployment complete"` exactly when
1503
+ // `isDeployStepComplete` is true, which is the same `isTestEnvironmentReady`
1504
+ // flag that opens step 3. Match it by attribute rather than getByLabel:
1505
+ // TestConfigurationPanel wraps the icon in an `aria-hidden` span, so it is
1506
+ // deliberately outside the accessibility tree. The sibling error state has
1507
+ // its own label ("Deployment failed"), so a failed deploy still surfaces as
1508
+ // a timeout here instead of passing. Keep both older locators for
1509
+ // pre-d2fd902 modeler builds.
1464
1510
  //
1465
- // Probed by ATTACHMENT, never visibility: the badge renders only once the
1466
- // product reports the deploy succeeded, but the panel reacts to that same
1467
- // state by hiding the setup overlay, which collapses the "Deploy process"
1468
- // accordion item the badge lives in. So it is present-but-hidden within a
1469
- // frame of appearing, and a toBeVisible() wait on it can never pass.
1511
+ // Probed by ATTACHMENT, never visibility: the panel reacts to the same
1512
+ // completion state by hiding the setup overlay and collapsing the deploy
1513
+ // accordion item, so the indicator can be present-but-hidden within a frame
1514
+ // of appearing and a toBeVisible() wait on it can never pass.
1470
1515
  const deploySuccessBadge = this.page
1471
- .getByTestId('deploy-success-badge')
1516
+ .locator('[aria-label="Deployment complete"]')
1517
+ .or(this.page.getByTestId('deploy-success-badge'))
1472
1518
  .or(this.page.getByText('Process has been successfully deployed'))
1473
1519
  .first();
1474
1520
  const isDeployReported = async () => (await deploySuccessBadge.count()) > 0;
@@ -1669,8 +1715,10 @@ class ModelerCreatePage {
1669
1715
  const maxRetries = 2;
1670
1716
  while (attempts < maxRetries) {
1671
1717
  try {
1718
+ // Call switchToPlay on subsequent attempts, i.e. after the reload at
1719
+ // the bottom of this loop.
1672
1720
  if (attempts > 0)
1673
- await this.switchToPlay(); // Call switchToPlay on subsequent attempts
1721
+ await this.switchToPlay(true);
1674
1722
  await this.selectCluster(clusterName);
1675
1723
  await this.continueToPlayButton.click({ timeout });
1676
1724
  await (0, test_1.expect)(this.page.getByText('Play environment is ready')).toBeVisible({ timeout: 90000 });
@@ -225,10 +225,9 @@ class ProcessApplicationPage {
225
225
  }
226
226
  const returnTo = this.page.url();
227
227
  const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
228
- await this.page.goto(`${paUrl}/connected-clusters`, {
229
- waitUntil: 'domcontentloaded',
230
- });
231
- await this.openStageDefinition();
228
+ const deepLink = `${paUrl}/connected-clusters`;
229
+ await this.page.goto(deepLink, { waitUntil: 'domcontentloaded' });
230
+ await this.openStageDefinition(deepLink);
232
231
  await this.defineDevelopmentStage(clusterName);
233
232
  container.developmentCluster = clusterName;
234
233
  await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
@@ -240,7 +239,7 @@ class ProcessApplicationPage {
240
239
  // the affordances hub offers before failing with exactly what was looked for.
241
240
  // Worth the noise: an unbound cluster otherwise surfaces as a deploy dialog
242
241
  // that silently offers nothing, which says nothing about its own cause.
243
- async openStageDefinition() {
242
+ async openStageDefinition(deepLink) {
244
243
  // Page-scoped .first(): hub renders one such control on this page, and a
245
244
  // second placement must not turn every rebinding test into a strict-mode
246
245
  // violation.
@@ -254,7 +253,30 @@ class ProcessApplicationPage {
254
253
  // dialog it was meant to open is already on screen. Re-probing for the
255
254
  // dialog at the top of every attempt settles that: whichever route got it
256
255
  // there, the next pass sees it and returns.
256
+ //
257
+ // Re-navigating between attempts is what makes that retry able to recover
258
+ // at all. Hub renders the "Configure" control and MOUNTS DefineStagesModal
259
+ // in the same component -- ConnectedClusters, inside the process
260
+ // application's right column -- and gates it on `isAdmin || isEditor`,
261
+ // i.e. on projectStore having loaded. processApplicationStore.init()
262
+ // swallows a failed fetch (it only toasts "Yikes! Couldn't retrieve your
263
+ // project" and clears its loading flag), so one transient 5xx on the
264
+ // freshly created application leaves a fully rendered page that has no
265
+ // card: no control to click, and the deep link's own auto-open never fires
266
+ // either, because useConnectedClustersDeepLink only calls open() when that
267
+ // same `canSee` is true. Re-probing and re-clicking cannot bring back a
268
+ // component that was never mounted -- only a reload, which re-runs the
269
+ // store init, can. Without it every attempt burned its two waits on
270
+ // elements that could not appear and the whole budget expired on a page
271
+ // that one navigation would have fixed.
272
+ let attempt = 0;
257
273
  await (0, test_1.expect)(async () => {
274
+ if (attempt++ > 0) {
275
+ await this.page.goto(deepLink, { waitUntil: 'domcontentloaded' });
276
+ // Proof the process application page itself came back, so a failure
277
+ // here still names the missing dialog rather than a blank page.
278
+ await this.waitForProcessApplication(60000);
279
+ }
258
280
  const opened = await this.defineStagesDialog
259
281
  .waitFor({ state: 'visible', timeout: 15000 })
260
282
  .then(() => true)
@@ -265,7 +287,7 @@ class ProcessApplicationPage {
265
287
  await reopen.waitFor({ state: 'visible', timeout: 10000 });
266
288
  await reopen.click({ timeout: 15000 });
267
289
  await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 15000 });
268
- }).toPass({ timeout: 150000 });
290
+ }).toPass({ timeout: 180000 });
269
291
  }
270
292
  async openByName(name) {
271
293
  const processApplication = this.page
@@ -12,7 +12,10 @@ declare class TaskPanelPage {
12
12
  assertAssigmentStatus(status: string): Promise<void>;
13
13
  assignTaskToMe(): Promise<void>;
14
14
  completeATask(): Promise<void>;
15
- openTask(name: string, userTaskName?: string): Promise<void>;
15
+ taskCard(name: string, userTaskName?: string): Locator;
16
+ revealTaskCard(card: Locator, name: string): Promise<void>;
17
+ assertTaskVisible(name: string): Promise<void>;
18
+ openTask(name: string, userTaskName?: string): Promise<string>;
16
19
  taskListBannerIsVisible(): Promise<void>;
17
20
  asssertUnnassignedTaskEmptyMessage(): Promise<void>;
18
21
  filterBy(option: 'All open tasks' | 'Unassigned' | 'Assigned to me' | 'Completed' | 'Custom'): Promise<void>;
@@ -56,16 +56,62 @@ class TaskPanelPage {
56
56
  await this.assignToMeButton.click();
57
57
  await this.completeTaskButton.click();
58
58
  }
59
- async openTask(name, userTaskName = '') {
60
- let tasks = this.availableTasks.getByText(name, { exact: true });
59
+ taskCard(name, userTaskName = '') {
60
+ let cards = this.availableTasks
61
+ .getByRole('article')
62
+ .filter({ has: this.page.getByText(name, { exact: true }) });
61
63
  if (userTaskName != '') {
62
- tasks = this.availableTasks
63
- .getByRole('article')
64
- .filter({ hasText: name })
65
- .filter({ hasText: userTaskName });
64
+ cards = cards.filter({ hasText: userTaskName });
66
65
  }
67
- await (0, test_1.expect)(tasks.nth(0)).toBeVisible({ timeout: 20000 });
68
- await tasks.nth(0).click({ timeout: 20000 });
66
+ return cards.first();
67
+ }
68
+ // AvailableTasks.tsx renders the list through @tanstack/react-virtual, so
69
+ // only the cards inside the scroll window (plus a five-item overscan) are
70
+ // in the DOM. A task further down the list is not hidden, it is not
71
+ // rendered at all, so getByText() keeps reporting "element(s) not found"
72
+ // however long it waits. Page through the list until the card renders,
73
+ // wrapping back to the top so the layout's 5s refetch can bring in a task
74
+ // that was still being indexed.
75
+ async revealTaskCard(card, name) {
76
+ const scrollableList = this.availableTasks.getByTestId('scrollable-list');
77
+ await test_1.expect
78
+ .poll(async () => {
79
+ if (await card.isVisible()) {
80
+ return true;
81
+ }
82
+ if (await scrollableList.isVisible()) {
83
+ await scrollableList.evaluate((list) => {
84
+ const maxScrollTop = list.scrollHeight - list.clientHeight;
85
+ if (list.scrollTop >= maxScrollTop - 1) {
86
+ list.scrollTop = 0;
87
+ return;
88
+ }
89
+ list.scrollTop += Math.max(list.clientHeight - 100, 200);
90
+ });
91
+ }
92
+ return false;
93
+ }, {
94
+ timeout: 90000,
95
+ intervals: [500],
96
+ message: `Task "${name}" never rendered in the task list`,
97
+ })
98
+ .toBe(true);
99
+ }
100
+ async assertTaskVisible(name) {
101
+ await this.revealTaskCard(this.taskCard(name), name);
102
+ }
103
+ // Returns the opened task's userTaskKey -- the only identifier that tells
104
+ // two tasks sharing a name apart.
105
+ async openTask(name, userTaskName = '') {
106
+ const card = this.taskCard(name, userTaskName);
107
+ await this.revealTaskCard(card, name);
108
+ await card.click({ timeout: 20000 });
109
+ await this.page.waitForURL(/\/tasklist\/\d+/, { timeout: 30000 });
110
+ const openedTask = /\/tasklist\/(\d+)/.exec(this.page.url());
111
+ if (openedTask === null) {
112
+ throw new Error(`Opening task "${name}" did not open a task`);
113
+ }
114
+ return openedTask[1];
69
115
  }
70
116
  async taskListBannerIsVisible() {
71
117
  await (0, test_1.expect)(this.taskListPageBanner).toBeVisible({ timeout: 30000 });
@@ -78,23 +124,44 @@ class TaskPanelPage {
78
124
  // appears. Unlike the old Carbon sidebar, the trigger is always visible
79
125
  // -- there's no separate expand/collapse step, and selecting an option
80
126
  // closes the menu on its own.
81
- await (0, test_1.expect)(this.filterSelectButton).toBeVisible({ timeout: 10000 });
82
- await this.filterSelectButton.click();
127
+ //
128
+ // The trigger also carries the active filter's label, so it doubles as
129
+ // the "selection applied" signal -- the same one camunda/camunda's own
130
+ // Filters.test.tsx and TasklistIndex.page.ts assert on. It replaces the
131
+ // heading the old Carbon panel rendered for the active filter, which the
132
+ // shadcn panel no longer has.
133
+ //
134
+ // TasksLayoutPage renders <Filters disabled={isPending} />, so a task
135
+ // list refetch landing while the menu is open disables the trigger and
136
+ // tears the menu down before Radix runs the item's onSelect: the menu
137
+ // closes but the filter never changes. Re-open and pick again rather
138
+ // than failing on the first dropped selection.
83
139
  const menuItem = this.page.getByRole('menuitem', {
84
140
  name: option,
85
141
  exact: true,
86
142
  });
87
- await (0, test_1.expect)(menuItem).toBeVisible({ timeout: 10000 });
88
- await menuItem.click();
89
- await (0, test_1.expect)(menuItem).toBeHidden({ timeout: 10000 });
90
- // The trigger renders the active filter's label, so this confirms the
91
- // selection was applied -- the same signal camunda/camunda's own
92
- // Filters.test.tsx asserts on the trigger. It replaces the heading the
93
- // old Carbon panel rendered for the active filter, which the shadcn
94
- // panel no longer has.
95
- await (0, test_1.expect)(this.filterSelectButton).toContainText(option, {
96
- timeout: 10000,
143
+ const triggerShowingOption = this.filterSelectButton.filter({
144
+ hasText: new RegExp(`^${option}$`),
97
145
  });
146
+ const maxAttempts = 3;
147
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
148
+ await (0, test_1.expect)(this.filterSelectButton).toBeEnabled({ timeout: 30000 });
149
+ await this.filterSelectButton.click();
150
+ await (0, test_1.expect)(menuItem).toBeVisible({ timeout: 10000 });
151
+ await menuItem.click();
152
+ const applied = await triggerShowingOption
153
+ .waitFor({ state: 'visible', timeout: 15000 })
154
+ .then(() => true)
155
+ .catch(() => false);
156
+ if (applied) {
157
+ return;
158
+ }
159
+ console.warn(`Attempt ${attempt} to apply the "${option}" task filter did not ` +
160
+ 'take effect. Retrying...');
161
+ await this.page.keyboard.press('Escape');
162
+ await (0, test_1.expect)(this.filterSelectButton).toHaveAttribute('aria-expanded', 'false', { timeout: 10000 });
163
+ }
164
+ throw new Error(`Task filter "${option}" was not applied after ${maxAttempts} attempts`);
98
165
  }
99
166
  }
100
167
  exports.TaskPanelPage = TaskPanelPage;
@@ -42,23 +42,32 @@ exports.validateURL = validateURL;
42
42
  async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, taskPriority, userTaskName = '', textInput = '', maxRetries = 5) {
43
43
  for (let attempt = 0; attempt < maxRetries; attempt++) {
44
44
  try {
45
- await taskPanelPage.openTask(taskName, userTaskName);
46
- const openTasksWithName = taskPanelPage.availableTasks.getByText(taskName, { exact: true });
47
- const openTaskCount = await openTasksWithName.count();
45
+ const userTaskKey = await taskPanelPage.openTask(taskName, userTaskName);
48
46
  await (0, sleep_1.sleep)(500);
49
47
  if (!(await taskDetailsPage.assignedToMeText.isVisible())) {
50
48
  await taskDetailsPage.clickAssignToMeButton();
51
49
  }
52
- await (0, test_1.expect)(taskDetailsPage.detailsPanel.getByText(taskPriority)).toBeVisible();
50
+ // The details panel is fetched after the task is opened, so the
51
+ // priority label can lag the click by more than Playwright's 5s
52
+ // default expect timeout on a loaded c8Run runner. Every other wait
53
+ // in this helper already carries an explicit, generous timeout.
54
+ await (0, test_1.expect)(taskDetailsPage.detailsPanel.getByText(taskPriority)).toBeVisible({ timeout: 30000 });
53
55
  if (textInput != '') {
54
56
  await taskDetailsPage.clickTextInput();
55
57
  await taskDetailsPage.fillTextInput(textInput);
56
58
  }
57
59
  await taskDetailsPage.taskCompletedBanner.waitFor({ state: 'hidden' });
58
60
  await taskDetailsPage.clickCompleteTaskButton();
59
- await (0, test_1.expect)(openTasksWithName).toHaveCount(openTaskCount - 1, {
60
- timeout: 60000,
61
- });
61
+ // clickCompleteTaskButton() already waits for the "Task completed"
62
+ // banner, so the completion itself is confirmed there. Counting the
63
+ // open cards that share this task's name cannot confirm it: the list
64
+ // is virtualised, so the count only ever covers the scrolled-in
65
+ // window, and any concurrently running copy of this spec completes
66
+ // tasks with the same name -- the count can stay flat or drop by two,
67
+ // and a false failure here sends the loop round to open and complete
68
+ // a second task. Wait for this exact card, keyed on its userTaskKey,
69
+ // to leave the open list instead.
70
+ await (0, test_1.expect)(taskPanelPage.availableTasks.locator(`[data-testid="task-${userTaskKey}"]`)).toHaveCount(0, { timeout: 60000 });
62
71
  return;
63
72
  }
64
73
  catch (error) {
@@ -67,7 +76,8 @@ async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, t
67
76
  }
68
77
  else {
69
78
  console.error(error);
70
- throw new Error(`Assertion failed after ${maxRetries} attempts`);
79
+ throw new Error(`Completing task ${taskName} failed after ${maxRetries} ` +
80
+ `attempts: ${error}`);
71
81
  }
72
82
  }
73
83
  }
@@ -138,10 +138,10 @@ c8Run_8_10_1.test.describe('HTO User Flow Tests', () => {
138
138
  // The shadcn task panel no longer renders the active filter as a
139
139
  // heading (filterBy asserts it on the Filters trigger instead), so
140
140
  // wait for a completed task to actually appear in the list -- that
141
- // also covers the indexing lag before the reload below.
142
- await (0, test_1.expect)(taskPanelPage.availableTasks
143
- .getByText('priorityTest4', { exact: true })
144
- .first()).toBeVisible({ timeout: 45000 });
141
+ // also covers the indexing lag before the reload below. The list is
142
+ // virtualised, so this has to page through it rather than wait on a
143
+ // locator that is simply not rendered yet.
144
+ await taskPanelPage.assertTaskVisible('priorityTest4');
145
145
  await page.reload();
146
146
  await (0, sleep_1.sleep)(1000);
147
147
  await taskPanelPage.openTask('priorityTest4');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1168",
3
+ "version": "0.0.1170",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",