@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.
@@ -15,16 +15,35 @@ class HomePage {
15
15
  appSwitcher;
16
16
  openOrganizationButton;
17
17
  consoleBanner;
18
+ hubHeader;
19
+ hubSidebar;
18
20
  modelerBanner;
19
21
  gettingStartedHeading;
20
22
  buttonSkipCustomization;
21
23
  constructor(page) {
22
24
  this.page = page;
25
+ // Camunda Hub replaces Console on SaaS: an app header (only carrying the
26
+ // user menu once authenticated) plus a sidebar whose Console section
27
+ // links to /org/<id>/clusters and /org/<id>/management.
28
+ this.hubHeader = page
29
+ .locator('header[data-slot="app-header"]')
30
+ .filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
31
+ this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
23
32
  this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
33
+ // Once an org has environmentsEnabled, camunda-hub's
34
+ // use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
35
+ // labelled "Environments" whose href is '/environments', not
36
+ // '/clusters' -- accept both hrefs so this resolves either way.
24
37
  this.clusterTab = page
25
38
  .getByRole('banner')
26
- .getByRole('link', { name: 'Clusters' });
27
- this.organizationTab = page.getByRole('link', { name: 'Organization' });
39
+ .getByRole('link', { name: 'Clusters' })
40
+ .or(this.hubSidebar.locator('a[href$="/clusters"]'))
41
+ .or(this.hubSidebar.locator('a[href$="/environments"]'))
42
+ .first();
43
+ this.organizationTab = page
44
+ .getByRole('link', { name: 'Organization' })
45
+ .or(this.hubSidebar.locator('a[href$="/management"]'))
46
+ .first();
28
47
  this.organizationViewTitle = page.getByText('Organization Management');
29
48
  this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
30
49
  this.informationDialog = page.getByRole('button', {
@@ -32,14 +51,23 @@ class HomePage {
32
51
  });
33
52
  this.camundaAppsButton = page.getByLabel('Camunda apps');
34
53
  this.appSwitcher = page.getByLabel('App Switcher');
54
+ // Specs assert this as "the authenticated app shell has rendered" before
55
+ // navigating. Camunda Hub has no app-switcher button at all (apps are
56
+ // reached from the sidebar), so on Hub its authenticated header is the
57
+ // equivalent readiness signal.
35
58
  this.camundaComponentsButton = page
36
59
  .getByLabel('Camunda components')
37
60
  .or(this.camundaAppsButton)
38
- .or(this.appSwitcher);
61
+ .or(this.appSwitcher)
62
+ .or(this.hubHeader);
39
63
  this.openOrganizationButton = page.getByLabel('Open Organizations');
40
- this.consoleBanner = page.getByRole('link', {
41
- name: 'Camunda logo Console',
42
- });
64
+ // "Camunda logo Console" was the old Carbon header's logo link. Camunda
65
+ // Hub has no equivalent link -- its post-login landing is `hubHeader`
66
+ // (see above) -- so callers that assert this banner as a login/Console
67
+ // arrival signal need the Hub fallback too.
68
+ this.consoleBanner = page
69
+ .getByRole('link', { name: 'Camunda logo Console' })
70
+ .or(this.hubHeader);
43
71
  this.gettingStartedHeading = page.getByText('start by creating a project');
44
72
  this.buttonSkipCustomization = page.getByRole('button', {
45
73
  name: 'Skip customization',
@@ -49,8 +77,35 @@ class HomePage {
49
77
  });
50
78
  }
51
79
  async clickClusters() {
52
- await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 15000 });
80
+ await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 30000 });
53
81
  await this.clusterTab.click({ timeout: 15000 });
