@camunda/e2e-test-suite 0.0.1276 → 0.0.1277

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.
@@ -82,28 +82,32 @@ class ClusterPage {
82
82
  });
83
83
  this.clusterVersionOption = page.getByText(`${process.env.CLUSTER_VERSION}`, { exact: true });
84
84
  this.generationSearchInput = page.getByPlaceholder('Search generations');
85
- this.g3DevClusterType = page.locator('label').filter({
86
- hasText: this.defaultClusterType,
87
- });
88
- this.londonRegion = page.locator('label').filter({
89
- hasText: 'London, Europe (europe-west2)',
90
- });
91
- this.ohioRegion = page.locator('label').filter({
85
+ // Camunda Hub's create-cluster form replaced the Carbon tiles (a <label>
86
+ // per option) and tabs with design-system choice cards and toggle groups,
87
+ // all exposed as role="radio" named by their title. Each locator below
88
+ // accepts either shape via this helper. Ported from
89
+ // pages/8.10/ClusterPage.ts (#3730), where the same change was made.
90
+ const choice = (legacy, name) => legacy.or(page.getByRole('radio', { name, exact: true }));
91
+ this.g3DevClusterType = choice(page.locator('label').filter({ hasText: this.defaultClusterType }), this.defaultClusterType);
92
+ this.londonRegion = choice(page.locator('label').filter({ hasText: 'London, Europe (europe-west2)' }), 'London, Europe (europe-west2)');
93
+ this.ohioRegion = choice(page.locator('label').filter({
92
94
  hasText: 'Ohio, North America (us-east-2)',
93
- });
94
- this.belgiumRegion = page.locator('label').filter({
95
+ }), 'Ohio, North America (us-east-2)');
96
+ this.belgiumRegion = choice(page.locator('label').filter({
95
97
  hasText: 'Chaos, Belgium, Europe (europe-west1)',
96
- });
97
- this.finlandRegion = page.locator('label').filter({
98
+ }), 'Chaos, Belgium, Europe (europe-west1)');
99
+ this.finlandRegion = choice(page.locator('label').filter({
98
100
  hasText: 'Finland, Europe (europe-north1)',
99
- });
100
- this.alphaTab = page.getByRole('tab', { name: 'Alpha' });
101
- this.devTab = page.getByRole('tab', { name: 'Internal Dev' });
102
- this.stableTab = page.getByRole('tab', { name: 'Stable' });
103
- this.qaGenerationsTab = page.getByRole('tab', { name: 'QA Generations' });
104
- this.gcpRegion = page.getByRole('tab', {
105
- name: 'GCP',
106
- });
101
+ }), 'Finland, Europe (europe-north1)');
102
+ this.alphaTab = choice(page.getByRole('tab', { name: 'Alpha' }), 'Alpha');
103
+ this.devTab = choice(page.getByRole('tab', { name: 'Internal Dev' }), 'Internal Dev');
104
+ this.stableTab = choice(page.getByRole('tab', { name: 'Stable' }), 'Stable');
105
+ this.qaGenerationsTab = choice(page.getByRole('tab', { name: 'QA Generations' }), 'QA Generations');
106
+ // The cloud-provider picker is a design-system toggle group, not tabs:
107
+ // a nightly trace's DOM has
108
+ // <button role="radio" aria-checked="true" data-slot="toggle-group-item">GCP</button>
109
+ // inside a role="radiogroup", and no role="tab" anywhere on the form.
110
+ this.gcpRegion = choice(page.getByRole('tab', { name: 'GCP' }), 'GCP');
107
111
  this.clustersList = page
108
112
  .getByRole('row')
109
113
  .filter({ hasNotText: 'Generation' }); //Filter out header row
@@ -224,6 +228,15 @@ class ClusterPage {
224
228
  visibilityTimeout: 20000,
225
229
  totalTimeout: 80000,
226
230
  postAction: async () => {
231
+ // The clusters route intermittently answers with an Envoy gateway
232
+ // error ("upstream connect error or disconnect/reset before headers.
233
+ // reset reason: connection timeout"), which renders as a bare text
234
+ // document -- clicking the banner cannot recover that, so every
235
+ // remaining attempt retried against a dead page. Reload first, then
236
+ // fall back to the banner for the ordinary not-yet-navigated case.
237
+ await this.page
238
+ .reload({ waitUntil: 'domcontentloaded', timeout: 30000 })
239
+ .catch(() => { });
227
240
  await this.clickClusterBanner();
228
241
  },
229
242
  });
