@camunda/e2e-test-suite 0.0.1169 → 0.0.1171

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.
@@ -17,6 +17,6 @@ declare class OperateProcessesPage {
17
17
  clickProcessIncidentsCheckbox(): Promise<void>;
18
18
  clickRunningProcessInstancesCheckbox(): Promise<void>;
19
19
  clickFinishedProcessInstancesCheckbox(): Promise<void>;
20
- clickProcessInstanceLink(processName: string): Promise<void>;
20
+ clickProcessInstanceLink(processName: string, processInstanceKey?: string): Promise<void>;
21
21
  }
22
22
  export { OperateProcessesPage };
@@ -61,20 +61,33 @@ class OperateProcessesPage {
61
61
  async clickFinishedProcessInstancesCheckbox() {
62
62
  await this.processFinishedInstancesCheckbox.click({ timeout: 90000 });
63
63
  }
64
- async clickProcessInstanceLink(processName) {
64
+ async clickProcessInstanceLink(processName, processInstanceKey) {
65
+ // Both Operate frontends label the instance-key cell link
66
+ // "View instance <key>" -- the aria-label in operate/client's
67
+ // InstancesTable, the operate.processes.instancesTable.viewInstance string
68
+ // in the orchestration-cluster webapp -- so passing a key pins the click
69
+ // to one instance. Without a key every row carrying the process name is a
70
+ // candidate and `.first()` opens an arbitrary instance of that process.
71
+ // Both frontends sort the list by start date descending, so a
72
+ // just-created instance is on the first page.
73
+ const instanceLink = processInstanceKey === undefined
74
+ ? this.page
75
+ .getByRole('row')
76
+ .filter({ hasText: new RegExp(`^(?:Select row)?${processName}\\d+`) })
77
+ .first()
78
+ .getByRole('link')
79
+ : this.page.getByRole('link', {
80
+ name: `View instance ${processInstanceKey}`,
81
+ exact: true,
82
+ });
65
83
  let retryCount = 0;
66
- const maxRetries = 3;
84
+ const maxRetries = 4;
67
85
  while (retryCount < maxRetries) {
68
86
  try {
69
87
  await (0, sleep_1.sleep)(1000);
70
88
  await this.page.reload();
71
89
  await (0, sleep_1.sleep)(1000);
72
- await this.page
73
- .getByRole('row')
74
- .filter({ hasText: new RegExp(`^(?:Select row)?${processName}\\d+`) })
75
- .first()
76
- .getByRole('link')
77
- .click({ timeout: 30000 });
90
+ await instanceLink.click({ timeout: 30000 });
78
91
  await (0, test_1.expect)(this.page.getByText('Instance History').first()).toBeVisible({
79
92
  timeout: 10000,
80
93
  });
@@ -220,7 +220,27 @@ class TaskDetailsPage {
220
220
  // taskVariablesValueLabel) associated by htmlFor -- the old Carbon input
221
221
  // exposed the same string as an HTML title attribute, which getByTitle
222
222
  // matched directly; getByLabel is the equivalent for a real <label>.
223
- await (0, test_1.expect)(this.page.getByLabel(variableName + ' Value')).toHaveValue(variableValue, { timeout: 60000 });
223
+ //
224
+ // The panel fetches the task's variables once, when the task is opened, so
225
+ // polling the DOM alone can never observe a value that was still being
226
+ // exported at that moment. Reload between attempts to force a re-fetch --
227
+ // same retry shape as taskAssertion/priorityAssertion above.
228
+ const valueInput = this.page.getByLabel(variableName + ' Value');
229
+ const maxRetries = 3;
230
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
231
+ try {
232
+ await (0, test_1.expect)(valueInput).toHaveValue(variableValue, { timeout: 30000 });
233
+ return;
234
+ }
235
+ catch (error) {
236
+ if (attempt === maxRetries) {
237
+ throw error;
238
+ }
239
+ console.log(`Attempt ${attempt} failed. Retrying...`);
240
+ await this.page.reload();
241
+ await (0, sleep_1.sleep)(5000);
242
+ }
243
+ }
224
244
  }
225
245
  }
226
246
  exports.TaskDetailsPage = TaskDetailsPage;
@@ -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;
@@ -1,7 +1,8 @@
1
- import { Page, Locator } from '@playwright/test';
1
+ import { APIRequestContext, Page, Locator } from '@playwright/test';
2
2
  import { TaskPanelPage } from '../c8Run-8.10/TaskPanelPage';
3
3
  import { TaskDetailsPage } from '../c8Run-8.10/TaskDetailsPage';
4
4
  export declare function navigateToApp(page: Page, appName: string): Promise<void>;
5
+ export declare function openUserTaskOfProcessInstance(page: Page, request: APIRequestContext, processInstanceKey: string, maxRetries?: number, retryDelayMs?: number): Promise<string>;
5
6
  export declare function assertLocatorVisibleWithRetry(page: Page, locator: Locator, text: string, timeout?: number, maxRetries?: number): Promise<void>;
6
7
  export declare function validateURL(page: Page, URL: RegExp): Promise<void>;
7
8
  export declare function completeTaskWithRetry(taskPanelPage: TaskPanelPage, taskDetailsPage: TaskDetailsPage, taskName: string, taskPriority: string, userTaskName?: string, textInput?: string, maxRetries?: number): Promise<void>;
@@ -7,13 +7,42 @@
7
7
  * except in compliance with the Camunda License 1.0.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.completeTaskWithRetry = exports.validateURL = exports.assertLocatorVisibleWithRetry = exports.navigateToApp = void 0;
10
+ exports.completeTaskWithRetry = exports.validateURL = exports.assertLocatorVisibleWithRetry = exports.openUserTaskOfProcessInstance = exports.navigateToApp = void 0;
11
11
  const test_1 = require("@playwright/test");
12
12
  const sleep_1 = require("../../utils/sleep");
13
13
  async function navigateToApp(page, appName) {
14
14
  page.goto('/' + appName.toLowerCase() + '/login');
15
15
  }
16
16
  exports.navigateToApp = navigateToApp;
17
+ async function openUserTaskOfProcessInstance(page, request, processInstanceKey, maxRetries = 30, retryDelayMs = 2000) {
18
+ // A task card shows only the task and process display names (Task.tsx in
19
+ // camunda/camunda), so nothing in the Tasklist UI tells two instances of the
20
+ // same process apart -- opening "the first card named X" lands on whichever
21
+ // instance happens to sort first. Resolve the user task of THIS instance
22
+ // over the v2 API instead (c8Run serves it unauthenticated:
23
+ // camunda.security.authentication.unprotected-api is true in
24
+ // c8run/configuration/application.yaml, and api-tests-v2.spec.ts calls
25
+ // /v2/user-tasks/search the same way) and open it at its own route,
26
+ // /tasklist/{userTaskKey}. Retrying covers the export lag between creating
27
+ // the instance and its user task becoming searchable.
28
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
29
+ const response = await request.post('/v2/user-tasks/search', {
30
+ data: { filter: { processInstanceKey } },
31
+ });
32
+ if (response.ok()) {
33
+ const body = await response.json();
34
+ const userTaskKey = body?.items?.[0]?.userTaskKey;
35
+ if (userTaskKey != null) {
36
+ await page.goto('/tasklist/' + userTaskKey);
37
+ return String(userTaskKey);
38
+ }
39
+ }
40
+ await (0, sleep_1.sleep)(retryDelayMs);
41
+ }
42
+ throw new Error(`No user task found for process instance ${processInstanceKey} after ` +
43
+ `${maxRetries} attempts.`);
44
+ }
45
+ exports.openUserTaskOfProcessInstance = openUserTaskOfProcessInstance;
17
46
  async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 20000, maxRetries = 3) {
18
47
  for (let attempt = 0; attempt < maxRetries; attempt++) {
19
48
  try {
@@ -42,23 +71,32 @@ exports.validateURL = validateURL;
42
71
  async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, taskPriority, userTaskName = '', textInput = '', maxRetries = 5) {
43
72
  for (let attempt = 0; attempt < maxRetries; attempt++) {
44
73
  try {
45
- await taskPanelPage.openTask(taskName, userTaskName);
46
- const openTasksWithName = taskPanelPage.availableTasks.getByText(taskName, { exact: true });
47
- const openTaskCount = await openTasksWithName.count();
74
+ const userTaskKey = await taskPanelPage.openTask(taskName, userTaskName);
48
75
  await (0, sleep_1.sleep)(500);
49
76
  if (!(await taskDetailsPage.assignedToMeText.isVisible())) {
50
77
  await taskDetailsPage.clickAssignToMeButton();
51
78
  }
52
- await (0, test_1.expect)(taskDetailsPage.detailsPanel.getByText(taskPriority)).toBeVisible();
79
+ // The details panel is fetched after the task is opened, so the
80
+ // priority label can lag the click by more than Playwright's 5s
81
+ // default expect timeout on a loaded c8Run runner. Every other wait
82
+ // in this helper already carries an explicit, generous timeout.
83
+ await (0, test_1.expect)(taskDetailsPage.detailsPanel.getByText(taskPriority)).toBeVisible({ timeout: 30000 });
53
84
  if (textInput != '') {
54
85
  await taskDetailsPage.clickTextInput();
55
86
  await taskDetailsPage.fillTextInput(textInput);
56
87
  }
57
88
  await taskDetailsPage.taskCompletedBanner.waitFor({ state: 'hidden' });
58
89
  await taskDetailsPage.clickCompleteTaskButton();
59
- await (0, test_1.expect)(openTasksWithName).toHaveCount(openTaskCount - 1, {
60
- timeout: 60000,
61
- });
90
+ // clickCompleteTaskButton() already waits for the "Task completed"
91
+ // banner, so the completion itself is confirmed there. Counting the
92
+ // open cards that share this task's name cannot confirm it: the list
93
+ // is virtualised, so the count only ever covers the scrolled-in
94
+ // window, and any concurrently running copy of this spec completes
95
+ // tasks with the same name -- the count can stay flat or drop by two,
96
+ // and a false failure here sends the loop round to open and complete
97
+ // a second task. Wait for this exact card, keyed on its userTaskKey,
98
+ // to leave the open list instead.
99
+ await (0, test_1.expect)(taskPanelPage.availableTasks.locator(`[data-testid="task-${userTaskKey}"]`)).toHaveCount(0, { timeout: 60000 });
62
100
  return;
63
101
  }
64
102
  catch (error) {
@@ -67,7 +105,8 @@ async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, t
67
105
  }
68
106
  else {
69
107
  console.error(error);
70
- throw new Error(`Assertion failed after ${maxRetries} attempts`);
108
+ throw new Error(`Completing task ${taskName} failed after ${maxRetries} ` +
109
+ `attempts: ${error}`);
71
110
  }
72
111
  }
73
112
  }
@@ -6,6 +6,11 @@ const zeebeClient_1 = require("../../utils/zeebeClient");
6
6
  const UtilitiesPage_1 = require("../../pages/c8Run-8.10/UtilitiesPage");
7
7
  const sleep_1 = require("../../utils/sleep");
8
8
  const _setup_1 = require("../../test-setup.js");
9
+ // The key of the User_Task_Editing_Variables instance this run owns. Operate
10
+ // and Tasklist both resolve that instance by key, so a second live instance of
11
+ // the same process -- another worker, a --repeat-each copy, a leftover from an
12
+ // earlier run -- can no longer be picked up by mistake.
13
+ let variableEditingInstanceKey;
9
14
  c8Run_8_10_1.test.beforeAll(async () => {
10
15
  await Promise.all([
11
16
  (0, zeebeClient_1.deploy)('./resources/User_Task_Editing_Variables.bpmn'),
@@ -15,7 +20,7 @@ c8Run_8_10_1.test.beforeAll(async () => {
15
20
  (0, zeebeClient_1.deploy)('./resources/Start_Form_Process.bpmn'),
16
21
  (0, zeebeClient_1.deploy)('./resources/Zeebe_User_Task_Process_With_Priority.bpmn'),
17
22
  ]);
18
- await Promise.all([
23
+ const [, variableEditingInstances] = await Promise.all([
19
24
  (0, zeebeClient_1.createInstances)('Zeebe_User_Task', 1, 1),
20
25
  (0, zeebeClient_1.createInstances)('User_Task_Editing_Variables', 1, 1, {
21
26
  testVariable: 'testValue',
@@ -24,6 +29,7 @@ c8Run_8_10_1.test.beforeAll(async () => {
24
29
  (0, zeebeClient_1.createInstances)('Start_Form_Process', 1, 1),
25
30
  (0, zeebeClient_1.createInstances)('Zeebe_User_Task_Process_With_Priority', 1, 1),
26
31
  ]);
32
+ variableEditingInstanceKey = String(variableEditingInstances[0].processInstanceKey);
27
33
  await (0, sleep_1.sleep)(5000);
28
34
  });
29
35
  c8Run_8_10_1.test.describe('HTO User Flow Tests', () => {
@@ -36,13 +42,13 @@ c8Run_8_10_1.test.describe('HTO User Flow Tests', () => {
36
42
  await (0, _setup_1.captureScreenshot)(page, testInfo);
37
43
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
38
44
  });
39
- (0, c8Run_8_10_1.test)('User Task Editing Variables Flow', async ({ page, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskPanelPage, taskListLoginPage, operateLoginPage, taskDetailsPage, }) => {
45
+ (0, c8Run_8_10_1.test)('User Task Editing Variables Flow', async ({ page, request, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskPanelPage, taskListLoginPage, operateLoginPage, taskDetailsPage, }) => {
40
46
  await c8Run_8_10_1.test.step('View Process Instance in Operate, Edit the Variable & Assert the Variable is Updated in Tasklist', async () => {
41
47
  await (0, UtilitiesPage_1.navigateToApp)(page, 'operate');
42
48
  await operateLoginPage.login('demo', 'demo');
43
49
  await (0, test_1.expect)(operateHomePage.processesTab).toBeVisible({ timeout: 180000 });
44
50
  await operateHomePage.clickProcessesTab();
45
- await operateProcessesPage.clickProcessInstanceLink('User_Task_Editing_Variables');
51
+ await operateProcessesPage.clickProcessInstanceLink('User_Task_Editing_Variables', variableEditingInstanceKey);
46
52
  await operateProcessInstancePage.activeIconAssertion();
47
53
  await (0, test_1.expect)(page.getByText('"testValue"')).toBeVisible({
48
54
  timeout: 120000,
@@ -63,7 +69,8 @@ c8Run_8_10_1.test.describe('HTO User Flow Tests', () => {
63
69
  await (0, sleep_1.sleep)(10000);
64
70
  await (0, UtilitiesPage_1.navigateToApp)(page, 'tasklist');
65
71
  await taskListLoginPage.login('demo', 'demo');
66
- await taskPanelPage.openTask('User_Task_Editing_Variables');
72
+ await taskPanelPage.taskListBannerIsVisible();
73
+ await (0, UtilitiesPage_1.openUserTaskOfProcessInstance)(page, request, variableEditingInstanceKey);
67
74
  await taskDetailsPage.clickAssignToMeButton();
68
75
  await (0, test_1.expect)(page.getByText('Assigning...')).not.toBeVisible({
69
76
  timeout: 90000,
@@ -138,10 +145,10 @@ c8Run_8_10_1.test.describe('HTO User Flow Tests', () => {
138
145
  // The shadcn task panel no longer renders the active filter as a
139
146
  // heading (filterBy asserts it on the Filters trigger instead), so
140
147
  // 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 });
148
+ // also covers the indexing lag before the reload below. The list is
149
+ // virtualised, so this has to page through it rather than wait on a
150
+ // locator that is simply not rendered yet.
151
+ await taskPanelPage.assertTaskVisible('priorityTest4');
145
152
  await page.reload();
146
153
  await (0, sleep_1.sleep)(1000);
147
154
  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.1169",
3
+ "version": "0.0.1171",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",