@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.
@@ -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
- // still carries "Camunda Modeler home". Keep the legacy named-banner branch
65
- // for shells where the rename has not shipped yet.
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
- .getByTitle(this.defaultFolderName, {
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
- async createCrossComponentProjectFolder() {
653
- await this.dismissOverlays();
654
- // The projects list renders asynchronously, so an instantaneous
655
- // isVisible() reports "missing" whenever the list is still loading — and
656
- // this method then creates a SECOND project with the same name. Those
657
- // duplicates are permanent and break every subsequent run for that user,
658
- // so wait for the folder to actually show up before concluding it is
659
- // absent (this also replaces the blind 15s sleep the check relied on).
660
- const folderExists = await this.crossComponentProjectFolder
661
- .waitFor({ state: 'visible', timeout: 60000 })
662
- .then(() => true)
663
- .catch(() => false);
664
- if (folderExists) {
665
- console.log('Cross Component Project folder already exists. Clicking into it');
666
- await this.clickCrossComponentProjectFolder();
667
- return;
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
- await this.clickCreateNewProjectButton();
670
- await this.enterNewProjectName(this.defaultFolderName);
671
- // Both branches must leave the browser inside the workspace: the callers
672
- // go straight on to clickDiagramTypeDropdown(), whose create control only
673
- // exists there. Creating normally navigates into the new project, but when
674
- // hub leaves the browser on the projects list the rename happens in the row
675
- // and no navigation follows.
676
- //
677
- // Keyed on "New project", which hub renders only on the projects list and
678
- // never inside an open project, so it answers which view we are on
679
- // directly. The folder tile is the wrong signal: on a slow list render it
680
- // is still absent, which would read as "already inside" and skip a hop
681
- // that was needed.
682
- if (await this.becameVisible(this.createNewProjectButton, 5000)) {
683
- await this.clickCrossComponentProjectFolder();
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: 30000,
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({ timeout: 60000 });
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
- const toggle = this.hubSidebar
105
- .getByRole('button', { name: clusterName, exact: true })
106
- .first();
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 an exact match on clusterName.
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(`^(?!${escapeRegExp(clusterName)}$).+$`),
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
- // 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.
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, { waitUntil: 'domcontentloaded' });
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 flyout portalled outside "Main navigation", whose links -- like
168
- // environment rows' -- are named "<app> in <cluster>".
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
- this.g3DevClusterType = page.locator('label').filter({
83
- hasText: this.defaultClusterType,
84
- });
85
- this.alphaTab = page.getByRole('tab', { name: 'Alpha' });
86
- this.devTab = page.getByRole('tab', { name: 'Internal Dev' });
87
- this.stableTab = page.getByRole('tab', { name: 'Stable' });
88
- this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
89
- this.gcpRegion = page.getByRole('tab', {
90
- name: 'GCP',
91
- });
92
- this.belgiumRegion = page.locator('label').filter({
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
- hasText: 'London, Europe (europe-west2)',
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.getByRole('tab', { name: region }).click();
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();