82
+ await this.clickClustersPageTabIfPresent();
83
+ }
84
+ // Landing on Hub's Environments overview (see clusterTab above) doesn't put
85
+ // us on the classic row-based cluster list ClusterPage.ts expects -- that
86
+ // page renders clusters as cards. Its own header renders a separate
87
+ // "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
88
+ // that does.
89
+ async clickClustersPageTabIfPresent() {
90
+ const clustersPageTab = this.page
91
+ .getByRole('navigation', { name: 'Environments views' })
92
+ .getByRole('link', { name: 'Clusters', exact: true });
93
+ // Required on Hub, not optional: this page's row-based cluster locators
94
+ // cannot be satisfied by the card-based Environments overview we just
95
+ // landed on. isVisible() does not wait, so a slow tab render used to be
96
+ // read as "no tab here" and the flow continued on the wrong route.
97
+ const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
98
+ const tabShown = await clustersPageTab
99
+ .waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
100
+ .then(() => true)
101
+ .catch(() => false);
102
+ if (tabShown) {
103
+ await clustersPageTab.click({ timeout: 15000 });
104
+ }
105
+ else if (onHub) {
106
+ throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
107
+ 'row-based cluster list this page needs is unreachable without it');
108
+ }
54
109
  }
55
110
  async clickDashboard() {
56
111
  await this.dashboardTab.click({ timeout: 90000 });
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
5
5
  const authErrorClassification_1 = require("../../utils/authErrorClassification");
6
6
  const UtilitiesPage_1 = require("./UtilitiesPage");
7
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
8
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
8
9
  class LoginPage {
9
10
  page;
10
11
  usernameInput;
@@ -50,7 +51,34 @@ class LoginPage {
50
51
  }
51
52
  async fillPassword(password) {
52
53
  await this.passwordInput.waitFor({ state: 'visible', timeout: 60000 });
53
- await this.passwordInput.fill(password);
54
+ // Auth0's password screen becomes visible before its own scripts finish
55
+ // wiring up the field, and that initialisation wipes whatever is already
56
+ // in it. A discarded value makes Continue post an empty password, Auth0
57
+ // re-renders the same screen with "Password is required", and a login can
58
+ // burn all five retries without ever reaching the app.
59
+ //
60
+ // Typed, not filled: a live trace of that failure shows six fill +
61
+ // read-back cycles in a row all coming back empty, so re-filling cannot
62
+ // help -- `fill()` sets the value in one shot and Auth0's own handlers
63
+ // never see it, leaving their state empty and the field cleared on the
64
+ // next render. pressSequentially dispatches real key events, which those
65
+ // handlers do process.
66
+ //
67
+ // Polled rather than slept on: the check settles as soon as the value is
68
+ // there, so a healthy login pays nothing.
69
+ //
70
+ // Only the LENGTH is ever read back or asserted on, so the secret cannot
71
+ // reach a log, a trace entry or an assertion-failure message.
72
+ await (0, test_1.expect)(async () => {
73
+ await this.passwordInput.click();
74
+ await this.passwordInput.fill('');
75
+ await this.passwordInput.pressSequentially(password, { delay: 20 });
76
+ await test_1.expect
77
+ .poll(async () => this.passwordInput
78
+ .evaluate((el) => el.value.length)
79
+ .catch(() => 0), { timeout: 5000, intervals: [200, 400, 800] })
80
+ .toBe(password.length);
81
+ }).toPass({ timeout: 60000, intervals: [500, 1000, 2000] });
54
82
  }
55
83
  async clickLoginButton() {
56
84
  await this.loginButton.click({ timeout: 60000 });
@@ -58,6 +86,7 @@ class LoginPage {
58
86
  async login(credentials = {}) {
59
87
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
60
88
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
89
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
61
90
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 200000 });
62
91
  await this.fillUsername(username);
63
92
  await this.clickContinueButton();
@@ -69,6 +98,7 @@ class LoginPage {
69
98
  async loginWithTestUser(credentials = {}) {
70
99
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
71
100
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
101
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
72
102
  // SSO re-auth may silently log us back in as the previous user after logout.
73
103
  // Detect Console (settings button visible) and retry logout until Auth0 form appears.
74
104
  const settingsBtn = this.page.getByLabel('Open Settings');
@@ -119,6 +149,7 @@ class LoginPage {
119
149
  async loginWithoutOrgAssertion(credentials = {}) {
120
150
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
121
151
  (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
152
+ (0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
122
153
  await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
123
154
  await this.fillUsername(username);
124
155
  await this.clickContinueButton();
@@ -145,6 +145,7 @@ declare class ModelerCreatePage {
145
145
  runProcessInstance(clusterName: string, variables?: string, formVariables?: {
146
146
  [key: string]: string;
147
147
  }, textToVisible?: string, expectRejection?: boolean): Promise<void>;
148
+ selectOnlyThisResource(): Promise<void>;
148
149
  selectCluster(clusterName: string): Promise<void>;
149
150
  clickViewProcessInstanceLink(): Promise<void>;
150
151
  clickNameInput(): Promise<void>;
@@ -7,6 +7,9 @@ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocat
7
7
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
8
8
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
+ const hubResourceRename_1 = require("../../utils/hubResourceRename");
11
+ const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
13
  class ModelerCreatePage {
11
14
  page;
12
15
  processApplicationPage;
@@ -172,14 +175,38 @@ class ModelerCreatePage {
172
175
  this.nameInput = page.getByLabel('Name', { exact: true });
173
176
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
174
177
  this.renameDiagramNameButton = page.getByText('Rename');
175
- this.diagramNameInput = page.locator('[data-test="editable-input"]');
178
+ // Hub's rename dialog field, with the legacy inline editable kept for
179
+ // shells where the design-system migration has not shipped.
180
+ this.diagramNameInput = page
181
+ .locator('[data-test="rename-input"]')
182
+ .or(page.locator('[data-test="editable-input"]'))
183
+ .first();
176
184
  this.variableInput = page.locator('[id="variables-json"]');
177
185
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
178
- this.embedButton = page.locator('[data-test="confirm-move"]');
186
+ // Hub's link picker confirms with `link-target-confirm` ("Link");
187
+ // `confirm-move` was the old move/link dialog's id and exists only in
188
+ // hub's older cypress specs now. Keep it as a fallback.
189
+ this.embedButton = page
190
+ .locator('[data-test="link-target-confirm"]')
191
+ .or(page.locator('[data-test="confirm-move"]'))
192
+ .first();
193
+ // Same picker as clickForm(): entries are buttons labelled
194
+ // "Select <name>" inside `link-target-list`, not `item-<name>` nodes.
179
195
  this.newForm = page
180
- .locator('[data-test="item-New form"]')
181
- .getByText('New form');
182
- this.deploySubButton = page.locator('[data-test="deploy-action"]');
196
+ .locator('[data-test="link-target-list"]')
197
+ .getByRole('button', { name: /^Select New form/ })
198
+ .first()
199
+ .or(page.locator('[data-test="item-New form"]').getByText('New form'))
200
+ .first();
201
+ // `deploy-action` is absent from Hub's shell (a DOM audit of a live run
202
+ // found no such id); its deploy dialog confirms with a plain button named
203
+ // "Deploy" in the dialog footer.
204
+ this.deploySubButton = page
205
+ .locator('[data-test="deploy-action"]')
206
+ .or(page
207
+ .getByRole('dialog')
208
+ .getByRole('button', { name: 'Deploy', exact: true }))
209
+ .first();
183
210
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
184
211
  this.restConnectorOption = page.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
185
212
  this.changeElementSearchInput = page.locator('.djs-popup-search input');
@@ -337,12 +364,25 @@ class ModelerCreatePage {
337
364
  this.backupDeployButton = page.getByRole('button', {
338
365
  name: 'Deploy',
339
366
  });
367
+ // Hub's design-system shell emits no Carbon markup, so the `cds--toast-*`
368
+ // scope matched nothing and this assertion read as "not linked" even on a
369
+ // successful link. Assert the message itself and keep the Carbon scope for
370
+ // shells that still render it.
340
371
  this.formLinkedSuccessMessage = page
372
+ .getByText('Form has been successfully linked.')
373
+ .or(page
341
374
  .locator('.cds--toast-notification__subtitle')
342
- .getByText('Form has been successfully linked.');
343
- this.diagramNameBreadcrumb = (diagramName) => page.locator('button[data-test="breadcrumb-diagram"]', {
375
+ .getByText('Form has been successfully linked.'))
376
+ .first();
377
+ // Hub's breadcrumbs carry no data-test (DOM audit of a live run found none
378
+ // of the breadcrumb-* ids present), so address the diagram by its own name
379
+ // in the breadcrumb navigation and keep the legacy id as a fallback.
380
+ this.diagramNameBreadcrumb = (diagramName) => page
381
+ .locator('button[data-test="breadcrumb-diagram"]', {
344
382
  hasText: diagramName,
345
- });
383
+ })
384
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page).getByText(diagramName, { exact: true }))
385
+ .first();
346
386
  this.webhookActiveMessage = page.getByText('Webhook is active');
347
387
  }
348
388
  async clickSecondDeployButton() {
@@ -500,10 +540,16 @@ class ModelerCreatePage {
500
540
  // a form with the same name and the picker then lists two identical entries.
501
541
  // Scope to the first matching item so the link step stays deterministic on retry
502
542
  // instead of failing with a strict-mode violation.
543
+ // Hub's link picker is `ul[data-test="link-target-list"]` whose entries are
544
+ // buttons labelled "Select <name>" with a per-file
545
+ // `data-test="link-target-file-<uuid>"`; the old `item-<name>` id does not
546
+ // exist on it (a DOM audit of a live run found no `item-*` at all). Keep
547
+ // the legacy id as a fallback for pre-design-system shells.
503
548
  await this.page
504
- .locator(`[data-test="item-${name}"]`)
549
+ .locator('[data-test="link-target-list"]')
550
+ .getByRole('button', { name: `Select ${name}`, exact: true })
551
+ .or(this.page.locator(`[data-test="item-${name}"]`).first().getByText(name))
505
552
  .first()
506
- .getByText(name)
507
553
  .click({ timeout: 90000 });
508
554
  }
509
555
  async clickUserTask(id) {
@@ -620,6 +666,12 @@ class ModelerCreatePage {
620
666
  const retryDelayMs = 20000;
621
667
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
622
668
  try {
669
+ // Hub lists only the ENVIRONMENTS assigned to the workspace in this
670
+ // dialog, so a cluster the workspace does not carry is simply absent
671
+ // from the picker. deployForm() ensures that link for the form path;
672
+ // a spec that runs a diagram without deploying a form first would
673
+ // otherwise reach this dialog with nothing to select.
674
+ await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
623
675
  // Inside the loop, not before it: a no-op once bound, but selectCluster
624
676
  // clears the binding when the dialog does not offer the cluster, so the
625
677
  // next attempt rebinds rather than reopening an unchanged dialog.
@@ -677,8 +729,18 @@ class ModelerCreatePage {
677
729
  }
678
730
  }
679
731
  }
732
+ // Scopes Hub's deploy/run dialog to the single resource being deployed.
733
+ // Mirrors FormJsPage.selectOnlyThisResource(): the select trigger carries no
734
+ // aria-labelledby, so it is addressed by its id (`deployment-scope-selection`)
735
+ // -- a name-based lookup silently matches nothing -- and its options render
736
+ // in a portal attached to <body>, outside the dialog, so they are looked up
737
+ // page-scoped. A no-op on dialogs that offer no scope choice.
738
+ async selectOnlyThisResource() {
739
+ await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
740
+ }
680
741
  async selectCluster(clusterName) {
681
742
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
743
+ await this.selectOnlyThisResource();
682
744
  const healthCheckTimeout = 30000;
683
745
  // Inside a process application the deploy dialog renders
684
746
  // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
@@ -720,15 +782,13 @@ class ModelerCreatePage {
720
782
  }
721
783
  return;
722
784
  }
723
- const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
785
+ const healthyRegex = new RegExp(`${clusterName}.*?Healthy`);
724
786
  const clusterLabel = this.dialog
725
787
  .locator('label')
726
788
  .filter({ hasText: new RegExp(`^${clusterName}`) });
727
789
  await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
728
790
  await clusterLabel.click();
729
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
730
- timeout: 30000,
731
- });
791
+ await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
732
792
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
733
793
  timeout: healthCheckTimeout,
734
794
  });
@@ -809,8 +869,10 @@ class ModelerCreatePage {
809
869
  }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
810
870
  }
811
871
  async enterDiagramName(name) {
812
- await this.diagramNameInput.fill(name, { timeout: 90000 });
813
- await this.diagramNameInput.press('Enter', { timeout: 90000 });
872
+ // Hub renames through the current crumb's actions menu into a shared
873
+ // dialog; the inline editable this used to fill never mounts on its own.
874
+ // "New BPMN diagram" is the server default the diagram still carries here.
875
+ await (0, hubResourceRename_1.renameHubResource)(this.page, 'New BPMN diagram', name);
814
876
  }
815
877
  async clickVariableInput() {
816
878
  await this.variableInput.click({ timeout: 60000 });
@@ -1,12 +1,18 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private defaultFolderName;
4
+ private defaultClusterName;
4
5
  private page;
5
6
  private processApplicationPage;
6
7
  private processApplicationUrl?;
7
8
  private processApplicationName?;
8
9
  readonly modelerPageBanner: Locator;
9
10
  readonly createNewProjectButton: Locator;
11
+ readonly createWorkspaceButton: Locator;
12
+ readonly workspaceWizardNameInput: Locator;
13
+ readonly workspaceWizardSubmitButton: Locator;
14
+ readonly workspaceWizardMembersStep: Locator;
15
+ readonly workspaceWizardEnvironmentsHeading: Locator;
10
16
  readonly projectNameInput: Locator;
11
17
  readonly formNameInput: Locator;
12
18
  readonly chooseBpmnTemplateButton: Locator;
@@ -49,6 +55,7 @@ declare class ModelerHomePage {
49
55
  reload(): Promise<void>;
50
56
  getProjectNames(name: string): Promise<string[]>;
51
57
  clickProcessDiagram(name: string): Promise<void>;
58
+ private openProjectCreateMenuWithResources;
52
59
  clickDiagramTypeDropdown(): Promise<void>;
53
60
  clickWorkspaceDiagramTypeDropdown(): Promise<void>;
54
61
  clickBpmnTemplateOption(): Promise<void>;
@@ -61,7 +68,9 @@ declare class ModelerHomePage {
61
68
  private ensureInResourceContainer;
62
69
  returnToResourceContainer(): Promise<void>;
63
70
  startBlankBpmnDiagram(): Promise<void>;
71
+ showAllWorkspaces(probeTimeout: number): Promise<void>;
64
72
  private becameVisible;
73
+ private createWorkspaceViaWizard;
65
74
  clickHomeBreadcrumb(): Promise<void>;
66
75
  private dismissBlockingDialogs;
67
76
  clickOpenOrganizationsButton(): Promise<void>;
@@ -70,6 +79,7 @@ declare class ModelerHomePage {
70
79
  clickCrossComponentProjectFolder(): Promise<void>;
71
80
  private clickCrossComponentProjectFolderWithRetry;
72
81
  createCrossComponentProjectFolder(): Promise<void>;
82
+ private enterProjectInsideWorkspace;
73
83
  clickManageButton(retries?: number): Promise<void>;
74
84
  clickUploadFilesButton(): Promise<void>;
75
85
  clickMessageBanner(): Promise<void>;