@camunda/e2e-test-suite 0.0.1249 → 0.0.1251

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 (64) hide show
  1. package/dist/pages/8.7/AppsPage.d.ts +7 -3
  2. package/dist/pages/8.7/AppsPage.js +187 -3
  3. package/dist/pages/8.7/ClusterPage.js +22 -2
  4. package/dist/pages/8.7/ClusterSecretsPage.js +22 -1
  5. package/dist/pages/8.7/HomePage.d.ts +2 -0
  6. package/dist/pages/8.7/HomePage.js +70 -6
  7. package/dist/pages/8.7/LoginPage.js +15 -0
  8. package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -0
  9. package/dist/pages/8.7/ModelerCreatePage.js +59 -5
  10. package/dist/pages/8.7/ModelerHomePage.d.ts +11 -0
  11. package/dist/pages/8.7/ModelerHomePage.js +184 -10
  12. package/dist/pages/8.7/ProcessApplicationPage.js +27 -7
  13. package/dist/pages/8.7/UtilitiesPage.js +54 -1
  14. package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +17 -0
  15. package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
  16. package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
  17. package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
  18. package/dist/pages/SM-8.10/ClusterPage.js +3 -1
  19. package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
  20. package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
  21. package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
  22. package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
  23. package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
  24. package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
  25. package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
  26. package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
  27. package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
  28. package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
  29. package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
  30. package/dist/pages/SM-8.7/NavigationPage.js +2 -1
  31. package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
  32. package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
  33. package/dist/tests/8.10/smoke-tests.spec.js +2 -1
  34. package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
  35. package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
  36. package/dist/tests/8.7/smoke-tests.spec.js +2 -1
  37. package/dist/tests/8.7/test-setup.spec.js +18 -5
  38. package/dist/tests/8.7/web-modeler-user-flows.spec.js +14 -14
  39. package/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
  40. package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
  41. package/dist/tests/8.8/smoke-tests.spec.js +2 -1
  42. package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
  43. package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
  44. package/dist/tests/8.9/smoke-tests.spec.js +2 -1
  45. package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
  46. package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
  47. package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
  48. package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
  49. package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
  50. package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
  51. package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
  52. package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
  53. package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
  54. package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
  55. package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
  56. package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
  57. package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
  58. package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
  59. package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
  60. package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
  61. package/dist/utils/modelerCluster.d.ts +4 -6
  62. package/dist/utils/modelerCluster.js +55 -32
  63. package/package.json +1 -1
  64. package/playwright.config.ts +18 -5
@@ -2,12 +2,18 @@ import { Page, Locator } from '@playwright/test';
2
2
  import { LoginPage } from './LoginPage';
