@camunda/e2e-test-suite 0.0.1191 → 0.0.1193

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.
@@ -23,31 +23,48 @@ class OCIdentityHomePage {
23
23
  this.adminBanner = page.getByRole('link', {
24
24
  name: 'Camunda Admin',
25
25
  });
26
+ // The same redesign that renamed the logo link moved the nav out of the
27
+ // header: AppHeaderV2 renders an AppSidebar, a <nav aria-label="Main
28
+ // navigation">, so nothing is left under the old
29
+ // banner/aria-label="Admin" scope these tabs were written against. Both
30
+ // shells are matched, the way pages/SM-8.10/OCIdentityHomePage.ts already
31
+ // does, because the design-system rollout is still in flight.
26
32
  this.groupsTab = page
33
+ .getByRole('navigation', { name: 'Main navigation' })
34
+ .getByRole('link', { name: 'Groups', exact: true })
35
+ .or(page
27
36
  .getByRole('banner')
28
37
  .getByLabel('Admin')
29
38
  .locator('a')
30
- .filter({ hasText: 'Groups' });
39
+ .filter({ hasText: 'Groups' }))
40
+ .first();
31
41
  this.rolesTab = page
42
+ .getByRole('navigation', { name: 'Main navigation' })
43
+ .getByRole('link', { name: 'Roles', exact: true })
44
+ .or(page
32
45
  .getByRole('banner')
33
46
  .getByLabel('Admin')
34
47
  .locator('a')
35
- .filter({ hasText: 'Roles' });
48
+ .filter({ hasText: 'Roles' }))
49
+ .first();
36
50
  this.authorizationsTab = page
51
+ .getByRole('navigation', { name: 'Main navigation' })
52
+ .getByRole('link', { name: 'Authorizations', exact: true })
53
+ .or(page
37
54
  .getByRole('banner')
38
55
  .getByLabel('Admin')
39
56
  .locator('a')
40
- .filter({ hasText: /^Authorizations$/ });
57
+ .filter({ hasText: /^Authorizations$/ }))
58
+ .first();
41
59
  this.clusterVariablesTab = page
42
- .getByRole('banner')
43
- .getByLabel('Admin')
44
- .locator('a')
45
- .filter({ hasText: /Cluster variables/i })
60
+ .getByRole('navigation', { name: 'Main navigation' })
61
+ .getByRole('link', { name: 'Cluster variables', exact: true })
46
62
  .or(page
47
63
  .getByRole('banner')
48
64
  .getByLabel('Admin')
49
65
  .locator('a')
50
- .filter({ hasText: /Variables/i }));
66
+ .filter({ hasText: /Cluster variables/i }))
67
+ .first();
51
68
  this.licenseKeyTagNonProduction = page
52
69
  .getByText('Non-production license')
53
70
  .first();
