@camunda/e2e-test-suite 0.0.1479 → 0.0.1480
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/SM-8.10/ConnectorTemplatePage.d.ts +1 -0
- package/dist/pages/SM-8.10/ConnectorTemplatePage.js +15 -0
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +11 -0
- package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -0
- package/dist/pages/SM-8.10/ModelerHomePage.d.ts +11 -2
- package/dist/pages/SM-8.10/ModelerHomePage.js +44 -6
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +102 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ declare class ConnectorTemplatePage {
|
|
|
6
6
|
constructor(page: Page);
|
|
7
7
|
clickPublishToOrgButton(): Promise<void>;
|
|
8
8
|
clickPublishButton(): Promise<void>;
|
|
9
|
+
publishImportedTemplateToWorkspace(version: number): Promise<void>;
|
|
9
10
|
publishConnector(): Promise<void>;
|
|
10
11
|
}
|
|
11
12
|
export { ConnectorTemplatePage };
|
|
@@ -20,6 +20,21 @@ class ConnectorTemplatePage {
|
|
|
20
20
|
async clickPublishButton() {
|
|
21
21
|
await this.publishButton.click({ timeout: 90000 });
|
|
22
22
|
}
|
|
23
|
+
async publishImportedTemplateToWorkspace(version) {
|
|
24
|
+
await this.page.getByRole('button', { name: 'Publish', exact: true }).click();
|
|
25
|
+
await this.page
|
|
26
|
+
.getByRole('menuitem', { name: 'Publish to workspace', exact: true })
|
|
27
|
+
.click();
|
|
28
|
+
const dialog = this.page.getByRole('dialog').filter({
|
|
29
|
+
has: this.page.getByRole('heading', {
|
|
30
|
+
name: 'Publish version to workspace',
|
|
31
|
+
exact: true,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
await (0, test_1.expect)(dialog.getByRole('spinbutton', { name: 'Version number', exact: true })).toHaveValue(String(version));
|
|
35
|
+
await dialog.getByRole('button', { name: 'Publish', exact: true }).click();
|
|
36
|
+
await (0, test_1.expect)(dialog).toBeHidden({ timeout: 60000 });
|
|
37
|
+
}
|
|
23
38
|
async publishConnector() {
|
|
24
39
|
try {
|
|
25
40
|
await (0, test_1.expect)(this.publishToOrgButton).toBeVisible({ timeout: 15000 });
|
|
@@ -175,6 +175,17 @@ declare class ModelerCreatePage {
|
|
|
175
175
|
openChangeElementPopupForElement(elementId: string): Promise<void>;
|
|
176
176
|
private clickChangeElementTemplateOption;
|
|
177
177
|
assertChangeElementTemplateListed(option: Locator, searchTerm: string, elementId: string): Promise<void>;
|
|
178
|
+
appendTemplateTarget(previousElementId: string, type: 'task' | 'receiveTask' | 'intermediateCatchEvent'): Promise<string>;
|
|
179
|
+
applyImportedTemplate(elementId: string, template: {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
}): Promise<void>;
|
|
183
|
+
assertExportedTemplateBindings(bindings: {
|
|
184
|
+
elementId: string;
|
|
185
|
+
id: string;
|
|
186
|
+
version: number;
|
|
187
|
+
elementType: string;
|
|
188
|
+
}[]): Promise<void>;
|
|
178
189
|
changeElementToAdHocSubProcess(elementId: string): Promise<void>;
|
|
179
190
|
applyAiAgentSubProcessTemplate(): Promise<void>;
|
|
180
191
|
clickWebhookMessageStartEventConnectorOption(): Promise<void>;
|
|
@@ -1424,6 +1424,84 @@ class ModelerCreatePage {
|
|
|
1424
1424
|
await (0, test_1.expect)(option).toBeVisible({ timeout: 30000 });
|
|
1425
1425
|
await (0, test_1.expect)(option).toBeEnabled({ timeout: 30000 });
|
|
1426
1426
|
}
|
|
1427
|
+
async appendTemplateTarget(previousElementId, type) {
|
|
1428
|
+
const previous = this.page.locator(`.djs-shape[data-element-id="${previousElementId}"]`);
|
|
1429
|
+
await this.page.keyboard.press('Escape');
|
|
1430
|
+
if (!(await previous.evaluate((shape) => shape.classList.contains('selected')))) {
|
|
1431
|
+
await previous.click();
|
|
1432
|
+
}
|
|
1433
|
+
await this.page
|
|
1434
|
+
.getByRole('button', {
|
|
1435
|
+
name: type === 'intermediateCatchEvent'
|
|
1436
|
+
? 'Append intermediate/boundary event'
|
|
1437
|
+
: 'Append task',
|
|
1438
|
+
exact: true,
|
|
1439
|
+
})
|
|
1440
|
+
.click();
|
|
1441
|
+
const selected = this.page.locator('.djs-shape.selected');
|
|
1442
|
+
await (0, test_1.expect)(selected).not.toHaveAttribute('data-element-id', previousElementId);
|
|
1443
|
+
const elementId = await selected.getAttribute('data-element-id');
|
|
1444
|
+
if (!elementId)
|
|
1445
|
+
throw new Error('The appended template target has no ID.');
|
|
1446
|
+
if (type !== 'task') {
|
|
1447
|
+
await this.openChangeElementPopupForElement(elementId);
|
|
1448
|
+
await this.changeElementSearchInput.fill('');
|
|
1449
|
+
await this.changeElementSearchInput.pressSequentially(type === 'receiveTask'
|
|
1450
|
+
? 'Receive task'
|
|
1451
|
+
: 'Message intermediate catch event');
|
|
1452
|
+
const entry = type === 'receiveTask'
|
|
1453
|
+
? 'replace-with-receive-task'
|
|
1454
|
+
: 'replace-with-message-intermediate-catch';
|
|
1455
|
+
await this.page.locator(`.djs-popup [data-id="${entry}"]`).click();
|
|
1456
|
+
}
|
|
1457
|
+
return elementId;
|
|
1458
|
+
}
|
|
1459
|
+
async applyImportedTemplate(elementId, template) {
|
|
1460
|
+
const option = this.page.locator(`.djs-popup [data-id="replace.template-${template.id}"]`);
|
|
1461
|
+
await this.assertChangeElementTemplateListed(option, template.name, elementId);
|
|
1462
|
+
await option.click();
|
|
1463
|
+
await (0, test_1.expect)(this.changeElementSearchInput).toBeHidden();
|
|
1464
|
+
const selected = this.page.locator(`.djs-shape[data-element-id="${elementId}"]`);
|
|
1465
|
+
await (0, test_1.expect)(selected).toHaveClass(/(?:^|\s)selected(?:\s|$)/);
|
|
1466
|
+
const details = this.page.getByRole('region', { name: 'Details panel' });
|
|
1467
|
+
await (0, test_1.expect)(details
|
|
1468
|
+
.locator('[data-test="properties-panel-header-container"]')
|
|
1469
|
+
.getByText(template.name, { exact: true })).toBeVisible();
|
|
1470
|
+
await (0, test_1.expect)(details.getByText('Applied', { exact: true })).toBeVisible();
|
|
1471
|
+
await (0, test_1.expect)(this.templateNotFoundTag).toBeHidden();
|
|
1472
|
+
}
|
|
1473
|
+
async assertExportedTemplateBindings(bindings) {
|
|
1474
|
+
await this.page
|
|
1475
|
+
.getByRole('button', { name: 'Browse more actions', exact: true })
|
|
1476
|
+
.click();
|
|
1477
|
+
const downloadPromise = this.page.waitForEvent('download');
|
|
1478
|
+
await this.page
|
|
1479
|
+
.getByRole('menuitem', { name: 'Download as BPMN 2.0 XML', exact: true })
|
|
1480
|
+
.click();
|
|
1481
|
+
const download = await downloadPromise;
|
|
1482
|
+
const stream = await download.createReadStream();
|
|
1483
|
+
if (!stream)
|
|
1484
|
+
throw new Error('The BPMN download has no readable content.');
|
|
1485
|
+
const chunks = [];
|
|
1486
|
+
for await (const chunk of stream)
|
|
1487
|
+
chunks.push(Buffer.from(chunk));
|
|
1488
|
+
const xml = Buffer.concat(chunks).toString('utf8');
|
|
1489
|
+
const actual = await this.page.evaluate(({ xml, bindings }) => {
|
|
1490
|
+
const document = new DOMParser().parseFromString(xml, 'application/xml');
|
|
1491
|
+
if (document.querySelector('parsererror'))
|
|
1492
|
+
throw new Error('Invalid BPMN XML.');
|
|
1493
|
+
return bindings.map(({ elementId }) => {
|
|
1494
|
+
const element = Array.from(document.getElementsByTagName('*')).find((element) => element.getAttribute('id') === elementId);
|
|
1495
|
+
return {
|
|
1496
|
+
elementId,
|
|
1497
|
+
id: element?.getAttribute('zeebe:modelerTemplate'),
|
|
1498
|
+
version: Number(element?.getAttribute('zeebe:modelerTemplateVersion')),
|
|
1499
|
+
elementType: element?.localName,
|
|
1500
|
+
};
|
|
1501
|
+
});
|
|
1502
|
+
}, { xml, bindings });
|
|
1503
|
+
(0, test_1.expect)(actual).toEqual(bindings);
|
|
1504
|
+
}
|
|
1427
1505
|
// Turns the appended task into an ad-hoc sub-process through the
|
|
1428
1506
|
// change-element popup. The popup is filtered first: it virtualizes its list.
|
|
1429
1507
|
async changeElementToAdHocSubProcess(elementId) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { Browser, Page, Locator } from '@playwright/test';
|
|
2
3
|
declare class ModelerHomePage {
|
|
3
4
|
private page;
|
|
@@ -75,7 +76,7 @@ declare class ModelerHomePage {
|
|
|
75
76
|
private openWorkspaceTile;
|
|
76
77
|
private openOrCreateLegacyFolder;
|
|
77
78
|
private dismissModals;
|
|
78
|
-
clickProcessDiagram(name: string): Promise<void>;
|
|
79
|
+
clickProcessDiagram(name: string, exact?: boolean): Promise<void>;
|
|
79
80
|
clickHomeBreadcrumb(): Promise<void>;
|
|
80
81
|
clickChooseBpmnTemplateButton(): Promise<void>;
|
|
81
82
|
clickDiagramTypeDropdown(): Promise<void>;
|
|
@@ -86,7 +87,15 @@ declare class ModelerHomePage {
|
|
|
86
87
|
private waitForEntityCrumb;
|
|
87
88
|
clickProjectBreadcrumb(): Promise<void>;
|
|
88
89
|
clickWorkspaceBreadcrumb(): Promise<void>;
|
|
89
|
-
enterResourceContainer(name?: string
|
|
90
|
+
enterResourceContainer(name?: string, options?: {
|
|
91
|
+
configureEnvironment?: boolean;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
uploadTemplateFiles(files: {
|
|
94
|
+
name: string;
|
|
95
|
+
buffer: Buffer;
|
|
96
|
+
}[]): Promise<void>;
|
|
97
|
+
private importedTemplateRow;
|
|
98
|
+
openImportedTemplate(resourceName: string): Promise<void>;
|
|
90
99
|
private clickCreateProcessApplication;
|
|
91
100
|
private ensureInResourceContainer;
|
|
92
101
|
returnToResourceContainer(): Promise<void>;
|
|
@@ -669,16 +669,20 @@ class ModelerHomePage {
|
|
|
669
669
|
// entirely (to Optimize, to Operate) and come back via
|
|
670
670
|
// clickCrossComponentProjectFolder(), which lands at workspace root -- the
|
|
671
671
|
// resource is one level down, so re-enter the container first.
|
|
672
|
-
async clickProcessDiagram(name) {
|
|
672
|
+
async clickProcessDiagram(name, exact = false) {
|
|
673
673
|
await this.ensureInResourceContainer();
|
|
674
674
|
// A Design System table row opens through a link laid over the row, which
|
|
675
675
|
// sits on top of the title text and takes its clicks -- so click the link.
|
|
676
676
|
// The link comes before the title in DOM order, so `first()` picks it
|
|
677
677
|
// whenever it exists; the title stays as the fallback for the classic table.
|
|
678
|
-
const row =
|
|
678
|
+
const row = exact
|
|
679
|
+
? this.importedTemplateRow(name)
|
|
680
|
+
: this.rows.filter({ hasText: name });
|
|
681
|
+
if (exact)
|
|
682
|
+
await (0, test_1.expect)(row).toHaveCount(1);
|
|
679
683
|
const process = row
|
|
680
|
-
.getByRole('link', { name })
|
|
681
|
-
.or(row.getByTitle(name))
|
|
684
|
+
.getByRole('link', { name, exact })
|
|
685
|
+
.or(row.getByTitle(name, { exact }))
|
|
682
686
|
.first();
|
|
683
687
|
await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
|
|
684
688
|
await process.click();
|
|
@@ -877,7 +881,7 @@ class ModelerHomePage {
|
|
|
877
881
|
// One container per test rather than one shared with the seeded workspace:
|
|
878
882
|
// deploying is process-application-wide, so a shared container would let one
|
|
879
883
|
// spec's deploy pick up every other spec's half-modelled diagram.
|
|
880
|
-
async enterResourceContainer(name) {
|
|
884
|
+
async enterResourceContainer(name, options = {}) {
|
|
881
885
|
if (this.processApplicationUrl) {
|
|
882
886
|
const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
|
|
883
887
|
if (!stillInside) {
|
|
@@ -902,7 +906,41 @@ class ModelerHomePage {
|
|
|
902
906
|
// this connection step (or failing on the warning). ensureStageCluster()
|
|
903
907
|
// memoizes per test, so runProcessInstance()'s own call is a no-op after
|
|
904
908
|
// this.
|
|
905
|
-
|
|
909
|
+
if (options.configureEnvironment !== false) {
|
|
910
|
+
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async uploadTemplateFiles(files) {
|
|
914
|
+
await (0, test_1.expect)(this.processApplicationPage.createNewDropdown).toBeVisible({
|
|
915
|
+
timeout: 30000,
|
|
916
|
+
});
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
await (0, test_1.expect)(this.importedTemplateRow(file.name.replace(/\.json$/, ''))).toHaveCount(0);
|
|
919
|
+
}
|
|
920
|
+
await this.processApplicationPage.createNewDropdown.click();
|
|
921
|
+
const chooserPromise = this.page.waitForEvent('filechooser');
|
|
922
|
+
await this.page
|
|
923
|
+
.getByRole('menuitem', { name: 'Upload files', exact: true })
|
|
924
|
+
.click();
|
|
925
|
+
await (await chooserPromise).setFiles(files.map((file) => ({ ...file, mimeType: 'application/json' })));
|
|
926
|
+
// The batch handler can report success for a partial upload.
|
|
927
|
+
for (const file of files) {
|
|
928
|
+
const row = this.importedTemplateRow(file.name.replace(/\.json$/, ''));
|
|
929
|
+
await (0, test_1.expect)(row).toHaveCount(1, { timeout: 60000 });
|
|
930
|
+
await (0, test_1.expect)(row).toBeVisible({ timeout: 60000 });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
importedTemplateRow(name) {
|
|
934
|
+
return this.rows.filter({
|
|
935
|
+
has: this.page
|
|
936
|
+
.getByRole('link', { name, exact: true })
|
|
937
|
+
.or(this.page.getByTitle(name, { exact: true }))
|
|
938
|
+
.or(this.page.getByRole('cell', { name, exact: true })),
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async openImportedTemplate(resourceName) {
|
|
942
|
+
await this.clickProcessDiagram(resourceName, true);
|
|
943
|
+
await this.page.waitForURL(/\/element-templates\//, { timeout: 30000 });
|
|
906
944
|
}
|
|
907
945
|
// Which control creates a process application depends on the hub version
|
|
908
946
|
// under test. Since camunda/camunda-hub#25831 it is a plain primary button on
|
|
@@ -351,6 +351,108 @@ test.describe('Connectors User Flow Tests @tasklistV2', () => {
|
|
|
351
351
|
await modelerCreatePage.applyAiAgentSubProcessTemplate();
|
|
352
352
|
});
|
|
353
353
|
});
|
|
354
|
+
test('Agentic Connector Template File Import User Flow', async ({ request, modelerHomePage, modelerCreatePage, connectorTemplatePage, }) => {
|
|
355
|
+
const suffix = await (0, _setup_1.generateRandomStringAsync)(8);
|
|
356
|
+
const source = 'https://raw.githubusercontent.com/camunda/connectors/' +
|
|
357
|
+
'44784a99762663e60b5224578645ecbff4a1065f/' +
|
|
358
|
+
'connectors/agentic-ai/connector-agentic-ai/element-templates/';
|
|
359
|
+
const templates = [
|
|
360
|
+
{
|
|
361
|
+
file: 'agenticai-mcp-client-outbound-connector.json',
|
|
362
|
+
id: 'io.camunda.connectors.agenticai.mcp.client.v0',
|
|
363
|
+
name: 'MCP Client',
|
|
364
|
+
version: 3,
|
|
365
|
+
engine: '^8.9',
|
|
366
|
+
target: 'task',
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
file: 'agenticai-mcp-remote-client-outbound-connector.json',
|
|
370
|
+
id: 'io.camunda.connectors.agenticai.mcp.remoteclient.v0',
|
|
371
|
+
name: 'MCP Remote Client',
|
|
372
|
+
version: 3,
|
|
373
|
+
engine: '^8.9',
|
|
374
|
+
target: 'task',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
file: 'agenticai-adhoctoolsschema-outbound-connector.json',
|
|
378
|
+
id: 'io.camunda.connectors.agenticai.adhoctoolsschema.v1',
|
|
379
|
+
name: 'Ad-hoc tools schema',
|
|
380
|
+
version: 2,
|
|
381
|
+
engine: '^8.8',
|
|
382
|
+
target: 'task',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
file: 'agenticai-a2a-client-webhook-inbound-connector-receive.json',
|
|
386
|
+
id: 'io.camunda.connectors.agenticai.a2a.client.webhook.receive.v0',
|
|
387
|
+
name: 'A2A Client Webhook Receive Task Connector (early access)',
|
|
388
|
+
version: 1,
|
|
389
|
+
engine: '^8.9',
|
|
390
|
+
target: 'receiveTask',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
file: 'agenticai-a2a-client-webhook-inbound-connector-intermediate.json',
|
|
394
|
+
id: 'io.camunda.connectors.agenticai.a2a.client.webhook.intermediate.v0',
|
|
395
|
+
name: 'A2A Client Webhook Intermediate Catch Event Connector (early access)',
|
|
396
|
+
version: 1,
|
|
397
|
+
engine: '^8.9',
|
|
398
|
+
target: 'intermediateCatchEvent',
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
const files = await test.step('Download pinned compatible template files', async () => {
|
|
402
|
+
const files = [];
|
|
403
|
+
for (const template of templates) {
|
|
404
|
+
const response = await request.get(source + template.file);
|
|
405
|
+
(0, test_1.expect)(response.ok(), `Download ${template.file}: ${response.status()}`).toBe(true);
|
|
406
|
+
const buffer = await response.body();
|
|
407
|
+
const content = JSON.parse(buffer.toString('utf8'));
|
|
408
|
+
(0, test_1.expect)(content).toMatchObject({
|
|
409
|
+
id: template.id,
|
|
410
|
+
name: template.name,
|
|
411
|
+
version: template.version,
|
|
412
|
+
engines: { camunda: template.engine },
|
|
413
|
+
});
|
|
414
|
+
(0, test_1.expect)(content).not.toHaveProperty('deprecated');
|
|
415
|
+
files.push({ name: `${template.id}_${suffix}.json`, buffer });
|
|
416
|
+
}
|
|
417
|
+
return files;
|
|
418
|
+
});
|
|
419
|
+
await test.step('Import files into a fresh Workspace project', async () => {
|
|
420
|
+
await modelerHomePage.createDisposableWorkspace(`Template import ${suffix}`);
|
|
421
|
+
await modelerHomePage.enterResourceContainer(`Template import ${suffix}`, {
|
|
422
|
+
configureEnvironment: false,
|
|
423
|
+
});
|
|
424
|
+
await modelerHomePage.uploadTemplateFiles(files);
|
|
425
|
+
});
|
|
426
|
+
await test.step('Publish each imported draft to this Workspace only', async () => {
|
|
427
|
+
for (const template of templates) {
|
|
428
|
+
await modelerHomePage.openImportedTemplate(`${template.id}_${suffix}`);
|
|
429
|
+
await connectorTemplatePage.publishImportedTemplateToWorkspace(template.version);
|
|
430
|
+
await modelerHomePage.returnToResourceContainer();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
await modelerHomePage.clickDiagramTypeDropdown();
|
|
434
|
+
await modelerHomePage.clickBpmnTemplateOption();
|
|
435
|
+
await modelerCreatePage.enterDiagramName(`Imported templates ${suffix}`);
|
|
436
|
+
await modelerCreatePage.setEngineVersion('8.10');
|
|
437
|
+
let previousElementId = 'StartEvent_1';
|
|
438
|
+
const bindings = [];
|
|
439
|
+
for (const template of templates) {
|
|
440
|
+
await test.step(`Apply ${template.name} to its supported element`, async () => {
|
|
441
|
+
const elementId = await modelerCreatePage.appendTemplateTarget(previousElementId, template.target);
|
|
442
|
+
await modelerCreatePage.applyImportedTemplate(elementId, template);
|
|
443
|
+
bindings.push({
|
|
444
|
+
elementId,
|
|
445
|
+
id: template.id,
|
|
446
|
+
version: template.version,
|
|
447
|
+
elementType: template.target === 'task' ? 'serviceTask' : template.target,
|
|
448
|
+
});
|
|
449
|
+
previousElementId = elementId;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
await test.step('Verify exact template IDs and versions in the exported diagram', async () => {
|
|
453
|
+
await modelerCreatePage.assertExportedTemplateBindings(bindings);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
354
456
|
//Skipped due to bug 1252: https://github.com/camunda/c8-cross-component-e2e-tests/issues/1252
|
|
355
457
|
test.skip('Connectors Timer Event User Flow', async ({ page, modelerHomePage, modelerCreatePage, connectorMarketplacePage, connectorTemplatePage, navigationPage, operateProcessInstancePage, operateHomePage, operateProcessesPage, }) => {
|
|
356
458
|
test.slow();
|