@camunda/e2e-test-suite 0.0.728 → 0.0.730

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.
@@ -445,12 +445,16 @@ class ClusterDetailsPage {
445
445
  async createAPIClient(name) {
446
446
  const maxRetries = 3;
447
447
  for (let attempt = 0; attempt < maxRetries; attempt++) {
448
+ // Use a suffixed name on retries so duplicate-name validation never
449
+ // leaves the Create button permanently disabled. A prior attempt may
450
+ // have created the client before we could detect the credentials dialog.
451
+ const attemptName = attempt === 0 ? name : `${name}-r${attempt}`;
448
452
  try {
449
453
  await this.clickCreateClientButton();
450
454
  await (0, test_1.expect)(this.createClientCredentialsDialog).toBeVisible({
451
455
  timeout: 30000,
452
456
  });
453
- await this.fillAPIClientName(name);
457
+ await this.fillAPIClientName(attemptName);
454
458
  await this.checkOrchestrationClusterCheckbox();
455
459
  await this.checkOptimizeCheckbox();
456
460
  await this.checkSecretsCheckbox();
@@ -487,7 +491,7 @@ class ClusterDetailsPage {
487
491
  .getByText('The Client Secret will not be shown again.')
488
492
  .first()).toBeVisible();
489
493
  try {
490
- await (0, test_1.expect)(this.clientsList.filter({ hasText: name })).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, {
494
+ await (0, test_1.expect)(this.clientsList.filter({ hasText: attemptName })).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, {
491
495
  timeout: 10000,
492
496
  });
493
497
  }
@@ -502,7 +506,7 @@ class ClusterDetailsPage {
502
506
  .catch(() => false);
503
507
  if (!oopsVisible)
504
508
  throw err;
505
- console.warn(`createAPIClient: clients list panel is in Oops state; skipping row scope assertion for "${name}".`);
509
+ console.warn(`createAPIClient: clients list panel is in Oops state; skipping row scope assertion for "${attemptName}".`);
506
510
  }
507
511
  return;
508
512
  }
@@ -5,7 +5,9 @@ declare class ConnectorMarketplacePage {
5
5
  readonly downloadToProjectButton: Locator;
6
6
  readonly snackbar: Locator;
7
7
  readonly closeButton: Locator;
8
+ readonly addToProjectButton: Locator;
8
9
  readonly replaceResourceButton: Locator;
10
+ readonly saveAsCopyButton: Locator;
9
11
  readonly cancelButton: Locator;
10
12
  constructor(page: Page);
11
13
  clickSearchForConnectorTextbox(): Promise<void>;
@@ -7,7 +7,9 @@ class ConnectorMarketplacePage {
7
7
  downloadToProjectButton;
8
8
  snackbar;
9
9
  closeButton;
10
+ addToProjectButton;
10
11
  replaceResourceButton;
12
+ saveAsCopyButton;
11
13
  cancelButton;
12
14
  constructor(page) {
13
15
  this.page = page;
@@ -17,9 +19,13 @@ class ConnectorMarketplacePage {
17
19
  .first();
18
20
  this.snackbar = page.locator('[data-test="snackbar"]');
19
21
  this.closeButton = page.getByRole('button', { name: 'Close' });
22
+ this.addToProjectButton = page.getByRole('button', {
23
+ name: 'Add to project',
24
+ });
20
25
  this.replaceResourceButton = page.getByRole('button', {
21
26
  name: 'Replace resource',
22
27
  });
28
+ this.saveAsCopyButton = page.getByRole('button', { name: 'Save as copy' });
23
29
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
24
30
  }
25
31
  async clickSearchForConnectorTextbox() {
@@ -42,14 +48,37 @@ class ConnectorMarketplacePage {
42
48
  }
43
49
  async downloadConnectorToProject() {
44
50
  await this.clickDownloadToProjectButton();
51
+ // After clicking "Download to project", Hub opens an ImportModal whose
52
+ // primary button depends on project state: "Add to project" (new template),
53
+ // "Replace resource" (same template exists), or "Save as copy" (conflict).
54
+ // Race all three click calls — Playwright's click() waits for actionability,
55
+ // so whichever button Hub actually renders will win the race and be clicked.
56
+ // If none appear within 20 s (e.g. the modal never opened), the race rejects
57
+ // and the catch path handles any residual Cancel button.
58
+ let imported = false;
45
59
  try {
46
60
  await Promise.race([
61
+ this.addToProjectButton.click({ timeout: 20000 }),
47
62
  this.replaceResourceButton.click({ timeout: 20000 }),
48
- this.cancelButton.click({ timeout: 20000 }),
63
+ this.saveAsCopyButton.click({ timeout: 20000 }),
49
64
  ]);
65
+ imported = true;
50
66
  }
51
67
  catch (e) {
52
- return;
68
+ if (await this.cancelButton.isVisible({ timeout: 3000 }).catch(() => false)) {
69
+ await this.cancelButton.click({ timeout: 10000 });
70
+ }
71
+ }
72
+ // Wait for the snackbar that confirms the template was received, then close
73
+ // the marketplace dialog so the change-element popup is unblocked before the
74
+ // caller selects the newly installed connector.
75
+ if (imported) {
76
+ await this.snackbar
77
+ .waitFor({ state: 'visible', timeout: 60000 })
78
+ .catch(() => { });
79
+ if (await this.closeButton.isVisible({ timeout: 5000 }).catch(() => false)) {
80
+ await this.closeButton.click({ timeout: 10000 });
81
+ }
53
82
  }
54
83
  }
55
84
  }
@@ -14,6 +14,7 @@ declare class ConnectorSettingsPage {
14
14
  readonly httpEndpointTab: Locator;
15
15
  readonly correlationTab: Locator;
16
16
  readonly inputTab: Locator;
17
+ readonly detailsPanelToggle: Locator;
17
18
  constructor(page: Page);
18
19
  clickUrlInput(): Promise<void>;
19
20
  fillUrlInput(url: string): Promise<void>;
@@ -18,6 +18,7 @@ class ConnectorSettingsPage {
18
18
  httpEndpointTab;
19
19
  correlationTab;
20
20
  inputTab;
21
+ detailsPanelToggle;
21
22
  constructor(page) {
22
23
  this.page = page;
23
24
  this.urlInput = page.getByLabel('URL');
@@ -50,6 +51,7 @@ class ConnectorSettingsPage {
50
51
  .locator('div')
51
52
  .filter({ hasText: /^Input$/ })
52
53
  .first();
54
+ this.detailsPanelToggle = page.locator('[data-test="details-panel-toggle"]');
53
55
  }
54
56
  async clickUrlInput() {
55
57
  await this.urlInput.click({ timeout: 90000 });
@@ -139,6 +141,9 @@ class ConnectorSettingsPage {
139
141
  await this.correlationTab.click({ timeout: 60000 });
140
142
  }
141
143
  async clickInputTab() {
144
+ if (!(await this.inputTab.isVisible({ timeout: 5000 }).catch(() => false))) {
145
+ await this.detailsPanelToggle.click({ timeout: 10000 });
146
+ }
142
147
  await this.inputTab.click({ timeout: 60000 });
143
148
  }
144
149
  }
@@ -70,14 +70,25 @@ class LoginPage {
70
70
  }
71
71
  async loginWithTestUser(credentials = {}) {
72
72
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
73
- // Navigate to app root to trigger auth redirect. The Console app (at
74
- // console.* hostnames) embeds a hidden invite-user email input also labeled
75
- // "Email address" that would otherwise satisfy the locator below before the
76
- // auth0 redirect has had a chance to complete.
77
- await this.page.goto('/');
78
- await this.page
79
- .waitForURL((url) => !url.hostname.includes('console'), { timeout: 60000 })
80
- .catch(() => { });
73
+ // Skip navigation if the auth0 login page is already loaded re-navigating
74
+ // to '/' when already on auth0 undoes the redirect and causes a 60-second
75
+ // waitForURL race. This is the common case when called from loginWithRetry,
76
+ // which already lands on the auth0 page before calling this method.
77
+ const alreadyOnLoginPage = await this.usernameInput.locator
78
+ .isVisible({ timeout: 2000 })
79
+ .catch(() => false);
80
+ if (!alreadyOnLoginPage) {
81
+ // Navigate to app root to trigger auth redirect. The Console app (at
82
+ // console.* hostnames) embeds a hidden invite-user email input also labeled
83
+ // "Email address" that would otherwise satisfy the locator below before the
84
+ // auth0 redirect has had a chance to complete.
85
+ await this.page.goto('/');
86
+ await this.page
87
+ .waitForURL((url) => !url.hostname.includes('console'), {
88
+ timeout: 60000,
89
+ })
90
+ .catch(() => { });
91
+ }
81
92
  const onPasswordStep = (await this.passwordHeading
82
93
  .isVisible({ timeout: 5000 })
83
94
  .catch(() => false)) ||
@@ -188,7 +188,8 @@ class ModelerCreatePage {
188
188
  this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
189
189
  this.publicHolidayConnectorOption = page
190
190
  .locator('[data-test="modeler"]')
191
- .getByText('Worldwide Public Holiday');
191
+ .getByText('Worldwide Public Holiday')
192
+ .first();
192
193
  this.publicHolidayYearOption = page.getByLabel('Year');
193
194
  this.publicHolidayCountryCodeOption = page.getByLabel('Countrycode');
194
195
  this.implementationSection = page.locator('[data-group-id="group-userTaskImplementation"]');
@@ -773,27 +774,40 @@ class ModelerCreatePage {
773
774
  const maxRetries = 4;
774
775
  for (let retries = 0; retries < maxRetries; retries++) {
775
776
  try {
776
- if (retries <= 2) {
777
- await this.publicHolidayConnectorOption.click({ timeout: 60000 });
778
- }
779
- else {
777
+ if (retries === 2) {
778
+ // Re-download connector on second retry in case the first download failed
780
779
  await this.clickMarketPlaceButton();
781
780
  const connectorMarketplacePage = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
782
781
  await connectorMarketplacePage.clickSearchForConnectorTextbox();
783
782
  await connectorMarketplacePage.fillSearchForConnectorTextbox('Public Holiday Connector');
784
783
  await (0, sleep_1.sleep)(10000);
785
784
  await connectorMarketplacePage.downloadConnectorToProject();
786
- await this.publicHolidayConnectorOption.click({ timeout: 120000 });
787
785
  }
786
+ // Always open the change-type popup before trying to click the option
787
+ await this.clickCanvas();
788
+ await this.secondElement.click({ timeout: 60000 });
789
+ await this.clickChangeTypeButton();
790
+ await (0, test_1.expect)(this.marketPlaceButton).toBeVisible({ timeout: 60000 });
791
+ // Filter the virtualized change-element popup so the option scrolls into view
792
+ const changeElementSearch = this.page.locator('.djs-popup-search input');
793
+ if (await changeElementSearch
794
+ .isVisible({ timeout: 5000 })
795
+ .catch(() => false)) {
796
+ await changeElementSearch.pressSequentially('Worldwide Public Holiday', { delay: 50 });
797
+ }
798
+ await this.publicHolidayConnectorOption.waitFor({
799
+ state: 'visible',
800
+ timeout: 60000,
801
+ });
802
+ await this.publicHolidayConnectorOption.click({ timeout: 30000 });
788
803
  return;
789
804
  }
790
805
  catch (error) {
791
806
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
807
+ if (retries >= maxRetries - 1)
808
+ break;
792
809
  await this.page.reload();
793
810
  await new Promise((resolve) => setTimeout(resolve, 10000));
794
- await this.clickCanvas();
795
- await this.secondElement.click({ timeout: 60000 });
796
- await this.clickChangeTypeButton();
797
811
  }
798
812
  }
799
813
  throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
@@ -31,6 +31,30 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3
31
31
  })
32
32
  .catch(() => { });
33
33
  await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
34
+ // The evaluate above ran on about:blank, not on the Console domain, so it
35
+ // did not clear Console's own localStorage. Clear it now while on the
36
+ // Console domain, then reload to force a fresh auth check with no cached
37
+ // tokens — without this, Console can keep a session alive via localStorage
38
+ // even after cookies are cleared, preventing the auth0 redirect.
39
+ await page
40
+ .evaluate(() => {
41
+ try {
42
+ localStorage.clear();
43
+ }
44
+ catch (_) {
45
+ // ignore — storage may be unavailable in some contexts
46
+ }
47
+ try {
48
+ sessionStorage.clear();
49
+ }
50
+ catch (_) {
51
+ // ignore — storage may be unavailable in some contexts
52
+ }
53
+ })
54
+ .catch(() => { });
55
+ await page
56
+ .reload({ waitUntil: 'domcontentloaded', timeout: 60000 })
57
+ .catch(() => { });
34
58
  await page
35
59
  .waitForLoadState('networkidle', { timeout: 30000 })
36
60
  .catch(() => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.728",
3
+ "version": "0.0.730",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",