@camunda/e2e-test-suite 0.0.1249 → 0.0.1251
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.7/AppsPage.d.ts +7 -3
- package/dist/pages/8.7/AppsPage.js +187 -3
- package/dist/pages/8.7/ClusterPage.js +22 -2
- package/dist/pages/8.7/ClusterSecretsPage.js +22 -1
- package/dist/pages/8.7/HomePage.d.ts +2 -0
- package/dist/pages/8.7/HomePage.js +70 -6
- package/dist/pages/8.7/LoginPage.js +15 -0
- package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -0
- package/dist/pages/8.7/ModelerCreatePage.js +59 -5
- package/dist/pages/8.7/ModelerHomePage.d.ts +11 -0
- package/dist/pages/8.7/ModelerHomePage.js +184 -10
- package/dist/pages/8.7/ProcessApplicationPage.js +27 -7
- package/dist/pages/8.7/UtilitiesPage.js +54 -1
- package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +17 -0
- package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
- package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
- package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
- package/dist/pages/SM-8.10/ClusterPage.js +3 -1
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
- package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
- package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
- package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
- package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
- package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
- package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
- package/dist/pages/SM-8.7/NavigationPage.js +2 -1
- package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.10/smoke-tests.spec.js +2 -1
- package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
- package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.7/smoke-tests.spec.js +2 -1
- package/dist/tests/8.7/test-setup.spec.js +18 -5
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +14 -14
- package/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.8/smoke-tests.spec.js +2 -1
- package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.9/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
- package/dist/utils/modelerCluster.d.ts +4 -6
- package/dist/utils/modelerCluster.js +55 -32
- package/package.json +1 -1
- package/playwright.config.ts +18 -5
|
@@ -2,12 +2,18 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
import { LoginPage } from './LoginPage';
|
|
3
3
|
declare class ModelerHomePage {
|
|
4
4
|
private defaultFolderName;
|
|
5
|
+
private defaultClusterName;
|
|
5
6
|
private page;
|
|
6
7
|
private processApplicationPage;
|
|
7
8
|
private processApplicationUrl?;
|
|
8
9
|
private processApplicationName?;
|
|
9
10
|
readonly modelerPageBanner: Locator;
|
|
10
11
|
readonly createNewProjectButton: Locator;
|
|
12
|
+
readonly createWorkspaceButton: Locator;
|
|
13
|
+
readonly workspaceWizardNameInput: Locator;
|
|
14
|
+
readonly workspaceWizardSubmitButton: Locator;
|
|
15
|
+
readonly workspaceWizardMembersStep: Locator;
|
|
16
|
+
readonly workspaceWizardEnvironmentsHeading: Locator;
|
|
11
17
|
readonly projectNameInput: Locator;
|
|
12
18
|
readonly formNameInput: Locator;
|
|
13
19
|
readonly chooseBpmnTemplateButton: Locator;
|
|
@@ -31,7 +37,11 @@ declare class ModelerHomePage {
|
|
|
31
37
|
readonly closeButton: Locator;
|
|
32
38
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
33
39
|
readonly formBreadcrumbButton: Locator;
|
|
40
|
+
readonly formActionsButton: Locator;
|
|
34
41
|
readonly renameFormMenuItem: Locator;
|
|
42
|
+
readonly renameDialog: Locator;
|
|
43
|
+
readonly renameDialogInput: Locator;
|
|
44
|
+
readonly renameDialogSubmitButton: Locator;
|
|
35
45
|
constructor(page: Page);
|
|
36
46
|
dismissOverlays(): Promise<void>;
|
|
37
47
|
clickCreateNewProjectButton(): Promise<void>;
|
|
@@ -63,6 +73,7 @@ declare class ModelerHomePage {
|
|
|
63
73
|
clickHomeBreadcrumb(): Promise<void>;
|
|
64
74
|
clickOpenOrganizationsButton(): Promise<void>;
|
|
65
75
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
76
|
+
private createWorkspaceViaWizard;
|
|
66
77
|
createCrossComponentProjectFolder(): Promise<void>;
|
|
67
78
|
/**
|
|
68
79
|
* Asserts the Cross Component project folder is listed on the Modeler home
|
|
@@ -9,6 +9,11 @@ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
|
9
9
|
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
10
10
|
class ModelerHomePage {
|
|
11
11
|
defaultFolderName = 'Cross Component Test Project';
|
|
12
|
+
// The default cluster test-setup.spec.ts's "Create Default Cluster" test
|
|
13
|
+
// provisions for the whole suite -- assigned to the shared workspace's
|
|
14
|
+
// Environments step so downstream specs can deploy to it without a manual
|
|
15
|
+
// assignment step first.
|
|
16
|
+
defaultClusterName = 'Test Cluster';
|
|
12
17
|
page;
|
|
13
18
|
// Since camunda/camunda-hub#25831 files and folders cannot sit at a
|
|
14
19
|
// workspace ("project" in today's UI) root -- only process applications and
|
|
@@ -25,6 +30,11 @@ class ModelerHomePage {
|
|
|
25
30
|
processApplicationName;
|
|
26
31
|
modelerPageBanner;
|
|
27
32
|
createNewProjectButton;
|
|
33
|
+
createWorkspaceButton;
|
|
34
|
+
workspaceWizardNameInput;
|
|
35
|
+
workspaceWizardSubmitButton;
|
|
36
|
+
workspaceWizardMembersStep;
|
|
37
|
+
workspaceWizardEnvironmentsHeading;
|
|
28
38
|
projectNameInput;
|
|
29
39
|
formNameInput;
|
|
30
40
|
chooseBpmnTemplateButton;
|
|
@@ -48,13 +58,31 @@ class ModelerHomePage {
|
|
|
48
58
|
closeButton;
|
|
49
59
|
formNameBreadcrumb;
|
|
50
60
|
formBreadcrumbButton;
|
|
61
|
+
formActionsButton;
|
|
51
62
|
renameFormMenuItem;
|
|
63
|
+
renameDialog;
|
|
64
|
+
renameDialogInput;
|
|
65
|
+
renameDialogSubmitButton;
|
|
52
66
|
constructor(page) {
|
|
53
67
|
this.page = page;
|
|
54
68
|
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
55
|
-
|
|
69
|
+
// Hub's design-system shell renders the app header as a role=banner with no
|
|
70
|
+
// accessible name after the Camunda Hub rebrand, so the classic
|
|
71
|
+
// name-matched banner no longer resolves; the Modeler logo link inside it
|
|
72
|
+
// carries "Camunda Modeler home" or, once camunda-hub PR#29349's rename
|
|
73
|
+
// has rolled out to an org, "Camunda Hub home". Keep the legacy
|
|
74
|
+
// named-banner branch for shells where the rename has not shipped yet.
|
|
75
|
+
this.modelerPageBanner = page
|
|
76
|
+
.getByRole('banner', {
|
|
56
77
|
name: 'Camunda Modeler',
|
|
57
|
-
})
|
|
78
|
+
})
|
|
79
|
+
.or(page
|
|
80
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
81
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
82
|
+
.or(page
|
|
83
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
84
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
85
|
+
.first();
|
|
58
86
|
// Hub renamed this button to "Create project" on both the classic and the
|
|
59
87
|
// design-system shell. The legacy label is kept so the flow still works
|
|
60
88
|
// against Modeler builds where the rename has not shipped yet. exact:true
|
|
@@ -67,6 +95,24 @@ class ModelerHomePage {
|
|
|
67
95
|
name: 'Create project',
|
|
68
96
|
exact: true,
|
|
69
97
|
}));
|
|
98
|
+
// Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
|
|
99
|
+
// hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
|
|
100
|
+
// wizard (General -> Members) that names the workspace up front. Same
|
|
101
|
+
// hooks as pages/8.10/ModelerHomePage.ts. exact:true keeps the split
|
|
102
|
+
// button's options chevron out of the match.
|
|
103
|
+
this.createWorkspaceButton = page.getByRole('button', {
|
|
104
|
+
name: 'Create workspace',
|
|
105
|
+
exact: true,
|
|
106
|
+
});
|
|
107
|
+
this.workspaceWizardNameInput = page.locator('#workspace-name');
|
|
108
|
+
this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
|
|
109
|
+
this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
|
|
110
|
+
// When the org has environments enabled, an "Assign environments" step is
|
|
111
|
+
// inserted after Members -- camunda-hub's own
|
|
112
|
+
// e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
|
|
113
|
+
this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
|
|
114
|
+
name: 'Assign environments',
|
|
115
|
+
});
|
|
70
116
|
this.projectNameInput = page.locator('[data-test="editable-input"]');
|
|
71
117
|
this.formNameInput = page.locator('[data-test="editable-input"]');
|
|
72
118
|
this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
|
|
@@ -98,10 +144,13 @@ class ModelerHomePage {
|
|
|
98
144
|
this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
|
|
99
145
|
this.openOrganizationsButton = page.getByLabel('Open Organizations');
|
|
100
146
|
this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
|
|
147
|
+
// On the Camunda Hub Workspaces overview the folder is a workspace tile
|
|
148
|
+
// whose "Open workspace <name>" button navigates into it.
|
|
101
149
|
this.crossComponentProjectFolder = page
|
|
102
|
-
.
|
|
150
|
+
.getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
|
|
151
|
+
.or(page.getByTitle(this.defaultFolderName, {
|
|
103
152
|
exact: true,
|
|
104
|
-
})
|
|
153
|
+
}))
|
|
105
154
|
.first();
|
|
106
155
|
this.rows = page.getByRole('row');
|
|
107
156
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
@@ -116,7 +165,26 @@ class ModelerHomePage {
|
|
|
116
165
|
hasText: formName,
|
|
117
166
|
});
|
|
118
167
|
this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
|
|
119
|
-
|
|
168
|
+
// Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
|
|
169
|
+
// Rename/Duplicate/Delete behind a separate "<name> actions" kebab
|
|
170
|
+
// instead (use-file-page-actions.ts); its own menu item carries no
|
|
171
|
+
// data-test, just the text "Rename".
|
|
172
|
+
this.formActionsButton = page
|
|
173
|
+
.getByRole('button', { name: / actions$/ })
|
|
174
|
+
.first();
|
|
175
|
+
this.renameFormMenuItem = page
|
|
176
|
+
.locator('[data-test="rename-form"]')
|
|
177
|
+
.or(page.getByRole('menuitem', { name: 'Rename', exact: true }));
|
|
178
|
+
// The design-system nav's Rename opens a modal dialog (RenameDialogDs.tsx)
|
|
179
|
+
// instead of the legacy inline breadcrumb editor.
|
|
180
|
+
this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
|
|
181
|
+
this.renameDialogInput = this.renameDialog.getByLabel('Name', {
|
|
182
|
+
exact: true,
|
|
183
|
+
});
|
|
184
|
+
this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
|
|
185
|
+
name: 'Rename',
|
|
186
|
+
exact: true,
|
|
187
|
+
});
|
|
120
188
|
}
|
|
121
189
|
// The release-announcement dialog ("Check out our new release") re-mounts
|
|
122
190
|
// after the projects list re-renders, so one dismissal pass is not enough.
|
|
@@ -300,10 +368,28 @@ class ModelerHomePage {
|
|
|
300
368
|
if (!editorOpen) {
|
|
301
369
|
await this.openFormRenameEditor();
|
|
302
370
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await this.
|
|
371
|
+
// The design-system nav's Rename opens a modal dialog
|
|
372
|
+
// (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
|
|
373
|
+
// editor -- fill and submit whichever is actually open.
|
|
374
|
+
const modalOpen = await this.renameDialog
|
|
375
|
+
.isVisible()
|
|
376
|
+
.catch(() => false);
|
|
377
|
+
if (modalOpen) {
|
|
378
|
+
await this.renameDialogInput.fill(name, { timeout: 45000 });
|
|
379
|
+
await this.renameDialogSubmitButton.click({ timeout: 10000 });
|
|
380
|
+
await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
await this.formNameInput.click({ timeout: 30000 });
|
|
384
|
+
await this.formNameInput.fill(name);
|
|
385
|
+
await this.formNameInput.press('Enter');
|
|
386
|
+
}
|
|
387
|
+
// The legacy breadcrumb shows the new name directly; the
|
|
388
|
+
// design-system kebab instead embeds it in its own accessible name
|
|
389
|
+
// ("<name> actions"). Accept either as confirmation the rename took.
|
|
390
|
+
await this.formNameBreadcrumb(name)
|
|
391
|
+
.or(this.page.getByRole('button', { name: `${name} actions` }))
|
|
392
|
+
.waitFor({
|
|
307
393
|
state: 'visible',
|
|
308
394
|
timeout: 30000,
|
|
309
395
|
});
|
|
@@ -319,7 +405,18 @@ class ModelerHomePage {
|
|
|
319
405
|
}
|
|
320
406
|
}
|
|
321
407
|
async openFormRenameEditor() {
|
|
322
|
-
|
|
408
|
+
// Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
|
|
409
|
+
// the dropdown behind a separate "<name> actions" kebab instead -- prefer
|
|
410
|
+
// it when present, and only fall back to the legacy breadcrumb otherwise.
|
|
411
|
+
const kebabVisible = await this.formActionsButton
|
|
412
|
+
.isVisible({ timeout: 5000 })
|
|
413
|
+
.catch(() => false);
|
|
414
|
+
if (kebabVisible) {
|
|
415
|
+
await this.formActionsButton.click({ timeout: 60000 });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
await this.formBreadcrumbButton.click({ timeout: 60000 });
|
|
419
|
+
}
|
|
323
420
|
await this.renameFormMenuItem.click({ timeout: 30000 });
|
|
324
421
|
}
|
|
325
422
|
// Kept under its original name because it is what the existing call sites
|
|
@@ -432,6 +529,28 @@ class ModelerHomePage {
|
|
|
432
529
|
return this.becameVisible(this.modelerPageBanner, timeout);
|
|
433
530
|
}
|
|
434
531
|
async clickHomeBreadcrumb() {
|
|
532
|
+
// Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
|
|
533
|
+
// overview, reached through the header logo link so the SPA keeps its
|
|
534
|
+
// in-memory session (a full navigation re-runs the whole Auth0 hand-off).
|
|
535
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
536
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
537
|
+
// The href stays literally "/login" until the client's auth state
|
|
538
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
539
|
+
// clicking it in that window would log the test out instead of
|
|
540
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
541
|
+
// one rather than racing it (same hazard AppsPage.clickModeler()
|
|
542
|
+
// guards against).
|
|
543
|
+
const homeLink = header
|
|
544
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
545
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
546
|
+
.or(header
|
|
547
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
548
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
549
|
+
.first();
|
|
550
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
551
|
+
await homeLink.click({ timeout: 30000 });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
435
554
|
try {
|
|
436
555
|
await this.homeBreadcrumb.click();
|
|
437
556
|
}
|
|
@@ -468,6 +587,57 @@ class ModelerHomePage {
|
|
|
468
587
|
},
|
|
469
588
|
});
|
|
470
589
|
}
|
|
590
|
+
// Drives Camunda Hub's workspace-creation wizard. The name is entered on
|
|
591
|
+
// the General step, which enables "Next"; the same submit button then
|
|
592
|
+
// relabels itself through the remaining steps and finally reads "Create
|
|
593
|
+
// workspace" once it creates the workspace and routes the creator into its
|
|
594
|
+
// projects listing. Gate the Members-step click on that step becoming
|
|
595
|
+
// active so the shared submit button is not clicked twice while still on
|
|
596
|
+
// General.
|
|
597
|
+
async createWorkspaceViaWizard(name) {
|
|
598
|
+
await this.dismissOverlays();
|
|
599
|
+
await this.createWorkspaceButton.click({ timeout: 60000 });
|
|
600
|
+
await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
|
|
601
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
602
|
+
await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
|
|
603
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
604
|
+
// When the org has environments enabled, this only advances the wizard
|
|
605
|
+
// into an "Assign environments" step instead of submitting. Assign the
|
|
606
|
+
// suite's default cluster here rather than submitting with an empty
|
|
607
|
+
// selection -- otherwise this shared workspace starts with no
|
|
608
|
+
// environments and every downstream spec that deploys to it needs its
|
|
609
|
+
// own manual assignment first. Each card's accessible name is
|
|
610
|
+
// "Select <cluster name>" before it is picked (camunda-hub's own
|
|
611
|
+
// create-workspace-environments.spec.tsx asserts the same pattern).
|
|
612
|
+
// Sampled immediately (isVisible() doesn't wait) since the step change
|
|
613
|
+
// is a synchronous client-side re-render.
|
|
614
|
+
if (await this.workspaceWizardEnvironmentsHeading
|
|
615
|
+
.isVisible()
|
|
616
|
+
.catch(() => false)) {
|
|
617
|
+
// "Create Default Cluster" runs in parallel with this flow
|
|
618
|
+
// (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
|
|
619
|
+
// lists may still be provisioning -- wait for it a generous 90s rather
|
|
620
|
+
// than sampling once. If it genuinely never appears (the cluster
|
|
621
|
+
// failed rather than just being slow), don't fail the whole shared
|
|
622
|
+
// workspace's setup over it: Hub's own recovery copy for this step
|
|
623
|
+
// ("Could not load environments. You can finish creating the
|
|
624
|
+
// workspace and assign them later.") confirms submitting unassigned
|
|
625
|
+
// is a supported, recoverable path, not a broken one.
|
|
626
|
+
const clusterOption = this.page.getByRole('button', {
|
|
627
|
+
name: new RegExp(`^Select ${this.defaultClusterName}`),
|
|
628
|
+
});
|
|
629
|
+
if (await this.becameVisible(clusterOption, 90000)) {
|
|
630
|
+
await clusterOption.click({ timeout: 10000 });
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
|
|
634
|
+
}
|
|
635
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
636
|
+
}
|
|
637
|
+
await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
|
|
638
|
+
timeout: 120000,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
471
641
|
async createCrossComponentProjectFolder() {
|
|
472
642
|
await this.dismissOverlays();
|
|
473
643
|
// The project list can take well over a minute to render after a session
|
|
@@ -483,6 +653,10 @@ class ModelerHomePage {
|
|
|
483
653
|
await this.clickCrossComponentProjectFolder();
|
|
484
654
|
return;
|
|
485
655
|
}
|
|
656
|
+
if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
|
|
657
|
+
await this.createWorkspaceViaWizard(this.defaultFolderName);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
486
660
|
await this.clickCreateNewProjectButton();
|
|
487
661
|
await this.enterNewProjectName(this.defaultFolderName);
|
|
488
662
|
}
|
|
@@ -48,21 +48,36 @@ class ProcessApplicationPage {
|
|
|
48
48
|
// .first() defensively only: hub renders exactly one of these today, in the
|
|
49
49
|
// workspace-root empty state. A future second placement should not turn
|
|
50
50
|
// every PA-creating test into a strict-mode violation.
|
|
51
|
+
//
|
|
52
|
+
// camunda-hub#25831 renamed the concept from "process application" to
|
|
53
|
+
// "project" (workspace root's CreateSplitButton renders literal text
|
|
54
|
+
// "Create project", opening a dialog titled "Create project"). Accept
|
|
55
|
+
// both names so this resolves whichever the org's live rollout has
|
|
56
|
+
// reached.
|
|
51
57
|
this.createProcessApplicationButton = page
|
|
52
58
|
.getByRole('button', { name: 'Create process application' })
|
|
59
|
+
.or(page.getByRole('button', { name: 'Create project' }))
|
|
53
60
|
.first();
|
|
54
61
|
this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
|
|
55
|
-
this.creationDialog = page
|
|
56
|
-
name: 'Create a process application'
|
|
57
|
-
|
|
62
|
+
this.creationDialog = page
|
|
63
|
+
.getByRole('dialog', { name: 'Create a process application' })
|
|
64
|
+
.or(page.getByRole('dialog', { name: 'Create project' }));
|
|
58
65
|
// Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
|
|
59
66
|
// getByLabel('Name', {exact: true}) for the properties panel, which would
|
|
60
67
|
// resolve to two elements while the creation modal is open.
|
|
61
|
-
|
|
62
|
-
this
|
|
63
|
-
|
|
68
|
+
// CreateProjectModal.tsx labels its field "Project name", not "Name" --
|
|
69
|
+
// accept both so this resolves under either dialog variant.
|
|
70
|
+
this.nameInput = this.creationDialog
|
|
71
|
+
.getByLabel('Name', { exact: true })
|
|
72
|
+
.or(this.creationDialog.getByLabel('Project name', { exact: true }));
|
|
73
|
+
// CreateProjectModal.tsx's submit button reads "Create project", not the
|
|
74
|
+
// exact "Create" the legacy dialog used.
|
|
75
|
+
this.submitButton = this.creationDialog
|
|
76
|
+
.getByRole('button', { name: 'Create', exact: true })
|
|
77
|
+
.or(this.creationDialog.getByRole('button', {
|
|
78
|
+
name: 'Create project',
|
|
64
79
|
exact: true,
|
|
65
|
-
});
|
|
80
|
+
}));
|
|
66
81
|
this.cancelButton = this.creationDialog.getByRole('button', {
|
|
67
82
|
name: 'Cancel',
|
|
68
83
|
exact: true,
|
|
@@ -72,8 +87,13 @@ class ProcessApplicationPage {
|
|
|
72
87
|
// The process application page renders more than one diagram-dropdown
|
|
73
88
|
// (hub's own unit tests reach for it with getAllByTestId), so the dropdown
|
|
74
89
|
// has to be resolved inside the folder explorer rather than page-wide.
|
|
90
|
+
// camunda-hub#25831's workspace-projects flow (WorkspaceProjectDetail.tsx)
|
|
91
|
+
// renders this control as CreateActionDs with test-id
|
|
92
|
+
// "create-action-dropdown" instead of the legacy "diagram-dropdown" --
|
|
93
|
+
// accept both.
|
|
75
94
|
this.createNewDropdown = this.folderExplorer
|
|
76
95
|
.locator('[data-test="diagram-dropdown"]')
|
|
96
|
+
.or(this.folderExplorer.locator('[data-test="create-action-dropdown"]'))
|
|
77
97
|
.first();
|
|
78
98
|
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
79
99
|
this.defineStagesDialog = page
|
|
@@ -7,11 +7,34 @@ const HomePage_1 = require("./HomePage");
|
|
|
7
7
|
const sleep_1 = require("../../utils/sleep");
|
|
8
8
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
9
9
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
10
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
10
11
|
// Tracks which worker-derived `timeout` values have already paid the
|
|
11
12
|
// pre-flight stagger below, so a worker only pays it once per process
|
|
12
13
|
// lifetime rather than on every call -- see the comment at the call sites.
|
|
13
14
|
const staggeredWorkerTimeouts = new Set();
|
|
15
|
+
const onboardingTourSuppressed = new WeakSet();
|
|
16
|
+
// Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
|
|
17
|
+
// (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
|
|
18
|
+
// "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
|
|
19
|
+
// hides the post-login banner link from the accessibility tree until dismissed, which times
|
|
20
|
+
// out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
|
|
21
|
+
// script on every navigation/reload, so there's no login-response race to lose; the flag is
|
|
22
|
+
// matched by prefix since the user id isn't known ahead of login.
|
|
23
|
+
async function suppressOnboardingTour(page) {
|
|
24
|
+
if (onboardingTourSuppressed.has(page)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
onboardingTourSuppressed.add(page);
|
|
28
|
+
await page.addInitScript((prefixPattern) => {
|
|
29
|
+
const pattern = new RegExp(prefixPattern);
|
|
30
|
+
const originalGetItem = Storage.prototype.getItem;
|
|
31
|
+
Storage.prototype.getItem = function (key) {
|
|
32
|
+
return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
|
|
33
|
+
};
|
|
34
|
+
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
35
|
+
}
|
|
14
36
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5, skipOrgAssertion = false) {
|
|
37
|
+
await suppressOnboardingTour(page);
|
|
15
38
|
// Callers derive `timeout` as (workerIndex + 1) * 1000, so it's a stable
|
|
16
39
|
// per-worker identifier: a Playwright worker is a long-lived process that
|
|
17
40
|
// calls loginWithRetry once per test/repeat it runs, and this stagger only
|
|
@@ -32,6 +55,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
32
55
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
33
56
|
try {
|
|
34
57
|
await page.context().clearCookies();
|
|
58
|
+
// The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
|
|
59
|
+
// for the tab's whole lifetime; without clearing it here alongside
|
|
60
|
+
// cookies/storage, a flow that logs a different user into the same
|
|
61
|
+
// page could have getUserToken() return the PREVIOUS user's token.
|
|
62
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
35
63
|
await page.goto('about:blank');
|
|
36
64
|
await page
|
|
37
65
|
.evaluate(() => {
|
|
@@ -429,8 +457,15 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
429
457
|
const homePage = new HomePage_1.HomePage(page);
|
|
430
458
|
if (page.url().includes('?code=') || page.url().includes('&state=')) {
|
|
431
459
|
console.log('OAuth callback detected, waiting for redirect...');
|
|
460
|
+
// Camunda Hub (which replaced Console on SaaS) only exchanges the code
|
|
461
|
+
// once its SPA has booted, and on CI runners that boot alone was measured
|
|
462
|
+
// at ~45s past /login-callback -- a 30s wait aborted the exchange on every
|
|
463
|
+
// attempt. The code is single-use, so falling back to page.goto('/') on
|
|
464
|
+
// timeout (as this used to) doesn't recover a slow-but-valid exchange, it
|
|
465
|
+
// just turns it into a guaranteed failure. Wait longer instead, matching
|
|
466
|
+
// the sibling 8.10 fix.
|
|
432
467
|
await page.waitForURL((url) => !url.toString().includes('?code='), {
|
|
433
|
-
timeout:
|
|
468
|
+
timeout: 120000,
|
|
434
469
|
});
|
|
435
470
|
await (0, sleep_1.sleep)(5000);
|
|
436
471
|
}
|
|
@@ -442,6 +477,24 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
442
477
|
else {
|
|
443
478
|
await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 30000 });
|
|
444
479
|
}
|
|
480
|
+
// Camunda Hub replaced Console + Web Modeler on SaaS: it lands on
|
|
481
|
+
// /workspaces (no org id in the URL) and has no "Open Organizations"
|
|
482
|
+
// button. Its sidebar links are org-scoped, so assert on those instead and
|
|
483
|
+
// switch via the same /org/<id> route Hub's org breadcrumb dropdown uses.
|
|
484
|
+
const isShown = (locator, timeout) => locator
|
|
485
|
+
.waitFor({ state: 'visible', timeout })
|
|
486
|
+
.then(() => true)
|
|
487
|
+
.catch(() => false);
|
|
488
|
+
if (await isShown(homePage.hubSidebar, 10000)) {
|
|
489
|
+
const orgScopedLink = homePage.hubSidebar
|
|
490
|
+
.locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
|
|
491
|
+
.first();
|
|
492
|
+
if (!(await isShown(orgScopedLink, 30000))) {
|
|
493
|
+
await page.goto(`/org/${process.env.ORG_ID}`);
|
|
494
|
+
}
|
|
495
|
+
await (0, test_1.expect)(orgScopedLink).toBeVisible({ timeout: 60000 });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
445
498
|
await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
|
|
446
499
|
try {
|
|
447
500
|
await (0, test_1.expect)(page.url()).toContain(process.env.ORG_ID);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
declare class BrowseAllConnectorMarketplacePage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly searchForConnectorTextbox: Locator;
|
|
5
|
+
private readonly marketplaceConnectorList;
|
|
6
|
+
private readonly marketplaceConnectorCard;
|
|
7
|
+
private readonly marketplaceDetailFooter;
|
|
8
|
+
private readonly availableElementsHeading;
|
|
9
|
+
private readonly importModal;
|
|
10
|
+
constructor(page: Page);
|
|
11
|
+
isOpen(): Promise<boolean>;
|
|
12
|
+
waitForConnectorSearchResult(connectorName?: string): Promise<Locator>;
|
|
13
|
+
importConnectorToProject(connectorName?: string): Promise<void>;
|
|
14
|
+
private completeImportReviewIfNecessary;
|
|
15
|
+
private findConnectorCard;
|
|
16
|
+
}
|
|
17
|
+
export { BrowseAllConnectorMarketplacePage };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BrowseAllConnectorMarketplacePage = void 0;
|
|
4
|
+
class BrowseAllConnectorMarketplacePage {
|
|
5
|
+
page;
|
|
6
|
+
searchForConnectorTextbox;
|
|
7
|
+
marketplaceConnectorList;
|
|
8
|
+
marketplaceConnectorCard;
|
|
9
|
+
marketplaceDetailFooter;
|
|
10
|
+
availableElementsHeading;
|
|
11
|
+
importModal;
|
|
12
|
+
constructor(page) {
|
|
13
|
+
this.page = page;
|
|
14
|
+
this.searchForConnectorTextbox = page.getByRole('searchbox', {
|
|
15
|
+
name: 'Search elements and reusable assets',
|
|
16
|
+
});
|
|
17
|
+
this.marketplaceConnectorList = page.getByRole('list', {
|
|
18
|
+
name: 'Marketplace connectors',
|
|
19
|
+
});
|
|
20
|
+
this.marketplaceConnectorCard = this.marketplaceConnectorList
|
|
21
|
+
.getByRole('button', {
|
|
22
|
+
name: /^View details for .+ from Marketplace$/,
|
|
23
|
+
})
|
|
24
|
+
.first();
|
|
25
|
+
this.marketplaceDetailFooter = page.locator('[data-test="marketplace-detail-footer"]');
|
|
26
|
+
this.availableElementsHeading = page.getByRole('heading', {
|
|
27
|
+
name: 'Available elements',
|
|
28
|
+
});
|
|
29
|
+
this.importModal = page.locator('[data-test="import-modal"]');
|
|
30
|
+
}
|
|
31
|
+
async isOpen() {
|
|
32
|
+
return await this.searchForConnectorTextbox.isVisible();
|
|
33
|
+
}
|
|
34
|
+
async waitForConnectorSearchResult(connectorName) {
|
|
35
|
+
await this.marketplaceConnectorCard.waitFor({
|
|
36
|
+
state: 'visible',
|
|
37
|
+
timeout: 120000,
|
|
38
|
+
});
|
|
39
|
+
return await this.findConnectorCard(connectorName);
|
|
40
|
+
}
|
|
41
|
+
async importConnectorToProject(connectorName) {
|
|
42
|
+
const connectorCard = await this.waitForConnectorSearchResult(connectorName);
|
|
43
|
+
await connectorCard.click({ timeout: 60000 });
|
|
44
|
+
const browseAllDialog = this.page.getByRole('dialog').filter({
|
|
45
|
+
has: this.page.getByRole('button', { name: 'Back to all results' }),
|
|
46
|
+
});
|
|
47
|
+
await browseAllDialog.waitFor({ state: 'visible', timeout: 60000 });
|
|
48
|
+
const addButton = this.marketplaceDetailFooter.getByRole('button', {
|
|
49
|
+
name: /^(Add to project|Add remaining elements)$/,
|
|
50
|
+
});
|
|
51
|
+
const addedStatus = this.marketplaceDetailFooter.getByText('Added to this project', { exact: true });
|
|
52
|
+
const retryButton = this.marketplaceDetailFooter.getByRole('button', {
|
|
53
|
+
name: 'Try again',
|
|
54
|
+
});
|
|
55
|
+
const detailState = addButton.or(addedStatus).or(retryButton);
|
|
56
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
57
|
+
await detailState.waitFor({ state: 'visible', timeout: 120000 });
|
|
58
|
+
if (await addedStatus.isVisible()) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (await retryButton.isVisible()) {
|
|
62
|
+
if (attempt === 3) {
|
|
63
|
+
throw new Error('Browse All could not inspect the Marketplace connector after 3 attempts.');
|
|
64
|
+
}
|
|
65
|
+
await retryButton.click({ timeout: 60000 });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await addButton.click({ timeout: 60000 });
|
|
69
|
+
if (await this.completeImportReviewIfNecessary(retryButton)) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (attempt === 3) {
|
|
73
|
+
throw new Error('Browse All could not import the Marketplace connector after 3 attempts.');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await this.availableElementsHeading.waitFor({
|
|
77
|
+
state: 'visible',
|
|
78
|
+
timeout: 120000,
|
|
79
|
+
});
|
|
80
|
+
await browseAllDialog
|
|
81
|
+
.getByRole('button', { name: 'Close' })
|
|
82
|
+
.click({ timeout: 60000 });
|
|
83
|
+
await browseAllDialog.waitFor({ state: 'hidden', timeout: 60000 });
|
|
84
|
+
}
|
|
85
|
+
async completeImportReviewIfNecessary(retryButton) {
|
|
86
|
+
await this.importModal
|
|
87
|
+
.or(this.availableElementsHeading)
|
|
88
|
+
.or(retryButton)
|
|
89
|
+
.waitFor({ state: 'visible', timeout: 120000 });
|
|
90
|
+
if (await retryButton.isVisible()) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!(await this.importModal.isVisible())) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const confirmButton = this.importModal
|
|
97
|
+
.getByRole('button', {
|
|
98
|
+
name: /^(Add to project|Replace resource|Save as copy)$/,
|
|
99
|
+
})
|
|
100
|
+
.first();
|
|
101
|
+
const confirmationVisible = await confirmButton
|
|
102
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
103
|
+
.then(() => true)
|
|
104
|
+
.catch(() => false);
|
|
105
|
+
if (confirmationVisible) {
|
|
106
|
+
await confirmButton.click({ timeout: 60000 });
|
|
107
|
+
}
|
|
108
|
+
await this.importModal.waitFor({ state: 'hidden', timeout: 120000 });
|
|
109
|
+
await this.availableElementsHeading
|
|
110
|
+
.or(retryButton)
|
|
111
|
+
.waitFor({ state: 'visible', timeout: 120000 });
|
|
112
|
+
return !(await retryButton.isVisible());
|
|
113
|
+
}
|
|
114
|
+
async findConnectorCard(connectorName) {
|
|
115
|
+
if (!connectorName) {
|
|
116
|
+
return this.marketplaceConnectorCard;
|
|
117
|
+
}
|
|
118
|
+
const exactCard = this.marketplaceConnectorList.getByRole('button', {
|
|
119
|
+
name: `View details for ${connectorName} from Marketplace`,
|
|
120
|
+
exact: true,
|
|
121
|
+
});
|
|
122
|
+
const searchWords = connectorName
|
|
123
|
+
.replace(/\s+connector$/i, '')
|
|
124
|
+
.trim()
|
|
125
|
+
.split(/\s+/)
|
|
126
|
+
.map(escapeRegExp)
|
|
127
|
+
.join('.*');
|
|
128
|
+
const relatedCard = this.marketplaceConnectorList
|
|
129
|
+
.getByRole('button', {
|
|
130
|
+
name: new RegExp(`^View details for .*${searchWords}.* from Marketplace$`, 'i'),
|
|
131
|
+
})
|
|
132
|
+
.first();
|
|
133
|
+
if (await exactCard.isVisible()) {
|
|
134
|
+
return exactCard;
|
|
135
|
+
}
|
|
136
|
+
if (await relatedCard.isVisible()) {
|
|
137
|
+
return relatedCard;
|
|
138
|
+
}
|
|
139
|
+
return this.marketplaceConnectorCard;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.BrowseAllConnectorMarketplacePage = BrowseAllConnectorMarketplacePage;
|
|
143
|
+
function escapeRegExp(value) {
|
|
144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
145
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ClusterDetailsPage {
|
|
3
3
|
private page;
|
|
4
|
-
readonly
|
|
4
|
+
readonly clusterDetailsHeading: Locator;
|
|
5
5
|
constructor(page: Page);
|
|
6
6
|
assertCustomPropertyIsNavigated(linkTitle: string, linkName: string, link: string | RegExp): Promise<void>;
|
|
7
7
|
}
|