@camunda/e2e-test-suite 0.0.1252 → 0.0.1254
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/AppsPage.js +6 -2
- package/dist/pages/8.10/ClusterDetailsPage.d.ts +2 -2
- package/dist/pages/8.10/ClusterDetailsPage.js +75 -31
- package/dist/pages/8.10/ClusterPage.js +30 -2
- package/dist/pages/8.10/ClusterSecretsPage.js +142 -10
- package/dist/pages/8.10/HomePage.d.ts +2 -0
- package/dist/pages/8.10/HomePage.js +40 -2
- package/dist/pages/8.10/LoginPage.d.ts +1 -0
- package/dist/pages/8.10/LoginPage.js +29 -0
- package/dist/pages/8.10/ModelerHomePage.d.ts +8 -0
- package/dist/pages/8.10/ModelerHomePage.js +221 -35
- package/dist/pages/8.10/UtilitiesPage.js +58 -2
- package/dist/pages/8.7/AppsPage.d.ts +1 -0
- package/dist/pages/8.7/AppsPage.js +23 -4
- package/dist/pages/8.7/ClusterDetailsPage.js +14 -2
- package/dist/pages/8.7/ClusterPage.js +8 -0
- package/dist/pages/8.7/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/8.7/ConnectorMarketplacePage.js +24 -2
- package/dist/pages/8.7/ConsoleOrganizationPage.js +11 -0
- package/dist/pages/8.7/FormJsPage.js +30 -4
- package/dist/pages/8.7/ModelerCreatePage.js +33 -1
- package/dist/pages/8.7/ModelerHomePage.d.ts +2 -0
- package/dist/pages/8.7/ModelerHomePage.js +18 -0
- package/dist/pages/8.7/ProcessApplicationPage.d.ts +1 -0
- package/dist/pages/8.7/ProcessApplicationPage.js +87 -0
- package/dist/pages/8.7/UtilitiesPage.js +7 -1
- package/dist/tests/8.10/test-setup.spec.js +5 -2
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +3 -15
- package/dist/utils/modelerCluster.d.ts +13 -0
- package/dist/utils/modelerCluster.js +2 -1
- package/package.json +1 -1
|
@@ -8,6 +8,11 @@ const randomName_1 = require("../../utils/randomName");
|
|
|
8
8
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
9
|
class ModelerHomePage {
|
|
10
10
|
defaultFolderName = 'Cross Component Test Project';
|
|
11
|
+
// The default cluster test-setup.spec.ts's "Create Default Cluster" test
|
|
12
|
+
// provisions for the whole suite -- assigned to the shared workspace's
|
|
13
|
+
// Environments step so downstream specs can deploy to it without a manual
|
|
14
|
+
// assignment step first.
|
|
15
|
+
defaultClusterName = 'Test Cluster';
|
|
11
16
|
// The name hub gives a project on create, before the inline rename commits.
|
|
12
17
|
defaultNewProjectName = 'New project';
|
|
13
18
|
page;
|
|
@@ -26,6 +31,11 @@ class ModelerHomePage {
|
|
|
26
31
|
processApplicationName;
|
|
27
32
|
modelerPageBanner;
|
|
28
33
|
createNewProjectButton;
|
|
34
|
+
createWorkspaceButton;
|
|
35
|
+
workspaceWizardNameInput;
|
|
36
|
+
workspaceWizardSubmitButton;
|
|
37
|
+
workspaceWizardMembersStep;
|
|
38
|
+
workspaceWizardEnvironmentsHeading;
|
|
29
39
|
projectNameInput;
|
|
30
40
|
formNameInput;
|
|
31
41
|
chooseBpmnTemplateButton;
|
|
@@ -61,12 +71,17 @@ class ModelerHomePage {
|
|
|
61
71
|
// Hub's design-system shell renders the app header as a role=banner with no
|
|
62
72
|
// accessible name after the Camunda Hub rebrand, so the classic
|
|
63
73
|
// name-matched banner no longer resolves; the Modeler logo link inside it
|
|
64
|
-
//
|
|
65
|
-
//
|
|
74
|
+
// carries "Camunda Hub home" when the org has workspacesEnabled, else
|
|
75
|
+
// "Camunda Modeler home" (camunda-hub NavShellDs.common.tsx) -- match
|
|
76
|
+
// either. Keep the legacy named-banner branch for shells where the rename
|
|
77
|
+
// has not shipped yet.
|
|
66
78
|
this.modelerPageBanner = page
|
|
67
79
|
.getByRole('banner', {
|
|
68
80
|
name: 'Camunda Modeler',
|
|
69
81
|
})
|
|
82
|
+
.or(page
|
|
83
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
84
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
70
85
|
.or(page
|
|
71
86
|
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
72
87
|
.and(page.locator(':not([href$="/login"])')))
|
|
@@ -83,6 +98,24 @@ class ModelerHomePage {
|
|
|
83
98
|
name: 'Create project',
|
|
84
99
|
exact: true,
|
|
85
100
|
}));
|
|
101
|
+
// Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
|
|
102
|
+
// hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
|
|
103
|
+
// wizard (General -> Members) that names the workspace up front. Same
|
|
104
|
+
// hooks as pages/SM-8.10/ModelerHomePage.ts. exact:true keeps the split
|
|
105
|
+
// button's options chevron out of the match.
|
|
106
|
+
this.createWorkspaceButton = page.getByRole('button', {
|
|
107
|
+
name: 'Create workspace',
|
|
108
|
+
exact: true,
|
|
109
|
+
});
|
|
110
|
+
this.workspaceWizardNameInput = page.locator('#workspace-name');
|
|
111
|
+
this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
|
|
112
|
+
this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
|
|
113
|
+
// When the org has environments enabled, an "Assign environments" step is
|
|
114
|
+
// inserted after Members -- camunda-hub's own
|
|
115
|
+
// e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
|
|
116
|
+
this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
|
|
117
|
+
name: 'Assign environments',
|
|
118
|
+
});
|
|
86
119
|
this.projectNameInput = page.locator('[data-test="editable-input"]');
|
|
87
120
|
this.formNameInput = page.locator('[data-test="editable-input"]');
|
|
88
121
|
this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
|
|
@@ -121,10 +154,13 @@ class ModelerHomePage {
|
|
|
121
154
|
// than one project with this name — an unscoped by-title match throws a
|
|
122
155
|
// strict-mode violation the moment a duplicate exists, which fails every
|
|
123
156
|
// later run in that org rather than just the run that created it.
|
|
157
|
+
// On the Camunda Hub Workspaces overview the folder is a workspace tile
|
|
158
|
+
// whose "Open workspace <name>" button navigates into it.
|
|
124
159
|
this.crossComponentProjectFolder = page
|
|
125
|
-
.
|
|
160
|
+
.getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
|
|
161
|
+
.or(page.getByTitle(this.defaultFolderName, {
|
|
126
162
|
exact: true,
|
|
127
|
-
})
|
|
163
|
+
}))
|
|
128
164
|
.first();
|
|
129
165
|
this.rows = page.getByRole('row');
|
|
130
166
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
@@ -582,7 +618,83 @@ class ModelerHomePage {
|
|
|
582
618
|
.then(() => true)
|
|
583
619
|
.catch(() => false);
|
|
584
620
|
}
|
|
621
|
+
// Drives Camunda Hub's workspace-creation wizard. The name is entered on
|
|
622
|
+
// the General step, which enables "Next"; the same submit button then
|
|
623
|
+
// relabels itself through the remaining steps and finally reads "Create
|
|
624
|
+
// workspace" once it creates the workspace and routes the creator into its
|
|
625
|
+
// projects listing. Gate the Members-step click on that step becoming
|
|
626
|
+
// active so the shared submit button is not clicked twice while still on
|
|
627
|
+
// General.
|
|
628
|
+
async createWorkspaceViaWizard(name) {
|
|
629
|
+
await this.dismissOverlays();
|
|
630
|
+
await this.createWorkspaceButton.click({ timeout: 60000 });
|
|
631
|
+
await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
|
|
632
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
633
|
+
await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
|
|
634
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
635
|
+
// When the org has environments enabled, this only advances the wizard
|
|
636
|
+
// into an "Assign environments" step instead of submitting. Assign the
|
|
637
|
+
// suite's default cluster here rather than submitting with an empty
|
|
638
|
+
// selection -- otherwise this shared workspace starts with no
|
|
639
|
+
// environments and every downstream spec that deploys to it needs its
|
|
640
|
+
// own manual assignment first. Each card's accessible name is
|
|
641
|
+
// "Select <cluster name>" before it is picked (camunda-hub's own
|
|
642
|
+
// create-workspace-environments.spec.tsx asserts the same pattern).
|
|
643
|
+
// isVisible() only samples the current state -- if the wizard's step
|
|
644
|
+
// transition hasn't rendered yet at that exact instant, this would
|
|
645
|
+
// wrongly conclude the org has no environments step and immediately
|
|
646
|
+
// wait for the post-submit URL while the wizard is actually still open
|
|
647
|
+
// (mid-transition), timing out every workspace creation. Give the step
|
|
648
|
+
// transition a bounded wait instead of an instantaneous probe.
|
|
649
|
+
if (await this.becameVisible(this.workspaceWizardEnvironmentsHeading, 5000)) {
|
|
650
|
+
// "Create Default Cluster" runs in parallel with this flow
|
|
651
|
+
// (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
|
|
652
|
+
// lists may still be provisioning -- wait for it a generous 90s rather
|
|
653
|
+
// than sampling once. If it genuinely never appears (the cluster
|
|
654
|
+
// failed rather than just being slow), don't fail the whole shared
|
|
655
|
+
// workspace's setup over it: Hub's own recovery copy for this step
|
|
656
|
+
// ("Could not load environments. You can finish creating the
|
|
657
|
+
// workspace and assign them later.") confirms submitting unassigned
|
|
658
|
+
// is a supported, recoverable path, not a broken one.
|
|
659
|
+
const clusterOption = this.page.getByRole('button', {
|
|
660
|
+
name: new RegExp(`^Select ${this.defaultClusterName}`),
|
|
661
|
+
});
|
|
662
|
+
if (await this.becameVisible(clusterOption, 90000)) {
|
|
663
|
+
await clusterOption.click({ timeout: 10000 });
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
|
|
667
|
+
}
|
|
668
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
669
|
+
}
|
|
670
|
+
await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
|
|
671
|
+
timeout: 120000,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
585
674
|
async clickHomeBreadcrumb() {
|
|
675
|
+
// Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
|
|
676
|
+
// overview, reached through the header logo link so the SPA keeps its
|
|
677
|
+
// in-memory session (a full navigation re-runs the whole Auth0 hand-off).
|
|
678
|
+
// The link's accessible name is "Camunda Hub home" when the org has
|
|
679
|
+
// workspacesEnabled, else "Camunda Modeler home" (camunda-hub
|
|
680
|
+
// NavShellDs.common.tsx) -- match either.
|
|
681
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
682
|
+
// The authenticated header can still expose this same link with its
|
|
683
|
+
// href pointed at /login while the header is mid-update (same race
|
|
684
|
+
// modelerPageBanner already guards against) -- clicking it then sends
|
|
685
|
+
// the flow back through Auth0 and drops the session.
|
|
686
|
+
await this.page
|
|
687
|
+
.locator('header[data-slot="app-header"]')
|
|
688
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
689
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
690
|
+
.or(this.page
|
|
691
|
+
.locator('header[data-slot="app-header"]')
|
|
692
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
693
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
694
|
+
.first()
|
|
695
|
+
.click({ timeout: 30000 });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
586
698
|
try {
|
|
587
699
|
await this.homeBreadcrumb.click();
|
|
588
700
|
}
|
|
@@ -649,38 +761,112 @@ class ModelerHomePage {
|
|
|
649
761
|
},
|
|
650
762
|
});
|
|
651
763
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
764
|
+
// Hub's "Create workspace" CTA is a persistent toolbar button, rendered
|
|
765
|
+
// regardless of whether the existing-workspaces list has finished loading
|
|
766
|
+
// -- so a single slow or failed list fetch must not be read as "no
|
|
767
|
+
// workspace exists yet". Retry the wait once after a reload before
|
|
768
|
+
// concluding the folder is genuinely absent, since creating one when it
|
|
769
|
+
// already exists (just hadn't rendered) leaves a permanent duplicate that
|
|
770
|
+
// breaks every later run for that user.
|
|
771
|
+
async waitForCrossComponentProjectFolder() {
|
|
772
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
773
|
+
const found = await this.crossComponentProjectFolder
|
|
774
|
+
.waitFor({ state: 'visible', timeout: 60000 })
|
|
775
|
+
.then(() => true)
|
|
776
|
+
.catch(() => false);
|
|
777
|
+
if (found)
|
|
778
|
+
return true;
|
|
779
|
+
if (attempt === 0) {
|
|
780
|
+
await this.page.reload();
|
|
781
|
+
await this.page.waitForLoadState('networkidle').catch(() => { });
|
|
782
|
+
}
|
|
668
783
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
// Retries the whole detect-or-create sequence, not just its individual
|
|
787
|
+
// steps. Up to 27 test-user workers race to create/open this one shared
|
|
788
|
+
// workspace concurrently, and a losing worker can land on a workspace-root
|
|
789
|
+
// page that never finishes rendering (neither the folder tile nor a create
|
|
790
|
+
// control ever becomes visible) for reasons the per-step probes/reloads
|
|
791
|
+
// below cannot resolve on their own -- each step bounds itself, but their
|
|
792
|
+
// sum does not bound the whole method, and a single unlucky pass used to
|
|
793
|
+
// run long enough to hit the 12-minute test timeout outright (run
|
|
794
|
+
// 36110457453 lost 6 "Create Project Folder" tests this way, pending
|
|
795
|
+
// inside a reload with no error of its own -- the test timeout fired
|
|
796
|
+
// first). Mirrors the 8.9 sibling's outer retry loop, which this method
|
|
797
|
+
// lost when it grew its own per-step reload-retries in place of it.
|
|
798
|
+
async createCrossComponentProjectFolder() {
|
|
799
|
+
const maxAttempts = 3;
|
|
800
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
801
|
+
await this.dismissOverlays();
|
|
802
|
+
// The projects list renders asynchronously, so an instantaneous
|
|
803
|
+
// isVisible() reports "missing" whenever the list is still loading — and
|
|
804
|
+
// this method then creates a SECOND project with the same name. Those
|
|
805
|
+
// duplicates are permanent and break every subsequent run for that user,
|
|
806
|
+
// so wait for the folder to actually show up before concluding it is
|
|
807
|
+
// absent (this also replaces the blind 15s sleep the check relied on).
|
|
808
|
+
// Re-checked on every attempt: a sibling worker may finish creating the
|
|
809
|
+
// folder while this one is retrying.
|
|
810
|
+
const folderExists = await this.waitForCrossComponentProjectFolder();
|
|
811
|
+
if (folderExists) {
|
|
812
|
+
console.log('Cross Component Project folder already exists. Clicking into it');
|
|
813
|
+
await this.clickCrossComponentProjectFolder();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
// Same joint-probe pattern as clickCreateProcessApplication above: a
|
|
818
|
+
// single ORed probe with a generous timeout settles which shell
|
|
819
|
+
// actually rendered (Hub's "Create workspace" empty state vs. the
|
|
820
|
+
// legacy "New project" / "Create project" button) before a short
|
|
821
|
+
// confirmation decides the branch. A live CI trace showed probing
|
|
822
|
+
// createWorkspaceButton alone for 5s racing Hub's slower loads under
|
|
823
|
+
// worker contention: the probe reported "absent" on a workspaces
|
|
824
|
+
// overview that was merely still loading, sending the flow down the
|
|
825
|
+
// legacy branch -- which that overview never renders.
|
|
826
|
+
const rootLoaded = await this.becameVisible(this.createWorkspaceButton.or(this.createNewProjectButton).first(), 60000);
|
|
827
|
+
if (!rootLoaded) {
|
|
828
|
+
// Mirrors clickCreateProcessApplication's own reload-retry: a reload
|
|
829
|
+
// gives one more 60s window in case the root was still loading
|
|
830
|
+
// rather than genuinely absent, instead of committing to the wrong
|
|
831
|
+
// branch.
|
|
832
|
+
await this.page.reload();
|
|
833
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
834
|
+
await this.becameVisible(this.createWorkspaceButton.or(this.createNewProjectButton).first(), 60000);
|
|
835
|
+
}
|
|
836
|
+
if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
|
|
837
|
+
await this.createWorkspaceViaWizard(this.defaultFolderName);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
await this.clickCreateNewProjectButton();
|
|
841
|
+
await this.enterNewProjectName(this.defaultFolderName);
|
|
842
|
+
// Both branches must leave the browser inside the workspace: the
|
|
843
|
+
// callers go straight on to clickDiagramTypeDropdown(), whose create
|
|
844
|
+
// control only exists there. Creating normally navigates into the new
|
|
845
|
+
// project, but when hub leaves the browser on the projects list the
|
|
846
|
+
// rename happens in the row and no navigation follows.
|
|
847
|
+
//
|
|
848
|
+
// Keyed on "New project", which hub renders only on the projects list
|
|
849
|
+
// and never inside an open project, so it answers which view we are
|
|
850
|
+
// on directly. The folder tile is the wrong signal: on a slow list
|
|
851
|
+
// render it is still absent, which would read as "already inside" and
|
|
852
|
+
// skip a hop that was needed.
|
|
853
|
+
if (await this.becameVisible(this.createNewProjectButton, 5000)) {
|
|
854
|
+
await this.clickCrossComponentProjectFolder();
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
if (attempt >= maxAttempts - 1) {
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
// The stuck-root case above, or a wizard/rename step that threw, can
|
|
863
|
+
// leave the browser on a half-loaded or errored page that retrying
|
|
864
|
+
// in place cannot recover from. Return Home so the next attempt's
|
|
865
|
+
// existence check starts clean, instead of retrying against the same
|
|
866
|
+
// stuck page.
|
|
867
|
+
await this.clickHomeBreadcrumb().catch(() => { });
|
|
868
|
+
console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed; retrying...`);
|
|
869
|
+
}
|
|
684
870
|
}
|
|
685
871
|
}
|
|
686
872
|
async clickManageButton(retries = 3) {
|
|
@@ -10,6 +10,7 @@ const authErrorClassification_1 = require("../../utils/authErrorClassification")
|
|
|
10
10
|
const LoginPage_1 = require("./LoginPage");
|
|
11
11
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
12
12
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
13
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
13
14
|
async function clearWebStorage(page) {
|
|
14
15
|
await page
|
|
15
16
|
.evaluate(() => {
|
|
@@ -101,6 +102,27 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
101
102
|
// stagger sleep below, so a worker only pays it once per process lifetime
|
|
102
103
|
// rather than on every call -- see the comment at the call site.
|
|
103
104
|
const staggeredWorkerTimeouts = new Set();
|
|
105
|
+
const onboardingTourSuppressed = new WeakSet();
|
|
106
|
+
// Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
|
|
107
|
+
// (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
|
|
108
|
+
// "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
|
|
109
|
+
// hides the post-login shell from the accessibility tree until dismissed, which times out
|
|
110
|
+
// assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
|
|
111
|
+
// script on every navigation/reload, so there's no login-response race to lose; the flag is
|
|
112
|
+
// matched by prefix since the user id isn't known ahead of login.
|
|
113
|
+
async function suppressOnboardingTour(page) {
|
|
114
|
+
if (onboardingTourSuppressed.has(page)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
onboardingTourSuppressed.add(page);
|
|
118
|
+
await page.addInitScript((prefixPattern) => {
|
|
119
|
+
const pattern = new RegExp(prefixPattern);
|
|
120
|
+
const originalGetItem = Storage.prototype.getItem;
|
|
121
|
+
Storage.prototype.getItem = function (key) {
|
|
122
|
+
return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
|
|
123
|
+
};
|
|
124
|
+
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
125
|
+
}
|
|
104
126
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
|
|
105
127
|
// A blank credential is not something retrying can fix, and from inside the
|
|
106
128
|
// browser it does not look like a credential problem at all: the form takes
|
|
@@ -131,6 +153,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
131
153
|
'"Skip output ... since it may contain secret" warning on the ' +
|
|
132
154
|
'matching C8_*_PASSWORD output.');
|
|
133
155
|
}
|
|
156
|
+
await suppressOnboardingTour(page);
|
|
134
157
|
let lastError;
|
|
135
158
|
// `timeout` is a stable per-worker identifier (derived from workerIndex),
|
|
136
159
|
// and a worker is a long-lived process across every test/repeat it runs, so
|
|
@@ -142,6 +165,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
142
165
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
143
166
|
try {
|
|
144
167
|
await page.context().clearCookies();
|
|
168
|
+
// The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
|
|
169
|
+
// for the tab's whole lifetime; without clearing it here alongside
|
|
170
|
+
// cookies/storage, a flow that logs a different user into the same
|
|
171
|
+
// page could have getUserToken() return the PREVIOUS user's token.
|
|
172
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
145
173
|
await page.goto('about:blank');
|
|
146
174
|
await page
|
|
147
175
|
.evaluate(() => {
|
|
@@ -196,6 +224,17 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
196
224
|
}
|
|
197
225
|
catch (error) {
|
|
198
226
|
lastError = error;
|
|
227
|
+
// A rejected password or a blocked account is a credential/
|
|
228
|
+
// provisioning-level failure, not a transient one: every attempt
|
|
229
|
+
// resubmits the same env-supplied credentials, so retrying just
|
|
230
|
+
// reproduces the same rejection up to maxRetries times (observed:
|
|
231
|
+
// ~6 minutes burned across 5 identical "Wrong email or password"
|
|
232
|
+
// cycles for one seeded user). Fail on the first rejection instead.
|
|
233
|
+
if (String(error).includes(authErrorClassification_1.AUTH0_INVALID_CREDENTIALS) ||
|
|
234
|
+
String(error).includes(authErrorClassification_1.AUTH0_ACCOUNT_BLOCKED)) {
|
|
235
|
+
console.error(lastError);
|
|
236
|
+
throw lastError;
|
|
237
|
+
}
|
|
199
238
|
if (attempt < maxRetries - 1) {
|
|
200
239
|
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
201
240
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
@@ -731,8 +770,12 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
731
770
|
const homePage = new HomePage_1.HomePage(page);
|
|
732
771
|
if (page.url().includes('?code=') || page.url().includes('&state=')) {
|
|
733
772
|
console.log('OAuth callback detected, waiting for redirect...');
|
|
773
|
+
// Camunda Hub (which replaced Console on SaaS) only exchanges the code
|
|
774
|
+
// once its SPA has booted, and on CI runners that boot alone was measured
|
|
775
|
+
// at ~45s past /login-callback -- a 30s wait aborted the exchange on every
|
|
776
|
+
// attempt.
|
|
734
777
|
await page.waitForURL((url) => !url.toString().includes('?code='), {
|
|
735
|
-
timeout:
|
|
778
|
+
timeout: 120000,
|
|
736
779
|
});
|
|
737
780
|
await (0, sleep_1.sleep)(5000);
|
|
738
781
|
}
|
|
@@ -742,7 +785,20 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
742
785
|
await (0, test_1.expect)(homePage.modelerBanner).toBeVisible({ timeout: 30000 });
|
|
743
786
|
}
|
|
744
787
|
else {
|
|
745
|
-
await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({
|
|
788
|
+
await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
|
|
789
|
+
timeout: 60000,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
// Camunda Hub replaces Console on SaaS (8.10 release notes), and the Hub
|
|
793
|
+
// shell has neither the "Camunda logo Console" link nor the "Open
|
|
794
|
+
// Organizations" switcher, and lands on /workspaces without the org id in
|
|
795
|
+
// the URL. Its sidebar scopes the Clusters/Organization links to the active
|
|
796
|
+
// org, so assert the expected org id there instead.
|
|
797
|
+
if (await homePage.hubHeader.isVisible()) {
|
|
798
|
+
await (0, test_1.expect)(homePage.hubSidebar
|
|
799
|
+
.locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
|
|
800
|
+
.first()).toBeVisible({ timeout: 180000 });
|
|
801
|
+
return;
|
|
746
802
|
}
|
|
747
803
|
await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
|
|
748
804
|
try {
|
|
@@ -20,6 +20,7 @@ declare class AppsPage {
|
|
|
20
20
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
21
21
|
private expandHubClusterGroup;
|
|
22
22
|
private clickHubClusterApp;
|
|
23
|
+
private hubAppLink;
|
|
23
24
|
isHubClusterAppPresent(clusterName: string, appName: string): Promise<boolean>;
|
|
24
25
|
clickModeler(): Promise<void>;
|
|
25
26
|
clickTasklist(clusterName: string): Promise<void>;
|
|
@@ -149,12 +149,31 @@ class AppsPage {
|
|
|
149
149
|
// additional content from the design-system sidebar item, e.g. an icon
|
|
150
150
|
// or badge). The five app names are distinct enough that a substring
|
|
151
151
|
// match carries no ambiguity risk.
|
|
152
|
-
const appLink = this.
|
|
153
|
-
name: appName,
|
|
154
|
-
});
|
|
152
|
+
const appLink = this.hubAppLink(clusterName, appName);
|
|
155
153
|
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
154
|
+
// With Hub environments enabled the sidebar lists environments, whose
|
|
155
|
+
// app links open in a new tab ("Operate in <env> (opens in a new tab)").
|
|
156
|
+
// Callers assert the app on this page, so follow the link here instead.
|
|
157
|
+
const href = await appLink.getAttribute('href');
|
|
158
|
+
if (href && (await appLink.getAttribute('target')) === '_blank') {
|
|
159
|
+
await this.page.goto(href, { waitUntil: 'domcontentloaded' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
156
162
|
await appLink.click({ timeout: 10000 });
|
|
157
163
|
}
|
|
164
|
+
// Inline (expanded rail) the app link carries the app's name. The rail can
|
|
165
|
+
// also start collapsed (camunda-hub seeds SidebarProvider's defaultExpanded
|
|
166
|
+
// from whether its items had loaded at first render); the group toggle then
|
|
167
|
+
// opens a flyout portalled outside "Main navigation", whose links -- like
|
|
168
|
+
// environment rows' -- are named "<app> in <cluster>".
|
|
169
|
+
hubAppLink(clusterName, appName) {
|
|
170
|
+
return this.hubSidebar
|
|
171
|
+
.getByRole('link', { name: appName })
|
|
172
|
+
.or(this.page.getByRole('link', {
|
|
173
|
+
name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
|
|
174
|
+
}))
|
|
175
|
+
.first();
|
|
176
|
+
}
|
|
158
177
|
// Checks whether an app link is present inside the requested cluster's Hub
|
|
159
178
|
// sidebar group, without clicking it. Unlike clickHubClusterApp, absence is
|
|
160
179
|
// an expected, valid outcome here -- this is how callers assert per-cluster
|
|
@@ -163,7 +182,7 @@ class AppsPage {
|
|
|
163
182
|
// rather than a disabled control -- so this never throws on a miss.
|
|
164
183
|
async isHubClusterAppPresent(clusterName, appName) {
|
|
165
184
|
await this.expandHubClusterGroup(clusterName);
|
|
166
|
-
const appLink = this.
|
|
185
|
+
const appLink = this.hubAppLink(clusterName, appName);
|
|
167
186
|
return appLink.isVisible({ timeout: 10000 }).catch(() => false);
|
|
168
187
|
}
|
|
169
188
|
async clickModeler() {
|
|
@@ -78,9 +78,15 @@ class ClusterDetailsPage {
|
|
|
78
78
|
.locator('label')
|
|
79
79
|
.filter({ hasText: /^Secrets$/ });
|
|
80
80
|
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
|
|
81
|
+
// The Hub shell's dialog names its close button through visually hidden
|
|
82
|
+
// text rather than an aria-label, which getByLabel does not see.
|
|
81
83
|
this.closeModalButton = page
|
|
82
84
|
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
83
|
-
.
|
|
85
|
+
.getByRole('button', { name: 'Close', exact: true })
|
|
86
|
+
.or(page
|
|
87
|
+
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
88
|
+
.getByLabel('Close'))
|
|
89
|
+
.first();
|
|
84
90
|
this.createClientButton = page.getByRole('button', {
|
|
85
91
|
name: 'Create new Client',
|
|
86
92
|
});
|
|
@@ -446,7 +452,13 @@ class ClusterDetailsPage {
|
|
|
446
452
|
timeout: 20000,
|
|
447
453
|
});
|
|
448
454
|
await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible();
|
|
449
|
-
|
|
455
|
+
// The Hub shell's credentials dialog is modal and marks the page behind it
|
|
456
|
+
// aria-hidden, so the new client's row is out of the accessibility tree
|
|
457
|
+
// until it closes -- include hidden rows to check its scopes meanwhile.
|
|
458
|
+
await (0, test_1.expect)(this.page
|
|
459
|
+
.getByRole('row', { includeHidden: true })
|
|
460
|
+
.filter({ hasNotText: 'Scopes' })
|
|
461
|
+
.filter({ hasText: name })).toContainText('Zeebe, Tasklist, Operate, Optimize, and Secrets');
|
|
450
462
|
}
|
|
451
463
|
async clickEnvVarsButton() {
|
|
452
464
|
await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
|
|
@@ -144,6 +144,14 @@ class ClusterPage {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
async deleteCluster(name) {
|
|
147
|
+
// A full reload in the Hub shell bounces through /login, the identity
|
|
148
|
+
// provider and /login-callback (~35s) before the SPA pushes its returnUrl
|
|
149
|
+
// back to /clusters. Counting rows mid-bounce throws "Execution context
|
|
150
|
+
// was destroyed", so wait for the clusters page to have settled first.
|
|
151
|
+
await this.page.waitForURL((url) => /\/clusters\/?$/.test(url.pathname), {
|
|
152
|
+
timeout: 120000,
|
|
153
|
+
});
|
|
154
|
+
await (0, test_1.expect)(this.createNewClusterButton).toBeVisible({ timeout: 60000 });
|
|
147
155
|
const clusterInstance = this.cluster(name).first();
|
|
148
156
|
if ((await clusterInstance.count()) <= 0) {
|
|
149
157
|
console.log(`No clusters found with name ${name}, skipping deletion.`);
|
|
@@ -8,6 +8,7 @@ declare class ConnectorMarketplacePage {
|
|
|
8
8
|
readonly addToProjectButton: Locator;
|
|
9
9
|
readonly cancelButton: Locator;
|
|
10
10
|
readonly closeButton: Locator;
|
|
11
|
+
readonly browseAllMarketplaceDetailsButton: Locator;
|
|
11
12
|
constructor(page: Page);
|
|
12
13
|
clickSearchForConnectorTextbox(): Promise<void>;
|
|
13
14
|
fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorMarketplacePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
class ConnectorMarketplacePage {
|
|
5
6
|
page;
|
|
6
7
|
searchForConnectorTextbox;
|
|
@@ -10,9 +11,22 @@ class ConnectorMarketplacePage {
|
|
|
10
11
|
addToProjectButton;
|
|
11
12
|
cancelButton;
|
|
12
13
|
closeButton;
|
|
14
|
+
browseAllMarketplaceDetailsButton;
|
|
13
15
|
constructor(page) {
|
|
14
16
|
this.page = page;
|
|
15
|
-
|
|
17
|
+
// The legacy marketplace modal, or camunda-hub's "Browse all" dialog
|
|
18
|
+
// (2026-09-23), which lists Marketplace connectors alongside the
|
|
19
|
+
// diagram's own elements and reusable assets.
|
|
20
|
+
this.searchForConnectorTextbox = page
|
|
21
|
+
.getByPlaceholder('Search for a connector')
|
|
22
|
+
.or(page.locator('#browse-all-search'))
|
|
23
|
+
.first();
|
|
24
|
+
// A Browse all Marketplace card opens its review step ("Add to project")
|
|
25
|
+
// through this button; self-hosted connectors only link to setup docs.
|
|
26
|
+
this.browseAllMarketplaceDetailsButton = page
|
|
27
|
+
.getByRole('list', { name: 'Marketplace connectors' })
|
|
28
|
+
.getByRole('button', { name: /^View details for .+ from Marketplace$/ })
|
|
29
|
+
.first();
|
|
16
30
|
this.downloadToProjectButton = page
|
|
17
31
|
.getByRole('button', { name: 'Download to project' })
|
|
18
32
|
.first();
|
|
@@ -42,7 +56,15 @@ class ConnectorMarketplacePage {
|
|
|
42
56
|
await this.replaceResourceButton.click({ timeout: 60000 });
|
|
43
57
|
}
|
|
44
58
|
async downloadConnectorToProject() {
|
|
45
|
-
await this.
|
|
59
|
+
await (0, test_1.expect)(this.downloadToProjectButton
|
|
60
|
+
.or(this.browseAllMarketplaceDetailsButton)
|
|
61
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
62
|
+
if (await this.downloadToProjectButton.isVisible()) {
|
|
63
|
+
await this.clickDownloadToProjectButton();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
|
|
67
|
+
}
|
|
46
68
|
let imported = false;
|
|
47
69
|
// A fresh download shows "Add to project"; an already-present resource
|
|
48
70
|
// shows "Replace resource". Pick whichever the ImportModal rendered —
|
|
@@ -515,6 +515,17 @@ class ConsoleOrganizationPage {
|
|
|
515
515
|
// to actually drop before targeting the next row: clicking a delete button
|
|
516
516
|
// while its row is being removed detaches the element from the DOM and the
|
|
517
517
|
// click never lands (retries against a continuously re-rendering table).
|
|
518
|
+
//
|
|
519
|
+
// The table fetches its rows after the tab renders, so sampling the count
|
|
520
|
+
// straight away read 0 while the previous run's groups were still loading
|
|
521
|
+
// -- they were left in place and re-creating them failed "name:duplicate".
|
|
522
|
+
// Give existing rows the chance to render before counting.
|
|
523
|
+
await (0, test_1.expect)(this.createGroupButton).toBeVisible({ timeout: 60000 });
|
|
524
|
+
await this.deleteButton
|
|
525
|
+
.first()
|
|
526
|
+
.waitFor({ state: 'visible', timeout: 20000 })
|
|
527
|
+
.then(() => true)
|
|
528
|
+
.catch(() => false);
|
|
518
529
|
let remaining = await this.deleteButton.count();
|
|
519
530
|
while (remaining > 0) {
|
|
520
531
|
await this.deleteButton.first().click({ timeout: 60000 });
|