@camunda/e2e-test-suite 0.0.1273 → 0.0.1274

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.
@@ -7,8 +7,16 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
7
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
8
8
  const randomName_1 = require("../../utils/randomName");
9
9
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
10
+ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
11
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
12
+ const hubResourceRename_1 = require("../../utils/hubResourceRename");
10
13
  class ModelerHomePage {
11
14
  defaultFolderName = 'Cross Component Test Project';
15
+ // The default cluster test-setup.spec.ts's "Create Default Cluster" test
16
+ // provisions for the whole suite -- assigned to the shared workspace's
17
+ // Environments step so downstream specs can deploy to it without a manual
18
+ // assignment step first.
19
+ defaultClusterName = 'Test Cluster';
12
20
  page;
13
21
  // Since camunda/camunda-hub#25831 files and folders cannot sit at a
14
22
  // workspace ("project" in today's UI) root -- only process applications and
@@ -25,6 +33,11 @@ class ModelerHomePage {
25
33
  processApplicationName;
26
34
  modelerPageBanner;
27
35
  createNewProjectButton;
36
+ createWorkspaceButton;
37
+ workspaceWizardNameInput;
38
+ workspaceWizardSubmitButton;
39
+ workspaceWizardMembersStep;
40
+ workspaceWizardEnvironmentsHeading;
28
41
  projectNameInput;
29
42
  formNameInput;
30
43
  chooseBpmnTemplateButton;
@@ -58,9 +71,24 @@ class ModelerHomePage {
58
71
  constructor(page) {
59
72
  this.page = page;
60
73
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
61
- this.modelerPageBanner = page.getByRole('banner', {
74
+ // Hub's design-system shell renders the app header as a role=banner with no
75
+ // accessible name after the Camunda Hub rebrand, so the classic
76
+ // name-matched banner no longer resolves; the Modeler logo link inside it
77
+ // carries "Camunda Hub home" when the org has workspacesEnabled, else
78
+ // "Camunda Modeler home" (camunda-hub NavShellDs.common.tsx) -- match
79
+ // either. Keep the legacy named-banner branch for shells where the rename
80
+ // has not shipped yet.
81
+ this.modelerPageBanner = page
82
+ .getByRole('banner', {
62
83
  name: 'Camunda Modeler',
63
- });
84
+ })
85
+ .or(page
86
+ .getByRole('link', { name: 'Camunda Hub home' })
87
+ .and(page.locator(':not([href$="/login"])')))
88
+ .or(page
89
+ .getByRole('link', { name: 'Camunda Modeler home' })
90
+ .and(page.locator(':not([href$="/login"])')))
91
+ .first();
64
92
  // Hub renamed this button to "Create project" on both the classic and the
65
93
  // design-system shell. The legacy label is kept so the flow still works
66
94
  // against Modeler builds where the rename has not shipped yet. exact:true
@@ -73,8 +101,32 @@ class ModelerHomePage {
73
101
  name: 'Create project',
74
102
  exact: true,
75
103
  }));
104
+ // Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
105
+ // hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
106
+ // two-step wizard (General -> Members) that names the workspace up front.
107
+ // Same hooks as pages/8.9/ModelerHomePage.ts. exact:true keeps the
108
+ // split button's options chevron out of the match.
109
+ this.createWorkspaceButton = page.getByRole('button', {
110
+ name: 'Create workspace',
111
+ exact: true,
112
+ });
113
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
114
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
115
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
116
+ // When the org has environments enabled, an "Assign environments" step is
117
+ // inserted after Members -- camunda-hub's own
118
+ // e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
119
+ this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
120
+ name: 'Assign environments',
121
+ });
76
122
  this.projectNameInput = page.locator('[data-test="editable-input"]');
77
- this.formNameInput = page.locator('[data-test="editable-input"]');
123
+ // Hub's rename flow moved into a dialog whose field is
124
+ // `[data-test="rename-input"]` (#rename-input); the legacy shells still
125
+ // rename through the inline editable title. Match either.
126
+ this.formNameInput = page
127
+ .locator('[data-test="rename-input"]')
128
+ .or(page.locator('[data-test="editable-input"]'))
129
+ .first();
78
130
  this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
