@camunda/e2e-test-suite 0.0.1271 → 0.0.1273
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/ClusterSecretsPage.js +17 -1
- package/dist/pages/8.9/AppsPage.js +10 -1
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +66 -1
- package/dist/pages/8.9/ClusterPage.js +11 -2
- package/dist/pages/8.9/ClusterSecretsPage.js +19 -4
- package/dist/pages/8.9/ConsoleOrganizationPage.js +50 -17
- package/dist/pages/8.9/FormJsPage.js +23 -0
- package/dist/pages/8.9/ModelerCreatePage.d.ts +3 -0
- package/dist/pages/8.9/ModelerCreatePage.js +108 -7
- package/dist/pages/8.9/ModelerHomePage.d.ts +8 -0
- package/dist/pages/8.9/ModelerHomePage.js +116 -7
- package/dist/pages/8.9/ProcessApplicationPage.d.ts +1 -0
- package/dist/pages/8.9/ProcessApplicationPage.js +96 -0
- package/dist/pages/8.9/SettingsPage.js +12 -2
- package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/BrowseAllConnectorMarketplacePage.js +26 -14
- package/dist/pages/SM-8.9/ClusterPage.js +5 -1
- package/dist/resources/Basic_Auth_REST_Connector.bpmn +2 -2
- package/dist/resources/ConnectorsBasicTest.bpmn +6 -6
- package/dist/resources/REST_Connector_Bearer_Auth_Process.bpmn +2 -2
- package/dist/resources/REST_Connector_No_Auth_Process.bpmn +1 -1
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +7 -29
- package/dist/tests/8.10/connectors-user-flows.spec.js +4 -7
- package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +1 -2
- package/dist/tests/8.10/smoke-tests.spec.js +2 -3
- package/dist/tests/8.7/connectors-user-flows.spec.js +10 -14
- package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +1 -2
- package/dist/tests/8.7/smoke-tests.spec.js +3 -3
- package/dist/tests/8.8/connectors-user-flows.spec.js +5 -7
- package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +1 -2
- package/dist/tests/8.8/smoke-tests.spec.js +3 -3
- package/dist/tests/8.9/connectors-user-flows.spec.js +5 -7
- package/dist/tests/8.9/console-user-flows.spec.js +3 -0
- package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +1 -2
- package/dist/tests/8.9/smoke-tests.spec.js +3 -3
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +6 -9
- package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +6 -6
- package/dist/tests/SM-8.10/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.7/connectors-user-flows.spec.js +4 -4
- package/dist/tests/SM-8.7/mt-enabled-user-flows.spec.js +5 -4
- package/dist/tests/SM-8.7/smoke-tests.spec.js +3 -2
- package/dist/tests/SM-8.8/connectors-user-flows.spec.js +4 -4
- package/dist/tests/SM-8.8/mt-enabled-user-flows.spec.js +5 -4
- package/dist/tests/SM-8.8/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.9/connectors-user-flows.spec.js +6 -6
- package/dist/tests/SM-8.9/mt-enabled-user-flows.spec.js +5 -4
- package/dist/tests/SM-8.9/smoke-tests.spec.js +2 -1
- package/dist/tests/c8Run-8.10/connectors-user-flows.spec.js +2 -2
- package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +5 -8
- package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +5 -8
- package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +5 -8
- package/dist/utils/connectorSecrets.js +2 -1
- package/dist/utils/constants.d.ts +2 -0
- package/dist/utils/constants.js +20 -1
- package/package.json +1 -1
- package/resources/Basic_Auth_REST_Connector.bpmn +2 -2
- package/resources/ConnectorsBasicTest.bpmn +6 -6
- package/resources/REST_Connector_Bearer_Auth_Process.bpmn +2 -2
- package/resources/REST_Connector_No_Auth_Process.bpmn +1 -1
|
@@ -7,6 +7,7 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
|
|
|
7
7
|
const randomName_1 = require("../../utils/randomName");
|
|
8
8
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
9
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
10
|
+
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
10
11
|
class ModelerHomePage {
|
|
11
12
|
defaultFolderName = 'Cross Component Test Project';
|
|
12
13
|
// The default cluster test-setup.spec.ts's "Create Default Cluster" test
|
|
@@ -52,6 +53,8 @@ class ModelerHomePage {
|
|
|
52
53
|
messageBanner;
|
|
53
54
|
idpTemplateOption;
|
|
54
55
|
idpEmptyStateButton;
|
|
56
|
+
idpProjectsTab;
|
|
57
|
+
createIdpProjectButton;
|
|
55
58
|
idpApplicationNameInput;
|
|
56
59
|
createIdpApplicationButton;
|
|
57
60
|
dialog;
|
|
@@ -60,6 +63,11 @@ class ModelerHomePage {
|
|
|
60
63
|
blockingDialogDismiss;
|
|
61
64
|
optionsButton;
|
|
62
65
|
formNameBreadcrumb;
|
|
66
|
+
formBreadcrumbButton;
|
|
67
|
+
formActionsButton;
|
|
68
|
+
renameFormMenuItem;
|
|
69
|
+
renameInput;
|
|
70
|
+
renameSubmitButton;
|
|
63
71
|
constructor(page) {
|
|
64
72
|
this.page = page;
|
|
65
73
|
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
@@ -113,7 +121,12 @@ class ModelerHomePage {
|
|
|
113
121
|
});
|
|
114
122
|
this.projectNameInput = page.locator('[data-test="editable-input"]');
|
|
115
123
|
this.formNameInput = page.locator('[data-test="editable-input"]');
|
|
116
|
-
|
|
124
|
+
// Hub's workspace projects page renamed IDP applications to "IDP projects"
|
|
125
|
+
// (camunda-hub CreateIdpApplicationModal.common.tsx).
|
|
126
|
+
this.idpApplicationNameInput = page
|
|
127
|
+
.getByPlaceholder('New IDP Application')
|
|
128
|
+
.or(page.getByPlaceholder('New IDP project'))
|
|
129
|
+
.first();
|
|
117
130
|
this.createIdpApplicationButton = page.getByRole('button', {
|
|
118
131
|
name: 'Create',
|
|
119
132
|
exact: true,
|
|
@@ -130,6 +143,13 @@ class ModelerHomePage {
|
|
|
130
143
|
this.idpEmptyStateButton = page
|
|
131
144
|
.getByRole('button', { name: 'Create IDP application' })
|
|
132
145
|
.first();
|
|
146
|
+
// Hub's workspace projects page (camunda-hub WorkspaceProjects.tsx) has no
|
|
147
|
+
// create menu at the root: IDP projects live on their own tab, created
|
|
148
|
+
// through its "Create IDP project" split button (header or empty state).
|
|
149
|
+
this.idpProjectsTab = page.locator('[data-test="tab-idp-applications"]');
|
|
150
|
+
this.createIdpProjectButton = page
|
|
151
|
+
.getByRole('button', { name: 'Create IDP project', exact: true })
|
|
152
|
+
.first();
|
|
133
153
|
this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
|
|
134
154
|
// Matched on the test id alone: hub's design-system variant of the create
|
|
135
155
|
// menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
|
|
@@ -192,9 +212,31 @@ class ModelerHomePage {
|
|
|
192
212
|
.getByRole('button', { name: /^(Got it|Close|Dismiss)/i })
|
|
193
213
|
.first();
|
|
194
214
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
195
|
-
|
|
215
|
+
// The legacy breadcrumb shows the name directly; Hub's newer nav
|
|
216
|
+
// (isNavV2) embeds it in the current crumb's kebab ("<name> actions").
|
|
217
|
+
this.formNameBreadcrumb = (formName) => page
|
|
218
|
+
.locator('button[data-test="breadcrumb-form"]', {
|
|
196
219
|
hasText: formName,
|
|
197
|
-
})
|
|
220
|
+
})
|
|
221
|
+
.or(page.getByRole('button', { name: `${formName} actions` }))
|
|
222
|
+
.first();
|
|
223
|
+
this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
|
|
224
|
+
// Nav-v2 moves Rename/Duplicate/Delete behind a kebab next to the current
|
|
225
|
+
// (last) crumb (camunda-hub use-file-page-actions.ts); same locator as
|
|
226
|
+
// camunda-hub's own e2e BasePage.breadcrumbActionsButton.
|
|
227
|
+
this.formActionsButton = page
|
|
228
|
+
.getByRole('navigation', { name: 'Context navigation' })
|
|
229
|
+
.or(page.getByRole('navigation', { name: 'Breadcrumb', exact: true }))
|
|
230
|
+
.getByRole('button', { name: / actions$/ })
|
|
231
|
+
.last();
|
|
232
|
+
this.renameFormMenuItem = page
|
|
233
|
+
.locator('[data-test="rename-form"]')
|
|
234
|
+
.or(page.getByRole('menuitem', { name: 'Rename', exact: true }))
|
|
235
|
+
.first();
|
|
236
|
+
// Nav-v2's Rename opens a modal dialog (camunda-hub RenameDialogDs.tsx)
|
|
237
|
+
// instead of the legacy inline breadcrumb editor.
|
|
238
|
+
this.renameInput = page.locator('[data-test="rename-input"]');
|
|
239
|
+
this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
|
|
198
240
|
}
|
|
199
241
|
// An announcement dialog renders over the projects list and swallows every
|
|
200
242
|
// click on it: Playwright reports the project link as visible, enabled and
|
|
@@ -361,10 +403,19 @@ class ModelerHomePage {
|
|
|
361
403
|
// clickCreateProcessApplication, which already covers this for the sibling
|
|
362
404
|
// resource type.
|
|
363
405
|
async startIdpApplicationCreation() {
|
|
364
|
-
const rootLoaded = await this.becameVisible(this.idpEmptyStateButton
|
|
406
|
+
const rootLoaded = await this.becameVisible(this.idpEmptyStateButton
|
|
407
|
+
.or(this.diagramTypeDropdown)
|
|
408
|
+
.or(this.idpProjectsTab)
|
|
409
|
+
.first(), 60000);
|
|
365
410
|
if (!rootLoaded) {
|
|
366
411
|
await this.page.reload();
|
|
367
412
|
}
|
|
413
|
+
if (await this.becameVisible(this.idpProjectsTab, 5000)) {
|
|
414
|
+
await this.idpProjectsTab.click({ timeout: 30000 });
|
|
415
|
+
await (0, test_1.expect)(this.createIdpProjectButton).toBeVisible({ timeout: 60000 });
|
|
416
|
+
await this.createIdpProjectButton.click({ timeout: 30000 });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
368
419
|
if (await this.becameVisible(this.idpEmptyStateButton, 5000)) {
|
|
369
420
|
await this.idpEmptyStateButton.click({ timeout: 60000 });
|
|
370
421
|
return;
|
|
@@ -381,10 +432,61 @@ class ModelerHomePage {
|
|
|
381
432
|
async clickFormOption() {
|
|
382
433
|
await this.formTemplateOption.click();
|
|
383
434
|
}
|
|
435
|
+
// The legacy shell opens a fresh form with its breadcrumb name in inline-edit
|
|
436
|
+
// mode. Nav-v2 has no inline editing: the form stays "New form" until renamed
|
|
437
|
+
// through the crumb's "<name> actions" kebab and the shared rename dialog.
|
|
438
|
+
// Retried as a unit, and confirmed on the crumb, because a re-render
|
|
439
|
+
// (post-create refetch, form store load) can drop the editor mid-fill.
|
|
384
440
|
async enterFormName(name) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
441
|
+
const maxAttempts = 3;
|
|
442
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
443
|
+
try {
|
|
444
|
+
if (await this.formNameBreadcrumb(name).isVisible()) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const editorOpen = await this.becameVisible(this.formNameInput.or(this.renameInput).first(), 15000);
|
|
448
|
+
if (!editorOpen) {
|
|
449
|
+
await this.openFormRenameEditor();
|
|
450
|
+
}
|
|
451
|
+
if (await this.renameInput.isVisible().catch(() => false)) {
|
|
452
|
+
await this.renameInput.fill(name, { timeout: 30000 });
|
|
453
|
+
await (0, test_1.expect)(this.renameInput).toHaveValue(name, { timeout: 5000 });
|
|
454
|
+
await this.renameSubmitButton.click({ timeout: 10000 });
|
|
455
|
+
await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 30000 });
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
await this.formNameInput.click({ timeout: 30000 });
|
|
459
|
+
await this.formNameInput.fill(name);
|
|
460
|
+
await this.formNameInput.press('Enter');
|
|
461
|
+
}
|
|
462
|
+
await (0, test_1.expect)(this.formNameBreadcrumb(name)).toBeVisible({
|
|
463
|
+
timeout: 30000,
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (attempt === maxAttempts) {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
console.warn(`Form rename to "${name}" did not commit ` +
|
|
472
|
+
`(attempt ${attempt}/${maxAttempts}): ${error}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async openFormRenameEditor() {
|
|
477
|
+
await (0, test_1.expect)(this.formBreadcrumbButton.or(this.formActionsButton).first()).toBeVisible({ timeout: 60000 });
|
|
478
|
+
if (await this.formActionsButton.isVisible().catch(() => false)) {
|
|
479
|
+
// Until the new form has loaded, nav-v2 keeps showing the previous
|
|
480
|
+
// page's crumbs (camunda-hub NavProvider), so the last kebab can still
|
|
481
|
+
// belong to the parent container. A fresh form is named "New form";
|
|
482
|
+
// wait for its own crumb before renaming anything.
|
|
483
|
+
await (0, test_1.expect)(this.formActionsButton).toHaveAccessibleName('New form actions', { timeout: 60000 });
|
|
484
|
+
await this.formActionsButton.click({ timeout: 30000 });
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
await this.formBreadcrumbButton.click({ timeout: 60000 });
|
|
488
|
+
}
|
|
489
|
+
await this.renameFormMenuItem.click({ timeout: 30000 });
|
|
388
490
|
}
|
|
389
491
|
// Kept under its original name because it is what the existing call sites
|
|
390
492
|
// already mean: "go back to where my resources live". That is now the
|
|
@@ -711,6 +813,13 @@ class ModelerHomePage {
|
|
|
711
813
|
async selectCluster(clusterName) {
|
|
712
814
|
//Select the correct cluster if multiple exist
|
|
713
815
|
const baseClusterName = clusterName.split('(')[0].trim();
|
|
816
|
+
// With Hub's Environments feature on (camunda-hub#29210) the IDP create
|
|
817
|
+
// dialog picks an Environment row instead of a cluster toggle.
|
|
818
|
+
const hasEnvironmentRow = await this.becameVisible(this.dialog.locator('[data-test^="environment-row-"]').first(), 10000);
|
|
819
|
+
if (hasEnvironmentRow) {
|
|
820
|
+
await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, baseClusterName);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
714
823
|
const cluster = this.dialog.getByText(new RegExp(`^${baseClusterName}`));
|
|
715
824
|
await (0, test_1.expect)(cluster).toBeVisible({
|
|
716
825
|
timeout: 30000,
|
|
@@ -24,6 +24,7 @@ declare class ProcessApplicationPage {
|
|
|
24
24
|
rememberResourceContainer(url: string): void;
|
|
25
25
|
forgetStageCluster(): void;
|
|
26
26
|
ensureStageCluster(clusterName: string): Promise<boolean>;
|
|
27
|
+
private ensureWorkspaceEnvironment;
|
|
27
28
|
private openStageDefinition;
|
|
28
29
|
openByName(name: string): Promise<void>;
|
|
29
30
|
}
|
|
@@ -266,7 +266,19 @@ class ProcessApplicationPage {
|
|
|
266
266
|
// suite is. Re-navigating is the recovery; waiting longer on a page that
|
|
267
267
|
// has already gone wrong is not, which is why the inner timeouts stay
|
|
268
268
|
// short. A genuinely unbindable cluster still fails, with the same error.
|
|
269
|
+
//
|
|
270
|
+
// Under Hub workspaces (/workspaces/<ws>/projects/<id>) there is no
|
|
271
|
+
// /connected-clusters route -- it renders "No match for the current
|
|
272
|
+
// location". With Hub's Environments feature on (camunda-hub#29210),
|
|
273
|
+
// WorkspaceProjectDetail hides the Connected clusters card altogether and
|
|
274
|
+
// the deploy dialog offers the environments assigned to the parent
|
|
275
|
+
// workspace instead, so the binding is made there.
|
|
276
|
+
const workspaceId = paUrl.match(/\/workspaces\/([^/]+)\/projects\//)?.[1];
|
|
269
277
|
await (0, test_1.expect)(async () => {
|
|
278
|
+
if (workspaceId) {
|
|
279
|
+
await this.ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
270
282
|
await this.page.goto(`${paUrl}/connected-clusters`, {
|
|
271
283
|
waitUntil: 'domcontentloaded',
|
|
272
284
|
});
|
|
@@ -277,6 +289,90 @@ class ProcessApplicationPage {
|
|
|
277
289
|
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
278
290
|
return true;
|
|
279
291
|
}
|
|
292
|
+
// Assigns the Environment backed by clusterName to the workspace, from
|
|
293
|
+
// workspace management's Environments tab (camunda-hub
|
|
294
|
+
// WorkspaceEnvironmentsTab). Without the Environments feature the project
|
|
295
|
+
// page still carries the Connected clusters card, whose "Configure" button
|
|
296
|
+
// opens the Define stages dialog -- that path is taken instead.
|
|
297
|
+
//
|
|
298
|
+
// Save replaces the workspace's whole assignment with the picker's
|
|
299
|
+
// selection, and parallel workers share this workspace, so only this
|
|
300
|
+
// cluster's toggle is touched and the result re-read afterwards. A
|
|
301
|
+
// concurrent save that drops it anyway surfaces as a deploy dialog without
|
|
302
|
+
// the cluster, which forgets the binding and comes back here (see
|
|
303
|
+
// forgetStageCluster).
|
|
304
|
+
async ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName) {
|
|
305
|
+
await this.page.goto(paUrl, { waitUntil: 'domcontentloaded' });
|
|
306
|
+
await this.waitForProcessApplication();
|
|
307
|
+
const configure = this.page.getByRole('button', {
|
|
308
|
+
name: 'Configure',
|
|
309
|
+
exact: true,
|
|
310
|
+
});
|
|
311
|
+
// isVisible() is a point-in-time check: the Connected clusters card can
|
|
312
|
+
// still be mounting right after waitForProcessApplication resolves, so an
|
|
313
|
+
// instant read here races the card and can wrongly fall through to the
|
|
314
|
+
// (possibly unavailable) Environments tab below. Give it the same short
|
|
315
|
+
// wait the analogous hasEnvironmentRow checks elsewhere in this PR use.
|
|
316
|
+
const configureAppeared = await configure
|
|
317
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
318
|
+
.then(() => true)
|
|
319
|
+
.catch(() => false);
|
|
320
|
+
if (configureAppeared) {
|
|
321
|
+
await configure.click({ timeout: 30000 });
|
|
322
|
+
await this.defineDevelopmentStage(clusterName);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const manageUrl = `${new URL(paUrl).origin}/workspaces/${workspaceId}/manage?tab=environments`;
|
|
326
|
+
await this.page.goto(manageUrl, { waitUntil: 'domcontentloaded' });
|
|
327
|
+
const heading = this.page.locator('[data-test="manage-environments-heading"]');
|
|
328
|
+
if (!(await heading
|
|
329
|
+
.waitFor({ state: 'visible', timeout: 60000 })
|
|
330
|
+
.then(() => true)
|
|
331
|
+
.catch(() => false))) {
|
|
332
|
+
throw new Error(`Could not bind "${clusterName}" as a deploy target: the project page ` +
|
|
333
|
+
'has no Connected clusters "Configure" control, and the workspace ' +
|
|
334
|
+
`Environments tab did not render at ${manageUrl}.`);
|
|
335
|
+
}
|
|
336
|
+
const editButton = this.page.locator('[data-test="edit-environments-button"]');
|
|
337
|
+
await (0, test_1.expect)(editButton).toBeEnabled({ timeout: 60000 });
|
|
338
|
+
await editButton.click({ timeout: 30000 });
|
|
339
|
+
const editor = this.page.getByRole('group', {
|
|
340
|
+
name: 'Edit assigned environments',
|
|
341
|
+
});
|
|
342
|
+
await (0, test_1.expect)(editor).toBeVisible({ timeout: 30000 });
|
|
343
|
+
const search = editor.locator('[data-test="picker-search"]');
|
|
344
|
+
if (await search.isVisible().catch(() => false)) {
|
|
345
|
+
await search.fill(clusterName);
|
|
346
|
+
}
|
|
347
|
+
// Picker card toggles are named "Select <environment> (<cluster>)" and
|
|
348
|
+
// flip to "Selected ..." with aria-pressed once picked.
|
|
349
|
+
const escapedClusterName = clusterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
350
|
+
const toggle = editor
|
|
351
|
+
.locator('[data-test="picker-environment"]')
|
|
352
|
+
.getByRole('button', {
|
|
353
|
+
name: new RegExp(`^Select(ed)? .*\\(${escapedClusterName}\\)$`),
|
|
354
|
+
})
|
|
355
|
+
.first();
|
|
356
|
+
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 60000 });
|
|
357
|
+
if ((await toggle.getAttribute('aria-pressed')) === 'true') {
|
|
358
|
+
await this.page
|
|
359
|
+
.getByRole('button', { name: 'Cancel', exact: true })
|
|
360
|
+
.click({ timeout: 30000 });
|
|
361
|
+
await (0, test_1.expect)(editor).toBeHidden({ timeout: 30000 });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await toggle.click({ timeout: 30000 });
|
|
365
|
+
await (0, test_1.expect)(toggle).toHaveAttribute('aria-pressed', 'true');
|
|
366
|
+
await this.page
|
|
367
|
+
.getByRole('button', { name: 'Save', exact: true })
|
|
368
|
+
.click({ timeout: 30000 });
|
|
369
|
+
await (0, test_1.expect)(editor).toBeHidden({ timeout: 60000 });
|
|
370
|
+
await (0, test_1.expect)(this.page
|
|
371
|
+
.getByRole('list', { name: 'Assigned environments' })
|
|
372
|
+
.getByRole('listitem')
|
|
373
|
+
.filter({ hasText: clusterName })
|
|
374
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
375
|
+
}
|
|
280
376
|
// The /connected-clusters deep link opens the Define stages dialog by itself
|
|
281
377
|
// while the application has no stage bound. Once one is bound -- a test that
|
|
282
378
|
// deploys to a second cluster -- it has to be reopened from the page, so try
|
|
@@ -7,8 +7,18 @@ class SettingsPage {
|
|
|
7
7
|
logoutButton;
|
|
8
8
|
constructor(page) {
|
|
9
9
|
this.page = page;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// The Camunda Hub shell replaces Console's "Open Settings" header button
|
|
11
|
+
// with the design system's UserMenu, whose trigger carries
|
|
12
|
+
// data-slot="user-menu-trigger" and whose logout entry is a "Log out"
|
|
13
|
+
// menuitem (camunda-hub NavShellDs.common.tsx, nav-shell-ds.spec.saas.tsx).
|
|
14
|
+
this.openSettingsButton = page
|
|
15
|
+
.getByLabel('Open Settings')
|
|
16
|
+
.or(page.locator('[data-slot="user-menu-trigger"]'))
|
|
17
|
+
.first();
|
|
18
|
+
this.logoutButton = page
|
|
19
|
+
.getByRole('button', { name: 'Log out' })
|
|
20
|
+
.or(page.getByRole('menuitem', { name: 'Log out', exact: true }))
|
|
21
|
+
.first();
|
|
12
22
|
}
|
|
13
23
|
async clickOpenSettingsButton(retries = 3) {
|
|
14
24
|
for (let i = 0; i < retries; i++) {
|
|
@@ -11,6 +11,7 @@ declare class BrowseAllConnectorMarketplacePage {
|
|
|
11
11
|
isOpen(): Promise<boolean>;
|
|
12
12
|
waitForConnectorSearchResult(connectorName?: string): Promise<Locator>;
|
|
13
13
|
importConnectorToProject(connectorName?: string): Promise<void>;
|
|
14
|
+
private waitForAnyVisible;
|
|
14
15
|
private completeImportReviewIfNecessary;
|
|
15
16
|
private findConnectorCard;
|
|
16
17
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BrowseAllConnectorMarketplacePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
class BrowseAllConnectorMarketplacePage {
|
|
5
6
|
page;
|
|
6
7
|
searchForConnectorTextbox;
|
|
@@ -52,9 +53,8 @@ class BrowseAllConnectorMarketplacePage {
|
|
|
52
53
|
const retryButton = this.marketplaceDetailFooter.getByRole('button', {
|
|
53
54
|
name: 'Try again',
|
|
54
55
|
});
|
|
55
|
-
const detailState = addButton.or(addedStatus).or(retryButton);
|
|
56
56
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
57
|
-
await
|
|
57
|
+
await this.waitForAnyVisible([addButton, addedStatus, retryButton], 120000);
|
|
58
58
|
if (await addedStatus.isVisible()) {
|
|
59
59
|
break;
|
|
60
60
|
}
|
|
@@ -73,20 +73,35 @@ class BrowseAllConnectorMarketplacePage {
|
|
|
73
73
|
throw new Error('Browse All could not import the Marketplace connector after 3 attempts.');
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
await this.availableElementsHeading
|
|
77
|
-
.or(addedStatus)
|
|
78
|
-
.waitFor({ state: 'visible', timeout: 120000 });
|
|
76
|
+
await this.waitForAnyVisible([this.availableElementsHeading, addedStatus], 120000);
|
|
79
77
|
await browseAllDialog
|
|
80
78
|
.getByRole('button', { name: 'Close' })
|
|
81
79
|
.click({ timeout: 60000 });
|
|
82
80
|
await browseAllDialog.waitFor({ state: 'hidden', timeout: 60000 });
|
|
83
81
|
}
|
|
82
|
+
// Waits until any one of `locators` is visible.
|
|
83
|
+
//
|
|
84
|
+
// `Locator.or()` cannot do this: the union it builds is strict, so the
|
|
85
|
+
// moment two of the states are on screen together `waitFor` throws
|
|
86
|
+
// "strict mode violation ... resolved to 2 elements" rather than
|
|
87
|
+
// resolving. The marketplace shows exactly that pair once an import
|
|
88
|
+
// finishes -- the "Available elements" heading and the "Added to this
|
|
89
|
+
// project" status -- which failed every REST connector flow that imports
|
|
90
|
+
// its connector from Browse All. Polling each state on its own keeps the
|
|
91
|
+
// "whichever comes first" semantics these waits need.
|
|
92
|
+
async waitForAnyVisible(locators, timeout) {
|
|
93
|
+
await (0, test_1.expect)(async () => {
|
|
94
|
+
const visible = await Promise.all(locators.map((locator) => locator.isVisible()));
|
|
95
|
+
(0, test_1.expect)(visible).toContain(true);
|
|
96
|
+
}).toPass({ timeout, intervals: [1000, 2000, 5000] });
|
|
97
|
+
}
|
|
84
98
|
async completeImportReviewIfNecessary(retryButton, addedStatus) {
|
|
85
|
-
await this.
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
await this.waitForAnyVisible([
|
|
100
|
+
this.importModal,
|
|
101
|
+
this.availableElementsHeading,
|
|
102
|
+
addedStatus,
|
|
103
|
+
retryButton,
|
|
104
|
+
], 120000);
|
|
90
105
|
if (await retryButton.isVisible()) {
|
|
91
106
|
return false;
|
|
92
107
|
}
|
|
@@ -106,10 +121,7 @@ class BrowseAllConnectorMarketplacePage {
|
|
|
106
121
|
await confirmButton.click({ timeout: 60000 });
|
|
107
122
|
}
|
|
108
123
|
await this.importModal.waitFor({ state: 'hidden', timeout: 120000 });
|
|
109
|
-
await this.availableElementsHeading
|
|
110
|
-
.or(addedStatus)
|
|
111
|
-
.or(retryButton)
|
|
112
|
-
.waitFor({ state: 'visible', timeout: 120000 });
|
|
124
|
+
await this.waitForAnyVisible([this.availableElementsHeading, addedStatus, retryButton], 120000);
|
|
113
125
|
return !(await retryButton.isVisible());
|
|
114
126
|
}
|
|
115
127
|
async findConnectorCard(connectorName) {
|
|
@@ -22,7 +22,11 @@ class ClusterPage {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
async assertCustomTagsAreVisible(tags) {
|
|
25
|
-
|
|
25
|
+
// No page.waitForLoadState('networkidle') here: the Console clusters page
|
|
26
|
+
// emits continuous polling/SSE traffic, so networkidle never fires and the
|
|
27
|
+
// wait times out on every attempt. The toBeVisible auto-wait below already
|
|
28
|
+
// covers the readiness window this assertion needs. SM-8.8 and SM-8.10
|
|
29
|
+
// dropped the same call for the same reason.
|
|
26
30
|
for (const tag of tags) {
|
|
27
31
|
await (0, test_1.expect)(this.clustersList.getByText(tag, { exact: true })).toBeVisible({
|
|
28
32
|
timeout: 60000,
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
<zeebe:input source="username" target="authentication.username" />
|
|
16
16
|
<zeebe:input source="password" target="authentication.password" />
|
|
17
17
|
<zeebe:input source="GET" target="method" />
|
|
18
|
-
<zeebe:input source="https://
|
|
18
|
+
<zeebe:input source="https://dummyjson.com/http/200/Awesome!" target="url" />
|
|
19
19
|
<zeebe:input source="20" target="connectionTimeoutInSeconds" />
|
|
20
20
|
<zeebe:input source="20" target="readTimeoutInSeconds" />
|
|
21
21
|
</zeebe:ioMapping>
|
|
22
22
|
<zeebe:taskHeaders>
|
|
23
23
|
<zeebe:header key="resultVariable" value="result" />
|
|
24
|
-
<zeebe:header key="resultExpression" value="={message:response.body.
|
|
24
|
+
<zeebe:header key="resultExpression" value="={message:response.body.message}" />
|
|
25
25
|
<zeebe:header key="errorExpression" />
|
|
26
26
|
<zeebe:header key="retryBackoff" value="PT0S" />
|
|
27
27
|
</zeebe:taskHeaders>
|
|
@@ -1041,13 +1041,13 @@
|
|
|
1041
1041
|
<zeebe:properties>
|
|
1042
1042
|
<zeebe:property name="inbound.type" value="io.camunda:http-polling:1" />
|
|
1043
1043
|
<zeebe:property name="authentication.type" value="oauth-client-credentials-flow" />
|
|
1044
|
-
<zeebe:property name="authentication.oauthTokenEndpoint" value="
|
|
1045
|
-
<zeebe:property name="authentication.clientId" value="
|
|
1046
|
-
<zeebe:property name="authentication.clientSecret" value="
|
|
1047
|
-
<zeebe:property name="authentication.clientAuthentication" value="
|
|
1048
|
-
<zeebe:property name="authentication.scopes"
|
|
1044
|
+
<zeebe:property name="authentication.oauthTokenEndpoint" value="=tokenEndpoint" />
|
|
1045
|
+
<zeebe:property name="authentication.clientId" value="=zeebeClientId" />
|
|
1046
|
+
<zeebe:property name="authentication.clientSecret" value="=zeebeSecret" />
|
|
1047
|
+
<zeebe:property name="authentication.clientAuthentication" value="credentialsBody" />
|
|
1048
|
+
<zeebe:property name="authentication.scopes" />
|
|
1049
1049
|
<zeebe:property name="method" value="GET" />
|
|
1050
|
-
<zeebe:property name="url" value="https://
|
|
1050
|
+
<zeebe:property name="url" value="https://dummyjson.com/http/200/Awesome!" />
|
|
1051
1051
|
<zeebe:property name="httpRequestInterval" value="PT15S" />
|
|
1052
1052
|
<zeebe:property name="processPollingInterval" value="PT5S" />
|
|
1053
1053
|
<zeebe:property name="connectionTimeoutInSeconds" value="20" />
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
<zeebe:input source="bearer" target="authentication.type" />
|
|
13
13
|
<zeebe:input source="thisisabearertoken" target="authentication.token" />
|
|
14
14
|
<zeebe:input source="POST" target="method" />
|
|
15
|
-
<zeebe:input source="https://
|
|
15
|
+
<zeebe:input source="https://dummyjson.com/http/200/Awesome!" target="url" />
|
|
16
16
|
<zeebe:input source="20" target="connectionTimeoutInSeconds" />
|
|
17
17
|
<zeebe:input source="20" target="readTimeoutInSeconds" />
|
|
18
18
|
</zeebe:ioMapping>
|
|
19
19
|
<zeebe:taskHeaders>
|
|
20
|
-
<zeebe:header key="resultExpression" value="={message:response.body.
|
|
20
|
+
<zeebe:header key="resultExpression" value="={message:response.body.message}" />
|
|
21
21
|
<zeebe:header key="retryBackoff" value="PT0S" />
|
|
22
22
|
</zeebe:taskHeaders>
|
|
23
23
|
</bpmn:extensionElements>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<zeebe:ioMapping>
|
|
12
12
|
<zeebe:input source="noAuth" target="authentication.type" />
|
|
13
13
|
<zeebe:input source="GET" target="method" />
|
|
14
|
-
<zeebe:input source="https://
|
|
14
|
+
<zeebe:input source="https://dummyjson.com/http/200/Awesome!" target="url" />
|
|
15
15
|
<zeebe:input source="20" target="connectionTimeoutInSeconds" />
|
|
16
16
|
<zeebe:input source="20" target="readTimeoutInSeconds" />
|
|
17
17
|
</zeebe:ioMapping>
|
package/dist/test-setup.d.ts
CHANGED
|
@@ -3,4 +3,3 @@ export declare function captureScreenshot(page: Page, testInfo: TestInfo): Promi
|
|
|
3
3
|
export declare function captureFailureVideo(page: Page, testInfo: TestInfo): Promise<void>;
|
|
4
4
|
export declare function generateRandomStringAsync(length: number): Promise<string>;
|
|
5
5
|
export declare function performBasicAuthPostRequest(requestUrl: string, username: string, password: string): Promise<void>;
|
|
6
|
-
export declare function performBearerTokenAuthPostRequest(requestUrl: string, bearerToken: string): Promise<void>;
|
package/dist/test-setup.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.performBasicAuthPostRequest = exports.generateRandomStringAsync = exports.captureFailureVideo = exports.captureScreenshot = void 0;
|
|
7
7
|
const test_1 = require("@playwright/test");
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
9
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -128,37 +128,15 @@ function generateRandomStringAsync(length) {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
exports.generateRandomStringAsync = generateRandomStringAsync;
|
|
131
|
+
// Fails the calling test when the REST mock the connector is about to call is
|
|
132
|
+
// unreachable, so the flow reports the mock rather than an unexplained Operate
|
|
133
|
+
// incident minutes later. The credentials go in the axios config, not in the
|
|
134
|
+
// second argument: that argument is the request body, so the previous version
|
|
135
|
+
// posted `{auth: {...}}` as JSON and sent no Authorization header at all.
|
|
131
136
|
async function performBasicAuthPostRequest(requestUrl, username, password) {
|
|
132
137
|
await (0, test_1.expect)(async () => {
|
|
133
|
-
const authResponse = await axios_1.default.post(requestUrl, {
|
|
134
|
-
auth: {
|
|
135
|
-
username: username,
|
|
136
|
-
password: password,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
138
|
+
const authResponse = await axios_1.default.post(requestUrl, {}, { auth: { username: username, password: password } });
|
|
139
139
|
(0, test_1.expect)(authResponse.status).toBe(200);
|
|
140
140
|
}).toPass(constants_1.defaultAssertionOptions);
|
|
141
141
|
}
|
|
142
142
|
exports.performBasicAuthPostRequest = performBasicAuthPostRequest;
|
|
143
|
-
async function performBearerTokenAuthPostRequest(requestUrl, bearerToken) {
|
|
144
|
-
try {
|
|
145
|
-
const authToken = bearerToken;
|
|
146
|
-
const apiUrl = requestUrl;
|
|
147
|
-
const response = await axios_1.default.post(apiUrl, {
|
|
148
|
-
headers: {
|
|
149
|
-
Authorization: `Bearer ${authToken}`,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
if (response.status === 200) {
|
|
153
|
-
console.log('Bearer Token API request successful.');
|
|
154
|
-
console.log('Response Data:', response.data);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
console.error('Bearer Token API request failed.');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
console.error('Bearer Token API request error:', error);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
exports.performBearerTokenAuthPostRequest = performBearerTokenAuthPostRequest;
|
|
@@ -25,8 +25,7 @@ _8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
|
|
|
25
25
|
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
26
26
|
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
27
27
|
});
|
|
28
|
-
|
|
29
|
-
_8_10_1.test.skip('REST Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }) => {
|
|
28
|
+
(0, _8_10_1.test)('REST Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }) => {
|
|
30
29
|
_8_10_1.test.slow();
|
|
31
30
|
const processName = 'REST_Connector_No_Auth_Process' + (await (0, _setup_1.generateRandomStringAsync)(3));
|
|
32
31
|
await _8_10_1.test.step('Navigate to Web Modeler', async () => {
|
|
@@ -45,7 +44,7 @@ _8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
|
|
|
45
44
|
await modelerHomePage.clickBpmnTemplateOption();
|
|
46
45
|
});
|
|
47
46
|
await _8_10_1.test.step('Create BPMN Diagram with REST Connector and Start Process Instance', async () => {
|
|
48
|
-
await (0, UtilitiesPage_2.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName,
|
|
47
|
+
await (0, UtilitiesPage_2.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName, constants_1.REST_MOCK_URL, 'noAuth', 'body');
|
|
49
48
|
await modelerCreatePage.runProcessInstance(defaultClusterName);
|
|
50
49
|
});
|
|
51
50
|
await _8_10_1.test.step('View Process Instance in Operate, assert it completes and assert result expression', async () => {
|
|
@@ -79,8 +78,7 @@ _8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
|
|
|
79
78
|
});
|
|
80
79
|
});
|
|
81
80
|
});
|
|
82
|
-
|
|
83
|
-
_8_10_1.test.skip('REST Connector Bearer Token Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }) => {
|
|
81
|
+
(0, _8_10_1.test)('REST Connector Bearer Token Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }) => {
|
|
84
82
|
_8_10_1.test.slow();
|
|
85
83
|
const processName = 'REST_Connector_Bearer_Auth_Process' +
|
|
86
84
|
(await (0, _setup_1.generateRandomStringAsync)(3));
|
|
@@ -100,9 +98,8 @@ _8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
|
|
|
100
98
|
await modelerHomePage.clickBpmnTemplateOption();
|
|
101
99
|
});
|
|
102
100
|
await _8_10_1.test.step('Create BPMN Diagram with REST Connector with Bearer Token Auth and Start Process Instance', async () => {
|
|
103
|
-
await (0, UtilitiesPage_2.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName,
|
|
101
|
+
await (0, UtilitiesPage_2.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName, constants_1.REST_MOCK_URL, 'bearer', '{message:response.body.message}');
|
|
104
102
|
await modelerCreatePage.runProcessInstance(defaultClusterName);
|
|
105
|
-
await (0, _setup_1.performBearerTokenAuthPostRequest)('https://camunda.proxy.beeceptor.com/pre-prod/bearer-auth-test', 'thisisabearertoken');
|
|
106
103
|
});
|
|
107
104
|
await _8_10_1.test.step('View Process Instance in Operate, assert it completes and assert result expression', async () => {
|
|
108
105
|
await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
|
|
@@ -110,8 +110,7 @@ _8_10_1.test.describe('SaaS Migration Path Tests', () => {
|
|
|
110
110
|
await (0, test_1.expect)(page.getByRole('cell', { name: 'Admin' })).toBeVisible();
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
-
|
|
114
|
-
_8_10_1.test.skip('Assert Connectors Still Function After SaaS Upgrade @tasklistV2', async ({ operateHomePage, appsPage, operateProcessesPage, operateProcessInstancePage, }) => {
|
|
113
|
+
(0, _8_10_1.test)('Assert Connectors Still Function After SaaS Upgrade @tasklistV2', async ({ operateHomePage, appsPage, operateProcessesPage, operateProcessInstancePage, }) => {
|
|
115
114
|
_8_10_1.test.slow();
|
|
116
115
|
// Use the same fixed name that was created in the pre-upgrade smoke tests
|
|
117
116
|
const processName = 'REST_Connector_Basic_Auth_ProcessUpgradeTest';
|
|
@@ -292,8 +292,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
|
|
|
292
292
|
await optimizeReportPage.assertUserTaskInstanceCountWithRetry(2, appTab, countRetries, Math.floor(flowBudget(countRetries * constants_1.TIMEOUT.navigation) / countRetries));
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
|
-
|
|
296
|
-
_8_10_1.test.skip('Most Common REST Connector User Flow', async ({ page, loginPage, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }, testInfo) => {
|
|
295
|
+
(0, _8_10_1.test)('Most Common REST Connector User Flow', async ({ page, loginPage, homePage, modelerHomePage, appsPage, modelerCreatePage, connectorSettingsPage, }, testInfo) => {
|
|
297
296
|
_8_10_1.test.slow();
|
|
298
297
|
const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
|
|
299
298
|
const processName = 'REST_Connector_Basic_Auth_Process' + randomString;
|
|
@@ -311,7 +310,7 @@ _8_10_1.test.describe('Smoke Tests', () => {
|
|
|
311
310
|
await modelerHomePage.clickBpmnTemplateOption();
|
|
312
311
|
});
|
|
313
312
|
await _8_10_1.test.step('Create BPMN diagram with REST Connector using Basic Auth', async () => {
|
|
314
|
-
await (0, UtilitiesPage_1.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName,
|
|
313
|
+
await (0, UtilitiesPage_1.modelRestConnector)(modelerCreatePage, connectorSettingsPage, processName, constants_1.REST_MOCK_BASIC_AUTH_URL, 'basic', '{message:response.body.message}', 'result', { username: 'username', password: 'password' });
|
|
315
314
|
});
|
|
316
315
|
await _8_10_1.test.step('Deploy and start process instance', async () => {
|
|
317
316
|
await modelerCreatePage.runProcessInstance(CLUSTER_NAME);
|