@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.
Files changed (31) hide show
  1. package/dist/pages/8.10/AppsPage.js +6 -2
  2. package/dist/pages/8.10/ClusterDetailsPage.d.ts +2 -2
  3. package/dist/pages/8.10/ClusterDetailsPage.js +75 -31
  4. package/dist/pages/8.10/ClusterPage.js +30 -2
  5. package/dist/pages/8.10/ClusterSecretsPage.js +142 -10
  6. package/dist/pages/8.10/HomePage.d.ts +2 -0
  7. package/dist/pages/8.10/HomePage.js +40 -2
  8. package/dist/pages/8.10/LoginPage.d.ts +1 -0
  9. package/dist/pages/8.10/LoginPage.js +29 -0
  10. package/dist/pages/8.10/ModelerHomePage.d.ts +8 -0
  11. package/dist/pages/8.10/ModelerHomePage.js +221 -35
  12. package/dist/pages/8.10/UtilitiesPage.js +58 -2
  13. package/dist/pages/8.7/AppsPage.d.ts +1 -0
  14. package/dist/pages/8.7/AppsPage.js +23 -4
  15. package/dist/pages/8.7/ClusterDetailsPage.js +14 -2
  16. package/dist/pages/8.7/ClusterPage.js +8 -0
  17. package/dist/pages/8.7/ConnectorMarketplacePage.d.ts +1 -0
  18. package/dist/pages/8.7/ConnectorMarketplacePage.js +24 -2
  19. package/dist/pages/8.7/ConsoleOrganizationPage.js +11 -0
  20. package/dist/pages/8.7/FormJsPage.js +30 -4
  21. package/dist/pages/8.7/ModelerCreatePage.js +33 -1
  22. package/dist/pages/8.7/ModelerHomePage.d.ts +2 -0
  23. package/dist/pages/8.7/ModelerHomePage.js +18 -0
  24. package/dist/pages/8.7/ProcessApplicationPage.d.ts +1 -0
  25. package/dist/pages/8.7/ProcessApplicationPage.js +87 -0
  26. package/dist/pages/8.7/UtilitiesPage.js +7 -1
  27. package/dist/tests/8.10/test-setup.spec.js +5 -2
  28. package/dist/tests/8.7/web-modeler-user-flows.spec.js +3 -15
  29. package/dist/utils/modelerCluster.d.ts +13 -0
  30. package/dist/utils/modelerCluster.js +2 -1
  31. 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
- // 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 {
@@ -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.hubSidebar.getByRole('link', {
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.hubSidebar.getByRole('link', { name: appName });
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
- .getByLabel('Close');
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
- await (0, test_1.expect)(this.clientRow(name)).toContainText('Zeebe, Tasklist, Operate, Optimize, and Secrets');
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
- this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
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.clickDownloadToProjectButton();
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 });