79
131
  this.createIdpApplicationButton = page.getByRole('button', {
80
132
  name: 'Create',
@@ -90,27 +142,60 @@ class ModelerHomePage {
90
142
  this.idpEmptyStateButton = page.getByRole('button', {
91
143
  name: 'Create IDP application',
92
144
  });
93
- this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
145
+ // Camunda Hub's design-system shell renders this trigger as
146
+ // `create-action-dropdown` (CreateActionDs.tsx, accessible name "Create
147
+ // new"); `diagram-dropdown` is the legacy shell's id and survives only in
148
+ // hub's own older cypress specs. Match either so both shells work.
149
+ this.diagramTypeDropdown = page
150
+ .locator('[data-test="create-action-dropdown"]')
151
+ .or(page.locator('[data-test="diagram-dropdown"]'))
152
+ .first();
94
153
  // Matched on the test id alone: hub's design-system variant of the create
95
154
  // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
96
155
  // day that variant ships.
97
156
  this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
98
157
  this.formTemplateOption = page.locator('[data-test="create-form"]');
99
158
  this.diagramPropertiesPanel = page.locator('[data-group-id="group-general"]');
100
- this.projectBreadcrumb = page.locator('[data-test="breadcrumb-project"]');
159
+ // Hub's breadcrumbs carry no data-test at all (confirmed by a DOM audit of
160
+ // a live run: none of the seven breadcrumb-* ids this file used are
161
+ // present). Its trail is a `navigation` landmark whose entries are links,
162
+ // so address the project by its own name there and keep the legacy id for
163
+ // shells that still emit it.
164
+ this.projectBreadcrumb = page
165
+ .locator('[data-test="breadcrumb-project"]')
166
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page).getByRole('link', {
167
+ name: this.defaultFolderName,
168
+ exact: true,
169
+ }))
170
+ .first();
101
171
  // Kept separate from projectBreadcrumb, which now means "the container my
102
172
  // resources live in". When hub's workspaces terminology flag flips,
103
173
  // breadcrumb-project comes to denote the process application, so the
104
174
  // workspace level needs its own name from the start.
175
+ // Neither breadcrumb-project nor breadcrumb-workspace exists on Hub (the
176
+ // DOM audit found none of the breadcrumb-* ids), so this click waited out
177
+ // its whole 60s. Hub's trail reads "<org> / <workspace> / <project>", and
178
+ // the workspace and its project share this name -- take the LAST entry, the
179
+ // project, since that is the container callers come back here to add
180
+ // resources to.
105
181
  this.workspaceBreadcrumb = page
106
182
  .locator('[data-test="breadcrumb-project"]')
107
183
  .or(page.locator('[data-test="breadcrumb-workspace"]'))
184
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page)
185
+ .getByRole('link', { name: this.defaultFolderName, exact: true })
186
+ .last())
187
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page)
188
+ .getByRole('button', { name: this.defaultFolderName, exact: true })
189
+ .last())
108
190
  .first();
109
191
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
110
192
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
111
193
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
194
+ // On the Camunda Hub Workspaces overview the folder is a workspace tile
195
+ // whose "Open workspace <name>" button navigates into it.
112
196
  this.crossComponentProjectFolder = page
113
- .getByTitle(this.defaultFolderName, { exact: true })
197
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
198
+ .or(page.getByTitle(this.defaultFolderName, { exact: true }))
114
199
  .first();
115
200
  this.rows = page.getByRole('row');
116
201
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
@@ -347,6 +432,24 @@ class ModelerHomePage {
347
432
  }
348
433
  }
