@camunda/e2e-test-suite 0.0.1020 → 0.0.1022

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.
@@ -16,6 +16,7 @@ declare class ClusterSecretsPage {
16
16
  readonly confirmDeleteInput: Locator;
17
17
  constructor(page: Page);
18
18
  deleteConnectorSecretsIfExist(): Promise<void>;
19
+ private dismissOpenModal;
19
20
  clickCreateNewSecretButton(clusterName: string): Promise<void>;
20
21
  clickClusterBanner(): Promise<void>;
21
22
  clickCluster(name: string): Promise<void>;
@@ -38,14 +38,25 @@ class ClusterSecretsPage {
38
38
  name: 'Delete',
39
39
  exact: true,
40
40
  });
41
- this.deleteConnectorSecretSubButton = page.getByRole('button', {
42
- name: 'danger Delete',
43
- });
41
+ // The confirmation modal's destructive action is a plain "Delete" button
42
+ // (Console renders Carbon's ModalFooter danger button, whose accessible
43
+ // name is just its label). The old 'danger Delete' name came from a Carbon
44
+ // version that prefixed danger buttons with a visually-hidden kind label;
45
+ // it matches nothing today, so the click timed out and left the modal open
46
+ // on top of the page. Scoped to the dialog so it cannot collide with the
47
+ // row-level Delete control behind it.
48
+ this.deleteConnectorSecretSubButton = page
49
+ .getByRole('dialog')
50
+ .getByRole('button', { name: 'Delete', exact: true });
44
51
  this.dialog = page.getByRole('dialog');
45
52
  this.clusterBanner = this.page
46
53
  .getByRole('banner')
47
54
  .getByRole('link', { name: 'Clusters' });
48
- this.clusterLink = (name) => page.getByRole('link', { name: name });
55
+ // exact: true because console-user-flows.spec.ts creates a second cluster
56
+ // called "UI Test Cluster" in the same org -- a substring match on
57
+ // "Test Cluster" then resolves to both links and fails on strict mode.
58
+ // ClusterPage's own cluster locators already match exactly.
59
+ this.clusterLink = (name) => page.getByRole('link', { name: name, exact: true });
49
60
  this.clusterSecretTab = this.page.getByRole('tab', {
50
61
  name: 'Connector Secrets',
51
62
  });
@@ -95,13 +106,39 @@ class ClusterSecretsPage {
95
106
  break;
96
107
  }
97
108
  }
98
- await (0, test_1.expect)(this.optionsButton).not.toBeVisible({
99
- timeout: 30000,
100
- });
109
+ // Console auto-provisions CAMUNDA_PROVIDED_LLM_* secrets whose Delete
110
+ // menu item is disabled, so the table is never empty once the sweep is
111
+ // done. A page-wide not.toBeVisible() therefore matched three Options
112
+ // buttons at once (strict mode violation) while asserting something that
113
+ // is no longer true. The post-condition this helper actually owns is
114
+ // that nothing *deletable* is left behind.
115
+ const deletableSecretRows = this.page
116
+ .getByRole('row')
117
+ .filter({ has: this.page.getByRole('cell', { name: 'Options' }) })
118
+ .filter({ hasNotText: 'CAMUNDA_PROVIDED_' });
119
+ await (0, test_1.expect)(deletableSecretRows).toHaveCount(0, { timeout: 30000 });
101
120
  }
102
121
  catch (error) {
103
122
  console.error('Error during Connector Secrets deletion: ', error);
123
+ // A half-completed delete leaves Carbon's confirmation modal on screen,
124
+ // where its overlay swallows pointer events for every later click -- the
125
+ // next "Create new secret" click then burns its whole timeout against
126
+ // the overlay instead of the button. Cleanup must never hand the page
127
+ // back with a modal still open.
128
+ await this.dismissOpenModal();
129
+ }
130
+ }
131
+ // Closes whichever Carbon modal is currently on screen, and waits for it to
132
+ // actually leave the DOM rather than assuming a fixed delay.
133
+ async dismissOpenModal() {
134
+ const openModal = this.page.locator('.cds--modal.is-visible');
135
+ if ((await openModal.count().catch(() => 0)) === 0) {
136
+ return;
104
137
  }
138
+ await this.page.keyboard.press('Escape').catch(() => { });
139
+ await (0, test_1.expect)(openModal)
140
+ .toHaveCount(0, { timeout: 10000 })
141
+ .catch(() => { });
105
142
  }
