@camunda/e2e-test-suite 0.0.1253 → 0.0.1255
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.js +65 -11
- package/dist/pages/8.7/ClusterPage.js +22 -20
- package/dist/pages/8.7/ConsoleOrganizationPage.js +37 -6
- package/dist/pages/8.7/FormJsPage.js +36 -9
- package/dist/pages/8.7/LoginPage.js +4 -0
- package/dist/pages/8.7/ModelerCreatePage.js +72 -24
- package/dist/tests/8.10/test-setup.spec.js +5 -2
- package/dist/tests/8.7/test-setup.spec.js +16 -0
- 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 {
|
|
@@ -81,6 +81,40 @@ class AppsPage {
|
|
|
81
81
|
// (see clickHubClusterApp and isHubClusterAppPresent below) gets at most
|
|
82
82
|
// one match, scoped to this cluster.
|
|
83
83
|
async expandHubClusterGroup(clusterName) {
|
|
84
|
+
const toggle = this.hubSidebar
|
|
85
|
+
.getByRole('button', { name: clusterName })
|
|
86
|
+
.first();
|
|
87
|
+
// The diagram editor (and other full-screen views reached from it, e.g.
|
|
88
|
+
// right after runProcessInstance/deployDiagram) has no "Main navigation"
|
|
89
|
+
// sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
|
|
90
|
+
// page's only <nav> is aria-label="Context navigation" (the breadcrumb),
|
|
91
|
+
// and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
|
|
92
|
+
// appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
|
|
93
|
+
// no switcher panel to open), so a caller reaching clickOperate/
|
|
94
|
+
// clickTasklist/clickOptimize straight from a diagram was left on a page
|
|
95
|
+
// this sidebar could never resolve on, no matter how the toggle itself
|
|
96
|
+
// is matched. Navigate home first, the same way clickModeler() does,
|
|
97
|
+
// whenever the sidebar isn't already there.
|
|
98
|
+
//
|
|
99
|
+
// A freshly re-authenticated page can also land on a workspace-agnostic
|
|
100
|
+
// route (e.g. bare /workspaces right after loginWithTestUser(), which
|
|
101
|
+
// clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
|
|
102
|
+
// sidebar renders but has not resolved this cluster's own group yet.
|
|
103
|
+
// Confirmed from a nightly trace: both the getAttribute and click below
|
|
104
|
+
// timed out (10s each) against this exact toggle while the URL sat on
|
|
105
|
+
// /workspaces, right after a mid-test re-login. Route through the same
|
|
106
|
+
// clickModeler() recovery whenever the toggle itself hasn't shown up
|
|
107
|
+
// either, not only when the whole sidebar is missing.
|
|
108
|
+
const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
|
|
109
|
+
(await toggle
|
|
110
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
111
|
+
.then(() => true)
|
|
112
|
+
.catch(() => false));
|
|
113
|
+
if (!sidebarReady) {
|
|
114
|
+
await this.clickModeler();
|
|
115
|
+
await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
|
|
116
|
+
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
|
|
117
|
+
}
|
|
84
118
|
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
|
|
85
119
|
// shape: "the design system splits the group row into a label control
|
|
86
120
|
// and a chevron control, both labelled by the cluster name; either one
|
|
@@ -101,9 +135,13 @@ class AppsPage {
|
|
|
101
135
|
// any container matchable by cluster name once expanded (confirmed
|
|
102
136
|
// against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
|
|
103
137
|
// own aria-expanded is the only reliable per-cluster signal available.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
// Not exact: the same suite asserts this row with a substring/regex
|
|
139
|
+
// match (`{name: /Production/}`), not an exact string -- and nightly
|
|
140
|
+
// traces confirm why: the rendered toggle's accessible name is not
|
|
141
|
+
// byte-for-byte the bare cluster name (it carries additional content,
|
|
142
|
+
// e.g. a trailing tag summary), so an exact-match lookup here finds
|
|
143
|
+
// nothing and the click below times out waiting on a locator that can
|
|
144
|
+
// never resolve.
|
|
107
145
|
const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
|
|
108
146
|
if (!alreadyExpanded) {
|
|
109
147
|
// This is the only expansion action in the Hub path -- swallowing its
|
|
@@ -125,10 +163,14 @@ class AppsPage {
|
|
|
125
163
|
// of its own, in which case hasNotText would fail to exclude the toggle
|
|
126
164
|
// we just expanded and this loop would immediately collapse it again.
|
|
127
165
|
// Match by accessible name (same algorithm getByRole's own name option
|
|
128
|
-
// uses) instead, excluding
|
|
166
|
+
// uses) instead, excluding any name CONTAINING clusterName -- not just
|
|
167
|
+
// one exactly equal to it, since (per the toggle locator above) the
|
|
168
|
+
// current cluster's own toggle name is not exactly clusterName either,
|
|
169
|
+
// and an exact-equality exclusion would wrongly treat it as "other" and
|
|
170
|
+
// immediately re-collapse the group this method just expanded.
|
|
129
171
|
const otherExpandedToggles = this.hubSidebar.getByRole('button', {
|
|
130
172
|
expanded: true,
|
|
131
|
-
name: new RegExp(`^(
|
|
173
|
+
name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
|
|
132
174
|
});
|
|
133
175
|
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
134
176
|
// Let a failed collapse throw rather than swallowing it: the app link
|
|
@@ -151,12 +193,15 @@ class AppsPage {
|
|
|
151
193
|
// match carries no ambiguity risk.
|
|
152
194
|
const appLink = this.hubAppLink(clusterName, appName);
|
|
153
195
|
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
154
|
-
//
|
|
155
|
-
// app
|
|
156
|
-
//
|
|
196
|
+
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
197
|
+
// the app in a new tab while every caller keeps asserting on this.page.
|
|
198
|
+
// Follow the href in the current tab instead, matching the same-tab
|
|
199
|
+
// navigation the Console app switcher gave these callers.
|
|
157
200
|
const href = await appLink.getAttribute('href');
|
|
158
201
|
if (href && (await appLink.getAttribute('target')) === '_blank') {
|
|
159
|
-
await this.page.goto(href, {
|
|
202
|
+
await this.page.goto(new URL(href, this.page.url()).toString(), {
|
|
203
|
+
waitUntil: 'domcontentloaded',
|
|
204
|
+
});
|
|
160
205
|
return;
|
|
161
206
|
}
|
|
162
207
|
await appLink.click({ timeout: 10000 });
|
|
@@ -164,11 +209,18 @@ class AppsPage {
|
|
|
164
209
|
// Inline (expanded rail) the app link carries the app's name. The rail can
|
|
165
210
|
// also start collapsed (camunda-hub seeds SidebarProvider's defaultExpanded
|
|
166
211
|
// from whether its items had loaded at first render); the group toggle then
|
|
167
|
-
// opens a
|
|
168
|
-
//
|
|
212
|
+
// opens a Radix popover -- a role="dialog" labelled with the cluster name
|
|
213
|
+
// and portalled OUTSIDE "Main navigation" -- whose links are plainly named.
|
|
214
|
+
// With Hub's Environments feature on, the sidebar instead lists
|
|
215
|
+
// environments whose app links render inline but are named
|
|
216
|
+
// "<app> in <cluster>" (opens in a new tab). Try the expanded-sidebar
|
|
217
|
+
// layout first, then each of these fallbacks.
|
|
169
218
|
hubAppLink(clusterName, appName) {
|
|
170
219
|
return this.hubSidebar
|
|
171
220
|
.getByRole('link', { name: appName })
|
|
221
|
+
.or(this.page
|
|
222
|
+
.getByRole('dialog', { name: clusterName, exact: true })
|
|
223
|
+
.getByRole('link', { name: appName }))
|
|
172
224
|
.or(this.page.getByRole('link', {
|
|
173
225
|
name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
|
|
174
226
|
}))
|
|
@@ -182,6 +234,8 @@ class AppsPage {
|
|
|
182
234
|
// rather than a disabled control -- so this never throws on a miss.
|
|
183
235
|
async isHubClusterAppPresent(clusterName, appName) {
|
|
184
236
|
await this.expandHubClusterGroup(clusterName);
|
|
237
|
+
// Same collapsed-sidebar popover / environment-row fallbacks as
|
|
238
|
+
// clickHubClusterApp -- see hubAppLink.
|
|
185
239
|
const appLink = this.hubAppLink(clusterName, appName);
|
|
186
240
|
return appLink.isVisible({ timeout: 10000 }).catch(() => false);
|
|
187
241
|
}
|
|
@@ -79,28 +79,27 @@ class ClusterPage {
|
|
|
79
79
|
exact: true,
|
|
80
80
|
});
|
|
81
81
|
this.generationSearchInput = page.getByPlaceholder('Search generations');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
this.
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
this.
|
|
82
|
+
// Camunda Hub's create-cluster form replaced the Carbon tiles (a <label>
|
|
83
|
+
// per option) and tabs with design-system choice cards and toggle
|
|
84
|
+
// groups, all exposed as role="radio" named by their title. Each locator
|
|
85
|
+
// below accepts either shape via this helper.
|
|
86
|
+
const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
|
|
87
|
+
this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
|
|
88
|
+
this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
|
|
89
|
+
this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
|
|
90
|
+
this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
|
|
91
|
+
this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
|
|
92
|
+
this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
|
|
93
|
+
this.belgiumRegion = choice(page.locator('label').filter({
|
|
93
94
|
hasText: 'Chaos, Belgium, Europe (europe-west1)',
|
|
94
|
-
});
|
|
95
|
-
this.finlandRegion = page.locator('label').filter({
|
|
95
|
+
}), 'Chaos, Belgium, Europe (europe-west1)');
|
|
96
|
+
this.finlandRegion = choice(page.locator('label').filter({
|
|
96
97
|
hasText: 'Finland, Europe (europe-north1)',
|
|
97
|
-
});
|
|
98
|
-
this.londonRegion = page.locator('label').filter({
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
this.ohioRegion = page.locator('label').filter({
|
|
98
|
+
}), 'Finland, Europe (europe-north1)');
|
|
99
|
+
this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
|
|
100
|
+
this.ohioRegion = choice(page.locator('label').filter({
|
|
102
101
|
hasText: 'Ohio, North America (us-east-2)',
|
|
103
|
-
});
|
|
102
|
+
}), 'Ohio, North America (us-east-2)');
|
|
104
103
|
this.clustersList = page
|
|
105
104
|
.getByRole('row')
|
|
106
105
|
.filter({ hasNotText: 'Generation' }); //Filter out header row
|
|
@@ -239,7 +238,10 @@ class ClusterPage {
|
|
|
239
238
|
await this.clickClusterNameInput();
|
|
240
239
|
await this.fillClusterNameInput(name);
|
|
241
240
|
await this.clickClusterType();
|
|
242
|
-
await this.page
|
|
241
|
+
await this.page
|
|
242
|
+
.getByRole('tab', { name: region })
|
|
243
|
+
.or(this.page.getByRole('radio', { name: region, exact: true }))
|
|
244
|
+
.click();
|
|
243
245
|
await this.selectSubRegion(region);
|
|
244
246
|
await this.clickClusterOption(process.env.CLUSTER_VERSION);
|
|
245
247
|
await this.clickCreateClusterButton();
|