@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.
@@ -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): Promise<void>;
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 = this.rows.filter({ hasText: name });
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
- await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1479",
3
+ "version": "0.0.1480",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",