@@ -2,6 +2,8 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class TaskPanelPage {
3
3
  private page;
4
4
  readonly availableTasks: Locator;
5
+ readonly filterPanelExpandButton: Locator;
6
+ readonly filterPanelCollapseButton: Locator;
5
7
  readonly filterSelectButton: Locator;
6
8
  readonly taskListPageBanner: Locator;
7
9
  readonly processesPageTab: Locator;
@@ -12,6 +14,9 @@ declare class TaskPanelPage {
12
14
  taskInList(name: string): Locator;
13
15
  openTask(name: string, totalTimeout?: number): Promise<void>;
14
16
  filterBy(option: 'All open tasks' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
17
+ private openFilterControls;
18
+ private collapseFilterPanel;
19
+ private static appliedFilter;
15
20
  taskCount(name: string): Promise<number>;
16
21
  clickTasksTab(): Promise<void>;
17
22
  scrollToLastTask(name: string): Promise<void>;
@@ -8,6 +8,8 @@ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLoc
8
8
  class TaskPanelPage {
9
9
  page;
10
10
  availableTasks;
11
+ filterPanelExpandButton;
12
+ filterPanelCollapseButton;
11
13
  filterSelectButton;
12
14
  taskListPageBanner;
13
15
  processesPageTab;
@@ -17,12 +19,19 @@ class TaskPanelPage {
17
19
  constructor(page) {
18
20
  this.page = page;
19
21
  this.availableTasks = page.getByTitle('Available tasks');
20
- // The old Carbon expandable filter sidebar (aria-controls="task-nav-bar",
21
- // "Expand to show filters") is gone after Tasklist's redesign. Filtering is
22
- // now a single dropdown-trigger button (id="filter-select") whose
23
- // accessible name is the static "Filters" aria-label; clicking it opens a
24
- // Radix DropdownMenu of filter options (matches the current locator in
25
- // camunda/camunda's own qa/c8-orchestration-cluster-e2e-test-suite).
22
+ // 8.10 still serves Tasklist on the Carbon shell, so filtering lives in
23
+ // the collapsible filter panel: the collapsed rail carries the expand
24
+ // control (aria-controls="task-nav-bar") and the expanded nav lists each
25
+ // filter as a link. Tasklist's design-system redesign -- a single
26
+ // "Filters" dropdown trigger opening a Radix menu -- landed on main
27
+ // (8.11) only: `stable/8.10` has no FilterSelect.tsx and its Filters.tsx
28
+ // still renders the Carbon panel against the /_carbon route tree, which
29
+ // is why targeting the redesign found no "Filters" button at all.
30
+ // Both shells are matched below because the SaaS design-system rollout is
31
+ // per app and still in flight (Admin has already flipped, hence the
32
+ // dual-shell locators in OCIdentityHomePage).
33
+ this.filterPanelExpandButton = page.locator('button[aria-controls="task-nav-bar"][aria-expanded="false"]');
34
+ this.filterPanelCollapseButton = page.locator('button[aria-controls="task-nav-bar"][aria-expanded="true"]');
26
35
  this.filterSelectButton = page.getByRole('button', {
27
36
  name: 'Filters',
28
37
  exact: true,
@@ -87,47 +96,100 @@ class TaskPanelPage {
87
96
  `${totalTimeout}ms: ${lastError}`);
88
97
  }
89
98
  async filterBy(option) {
90
- // Open the always-visible filter dropdown, then pick the option from the
91
- // Radix menu that appears (role="menuitem"). There is no separate
92
- // expand/collapse step like the old Carbon sidebar had.
93
- //
94
- // The trigger also carries the active filter's label, so it doubles as
95
- // the "selection applied" signal, replacing the heading the old Carbon
96
- // panel rendered for the active filter.
97
- //
98
- // TasksLayoutPage renders <Filters disabled={isPending} />, so a task
99
- // list refetch landing while the menu is open disables the trigger and
100
- // tears the menu down before Radix runs the item's onSelect: the menu
101
- // closes but the filter never changes. Re-open and pick again rather
102
- // than returning with the list still on the previous filter (same
103
- // bounded retry as pages/c8Run-8.10/TaskPanelPage.ts).
104
- const menuItem = this.page.getByRole('menuitem', {
105
- name: option,
106
- exact: true,
107
- });
108
- const triggerShowingOption = this.filterSelectButton.filter({
109
- hasText: new RegExp(`^${option}$`),
110
- });
99
+ // The filter is a link in the Carbon panel's expanded nav and a
100
+ // role="menuitem" in the redesigned dropdown, so match either. The
101
+ // Carbon links are scoped to #task-nav-bar so "Completed" cannot pick up
102
+ // a custom filter or a task row carrying the same words.
103
+ const filterOption = this.page
104
+ .locator('#task-nav-bar')
105
+ .getByRole('link', { name: option, exact: true })
106
+ .or(this.page.getByRole('menuitem', { name: option, exact: true }));
111
107
  const maxAttempts = 3;
112
108
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
113
- await (0, test_1.expect)(this.filterSelectButton).toBeEnabled({ timeout: 60000 });
114
- await this.filterSelectButton.click();
115
- await (0, test_1.expect)(menuItem).toBeVisible({ timeout: 10000 });
116
- await menuItem.click();
117
- const applied = await triggerShowingOption
118
- .waitFor({ state: 'visible', timeout: 15000 })
119
- .then(() => true)
120
- .catch(() => false);
109
+ await this.openFilterControls();
110
+ // Only the first attempt waits out a slow first render; later attempts
111
+ // pick from controls that have already rendered once, so three
112
+ // attempts at the full budget cannot outlast the test timeout.
113
+ const renderTimeout = attempt === 1 ? 60000 : 15000;
114
+ await (0, test_1.expect)(filterOption).toBeVisible({ timeout: renderTimeout });
115
+ await filterOption.click({ timeout: renderTimeout });
116
+ // The route -- not the panel -- is what proves the pick landed.
117
+ // Tasklist refetches the list every 5s behind a suspense boundary, and
118
+ // a poll landing on the click tears the controls down before the
119
+ // router navigates: they close but the filter never changes, and
120
+ // nothing downstream notices until a much later assertion reads the
121
+ // still-unfiltered list.
122
+ // Only a timeout means "the pick did not land", which is what the
123
+ // retry below is for. Anything else -- a closed page, a failed
124
+ // navigation -- is fatal, and swallowing it would surface three
125
+ // attempts later as the misleading "filter was not applied" error
126
+ // instead of its real cause.
127
+ let applied = true;
128
+ try {
129
+ await this.page.waitForURL(TaskPanelPage.appliedFilter(option), {
130
+ timeout: 15000,
131
+ });
132
+ }
133
+ catch (error) {
134
+ if (!(error instanceof test_1.errors.TimeoutError)) {
135
+ throw error;
136
+ }
137
+ applied = false;
138
+ }
121
139
  if (applied) {
140
+ await this.collapseFilterPanel();
122
141
  return;
123
142
  }
124
143
  console.warn(`Attempt ${attempt} to apply the "${option}" task filter did not ` +
125
144
  'take effect. Retrying...');
126
145
  await this.page.keyboard.press('Escape');
127
- await (0, test_1.expect)(this.filterSelectButton).toHaveAttribute('aria-expanded', 'false', { timeout: 10000 });
128
146
  }
129
147
  throw new Error(`Task filter "${option}" was not applied after ${maxAttempts} attempts`);
130
148
  }
149
+ // The Carbon panel renders either the collapsed rail (which carries the
150
+ // expand control) or the expanded nav -- never both -- and picking a filter
151
+ // leaves it expanded, so a retry must not click "expand" a second time. The
152
+ // redesigned dropdown instead has to be re-opened every attempt, because
153
+ // picking an option closes the menu.
154
+ async openFilterControls() {
155
+ const alreadyExpanded = await this.filterPanelCollapseButton
156
+ .waitFor({ state: 'visible', timeout: 2000 })
157
+ .then(() => true)
158
+ .catch(() => false);
159
+ if (alreadyExpanded) {
160
+ return;
161
+ }
162
+ await this.filterPanelExpandButton
163
+ .or(this.filterSelectButton)
164
+ .first()
165
+ .click({ timeout: 60000 });
166
+ }
167
+ // Picking a filter leaves the Carbon panel expanded, and it floats over the
168
+ // task list -- callers that go straight to a task row would be clicking
169
+ // through the overlay (pages/SM-8.10 collapses it from the specs for the
170
+ // same reason). The redesigned dropdown closes itself on select, so this is
171
+ // a probe rather than an unconditional click.
172
+ async collapseFilterPanel() {
173
+ const expanded = await this.filterPanelCollapseButton
174
+ .waitFor({ state: 'visible', timeout: 2000 })
175
+ .then(() => true)
176
+ .catch(() => false);
177
+ if (expanded) {
178
+ await this.filterPanelCollapseButton.click({ timeout: 30000 });
179
+ }
180
+ }
181
+ // Tasklist keeps the active filter in the `filter` search param, on both
182
+ // shells. "All open tasks" is the route default and is stripped from the
183
+ // URL, so it is asserted as the absence of the param.
184
+ static appliedFilter(option) {
185
+ const filterParam = {
186
+ Unassigned: 'unassigned',
187
+ 'Assigned to me': 'assigned-to-me',
188
+ Completed: 'completed',
189
+ };
190
+ const param = filterParam[option] ?? null;
191
+ return (url) => url.searchParams.get('filter') === param;
192
+ }
131
193
  async taskCount(name) {
132
194
  await (0, sleep_1.sleep)(20000);
133
195
  return this.availableTasks.getByText(name, { exact: true }).count();
@@ -16,9 +16,9 @@ declare class TaskPanelPage {
16
16
  filterBy(option: 'All open' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
17
17
  private expandFilterPanel;
18
18
  private static appliedFilterUrl;
19
+ clickCollapseFilter(): Promise<void>;
19
20
  scrollToLastTask(name: string): Promise<void>;
20
21
  scrollToFirstTask(name: string): Promise<void>;
21
- clickCollapseFilter(): Promise<void>;
22
22
  clickProcessesTab(): Promise<void>;
23
23
  taskCount(name: string, waitForStable?: boolean): Promise<number>;
24
24
  clickTasksTab(): Promise<void>;
@@ -76,35 +76,46 @@ class TaskPanelPage {
76
76
  // landing on the filter click tears the floating filter panel down before
77
77
  // the router navigates: the panel closes but the filter never changes.
78
78
  // Nothing downstream notices, so the test fails much later on an assertion
79
- // that reads the still-unfiltered list -- which is how a dropped click on
80
- // "Completed" surfaced as the completed-filter heading never appearing.
81
- // Confirm the route actually carries the filter, and re-pick if it does not.
79
+ // that reads the still-unfiltered list. Confirm the route actually
80
+ // carries the filter, and re-pick if it does not.
82
81
  const maxAttempts = 3;
82
+ let lastError;
83
83
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
84
- await this.expandFilterPanel();
85
- // Deliberately not `exact`: the panel spells the default filter
86
- // "All open tasks" while callers pass "All open".
87
- const filterLink = this.page.getByRole('link', { name: option });
88
- // Only the first attempt waits out a slow first render. Later attempts
89
- // re-pick from a panel that has already rendered once, so they stay
90
- // short -- three attempts at the full budget would outlast the
91
- // 12-minute test timeout and turn a dropped click into a timeout with
92
- // no retry spent.
93
- const renderTimeout = attempt === 1 ? 120000 : 15000;
94
- await (0, test_1.expect)(filterLink).toBeVisible({ timeout: renderTimeout });
95
- await filterLink.click({ timeout: renderTimeout });
96
- const applied = await this.page
97
- .waitForURL(TaskPanelPage.appliedFilterUrl(option), { timeout: 15000 })
98
- .then(() => true)
99
- .catch(() => false);
100
- if (applied) {
101
- return;
84
+ try {
85
+ await this.expandFilterPanel();
86
+ // Deliberately not `exact`: the panel spells the default filter
87
+ // "All open tasks" while callers pass "All open".
88
+ const filterLink = this.page.getByRole('link', { name: option });
89
+ // Only the first attempt waits out a slow first render. Later
90
+ // attempts re-pick from a panel that has already rendered once, so
91
+ // they stay short -- three attempts at the full budget would outlast
92
+ // the 12-minute test timeout and turn a dropped click into a timeout
93
+ // with no retry spent.
94
+ const renderTimeout = attempt === 1 ? 120000 : 15000;
95
+ await (0, test_1.expect)(filterLink).toBeVisible({ timeout: renderTimeout });
96
+ await filterLink.click({ timeout: renderTimeout });
97
+ const applied = await this.page
98
+ .waitForURL(TaskPanelPage.appliedFilterUrl(option), { timeout: 15000 })
99
+ .then(() => true)
100
+ .catch(() => false);
101
+ if (applied) {
102
+ return;
103
+ }
104
+ lastError = new Error(`Filter "${option}" was picked but the URL never carried it`);
105
+ }
106
+ catch (error) {
107
+ // Without this catch, a thrown assertion (the expand control or the
108
+ // filter link never appearing) propagated straight out of filterBy()
109
+ // on attempt 1, so attempts 2 and 3 -- and their shorter, faster-
110
+ // failing budget -- could never run. maxAttempts was dead code for
111
+ // every failure mode except a click that landed but didn't navigate.
112
+ lastError = error;
102
113
  }
103
114
  if (attempt < maxAttempts) {
104
115
  console.warn(`Attempt ${attempt} did not apply the "${option}" filter. Retrying...`);
105
116
  }
106
117
  }
107
- throw new Error(`Filter "${option}" was not applied after ${maxAttempts} attempts`);
118
+ throw new Error(`Filter "${option}" was not applied after ${maxAttempts} attempts: ${lastError}`);
108
119
  }
109
120
  // The filter panel renders either the collapsed rail (which carries the
110
121
  // expand control) or the expanded nav -- never both -- and picking a filter
@@ -133,27 +144,52 @@ class TaskPanelPage {
133
144
  ? /\/tasklist(?:\/[^?]*)?(?:\?(?!.*\bfilter=).*)?$/
134
145
  : new RegExp(`[?&]filter=${param}(?:&|$)`);
135
146
  }
147
+ async clickCollapseFilter() {
148
+ await this.collapseFilter.click({ timeout: 45000 });
149
+ }
136
150
  async scrollToLastTask(name) {
137
151
  await this.page.getByText(name).last().scrollIntoViewIfNeeded();
138
152
  }
139
153
  async scrollToFirstTask(name) {
140
154
  await this.page.getByText(name).first().scrollIntoViewIfNeeded();
141
155
  }
142
- async clickCollapseFilter() {
143
- await this.collapseFilter.click({ timeout: 45000 });
144
- }
145
156
  async clickProcessesTab() {
146
157
  await (0, test_1.expect)(this.processesPageTab).toBeVisible();
158
+ // Landing on Processes shows either the process search box or, on the
159
+ // first visit, the "Start your process on demand" onboarding dialog.
160
+ // `FirstTimeProcessWarning` renders the Processes content only after
161
+ // consent, so while that dialog is open the search box is not in the
162
+ // DOM at all -- confirming via the search box alone would never resolve
163
+ // on a first visit, and the retry would re-click Processes, which the
164
+ // dialog's modal overlay now covers. Treat either as proof the Processes
165
+ // view rendered; the caller dismisses the dialog.
166
+ const processesReady = this.page
167
+ .getByRole('searchbox', { name: 'Search processes' })
168
+ .or(this.page.getByRole('dialog', {
169
+ name: 'Start your process on demand',
170
+ }));
147
171
  const maxAttempts = 3;
172
+ let lastError;
148
173
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
149
- await this.processesPageTab.click();
174
+ try {
175
+ await this.processesPageTab.click();
176
+ }
177
+ catch (error) {
178
+ // The click belongs inside the try for the same reason filterBy()'s
179
+ // assertions do: the onboarding dialog's modal overlay covers the
180
+ // nav, so a click that lands on it throws once the repo's 10s action
181
+ // timeout expires. Letting that escape made maxAttempts dead code on
182
+ // exactly the path that needs it. Fall through to the landing probe
183
+ // below -- a click that threw may still have navigated.
184
+ lastError = error;
185
+ }
150
186
  // A newly-arrived task can bounce the app back to the Tasks tab right
151
187
  // as this click lands (nightly SM-8.10 run 31782502787: the click
152
188
  // itself succeeded but the app ended up back on Tasks with the new
153
189
  // task selected), so confirm the Processes view actually rendered
154
190
  // before returning instead of trusting the click alone.
155
- const landedOnProcesses = await this.page
156
- .getByRole('searchbox', { name: 'Search processes' })
191
+ const landedOnProcesses = await processesReady
192
+ .first()
157
193
  .waitFor({ state: 'visible', timeout: 5000 })
158
194
  .then(() => true)
159
195
  .catch(() => false);
@@ -164,7 +200,7 @@ class TaskPanelPage {
164
200
  console.warn(`Attempt ${attempt} landed back on Tasks instead of Processes. Retrying...`);
165
201
  }
166
202
  }
167
- throw new Error('Processes tab did not open after 3 attempts');
203
+ throw new Error(`Processes tab did not open after ${maxAttempts} attempts: ${lastError}`);
168
204
  }
169
205
  async taskCount(name, waitForStable = true) {
170
206
  // Removed waitForLoadState('networkidle') — Tasklist polls;
@@ -0,0 +1,71 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
4
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
5
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
6
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
7
+ xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
8
+ targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
9
+ modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
10
+ <bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
11
+ <bpmn:startEvent id="StartEvent_1">
12
+ <bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
13
+ </bpmn:startEvent>
14
+ <bpmn:serviceTask id="Activity_echo_meta"
15
+ zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
16
+ zeebe:modelerTemplateVersion="3">
17
+ <bpmn:extensionElements>
18
+ <zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
19
+ <zeebe:ioMapping>
20
+ <zeebe:input source="http" target="data.transport.type" />
21
+ <zeebe:input source="http://localhost:12001/mcp"
22
+ target="data.transport.http.url" />
23
+ <zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
24
+ <zeebe:input source="=false" target="data.options.clientCache" />
25
+ <zeebe:input source="standalone" target="data.connectorMode.type" />
26
+ <zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
27
+ <zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
28
+ <zeebe:input source="={exampleMetaKey: &#34;exampleMetaValue&#34;}"
29
+ target="data.connectorMode.meta" />
30
+ </zeebe:ioMapping>
31
+ <zeebe:taskHeaders>
32
+ <zeebe:header key="elementTemplateVersion" value="3" />
33
+ <zeebe:header key="elementTemplateId"
34
+ value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
35
+ <zeebe:header key="resultVariable" value="echoMetaResult" />
36
+ <zeebe:header key="retryBackoff" value="PT0S" />
37
+ </zeebe:taskHeaders>
38
+ </bpmn:extensionElements>
39
+ <bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
40
+ <bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
41
+ </bpmn:serviceTask>
42
+ <bpmn:endEvent id="Event_end">
43
+ <bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
44
+ </bpmn:endEvent>
45
+ <bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
46
+ targetRef="Activity_echo_meta" />
47
+ <bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
48
+ targetRef="Event_end" />
49
+ </bpmn:process>
50
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
51
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
52
+ <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
53
+ <dc:Bounds x="152" y="99" width="36" height="36" />
54
+ </bpmndi:BPMNShape>
55
+ <bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
56
+ <dc:Bounds x="250" y="77" width="100" height="80" />
57
+ </bpmndi:BPMNShape>
58
+ <bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
59
+ <dc:Bounds x="412" y="99" width="36" height="36" />
60
+ </bpmndi:BPMNShape>
61
+ <bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
62
+ <di:waypoint x="188" y="117" />
63
+ <di:waypoint x="250" y="117" />
64
+ </bpmndi:BPMNEdge>
65
+ <bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
66
+ <di:waypoint x="350" y="117" />
67
+ <di:waypoint x="412" y="117" />
68
+ </bpmndi:BPMNEdge>
69
+ </bpmndi:BPMNPlane>
70
+ </bpmndi:BPMNDiagram>
71
+ </bpmn:definitions>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const node_os_1 = require("node:os");
5
+ const node_path_1 = require("node:path");
6
+ const c8Run_8_10_1 = require("../../fixtures/c8Run-8.10");
7
+ const test_1 = require("@playwright/test");
8
+ const UtilitiesPage_1 = require("../../pages/c8Run-8.10/UtilitiesPage");
9
+ const _setup_1 = require("../../test-setup.js");
10
+ const apiHelpers_1 = require("../../utils/apiHelpers");
11
+ const zeebeClient_1 = require("../../utils/zeebeClient");
12
+ let processId;
13
+ let bpmnPath;
14
+ c8Run_8_10_1.test.beforeAll(async () => {
15
+ if (process.env.MCP_SERVER_AVAILABLE !== 'true') {
16
+ return;
17
+ }
18
+ await (0, apiHelpers_1.validateMcpServerHealth)();
19
+ await (0, apiHelpers_1.waitForConnectorsReady)(undefined, 300000, [
20
+ 'io.camunda.agenticai:mcpremoteclient:1',
21
+ ]);
22
+ processId = `mcp_remote_client_meta_${await (0, _setup_1.generateRandomStringAsync)(6)}`;
23
+ const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mcp-meta-'));
24
+ bpmnPath = (0, node_path_1.join)(tempDir, 'mcp_remote_client_meta.bpmn');
25
+ const bpmn = await (0, promises_1.readFile)('./resources/mcp_server/mcp_remote_client_meta.bpmn', 'utf8');
26
+ await (0, promises_1.writeFile)(bpmnPath, bpmn.replaceAll('mcp_remote_client_meta', processId));
27
+ await (0, zeebeClient_1.deploy)([bpmnPath]);
28
+ await (0, zeebeClient_1.createInstances)(processId, 1, 1);
29
+ });
30
+ c8Run_8_10_1.test.afterAll(async () => {
31
+ if (bpmnPath) {
32
+ await (0, promises_1.rm)((0, node_path_1.dirname)(bpmnPath), { recursive: true, force: true });
33
+ }
34
+ });
35
+ c8Run_8_10_1.test.describe('@tasklistV2 MCP Client connector _meta tests', () => {
36
+ c8Run_8_10_1.test.skip(process.env.MCP_SERVER_AVAILABLE !== 'true', 'MCP server is only available on Linux and macOS CI runners');
37
+ c8Run_8_10_1.test.beforeEach(async ({ page, operateLoginPage, operateHomePage }) => {
38
+ await (0, UtilitiesPage_1.navigateToApp)(page, 'operate');
39
+ await operateLoginPage.login('demo', 'demo');
40
+ await operateHomePage.operateBannerIsVisible();
41
+ });
42
+ c8Run_8_10_1.test.afterEach(async ({ page }, testInfo) => {
43
+ await (0, _setup_1.captureScreenshot)(page, testInfo);
44
+ await (0, _setup_1.captureFailureVideo)(page, testInfo);
45
+ });
46
+ (0, c8Run_8_10_1.test)('As a user I can forward MCP _meta and have it echoed back by the server', async ({ operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
47
+ c8Run_8_10_1.test.slow();
48
+ await c8Run_8_10_1.test.step('Navigate to completed process instance', async () => {
49
+ await operateHomePage.clickProcessesTab();
50
+ await operateProcessesPage.clickProcessCompletedCheckbox();
51
+ await operateProcessesPage.clickProcessInstanceLink(processId);
52
+ await operateProcessInstancePage.completedIconAssertion();
53
+ });
54
+ await c8Run_8_10_1.test.step('Verify echoMetaResult reflects the configured _meta', async () => {
55
+ await operateProcessInstancePage.verifyVariableJsonContent('echoMetaResult', (value) => {
56
+ const data = value;
57
+ (0, test_1.expect)(data.name).toBe('echoMeta');
58
+ (0, test_1.expect)(data.isError).toBe(false);
59
+ (0, test_1.expect)(data.content).toBeDefined();
60
+ (0, test_1.expect)(data.content.length).toBeGreaterThan(0);
61
+ (0, test_1.expect)(data.content[0].type).toBe('text');
62
+ // The mcp-test-server echoMeta tool returns the request `_meta`
63
+ // serialized verbatim as a JSON string. The MCP client forwards
64
+ // `data.connectorMode.meta` unmodified as the request `_meta`, so a
65
+ // successful round-trip proves end-to-end `_meta` propagation.
66
+ const echoed = JSON.parse(data.content[0].text);
67
+ (0, test_1.expect)(echoed).toMatchObject({
68
+ exampleMetaKey: 'exampleMetaValue',
69
+ });
70
+ });
71
+ });
72
+ });
73
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1191",
3
+ "version": "0.0.1193",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,71 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
4
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
5
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
6
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
7
+ xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
8
+ targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
9
+ modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
10
+ <bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
11
+ <bpmn:startEvent id="StartEvent_1">
12
+ <bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
13
+ </bpmn:startEvent>
14
+ <bpmn:serviceTask id="Activity_echo_meta"
15
+ zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
16
+ zeebe:modelerTemplateVersion="3">
17
+ <bpmn:extensionElements>
18
+ <zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
19
+ <zeebe:ioMapping>
20
+ <zeebe:input source="http" target="data.transport.type" />
21
+ <zeebe:input source="http://localhost:12001/mcp"
22
+ target="data.transport.http.url" />
23
+ <zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
24
+ <zeebe:input source="=false" target="data.options.clientCache" />
25
+ <zeebe:input source="standalone" target="data.connectorMode.type" />
26
+ <zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
27
+ <zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
28
+ <zeebe:input source="={exampleMetaKey: &#34;exampleMetaValue&#34;}"
29
+ target="data.connectorMode.meta" />
30
+ </zeebe:ioMapping>
31
+ <zeebe:taskHeaders>
32
+ <zeebe:header key="elementTemplateVersion" value="3" />
33
+ <zeebe:header key="elementTemplateId"
34
+ value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
35
+ <zeebe:header key="resultVariable" value="echoMetaResult" />
36
+ <zeebe:header key="retryBackoff" value="PT0S" />
37
+ </zeebe:taskHeaders>
38
+ </bpmn:extensionElements>
39
+ <bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
40
+ <bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
41
+ </bpmn:serviceTask>
42
+ <bpmn:endEvent id="Event_end">
43
+ <bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
44
+ </bpmn:endEvent>
45
+ <bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
46
+ targetRef="Activity_echo_meta" />
47
+ <bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
48
+ targetRef="Event_end" />
49
+ </bpmn:process>
50
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
51
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
52
+ <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
53
+ <dc:Bounds x="152" y="99" width="36" height="36" />
54
+ </bpmndi:BPMNShape>
55
+ <bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
56
+ <dc:Bounds x="250" y="77" width="100" height="80" />
57
+ </bpmndi:BPMNShape>
58
+ <bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
59
+ <dc:Bounds x="412" y="99" width="36" height="36" />
60
+ </bpmndi:BPMNShape>
61
+ <bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
62
+ <di:waypoint x="188" y="117" />
63
+ <di:waypoint x="250" y="117" />
64
+ </bpmndi:BPMNEdge>
65
+ <bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
66
+ <di:waypoint x="350" y="117" />
67
+ <di:waypoint x="412" y="117" />
68
+ </bpmndi:BPMNEdge>
69
+ </bpmndi:BPMNPlane>
70
+ </bpmndi:BPMNDiagram>
71
+ </bpmn:definitions>