@@ -276,7 +289,7 @@ class ClusterPage {
276
289
  await this.clickClusterNameInput();
277
290
  await this.fillClusterNameInput(name);
278
291
  await this.clickClusterType();
279
- await this.page.getByRole('tab', { name: region }).click();
292
+ await this.clickProviderOption(region);
280
293
  await this.selectSubRegion(region);
281
294
  await this.clickClusterOption(process.env.CLUSTER_VERSION);
282
295
  await this.determineTasklistAPI(tasklistV1Api);
@@ -284,6 +297,22 @@ class ClusterPage {
284
297
  await this.logClusterCreationInfo(name);
285
298
  await this.clickClustersBreadcrumb(name);
286
299
  }
300
+ // Selects the cloud provider ("GCP"/"AWS"). Hub renders the picker as a
301
+ // toggle group of role="radio" buttons rather than tabs, and it arrives with
302
+ // GCP already selected -- so a `role=tab` click both targeted nothing and
303
+ // was unnecessary. Accept either shape and skip the click when the option is
304
+ // already the selected one.
305
+ async clickProviderOption(region) {
306
+ const providerOption = this.page
307
+ .getByRole('tab', { name: region })
308
+ .or(this.page.getByRole('radio', { name: region, exact: true }))
309
+ .first();
310
+ await (0, test_1.expect)(providerOption).toBeVisible({ timeout: 40000 });
311
+ if ((await providerOption.getAttribute('aria-checked')) === 'true') {
312
+ return;
313
+ }
314
+ await providerOption.click({ timeout: 30000 });
315
+ }
287
316
  async selectSubRegion(region) {
288
317
  if (region === 'GCP') {
289
318
  if (process.env.IS_PROD == 'true') {
@@ -8,7 +8,9 @@ declare class ConnectorMarketplacePage {
8
8
  readonly replaceResourceButton: Locator;
9
9
  readonly addToProjectButton: Locator;
10
10
  readonly saveAsCopyButton: Locator;
11
+ readonly cancelButton: Locator;
11
12
  readonly importModal: Locator;
13
+ readonly browseAllMarketplaceDetailsButton: Locator;
12
14
  constructor(page: Page);
13
15
  clickSearchForConnectorTextbox(): Promise<void>;
14
16
  fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorMarketplacePage = void 0;
4
+ const test_1 = require("@playwright/test");
4
5
  class ConnectorMarketplacePage {
5
6
  page;
6
7
  searchForConnectorTextbox;
@@ -10,10 +11,27 @@ class ConnectorMarketplacePage {
10
11
  replaceResourceButton;
11
12
  addToProjectButton;
12
13
  saveAsCopyButton;
14
+ cancelButton;
13
15
  importModal;
16
+ browseAllMarketplaceDetailsButton;
14
17
  constructor(page) {
15
18
  this.page = page;
16
- this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
19
+ // The legacy marketplace modal, or camunda-hub's "Browse all" dialog,
20
+ // which lists Marketplace connectors alongside the diagram's own elements
21
+ // and reusable assets. Same shape as pages/8.7.
22
+ this.searchForConnectorTextbox = page
23
+ .getByPlaceholder('Search for a connector')
24
+ .or(page.locator('#browse-all-search'))
25
+ .or(page.getByRole('searchbox', {
26
+ name: 'Search elements and reusable assets',
27
+ }))
28
+ .first();
29
+ // A Browse all Marketplace card opens its review step ("Add to project")
30
+ // through this button; self-hosted connectors only link to setup docs.
31
+ this.browseAllMarketplaceDetailsButton = page
32
+ .getByRole('list', { name: 'Marketplace connectors' })
33
+ .getByRole('button', { name: /^View details for .+ from Marketplace$/ })
34
+ .first();
17
35
  this.downloadToProjectButton = page
18
36
  .getByRole('button', { name: 'Download to project' })
19
37
  .first();
@@ -26,6 +44,7 @@ class ConnectorMarketplacePage {
26
44
  name: 'Add to project',
27
45
  });
28
46
  this.saveAsCopyButton = page.getByRole('button', { name: 'Save as copy' });
47
+ this.cancelButton = page.getByRole('button', { name: 'Cancel' });
29
48
  this.importModal = page.locator('[data-test="import-modal"]');
30
49
  }
31
50
  async clickSearchForConnectorTextbox() {
@@ -47,7 +66,30 @@ class ConnectorMarketplacePage {
47
66
  await this.replaceResourceButton.click({ timeout: 60000 });
48
67
  }
49
68
  async downloadConnectorToProject() {
50
- await this.clickDownloadToProjectButton();
69
+ // Browse all has no "Download to project": a result is a card that opens
70
+ // its own review step, which is where the import action lives. Wait for
71
+ // whichever entry point this shell renders before deciding -- isVisible()
72
+ // never waits, so probing it straight away raced the dialog's own render
73
+ // and fell through both branches.
74
+ await (0, test_1.expect)(this.downloadToProjectButton
75
+ .or(this.browseAllMarketplaceDetailsButton)
76
+ .first()).toBeVisible({ timeout: 60000 });
77
+ if (await this.downloadToProjectButton.isVisible()) {
78
+ await this.clickDownloadToProjectButton();
79
+ }
80
+ else {
81
+ await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
82
+ }
83
+ // The Browse all review step fetches the connector's details before it
84
+ // renders "Add to project". Let one of the outcomes render before the
85
+ // probes below, which would otherwise repeat that same race.
86
+ await this.addToProjectButton
87
+ .or(this.replaceResourceButton)
88
+ .or(this.saveAsCopyButton)
89
+ .or(this.cancelButton)
90
+ .first()
91
+ .waitFor({ state: 'visible', timeout: 60000 })
92
+ .catch(() => { });
51
93
  // "Download to project" opens Web Modeler's "Import resource" modal, which
52
94
  // can take ~20s to appear. The import-confirming button depends on whether
53
95
  // the connector template already exists in the project: an existing one
@@ -43,20 +43,44 @@ class ConsoleOrganizationPage {
43
43
  constructor(page) {
44
44
  this.page = page;
45
45
  this.usersTab = page.getByRole('tab', { name: 'Users' });
46
- this.optionsButton = page.getByRole('button', { name: 'Options' });
47
- this.editUserMenuItem = page.getByRole('menuitem', { name: 'Edit User' });
48
- this.modelerCheckbox = page
49
- .getByRole('group')
50
- .locator('label')
51
- .filter({ hasText: 'Modeler' });
52
- this.adminCheckbox = page
53
- .getByRole('group')
54
- .locator('label')
55
- .filter({ hasText: 'Admin' });
56
- this.analystCheckbox = page
57
- .getByRole('group')
58
- .locator('label')
59
- .filter({ hasText: 'Analyst' });
46
+ // Hub's org-management Users table is a design-system data table whose
47
+ // per-row action affordance is a kebab named "Row actions", not Carbon's
48
+ // "Options" -- a nightly screenshot shows the kebab and the trace has no
49
+ // "Options" node at all. Scoped to the visible tab panel so another tab's
50
+ // rows cannot match. Same shape as pages/8.9.
51
+ this.optionsButton = page
52
+ .getByRole('button', { name: 'Options' })
53
+ .or(page.getByRole('tabpanel').getByRole('button', { name: 'Row actions' }))
54
+ .first();
55
+ this.editUserMenuItem = page.getByRole('menuitem', {
56
+ name: /^Edit user$/i,
57
+ });
58
+ // The "Edit user" dialog's role toggles are plain Carbon Checkboxes
59
+ // (@camunda/ccma-saas-frontend c4-user-details-page.tsx), each its own
60
+ // labelText-bound <input type="checkbox">, not a role="group" of
61
+ // <label>s -- there is no group wrapper around them at all. Scoped to
62
+ // whichever dialog is open rather than one named "Edit user": a modal
63
+ // whose heading isn't wired via aria-labelledby carries no accessible
64
+ // name at all, which silently zeroes out a name-matched dialog locator.
65
+ const openDialog = page.getByRole('dialog');
66
+ // Hub renamed the platform roles, and the "Edit user" dialog states each
67
+ // mapping in the role's own description (confirmed live against the
68
+ // nightly org): "Member ... Formerly the Modeler role.",
69
+ // "Organization Admin ... Formerly the Admin role.", "DevOps ... Formerly
70
+ // the Operations Engineer role."; Analyst is unchanged. There is no
71
+ // "Modeler" checkbox at all any more, which is why the exact-name lookup
72
+ // resolved to nothing.
73
+ //
74
+ // Anchored regex rather than `exact`: each checkbox's accessible name
75
+ // carries that description after the label, so an exact match on the bare
76
+ // role name would miss it.
77
+ const roleCheckbox = (current, legacy) => openDialog
78
+ .getByRole('checkbox', { name: current })
79
+ .or(openDialog.getByRole('checkbox', { name: legacy, exact: true }))
80
+ .first();
81
+ this.modelerCheckbox = roleCheckbox(/^Member\b/, 'Modeler');
82
+ this.adminCheckbox = roleCheckbox(/^Organization Admin\b/, 'Admin');
83
+ this.analystCheckbox = roleCheckbox(/^Analyst\b/, 'Analyst');
60
84
  this.confirmButton = page.getByRole('button', { name: 'Confirm' });
61
85
  this.settingsTab = page.getByRole('tab', { name: 'Settings' });
62
86
  this.optInButton = page.getByRole('button', { name: 'Opt-in', exact: true });
@@ -118,9 +142,16 @@ class ConsoleOrganizationPage {
118
142
  name: 'danger',
119
143
  })
120
144
  .last();
121
- this.filterTableSearchbox = page.getByRole('searchbox', {
122
- name: 'Filter table',
123
- });
145
+ // Hub's org-management "Users" tab renders its table filter as a
146
+ // design-system <input type="search" placeholder="Search"
147
+ // aria-label="Search">, not Carbon's "Filter table" -- confirmed from a
148
+ // nightly trace's DOM snapshot, where "Filter table" occurs only inside
149
+ // our own call log. Keep the legacy name as a fallback. Same shape as
150
+ // pages/8.9.
151
+ this.filterTableSearchbox = page
152
+ .getByRole('searchbox', { name: 'Search', exact: true })
153
+ .or(page.getByRole('searchbox', { name: 'Filter table' }))
154
+ .first();
124
155
  }
125
156
  async filterTable(names) {
126
157
  for (const name of names) {
@@ -189,7 +220,21 @@ class ConsoleOrganizationPage {
189
220
  await this.optionsButton.click({ timeout: 60000 });
190
221
  }
191
222
  async clickEditUserMenuItem() {
192
- await this.editUserMenuItem.click({ timeout: 60000 });
223
+ // Hub's row menu is a Radix dropdown that animates in and re-mounts its
224
+ // items while opening. A live run against the nightly org resolved this
225
+ // item correctly -- <div role="menuitem" data-slot="dropdown-menu-item">
226
+ // Edit user</div> -- and still failed the click: "element is not stable"
227
+ // twice, then "element was detached from the DOM, retrying" until the 60s
228
+ // budget ran out. A detached item means the menu itself went away, so
229
+ // clicking harder cannot help; re-open it and retry the pair as one unit,
230
+ // gated on the dialog actually opening.
231
+ await (0, test_1.expect)(async () => {
232
+ if (!(await this.editUserMenuItem.isVisible().catch(() => false))) {
233
+ await this.clickOptionsButton();
234
+ }
235
+ await this.editUserMenuItem.click({ timeout: 10000 });
236
+ await (0, test_1.expect)(this.dialog.first()).toBeVisible({ timeout: 10000 });
237
+ }).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
193
238
  }
194
239
  async checkModelerCheckbox() {
195
240
  if (!(await this.modelerCheckbox.isChecked({ timeout: 60000 }))) {
@@ -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;
@@ -22,6 +25,7 @@ declare class FormJsPage {
22
25
  readonly deployErrorMessage: Locator;
23
26
  constructor(page: Page);
24
27
  generateAIForm(request?: string): Promise<void>;
28
+ private generateFormWithCopilot;
25
29
  clickAIFormGeneratorButton(): Promise<void>;
26
30
  clickFormRequestInput(): Promise<void>;
27
31
  fillFormRequestInput(formRequest: string): Promise<void>;
@@ -11,6 +11,9 @@ class FormJsPage {
11
11
  page;
12
12
  processApplicationPage;
13
13
  aiFormGeneratorButton;
14
+ copilotCtaButton;
15
+ copilotChatInput;
16
+ copilotSendButton;
14
17
  formRequestInput;
15
18
  generateFormButton;
16
19
  formEditor;
@@ -34,6 +37,16 @@ class FormJsPage {
34
37
  this.aiFormGeneratorButton = page.getByRole('button', {
35
38
  name: 'AI Form Generator',
36
39
  });
40
+ // camunda-hub #21398 retired the form page's AI Form Generator in favour
41
+ // of the Camunda Copilot sidecar: with One Copilot on, the empty form
42
+ // offers "Try Camunda Copilot" instead (FormDs.tsx / CopilotCTA.tsx), and
43
+ // the sidecar's chat input and send button come from
44
+ // camunda-copilot-client's ChatInput. Same shape as pages/8.7.
45
+ this.copilotCtaButton = page.getByRole('button', {
46
+ name: 'Try Camunda Copilot',
47
+ });
48
+ this.copilotChatInput = page.getByPlaceholder('Type a message...');
49
+ this.copilotSendButton = page.getByRole('button', { name: 'Send message' });
37
50
  this.formRequestInput = page.getByPlaceholder('A form for an american standard loan request');
38
51
  this.generateFormButton = page.getByRole('button', { name: 'Generate form' });
39
52
  this.formEditor = page.getByLabel('Form Definition');
@@ -89,9 +102,13 @@ class FormJsPage {
89
102
  this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
90
103
  }
91
104
  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"') {
92
- await (0, test_1.expect)(this.aiFormGeneratorButton).toBeVisible({
105
+ await (0, test_1.expect)(this.aiFormGeneratorButton.or(this.copilotCtaButton).first()).toBeVisible({
93
106
  timeout: 90000,
94
107
  });
108
+ if (!(await this.aiFormGeneratorButton.isVisible())) {
109
+ await this.generateFormWithCopilot(request);
110
+ return;
111
+ }
95
112
  await this.clickAIFormGeneratorButton();
96
113
  await this.clickFormRequestInput();
97
114
  await this.fillFormRequestInput(request);
@@ -100,6 +117,19 @@ class FormJsPage {
100
117
  await (0, test_1.expect)(this.page.getByText('Successfully generated smart form.')).toBeVisible({ timeout: 300000 });
101
118
  await (0, sleep_1.sleep)(60000);
102
119
  }
120
+ async generateFormWithCopilot(request) {
121
+ await this.copilotCtaButton.click({ timeout: 30000 });
122
+ await (0, test_1.expect)(this.copilotChatInput).toBeVisible({ timeout: 60000 });
123
+ await this.copilotChatInput.fill(request);
124
+ await this.copilotSendButton.click({ timeout: 30000 });
125
+ // Copilot edits the open form in place; the generated form is ready once
126
+ // every requested field shows up on the canvas.
127
+ for (const label of ['Full Name', 'Count', 'Date of birth', 'Agree']) {
128
+ await (0, test_1.expect)(this.formEditor.getByText(label).first()).toBeVisible({
129
+ timeout: 300000,
130
+ });
131
+ }
132
+ }
103
133
  async clickAIFormGeneratorButton() {
104
134
  await this.aiFormGeneratorButton.click();
105
135
  }
@@ -71,6 +71,7 @@ declare class ModelerCreatePage {
71
71
  readonly publicAccessToggle: Locator;
72
72
  readonly publicationTab: Locator;
73
73
  readonly copyPublicLinkButton: Locator;
74
+ readonly publicLinkCodeBlock: Locator;
74
75
  readonly closeButton: Locator;
75
76
  readonly rbaEnabledDeployedDialog: Locator;
76
77
  readonly viewProcessLink: Locator;
@@ -214,9 +215,11 @@ declare class ModelerCreatePage {
214
215
  fillJobTypeInput(name: string): Promise<void>;
215
216
  private suppressTestModeAnnouncement;
216
217
  switchToPlay(): Promise<void>;
218
+ private selectPlayDeploySingleResource;
217
219
  completePlayConfiguration(clusterName: string): Promise<void>;
218
220
  embedForm(formName: string): Promise<void>;
219
221
  fillStartEventVariablesForDocHandling(zeebeUrl: string, zeebeClientId: string, zeebeClientSecret: string): Promise<void>;
222
+ private openOutputMappingEntry;
220
223
  fillZeebeClientId(clientId: string): Promise<void>;
221
224
  fillZeebeSecretValue(zeebeSecret: string): Promise<void>;
222
225
  fillZeebeRestValue(zeebeUrl: string): Promise<void>;