349
434
  async enterNewProjectName(name) {
435
+ // Camunda Hub names a project up front in a creation dialog
436
+ // (`#create-project-name`) instead of the legacy inline rename on a
437
+ // project that already exists. When that dialog is what "Create project"
438
+ // opened, the inline editor below never mounts, so name it here and
439
+ // return -- the caller only needs the project to exist and be named.
440
+ const createProjectNameInput = this.page.locator('#create-project-name');
441
+ if (await this.becameVisible(createProjectNameInput, 10000)) {
442
+ const dialog = this.page.getByRole('dialog').filter({
443
+ has: createProjectNameInput,
444
+ });
445
+ await createProjectNameInput.fill(name);
446
+ await (0, test_1.expect)(createProjectNameInput).toHaveValue(name, { timeout: 10000 });
447
+ await (0, submitModalWithRetry_1.submitModalWithRetry)(dialog
448
+ .getByRole('button', { name: 'Create project', exact: true })
449
+ .or(dialog.getByRole('button', { name: 'Create', exact: true }))
450
+ .first(), dialog);
451
+ return;
452
+ }
350
453
  // The inline rename silently drops the value when the editable title field
351
454
  // is not yet in edit mode as fill() runs, leaving the project named
352
455
  // "New project". Re-enter until the field reflects the requested name.
@@ -442,7 +545,34 @@ class ModelerHomePage {
442
545
  // workspace root no longer offers BPMN, Form or Upload -- those resources
443
546
  // cannot live there -- so the container is entered first. Callers are
444
547
  // unchanged, which is why the existing spec call sites need no edit.
548
+ // Camunda Hub lets blank resources live directly in a project: the project's
549
+ // own "Create new" menu (ProjectActionButtonDs, data-test="diagram-dropdown")
550
+ // renders BPMN diagram / DMN diagram / Form / RPA script / Element template /
551
+ // README / Folder whenever the current container is not the project ROOT, and
552
+ // offers "Process application" only at that root. Opening it and looking for
553
+ // the resource items is the only way to tell the two menus apart, since both
554
+ // share the trigger.
555
+ //
556
+ // Returns true with the menu left OPEN for the caller's option click.
557
+ async openProjectCreateMenuWithResources() {
558
+ if (!(await this.becameVisible(this.diagramTypeDropdown, 15000))) {
559
+ return false;
560
+ }
561
+ await this.dismissOverlays();
562
+ await this.diagramTypeDropdown.click({ timeout: 30000 });
563
+ if (await this.becameVisible(this.formTemplateOption.or(this.bpmnTemplateOption).first(), 10000)) {
564
+ return true;
565
+ }
566
+ // The root menu instead: close it so the process-application path below
567
+ // starts from a page with no open dropdown holding the pointer-events lock
568
+ // Radix puts on <body> while a menu is open.
569
+ await this.page.keyboard.press('Escape').catch(() => { });
570
+ return false;
571
+ }
445
572
  async clickDiagramTypeDropdown() {
573
+ if (await this.openProjectCreateMenuWithResources()) {
574
+ return;
575
+ }
446
576
  await this.enterResourceContainer();
447
577
  const dropdown = this.processApplicationPage.createNewDropdown;
448
578
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -539,10 +669,8 @@ class ModelerHomePage {
539
669
  }
540
670
  async enterFormName(name) {
541
671
  await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
542
- await (0, test_1.expect)(this.formNameInput).toBeVisible({ timeout: 120000 });
543
- await this.formNameInput.click({ timeout: 60000, force: true });
544
- await this.formNameInput.fill(name, { timeout: 60000 });
545
- await this.formNameInput.press('Enter');
672
+ // "New form" is the server default the form still carries at this point.
673
+ await (0, hubResourceRename_1.renameHubResource)(this.page, 'New form', name);
546
674
  }
547
675
  // Kept under its original name because it is what the existing call sites
548
676
  // already mean: "go back to where my resources live". That is now the
@@ -642,13 +770,110 @@ class ModelerHomePage {
642
770
  }
643
771
  // Boolean visibility probe. waitFor rather than isVisible, which resolves
644
772
  // immediately and would report a false negative on a still-rendering page.
773
+ // Since camunda-hub #28956 the Workspaces overview defaults org-elevated
774
+ // users to "My workspaces" -- only workspaces they are an explicit member
775
+ // of -- and resets to it on every load. The shared folder is created by
776
+ // whichever test user wins the race, so every other user sees "No
777
+ // workspaces match this filter" and never finds it. Widen the scope to
778
+ // "All workspaces" first. The control is absent outside Hub and for
779
+ // non-elevated users (their list is membership-scoped anyway), so a missing
780
+ // control is a no-op. Ported from pages/8.10/ModelerHomePage.ts (#3710).
781
+ async showAllWorkspaces(probeTimeout) {
782
+ const scopeFilter = this.page.getByRole('combobox', {
783
+ name: 'Filter workspaces',
784
+ });
785
+ if (!(await this.becameVisible(scopeFilter, probeTimeout)))
786
+ return;
787
+ if ((await scopeFilter.innerText()).includes('All workspaces'))
788
+ return;
789
+ await scopeFilter.click({ timeout: 10000 });
790
+ await this.page
791
+ .getByRole('option', { name: 'All workspaces' })
792
+ .click({ timeout: 10000 });
793
+ await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
794
+ timeout: 10000,
795
+ });
796
+ }
645
797
  async becameVisible(locator, timeout) {
646
798
  return locator
647
799
  .waitFor({ state: 'visible', timeout })
648
800
  .then(() => true)
649
801
  .catch(() => false);
650
802
  }
803
+ // Drives Camunda Hub's workspace-creation wizard. The name is entered on
804
+ // the General step, which enables "Next"; the same submit button then
805
+ // relabels itself through the remaining steps and finally reads "Create
806
+ // workspace" once it creates the workspace and routes the creator into its
807
+ // projects listing. Gate the Members-step click on that step becoming
808
+ // active so the shared submit button is not clicked twice while still on
809
+ // General.
810
+ async createWorkspaceViaWizard(name) {
811
+ await this.dismissOverlays();
812
+ await this.createWorkspaceButton.click({ timeout: 60000 });
813
+ await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
814
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
815
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
816
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
817
+ // When the org has environments enabled, this only advances the wizard
818
+ // into an "Assign environments" step instead of submitting. Assign the
819
+ // suite's default cluster here rather than submitting with an empty
820
+ // selection -- otherwise this shared workspace starts with no
821
+ // environments and every downstream spec that deploys to it needs its
822
+ // own manual assignment first. Each card's accessible name is
823
+ // "Select <cluster name>" before it is picked (camunda-hub's own
824
+ // create-workspace-environments.spec.tsx asserts the same pattern).
825
+ // Sampled immediately (isVisible() doesn't wait) since the step change
826
+ // is a synchronous client-side re-render.
827
+ // Waited for, not sampled: the step transition is not always synchronous,
828
+ // and an immediate isVisible() read false would skip the cluster
829
+ // assignment and then wait out the projects-URL check while the wizard is
830
+ // still open. Matches the 8.10 flow.
831
+ if (await this.becameVisible(this.workspaceWizardEnvironmentsHeading, 5000)) {
832
+ // "Create Default Cluster" runs in parallel with this flow
833
+ // (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
834
+ // lists may still be provisioning -- wait for it a generous 90s rather
835
+ // than sampling once. Fail loudly rather than silently creating the
836
+ // shared workspace without its default environment, which would turn a
837
+ // setup problem here into confusing failures in unrelated specs later.
838
+ const clusterOption = this.page.getByRole('button', {
839
+ name: new RegExp(`^Select ${this.defaultClusterName}`),
840
+ });
841
+ if (await this.becameVisible(clusterOption, 90000)) {
842
+ await clusterOption.click({ timeout: 10000 });
843
+ }
844
+ else {
845
+ throw new Error(`"${this.defaultClusterName}" was not selectable on the Environments step within 90s -- refusing to create the shared workspace without its default environment assigned, since every downstream spec that deploys to it depends on that assignment.`);
846
+ }
847
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
848
+ }
849
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
850
+ timeout: 120000,
851
+ });
852
+ }
651
853
  async clickHomeBreadcrumb() {
854
+ // Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
855
+ // overview, reached through the header logo link so the SPA keeps its
856
+ // in-memory session (a full navigation re-runs the whole Auth0 hand-off).
857
+ // The link's accessible name is "Camunda Hub home" when the org has
858
+ // workspacesEnabled, else "Camunda Modeler home" (camunda-hub
859
+ // NavShellDs.common.tsx) -- match either.
860
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
861
+ // The href stays literally "/login" until the client's auth state
862
+ // catches up just after login, so clicking it in that window logs the
863
+ // session out instead of navigating home. Exclude that href and wait
864
+ // for the authenticated one, as AppsPage and the 8.9/8.10 flows do.
865
+ const header = this.page.locator('header[data-slot="app-header"]');
866
+ const notLogin = this.page.locator(':not([href$="/login"])');
867
+ await header
868
+ .getByRole('link', { name: 'Camunda Hub home' })
869
+ .and(notLogin)
870
+ .or(header
871
+ .getByRole('link', { name: 'Camunda Modeler home' })
872
+ .and(notLogin))
873
+ .first()
874
+ .click({ timeout: 30000 });
875
+ return;
876
+ }
652
877
  // The Console shell's HelpCenter panel ("Your recommendations") renders on
