@camunda/e2e-test-suite 0.0.1266 → 0.0.1268

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 AppsPage {
16
16
  readonly camundaComponentsButton: Locator;
17
17
  readonly camundaAppsButton: Locator;
18
18
  readonly hubSidebar: Locator;
19
+ private readonly hubAppUrls;
19
20
  constructor(page: Page);
20
21
  clickCluster(component: Locator, name: string): Promise<void>;
21
22
  private expandHubClusterGroup;
@@ -26,6 +26,8 @@ class AppsPage {
26
26
  camundaComponentsButton;
27
27
  camundaAppsButton;
28
28
  hubSidebar;
29
+ // App URLs already resolved from the Hub sidebar, keyed by cluster + app.
30
+ hubAppUrls = new Map();
29
31
  constructor(page) {
30
32
  this.page = page;
31
33
  this.modelerLink = page
@@ -181,7 +183,25 @@ class AppsPage {
181
183
  }
182
184
  }
183
185
  async clickHubClusterApp(clusterName, appName) {
184
- await this.expandHubClusterGroup(clusterName);
186
+ const cacheKey = `${clusterName}::${appName}`;
187
+ const knownUrl = this.hubAppUrls.get(cacheKey);
188
+ try {
189
+ await this.expandHubClusterGroup(clusterName);
190
+ }
191
+ catch (error) {
192
+ // Hub's sidebar lists Environments filtered server-side: a member who
193
+ // is neither a cluster manager nor in a workspace the environment is
194
+ // assigned to gets no sidebar entry for it at all (camunda-hub
195
+ // use-organization-environment-sidebar-model.ts). A test that re-logs
196
+ // in as such a user after resetSession() then has no link to follow,
197
+ // even though the app itself is still theirs to open -- so reuse the
198
+ // URL this page object already resolved earlier in the same test.
199
+ if (!knownUrl) {
200
+ throw error;
201
+ }
202
+ await this.page.goto(knownUrl, { waitUntil: 'domcontentloaded' });
203
+ return;
204
+ }
185
205
  // Not exact: camunda-hub's own nav-shell-ds.spec.saas.tsx asserts this same
186
206
  // link with a substring/regex match (`{name: /Operate/}`), not an exact
187
207
  // string -- and CI traces confirm why: even after the toggle click
@@ -198,6 +218,9 @@ class AppsPage {
198
218
  // Follow the href in the current tab instead, matching the same-tab
199
219
  // navigation the Console app switcher gave these callers.
200
220
  const href = await appLink.getAttribute('href');
221
+ if (href) {
222
+ this.hubAppUrls.set(cacheKey, new URL(href, this.page.url()).toString());
223
+ }
201
224
  if (href && (await appLink.getAttribute('target')) === '_blank') {
202
225
  await this.page.goto(new URL(href, this.page.url()).toString(), {
203
226
  waitUntil: 'domcontentloaded',
@@ -65,25 +65,34 @@ class ConnectorMarketplacePage {
65
65
  else {
66
66
  await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
67
67
  }
68
+ // isVisible() never waits (its timeout is ignored), and the Browse all
69
+ // review step fetches the connector's details before it renders "Add to
70
+ // project" -- the nightly probed ~60ms after the click and fell through
71
+ // every branch. Wait for one of the outcomes to render first, and let a
72
+ // genuine timeout here throw rather than silently falling through to the
73
+ // immediate isVisible() probes below, which would just repeat the same
74
+ // race this wait exists to close.
75
+ await this.addToProjectButton
76
+ .or(this.replaceResourceButton)
77
+ .or(this.cancelButton)
78
+ .first()
79
+ .waitFor({ state: 'visible', timeout: 30000 });
68
80
  let imported = false;
69
81
  // A fresh download shows "Add to project"; an already-present resource
70
82
  // shows "Replace resource". Pick whichever the ImportModal rendered —
71
83
  // the old code only handled "Replace resource", so a first-time download
72
84
  // (the SaaS nightly always runs in a fresh project) was never confirmed
73
- // and the connector never reached the diagram.
74
- if (await this.addToProjectButton
75
- .isVisible({ timeout: 20000 })
76
- .catch(() => false)) {
85
+ // and the connector never reached the diagram. The wait above already
86
+ // confirmed one of the three is visible, so these are immediate checks.
87
+ if (await this.addToProjectButton.isVisible()) {
77
88
  await this.addToProjectButton.click({ timeout: 10000 });
78
89
  imported = true;
79
90
  }
80
- else if (await this.replaceResourceButton
81
- .isVisible({ timeout: 5000 })
82
- .catch(() => false)) {
91
+ else if (await this.replaceResourceButton.isVisible()) {
83
92
  await this.replaceResourceButton.click({ timeout: 10000 });
84
93
  imported = true;
85
94
  }
86
- else if (await this.cancelButton.isVisible({ timeout: 3000 }).catch(() => false)) {
95
+ else if (await this.cancelButton.isVisible()) {
87
96
  await this.cancelButton.click({ timeout: 10000 });
88
97
  }
89
98
  // After a successful import, wait for the snackbar (confirms the connector
@@ -52,7 +52,10 @@ class ConsoleOrganizationPage {
52
52
  constructor(page) {
53
53
  this.page = page;
54
54
  this.usersTab = page.getByRole('tab', { name: 'Users' });
55
- this.optionsButton = page.getByRole('button', { name: 'Options' });
55
+ // The design-system Users table labels its row kebab "Row actions".
56
+ this.optionsButton = page
57
+ .getByRole('button', { name: 'Options' })
58
+ .or(page.getByRole('button', { name: 'Row actions' }));
56
59
  this.editUserMenuItem = page.getByRole('menuitem', { name: 'Edit User' });
57
60
  this.operationsEngineerCheckbox = page
58
61
  .getByRole('group')
@@ -239,6 +242,7 @@ class ConsoleOrganizationPage {
239
242
  try {
240
243
  await dataRow
241
244
  .getByRole('button', { name: 'Options' })
245
+ .or(dataRow.getByRole('button', { name: 'Row actions' }))
242
246
  .click({ timeout: 30000 });
243
247
  }
244
248
  catch (_e) {
@@ -3,6 +3,9 @@ declare class FormJsPage {
3
3
  private page;
4
4
  private processApplicationPage;
5
5
  readonly aiFormGeneratorButton: Locator;
6
+ readonly copilotCtaButton: Locator;
7
+ readonly copilotChatInput: Locator;
8
+ readonly copilotSendButton: Locator;
6
9
  readonly formRequestInput: Locator;
7
10
  readonly generateFormButton: Locator;
8
11
  readonly formEditor: Locator;
@@ -20,6 +23,7 @@ declare class FormJsPage {
20
23
  readonly deploySuccessNotification: Locator;
21
24
  constructor(page: Page);
22
25
  generateAIForm(request?: string): Promise<void>;
26
+ private generateFormWithCopilot;
23
27
  clickAIFormGeneratorButton(): Promise<void>;
24
28
  clickFormRequestInput(): Promise<void>;
25
29
  fillFormRequestInput(formRequest: string): Promise<void>;
@@ -9,6 +9,9 @@ class FormJsPage {
9
9
  page;
10
10
  processApplicationPage;
11
11
  aiFormGeneratorButton;
12
+ copilotCtaButton;
13
+ copilotChatInput;
14
+ copilotSendButton;
12
15
  formRequestInput;
13
16
  generateFormButton;
14
17
  formEditor;
@@ -30,6 +33,16 @@ class FormJsPage {
30
33
  this.aiFormGeneratorButton = page.getByRole('button', {
31
34
  name: 'AI Form Generator',
32
35
  });
36
+ // camunda-hub #21398 retired the form page's AI Form Generator in favour
37
+ // of the Camunda Copilot sidecar: with One Copilot on, the empty form
38
+ // offers "Try Camunda Copilot" instead (FormDs.tsx / CopilotCTA.tsx), and
39
+ // the sidecar's chat input and send button come from
40
+ // camunda-copilot-client's ChatInput.
41
+ this.copilotCtaButton = page.getByRole('button', {
42
+ name: 'Try Camunda Copilot',
43
+ });
44
+ this.copilotChatInput = page.getByPlaceholder('Type a message...');
45
+ this.copilotSendButton = page.getByRole('button', { name: 'Send message' });
33
46
  this.formRequestInput = page.getByPlaceholder('A form for an american standard loan request');
34
47
  this.generateFormButton = page.getByRole('button', { name: 'Generate form' });
35
48
  this.formEditor = page.getByLabel('Form Definition');
@@ -64,9 +77,13 @@ class FormJsPage {
64
77
  .filter({ hasText: 'Form deployed!' });
65
78
  }
66
79
  async generateAIForm(request = 'Create a form with the following fields: 1. A text field with the label "Full Name" 2. A number with the label "Count" 3. A date input with the label "Date of birth"4. A Checkbox with the label "Agree"') {
67
- await (0, test_1.expect)(this.aiFormGeneratorButton).toBeVisible({
80
+ await (0, test_1.expect)(this.aiFormGeneratorButton.or(this.copilotCtaButton).first()).toBeVisible({
68
81
  timeout: 90000,
69
82
  });
83
+ if (!(await this.aiFormGeneratorButton.isVisible())) {
84
+ await this.generateFormWithCopilot(request);
85
+ return;
86
+ }
70
87
  await this.clickAIFormGeneratorButton();
71
88
  await this.clickFormRequestInput();
72
89
  await this.fillFormRequestInput(request);
@@ -75,6 +92,19 @@ class FormJsPage {
75
92
  await (0, test_1.expect)(this.page.getByText('Successfully generated smart form.')).toBeVisible({ timeout: 300000 });
76
93
  await (0, sleep_1.sleep)(60000);
77
94
  }
95
+ async generateFormWithCopilot(request) {
96
+ await this.copilotCtaButton.click({ timeout: 30000 });
97
+ await (0, test_1.expect)(this.copilotChatInput).toBeVisible({ timeout: 60000 });
98
+ await this.copilotChatInput.fill(request);
99
+ await this.copilotSendButton.click({ timeout: 30000 });
100
+ // Copilot edits the open form in place; the generated form is ready once
101
+ // every requested field shows up on the canvas.
102
+ for (const label of ['Full Name', 'Count', 'Date of birth', 'Agree']) {
103
+ await (0, test_1.expect)(this.formEditor.getByText(label).first()).toBeVisible({
104
+ timeout: 300000,
105
+ });
106
+ }
107
+ }
78
108
  async clickAIFormGeneratorButton() {
79
109
  await this.aiFormGeneratorButton.click();
80
110
  }
@@ -155,7 +155,13 @@ class ModelerCreatePage {
155
155
  this.viewProcessInstanceLink = page.getByRole('button', {
156
156
  name: 'View process instance',
157
157
  });
158
- this.viewProcessLink = page.getByText('view process in Operate');
158
+ // camunda-hub 6813a7e (2026-09-25) dropped the "View process in Operate"
159
+ // action from the deploy toast; the toast itself ("Diagram deployed!" or
160
+ // "<Project> deployed!") is still the deploy-succeeded signal.
161
+ this.viewProcessLink = page
162
+ .getByText('view process in Operate')
163
+ .or(page.getByText(/^(Diagram|Project|Process application) deployed!$/))
164
+ .first();
159
165
  this.autosaveIndicator = page.locator('[data-test="autosave"]');
160
166
  this.nameInput = page.getByLabel('Name', { exact: true });
161
167
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
@@ -181,7 +187,12 @@ class ModelerCreatePage {
181
187
  });
182
188
  this.variableInput = page.locator('textarea#variables-json');
183
189
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
184
- this.embedButton = page.locator('[data-test="confirm-move"]');
190
+ // Workspaces-enabled Web Modeler renders the "Link a form" picker via
191
+ // LinkTargetModal, whose primary action carries data-test="link-target-confirm"
192
+ // instead of the legacy move dialog's "confirm-move".
193
+ this.embedButton = page
194
+ .locator('[data-test="confirm-move"]')
195
+ .or(page.locator('[data-test="link-target-confirm"]'));
185
196
  this.deployMainButton = page.locator('[data-test="deploy-button"]');
186
197
  this.deploySubButton = page.locator('[data-test="deploy-action"]');
187
198
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
@@ -1416,9 +1427,12 @@ class ModelerCreatePage {
1416
1427
  // only renders visibly while that item is expanded. It expands by itself
1417
1428
  // once the cluster step completes, but a manual heading click is the
1418
1429
  // recovery path if it did not.
1419
- const deployStepHeading = this.page.getByRole('button', {
1420
- name: 'Deploy process',
1421
- });
1430
+ // With Hub's Environments feature on, the panel is "Set up test run" and
1431
+ // this step is headed "Choose resources to deploy".
1432
+ const deployStepHeading = this.page
1433
+ .getByRole('button', { name: 'Deploy process' })
1434
+ .or(this.page.getByRole('button', { name: 'Choose resources to deploy' }))
1435
+ .first();
1422
1436
  // Play settles into exactly ONE of three states, and which one is not
1423
1437
  // knowable up front, so race them instead of timing out on each in turn:
1424
1438
  // the setup overlay, the legacy Continue button, or the configure-test
@@ -1449,6 +1463,7 @@ class ModelerCreatePage {
1449
1463
  .locator('[data-test="test-studio-configuration-overlay"]')
1450
1464
  .or(this.page.locator('[data-test="play-configuration-overlay"]'))
1451
1465
  .or(this.page.getByText('Setup environment'))
1466
+ .or(this.page.getByText('Set up test run', { exact: true }))
1452
1467
  .first();
1453
1468
  const configureTestPanel = this.page.getByTestId('configure-test-panel');
1454
1469
  await (0, test_1.expect)(configurationOverlay
@@ -1469,8 +1484,34 @@ class ModelerCreatePage {
1469
1484
  const connectClusterButton = this.page.getByRole('button', {
1470
1485
  name: 'Connect cluster',
1471
1486
  });
1487
+ // With Hub's Environments feature on, step 1 ("Choose where to run")
1488
+ // offers "Select environment" instead, which opens the "Choose where to
1489
+ // run" dialog listing the workspace's environment rows (camunda-hub
1490
+ // test-studio ClusterStepContent / TestModeDeployEndpointConfigModal).
1491
+ const selectEnvironmentButton = this.page.getByRole('button', {
1492
+ name: 'Select environment',
1493
+ });
1494
+ // The step shows "Verifying cluster availability" before either button
1495
+ // renders, so give it a moment instead of a non-waiting isVisible().
1496
+ await connectClusterButton
1497
+ .or(selectEnvironmentButton)
1498
+ .first()
1499
+ .waitFor({ state: 'visible', timeout: 15000 })
1500
+ .catch(() => { });
1501
+ if (await selectEnvironmentButton.isVisible().catch(() => false)) {
1502
+ await selectEnvironmentButton.click({ timeout });
1503
+ const chooseWhereToRunDialog = this.page.getByRole('dialog', {
1504
+ name: 'Choose where to run',
1505
+ });
1506
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeVisible({ timeout });
1507
+ await (0, modelerCluster_1.selectDeployEnvironment)(chooseWhereToRunDialog, clusterName);
1508
+ await chooseWhereToRunDialog
1509
+ .getByRole('button', { name: 'Save', exact: true })
1510
+ .click({ timeout });
1511
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeHidden({ timeout });
1512
+ }
1472
1513
  const needsConnect = await connectClusterButton
1473
- .isVisible({ timeout: 3000 })
1514
+ .isVisible()
1474
1515
  .catch(() => false);
1475
1516
  if (needsConnect) {
1476
1517
  await connectClusterButton.click({ timeout });
@@ -1928,7 +1969,11 @@ class ModelerCreatePage {
1928
1969
  await (0, test_1.expect)(this.formLinkedSuccessMessage).toBeVisible({ timeout: 15000 });
1929
1970
  }
1930
1971
  async assertDiagramNameSet(diagramName) {
1931
- await (0, test_1.expect)(this.diagramNameBreadcrumb(diagramName)).toBeVisible({
1972
+ // Same legacy-crumb / design-system "<name> actions" kebab pair that
1973
+ // enterDiagramName accepts as proof the rename took.
1974
+ await (0, test_1.expect)(this.diagramNameBreadcrumb(diagramName)
1975
+ .or(this.page.getByRole('button', { name: `${diagramName} actions` }))
1976
+ .first()).toBeVisible({
1932
1977
  timeout: 60000,
1933
1978
  });
1934
1979
  }
@@ -226,6 +226,16 @@ class ProcessApplicationPage {
226
226
  if (!container || container.developmentCluster === clusterName) {
227
227
  return false;
228
228
  }
229
+ // A reload just before this can leave the page mid-way through Hub's
230
+ // auth redirect. Returning to /login-callback?code=... replays a spent
231
+ // OAuth code and logs the session out (the nightly then sat on the Auth0
232
+ // login form), so wait until the redirect has landed before capturing it.
233
+ // Hub only exchanges the code once its SPA has booted, measured at ~45s
234
+ // on CI runners elsewhere in this suite (assertTestUsesCorrectOrganization
235
+ // in UtilitiesPage.ts) -- match that 120s timeout, and let a genuine
236
+ // failure here throw rather than silently falling through with a spent
237
+ // callback URL still captured as returnTo.
238
+ await this.page.waitForURL((url) => !/^\/login(-callback)?\/?$/.test(url.pathname), { timeout: 120000 });
229
239
  const returnTo = this.page.url();
230
240
  const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
231
241
  // Retried as a whole, re-entering the page on every attempt. This binding
@@ -361,6 +361,18 @@ async function runProcess(clusterName, page, modelerHomePage, modelerCreatePage,
361
361
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
362
362
  timeout: 60000,
363
363
  });
364
+ // camunda-hub 6813a7e (2026-09-25) made the "Instance started!" toast
365
+ // persistent (shouldPersist) whenever it carries the Operate action, so it
366
+ // no longer times out on its own -- close it via its "Dismiss" button.
367
+ const instanceStartedToast = page
368
+ .locator('[data-sonner-toast]')
369
+ .filter({ has: modelerCreatePage.viewProcessInstanceLink });
370
+ const dismissButton = instanceStartedToast.getByRole('button', {
371
+ name: 'Dismiss',
372
+ });
373
+ if (await dismissButton.isVisible().catch(() => false)) {
374
+ await dismissButton.click({ timeout: 10000 });
375
+ }
364
376
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeHidden({
365
377
  timeout: 60000,
366
378
  });
@@ -100,7 +100,8 @@ _8_7_1.test.describe('Connectors User Flow Tests', () => {
100
100
  });
101
101
  });
102
102
  });
103
- (0, _8_7_1.test)('Message Start Event Webhook Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, request, operateHomePage, operateProcessInstancePage, operateProcessesPage, }) => {
103
+ // Skipped due to bug #29573: https://github.com/camunda/camunda-hub/issues/29573
104
+ _8_7_1.test.skip('Message Start Event Webhook Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, request, operateHomePage, operateProcessInstancePage, operateProcessesPage, }) => {
104
105
  _8_7_1.test.slow();
105
106
  const processName = 'Start_Event_Webhook_Connector_No_Auth_Process' +
106
107
  (await (0, _setup_1.generateRandomStringAsync)(3));
@@ -338,7 +339,8 @@ _8_7_1.test.describe('Connectors User Flow Tests', () => {
338
339
  });
339
340
  });
340
341
  });
341
- (0, _8_7_1.test)('Intermediate Event Webhook Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, request, operateHomePage, operateProcessInstancePage, operateProcessesPage, connectorSettingsPage, }) => {
342
+ // Skipped due to bug #29573: https://github.com/camunda/camunda-hub/issues/29573
343
+ _8_7_1.test.skip('Intermediate Event Webhook Connector No Auth User Flow', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, request, operateHomePage, operateProcessInstancePage, operateProcessesPage, connectorSettingsPage, }) => {
342
344
  const processName = 'Intermediate_Event_Webhook_Connector_No_Auth_Process' +
343
345
  (await (0, _setup_1.generateRandomStringAsync)(3));
344
346
  await _8_7_1.test.step('Navigate to Web Modeler', async () => {
@@ -33,7 +33,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
33
33
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
34
34
  }
35
35
  });
36
- (0, _8_7_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
36
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
37
+ // Also tracked in https://github.com/camunda/product-hub/issues/3655
38
+ _8_7_1.test.skip('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
37
39
  _8_7_1.test.slow();
38
40
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
39
41
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -268,7 +270,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
268
270
  });
269
271
  });
270
272
  });
271
- (0, _8_7_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
273
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
274
+ // Also tracked in https://github.com/camunda/product-hub/issues/3655
275
+ _8_7_1.test.skip('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
272
276
  _8_7_1.test.slow();
273
277
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
274
278
  const processName = 'AI-generated-Start-form' + randomString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1266",
3
+ "version": "0.0.1268",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",