@camunda/e2e-test-suite 0.0.1197 → 0.0.1199
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.
- package/dist/pages/8.10/AppsPage.d.ts +4 -0
- package/dist/pages/8.10/AppsPage.js +64 -29
- package/dist/pages/8.10/Authorization.d.ts +11 -1
- package/dist/pages/8.10/Authorization.js +308 -112
- package/dist/pages/8.10/OCIdentityAuthorizationsPage.js +5 -12
- package/dist/pages/8.10/OCIdentityClusterVariablesPage.d.ts +1 -0
- package/dist/pages/8.10/OCIdentityClusterVariablesPage.js +72 -20
- package/dist/pages/8.10/OCIdentityRolesPage.d.ts +1 -0
- package/dist/pages/8.10/OCIdentityRolesPage.js +21 -3
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +1 -1
- package/dist/resources/mcp_server_saas/mcp_variable_tools.bpmn +3 -1
- package/dist/tests/8.10/orchestration-cluster-mcp-server/mcp-user-task-tools.spec.d.ts +1 -0
- package/dist/tests/8.10/orchestration-cluster-mcp-server/mcp-user-task-tools.spec.js +138 -0
- package/dist/tests/8.10/orchestration-cluster-mcp-server/mcp-variable-tools.spec.d.ts +1 -0
- package/dist/tests/8.10/orchestration-cluster-mcp-server/mcp-variable-tools.spec.js +123 -0
- package/package.json +1 -1
- package/resources/mcp_server_saas/mcp_variable_tools.bpmn +3 -1
|
@@ -22,8 +22,12 @@ declare class AppsPage {
|
|
|
22
22
|
readonly tasklistMenuItem: Locator;
|
|
23
23
|
readonly optimizeMenuItem: Locator;
|
|
24
24
|
readonly adminMenuItem: Locator;
|
|
25
|
+
readonly operateMenuItem: Locator;
|
|
26
|
+
readonly consoleMenuItem: Locator;
|
|
27
|
+
readonly modelerMenuItem: Locator;
|
|
25
28
|
constructor(page: Page);
|
|
26
29
|
private reloadForRecovery;
|
|
30
|
+
private entryAppeared;
|
|
27
31
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
28
32
|
clickModeler(): Promise<void>;
|
|
29
33
|
private isModelerAuthTransit;
|
|
@@ -27,6 +27,9 @@ class AppsPage {
|
|
|
27
27
|
tasklistMenuItem;
|
|
28
28
|
optimizeMenuItem;
|
|
29
29
|
adminMenuItem;
|
|
30
|
+
operateMenuItem;
|
|
31
|
+
consoleMenuItem;
|
|
32
|
+
modelerMenuItem;
|
|
30
33
|
constructor(page) {
|
|
31
34
|
this.page = page;
|
|
32
35
|
this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
|
|
@@ -52,6 +55,15 @@ class AppsPage {
|
|
|
52
55
|
this.adminMenuItem = page.getByRole('menuitemradio', {
|
|
53
56
|
name: /^(Admin|Identity)$/, // Identity Should be removed after https://github.com/camunda/camunda/issues/46447 is fixed
|
|
54
57
|
});
|
|
58
|
+
// Admin is on nav V2 too now, so the switcher is also the way OUT of an
|
|
59
|
+
// app and not only the way in. Console and Modeler are org-level entries
|
|
60
|
+
// of the same dropdown (`CamundaAppKey` in @camunda/design-system covers
|
|
61
|
+
// operate | tasklist | optimize | admin | identity | modeler | console),
|
|
62
|
+
// so all three need the `menuitemradio` shape their link-role
|
|
63
|
+
// counterparts above no longer have.
|
|
64
|
+
this.operateMenuItem = page.getByRole('menuitemradio', { name: 'Operate' });
|
|
65
|
+
this.consoleMenuItem = page.getByRole('menuitemradio', { name: 'Console' });
|
|
66
|
+
this.modelerMenuItem = page.getByRole('menuitemradio', { name: 'Modeler' });
|
|
55
67
|
this.tasklistListItem = this.page
|
|
56
68
|
.getByRole('listitem')
|
|
57
69
|
.filter({ hasText: 'Tasklist' });
|
|
@@ -115,6 +127,16 @@ class AppsPage {
|
|
|
115
127
|
console.warn(`Could not reload while ${whileDoing}: ${error}`);
|
|
116
128
|
}
|
|
117
129
|
}
|
|
130
|
+
// Boolean probe for "did an apps-menu entry render?", with the same window
|
|
131
|
+
// the `expect(...).toBeVisible({timeout: 30000})` calls it replaces used —
|
|
132
|
+
// so a slow legacy flyout still gets its full 30s before any caller decides
|
|
133
|
+
// the entry is absent rather than merely late.
|
|
134
|
+
async entryAppeared(entry) {
|
|
135
|
+
return entry
|
|
136
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
137
|
+
.then(() => true)
|
|
138
|
+
.catch(() => false);
|
|
139
|
+
}
|
|
118
140
|
async clickCluster(component, name) {
|
|
119
141
|
// Cluster names are not unique in an org, so this app's sub-menu can list
|
|
120
142
|
// the same name twice; pick the first entry (same row the Console cluster
|
|
@@ -131,17 +153,25 @@ class AppsPage {
|
|
|
131
153
|
.getByText(/local_rate_limited/i)
|
|
132
154
|
.or(this.page.getByText(/Well, that's awkward/i))
|
|
133
155
|
.first();
|
|
156
|
+
// nav V2 breadcrumb switcher: Modeler is a `menuitemradio`, the legacy
|
|
157
|
+
// apps flyout renders it as a link. Resolve whichever this build shows.
|
|
158
|
+
const modelerEntry = this.modelerMenuItem.or(this.modelerLink).first();
|
|
134
159
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
135
160
|
try {
|
|
136
|
-
if (retries
|
|
137
|
-
await
|
|
138
|
-
await this.modelerLink.click();
|
|
161
|
+
if (retries > 0) {
|
|
162
|
+
await this.clickCamundaApps();
|
|
139
163
|
}
|
|
140
|
-
|
|
164
|
+
if (!(await this.entryAppeared(modelerEntry))) {
|
|
165
|
+
// Nothing to click, because the nav V2 breadcrumb switcher lists
|
|
166
|
+
// only the cluster's OWN webapps. Modeler is org-level, so unlike
|
|
167
|
+
// Operate it has no entry there at all. Re-enter through Console,
|
|
168
|
+
// whose apps flyout does list it — the same recovery the
|
|
169
|
+
// `open*WithRetry` helpers below use.
|
|
170
|
+
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
141
171
|
await this.clickCamundaApps();
|
|
142
|
-
await (0, test_1.expect)(
|
|
143
|
-
await this.modelerLink.click();
|
|
172
|
+
await (0, test_1.expect)(modelerEntry).toBeVisible({ timeout: 30000 });
|
|
144
173
|
}
|
|
174
|
+
await modelerEntry.click();
|
|
145
175
|
// After clicking, the browser is redirected through
|
|
146
176
|
// modeler.ultrawombat.com/login which can return 429
|
|
147
177
|
// (`local_rate_limited`) or a generic "Well, that's awkward" error
|
|
@@ -473,24 +503,21 @@ class AppsPage {
|
|
|
473
503
|
const maxRetries = 3;
|
|
474
504
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
475
505
|
try {
|
|
476
|
-
if (retries
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
506
|
+
if (retries > 0) {
|
|
507
|
+
await this.clickCamundaApps();
|
|
508
|
+
}
|
|
509
|
+
if (await this.operateMenuItem.isVisible({ timeout: 5000 })) {
|
|
510
|
+
// nav V2 breadcrumb switcher: apps are menuitemradio entries that
|
|
511
|
+
// hop to the same cluster's app directly, no cluster sub-select.
|
|
512
|
+
await this.operateMenuItem.click({ timeout: 10000 });
|
|
513
|
+
}
|
|
514
|
+
else if (await this.operateLink.isVisible({ timeout: 5000 })) {
|
|
515
|
+
//Single Cluster
|
|
516
|
+
await this.operateLink.click({ timeout: 10000 });
|
|
485
517
|
}
|
|
486
518
|
else {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
await this.operateLink.click({ timeout: 10000 });
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
await this.doClickClusterInOperate(clusterName);
|
|
493
|
-
}
|
|
519
|
+
//Multiple Cluster
|
|
520
|
+
await this.doClickClusterInOperate(clusterName);
|
|
494
521
|
}
|
|
495
522
|
return;
|
|
496
523
|
}
|
|
@@ -562,17 +589,25 @@ class AppsPage {
|
|
|
562
589
|
}
|
|
563
590
|
async clickConsoleLink() {
|
|
564
591
|
const maxRetries = 3;
|
|
592
|
+
// nav V2 breadcrumb switcher: Console is a `menuitemradio`, the legacy
|
|
593
|
+
// apps flyout renders it as a link. Resolve whichever this build shows.
|
|
594
|
+
const consoleEntry = this.consoleMenuItem.or(this.consoleLink).first();
|
|
565
595
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
566
596
|
try {
|
|
567
|
-
if (retries
|
|
568
|
-
await (0, test_1.expect)(this.consoleLink).toBeVisible({ timeout: 30000 });
|
|
569
|
-
await this.consoleLink.click();
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
597
|
+
if (retries > 0) {
|
|
572
598
|
await this.clickCamundaApps();
|
|
573
|
-
await (0, test_1.expect)(this.consoleLink).toBeVisible({ timeout: 30000 });
|
|
574
|
-
await this.consoleLink.click();
|
|
575
599
|
}
|
|
600
|
+
if (await this.entryAppeared(consoleEntry)) {
|
|
601
|
+
await consoleEntry.click();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// Nothing to click, because the nav V2 breadcrumb switcher lists only
|
|
605
|
+
// the cluster's OWN webapps (Operate, Tasklist, Optimize, Admin) —
|
|
606
|
+
// which is why `clickOperate` resolves from Admin through
|
|
607
|
+
// `operateMenuItem` and this does not. Console is org-level and has no
|
|
608
|
+
// entry there, but it IS what `baseURL` points at, so navigate to it
|
|
609
|
+
// directly rather than re-opening a menu that will never list it.
|
|
610
|
+
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
576
611
|
return;
|
|
577
612
|
}
|
|
578
613
|
catch (error) {
|
|
@@ -2,13 +2,16 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class Authorization {
|
|
3
3
|
readonly page: Page;
|
|
4
4
|
protected readonly selectResourceTypeTab: (name: string) => Locator;
|
|
5
|
+
protected readonly resourceTypeFilterCombobox: Locator;
|
|
5
6
|
protected readonly createAuthorizationModal: Locator;
|
|
6
7
|
protected readonly createAuthorizationButton: Locator;
|
|
7
8
|
protected readonly createAuthorizationOwnerTypeCombobox: Locator;
|
|
8
9
|
protected readonly createAuthorizationOwnerTypeOption: (name: string) => Locator;
|
|
9
10
|
protected readonly createAuthorizationOwnerComboBox: Locator;
|
|
10
11
|
protected readonly createAuthorizationOwnerSearchInput: Locator;
|
|
11
|
-
protected readonly
|
|
12
|
+
protected readonly createAuthorizationOwnerSearchOption: (name: string) => Locator;
|
|
13
|
+
protected readonly createAuthorizationOwnerCommandInput: Locator;
|
|
14
|
+
protected readonly createAuthorizationOwnerCommandOptions: Locator;
|
|
12
15
|
protected readonly createAuthorizationOwnerOption: (name: string) => Locator;
|
|
13
16
|
protected readonly createAuthorizationResourceTypeDropdown: Locator;
|
|
14
17
|
protected readonly createAuthorizationResourceTypeOption: (name: string) => Locator;
|
|
@@ -19,8 +22,11 @@ declare class Authorization {
|
|
|
19
22
|
protected readonly deleteAuthorizationButton: (name: string) => Locator;
|
|
20
23
|
protected readonly deleteAuthorizationModal: Locator;
|
|
21
24
|
protected readonly deleteAuthorizationModalDeleteButton: Locator;
|
|
25
|
+
protected readonly tableFilter: Locator;
|
|
22
26
|
constructor(page: Page);
|
|
27
|
+
protected selectResourceType(resourceType: string, timeout?: number): Promise<void>;
|
|
23
28
|
assertAuthorization(auth: string): Promise<void>;
|
|
29
|
+
private selectAuthorizationOwner;
|
|
24
30
|
createAuthorization(authorization: {
|
|
25
31
|
ownerType: string;
|
|
26
32
|
ownerId: string;
|
|
@@ -29,6 +35,10 @@ declare class Authorization {
|
|
|
29
35
|
accessPermissions: string[];
|
|
30
36
|
}): Promise<void>;
|
|
31
37
|
selectResourceTypeFromDialog(resourceType: string): Promise<void>;
|
|
38
|
+
private checkAccessPermission;
|
|
39
|
+
private authorizationRow;
|
|
40
|
+
private expectAuthorizationListed;
|
|
41
|
+
private filterAuthorizationsByOwner;
|
|
32
42
|
deleteAuthorization(resourceId: string, resourceType: string): Promise<void>;
|
|
33
43
|
}
|
|
34
44
|
export { Authorization };
|
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Authorization = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
-
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
6
5
|
const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
|
|
6
|
+
const findLocatorWithPagination_1 = require("../../utils/assertionHelpers/findLocatorWithPagination");
|
|
7
|
+
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
7
8
|
class Authorization {
|
|
8
9
|
page;
|
|
9
10
|
selectResourceTypeTab;
|
|
11
|
+
resourceTypeFilterCombobox;
|
|
10
12
|
createAuthorizationModal;
|
|
11
13
|
createAuthorizationButton;
|
|
12
14
|
createAuthorizationOwnerTypeCombobox;
|
|
13
15
|
createAuthorizationOwnerTypeOption;
|
|
14
16
|
createAuthorizationOwnerComboBox;
|
|
15
17
|
createAuthorizationOwnerSearchInput;
|
|
16
|
-
|
|
18
|
+
createAuthorizationOwnerSearchOption;
|
|
19
|
+
createAuthorizationOwnerCommandInput;
|
|
20
|
+
createAuthorizationOwnerCommandOptions;
|
|
17
21
|
createAuthorizationOwnerOption;
|
|
18
22
|
createAuthorizationResourceTypeDropdown;
|
|
19
23
|
createAuthorizationResourceTypeOption;
|
|
@@ -24,9 +28,20 @@ class Authorization {
|
|
|
24
28
|
deleteAuthorizationButton;
|
|
25
29
|
deleteAuthorizationModal;
|
|
26
30
|
deleteAuthorizationModalDeleteButton;
|
|
31
|
+
tableFilter;
|
|
27
32
|
constructor(page) {
|
|
28
33
|
this.page = page;
|
|
29
34
|
this.selectResourceTypeTab = (resourceType) => page.getByRole('tab', { name: resourceType });
|
|
35
|
+
// The Admin's Authorizations page moved onto the new design-system shell,
|
|
36
|
+
// which replaces the per-resource-type Carbon tabs with a single filter
|
|
37
|
+
// combobox in a left "Filter" card (identity/client ListV2.tsx). The
|
|
38
|
+
// combobox's accessible name was renamed from "Authorization type" to
|
|
39
|
+
// "Resource type" in camunda/camunda commit 377b0e3; match either so both
|
|
40
|
+
// image versions resolve.
|
|
41
|
+
this.resourceTypeFilterCombobox = page
|
|
42
|
+
.getByRole('combobox', { name: 'Resource type' })
|
|
43
|
+
.or(page.getByRole('combobox', { name: 'Authorization type' }))
|
|
44
|
+
.first();
|
|
30
45
|
this.authorizationsList = page.getByRole('table');
|
|
31
46
|
this.createAuthorizationButton = page.getByRole('button', {
|
|
32
47
|
name: /create authorization/i,
|
|
@@ -38,63 +53,84 @@ class Authorization {
|
|
|
38
53
|
this.createAuthorizationModal.getByRole('combobox', {
|
|
39
54
|
name: 'Owner type',
|
|
40
55
|
});
|
|
41
|
-
|
|
56
|
+
// The design-system Select portals its listbox to the end of <body>, so
|
|
57
|
+
// the options are siblings of the dialog rather than descendants, and
|
|
58
|
+
// while the listbox is open Radix marks everything outside it aria-hidden
|
|
59
|
+
// — which takes the dialog itself out of the accessibility tree. Query the
|
|
60
|
+
// options from the page root so both placements resolve.
|
|
61
|
+
this.createAuthorizationOwnerTypeOption = (name) => this.page
|
|
62
|
+
.getByRole('option', {
|
|
42
63
|
name,
|
|
43
64
|
exact: true,
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
name: 'Owner',
|
|
63
|
-
exact: true,
|
|
64
|
-
});
|
|
65
|
-
this.createAuthorizationOwnerComboBox =
|
|
66
|
-
this.createAuthorizationOwnerSearchInput
|
|
67
|
-
.or(this.createAuthorizationOwnerLegacyComboBox)
|
|
68
|
-
.first();
|
|
65
|
+
})
|
|
66
|
+
.first();
|
|
67
|
+
// On the design-system build the closed Owner control is a combobox-role
|
|
68
|
+
// trigger rendered by EntitySearchSingleSelect. Its <Label htmlFor> does
|
|
69
|
+
// not always name the focusable trigger, so the product team's own suite
|
|
70
|
+
// identifies it by the placeholder it paints as static text instead
|
|
71
|
+
// (camunda/camunda qa/c8-orchestration-cluster-e2e-test-suite/pages/
|
|
72
|
+
// IdentityAuthorizationsPage.ts). Match either, and keep `exact` on the
|
|
73
|
+
// label form so it cannot collide with "Owner type".
|
|
74
|
+
this.createAuthorizationOwnerComboBox = this.createAuthorizationModal
|
|
75
|
+
.getByRole('combobox', {
|
|
76
|
+
name: 'Owner',
|
|
77
|
+
exact: true,
|
|
78
|
+
})
|
|
79
|
+
.or(this.createAuthorizationModal
|
|
80
|
+
.getByRole('combobox')
|
|
81
|
+
.filter({ hasText: 'Search by owner ID' }))
|
|
82
|
+
.first();
|
|
69
83
|
this.createAuthorizationOwnerOption = (name) => this.createAuthorizationModal.getByRole('option', {
|
|
70
84
|
name,
|
|
71
85
|
exact: true,
|
|
72
86
|
});
|
|
87
|
+
// Every owner type now renders a server-backed search dropdown instead of
|
|
88
|
+
// the combobox that listed all owners up front. On the entity-search build
|
|
89
|
+
// that dropdown is a Carbon Search inside a ListBox, so it has no combobox
|
|
90
|
+
// role and its results are list-box menu items. The design-system build
|
|
91
|
+
// renders the same field as a cmdk command palette whose results are
|
|
92
|
+
// role=option items in a popover portalled out of the dialog.
|
|
93
|
+
this.createAuthorizationOwnerSearchInput =
|
|
94
|
+
this.createAuthorizationModal.getByPlaceholder('Search by owner ID');
|
|
95
|
+
this.createAuthorizationOwnerSearchOption = (name) => this.page.getByRole('option').filter({ hasText: name }).first();
|
|
96
|
+
// In the design-system build the closed Owner control is a combobox-role
|
|
97
|
+
// trigger that paints "Search by owner ID" as static text (no placeholder
|
|
98
|
+
// attribute), and the cmdk search box plus its result listbox only mount
|
|
99
|
+
// in a popover once the trigger is opened. That popover is portalled out
|
|
100
|
+
// of the dialog, and while it is open Radix marks the dialog aria-hidden,
|
|
101
|
+
// so both must be queried from the page root.
|
|
102
|
+
this.createAuthorizationOwnerCommandInput = this.page
|
|
103
|
+
.locator('[data-slot="command-input"]')
|
|
104
|
+
.first();
|
|
105
|
+
this.createAuthorizationOwnerCommandOptions = this.page
|
|
106
|
+
.getByRole('listbox')
|
|
107
|
+
.getByRole('option');
|
|
73
108
|
this.createAuthorizationResourceTypeDropdown =
|
|
74
109
|
this.createAuthorizationModal.getByRole('combobox', {
|
|
75
110
|
name: 'Resource type',
|
|
76
111
|
});
|
|
77
|
-
this.createAuthorizationResourceTypeOption = (name) => this.
|
|
112
|
+
this.createAuthorizationResourceTypeOption = (name) => this.page
|
|
113
|
+
.getByRole('option', {
|
|
78
114
|
name,
|
|
79
115
|
exact: true,
|
|
80
|
-
})
|
|
116
|
+
})
|
|
117
|
+
.first();
|
|
81
118
|
this.createAuthorizationResourceIdField =
|
|
82
119
|
this.createAuthorizationModal.getByRole('textbox', {
|
|
83
120
|
name: 'Resource ID',
|
|
84
121
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
// Carbon wired its checkbox input and label together the same way the
|
|
123
|
+
// design-system Checkbox does (`id={permission}` / `htmlFor={permission}`),
|
|
124
|
+
// and on both builds the input itself is not the click target — the label
|
|
125
|
+
// is.
|
|
126
|
+
this.createAuthorizationAccessPermission = (name) => this.createAuthorizationModal.locator(`label[for="${name}"]`);
|
|
88
127
|
this.createAuthorizationSubmitButton =
|
|
89
128
|
this.createAuthorizationModal.getByRole('button', {
|
|
90
129
|
name: 'Create authorization',
|
|
91
130
|
});
|
|
92
|
-
this.authorizationsList = page.getByRole('table');
|
|
93
131
|
this.deleteAuthorizationButton = (name) => this.authorizationsList
|
|
94
|
-
.getByRole('row')
|
|
95
|
-
.
|
|
96
|
-
.first()
|
|
97
|
-
.getByRole('button', { name: 'danger Delete' });
|
|
132
|
+
.getByRole('row', { name })
|
|
133
|
+
.getByRole('button', { name: /^delete/i });
|
|
98
134
|
this.deleteAuthorizationModal = page.getByRole('dialog', {
|
|
99
135
|
name: /delete authorization/i,
|
|
100
136
|
});
|
|
@@ -102,6 +138,30 @@ class Authorization {
|
|
|
102
138
|
this.deleteAuthorizationModal.getByRole('button', {
|
|
103
139
|
name: /delete authorization/i,
|
|
104
140
|
});
|
|
141
|
+
// Carbon labelled the table toolbar search "Filter table"; the
|
|
142
|
+
// design-system EntityList exposes the placeholder as the accessible name
|
|
143
|
+
// instead. Match either.
|
|
144
|
+
this.tableFilter = page
|
|
145
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
146
|
+
.or(page.getByRole('searchbox', { name: 'Search by owner ID' }))
|
|
147
|
+
.first();
|
|
148
|
+
}
|
|
149
|
+
// Select the resource type through whichever control this build renders:
|
|
150
|
+
// click the Carbon tab, or open the design-system filter combobox and pick
|
|
151
|
+
// the option.
|
|
152
|
+
async selectResourceType(resourceType, timeout = 60000) {
|
|
153
|
+
const tab = this.selectResourceTypeTab(resourceType);
|
|
154
|
+
await (0, test_1.expect)(tab.or(this.resourceTypeFilterCombobox)).toBeVisible({
|
|
155
|
+
timeout,
|
|
156
|
+
});
|
|
157
|
+
if (await tab.isVisible()) {
|
|
158
|
+
await tab.click();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await this.resourceTypeFilterCombobox.click();
|
|
162
|
+
await this.page
|
|
163
|
+
.getByRole('option', { name: resourceType, exact: true })
|
|
164
|
+
.click({ timeout: Math.min(timeout, 30000) });
|
|
105
165
|
}
|
|
106
166
|
// The authorizations table paginates, and the RBA clusters these flows run
|
|
107
167
|
// against are long-lived: every night's run leaves more process-definition
|
|
@@ -114,100 +174,236 @@ class Authorization {
|
|
|
114
174
|
async assertAuthorization(auth) {
|
|
115
175
|
await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, this.authorizationsList.getByRole('row').filter({ hasText: auth }).first());
|
|
116
176
|
}
|
|
177
|
+
// Released 8.10 alpha images still render the Carbon combobox that lists
|
|
178
|
+
// every owner, so accept whichever of the three owner fields this build
|
|
179
|
+
// shows: the Carbon search dropdown, the Carbon combobox that lists every
|
|
180
|
+
// owner inline, or the design-system combobox whose cmdk popover only loads
|
|
181
|
+
// results once the owner ID is typed.
|
|
182
|
+
async selectAuthorizationOwner(ownerId) {
|
|
183
|
+
await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput.or(this.createAuthorizationOwnerComboBox)).toBeVisible({ timeout: 60000 });
|
|
184
|
+
if (await this.createAuthorizationOwnerSearchInput.isVisible()) {
|
|
185
|
+
await this.createAuthorizationOwnerSearchInput.fill(ownerId);
|
|
186
|
+
const ownerOption = this.createAuthorizationOwnerSearchOption(ownerId);
|
|
187
|
+
// The search is debounced and served by the API, so a just-created
|
|
188
|
+
// owner can take a while to surface.
|
|
189
|
+
await (0, test_1.expect)(ownerOption).toBeVisible({ timeout: 60000 });
|
|
190
|
+
await ownerOption.click({ timeout: 30000 });
|
|
191
|
+
// Picking an owner replaces the search input with a tag holding the
|
|
192
|
+
// selection, which is how we know the choice was committed.
|
|
193
|
+
await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput).toBeHidden({
|
|
194
|
+
timeout: 30000,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await this.createAuthorizationOwnerComboBox.click();
|
|
199
|
+
const commandInput = this.createAuthorizationOwnerCommandInput;
|
|
200
|
+
const inlineOwnerOption = this.createAuthorizationOwnerOption(ownerId);
|
|
201
|
+
await (0, test_1.expect)(commandInput.or(inlineOwnerOption).first()).toBeVisible({
|
|
202
|
+
timeout: 30000,
|
|
203
|
+
});
|
|
204
|
+
if (await commandInput.isVisible()) {
|
|
205
|
+
await commandInput.fill(ownerId);
|
|
206
|
+
// The result list is served by the API and debounced, and each option is
|
|
207
|
+
// labelled "<owner id> — <name>" rather than the bare ID
|
|
208
|
+
// (identity/client RoleSelection.tsx `itemLabel`), so narrow the search
|
|
209
|
+
// to the single row this unique owner ID resolves to instead of matching
|
|
210
|
+
// on the label text.
|
|
211
|
+
const ownerOptions = this.createAuthorizationOwnerCommandOptions;
|
|
212
|
+
await (0, test_1.expect)(ownerOptions).toHaveCount(1, { timeout: 60000 });
|
|
213
|
+
await ownerOptions.first().click({ timeout: 30000, force: true });
|
|
214
|
+
// Committing the choice closes the popover, which is what hands the
|
|
215
|
+
// dialog back to the accessibility tree for the fields that follow.
|
|
216
|
+
await (0, test_1.expect)(commandInput).toBeHidden({ timeout: 30000 });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await (0, test_1.expect)(inlineOwnerOption).toBeVisible({ timeout: 30000 });
|
|
220
|
+
await inlineOwnerOption.click({ timeout: 30000 });
|
|
221
|
+
}
|
|
117
222
|
async createAuthorization(authorization) {
|
|
118
223
|
await this.createAuthorizationButton.click();
|
|
119
224
|
await (0, test_1.expect)(this.createAuthorizationModal).toBeVisible();
|
|
120
225
|
await this.createAuthorizationOwnerTypeCombobox.click();
|
|
121
226
|
await this.createAuthorizationOwnerTypeOption(authorization.ownerType).click();
|
|
122
|
-
|
|
123
|
-
// entity-search build that swap is query-driven, so it can lag well past
|
|
124
|
-
// 30s under CI load — the product team's own e2e suite raised an
|
|
125
|
-
// analogous wait on the same widget from 20s to 60s (camunda/camunda#61353).
|
|
126
|
-
await (0, test_1.expect)(this.createAuthorizationOwnerComboBox).toBeVisible({
|
|
127
|
-
timeout: 60000,
|
|
128
|
-
});
|
|
129
|
-
const usesEntitySearch = await this.createAuthorizationOwnerSearchInput.isVisible();
|
|
130
|
-
const ownerField = usesEntitySearch
|
|
131
|
-
? this.createAuthorizationOwnerSearchInput
|
|
132
|
-
: this.createAuthorizationOwnerLegacyComboBox;
|
|
133
|
-
if (!usesEntitySearch) {
|
|
134
|
-
// The legacy Carbon ComboBox renders its menu only once opened; the
|
|
135
|
-
// entity-search input renders results off the typed value alone.
|
|
136
|
-
await ownerField.click();
|
|
137
|
-
}
|
|
138
|
-
await ownerField.fill(authorization.ownerId);
|
|
139
|
-
// The dropdown renders results only while the search box holds text, and
|
|
140
|
-
// the list is fetched from the server on a debounce rather than filtered
|
|
141
|
-
// client-side, so the matching entry appears a beat later. Clicking the
|
|
142
|
-
// Carbon menu item is what commits the selection: DropdownSearch wires
|
|
143
|
-
// `onSelect` to `ListBox.MenuItem`'s onClick
|
|
144
|
-
// (identity/client/src/components/form/DropdownSearch.tsx), and that is
|
|
145
|
-
// the only path that promotes an entry into the form's owner value.
|
|
146
|
-
const ownerMenuItem = this.createAuthorizationModal
|
|
147
|
-
.locator('.cds--list-box__menu-item')
|
|
148
|
-
.filter({ hasText: authorization.ownerId })
|
|
149
|
-
.first();
|
|
150
|
-
await (0, test_1.expect)(ownerMenuItem).toBeVisible({ timeout: 60000 });
|
|
151
|
-
await ownerMenuItem.click({ timeout: 20000 });
|
|
152
|
-
// On the entity-search build a committed selection swaps the search box
|
|
153
|
-
// for a filter Tag (EntitySearchSingleSelect renders one or the other), so
|
|
154
|
-
// the box going away is how we know the owner actually landed on the form
|
|
155
|
-
// rather than the click being swallowed. The legacy ComboBox keeps its
|
|
156
|
-
// input and shows the selection as the input value, so this check only
|
|
157
|
-
// applies to the entity-search shape.
|
|
158
|
-
if (usesEntitySearch) {
|
|
159
|
-
await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput).toBeHidden({
|
|
160
|
-
timeout: 15000,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
227
|
+
await this.selectAuthorizationOwner(authorization.ownerId);
|
|
163
228
|
await this.selectResourceTypeFromDialog(authorization.resourceType);
|
|
164
229
|
await this.createAuthorizationResourceIdField.fill(authorization.resourceId);
|
|
165
230
|
for (const permission of authorization.accessPermissions) {
|
|
166
|
-
await this.
|
|
167
|
-
force: true,
|
|
168
|
-
});
|
|
231
|
+
await this.checkAccessPermission(permission);
|
|
169
232
|
}
|
|
170
|
-
await this.createAuthorizationSubmitButton.
|
|
171
|
-
await
|
|
233
|
+
await (0, submitModalWithRetry_1.submitModalWithRetry)(this.createAuthorizationSubmitButton, this.createAuthorizationModal);
|
|
234
|
+
await this.selectResourceType(authorization.resourceType);
|
|
235
|
+
await this.expectAuthorizationListed(authorization, this.authorizationRow(authorization));
|
|
236
|
+
}
|
|
237
|
+
// The Resource type field is pre-selected from the list's current resource
|
|
238
|
+
// type and rendered `disabled` on the design-system build
|
|
239
|
+
// (identity/client/src/pages/authorizations/modalsV2/add-modal/AddModal.tsx
|
|
240
|
+
// passes a literal `disabled`), so there is nothing to pick. Only open the
|
|
241
|
+
// dropdown when the field is still editable.
|
|
242
|
+
async selectResourceTypeFromDialog(resourceType) {
|
|
243
|
+
await (0, test_1.expect)(this.createAuthorizationResourceTypeDropdown).toBeVisible({
|
|
172
244
|
timeout: 30000,
|
|
173
245
|
});
|
|
174
|
-
await this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await (
|
|
179
|
-
|
|
246
|
+
if (!(await this.createAuthorizationResourceTypeDropdown.isEnabled())) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await this.createAuthorizationResourceTypeDropdown.click();
|
|
250
|
+
await this.createAuthorizationResourceTypeOption(resourceType).click({
|
|
251
|
+
timeout: 30000,
|
|
180
252
|
});
|
|
181
253
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
254
|
+
// The permission control is a label + hidden input pair on both builds, so
|
|
255
|
+
// the label is the click target while the input carries the checked state.
|
|
256
|
+
// The design-system Checkbox re-renders on every toggle, which can swallow a
|
|
257
|
+
// click that lands mid-render, so confirm the state and re-click if needed.
|
|
258
|
+
async checkAccessPermission(permission) {
|
|
259
|
+
const permissionLabel = this.createAuthorizationAccessPermission(permission);
|
|
260
|
+
const permissionInput = this.createAuthorizationModal.locator(`input[id="${permission}"], [role="checkbox"][id="${permission}"]`);
|
|
261
|
+
await (0, test_1.expect)(permissionLabel).toBeVisible({ timeout: 30000 });
|
|
262
|
+
await permissionLabel.scrollIntoViewIfNeeded();
|
|
263
|
+
const maxAttempts = 3;
|
|
264
|
+
for (let attempt = 1; attempt < maxAttempts; attempt++) {
|
|
265
|
+
if (await permissionInput.isChecked()) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await permissionLabel.click({ force: true });
|
|
269
|
+
try {
|
|
270
|
+
await (0, test_1.expect)(permissionInput).toBeChecked({ timeout: 5000 });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// A click that landed mid-rerender is dropped: try again. The
|
|
275
|
+
// assertion after the loop is the one that reports a permission that
|
|
276
|
+
// never took.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!(await permissionInput.isChecked())) {
|
|
280
|
+
await permissionLabel.click({ force: true });
|
|
281
|
+
}
|
|
282
|
+
await (0, test_1.expect)(permissionInput).toBeChecked({ timeout: 10000 });
|
|
283
|
+
}
|
|
284
|
+
// The authorizations list caps every cell at 25 characters
|
|
285
|
+
// (`maxDisplayCellLength={25}` in camunda/camunda
|
|
286
|
+
// identity/client/src/pages/authorizations/AuthorizationsListV2.tsx), and on
|
|
287
|
+
// the design-system build `truncateCell` renders an over-long value as
|
|
288
|
+
// `<button aria-label="<full value>">{first 25 chars}…</button>` with the
|
|
289
|
+
// tooltip carrying the rest portalled out of the row. An owner ID longer
|
|
290
|
+
// than 25 characters - the RBA specs' 33-character
|
|
291
|
+
// `rbaSelectedProcessesPermission<xxx>` roles - is therefore absent from the
|
|
292
|
+
// row's text, so filtering rows on `<ownerId><resourceId>` cannot match it
|
|
293
|
+
// however long we wait. The full value survives in the cell's accessible
|
|
294
|
+
// name, so also accept a row that HAS a cell named exactly the owner ID.
|
|
295
|
+
// Carbon truncates at 25 as well, but renders its tooltip inline, which
|
|
296
|
+
// keeps the full value in the row's text - the `hasText` branch covers that
|
|
297
|
+
// build, and short owner IDs resolve on both.
|
|
298
|
+
authorizationRow(authorization) {
|
|
299
|
+
const rows = this.page.getByRole('row');
|
|
300
|
+
return rows
|
|
301
|
+
.filter({ hasText: `${authorization.ownerId}${authorization.resourceId}` })
|
|
302
|
+
.or(rows
|
|
303
|
+
.filter({
|
|
304
|
+
has: this.page.getByRole('cell', {
|
|
305
|
+
name: authorization.ownerId,
|
|
306
|
+
exact: true,
|
|
307
|
+
}),
|
|
308
|
+
})
|
|
309
|
+
.filter({ hasText: authorization.resourceId }))
|
|
310
|
+
.first();
|
|
311
|
+
}
|
|
312
|
+
// The toolbar search filters by owner ID, so filling it with the resource
|
|
313
|
+
// type narrows nothing, and the retry helper it fed reloads before it
|
|
314
|
+
// asserts, which dropped the filter entirely. That left the new row to be
|
|
315
|
+
// found on page 1 of the unfiltered table - and the authorizations table
|
|
316
|
+
// paginates, so with the RBA specs creating owners in parallel it is
|
|
317
|
+
// regularly on a later page. Filter by owner, then page through what is
|
|
318
|
+
// left; fall back to the unfiltered table for builds whose search has not
|
|
319
|
+
// caught up with a just-created owner yet.
|
|
320
|
+
async expectAuthorizationListed(authorization, item) {
|
|
321
|
+
const deadline = Date.now() + 180000;
|
|
322
|
+
const budget = (max) => Math.max(5000, Math.min(max, deadline - Date.now()));
|
|
323
|
+
const maxAttempts = 5;
|
|
324
|
+
let lastError;
|
|
325
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
326
|
+
if (attempt > 0 && Date.now() >= deadline) {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
await this.selectResourceType(authorization.resourceType, budget(30000));
|
|
331
|
+
for (const filterValue of [authorization.ownerId, '']) {
|
|
332
|
+
await this.filterAuthorizationsByOwner(filterValue);
|
|
333
|
+
const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, item, {
|
|
334
|
+
timeout: budget(filterValue === '' ? 45000 : 15000),
|
|
335
|
+
});
|
|
336
|
+
if (found) {
|
|
337
|
+
await (0, test_1.expect)(item).toBeVisible({ timeout: 5000 });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`No row for owner "${authorization.ownerId}" and resource ` +
|
|
342
|
+
`"${authorization.resourceId}" in the ` +
|
|
343
|
+
`${authorization.resourceType} table`);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
lastError = error;
|
|
347
|
+
if (attempt < maxAttempts - 1) {
|
|
348
|
+
console.warn(`Attempt ${attempt + 1} did not list the ` +
|
|
349
|
+
`${authorization.resourceType} authorization for ` +
|
|
350
|
+
`"${authorization.ownerId}". Retrying...`);
|
|
351
|
+
await this.page.reload();
|
|
352
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`Authorization for "${authorization.ownerId}" on ` +
|
|
357
|
+
`"${authorization.resourceId}" was not listed: ${String(lastError)}`);
|
|
358
|
+
}
|
|
359
|
+
async filterAuthorizationsByOwner(ownerId) {
|
|
360
|
+
const hasFilter = await this.tableFilter
|
|
361
|
+
.isVisible({ timeout: 5000 })
|
|
362
|
+
.catch(() => false);
|
|
363
|
+
if (!hasFilter) {
|
|
185
364
|
return;
|
|
186
365
|
}
|
|
187
|
-
await this.
|
|
188
|
-
|
|
366
|
+
await this.tableFilter.fill(ownerId);
|
|
367
|
+
// The search is debounced and served by the API; the paginated lookup this
|
|
368
|
+
// feeds waits for the row itself, so there is nothing to wait on here
|
|
369
|
+
// beyond the value having landed in the box.
|
|
370
|
+
await (0, test_1.expect)(this.tableFilter).toHaveValue(ownerId);
|
|
189
371
|
}
|
|
190
372
|
async deleteAuthorization(resourceId, resourceType) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
195
|
-
|
|
373
|
+
// Same paginated table as createAuthorization: the row being deleted is
|
|
374
|
+
// not necessarily on page 1, and the toolbar search cannot narrow by
|
|
375
|
+
// resource ID.
|
|
376
|
+
const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, this.deleteAuthorizationButton(resourceId), { timeout: 60000 });
|
|
377
|
+
if (!found) {
|
|
378
|
+
throw new Error(`No ${resourceType} authorization row for resource "${resourceId}"`);
|
|
379
|
+
}
|
|
380
|
+
await this.deleteAuthorizationButton(resourceId).click();
|
|
196
381
|
await (0, test_1.expect)(this.deleteAuthorizationModal).toBeVisible();
|
|
197
382
|
await this.deleteAuthorizationModalDeleteButton.click();
|
|
198
383
|
await (0, test_1.expect)(this.deleteAuthorizationModal).not.toBeVisible();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
384
|
+
// Re-selecting the resource type takes two interactions on the
|
|
385
|
+
// design-system build, so this cannot go through the shared
|
|
386
|
+
// reload-click-assert helper, which only accepts a single click locator.
|
|
387
|
+
const deletedCell = this.authorizationsList
|
|
388
|
+
.getByRole('cell', { name: resourceId })
|
|
389
|
+
.first();
|
|
390
|
+
let lastError;
|
|
391
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
392
|
+
await this.page.reload();
|
|
393
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
394
|
+
await this.selectResourceType(resourceType);
|
|
395
|
+
try {
|
|
396
|
+
await (0, test_1.expect)(deletedCell).not.toBeVisible({ timeout: 5000 });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
lastError = error;
|
|
401
|
+
console.warn(`Attempt ${attempt + 1} still listed the ${resourceType} ` +
|
|
402
|
+
`authorization for "${resourceId}". Retrying...`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
throw new Error(`Authorization for resource "${resourceId}" is still listed: ` +
|
|
406
|
+
String(lastError));
|
|
211
407
|
}
|
|
212
408
|
}
|
|
213
409
|
exports.Authorization = Authorization;
|
|
@@ -2,31 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OCIdentityAuthorizationsPage = void 0;
|
|
4
4
|
const Authorization_1 = require("../8.10/Authorization");
|
|
5
|
-
const test_1 = require("@playwright/test");
|
|
6
|
-
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
7
5
|
class OCIdentityAuthorizationsPage extends Authorization_1.Authorization {
|
|
8
6
|
constructor(page) {
|
|
9
7
|
super(page);
|
|
10
8
|
}
|
|
11
9
|
async clickProcessDefinitionTab() {
|
|
12
|
-
await
|
|
13
|
-
await this.selectResourceTypeTab('PROCESS_DEFINITION').click();
|
|
10
|
+
await this.selectResourceType('PROCESS_DEFINITION');
|
|
14
11
|
}
|
|
15
12
|
async clickAuthorizationTab() {
|
|
16
|
-
await
|
|
17
|
-
await this.selectResourceTypeTab('AUTHORIZATION').click();
|
|
13
|
+
await this.selectResourceType('AUTHORIZATION');
|
|
18
14
|
}
|
|
19
15
|
async clickRoleTab() {
|
|
20
|
-
await
|
|
21
|
-
await this.selectResourceTypeTab('Role').click();
|
|
16
|
+
await this.selectResourceType('ROLE');
|
|
22
17
|
}
|
|
23
18
|
async clickComponentTab() {
|
|
24
|
-
await
|
|
25
|
-
await this.selectResourceTypeTab('COMPONENT').click();
|
|
19
|
+
await this.selectResourceType('COMPONENT');
|
|
26
20
|
}
|
|
27
21
|
async clickResourceTab() {
|
|
28
|
-
await
|
|
29
|
-
await this.selectResourceTypeTab('RESOURCE').click();
|
|
22
|
+
await this.selectResourceType('RESOURCE');
|
|
30
23
|
}
|
|
31
24
|
}
|
|
32
25
|
exports.OCIdentityAuthorizationsPage = OCIdentityAuthorizationsPage;
|
|
@@ -13,6 +13,7 @@ export declare class OCIdentityClusterVariablesPage {
|
|
|
13
13
|
readonly monacoEditorTextArea: Locator;
|
|
14
14
|
readonly successMessage: Locator;
|
|
15
15
|
readonly editMenuItem: Locator;
|
|
16
|
+
readonly editModalHeading: Locator;
|
|
16
17
|
constructor(page: Page);
|
|
17
18
|
editVariable(variableName: string, newValue: string): Promise<void>;
|
|
18
19
|
assertVariableExists(variableName: string): Promise<void>;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OCIdentityClusterVariablesPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
5
7
|
class OCIdentityClusterVariablesPage {
|
|
6
8
|
page;
|
|
7
9
|
variablesList;
|
|
@@ -16,21 +18,36 @@ class OCIdentityClusterVariablesPage {
|
|
|
16
18
|
monacoEditorTextArea;
|
|
17
19
|
successMessage;
|
|
18
20
|
editMenuItem;
|
|
21
|
+
editModalHeading;
|
|
19
22
|
constructor(page) {
|
|
20
23
|
this.page = page;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// The Admin UI moved off Carbon onto the Camunda design system:
|
|
25
|
+
// `identity/client/src/pages/cluster-variables/index.tsx` renders ListV2
|
|
26
|
+
// (design-system `DataTable`, Radix dialog/menu) when the
|
|
27
|
+
// `isNewDesignSystemEnabled` client config is on, which SaaS clusters now
|
|
28
|
+
// ship with, so the Carbon `table.cds--data-table` this was scoped to no
|
|
29
|
+
// longer exists. The table role resolves both DOMs, and filtering the rows
|
|
30
|
+
// by the variable name already excludes the header row, so the removed
|
|
31
|
+
// tbody scoping is not needed. Mirrors
|
|
32
|
+
// pages/SM-8.10/OCIdentityClusterVariablesPage.ts.
|
|
33
|
+
this.variablesList = page.getByRole('table');
|
|
34
|
+
this.variableRow = (variableName) => this.variablesList.getByRole('row').filter({ hasText: variableName });
|
|
35
|
+
// Carbon's OverflowMenu labels its trigger "Options"; the design-system
|
|
36
|
+
// DataTable labels it "Row actions".
|
|
37
|
+
this.editVariableButton = (variableName) => this.variableRow(variableName).getByRole('button', {
|
|
38
|
+
name: /^(Options|Row actions)$/,
|
|
39
|
+
});
|
|
27
40
|
this.editVariableModal = page.getByRole('dialog', {
|
|
28
41
|
name: /Edit.*variable/i,
|
|
29
42
|
});
|
|
30
43
|
this.variableValueField = this.editVariableModal.getByRole('textbox', {
|
|
31
44
|
name: 'Editor content',
|
|
32
45
|
});
|
|
33
|
-
|
|
46
|
+
// Both modals pass `confirmLabel={t("save")}`, so the accessible name is
|
|
47
|
+
// stable across the two footers.
|
|
48
|
+
this.saveVariableButton = this.editVariableModal.getByRole('button', {
|
|
49
|
+
name: 'Save',
|
|
50
|
+
});
|
|
34
51
|
this.closeEditVariableModal = this.editVariableModal.getByRole('button', {
|
|
35
52
|
name: 'Close',
|
|
36
53
|
});
|
|
@@ -40,9 +57,15 @@ class OCIdentityClusterVariablesPage {
|
|
|
40
57
|
this.monacoEditor = this.editVariableModal.locator('.monaco-editor');
|
|
41
58
|
this.monacoEditorTextArea = this.monacoEditor.locator('textarea.inputarea');
|
|
42
59
|
this.successMessage = this.page.getByText('Cluster variable updated');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
// Carbon renders the modal title as an h3, the design-system dialog as an
|
|
61
|
+
// h2; the heading role covers both.
|
|
62
|
+
this.editModalHeading = this.editVariableModal.getByRole('heading').first();
|
|
63
|
+
// Carbon menu items are buttons, Radix menu items are divs — match on the
|
|
64
|
+
// role instead of the tag.
|
|
65
|
+
this.editMenuItem = this.page.getByRole('menuitem', {
|
|
66
|
+
name: 'Edit',
|
|
67
|
+
exact: true,
|
|
68
|
+
});
|
|
46
69
|
}
|
|
47
70
|
async editVariable(variableName, newValue) {
|
|
48
71
|
await this.editVariableButton(variableName).click();
|
|
@@ -50,21 +73,50 @@ class OCIdentityClusterVariablesPage {
|
|
|
50
73
|
await this.editMenuItem.click();
|
|
51
74
|
await (0, test_1.expect)(this.variableValueField).toBeVisible();
|
|
52
75
|
await (0, test_1.expect)(this.monacoEditor).toBeVisible();
|
|
53
|
-
// Monaco's hidden <textarea> only forwards keyboard events to the
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
// Monaco's hidden <textarea> only forwards keyboard events to the editor
|
|
77
|
+
// model — `fill()` writes the value but doesn't trigger Monaco's
|
|
78
|
+
// onDidChangeContent, so the modal's JSON validator never re-runs and Save
|
|
79
|
+
// stays disabled. Use real keyboard input instead, and retry the
|
|
80
|
+
// clear-type-enable cycle because Monaco's auto-closing brackets/quotes can
|
|
81
|
+
// corrupt the JSON on the first attempt.
|
|
82
|
+
const MAX_TYPE_ATTEMPTS = 3;
|
|
83
|
+
for (let attempt = 1; attempt <= MAX_TYPE_ATTEMPTS; attempt++) {
|
|
84
|
+
await this.monacoEditor.click();
|
|
85
|
+
await this.monacoEditorTextArea.press('ControlOrMeta+A');
|
|
86
|
+
await this.monacoEditorTextArea.press('Delete');
|
|
87
|
+
// Let Monaco settle after clearing content
|
|
88
|
+
await (0, sleep_1.sleep)(500);
|
|
89
|
+
await this.monacoEditorTextArea.pressSequentially(newValue, { delay: 20 });
|
|
90
|
+
// Dismiss any auto-complete popup by clicking the modal heading.
|
|
91
|
+
// Do NOT use press('Escape') on the textarea — when no autocomplete
|
|
92
|
+
// is showing, the Escape event propagates to the dialog and closes it.
|
|
93
|
+
await this.editModalHeading.click();
|
|
94
|
+
// Let Monaco settle and validate the new content
|
|
95
|
+
await (0, sleep_1.sleep)(500);
|
|
96
|
+
try {
|
|
97
|
+
await (0, test_1.expect)(this.saveVariableButton).toBeEnabled({ timeout: 10000 });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
if (attempt === MAX_TYPE_ATTEMPTS) {
|
|
102
|
+
throw new Error(`Save button did not become enabled after ${MAX_TYPE_ATTEMPTS} typing attempts`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
62
106
|
await this.saveVariableButton.click();
|
|
63
107
|
await (0, test_1.expect)(this.successMessage).toBeVisible();
|
|
64
108
|
await (0, test_1.expect)(this.editVariableModal).toBeHidden();
|
|
65
109
|
}
|
|
66
110
|
async assertVariableExists(variableName) {
|
|
67
|
-
|
|
111
|
+
// ListV2 fetches the variables with react-query after the Cluster
|
|
112
|
+
// variables nav link is followed, so the table can still be empty when
|
|
113
|
+
// this runs; reload between attempts rather than racing a single wait.
|
|
114
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.variableRow(variableName), {
|
|
115
|
+
visibilityTimeout: 30000,
|
|
116
|
+
postAction: async () => {
|
|
117
|
+
await this.page.reload();
|
|
118
|
+
},
|
|
119
|
+
});
|
|
68
120
|
}
|
|
69
121
|
async assertVariableValue(variableName, expectedValue) {
|
|
70
122
|
const row = this.variableRow(variableName);
|
|
@@ -10,6 +10,7 @@ declare class OCIdentityRolesPage {
|
|
|
10
10
|
readonly rolesList: Locator;
|
|
11
11
|
readonly roleCell: (name: string) => Locator;
|
|
12
12
|
readonly rowValue: (name: string) => Locator;
|
|
13
|
+
readonly memberCell: (name: string) => Locator;
|
|
13
14
|
readonly createRoleButton: Locator;
|
|
14
15
|
readonly createRoleModal: Locator;
|
|
15
16
|
readonly createNameField: Locator;
|
|
@@ -15,6 +15,7 @@ class OCIdentityRolesPage {
|
|
|
15
15
|
rolesList;
|
|
16
16
|
roleCell;
|
|
17
17
|
rowValue;
|
|
18
|
+
memberCell;
|
|
18
19
|
createRoleButton;
|
|
19
20
|
createRoleModal;
|
|
20
21
|
createNameField;
|
|
@@ -52,6 +53,18 @@ class OCIdentityRolesPage {
|
|
|
52
53
|
});
|
|
53
54
|
this.roleCell = (roleID) => this.rolesList.getByRole('cell', { name: roleID, exact: true });
|
|
54
55
|
this.rowValue = (name) => page.getByRole('row').filter({ hasText: name });
|
|
56
|
+
// The role detail page's members table is the design-system EntityList,
|
|
57
|
+
// which truncates any cell longer than 50 characters: `truncateCell` in
|
|
58
|
+
// camunda/camunda identity/client/src/components/entityListV2/EntityList.tsx
|
|
59
|
+
// replaces the text with
|
|
60
|
+
// `<button aria-label="<full value>">{first 50 chars}…</button>`, and the
|
|
61
|
+
// tooltip that carries the rest is portalled out of the row. The QA
|
|
62
|
+
// usernames are 60-character addresses, so the row's text no longer
|
|
63
|
+
// contains the address at all and `hasText` cannot see it — the full value
|
|
64
|
+
// now only survives in the accessible name, which a `cell` derives from its
|
|
65
|
+
// contents. Matching on the cell name resolves the untruncated (Carbon)
|
|
66
|
+
// rendering just as well, so this covers both builds.
|
|
67
|
+
this.memberCell = (name) => this.rolesList.getByRole('cell', { name });
|
|
55
68
|
this.createRoleModal = page.getByRole('dialog', {
|
|
56
69
|
name: 'Create role',
|
|
57
70
|
});
|
|
@@ -64,7 +77,13 @@ class OCIdentityRolesPage {
|
|
|
64
77
|
exact: true,
|
|
65
78
|
});
|
|
66
79
|
this.createRoleModalCreateButton = this.createRoleModal.getByRole('button', { name: 'Create role' });
|
|
67
|
-
|
|
80
|
+
// Carbon rendered the row's delete action as an icon-only button carrying
|
|
81
|
+
// an aria-label; the design-system EntityList renders the (dangerous) row
|
|
82
|
+
// action inline as a button with visible text instead, so there is no label
|
|
83
|
+
// to match. The accessible name covers both shapes.
|
|
84
|
+
this.deleteRoleButton = (rowName) => this.rolesList
|
|
85
|
+
.getByRole('row', { name: rowName })
|
|
86
|
+
.getByRole('button', { name: /^delete/i });
|
|
68
87
|
this.deleteRoleModal = page.getByRole('dialog', { name: /delete role/i });
|
|
69
88
|
this.closeDeleteRoleModal = this.deleteRoleModal.getByRole('button', {
|
|
70
89
|
name: 'Close',
|
|
@@ -159,13 +178,12 @@ class OCIdentityRolesPage {
|
|
|
159
178
|
await this.createRole({ id: role, name: role });
|
|
160
179
|
}
|
|
161
180
|
await this.doAssignUserToRole(role, email);
|
|
162
|
-
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.
|
|
181
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.memberCell(email).first(), {
|
|
163
182
|
preAction: async () => {
|
|
164
183
|
await this.page.reload();
|
|
165
184
|
await (0, test_1.expect)(this.rolesList.first()).toBeVisible({ timeout: 20000 });
|
|
166
185
|
},
|
|
167
186
|
});
|
|
168
|
-
await (0, test_1.expect)(this.rowValue(email)).toContainText(`${email}`);
|
|
169
187
|
}
|
|
170
188
|
async deleteRole(name) {
|
|
171
189
|
const locator = this.deleteRoleButton(name);
|
|
@@ -12,7 +12,7 @@ const resourceContainers = new WeakMap();
|
|
|
12
12
|
// the resource container -- files, folders and uploads live directly inside it,
|
|
13
13
|
// there is no separate process-application level to descend into. It is created
|
|
14
14
|
// from the workspace's projects listing via the "Create project" split button +
|
|
15
|
-
// dialog, and the app lands on `/
|
|
15
|
+
// dialog, and the app lands on `/workspace/{wsId}/projects/{projectId}`.
|
|
16
16
|
//
|
|
17
17
|
// This page object owns that container's surface (creating a Project, and binding
|
|
18
18
|
// its development-stage cluster). Entering and leaving it is driven from
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
<zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
|
|
44
44
|
<zeebe:input source="searchVariables"
|
|
45
45
|
target="data.connectorMode.operation.toolName" />
|
|
46
|
-
<zeebe:input
|
|
46
|
+
<zeebe:input
|
|
47
|
+
source="={"filter":{"variableKey":string(testVariableKey)}}"
|
|
48
|
+
target="data.connectorMode.operation.toolArguments" />
|
|
47
49
|
</zeebe:ioMapping>
|
|
48
50
|
<zeebe:taskHeaders>
|
|
49
51
|
<zeebe:header key="elementTemplateVersion" value="2" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const _8_10_1 = require("../../../fixtures/8.10");
|
|
4
|
+
const UtilitiesPage_1 = require("../../../pages/8.10/UtilitiesPage");
|
|
5
|
+
const _setup_1 = require("../../../test-setup.js");
|
|
6
|
+
const apiHelpers_1 = require("../../../utils/apiHelpers");
|
|
7
|
+
const userTaskHelpers_1 = require("../../../utils/userTaskHelpers");
|
|
8
|
+
const users_1 = require("../../../utils/users");
|
|
9
|
+
const test_1 = require("@playwright/test");
|
|
10
|
+
const testUser = (0, users_1.getTestUser)('fourteenthUser');
|
|
11
|
+
_8_10_1.test.describe.configure({ mode: 'parallel' });
|
|
12
|
+
_8_10_1.test.describe('Orchestration Cluster MCP Server - User Task Tools @tasklistV2', () => {
|
|
13
|
+
const clusterName = 'Agentic Orchestration Cluster';
|
|
14
|
+
const createTestData = async () => {
|
|
15
|
+
const authToken = await (0, apiHelpers_1.authSaasAPI)(undefined, 'agentic_cluster');
|
|
16
|
+
const [deployedTestFileProcessKey, toolsProcessKey] = await Promise.all([
|
|
17
|
+
(0, apiHelpers_1.deployProcess)('./resources/mcp_server_saas/mcp_user_task_test_file.bpmn', authToken, 'saas', 'agentic_cluster'),
|
|
18
|
+
(0, apiHelpers_1.deployProcess)('./resources/mcp_server_saas/mcp_user_task_tools.bpmn', authToken, 'saas', 'agentic_cluster'),
|
|
19
|
+
]);
|
|
20
|
+
if (deployedTestFileProcessKey == null || toolsProcessKey == null) {
|
|
21
|
+
throw new Error('Failed to deploy user task tools processes');
|
|
22
|
+
}
|
|
23
|
+
const testFileProcessKey = String(deployedTestFileProcessKey);
|
|
24
|
+
// Create test instance which will create a user task
|
|
25
|
+
const testInstanceKey = await (0, apiHelpers_1.createProcessInstance)(testFileProcessKey, authToken, 'saas', 'agentic_cluster');
|
|
26
|
+
// Get the user task key created by the test instance (with retry logic)
|
|
27
|
+
const testUserTaskKey = await (0, userTaskHelpers_1.getUserTaskKeyByProcessInstanceKey)(testInstanceKey, authToken, 'saas', 'agentic_cluster');
|
|
28
|
+
console.log('Creating tools instance with variables:', {
|
|
29
|
+
testFileProcessKey,
|
|
30
|
+
testUserTaskKey,
|
|
31
|
+
});
|
|
32
|
+
// Create tools instance with required variables
|
|
33
|
+
const toolsInstanceKey = await (0, apiHelpers_1.createProcessInstance)(String(toolsProcessKey), authToken, 'saas', 'agentic_cluster', { testFileProcessKey, testUserTaskKey });
|
|
34
|
+
return {
|
|
35
|
+
toolsInstanceKey,
|
|
36
|
+
testFileProcessKey,
|
|
37
|
+
testInstanceKey,
|
|
38
|
+
testUserTaskKey,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
_8_10_1.test.beforeEach(async ({ page, loginPage }, testInfo) => {
|
|
42
|
+
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, (testInfo.workerIndex + 1) * 1000);
|
|
43
|
+
});
|
|
44
|
+
_8_10_1.test.afterEach(async ({ page }, testInfo) => {
|
|
45
|
+
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
46
|
+
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
47
|
+
});
|
|
48
|
+
const userTaskToolTests = [
|
|
49
|
+
{
|
|
50
|
+
name: 'searchUserTasks - search user tasks with filters such as processDefinitionKey',
|
|
51
|
+
variable: 'mcpResultSearchUserTasks',
|
|
52
|
+
assertions: (data) => [
|
|
53
|
+
{
|
|
54
|
+
text: 'totalItems',
|
|
55
|
+
description: 'search response contains totalItems field',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
text: () => data.testUserTaskKey,
|
|
59
|
+
description: 'search response contains userTaskKey field',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
text: () => data.testFileProcessKey,
|
|
63
|
+
description: 'search response contains processDefinitionKey field',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'getUserTask - retrieve a user task by key',
|
|
69
|
+
variable: 'mcpResultGetUserTask',
|
|
70
|
+
assertions: (data) => [
|
|
71
|
+
{
|
|
72
|
+
text: () => data.testUserTaskKey,
|
|
73
|
+
description: 'response contains userTaskKey field',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
text: () => data.testInstanceKey,
|
|
77
|
+
description: 'response contains processInstanceKey field',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
text: 'assignee',
|
|
81
|
+
description: 'response contains assignee field',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'assignUserTask - update the assignment of a user task',
|
|
87
|
+
variable: 'mcpResultAssignUserTask',
|
|
88
|
+
assertions: () => [
|
|
89
|
+
{
|
|
90
|
+
text: 'mcpUser',
|
|
91
|
+
description: 'response reflects the assignee value mcpUser',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'searchUserTaskVariables - search variables scoped to a specific user task',
|
|
97
|
+
variable: 'mcpResultSearchUserTaskVariables',
|
|
98
|
+
assertions: (data) => [
|
|
99
|
+
{
|
|
100
|
+
text: 'totalItems',
|
|
101
|
+
description: 'search response contains totalItems field',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
text: () => data.testUserTaskKey,
|
|
105
|
+
description: 'search response contains the created user task',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
for (const testCase of userTaskToolTests) {
|
|
111
|
+
(0, _8_10_1.test)(testCase.name, async ({ page, homePage, appsPage, operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
|
|
112
|
+
_8_10_1.test.slow();
|
|
113
|
+
const data = await createTestData();
|
|
114
|
+
const { toolsInstanceKey } = data;
|
|
115
|
+
await _8_10_1.test.step('Navigate to completed process in Operate', async () => {
|
|
116
|
+
await page.goto('/');
|
|
117
|
+
await homePage.clickClusters();
|
|
118
|
+
await appsPage.clickCamundaApps();
|
|
119
|
+
await appsPage.clickOperate(clusterName);
|
|
120
|
+
await (0, test_1.expect)(operateHomePage.operateBanner).toBeVisible({
|
|
121
|
+
timeout: 60000,
|
|
122
|
+
});
|
|
123
|
+
await operateHomePage.clickProcessesTab();
|
|
124
|
+
await operateProcessesPage.clickProcessCompletedCheckbox();
|
|
125
|
+
await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', String(toolsInstanceKey));
|
|
126
|
+
await operateProcessesPage.clickProcessInstanceLink('MCP User Task Tools');
|
|
127
|
+
await operateProcessInstancePage.assertProcessCompleteStatusWithRetry();
|
|
128
|
+
});
|
|
129
|
+
for (const assertion of testCase.assertions(data)) {
|
|
130
|
+
await _8_10_1.test.step(assertion.description, async () => {
|
|
131
|
+
await operateProcessInstancePage.assertProcessVariableContainsText(testCase.variable, typeof assertion.text === 'function'
|
|
132
|
+
? assertion.text()
|
|
133
|
+
: assertion.text);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const _8_10_1 = require("../../../fixtures/8.10");
|
|
4
|
+
const UtilitiesPage_1 = require("../../../pages/8.10/UtilitiesPage");
|
|
5
|
+
const _setup_1 = require("../../../test-setup.js");
|
|
6
|
+
const apiHelpers_1 = require("../../../utils/apiHelpers");
|
|
7
|
+
const variableHelpers_1 = require("../../../utils/variableHelpers");
|
|
8
|
+
const users_1 = require("../../../utils/users");
|
|
9
|
+
const test_1 = require("@playwright/test");
|
|
10
|
+
const testUser = (0, users_1.getTestUser)('sixteenthUser');
|
|
11
|
+
_8_10_1.test.describe.configure({ mode: 'parallel' });
|
|
12
|
+
_8_10_1.test.describe('Orchestration Cluster MCP Server - Variable Tools @tasklistV2', () => {
|
|
13
|
+
const clusterName = 'Agentic Orchestration Cluster';
|
|
14
|
+
const createTestData = async () => {
|
|
15
|
+
const authToken = await (0, apiHelpers_1.authSaasAPI)(undefined, 'agentic_cluster');
|
|
16
|
+
const [testFileProcessKey, toolsProcessKey] = await Promise.all([
|
|
17
|
+
(0, apiHelpers_1.deployProcess)('./resources/mcp_server_saas/mcp_variable_test_file.bpmn', authToken, 'saas', 'agentic_cluster'),
|
|
18
|
+
(0, apiHelpers_1.deployProcess)('./resources/mcp_server_saas/mcp_variable_tools.bpmn', authToken, 'saas', 'agentic_cluster'),
|
|
19
|
+
]);
|
|
20
|
+
if (testFileProcessKey == null || toolsProcessKey == null) {
|
|
21
|
+
throw new Error('Failed to deploy variable tools processes');
|
|
22
|
+
}
|
|
23
|
+
// Create test instance which will create variables
|
|
24
|
+
const testInstanceKey = await (0, apiHelpers_1.createProcessInstance)(String(testFileProcessKey), authToken, 'saas', 'agentic_cluster', { testVariable1: 'test_value_1', testVariable2: 'test_value_2' });
|
|
25
|
+
// Get a variable key created by the test instance (with retry logic)
|
|
26
|
+
const testVariableKey = await (0, variableHelpers_1.getVariableKeyByProcessInstanceKey)(testInstanceKey, 'testVariable1', authToken, 'saas', 'agentic_cluster');
|
|
27
|
+
console.log('Creating tools instance with variables:', {
|
|
28
|
+
testFileProcessKey,
|
|
29
|
+
testVariableKey,
|
|
30
|
+
});
|
|
31
|
+
// Create tools instance with required variables
|
|
32
|
+
const toolsInstanceKey = await (0, apiHelpers_1.createProcessInstance)(String(toolsProcessKey), authToken, 'saas', 'agentic_cluster', { testFileProcessKey, testVariableKey });
|
|
33
|
+
return { toolsInstanceKey, testInstanceKey, testVariableKey };
|
|
34
|
+
};
|
|
35
|
+
_8_10_1.test.beforeEach(async ({ page, loginPage }, testInfo) => {
|
|
36
|
+
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, (testInfo.workerIndex + 1) * 1000);
|
|
37
|
+
});
|
|
38
|
+
_8_10_1.test.afterEach(async ({ page }, testInfo) => {
|
|
39
|
+
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
40
|
+
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
41
|
+
});
|
|
42
|
+
const variableToolTests = [
|
|
43
|
+
{
|
|
44
|
+
name: 'searchVariables - search variables with filters',
|
|
45
|
+
variable: 'mcpResultSearchVariables',
|
|
46
|
+
assertions: (data) => [
|
|
47
|
+
{
|
|
48
|
+
text: 'items',
|
|
49
|
+
description: 'search response contains items field',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
text: () => data.testVariableKey,
|
|
53
|
+
description: 'search response contains the created variable key',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
text: 'testVariable1',
|
|
57
|
+
description: 'search response contains the variable name',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
text: 'test_value_1',
|
|
61
|
+
description: 'search response contains the variable value',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
text: () => data.testInstanceKey,
|
|
65
|
+
description: 'search response contains the process instance key',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'getVariable - retrieve a variable by key',
|
|
71
|
+
variable: 'mcpResultGetVariable',
|
|
72
|
+
assertions: (data) => [
|
|
73
|
+
{
|
|
74
|
+
text: () => data.testVariableKey,
|
|
75
|
+
description: 'response contains variableKey field',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
text: 'testVariable1',
|
|
79
|
+
description: 'response contains name field',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
text: 'test_value_1',
|
|
83
|
+
description: 'response contains value field',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
text: () => data.testInstanceKey,
|
|
87
|
+
description: 'response contains processInstanceKey field',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
text: 'scopeKey',
|
|
91
|
+
description: 'response contains scopeKey field',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
for (const testCase of variableToolTests) {
|
|
97
|
+
(0, _8_10_1.test)(testCase.name, async ({ homePage, appsPage, operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
|
|
98
|
+
_8_10_1.test.slow();
|
|
99
|
+
const data = await createTestData();
|
|
100
|
+
const { toolsInstanceKey } = data;
|
|
101
|
+
await _8_10_1.test.step('Navigate to completed process in Operate', async () => {
|
|
102
|
+
await homePage.clickClusters();
|
|
103
|
+
await appsPage.clickCamundaApps();
|
|
104
|
+
await appsPage.clickOperate(clusterName);
|
|
105
|
+
await (0, test_1.expect)(operateHomePage.operateBanner).toBeVisible({
|
|
106
|
+
timeout: 60000,
|
|
107
|
+
});
|
|
108
|
+
await operateHomePage.clickProcessesTab();
|
|
109
|
+
await operateProcessesPage.clickProcessCompletedCheckbox();
|
|
110
|
+
await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', String(toolsInstanceKey));
|
|
111
|
+
await operateProcessesPage.clickProcessInstanceLink('MCP Variable Tools');
|
|
112
|
+
await operateProcessInstancePage.assertProcessCompleteStatusWithRetry();
|
|
113
|
+
});
|
|
114
|
+
for (const assertion of testCase.assertions(data)) {
|
|
115
|
+
await _8_10_1.test.step(assertion.description, async () => {
|
|
116
|
+
await operateProcessInstancePage.assertProcessVariableContainsText(testCase.variable, typeof assertion.text === 'function'
|
|
117
|
+
? assertion.text()
|
|
118
|
+
: assertion.text);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
package/package.json
CHANGED
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
<zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
|
|
44
44
|
<zeebe:input source="searchVariables"
|
|
45
45
|
target="data.connectorMode.operation.toolName" />
|
|
46
|
-
<zeebe:input
|
|
46
|
+
<zeebe:input
|
|
47
|
+
source="={"filter":{"variableKey":string(testVariableKey)}}"
|
|
48
|
+
target="data.connectorMode.operation.toolArguments" />
|
|
47
49
|
</zeebe:ioMapping>
|
|
48
50
|
<zeebe:taskHeaders>
|
|
49
51
|
<zeebe:header key="elementTemplateVersion" value="2" />
|