@camunda/e2e-test-suite 0.0.1478 → 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/8.8/FormJsPage.js +15 -1
- package/dist/pages/8.8/ModelerCreatePage.js +14 -1
- package/dist/pages/8.8/ModelerHomePage.js +13 -1
- 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/dist/utils/apiHelpers.js +35 -20
- package/package.json +1 -1
|
@@ -209,7 +209,21 @@ class FormJsPage {
|
|
|
209
209
|
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
210
210
|
'binding did not take. It will be rebound on the next attempt.');
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
// The probe above runs the instant the dialog opens, before its cluster
|
|
213
|
+
// list has rendered, so a selectable list can be mistaken for the
|
|
214
|
+
// auto-selecting one. The dialog then deploys to whichever cluster it
|
|
215
|
+
// pre-selected: a nightly trace shows the form going to "Test Cluster"
|
|
216
|
+
// while "AWS Cluster" was requested, so the user task linking it never
|
|
217
|
+
// reached Tasklist. Re-probe now that the name has rendered and select
|
|
218
|
+
// the cluster explicitly when it turns out to be a list row.
|
|
219
|
+
const isSelectableRow = await legacyClusterToggle
|
|
220
|
+
.first()
|
|
221
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
222
|
+
.then(() => true)
|
|
223
|
+
.catch(() => false);
|
|
224
|
+
if (!isSelectableRow) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
213
227
|
}
|
|
214
228
|
// "HealthydevManage" was the Carbon row's chrome concatenated together.
|
|
215
229
|
// The design-system dialog renders the cluster row as
|
|
@@ -879,7 +879,20 @@ class ModelerCreatePage {
|
|
|
879
879
|
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
880
880
|
'binding did not take. It will be rebound on the next attempt.');
|
|
881
881
|
}
|
|
882
|
-
|
|
882
|
+
// The probe above runs the instant the dialog opens, before its cluster
|
|
883
|
+
// list has rendered, so a selectable list can be mistaken for the
|
|
884
|
+
// auto-selecting one and the run goes to whichever cluster the dialog
|
|
885
|
+
// pre-selected (seen in FormJsPage.selectCluster's nightly trace).
|
|
886
|
+
// Re-probe now that the name has rendered and select the cluster
|
|
887
|
+
// explicitly when it turns out to be a list row.
|
|
888
|
+
const isSelectableRow = await legacyClusterToggle
|
|
889
|
+
.first()
|
|
890
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
891
|
+
.then(() => true)
|
|
892
|
+
.catch(() => false);
|
|
893
|
+
if (!isSelectableRow) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
883
896
|
}
|
|
884
897
|
const healthyRegex = new RegExp(`${clusterName}.*?Healthy`);
|
|
885
898
|
const clusterLabel = this.dialog
|
|
@@ -1169,7 +1169,19 @@ class ModelerHomePage {
|
|
|
1169
1169
|
// rows. Fall back to clicking the row itself (it is `cursor-pointer` and
|
|
1170
1170
|
// handles the row activation) when the id is not exposed.
|
|
1171
1171
|
const rowMenuTrigger = projectEntryRow.locator('[data-test="project-row-menu-trigger"]');
|
|
1172
|
-
|
|
1172
|
+
let rowVisible = await this.becameVisible(rowMenuTrigger, 10000);
|
|
1173
|
+
// Hub can drop the session while the list loads (its projects call answers
|
|
1174
|
+
// 230 and the SPA bounces to /login), so the probe above misses a project
|
|
1175
|
+
// that exists and the create branch below adds an empty duplicate -- in
|
|
1176
|
+
// which the caller's diagram can never be found. Re-sign-in (often a
|
|
1177
|
+
// silent SSO round trip that has already finished by now, so do not gate
|
|
1178
|
+
// on recoverModelerSession's result) returns the browser to this list;
|
|
1179
|
+
// look again before creating anything.
|
|
1180
|
+
if (!rowVisible) {
|
|
1181
|
+
await this.recoverModelerSession();
|
|
1182
|
+
rowVisible = await this.becameVisible(rowMenuTrigger, 30000);
|
|
1183
|
+
}
|
|
1184
|
+
if (rowVisible) {
|
|
1173
1185
|
const projectId = await rowMenuTrigger.getAttribute('data-project-id');
|
|
1174
1186
|
const workspaceId = new URL(this.page.url()).pathname.match(/\/workspaces\/([^/]+)/)?.[1];
|
|
1175
1187
|
if (workspaceId) {
|
|
@@ -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();
|
package/dist/utils/apiHelpers.js
CHANGED
|
@@ -273,31 +273,46 @@ async function authSaasAPI(audience, clusterType) {
|
|
|
273
273
|
// "You are being rate limited" page when many specs fetch a token at once
|
|
274
274
|
// (each MCP spec authenticates in beforeAll). Retry with exponential
|
|
275
275
|
// backoff so the rate-limit window clears instead of failing the suite.
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
// While throttled, Cloudflare may also hold the connection open, so a
|
|
277
|
+
// request timeout is retried like a 429 rather than aborting the loop.
|
|
278
|
+
const maxAttempts = 7;
|
|
279
|
+
const baseDelayMs = 5000;
|
|
280
|
+
const maxDelayMs = 60000;
|
|
281
|
+
const requestTimeoutMs = 30000;
|
|
278
282
|
let lastErrorMessage = '';
|
|
279
283
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
284
|
+
let retryAfterMs = 0;
|
|
285
|
+
try {
|
|
286
|
+
const tokenResponse = await apiRequestContext.post(creds.authUrl, {
|
|
287
|
+
headers: {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
},
|
|
290
|
+
data: {
|
|
291
|
+
audience: tokenAudience,
|
|
292
|
+
client_id: creds.clientId,
|
|
293
|
+
client_secret: creds.clientSecret,
|
|
294
|
+
grant_type: 'client_credentials',
|
|
295
|
+
},
|
|
296
|
+
timeout: requestTimeoutMs,
|
|
297
|
+
});
|
|
298
|
+
if (tokenResponse.status() === 200) {
|
|
299
|
+
const tokenJson = await tokenResponse.json();
|
|
300
|
+
return `Bearer ${tokenJson.access_token}`;
|
|
301
|
+
}
|
|
302
|
+
const retryAfter = Number(tokenResponse.headers()['retry-after']);
|
|
303
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
304
|
+
retryAfterMs = Math.min(retryAfter * 1000, maxDelayMs);
|
|
305
|
+
}
|
|
306
|
+
const errorBody = await tokenResponse.text();
|
|
307
|
+
lastErrorMessage = `HTTP ${tokenResponse.status()} - ${errorBody.slice(0, 200)}`;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
lastErrorMessage = error.message.split('\n')[0];
|
|
294
311
|
}
|
|
295
|
-
const errorBody = await tokenResponse.text();
|
|
296
|
-
lastErrorMessage = `HTTP ${tokenResponse.status()} - ${errorBody.slice(0, 200)}`;
|
|
297
312
|
if (attempt < maxAttempts) {
|
|
313
|
+
const backoffMs = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
|
|
298
314
|
console.warn(`authSaasAPI attempt ${attempt}/${maxAttempts} failed to fetch access token (${lastErrorMessage}); retrying after backoff...`);
|
|
299
|
-
await new Promise((r) => setTimeout(r,
|
|
300
|
-
continue;
|
|
315
|
+
await new Promise((r) => setTimeout(r, Math.max(backoffMs, retryAfterMs)));
|
|
301
316
|
}
|
|
302
317
|
}
|
|
303
318
|
throw new Error(`Failed to fetch access token. Response: ${lastErrorMessage}`);
|