653
878
  // top of Web Modeler as a modal dialog that covers the whole page, so the
654
879
  // breadcrumb click retries against an element whose pointer events the
@@ -723,6 +948,13 @@ class ModelerHomePage {
723
948
  async clickCrossComponentProjectFolder() {
724
949
  try {
725
950
  await this.clickCrossComponentProjectFolderWithRetry();
951
+ // Opening the workspace lands on its projects LIST, which renders no
952
+ // resource-creation control. Callers go straight on to createForm() /
953
+ // clickCreateProcessApplication(), whose controls only exist inside an
954
+ // open project, so the hop belongs here rather than in
955
+ // createCrossComponentProjectFolder() -- smoke-tests and the rba specs
956
+ // reach the workspace through this method and never through that one.
957
+ await this.enterProjectInsideWorkspace();
726
958
  }
727
959
  catch (error) {
728
960
  if (this.consecutiveSessionRecoveries > 0) {
@@ -767,6 +999,13 @@ class ModelerHomePage {
767
999
  // it at all: the migration notice ignores Escape.
768
1000
  await this.dismissBlockingDialog();
769
1001
  await this.clickMessageBanner();
1002
+ // postAction's reload resets Hub's scope filter to "My workspaces".
1003
+ // Let a failed scope switch propagate like dismissBlockingDialog()
1004
+ // and clickMessageBanner() above -- clickLocatorWithRetry's own
1005
+ // preAction handling already logs and retries it; swallowing it here
1006
+ // would otherwise leave the tile wait running against the wrong
1007
+ // ("My workspaces") scope for its full visibilityTimeout.
1008
+ await this.showAllWorkspaces(5000);
770
1009
  },
771
1010
  postAction: async () => {
772
1011
  // Before anything else: a 5xx from Modeler's own /api/internal/login
@@ -785,9 +1024,22 @@ class ModelerHomePage {
785
1024
  // and every remaining attempt just waits out its 60s visibility
786
1025
  // timeout before the helper throws. Step back out to home first.
787
1026
  const tileVisible = await this.becameVisible(this.crossComponentProjectFolder, 2000);
788
- if (!tileVisible &&
789
- (await this.becameVisible(this.homeBreadcrumb, 5000))) {
790
- await this.homeBreadcrumb.click({ timeout: 15000 });
1027
+ if (!tileVisible) {
1028
+ // Hub renders no `[data-test="breadcrumb-home"]` at all, so probing
1029
+ // that locator was a guaranteed miss there and the reload below just
1030
+ // reloaded whatever page we were stuck on. That matters because
1031
+ // recoverStuckModelerLogin returns to the login route's returnUrl,
1032
+ // which is the workspace's own /projects page -- INSIDE the
1033
+ // workspace, where this tile does not exist by construction. Every
1034
+ // retry then burned its full 60s visibility timeout against a page
1035
+ // that could never show the tile. Go home the way Hub allows (the
1036
+ // header logo link) instead.
1037
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
1038
+ await this.clickHomeBreadcrumb();
1039
+ }
1040
+ else if (await this.becameVisible(this.homeBreadcrumb, 5000)) {
1041
+ await this.homeBreadcrumb.click({ timeout: 15000 });
1042
+ }
791
1043
  }
792
1044
  await this.page.reload();
793
1045
  await this.page.waitForLoadState('domcontentloaded');
@@ -796,11 +1048,84 @@ class ModelerHomePage {
796
1048
  }
797
1049
  async createCrossComponentProjectFolder() {
798
1050
  await this.dismissOverlays();
799
- if (await this.crossComponentProjectFolder.isVisible({ timeout: 10000 })) {
1051
+ // Before the probe below, not after: under "My workspaces" the shared
1052
+ // folder is invisible to every user who did not create it, so probing
1053
+ // first would send them all down the create path and race duplicate
1054
+ // workspaces of the same name.
1055
+ await this.showAllWorkspaces(10000);
1056
+ // becameVisible, not isVisible: the timeout argument does not make
1057
+ // isVisible() wait, so a slow workspace list read as "missing" and this
1058
+ // parallel flow created another fixed-name workspace -- durable
1059
+ // duplicates that every later run then has to disambiguate.
1060
+ if (await this.becameVisible(this.crossComponentProjectFolder, 10000)) {
800
1061
  console.log('Cross Component Project folder already exists. Clicking into it');
801
1062
  await this.clickCrossComponentProjectFolder();
802
1063
  return;
803
1064
  }
1065
+ if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
1066
+ await this.createWorkspaceViaWizard(this.defaultFolderName);
1067
+ await this.enterProjectInsideWorkspace();
1068
+ return;
1069
+ }
1070
+ await this.clickCreateNewProjectButton();
1071
+ await this.enterNewProjectName(this.defaultFolderName);
1072
+ }
1073
+ // Camunda Hub nests Workspace > Project > resources, and opening a workspace
1074
+ // (or finishing its creation wizard) lands on /workspaces/<id>/projects --
1075
+ // the projects LIST, where no resource-creation control exists. Callers go
1076
+ // straight on to clickDiagramTypeDropdown()/clickCreateProcessApplication(),
1077
+ // whose controls only exist inside an open project, so the hop has to happen
1078
+ // here.
1079
+ //
1080
+ // Keyed on the "New project" / "Create project" button, which hub renders
1081
+ // only on the projects list and never inside an open project, so it answers
1082
+ // which view we are on directly -- the project tile is the wrong signal,
1083
+ // since on a slow list render it is still absent, which would read as
1084
+ // "already inside" and skip a hop that was needed. Same shape as
1085
+ // pages/8.10/ModelerHomePage.ts.
1086
+ async enterProjectInsideWorkspace() {
1087
+ if (!(await this.becameVisible(this.createNewProjectButton, 15000))) {
1088
+ return;
1089
+ }
1090
+ // Scoped to a table row, NOT reusing crossComponentProjectFolder: that
1091
+ // locator matches workspace-level shapes only (an "Open workspace <name>"
1092
+ // button, or a `title` attribute), and the projects list renders neither --
1093
+ // it is a data table whose name cell holds a plain link. Reusing it made
1094
+ // detection fail on every run, so the create branch below ran every time
1095
+ // and the workspace accumulated duplicate same-named projects.
1096
+ //
1097
+ // The row scope also keeps the breadcrumb out of the match: it renders the
1098
+ // workspace's own name, identical to the project's, and clicking it would
1099
+ // navigate back out to the workspace instead of into the project.
1100
+ const projectEntryRow = this.page
1101
+ .getByRole('row')
1102
+ .filter({ hasText: this.defaultFolderName })
1103
+ .first();
1104
+ // The name cell holds no link and no ordinary button: its only button is
1105
+ // `data-slot="data-table-row-primary-action"`, an absolutely-positioned
1106
+ // overlay carrying `pointer-events-none`, so every clickable-element
1107
+ // locator came up empty here and the create branch below ran on every
1108
+ // run, accumulating duplicate same-named projects.
1109
+ //
1110
+ // The row's kebab exposes the project's own id as `data-project-id`, so
1111
+ // read it and navigate straight there -- deterministic, and it sidesteps
1112
+ // both the dead overlay and the ambiguity of several identically named
1113
+ // rows. Fall back to clicking the row itself (it is `cursor-pointer` and
1114
+ // handles the row activation) when the id is not exposed.
1115
+ const rowMenuTrigger = projectEntryRow.locator('[data-test="project-row-menu-trigger"]');
1116
+ if (await this.becameVisible(rowMenuTrigger, 10000)) {
1117
+ const projectId = await rowMenuTrigger.getAttribute('data-project-id');
1118
+ const workspaceId = new URL(this.page.url()).pathname.match(/\/workspaces\/([^/]+)/)?.[1];
1119
+ if (workspaceId) {
1120
+ (0, consoleApiHelpers_1.rememberHubWorkspaceId)(this.page, workspaceId);
1121
+ }
1122
+ if (projectId && workspaceId) {
1123
+ await this.page.goto(new URL(`/workspaces/${workspaceId}/projects/${projectId}`, this.page.url()).toString());
1124
+ return;
1125
+ }
1126
+ await projectEntryRow.click({ timeout: 30000 });
1127
+ return;
1128
+ }
804
1129
  await this.clickCreateNewProjectButton();
805
1130
  await this.enterNewProjectName(this.defaultFolderName);
806
1131
  }
@@ -9,6 +9,7 @@ const sleep_1 = require("../../utils/sleep");
9
9
  const randomSleep_1 = require("../../utils/randomSleep");
10
10
  const SettingsPage_1 = require("./SettingsPage");
11
11
  const fileUpload_1 = require("../../utils/fileUpload");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
12
13
  async function forceLogoutIfNeeded(page) {
13
14
  const settingsPage = new SettingsPage_1.SettingsPage(page);
14
15
  const loginMessage = page.getByText('Log in to continue to Camunda.');
@@ -37,7 +38,29 @@ exports.forceLogoutIfNeeded = forceLogoutIfNeeded;
37
38
  // pre-flight stagger below, so a worker only pays it once per process
38
39
  // lifetime rather than on every call -- see the comment at the call site.
39
40
  const staggeredWorkerTimeouts = new Set();
41
+ const onboardingTourSuppressed = new WeakSet();
42
+ // Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
43
+ // (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
44
+ // "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
45
+ // hides the post-login banner link from the accessibility tree until dismissed, which times
46
+ // out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
47
+ // script on every navigation/reload, so there's no login-response race to lose; the flag is
48
+ // matched by prefix since the user id isn't known ahead of login.
49
+ async function suppressOnboardingTour(page) {
50
+ if (onboardingTourSuppressed.has(page)) {
51
+ return;
52
+ }
53
+ onboardingTourSuppressed.add(page);
54
+ await page.addInitScript((prefixPattern) => {
55
+ const pattern = new RegExp(prefixPattern);
56
+ const originalGetItem = Storage.prototype.getItem;
57
+ Storage.prototype.getItem = function (key) {
58
+ return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
59
+ };
60
+ }, '^hub\\.onboarding\\..*\\.seen\\.');
61
+ }
40
62
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
63
+ await suppressOnboardingTour(page);
41
64
  let lastError;
42
65
  // Stagger the first navigation across parallel workers. Without this, every
43
66
  // worker's initial page.goto('/') fires within the same second -- with 18+
@@ -56,6 +79,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
56
79
  for (let attempt = 0; attempt < maxRetries; attempt++) {
57
80
  try {
58
81
  await page.context().clearCookies();
82
+ // The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
83
+ // for the tab's whole lifetime; without clearing it here alongside
84
+ // cookies/storage, a flow that logs a different user into the same
85
+ // page could have getUserToken() return the PREVIOUS user's token.
86
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
59
87
  await page.goto('about:blank');
60
88
  await page
61
89
  .evaluate(() => {
@@ -407,8 +435,12 @@ async function assertTestUsesCorrectOrganization(page) {
407
435
  const homePage = new HomePage_1.HomePage(page);
408
436
  if (page.url().includes('?code=') || page.url().includes('&state=')) {
409
437
  console.log('OAuth callback detected, waiting for redirect...');
438
+ // Camunda Hub (which replaced Console on SaaS) only exchanges the code
439
+ // once its SPA has booted, and on CI runners that boot alone was measured
440
+ // at ~45s past /login-callback -- a 30s wait aborted the exchange on every
441
+ // attempt. Ported from the already-verified 8.9/8.10 fix.
410
442
  await page.waitForURL((url) => !url.toString().includes('?code='), {
411
- timeout: 30000,
443
+ timeout: 120000,
412
444
  });
413
445
  await (0, sleep_1.sleep)(5000);
414
446
  }
@@ -418,7 +450,20 @@ async function assertTestUsesCorrectOrganization(page) {
418
450
  await (0, test_1.expect)(homePage.modelerBanner).toBeVisible({ timeout: 30000 });
419
451
  }
420
452
  else {
421
- await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 30000 });
453
+ await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
454
+ timeout: 60000,
455
+ });
456
+ }
457
+ // Camunda Hub replaces Console on SaaS (8.10 release notes), and the Hub
458
+ // shell has neither the "Camunda logo Console" link nor the "Open
459
+ // Organizations" switcher, and lands on /workspaces without the org id in
460
+ // the URL. Its sidebar scopes the Clusters/Organization links to the active
461
+ // org, so assert the expected org id there instead.
462
+ if (await homePage.hubHeader.isVisible()) {
463
+ await (0, test_1.expect)(homePage.hubSidebar
464
+ .locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
465
+ .first()).toBeVisible({ timeout: 180000 });
466
+ return;
422
467
  }
423
468
  await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
424
469
  try {
@@ -18,7 +18,8 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
18
18
  await (0, _setup_1.captureScreenshot)(page, testInfo);
19
19
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
20
20
  });
21
- (0, _8_8_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
21
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
22
+ _8_8_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
22
23
  _8_8_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
23
24
  _8_8_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
24
25
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
@@ -45,6 +46,14 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
45
46
  await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid);
46
47
  await homePage.clickClusters();
47
48
  }
49
+ // Whichever way the cluster was just created -- through the UI above or via
50
+ // the API in the else branch -- link it to the suite's shared Hub
51
+ // workspace: Hub gates deployment on the ENVIRONMENTS assigned to a
52
+ // workspace, so a cluster the workspace does not list is invisible in every
53
+ // deploy dialog. A no-op when the workspace does not exist yet (this test
54
+ // races the workspace-creating ones), which the deploy-time ensure-link
55
+ // then covers.
56
+ await (0, consoleApiHelpers_1.linkClusterToSharedWorkspaceViaApi)(page, clusterName);
48
57
  await clusterPage.clickClusterLink(clusterName);
49
58
  await clusterDetailsPage.assertComponentsHealth();
50
59
  // API-client creation is needed whenever this run will also exercise the
@@ -94,6 +103,14 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
94
103
  await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
95
104
  await homePage.clickClusters();
96
105
  }
106
+ // Whichever way the cluster was just created -- through the UI above or via
107
+ // the API in the else branch -- link it to the suite's shared Hub
108
+ // workspace: Hub gates deployment on the ENVIRONMENTS assigned to a
109
+ // workspace, so a cluster the workspace does not list is invisible in every
110
+ // deploy dialog. A no-op when the workspace does not exist yet (this test
111
+ // races the workspace-creating ones), which the deploy-time ensure-link
112
+ // then covers.
113
+ await (0, consoleApiHelpers_1.linkClusterToSharedWorkspaceViaApi)(page, clusterName);
97
114
  await clusterPage.clickClusterLink(clusterName);
98
115
  await clusterDetailsPage.assertComponentsHealth();
99
116
  if (process.env.IS_AG !== 'true') {
@@ -107,11 +124,17 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
107
124
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
108
125
  });
109
126
  await _8_8_1.test.step('Navigate to Web Modeler', async () => {
110
- await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
127
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
128
+ // Modeler home directly and has no app switcher to wait for.
129
+ await (0, test_1.expect)(homePage.camundaComponentsButton.or(homePage.hubHeader)).toBeVisible({
111
130
  timeout: 120000,
112
131
  });
113
- await appsPage.clickCamundaApps();
114
- await appsPage.clickModeler();
132
+ // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
133
+ // app switcher: its home already is the Modeler home.
134
+ if (!new URL(page.url()).hostname.startsWith('hub.')) {
135
+ await appsPage.clickCamundaApps();
136
+ await appsPage.clickModeler();
137
+ }
115
138
  await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
116
139
  timeout: 180000,
117
140
  });
@@ -120,6 +143,8 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
120
143
  await homePage.clickSkipCustomization();
121
144
  await modelerHomePage.createCrossComponentProjectFolder();
122
145
  await modelerHomePage.clickHomeBreadcrumb();
146
+ // Hub resets the overview to "My workspaces" on every load.
147
+ await modelerHomePage.showAllWorkspaces(10000);
123
148
  // The project-rename PATCH commits on the backend, but navigating to
124
149
  // Home right after can render the SPA's stale (pre-rename) project
125
150
  // list without refetching, so the folder never appears under a plain
@@ -9,3 +9,8 @@ export declare function waitForClusterHealthyViaApi(page: Page, clusterUuid: str
9
9
  export declare function waitForClusterRestartHealthyViaApi(page: Page, clusterUuid: string, timeoutMs?: number): Promise<void>;
10
10
  export declare function areAiFeaturesEnabledViaApi(page: Page): Promise<boolean>;
11
11
  export declare function waitForAiFeaturesEnabledViaApi(page: Page, timeoutMs?: number): Promise<void>;
12
+ export declare function linkClusterToWorkspaceViaApi(page: Page, workspaceId: string, clusterName: string): Promise<void>;
13
+ export declare function rememberHubWorkspaceId(page: Page, workspaceId: string): void;
14
+ export declare function getHubWorkspaceId(page: Page): string | undefined;
15
+ export declare function linkClusterToSharedWorkspaceViaApi(page: Page, clusterName: string): Promise<void>;
16
+ export declare function ensureClusterLinkedToSharedWorkspace(page: Page, clusterName: string): Promise<void>;