@camunda/e2e-test-suite 0.0.1248 → 0.0.1250
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/8.9/AppsPage.d.ts +2 -0
- package/dist/pages/8.9/AppsPage.js +114 -0
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
- package/dist/pages/8.9/ClusterPage.js +49 -8
- package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
- package/dist/pages/8.9/HomePage.d.ts +3 -0
- package/dist/pages/8.9/HomePage.js +44 -5
- package/dist/pages/8.9/LoginPage.d.ts +1 -0
- package/dist/pages/8.9/LoginPage.js +19 -0
- package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/8.9/ModelerHomePage.js +122 -3
- package/dist/pages/8.9/UtilitiesPage.js +60 -2
- 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.9/test-setup.spec.js +11 -4
- package/dist/utils/authErrorClassification.d.ts +5 -0
- package/dist/utils/authErrorClassification.js +49 -1
- package/dist/utils/consoleApiHelpers.d.ts +1 -0
- package/dist/utils/consoleApiHelpers.js +183 -30
- package/dist/utils/resetSession.js +7 -0
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -18,8 +18,10 @@ declare class AppsPage {
|
|
|
18
18
|
readonly consoleLink: Locator;
|
|
19
19
|
readonly camundaComponentsButton: Locator;
|
|
20
20
|
readonly camundaAppsButton: Locator;
|
|
21
|
+
readonly hubSidebar: Locator;
|
|
21
22
|
constructor(page: Page);
|
|
22
23
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
24
|
+
private clickHubClusterApp;
|
|
23
25
|
clickModeler(): Promise<void>;
|
|
24
26
|
clickTasklist(clusterName: string): Promise<void>;
|
|
25
27
|
clickAdmin(clusterName: string): Promise<void>;
|
|
@@ -23,9 +23,15 @@ class AppsPage {
|
|
|
23
23
|
consoleLink;
|
|
24
24
|
camundaComponentsButton;
|
|
25
25
|
camundaAppsButton;
|
|
26
|
+
hubSidebar;
|
|
26
27
|
constructor(page) {
|
|
27
28
|
this.page = page;
|
|
28
29
|
this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
|
|
30
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
31
|
+
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
32
|
+
// -- there is no per-app switcher panel to open on Hub (see
|
|
33
|
+
// clickCamundaApps below). Mirrors pages/8.7/AppsPage.ts.
|
|
34
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
29
35
|
this.appSwitcherButton = page.getByLabel('App Switcher');
|
|
30
36
|
this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
|
|
31
37
|
this.adminLink = page.getByRole('link', { name: /^(Admin|Identity)$/ });
|
|
@@ -75,7 +81,78 @@ class AppsPage {
|
|
|
75
81
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
76
82
|
await cluster.click();
|
|
77
83
|
}
|
|
84
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
85
|
+
// expands to reveal its own per-app child links (Operate/Tasklist/...).
|
|
86
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: "the
|
|
87
|
+
// design system splits the group row into a label control and a chevron
|
|
88
|
+
// control, both labelled by the cluster name; either one opens it" -- and
|
|
89
|
+
// the child links do not exist in the DOM at all until expanded (not just
|
|
90
|
+
// hidden), and are not nested under any container matchable by cluster
|
|
91
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
92
|
+
// (`.first()` because both controls share that name), click it, then
|
|
93
|
+
// look up the app link sidebar-wide rather than scoped to a "group"
|
|
94
|
+
// container that doesn't reliably exist.
|
|
95
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
96
|
+
// Not exact: camunda-hub's own nav-shell-ds.spec.saas.tsx asserts this same
|
|
97
|
+
// link with a substring/regex match (`{name: /Operate/}`), not an exact
|
|
98
|
+
// string -- and CI traces confirm why: even after the toggle click
|
|
99
|
+
// succeeds (no error thrown), an exact-match lookup here still times out
|
|
100
|
+
// with "element(s) not found", meaning the rendered link's accessible
|
|
101
|
+
// name is not byte-for-byte "Operate"/"Tasklist"/"Optimize" (likely
|
|
102
|
+
// additional content from the design-system sidebar item, e.g. an icon
|
|
103
|
+
// or badge). The app names are distinct enough that a substring match
|
|
104
|
+
// carries no ambiguity risk.
|
|
105
|
+
const appLink = this.hubSidebar.getByRole('link', {
|
|
106
|
+
name: appName,
|
|
107
|
+
});
|
|
108
|
+
// Decide whether to expand from clusterName's OWN toggle state, not from
|
|
109
|
+
// whether "some" appLink is already visible sidebar-wide: in a
|
|
110
|
+
// multi-cluster org another cluster's group can already be expanded with
|
|
111
|
+
// the same app name, which made this check pass while clusterName's own
|
|
112
|
+
// group stayed collapsed -- appLink.click() below then fired on the
|
|
113
|
+
// wrong cluster's link (see PR #3627 review).
|
|
114
|
+
const collapsedToggle = this.hubSidebar
|
|
115
|
+
.getByRole('button', { name: clusterName, expanded: false })
|
|
116
|
+
.first();
|
|
117
|
+
if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
118
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
119
|
+
// failure made a broken expansion look successful and left the caller
|
|
120
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
121
|
+
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
122
|
+
// exist or isn't clickable) behind an unrelated timeout.
|
|
123
|
+
await collapsedToggle.click({ timeout: 10000 });
|
|
124
|
+
}
|
|
125
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
126
|
+
await appLink.click({ timeout: 10000 });
|
|
127
|
+
}
|
|
78
128
|
async clickModeler() {
|
|
129
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
130
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
131
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
132
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
133
|
+
// caller that reaches clickModeler() from a cluster/API-client page
|
|
134
|
+
// stranded with the next locator running against the wrong page.
|
|
135
|
+
// Navigate home through the header logo link instead, same as
|
|
136
|
+
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
137
|
+
// Modeler home is a harmless in-SPA no-op.
|
|
138
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
139
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
140
|
+
// The href stays literally "/login" until the client's auth state
|
|
141
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
142
|
+
// clicking it in that window would log the test out instead of
|
|
143
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
144
|
+
// one rather than racing it.
|
|
145
|
+
const homeLink = header
|
|
146
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
147
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
148
|
+
.or(header
|
|
149
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
150
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
151
|
+
.first();
|
|
152
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
153
|
+
await homeLink.click({ timeout: 30000 });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
79
156
|
const maxRetries = 3;
|
|
80
157
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
81
158
|
try {
|
|
@@ -106,6 +183,10 @@ class AppsPage {
|
|
|
106
183
|
throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
|
|
107
184
|
}
|
|
108
185
|
async clickTasklist(clusterName) {
|
|
186
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
187
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
109
190
|
const maxRetries = 3;
|
|
110
191
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
111
192
|
try {
|
|
@@ -144,6 +225,14 @@ class AppsPage {
|
|
|
144
225
|
throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
|
|
145
226
|
}
|
|
146
227
|
async clickAdmin(clusterName) {
|
|
228
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
229
|
+
// Non-anchored, matching the substring lookup clickHubClusterApp uses
|
|
230
|
+
// for Operate/Tasklist/Optimize -- an anchored pattern fails the same
|
|
231
|
+
// way an exact-string match did for those apps if the rendered link's
|
|
232
|
+
// accessible name carries any extra content (icon/badge).
|
|
233
|
+
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
147
236
|
const maxRetries = 5;
|
|
148
237
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
149
238
|
try {
|
|
@@ -218,6 +307,10 @@ class AppsPage {
|
|
|
218
307
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
219
308
|
}
|
|
220
309
|
async clickOperate(clusterName) {
|
|
310
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
311
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
221
314
|
const maxRetries = 3;
|
|
222
315
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
223
316
|
try {
|
|
@@ -252,6 +345,10 @@ class AppsPage {
|
|
|
252
345
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
253
346
|
}
|
|
254
347
|
async clickOptimize(clusterName) {
|
|
348
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
349
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
255
352
|
const maxRetries = 3;
|
|
256
353
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
257
354
|
try {
|
|
@@ -329,6 +426,13 @@ class AppsPage {
|
|
|
329
426
|
}
|
|
330
427
|
}
|
|
331
428
|
async clickCamundaApps(maxRetries = 3) {
|
|
429
|
+
// Camunda Hub's rewritten shells have no app-switcher button to open --
|
|
430
|
+
// Modeler is already Hub's home area and the other apps are reached
|
|
431
|
+
// per-cluster via the persistent sidebar, not through a global switcher
|
|
432
|
+
// panel. Mirrors the same hostname guard in clickModeler() above.
|
|
433
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
332
436
|
await (0, sleep_1.sleep)(2000);
|
|
333
437
|
await this.dismissBlockingOverlays();
|
|
334
438
|
const appButtons = [
|
|
@@ -357,6 +461,16 @@ class AppsPage {
|
|
|
357
461
|
}
|
|
358
462
|
}
|
|
359
463
|
async clickConsoleLink() {
|
|
464
|
+
// Camunda Hub folded Console into the same persistent shell as
|
|
465
|
+
// Modeler/home: Console's Dashboard/Clusters/Organization links are
|
|
466
|
+
// just another section of the same "Main navigation" sidebar, not a
|
|
467
|
+
// separate app to switch into. Its header banner (homePage.consoleBanner,
|
|
468
|
+
// via hubHeader) is already visible from any Hub page, so there is no
|
|
469
|
+
// navigation action needed here -- mirrors the same hostname guard in
|
|
470
|
+
// clickModeler().
|
|
471
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
360
474
|
const maxRetries = 3;
|
|
361
475
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
362
476
|
try {
|