106
143
  async clickCreateNewSecretButton(clusterName) {
107
144
  try {
@@ -118,16 +155,7 @@ class ClusterSecretsPage {
118
155
  }
119
156
  }
120
157
  async clickClusterBanner() {
121
- try {
122
- const blockingModal = this.page.locator('.cds--modal.is-visible');
123
- if ((await blockingModal.count()) > 0) {
124
- await this.page.keyboard.press('Escape');
125
- await (0, sleep_1.sleep)(1000);
126
- }
127
- }
128
- catch (_e) {
129
- // no blocking modal to dismiss
130
- }
158
+ await this.dismissOpenModal();
131
159
  await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
132
160
  await this.clusterBanner.click({ timeout: 60000 });
133
161
  }
@@ -89,8 +89,14 @@ async function modelRestConnector(modelerCreatePage, connectorSettingsPage, proc
89
89
  username: '',
90
90
  password: '',
91
91
  }) {
92
+ // 120s to match every other properties-panel wait in the 8.7 suite
93
+ // (modelJobWorkerDiagram, clickDiagramBreadcrumb and the specs' own waits all
94
+ // allow 90-200s). 30s was the lone outlier here and the bpmn-js properties
95
+ // panel regularly needs longer than that to mount on a freshly created
96
+ // diagram, which failed the REST connector flows before a single element was
97
+ // modelled.
92
98
  await (0, test_1.expect)(modelerCreatePage.generalPanel).toBeVisible({
93
- timeout: 30000,
99
+ timeout: 120000,
94
100
  });
95
101
  await modelerCreatePage.enterDiagramName(processName);
96
102
  await (0, sleep_1.sleep)(10000);
@@ -27,7 +27,7 @@ declare class ManagementIdentityPage {
27
27
  clickUser(username: string): Promise<void>;
28
28
  clickAssignedRolesTab(): Promise<void>;
29
29
  clickDemoUser(): Promise<void>;
30
- clickDeleteAccessButton(accessToDelete: string): Promise<void>;
30
+ clickDeleteAccessButton(accessToDelete: string | string[]): Promise<void>;
31
31
  clickConfirmDeleteButton(): Promise<void>;
32
32
  clickAssignRolesButton(): Promise<void>;
33
33
  clickAddButton(): Promise<void>;
@@ -46,9 +46,16 @@ class ManagementIdentityPage {
46
46
  this.optimizeCheckbox = page
47
47
  .locator('label')
48
48
  .filter({ hasText: 'Optimize (Grants full access to Optimize)' });
49
- this.modelerCheckbox = page
50
- .locator('label')
51
- .filter({ hasText: 'Hub (Grants full access to Hub)' });
49
+ // Mid-rename: camunda/identity#4541 moves these role descriptions from Web
50
+ // Modeler to Hub, and the role names follow only later. Depending on which
51
+ // Identity image the deployed chart carries, the same checkbox renders as
52
+ // "Web Modeler (Grants full access to Web Modeler)", the half-renamed
53
+ // "Web Modeler (Grants full access to Hub)", or "Hub (Grants full access to
54
+ // Hub)". Match all three so the suite stays green on either side of the
55
+ // rollout. The " (" after the name keeps this off the Admin row.
56
+ this.modelerCheckbox = page.locator('label').filter({
57
+ hasText: /(Web Modeler|Hub) \(Grants full access to (Web Modeler|Hub)\)/,
58
+ });
52
59
  this.identityCheckbox = page.locator('label').filter({
53
60
  hasText: 'ManagementIdentity (Provides full access to Management Identity)',
54
61
  });
@@ -60,8 +67,9 @@ class ManagementIdentityPage {
60
67
  this.licenseKeyTagNonCommercial = page.getByText('Non-commercial license');
61
68
  this.licenseKeyTagProduction = page.getByText('Production license').first();
62
69
  this.licenseKeyTagCommercial = page.getByText('Commercial license');
70
+ // Same Web Modeler → Hub rename straddle as `modelerCheckbox` above.
63
71
  this.modelerAdminCheckbox = page.locator('label').filter({
64
- hasText: 'Hub Admin (Grants elevated access to Hub)',
72
+ hasText: /(Web Modeler|Hub) Admin \(Grants elevated access to (Web Modeler|Hub)\)/,
65
73
  });
66
74
  this.openSidebarButton = page.getByLabel('Open User Sidebar');
67
75
  this.logoutButton = page.getByRole('button', { name: 'Log out' });
@@ -100,9 +108,9 @@ class ManagementIdentityPage {
100
108
  // and it does not re-render on its own. Rebuild it from a fresh response
101
109
  // between attempts before reporting the role as missing.
102
110
  async clickDeleteAccessButton(accessToDelete) {
103
- const roleRow = this.page
104
- .locator(':text("' + accessToDelete + '")')
105
- .first();
111
+ const candidates = typeof accessToDelete === 'string' ? [accessToDelete] : accessToDelete;
112
+ const rowSelector = candidates.map((c) => `:text("${c}")`).join(', ');
113
+ const roleRow = this.page.locator(rowSelector).first();
106
114
  for (let attempt = 1; attempt <= 2; attempt++) {
107
115
  const rowVisible = await roleRow
108
116
  .waitFor({ state: 'visible', timeout: 30000 })
@@ -114,10 +122,12 @@ class ManagementIdentityPage {
114
122
  await this.page.reload({ waitUntil: 'domcontentloaded' });
115
123
  await this.clickAssignedRolesTab();
116
124
  }
117
- await (0, test_1.expect)(roleRow, `No assigned role matching "${accessToDelete}" was found. The role may ` +
118
- `already be removed, or its label may have changed in this version.`).toBeVisible({ timeout: 60000 });
125
+ await (0, test_1.expect)(roleRow, `No assigned role matching ${candidates
126
+ .map((c) => `"${c}"`)
127
+ .join(' or ')} was found. The role may already be removed, or its ` +
128
+ `label may have changed in this version.`).toBeVisible({ timeout: 60000 });
119
129
  await this.page
120
- .locator('td:right-of(:text("' + accessToDelete + '"))')
130
+ .locator(candidates.map((c) => `td:right-of(:text("${c}"))`).join(', '))
121
131
  .first()
122
132
  .click({ timeout: 60000 });
123
133
  }
@@ -6,13 +6,17 @@ const LoginPage_1 = require("./LoginPage");
6
6
  const sleep_1 = require("../../utils/sleep");
7
7
  const constants_1 = require("../../utils/constants");
8
8
  const env_1 = require("../../utils/env");
9
- const ORCHESTRATION_CONTEXT_PATH = process.env.ORCHESTRATION_CONTEXT_PATH ?? '/orchestration';
9
+ // Context paths use `||`, not `??`: an env var set to the empty string must fall back to the
10
+ // default. CI wiring that always exports a variable (set to "" when the leg has no override)
11
+ // otherwise defeats the default — `page.goto('')` resolves to Playwright's baseURL, so the app
12
+ // under test is never reached and the failure surfaces as an unrelated navigation timeout.
13
+ const ORCHESTRATION_CONTEXT_PATH = process.env.ORCHESTRATION_CONTEXT_PATH || '/orchestration';
10
14
  const NORMALIZED_ORCHESTRATION_CONTEXT_PATH = ORCHESTRATION_CONTEXT_PATH.startsWith('/')
11
15
  ? ORCHESTRATION_CONTEXT_PATH
12
16
  : `/${ORCHESTRATION_CONTEXT_PATH}`;
13
- const MODELER_CONTEXT_PATH = process.env.MODELER_CONTEXT_PATH ?? '/modeler';
14
- const OPTIMIZE_CONTEXT_PATH = process.env.OPTIMIZE_CONTEXT_PATH ?? '/optimize';
15
- const MANAGEMENT_IDENTITY_CONTEXT_PATH = process.env.MANAGEMENT_IDENTITY_CONTEXT_PATH ?? '/identity';
17
+ const MODELER_CONTEXT_PATH = process.env.MODELER_CONTEXT_PATH || '/modeler';
18
+ const OPTIMIZE_CONTEXT_PATH = process.env.OPTIMIZE_CONTEXT_PATH || '/optimize';
19
+ const MANAGEMENT_IDENTITY_CONTEXT_PATH = process.env.MANAGEMENT_IDENTITY_CONTEXT_PATH || '/identity';
16
20
  const IDENTITY_FIRSTUSER_USERNAME = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_USERNAME || 'demo';
17
21
  const IDENTITY_FIRSTUSER_PASSWORD = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD || 'demo';
18
22
  class NavigationPage {
@@ -142,7 +142,7 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
142
142
  // be re-opened once the blip clears. Progressive backoff gives the
143
143
  // backend time to recover between attempts.
144
144
  await (0, sleep_1.sleep)(2000 * (attempt + 1));
145
- const orchestrationContextPath = process.env.ORCHESTRATION_CONTEXT_PATH ?? '/orchestration';
145
+ const orchestrationContextPath = process.env.ORCHESTRATION_CONTEXT_PATH || '/orchestration';
146
146
  const normalizedOrchestrationPath = orchestrationContextPath.startsWith('/')
147
147
  ? orchestrationContextPath
148
148
  : `/${orchestrationContextPath}`;
@@ -5,14 +5,18 @@ const test_1 = require("@playwright/test");
5
5
  const LoginPage_1 = require("./LoginPage");
6
6
  const sleep_1 = require("../../utils/sleep");
7
7
  const constants_1 = require("../../utils/constants");
8
- const ORCHESTRATION_CONTEXT_PATH = process.env.ORCHESTRATION_CONTEXT_PATH ?? '/orchestration';
8
+ // Context paths use `||`, not `??`: an env var set to the empty string must fall back to the
9
+ // default. CI wiring that always exports a variable (set to "" when the leg has no override)
10
+ // otherwise defeats the default — `page.goto('')` resolves to Playwright's baseURL, so the app
11
+ // under test is never reached and the failure surfaces as an unrelated navigation timeout.
12
+ const ORCHESTRATION_CONTEXT_PATH = process.env.ORCHESTRATION_CONTEXT_PATH || '/orchestration';
9
13
  const NORMALIZED_ORCHESTRATION_CONTEXT_PATH = ORCHESTRATION_CONTEXT_PATH.startsWith('/')
10
14
  ? ORCHESTRATION_CONTEXT_PATH
11
15
  : `/${ORCHESTRATION_CONTEXT_PATH}`;
12
- const MODELER_CONTEXT_PATH = process.env.MODELER_CONTEXT_PATH ?? '/modeler';
13
- const OPTIMIZE_CONTEXT_PATH = process.env.OPTIMIZE_CONTEXT_PATH ?? '/optimize';
14
- const CONSOLE_CONTEXT_PATH = process.env.CONSOLE_CONTEXT_PATH ?? '/console';
15
- const MANAGEMENT_IDENTITY_CONTEXT_PATH = process.env.MANAGEMENT_IDENTITY_CONTEXT_PATH ?? '/identity';
16
+ const MODELER_CONTEXT_PATH = process.env.MODELER_CONTEXT_PATH || '/modeler';
17
+ const OPTIMIZE_CONTEXT_PATH = process.env.OPTIMIZE_CONTEXT_PATH || '/optimize';
18
+ const CONSOLE_CONTEXT_PATH = process.env.CONSOLE_CONTEXT_PATH || '/console';
19
+ const MANAGEMENT_IDENTITY_CONTEXT_PATH = process.env.MANAGEMENT_IDENTITY_CONTEXT_PATH || '/identity';
16
20
  const IDENTITY_FIRSTUSER_USERNAME = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_USERNAME || 'demo';
17
21
  const IDENTITY_FIRSTUSER_PASSWORD = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD || 'demo';
18
22
  // Navigation errors raised by Chromium's network stack rather than by the app:
@@ -53,7 +53,12 @@ SM_8_10_1.test.describe('Identity User Flow Tests', () => {
53
53
  });
54
54
  }
55
55
  await SM_8_10_1.test.step('Remove Access from Hub', async () => {
56
- await managementIdentityPage.clickDeleteAccessButton('Grants full access to Hub');
56
+ // Either wording, for the same Web Modeler → Hub rename straddle the
57
+ // `modelerCheckbox` locator documents.
58
+ await managementIdentityPage.clickDeleteAccessButton([
59
+ 'Grants full access to Hub',
60
+ 'Grants full access to Web Modeler',
61
+ ]);
57
62
  await managementIdentityPage.clickConfirmDeleteButton();
58
63
  });
59
64
  await SM_8_10_1.test.step('Reset Session', async () => {
@@ -117,8 +117,28 @@ SM_8_9_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
117
117
  await SM_8_9_1.test.step('Make Authorization Request', async () => {
118
118
  const baseURL = process.env.PLAYWRIGHT_BASE_URL ||
119
119
  `http://gke-${process.env.BASE_URL}.ci.distro.ultrawombat.com`;
120
- const response = await request.post(`${baseURL}/connectors/inbound/test-webhook-id${randomString}`);
121
- await (0, test_1.expect)(response.status()).toBe(200);
120
+ const url = `${baseURL}/connectors/inbound/test-webhook-id${randomString}`;
121
+ // Deploying the process does not mean the gateway is already serving the
122
+ // inbound endpoint, so the first POST can land on a not-yet-registered
123
+ // webhook and come back 5xx with nothing actually wrong. The sibling
124
+ // Intermediate Event step below polls for the same reason.
125
+ //
126
+ // Retry ONLY on 5xx. A 5xx means the request was not processed, so
127
+ // re-posting cannot double-trigger the process; a 2xx or 4xx is a real
128
+ // answer about a live endpoint and must be asserted as-is rather than
129
+ // retried until it changes.
130
+ let response = await request.post(url);
131
+ const deadline = Date.now() + 180000;
132
+ let attempt = 0;
133
+ while (response.status() >= 500 && Date.now() < deadline) {
134
+ attempt++;
135
+ console.error(`Webhook readiness attempt ${attempt}: ${url} -> ${response.status()}`);
136
+ await (0, sleep_1.sleep)(10000);
137
+ response = await request.post(url);
138
+ }
139
+ // Not awaited: expect() on a plain value is synchronous, so an `await`
140
+ // here would read as a retrying assertion while doing nothing.
141
+ (0, test_1.expect)(response.status()).toBe(200);
122
142
  });
123
143
  await SM_8_9_1.test.step('Assert Diagram Has Successfully Completed in Operate', async () => {
124
144
  await navigationPage.goToOperate();
@@ -190,7 +190,7 @@ function buildZeebeApiUrl(resourcePath, environmentOverride, clusterType) {
190
190
  const normalizedPath = resourcePath.startsWith('/')
191
191
  ? resourcePath
192
192
  : `/${resourcePath}`;
193
- const contextPath = process.env.ORCHESTRATION_CONTEXT_PATH ?? '/orchestration';
193
+ const contextPath = process.env.ORCHESTRATION_CONTEXT_PATH || '/orchestration';
194
194
  const pathWithPrefix = environment === 'sm' ? `${contextPath}${normalizedPath}` : normalizedPath;
195
195
  return `${baseUrl}${pathWithPrefix}`;
196
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1020",
3
+ "version": "0.0.1022",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",