@camunda/e2e-test-suite 0.0.1172 → 0.0.1174

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.
@@ -13,6 +13,8 @@ declare class ClusterSecretsPage {
13
13
  readonly fromFileButton: Locator;
14
14
  readonly bulkImportTextArea: Locator;
15
15
  readonly dialogImportButton: Locator;
16
+ readonly bulkImportCancelButton: Locator;
17
+ readonly bulkImportErrorBanner: Locator;
16
18
  readonly secretsSearchBox: Locator;
17
19
  readonly clusterBanner: Locator;
18
20
  readonly clusterLink: (name: string) => Locator;
@@ -18,6 +18,8 @@ class ClusterSecretsPage {
18
18
  fromFileButton;
19
19
  bulkImportTextArea;
20
20
  dialogImportButton;
21
+ bulkImportCancelButton;
22
+ bulkImportErrorBanner;
21
23
  secretsSearchBox;
22
24
  clusterBanner;
23
25
  clusterLink;
@@ -50,6 +52,11 @@ class ClusterSecretsPage {
50
52
  this.dialogImportButton = this.page
51
53
  .getByRole('dialog')
52
54
  .getByRole('button', { name: 'Import', exact: true });
55
+ this.bulkImportCancelButton = this.dialog.getByRole('button', {
56
+ name: 'Cancel',
57
+ exact: true,
58
+ });
59
+ this.bulkImportErrorBanner = this.dialog.getByText(/Imported \d+ secrets?, \d+ failed/);
53
60
  this.secretsSearchBox = page.getByRole('searchbox', { name: 'Filter table' });
54
61
  this.clusterBanner = this.page
55
62
  .getByRole('banner')
@@ -210,9 +217,28 @@ class ClusterSecretsPage {
210
217
  await this.bulkImportTextArea.click();
211
218
  await this.bulkImportTextArea.fill(mapped);
212
219
  await this.dialogImportButton.click();
213
- await (0, test_1.expect)(this.dialog).not.toBeVisible({
214
- timeout: 60000,
215
- });
220
+ // This suite intentionally reuses a fixed cluster across nightly runs
221
+ // (see ensureClusterViaApi in test-setup.spec.ts), so some or all of
222
+ // these secrets can already exist from a previous run. The Console then
223
+ // leaves the dialog open with an "Imported X secrets, Y failed" banner
224
+ // instead of closing it, which used to hang here for 60s and fail setup
225
+ // outright. Tolerate that banner and close the dialog ourselves --
226
+ // assertSecretsOrCreate() (the very next call in createSetOfSecrets)
227
+ // independently verifies every requested secret is present and creates
228
+ // any that are genuinely missing, so this doesn't mask a real failure,
229
+ // it just defers the correctness check to the step that already owns it.
230
+ try {
231
+ await (0, test_1.expect)(this.dialog).not.toBeVisible({ timeout: 60000 });
232
+ }
233
+ catch (error) {
234
+ if (await this.bulkImportErrorBanner.isVisible().catch(() => false)) {
235
+ await this.bulkImportCancelButton.click();
236
+ await (0, test_1.expect)(this.dialog).not.toBeVisible({ timeout: 10000 });
237
+ }
238
+ else {
239
+ throw error;
240
+ }
241
+ }
216
242
  await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
217
243
  timeout: 60000,
218
244
  });
@@ -61,7 +61,14 @@ class FormJsPage {
61
61
  .locator('.cds--toast-notification--success')
62
62
  .filter({ hasText: 'Form deployed!' });
63
63
  }
64
- 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"') {
64
+ async generateAIForm(
65
+ // "date only, no time": asked for a plain date the generator sometimes
66
+ // emits a datetime component in its "date and time" subtype, which adds a
67
+ // second required time subfield that then blocks task completion. The
68
+ // task form fills it anyway (TaskDetailsPage.fillTimeSubfield), but asking
69
+ // for the date-only variant keeps the generated form matching what the
70
+ // tests model.
71
+ 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 (date only, no time) with the label "Date of birth" 4. A Checkbox with the label "Agree"') {
65
72
  // The "Successfully generated smart form." toast is delivered over a Pusher
66
73
  // websocket. A transient websocket/network drop during generation means the
67
74
  // toast never renders even though generation finished, leaving the old
@@ -421,6 +421,20 @@ class ModelerCreatePage {
421
421
  if (processId.length > 0) {
422
422
  await this.clickStartEventElement();
423
423
  await this.clickGeneralPropertiesPanel();
424
+ // Renaming the file does not reliably rename the process: hub only syncs
425
+ // the new file name into the BPMN process name from inside
426
+ // `renameDiagram`, which returns early when the submitted name already
427
+ // equals the stored one (camunda-hub
428
+ // `stores/current-diagram-store.tsx` -> `checkAndSyncProcessName`). Any
429
+ // rename attempt that commits server-side but is not observed in time --
430
+ // the case `enterDiagramName`'s retry loop exists for -- therefore
431
+ // leaves the process named "New BPMN diagram" while the breadcrumb shows
432
+ // the new name, which is exactly what the nightly screenshot showed.
433
+ // Tasklist and Operate list the process *name*, so the task can then
434
+ // never be found by `processName`. Set it explicitly, the same way
435
+ // `UtilitiesPage` already does for uploaded diagrams.
436
+ await this.clickNameInput();
437
+ await this.fillNamedInput(processName);
424
438
  await this.clickIdInput();
425
439
  await this.fillIdInput(processId);
426
440
  await this.clickStartEventElement();
@@ -24,6 +24,7 @@ declare class TaskDetailsPage {
24
24
  readonly decrementButton: Locator;
25
25
  readonly dateInput: Locator;
26
26
  readonly timeInput: Locator;
27
+ readonly timeSubfield: Locator;
27
28
  readonly checkbox: Locator;
28
29
  readonly selectDropdown: Locator;
29
30
  readonly tagList: Locator;
@@ -55,6 +56,7 @@ declare class TaskDetailsPage {
55
56
  clickIncrementButton(): Promise<void>;
56
57
  clickDecrementButton(): Promise<void>;
57
58
  fillDate(date: string): Promise<void>;
59
+ private fillTimeSubfield;
58
60
  enterTime(time: string): Promise<void>;
59
61
  checkCheckbox(): Promise<void>;
60
62
  selectDropdownValue(value: string): Promise<void>;
@@ -44,6 +44,7 @@ class TaskDetailsPage {
44
44
  decrementButton;
45
45
  dateInput;
46
46
  timeInput;
47
+ timeSubfield;
47
48
  checkbox;
48
49
  selectDropdown;
49
50
  tagList;
@@ -91,6 +92,7 @@ class TaskDetailsPage {
91
92
  this.decrementButton = page.getByRole('button', { name: 'Decrement' });
92
93
  this.dateInput = page.getByLabel('Date of Birth');
93
94
  this.timeInput = page.getByPlaceholder('hh:mm ?m');
95
+ this.timeSubfield = this.form.locator('.fjs-timepicker input').first();
94
96
  this.checkbox = this.form.getByLabel('Agree');
95
97
  this.selectDropdown = this.form.getByText('Select').last();
96
98
  this.tagList = page.getByPlaceholder('Search');
@@ -174,6 +176,50 @@ class TaskDetailsPage {
174
176
  await this.dateInput.click();
175
177
  await this.dateInput.fill(date);
176
178
  await this.dateInput.press('Enter');
179
+ await this.fillTimeSubfield();
180
+ }
181
+ // The AI form generator is non-deterministic about how it renders the
182
+ // requested "Date of birth" input: it sometimes emits a form-js datetime
183
+ // component in its "date and time" subtype, which renders a second - and
184
+ // equally required - time subfield next to the date input. The nightly
185
+ // trace shows form validation rejecting the completion on exactly that
186
+ // empty subfield ("Field is required." / "Please review 1 field"), so the
187
+ // task was never completed and no completion confirmation could appear.
188
+ // Fill the time subfield whenever the rendered form has one; the fixed,
189
+ // date-only forms every other spec deploys have no timepicker and skip this.
190
+ async fillTimeSubfield() {
191
+ const hasTimeSubfield = await this.timeSubfield
192
+ .waitFor({ state: 'visible', timeout: 5000 })
193
+ .then(() => true)
194
+ .catch(() => false);
195
+ if (!hasTimeSubfield) {
196
+ return;
197
+ }
198
+ // Whether the component is configured for 12h or 24h notation is up to the
199
+ // generator, so prefer picking one of the times the timepicker itself
200
+ // offers over typing a notation that may not parse.
201
+ await this.timeSubfield.click({ timeout: 30000 });
202
+ const offeredTime = this.page.locator('.fjs-dropdownlist-item').first();
203
+ const timeIsOffered = await offeredTime
204
+ .waitFor({ state: 'visible', timeout: 10000 })
205
+ .then(() => true)
206
+ .catch(() => false);
207
+ if (timeIsOffered) {
208
+ await offeredTime.click({ timeout: 30000 });
209
+ }
210
+ if ((await this.timeSubfield.inputValue()) !== '') {
211
+ return;
212
+ }
213
+ // No dropdown, or it committed nothing: type instead. form-js discards a
214
+ // value it cannot parse on blur, so the input keeping the value is what
215
+ // tells us the notation was accepted.
216
+ for (const time of ['09:00 AM', '09:00']) {
217
+ await this.timeSubfield.fill(time);
218
+ await this.timeSubfield.press('Tab');
219
+ if ((await this.timeSubfield.inputValue()) !== '') {
220
+ return;
221
+ }
222
+ }
177
223
  }
178
224
  async enterTime(time) {
179
225
  await this.timeInput.click();
@@ -4,6 +4,7 @@ declare class OCIdentityRolesPage {
4
4
  readonly selectResourceTypeTab: (name: string) => Locator;
5
5
  readonly assignMappingRuleButton: Locator;
6
6
  readonly mappingSearchBox: Locator;
7
+ readonly mappingSearchTrigger: Locator;
7
8
  readonly assignMappingModal: Locator;
8
9
  readonly assignMappingRuleOption: (name: string) => Locator;
9
10
  readonly assignMappingModalAssignRuleButton: Locator;
@@ -12,6 +12,7 @@ class OCIdentityRolesPage {
12
12
  selectResourceTypeTab;
13
13
  assignMappingRuleButton;
14
14
  mappingSearchBox;
15
+ mappingSearchTrigger;
15
16
  assignMappingModal;
16
17
  assignMappingRuleOption;
17
18
  assignMappingModalAssignRuleButton;
@@ -49,20 +50,32 @@ class OCIdentityRolesPage {
49
50
  this.assignMappingModal = page.getByRole('dialog', {
50
51
  name: 'Assign mapping rule',
51
52
  });
52
- // The design-system modal's search field is a cmdk combobox
53
- // (role="combobox"), not the searchbox the pre design-system modal
53
+ // The design-system modal renders a MultiSelect whose trigger is a
54
+ // div[role="combobox"], not the searchbox the pre design-system modal
54
55
  // rendered; the Mapping rules list page keeps a page-level searchbox with
55
56
  // the same accessible name, so scope this to the dialog and accept either
56
57
  // role. The modal's field resolves its placeholder through the
57
58
  // `entitySelection` i18n namespace, which carries no `searchByMappingRuleId`
58
59
  // entry, so it is labelled with the raw key instead of the translated
59
60
  // string the list page renders - match either form.
60
- this.mappingSearchBox = this.assignMappingModal
61
+ this.mappingSearchTrigger = this.assignMappingModal
61
62
  .getByRole('combobox', { name: SEARCH_MAPPING_RULE_LABEL })
62
63
  .or(this.assignMappingModal.getByRole('searchbox', {
63
64
  name: SEARCH_MAPPING_RULE_LABEL,
64
65
  }))
65
66
  .first();
67
+ // Clicking that trigger opens a Radix popover that portals to <body> as a
68
+ // sibling of the dialog, and the editable field is the cmdk <input> inside
69
+ // it - the trigger div carries the same role and accessible name but
70
+ // swallows key events, so typing at it leaves the query empty. Match the
71
+ // popover's input, falling back to the dialog-scoped searchbox that
72
+ // pre-design-system builds render instead of the trigger/popover pair.
73
+ this.mappingSearchBox = page
74
+ .locator('[data-slot="multi-select-content"] input[cmdk-input]')
75
+ .or(this.assignMappingModal.getByRole('searchbox', {
76
+ name: SEARCH_MAPPING_RULE_LABEL,
77
+ }))
78
+ .first();
66
79
  this.createRoleButton = page.getByRole('button', {
67
80
  name: /create role/i,
68
81
  });
@@ -175,15 +188,23 @@ class OCIdentityRolesPage {
175
188
  await (0, test_1.expect)(this.assignMappingModal).toBeVisible({ timeout: 60000 });
176
189
  // The design-system search input is a cmdk combobox that only issues
177
190
  // its server-side query on real keystrokes, so type the id in rather
178
- // than setting the value in one shot. Budgeted like every other step
179
- // here rather than inheriting the 10s actionTimeout: the field mounts
180
- // with the dialog it was just asserted visible in.
181
- await this.mappingSearchBox.click({ timeout: 60000 });
191
+ // than setting the value in one shot. It lives in the popover the
192
+ // trigger opens, so wait for it before typing. Budgeted like every
193
+ // other step here rather than inheriting the 10s actionTimeout.
194
+ await this.mappingSearchTrigger.click({ timeout: 60000 });
195
+ await (0, test_1.expect)(this.mappingSearchBox).toBeVisible({ timeout: 60000 });
182
196
  await this.mappingSearchBox.pressSequentially(mappingId, {
183
197
  timeout: 60000,
184
198
  });
185
199
  await this.assignMappingRuleOption(mappingId).click({ timeout: 30000 });
186
- await this.assignMappingModalAssignRuleButton.click();
200
+ // The submit button stays disabled until the MultiSelect commits the
201
+ // pick, which lands a tick after the option click - budget that wait
202
+ // like the rest of the method instead of leaving it to the 10s
203
+ // actionTimeout the bare click would inherit.
204
+ await (0, test_1.expect)(this.assignMappingModalAssignRuleButton).toBeEnabled({
205
+ timeout: 30000,
206
+ });
207
+ await this.assignMappingModalAssignRuleButton.click({ timeout: 30000 });
187
208
  return;
188
209
  }
189
210
  catch (error) {
@@ -22,6 +22,7 @@ declare class OCTenantPage {
22
22
  readonly rolesTab: Locator;
23
23
  readonly assignRoleButton: Locator;
24
24
  readonly roleIdSearchBox: Locator;
25
+ readonly roleIdSearchTrigger: Locator;
25
26
  readonly assignRoleSubButton: Locator;
26
27
  readonly assignRoleModal: Locator;
27
28
  readonly cancelButton: Locator;
@@ -63,6 +64,7 @@ declare class OCTenantPage {
63
64
  clickRolesTab(): Promise<void>;
64
65
  clickAssignRoleButton(): Promise<void>;
65
66
  clickRoleIdSearchBox(): Promise<void>;
67
+ private ensureRoleIdSearchBoxOpen;
66
68
  fillRoleIdSearchBox(roleId: string, roleIdentifier: string): Promise<void>;
67
69
  clickAssignRoleSubButton(): Promise<void>;
68
70
  private isRoleAssigned;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OCTenantPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const expectTextWithPagination_1 = require("../../utils/assertionHelpers/expectTextWithPagination");
6
+ const ROLE_ID_SEARCH_LABEL = /^(Search by role ID|searchByRoleId)$/i;
6
7
  class OCTenantPage {
7
8
  page;
8
9
  createTenantMainButton;
@@ -26,6 +27,7 @@ class OCTenantPage {
26
27
  rolesTab;
27
28
  assignRoleButton;
28
29
  roleIdSearchBox;
30
+ roleIdSearchTrigger;
29
31
  assignRoleSubButton;
30
32
  assignRoleModal;
31
33
  cancelButton;
@@ -71,12 +73,25 @@ class OCTenantPage {
71
73
  this.rolesTab = page.getByRole('tab', { name: 'Roles' });
72
74
  this.assignRoleButton = page.getByRole('button', { name: 'Assign role' });
73
75
  // The design-system Admin shell renders the Assign role modal's search
74
- // field as a cmdk command input (role="combobox", type="text"), so the
75
- // Carbon searchbox role no longer resolves - match the new field by its
76
- // placeholder and keep the legacy role match as a fallback, the same
77
- // dual-shell pattern the table filter locator below uses.
76
+ // field as a MultiSelect whose trigger is a div[role="combobox"] carrying
77
+ // the label as a child span - it has no placeholder and is not editable,
78
+ // so scope the trigger to the dialog and keep the legacy searchbox match
79
+ // as a fallback, the same dual-shell pattern the table filter locator
80
+ // below uses.
81
+ this.roleIdSearchTrigger = page
82
+ .getByRole('dialog', { name: 'Assign role' })
83
+ .getByRole('combobox', { name: ROLE_ID_SEARCH_LABEL })
84
+ .or(page.getByRole('searchbox', { name: 'Search by Role ID' }))
85
+ .first();
86
+ // Clicking that trigger opens a Radix popover that portals to <body> as a
87
+ // sibling of the dialog, and the editable field is the cmdk <input> inside
88
+ // it. Nothing opened that popover before, so the placeholder-bearing input
89
+ // never existed and the click at it timed out. Match the popover's input,
90
+ // falling back to the placeholder/searchbox that pre-design-system builds
91
+ // render instead of the trigger/popover pair.
78
92
  this.roleIdSearchBox = page
79
- .getByPlaceholder('Search by role ID')
93
+ .locator('[data-slot="multi-select-content"] input[cmdk-input]')
94
+ .or(page.getByPlaceholder('Search by role ID'))
80
95
  .or(page.getByRole('searchbox', { name: 'Search by Role ID' }))
81
96
  .first();
82
97
  this.assignRoleModal = page.getByLabel('Assign role');
@@ -273,7 +288,19 @@ class OCTenantPage {
273
288
  await this.assignRoleButton.click();
274
289
  }
275
290
  async clickRoleIdSearchBox() {
276
- await this.roleIdSearchBox.click({ timeout: 60000 });
291
+ await this.roleIdSearchTrigger.click({ timeout: 60000 });
292
+ await (0, test_1.expect)(this.roleIdSearchBox).toBeVisible({ timeout: 60000 });
293
+ }
294
+ // Selecting an option or losing focus dismisses the popover, which unmounts
295
+ // the search input, so reopen it before the next term is typed.
296
+ async ensureRoleIdSearchBoxOpen() {
297
+ const open = await this.roleIdSearchBox
298
+ .waitFor({ state: 'visible', timeout: 5000 })
299
+ .then(() => true)
300
+ .catch(() => false);
301
+ if (!open) {
302
+ await this.clickRoleIdSearchBox();
303
+ }
277
304
  }
278
305
  async fillRoleIdSearchBox(roleId, roleIdentifier) {
279
306
  // The design-system shell portals the matches into a listbox rendered as
@@ -304,9 +331,12 @@ class OCTenantPage {
304
331
  // between terms resets it before the next one is typed.
305
332
  await (0, test_1.expect)(async () => {
306
333
  for (const searchTerm of searchTerms) {
334
+ await this.ensureRoleIdSearchBoxOpen();
307
335
  await this.roleIdSearchBox.fill('');
308
336
  await this.page.waitForLoadState('networkidle');
309
- await this.roleIdSearchBox.fill(searchTerm);
337
+ // The cmdk input only issues its server-side query on real
338
+ // keystrokes, so type the term in rather than setting it in one shot.
339
+ await this.roleIdSearchBox.pressSequentially(searchTerm);
310
340
  const offered = await result
311
341
  .waitFor({ state: 'visible', timeout: 15000 })
312
342
  .then(() => true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1172",
3
+ "version": "0.0.1174",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",