3
3
  declare class ModelerHomePage {
4
4
  private defaultFolderName;
5
+ private defaultClusterName;
5
6
  private page;
6
7
  private processApplicationPage;
7
8
  private processApplicationUrl?;
8
9
  private processApplicationName?;
9
10
  readonly modelerPageBanner: Locator;
10
11
  readonly createNewProjectButton: Locator;
12
+ readonly createWorkspaceButton: Locator;
13
+ readonly workspaceWizardNameInput: Locator;
14
+ readonly workspaceWizardSubmitButton: Locator;
15
+ readonly workspaceWizardMembersStep: Locator;
16
+ readonly workspaceWizardEnvironmentsHeading: Locator;
11
17
  readonly projectNameInput: Locator;
12
18
  readonly formNameInput: Locator;
13
19
  readonly chooseBpmnTemplateButton: Locator;
@@ -31,7 +37,11 @@ declare class ModelerHomePage {
31
37
  readonly closeButton: Locator;
32
38
  readonly formNameBreadcrumb: (formName: string) => Locator;
33
39
  readonly formBreadcrumbButton: Locator;
40
+ readonly formActionsButton: Locator;
34
41
  readonly renameFormMenuItem: Locator;
42
+ readonly renameDialog: Locator;
43
+ readonly renameDialogInput: Locator;
44
+ readonly renameDialogSubmitButton: Locator;
35
45
  constructor(page: Page);
36
46
  dismissOverlays(): Promise<void>;
37
47
  clickCreateNewProjectButton(): Promise<void>;
@@ -63,6 +73,7 @@ declare class ModelerHomePage {
63
73
  clickHomeBreadcrumb(): Promise<void>;
64
74
  clickOpenOrganizationsButton(): Promise<void>;
65
75
  clickCrossComponentProjectFolder(): Promise<void>;
76
+ private createWorkspaceViaWizard;
66
77
  createCrossComponentProjectFolder(): Promise<void>;
67
78
  /**
68
79
  * Asserts the Cross Component project folder is listed on the Modeler home
@@ -9,6 +9,11 @@ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
10
10
  class ModelerHomePage {
11
11
  defaultFolderName = 'Cross Component Test Project';
12
+ // The default cluster test-setup.spec.ts's "Create Default Cluster" test
13
+ // provisions for the whole suite -- assigned to the shared workspace's
14
+ // Environments step so downstream specs can deploy to it without a manual
15
+ // assignment step first.
16
+ defaultClusterName = 'Test Cluster';
12
17
  page;
13
18
  // Since camunda/camunda-hub#25831 files and folders cannot sit at a
14
19
  // workspace ("project" in today's UI) root -- only process applications and
@@ -25,6 +30,11 @@ class ModelerHomePage {
25
30
  processApplicationName;
26
31
  modelerPageBanner;
27
32
  createNewProjectButton;
33
+ createWorkspaceButton;
34
+ workspaceWizardNameInput;
35
+ workspaceWizardSubmitButton;
36
+ workspaceWizardMembersStep;
37
+ workspaceWizardEnvironmentsHeading;
28
38
  projectNameInput;
29
39
  formNameInput;
30
40
  chooseBpmnTemplateButton;
@@ -48,13 +58,31 @@ class ModelerHomePage {
48
58
  closeButton;
49
59
  formNameBreadcrumb;
50
60
  formBreadcrumbButton;
61
+ formActionsButton;
51
62
  renameFormMenuItem;
63
+ renameDialog;
64
+ renameDialogInput;
65
+ renameDialogSubmitButton;
52
66
  constructor(page) {
53
67
  this.page = page;
54
68
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
55
- this.modelerPageBanner = page.getByRole('banner', {
69
+ // Hub's design-system shell renders the app header as a role=banner with no
70
+ // accessible name after the Camunda Hub rebrand, so the classic
71
+ // name-matched banner no longer resolves; the Modeler logo link inside it
72
+ // carries "Camunda Modeler home" or, once camunda-hub PR#29349's rename
73
+ // has rolled out to an org, "Camunda Hub home". Keep the legacy
74
+ // named-banner branch for shells where the rename has not shipped yet.
75
+ this.modelerPageBanner = page
76
+ .getByRole('banner', {
56
77
  name: 'Camunda Modeler',
57
- });
78
+ })
79
+ .or(page
80
+ .getByRole('link', { name: 'Camunda Modeler home' })
81
+ .and(page.locator(':not([href$="/login"])')))
82
+ .or(page
83
+ .getByRole('link', { name: 'Camunda Hub home' })
84
+ .and(page.locator(':not([href$="/login"])')))
85
+ .first();
58
86
  // Hub renamed this button to "Create project" on both the classic and the
59
87
  // design-system shell. The legacy label is kept so the flow still works
60
88
  // against Modeler builds where the rename has not shipped yet. exact:true
@@ -67,6 +95,24 @@ class ModelerHomePage {
67
95
  name: 'Create project',
68
96
  exact: true,
69
97
  }));
98
+ // Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
99
+ // hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
100
+ // wizard (General -> Members) that names the workspace up front. Same
101
+ // hooks as pages/8.10/ModelerHomePage.ts. exact:true keeps the split
102
+ // button's options chevron out of the match.
103
+ this.createWorkspaceButton = page.getByRole('button', {
104
+ name: 'Create workspace',
105
+ exact: true,
106
+ });
107
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
108
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
109
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
110
+ // When the org has environments enabled, an "Assign environments" step is
111
+ // inserted after Members -- camunda-hub's own
112
+ // e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
113
+ this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
114
+ name: 'Assign environments',
115
+ });
70
116
  this.projectNameInput = page.locator('[data-test="editable-input"]');
71
117
  this.formNameInput = page.locator('[data-test="editable-input"]');
72
118
  this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
@@ -98,10 +144,13 @@ class ModelerHomePage {
98
144
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
99
145
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
100
146
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
147
+ // On the Camunda Hub Workspaces overview the folder is a workspace tile
148
+ // whose "Open workspace <name>" button navigates into it.
101
149
  this.crossComponentProjectFolder = page
102
- .getByTitle(this.defaultFolderName, {
150
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
151
+ .or(page.getByTitle(this.defaultFolderName, {
103
152
  exact: true,
104
- })
153
+ }))
105
154
  .first();
106
155
  this.rows = page.getByRole('row');
107
156
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
@@ -116,7 +165,26 @@ class ModelerHomePage {
116
165
  hasText: formName,
117
166
  });
118
167
  this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
119
- this.renameFormMenuItem = page.locator('[data-test="rename-form"]');
168
+ // Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
169
+ // Rename/Duplicate/Delete behind a separate "<name> actions" kebab
170
+ // instead (use-file-page-actions.ts); its own menu item carries no
171
+ // data-test, just the text "Rename".
172
+ this.formActionsButton = page
173
+ .getByRole('button', { name: / actions$/ })
174
+ .first();
175
+ this.renameFormMenuItem = page
176
+ .locator('[data-test="rename-form"]')
177
+ .or(page.getByRole('menuitem', { name: 'Rename', exact: true }));
178
+ // The design-system nav's Rename opens a modal dialog (RenameDialogDs.tsx)
179
+ // instead of the legacy inline breadcrumb editor.
180
+ this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
181
+ this.renameDialogInput = this.renameDialog.getByLabel('Name', {
182
+ exact: true,
183
+ });
184
+ this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
185
+ name: 'Rename',
186
+ exact: true,
187
+ });
120
188
  }
121
189
  // The release-announcement dialog ("Check out our new release") re-mounts
122
190
  // after the projects list re-renders, so one dismissal pass is not enough.
@@ -300,10 +368,28 @@ class ModelerHomePage {
300
368
  if (!editorOpen) {
301
369
  await this.openFormRenameEditor();
302
370
  }
303
- await this.formNameInput.click({ timeout: 30000 });
304
- await this.formNameInput.fill(name);
305
- await this.formNameInput.press('Enter');
306
- await this.formNameBreadcrumb(name).waitFor({
371
+ // The design-system nav's Rename opens a modal dialog
372
+ // (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
373
+ // editor -- fill and submit whichever is actually open.
374
+ const modalOpen = await this.renameDialog
375
+ .isVisible()
376
+ .catch(() => false);
377
+ if (modalOpen) {
378
+ await this.renameDialogInput.fill(name, { timeout: 45000 });
379
+ await this.renameDialogSubmitButton.click({ timeout: 10000 });
380
+ await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
381
+ }
382
+ else {
383
+ await this.formNameInput.click({ timeout: 30000 });
384
+ await this.formNameInput.fill(name);
385
+ await this.formNameInput.press('Enter');
386
+ }
387
+ // The legacy breadcrumb shows the new name directly; the
388
+ // design-system kebab instead embeds it in its own accessible name
389
+ // ("<name> actions"). Accept either as confirmation the rename took.
390
+ await this.formNameBreadcrumb(name)
391
+ .or(this.page.getByRole('button', { name: `${name} actions` }))
392
+ .waitFor({
307
393
  state: 'visible',
308
394
  timeout: 30000,
309
395
  });
@@ -319,7 +405,18 @@ class ModelerHomePage {
319
405
  }
320
406
  }
321
407
  async openFormRenameEditor() {
322
- await this.formBreadcrumbButton.click({ timeout: 60000 });
408
+ // Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
409
+ // the dropdown behind a separate "<name> actions" kebab instead -- prefer
410
+ // it when present, and only fall back to the legacy breadcrumb otherwise.
411
+ const kebabVisible = await this.formActionsButton
412
+ .isVisible({ timeout: 5000 })
413
+ .catch(() => false);
414
+ if (kebabVisible) {
415
+ await this.formActionsButton.click({ timeout: 60000 });
416
+ }
417
+ else {
418
+ await this.formBreadcrumbButton.click({ timeout: 60000 });
419
+ }
323
420
  await this.renameFormMenuItem.click({ timeout: 30000 });
324
421
  }
325
422
  // Kept under its original name because it is what the existing call sites
@@ -432,6 +529,28 @@ class ModelerHomePage {
432
529
  return this.becameVisible(this.modelerPageBanner, timeout);
433
530
  }
434
531
  async clickHomeBreadcrumb() {
532
+ // Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
533
+ // overview, reached through the header logo link so the SPA keeps its
534
+ // in-memory session (a full navigation re-runs the whole Auth0 hand-off).
535
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
536
+ const header = this.page.locator('header[data-slot="app-header"]');
537
+ // The href stays literally "/login" until the client's auth state
538
+ // catches up just after login (camunda-hub NavShellDs.common.tsx) --
539
+ // clicking it in that window would log the test out instead of
540
+ // navigating home, so exclude that href and wait for the authenticated
541
+ // one rather than racing it (same hazard AppsPage.clickModeler()
542
+ // guards against).
543
+ const homeLink = header
544
+ .getByRole('link', { name: 'Camunda Modeler home' })
545
+ .and(this.page.locator(':not([href$="/login"])'))
546
+ .or(header
547
+ .getByRole('link', { name: 'Camunda Hub home' })
548
+ .and(this.page.locator(':not([href$="/login"])')))
549
+ .first();
550
+ await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
551
+ await homeLink.click({ timeout: 30000 });
552
+ return;
553
+ }
435
554
  try {
436
555
  await this.homeBreadcrumb.click();
437
556
  }
@@ -468,6 +587,57 @@ class ModelerHomePage {
468
587
  },
469
588
  });
470
589
  }
590
+ // Drives Camunda Hub's workspace-creation wizard. The name is entered on
591
+ // the General step, which enables "Next"; the same submit button then
592
+ // relabels itself through the remaining steps and finally reads "Create
593
+ // workspace" once it creates the workspace and routes the creator into its
594
+ // projects listing. Gate the Members-step click on that step becoming
595
+ // active so the shared submit button is not clicked twice while still on
596
+ // General.
597
+ async createWorkspaceViaWizard(name) {
598
+ await this.dismissOverlays();
599
+ await this.createWorkspaceButton.click({ timeout: 60000 });
600
+ await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
601
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
602
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
603
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
604
+ // When the org has environments enabled, this only advances the wizard
605
+ // into an "Assign environments" step instead of submitting. Assign the
606
+ // suite's default cluster here rather than submitting with an empty
607
+ // selection -- otherwise this shared workspace starts with no
608
+ // environments and every downstream spec that deploys to it needs its
609
+ // own manual assignment first. Each card's accessible name is
610
+ // "Select <cluster name>" before it is picked (camunda-hub's own
611
+ // create-workspace-environments.spec.tsx asserts the same pattern).
612
+ // Sampled immediately (isVisible() doesn't wait) since the step change
613
+ // is a synchronous client-side re-render.
614
+ if (await this.workspaceWizardEnvironmentsHeading
615
+ .isVisible()
616
+ .catch(() => false)) {
617
+ // "Create Default Cluster" runs in parallel with this flow
618
+ // (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
619
+ // lists may still be provisioning -- wait for it a generous 90s rather
620
+ // than sampling once. If it genuinely never appears (the cluster
621
+ // failed rather than just being slow), don't fail the whole shared
622
+ // workspace's setup over it: Hub's own recovery copy for this step
623
+ // ("Could not load environments. You can finish creating the
624
+ // workspace and assign them later.") confirms submitting unassigned
625
+ // is a supported, recoverable path, not a broken one.
626
+ const clusterOption = this.page.getByRole('button', {
627
+ name: new RegExp(`^Select ${this.defaultClusterName}`),
628
+ });
629
+ if (await this.becameVisible(clusterOption, 90000)) {
630
+ await clusterOption.click({ timeout: 10000 });
631
+ }
632
+ else {
633
+ console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
634
+ }
635
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
636
+ }
637
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
638
+ timeout: 120000,
639
+ });
640
+ }
471
641
  async createCrossComponentProjectFolder() {
472
642
  await this.dismissOverlays();
473
643
  // The project list can take well over a minute to render after a session
@@ -483,6 +653,10 @@ class ModelerHomePage {
483
653
  await this.clickCrossComponentProjectFolder();
484
654
  return;
485
655
  }
656
+ if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
657
+ await this.createWorkspaceViaWizard(this.defaultFolderName);
658
+ return;
659
+ }
486
660
  await this.clickCreateNewProjectButton();
487
661
  await this.enterNewProjectName(this.defaultFolderName);
488
662
  }
@@ -48,21 +48,36 @@ class ProcessApplicationPage {
48
48
  // .first() defensively only: hub renders exactly one of these today, in the
49
49
  // workspace-root empty state. A future second placement should not turn
50
50
  // every PA-creating test into a strict-mode violation.
51
+ //
52
+ // camunda-hub#25831 renamed the concept from "process application" to
53
+ // "project" (workspace root's CreateSplitButton renders literal text
54
+ // "Create project", opening a dialog titled "Create project"). Accept
55
+ // both names so this resolves whichever the org's live rollout has
56
+ // reached.
51
57
  this.createProcessApplicationButton = page
52
58
  .getByRole('button', { name: 'Create process application' })
59
+ .or(page.getByRole('button', { name: 'Create project' }))
53
60
  .first();
54
61
  this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
55
- this.creationDialog = page.getByRole('dialog', {
56
- name: 'Create a process application',
57
- });
62
+ this.creationDialog = page
63
+ .getByRole('dialog', { name: 'Create a process application' })
64
+ .or(page.getByRole('dialog', { name: 'Create project' }));
58
65
  // Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
59
66
  // getByLabel('Name', {exact: true}) for the properties panel, which would
60
67
  // resolve to two elements while the creation modal is open.
61
- this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
62
- this.submitButton = this.creationDialog.getByRole('button', {
63
- name: 'Create',
68
+ // CreateProjectModal.tsx labels its field "Project name", not "Name" --
69
+ // accept both so this resolves under either dialog variant.
70
+ this.nameInput = this.creationDialog
71
+ .getByLabel('Name', { exact: true })
72
+ .or(this.creationDialog.getByLabel('Project name', { exact: true }));
73
+ // CreateProjectModal.tsx's submit button reads "Create project", not the
74
+ // exact "Create" the legacy dialog used.
75
+ this.submitButton = this.creationDialog
76
+ .getByRole('button', { name: 'Create', exact: true })
77
+ .or(this.creationDialog.getByRole('button', {
78
+ name: 'Create project',
64
79
  exact: true,
65
- });
80
+ }));
66
81
  this.cancelButton = this.creationDialog.getByRole('button', {
67
82
  name: 'Cancel',
68
83
  exact: true,
@@ -72,8 +87,13 @@ class ProcessApplicationPage {
72
87
  // The process application page renders more than one diagram-dropdown
73
88
  // (hub's own unit tests reach for it with getAllByTestId), so the dropdown
74
89
  // has to be resolved inside the folder explorer rather than page-wide.
90
+ // camunda-hub#25831's workspace-projects flow (WorkspaceProjectDetail.tsx)
91
+ // renders this control as CreateActionDs with test-id
92
+ // "create-action-dropdown" instead of the legacy "diagram-dropdown" --
93
+ // accept both.
75
94
  this.createNewDropdown = this.folderExplorer
76
95
  .locator('[data-test="diagram-dropdown"]')
96
+ .or(this.folderExplorer.locator('[data-test="create-action-dropdown"]'))
77
97
  .first();
78
98
  this.inlineNameInput = page.locator('[data-test="editable-input"]');
79
99
  this.defineStagesDialog = page
@@ -7,11 +7,34 @@ const HomePage_1 = require("./HomePage");
7
7
  const sleep_1 = require("../../utils/sleep");
8
8
  const fileUpload_1 = require("../../utils/fileUpload");
9
9
  const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
10
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
11
  // Tracks which worker-derived `timeout` values have already paid the
11
12
  // pre-flight stagger below, so a worker only pays it once per process
12
13
  // lifetime rather than on every call -- see the comment at the call sites.
13
14
  const staggeredWorkerTimeouts = new Set();
15
+ const onboardingTourSuppressed = new WeakSet();
16
+ // Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
17
+ // (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
18
+ // "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
19
+ // hides the post-login banner link from the accessibility tree until dismissed, which times
20
+ // out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
21
+ // script on every navigation/reload, so there's no login-response race to lose; the flag is
22
+ // matched by prefix since the user id isn't known ahead of login.
23
+ async function suppressOnboardingTour(page) {
24
+ if (onboardingTourSuppressed.has(page)) {
25
+ return;
26
+ }
27
+ onboardingTourSuppressed.add(page);
28
+ await page.addInitScript((prefixPattern) => {
29
+ const pattern = new RegExp(prefixPattern);
30
+ const originalGetItem = Storage.prototype.getItem;
31
+ Storage.prototype.getItem = function (key) {
32
+ return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
33
+ };
34
+ }, '^hub\\.onboarding\\..*\\.seen\\.');
35
+ }
14
36
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5, skipOrgAssertion = false) {
37
+ await suppressOnboardingTour(page);
15
38
  // Callers derive `timeout` as (workerIndex + 1) * 1000, so it's a stable
16
39
  // per-worker identifier: a Playwright worker is a long-lived process that
17
40
  // calls loginWithRetry once per test/repeat it runs, and this stagger only
@@ -32,6 +55,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
32
55
  for (let attempt = 0; attempt < maxRetries; attempt++) {
33
56
  try {
34
57
  await page.context().clearCookies();
58
+ // The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
59
+ // for the tab's whole lifetime; without clearing it here alongside
60
+ // cookies/storage, a flow that logs a different user into the same
61
+ // page could have getUserToken() return the PREVIOUS user's token.
62
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
35
63
  await page.goto('about:blank');
36
64
  await page
37
65
  .evaluate(() => {
@@ -429,8 +457,15 @@ async function assertTestUsesCorrectOrganization(page) {
429
457
  const homePage = new HomePage_1.HomePage(page);
430
458
  if (page.url().includes('?code=') || page.url().includes('&state=')) {
431
459
  console.log('OAuth callback detected, waiting for redirect...');
460
+ // Camunda Hub (which replaced Console on SaaS) only exchanges the code
461
+ // once its SPA has booted, and on CI runners that boot alone was measured
462
+ // at ~45s past /login-callback -- a 30s wait aborted the exchange on every
463
+ // attempt. The code is single-use, so falling back to page.goto('/') on
464
+ // timeout (as this used to) doesn't recover a slow-but-valid exchange, it
465
+ // just turns it into a guaranteed failure. Wait longer instead, matching
466
+ // the sibling 8.10 fix.
432
467
  await page.waitForURL((url) => !url.toString().includes('?code='), {
433
- timeout: 30000,
468
+ timeout: 120000,
434
469
  });
435
470
  await (0, sleep_1.sleep)(5000);
436
471
  }
@@ -442,6 +477,24 @@ async function assertTestUsesCorrectOrganization(page) {
442
477
  else {
443
478
  await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 30000 });
444
479
  }
480
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on
481
+ // /workspaces (no org id in the URL) and has no "Open Organizations"
482
+ // button. Its sidebar links are org-scoped, so assert on those instead and
483
+ // switch via the same /org/<id> route Hub's org breadcrumb dropdown uses.
484
+ const isShown = (locator, timeout) => locator
485
+ .waitFor({ state: 'visible', timeout })
486
+ .then(() => true)
487
+ .catch(() => false);
488
+ if (await isShown(homePage.hubSidebar, 10000)) {
489
+ const orgScopedLink = homePage.hubSidebar
490
+ .locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
491
+ .first();
492
+ if (!(await isShown(orgScopedLink, 30000))) {
493
+ await page.goto(`/org/${process.env.ORG_ID}`);
494
+ }
495
+ await (0, test_1.expect)(orgScopedLink).toBeVisible({ timeout: 60000 });
496
+ return;
497
+ }
445
498
  await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
446
499
  try {
447
500
  await (0, test_1.expect)(page.url()).toContain(process.env.ORG_ID);
@@ -0,0 +1,17 @@
1
+ import { Locator, Page } from '@playwright/test';
2
+ declare class BrowseAllConnectorMarketplacePage {
3
+ private page;
4
+ readonly searchForConnectorTextbox: Locator;
5
+ private readonly marketplaceConnectorList;
6
+ private readonly marketplaceConnectorCard;
7
+ private readonly marketplaceDetailFooter;
8
+ private readonly availableElementsHeading;
9
+ private readonly importModal;
10
+ constructor(page: Page);
11
+ isOpen(): Promise<boolean>;
12
+ waitForConnectorSearchResult(connectorName?: string): Promise<Locator>;
13
+ importConnectorToProject(connectorName?: string): Promise<void>;
14
+ private completeImportReviewIfNecessary;
15
+ private findConnectorCard;
16
+ }
17
+ export { BrowseAllConnectorMarketplacePage };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BrowseAllConnectorMarketplacePage = void 0;
4
+ class BrowseAllConnectorMarketplacePage {
5
+ page;
6
+ searchForConnectorTextbox;
7
+ marketplaceConnectorList;
8
+ marketplaceConnectorCard;
9
+ marketplaceDetailFooter;
10
+ availableElementsHeading;
11
+ importModal;
12
+ constructor(page) {
13
+ this.page = page;
14
+ this.searchForConnectorTextbox = page.getByRole('searchbox', {
15
+ name: 'Search elements and reusable assets',
16
+ });
17
+ this.marketplaceConnectorList = page.getByRole('list', {
18
+ name: 'Marketplace connectors',
19
+ });
20
+ this.marketplaceConnectorCard = this.marketplaceConnectorList
21
+ .getByRole('button', {
22
+ name: /^View details for .+ from Marketplace$/,
23
+ })
24
+ .first();
25
+ this.marketplaceDetailFooter = page.locator('[data-test="marketplace-detail-footer"]');
26
+ this.availableElementsHeading = page.getByRole('heading', {
27
+ name: 'Available elements',
28
+ });
29
+ this.importModal = page.locator('[data-test="import-modal"]');
30
+ }
31
+ async isOpen() {
32
+ return await this.searchForConnectorTextbox.isVisible();
33
+ }
34
+ async waitForConnectorSearchResult(connectorName) {
35
+ await this.marketplaceConnectorCard.waitFor({
36
+ state: 'visible',
37
+ timeout: 120000,
38
+ });
39
+ return await this.findConnectorCard(connectorName);
40
+ }
41
+ async importConnectorToProject(connectorName) {
42
+ const connectorCard = await this.waitForConnectorSearchResult(connectorName);
43
+ await connectorCard.click({ timeout: 60000 });
44
+ const browseAllDialog = this.page.getByRole('dialog').filter({
45
+ has: this.page.getByRole('button', { name: 'Back to all results' }),
46
+ });
47
+ await browseAllDialog.waitFor({ state: 'visible', timeout: 60000 });
48
+ const addButton = this.marketplaceDetailFooter.getByRole('button', {
49
+ name: /^(Add to project|Add remaining elements)$/,
50
+ });
51
+ const addedStatus = this.marketplaceDetailFooter.getByText('Added to this project', { exact: true });
52
+ const retryButton = this.marketplaceDetailFooter.getByRole('button', {
53
+ name: 'Try again',
54
+ });
55
+ const detailState = addButton.or(addedStatus).or(retryButton);
56
+ for (let attempt = 1; attempt <= 3; attempt++) {
57
+ await detailState.waitFor({ state: 'visible', timeout: 120000 });
58
+ if (await addedStatus.isVisible()) {
59
+ break;
60
+ }
61
+ if (await retryButton.isVisible()) {
62
+ if (attempt === 3) {
63
+ throw new Error('Browse All could not inspect the Marketplace connector after 3 attempts.');
64
+ }
65
+ await retryButton.click({ timeout: 60000 });
66
+ continue;
67
+ }
68
+ await addButton.click({ timeout: 60000 });
69
+ if (await this.completeImportReviewIfNecessary(retryButton)) {
70
+ break;
71
+ }
72
+ if (attempt === 3) {
73
+ throw new Error('Browse All could not import the Marketplace connector after 3 attempts.');
74
+ }
75
+ }
76
+ await this.availableElementsHeading.waitFor({
77
+ state: 'visible',
78
+ timeout: 120000,
79
+ });
80
+ await browseAllDialog
81
+ .getByRole('button', { name: 'Close' })
82
+ .click({ timeout: 60000 });
83
+ await browseAllDialog.waitFor({ state: 'hidden', timeout: 60000 });
84
+ }
85
+ async completeImportReviewIfNecessary(retryButton) {
86
+ await this.importModal
87
+ .or(this.availableElementsHeading)
88
+ .or(retryButton)
89
+ .waitFor({ state: 'visible', timeout: 120000 });
90
+ if (await retryButton.isVisible()) {
91
+ return false;
92
+ }
93
+ if (!(await this.importModal.isVisible())) {
94
+ return true;
95
+ }
96
+ const confirmButton = this.importModal
97
+ .getByRole('button', {
98
+ name: /^(Add to project|Replace resource|Save as copy)$/,
99
+ })
100
+ .first();
101
+ const confirmationVisible = await confirmButton
102
+ .waitFor({ state: 'visible', timeout: 10000 })
103
+ .then(() => true)
104
+ .catch(() => false);
105
+ if (confirmationVisible) {
106
+ await confirmButton.click({ timeout: 60000 });
107
+ }
108
+ await this.importModal.waitFor({ state: 'hidden', timeout: 120000 });
109
+ await this.availableElementsHeading
110
+ .or(retryButton)
111
+ .waitFor({ state: 'visible', timeout: 120000 });
112
+ return !(await retryButton.isVisible());
113
+ }
114
+ async findConnectorCard(connectorName) {
115
+ if (!connectorName) {
116
+ return this.marketplaceConnectorCard;
117
+ }
118
+ const exactCard = this.marketplaceConnectorList.getByRole('button', {
119
+ name: `View details for ${connectorName} from Marketplace`,
120
+ exact: true,
121
+ });
122
+ const searchWords = connectorName
123
+ .replace(/\s+connector$/i, '')
124
+ .trim()
125
+ .split(/\s+/)
126
+ .map(escapeRegExp)
127
+ .join('.*');
128
+ const relatedCard = this.marketplaceConnectorList
129
+ .getByRole('button', {
130
+ name: new RegExp(`^View details for .*${searchWords}.* from Marketplace$`, 'i'),
131
+ })
132
+ .first();
133
+ if (await exactCard.isVisible()) {
134
+ return exactCard;
135
+ }
136
+ if (await relatedCard.isVisible()) {
137
+ return relatedCard;
138
+ }
139
+ return this.marketplaceConnectorCard;
140
+ }
141
+ }
142
+ exports.BrowseAllConnectorMarketplacePage = BrowseAllConnectorMarketplacePage;
143
+ function escapeRegExp(value) {
144
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
145
+ }
@@ -1,7 +1,7 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ClusterDetailsPage {
3
3
  private page;
4
- readonly customPropertiesHeading: Locator;
4
+ readonly clusterDetailsHeading: Locator;
5
5
  constructor(page: Page);
6
6
  assertCustomPropertyIsNavigated(linkTitle: string, linkName: string, link: string | RegExp): Promise<void>;
7
7
  }