@camunda/e2e-test-suite 0.0.1010 → 0.0.1012

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.
@@ -104,9 +104,14 @@ class FormJsPage {
104
104
  return;
105
105
  }
106
106
  await scopeSelect.click();
107
- await this.deployDialog
107
+ // The scope select is a Radix UI Select: its listbox is portalled to
108
+ // document.body, OUTSIDE the dialog element, so a dialog-scoped option
109
+ // query never resolves even while the option is visible on screen. Query
110
+ // the portalled option at page scope instead; the trigger above stays
111
+ // dialog-scoped because it renders inside the dialog.
112
+ await this.page
108
113
  .getByRole('option', { name: 'Only this resource' })
109
- .or(this.deployDialog.getByText('Only this resource', { exact: true }))
114
+ .or(this.page.getByText('Only this resource', { exact: true }))
110
115
  .first()
111
116
  .click({ timeout: 30000 });
112
117
  }
@@ -56,8 +56,10 @@ class NavigationPage {
56
56
  });
57
57
  // TODO: Drop support for Identity once we fully rename it to Admin
58
58
  this.identityPageBanner = page
59
+ .getByRole('link', { name: 'Camunda Admin', exact: true })
60
+ .or(page
59
61
  .locator('a')
60
- .filter({ hasText: /^Camunda logo\s*(Identity|Admin)$/ })
62
+ .filter({ hasText: /^Camunda logo\s*(Identity|Admin)$/ }))
61
63
  .first();
62
64
  this.managementIdentityPageBanner = page
63
65
  .locator('a')
@@ -19,34 +19,57 @@ class OCIdentityHomePage {
19
19
  constructor(page) {
20
20
  this.page = page;
21
21
  this.adminBanner = page
22
- .locator('a')
23
- .filter({ hasText: /^Camunda logo\s*Admin$/ })
22
+ .getByRole('link', { name: 'Camunda Admin', exact: true })
23
+ .or(page.locator('a').filter({ hasText: /^Camunda logo\s*Admin$/ }))
24
24
  .first();
25
25
  this.groupsTab = page
26
+ .getByRole('navigation', { name: 'Main navigation' })
27
+ .getByRole('link', { name: 'Groups', exact: true })
28
+ .or(page
26
29
  .getByRole('banner')
27
30
  .locator('a')
28
- .filter({ hasText: /^Groups$/ });
31
+ .filter({ hasText: /^Groups$/ }))
32
+ .first();
29
33
  this.mappingRulesTab = page
34
+ .getByRole('navigation', { name: 'Main navigation' })
35
+ .getByRole('link', { name: 'Mapping rules', exact: true })
36
+ .or(page
30
37
  .getByRole('banner')
31
38
  .locator('a')
32
- .filter({ hasText: /^Mapping rules$/ })
39
+ .filter({ hasText: /^Mapping rules$/ }))
33
40
  .first();
34
41
  this.tenantsTab = page
42
+ .getByRole('navigation', { name: 'Main navigation' })
43
+ .getByRole('link', { name: 'Tenants', exact: true })
44
+ .or(page
35
45
  .getByRole('banner')
36
46
  .locator('a')
37
- .filter({ hasText: /^Tenants$/ });
47
+ .filter({ hasText: /^Tenants$/ }))
48
+ .first();
38
49
  this.rolesTab = page
50
+ .getByRole('navigation', { name: 'Main navigation' })
51
+ .getByRole('link', { name: 'Roles', exact: true })
52
+ .or(page
39
53
  .getByRole('banner')
40
54
  .locator('a')
41
- .filter({ hasText: 'Roles' });
55
+ .filter({ hasText: /^Roles$/ }))
56
+ .first();
42
57
  this.authorizationsTab = page
58
+ .getByRole('navigation', { name: 'Main navigation' })
59
+ .getByRole('link', { name: 'Authorizations', exact: true })
60
+ .or(page
43
61
  .getByRole('banner')
44
62
  .locator('a')
45
- .filter({ hasText: /^Authorizations$/ });
63
+ .filter({ hasText: /^Authorizations$/ }))
64
+ .first();
46
65
  this.clusterVariablesTab = page
66
+ .getByRole('navigation', { name: 'Main navigation' })
67
+ .getByRole('link', { name: 'Cluster variables', exact: true })
68
+ .or(page
47
69
  .getByRole('banner')
48
70
  .locator('a')
49
- .filter({ hasText: /Cluster variables/i });
71
+ .filter({ hasText: /Cluster variables/i }))
72
+ .first();
50
73
  this.licenseKeyTagNonProduction = page
51
74
  .getByText('Non-production license')
52
75
  .first();
@@ -87,9 +87,12 @@ class OCIdentityRolesPage {
87
87
  });
88
88
  this.adminRole = page.getByRole('cell', { name: 'admin', exact: true });
89
89
  this.rolesNavTab = page
90
+ .getByRole('navigation', { name: 'Main navigation' })
91
+ .getByRole('link', { name: 'Roles', exact: true })
92
+ .or(page
90
93
  .getByRole('banner')
91
94
  .locator('a')
92
- .filter({ hasText: /^Roles$/ })
95
+ .filter({ hasText: /^Roles$/ }))
93
96
  .first();
94
97
  this.assignUserButton = page.getByRole('button', { name: 'Assign user' });
95
98
  this.usernameTextBox = page.getByRole('textbox', { name: 'Username' });
@@ -8,9 +8,18 @@ class SettingsPage {
8
8
  openUserSidebarButton;
9
9
  constructor(page) {
10
10
  this.page = page;
11
- this.openSettingsButton = page.getByLabel('Open Settings');
12
- this.openUserSidebarButton = page.getByLabel('Open User Sidebar');
13
- this.logoutButton = page.getByRole('button', { name: 'Log out' });
11
+ this.openSettingsButton = page
12
+ .getByRole('button', { name: /^User menu/ })
13
+ .or(page.getByLabel('Open Settings'))
14
+ .first();
15
+ this.openUserSidebarButton = page
16
+ .getByRole('button', { name: /^User menu/ })
17
+ .or(page.getByLabel('Open User Sidebar'))
18
+ .first();
19
+ this.logoutButton = page
20
+ .getByRole('menuitem', { name: /log out/i })
21
+ .or(page.getByRole('button', { name: 'Log out' }))
22
+ .first();
14
23
  }
15
24
  async clickOpenSettingsButton() {
16
25
  await this.openSettingsButton.click({ timeout: 120000 });
@@ -2,11 +2,15 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private page;
4
4
  private defaultFolderName;
5
+ private processApplicationPage;
6
+ private processApplicationUrl?;
7
+ private processApplicationName?;
5
8
  readonly modelerPageBanner: Locator;
6
9
  readonly createNewProjectButton: Locator;
7
10
  readonly projectNameInput: Locator;
8
11
  readonly chooseBpmnTemplateButton: Locator;
9
12
  readonly diagramTypeDropdown: Locator;
13
+ readonly createIdpApplicationButton: Locator;
10
14
  readonly bpmnTemplateOption: Locator;
11
15
  readonly formTemplateOption: Locator;
12
16
  readonly projectBreadcrumb: Locator;
@@ -34,6 +38,9 @@ declare class ModelerHomePage {
34
38
  clickHomeBreadcrumb(): Promise<void>;
35
39
  clickChooseBpmnTemplateButton(): Promise<void>;
36
40
  clickDiagramTypeDropdown(): Promise<void>;
41
+ private enterResourceContainer;
42
+ private clickCreateProcessApplication;
43
+ private becameVisible;
37
44
  clickBpmnTemplateOption(): Promise<void>;
38
45
  clickFormOption(): Promise<void>;
39
46
  enterFormName(name: string): Promise<void>;
@@ -4,14 +4,26 @@ exports.ModelerHomePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
+ const randomName_1 = require("../../utils/randomName");
8
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
9
  class ModelerHomePage {
8
10
  page;
9
11
  defaultFolderName = 'Cross Component Test Project';
12
+ // Only used on hub's own image, where content can live solely inside a
13
+ // process application (camunda/camunda-hub#27592). The released 8.9 image
14
+ // still creates resources at the project root and never touches this. The
15
+ // container is created lazily, the first time a resource is created, and
16
+ // remembered for the lifetime of this page object (one instance per test,
17
+ // see fixtures/SM-8.9.ts).
18
+ processApplicationPage;
19
+ processApplicationUrl;
20
+ processApplicationName;
10
21
  modelerPageBanner;
11
22
  createNewProjectButton;
12
23
  projectNameInput;
13
24
  chooseBpmnTemplateButton;
14
25
  diagramTypeDropdown;
26
+ createIdpApplicationButton;
15
27
  bpmnTemplateOption;
16
28
  formTemplateOption;
17
29
  projectBreadcrumb;
@@ -31,6 +43,7 @@ class ModelerHomePage {
31
43
  modalCloseButton;
32
44
  constructor(page) {
33
45
  this.page = page;
46
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
34
47
  this.modelerPageBanner = page
35
48
  .locator('a')
36
49
  .filter({ hasText: 'Camunda logoModeler' });
@@ -46,6 +59,14 @@ class ModelerHomePage {
46
59
  name: 'Choose BPMN template',
47
60
  });
48
61
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
62
+ // Positive marker of the process-application-only layout (hub's own image,
63
+ // camunda/camunda-hub#27592): that empty project root surfaces IDP-app
64
+ // creation as a top-level button. The released 8.9 root has no such button
65
+ // -- its IDP entry is a menu item inside the "Create new" dropdown -- so
66
+ // this reliably distinguishes the two layouts.
67
+ this.createIdpApplicationButton = page.getByRole('button', {
68
+ name: 'Create IDP application',
69
+ });
49
70
  this.bpmnTemplateOption = page
50
71
  .locator('[data-test="create-bpmn-diagram"]')
51
72
  .getByText('BPMN Diagram');
@@ -212,7 +233,35 @@ class ModelerHomePage {
212
233
  // -- the reload-and-wait retry never actually ran. Bound the recovery wait
213
234
  // and widen the budget so all three attempts fit.
214
235
  await this.waitForModelerReady();
215
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.diagramTypeDropdown, {
236
+ // Two Web Modeler layouts run this suite. The released 8.9 image (the SM
237
+ // nightly) keeps the create-resource dropdown at the project root. Hub's
238
+ // own image (this pipeline) only lets content live inside a process
239
+ // application since camunda/camunda-hub#27592, so the root offers no
240
+ // dropdown and instead surfaces IDP-application creation as a top-level
241
+ // button.
242
+ //
243
+ // Route on a positive marker of EACH layout, never on the mere absence of
244
+ // the dropdown: wait for the root dropdown (old layout) OR the IDP button
245
+ // (new layout). If the released image's root dropdown were itself broken --
246
+ // the regression the old path exists to catch -- neither marker would
247
+ // render and this assertion throws, rather than silently taking the
248
+ // process-application path (whose "Create process application" button exists
249
+ // on both images).
250
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.diagramTypeDropdown.first().or(this.createIdpApplicationButton), {
251
+ visibilityTimeout: 15000,
252
+ totalTimeout: 180000,
253
+ postAction: async () => {
254
+ await this.page.reload();
255
+ await this.waitForModelerReady(45000);
256
+ },
257
+ });
258
+ if (await this.diagramTypeDropdown.first().isVisible()) {
259
+ await this.diagramTypeDropdown.first().click();
260
+ return;
261
+ }
262
+ await this.enterResourceContainer();
263
+ const dropdown = this.processApplicationPage.createNewDropdown;
264
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, dropdown, {
216
265
  visibilityTimeout: 15000,
217
266
  totalTimeout: 180000,
218
267
  postAction: async () => {
@@ -220,7 +269,53 @@ class ModelerHomePage {
220
269
  await this.waitForModelerReady(45000);
221
270
  },
222
271
  });
223
- await this.diagramTypeDropdown.click();
272
+ await dropdown.click();
273
+ }
274
+ // Creates, once, the process application that holds this test's resources and
275
+ // leaves the browser inside it. Only reached on hub's process-application-only
276
+ // layout. Idempotent: re-entered by remembered URL if already created.
277
+ async enterResourceContainer() {
278
+ if (this.processApplicationUrl) {
279
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
280
+ if (!stillInside) {
281
+ await this.page.goto(this.processApplicationUrl, {
282
+ waitUntil: 'domcontentloaded',
283
+ });
284
+ await this.processApplicationPage.waitForProcessApplication();
285
+ await this.clickMessageBanner();
286
+ }
287
+ return;
288
+ }
289
+ this.processApplicationName = await (0, randomName_1.randomNameAgregator)('Cross Component Process Application');
290
+ await this.clickCreateProcessApplication();
291
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
292
+ this.processApplicationUrl = this.page.url();
293
+ }
294
+ // Which control creates a process application depends on the hub version under
295
+ // test. Since camunda/camunda-hub#25831 it is a plain primary button on the
296
+ // workspace root, and the "Create new" menu explicitly hides the item because
297
+ // a process application is no longer just another resource. Before that change
298
+ // the menu item was the only way in. Race both entry points so neither version
299
+ // pays a full timeout, then use whichever one rendered.
300
+ async clickCreateProcessApplication() {
301
+ const button = this.processApplicationPage.createProcessApplicationButton;
302
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
303
+ if (!rootLoaded) {
304
+ await this.page.reload();
305
+ await this.waitForModelerReady(45000);
306
+ }
307
+ if (await this.becameVisible(button, 5000)) {
308
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
309
+ return;
310
+ }
311
+ await this.diagramTypeDropdown.click({ timeout: 60000 });
312
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
313
+ }
314
+ async becameVisible(locator, timeout) {
315
+ return locator
316
+ .waitFor({ state: 'visible', timeout })
317
+ .then(() => true)
318
+ .catch(() => false);
224
319
  }
225
320
  async clickBpmnTemplateOption() {
226
321
  await (0, test_1.expect)(this.bpmnTemplateOption).toBeVisible({ timeout: 60000 });
@@ -0,0 +1,22 @@
1
+ import { Locator, Page } from '@playwright/test';
2
+ declare class ProcessApplicationPage {
3
+ private page;
4
+ readonly createProcessApplicationButton: Locator;
5
+ readonly createProcessApplicationOption: Locator;
6
+ readonly creationDialog: Locator;
7
+ readonly nameInput: Locator;
8
+ readonly submitButton: Locator;
9
+ readonly cancelButton: Locator;
10
+ readonly breadcrumb: Locator;
11
+ readonly folderExplorer: Locator;
12
+ readonly titleBar: Locator;
13
+ readonly createNewDropdown: Locator;
14
+ readonly inlineNameInput: Locator;
15
+ constructor(page: Page);
16
+ private pageReady;
17
+ waitForProcessApplication(timeout?: number): Promise<void>;
18
+ clickCreateProcessApplicationButton(): Promise<void>;
19
+ clickCreateProcessApplicationOption(): Promise<void>;
20
+ nameNewProcessApplication(name: string): Promise<void>;
21
+ }
22
+ export { ProcessApplicationPage };
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessApplicationPage = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
6
+ // Since camunda/camunda-hub#27592 a process application is the only container
7
+ // that can hold files and folders; a workspace root ("project" in today's UI)
8
+ // holds nothing but process applications and IDP applications. The released 8.9
9
+ // image still lets resources live at the project root, so this page object is
10
+ // only reached when the newer, process-application-only layout is detected --
11
+ // entering and leaving it is driven from ModelerHomePage so the existing spec
12
+ // call sites keep working unchanged.
13
+ class ProcessApplicationPage {
14
+ page;
15
+ // Empty workspace root: a plain primary button rendered next to the "Create
16
+ // new" menu (hub's EmptyStateActionButtons).
17
+ createProcessApplicationButton;
18
+ // Non-empty root: the item inside the "Create new" menu. Hub passes
19
+ // hideProcessApplication at workspace level, so this is the fallback rather
20
+ // than the primary path. Kept because the Hub PR is still moving this surface.
21
+ createProcessApplicationOption;
22
+ creationDialog;
23
+ nameInput;
24
+ submitButton;
25
+ cancelButton;
26
+ breadcrumb;
27
+ folderExplorer;
28
+ titleBar;
29
+ createNewDropdown;
30
+ inlineNameInput;
31
+ constructor(page) {
32
+ this.page = page;
33
+ // .first() defensively only: hub renders exactly one of these today, in the
34
+ // workspace-root empty state. A future second placement should not turn
35
+ // every PA-creating test into a strict-mode violation.
36
+ this.createProcessApplicationButton = page
37
+ .getByRole('button', { name: 'Create process application' })
38
+ .first();
39
+ this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
40
+ this.creationDialog = page.getByRole('dialog', {
41
+ name: 'Create a process application',
42
+ });
43
+ // Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
44
+ // getByLabel('Name', {exact: true}) for the properties panel, which would
45
+ // resolve to two elements while the creation modal is open.
46
+ this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
47
+ this.submitButton = this.creationDialog.getByRole('button', {
48
+ name: 'Create',
49
+ exact: true,
50
+ });
51
+ this.cancelButton = this.creationDialog.getByRole('button', {
52
+ name: 'Cancel',
53
+ exact: true,
54
+ });
55
+ this.breadcrumb = page.locator('[data-test="breadcrumb-process-application-menu"]');
56
+ // The Design System shell renames this region "Resources"; the Carbon shell
57
+ // calls it "Folder explorer". Both hold the resource table and its toolbar,
58
+ // so matching either keeps createNewDropdown below correctly scoped.
59
+ this.folderExplorer = page
60
+ .getByRole('region', { name: 'Folder explorer' })
61
+ .or(page.getByRole('region', { name: 'Resources' }))
62
+ .first();
63
+ // Design System proof that we are on a process application page: the
64
+ // Carbon-era breadcrumb hook is gone under that shell, the same way
65
+ // breadcrumb-project is.
66
+ this.titleBar = page.locator('[data-test="process-application-title-bar"]');
67
+ // The process application page renders more than one diagram-dropdown
68
+ // (hub's own unit tests reach for it with getAllByTestId), so the dropdown
69
+ // has to be resolved inside the folder explorer rather than page-wide.
70
+ this.createNewDropdown = this.folderExplorer
71
+ .locator('[data-test="diagram-dropdown"]')
72
+ .first();
73
+ this.inlineNameInput = page.locator('[data-test="editable-input"]');
74
+ }
75
+ // Any one of the title bar, the resource region or the breadcrumb proves we
76
+ // are on the process application page. Three branches because the Design
77
+ // System and Carbon shells expose different subsets: only the title bar is
78
+ // present under both. .first() because several match once it has rendered.
79
+ pageReady() {
80
+ return this.titleBar.or(this.folderExplorer).or(this.breadcrumb).first();
81
+ }
82
+ async waitForProcessApplication(timeout = 90000) {
83
+ await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
84
+ }
85
+ async clickCreateProcessApplicationButton() {
86
+ await (0, test_1.expect)(this.createProcessApplicationButton).toBeVisible({
87
+ timeout: 60000,
88
+ });
89
+ await this.createProcessApplicationButton.click({ timeout: 60000 });
90
+ }
91
+ async clickCreateProcessApplicationOption() {
92
+ await (0, test_1.expect)(this.createProcessApplicationOption).toBeVisible({
93
+ timeout: 60000,
94
+ });
95
+ await this.createProcessApplicationOption.click({ timeout: 60000 });
96
+ }
97
+ // Hub creates a process application through a modal today. It used an inline
98
+ // editable title before, and #27593 is still moving this surface, so the
99
+ // inline path is kept as a fallback -- this is the one method that has to
100
+ // change if naming moves back inline.
101
+ async nameNewProcessApplication(name) {
102
+ const isModal = await this.creationDialog
103
+ .waitFor({ state: 'visible', timeout: 15000 })
104
+ .then(() => true)
105
+ .catch(() => false);
106
+ if (isModal) {
107
+ await this.nameInput.click({ timeout: 30000 });
108
+ await this.nameInput.fill(name);
109
+ await (0, test_1.expect)(this.nameInput).toHaveValue(name, { timeout: 10000 });
110
+ await (0, submitModalWithRetry_1.submitModalWithRetry)(this.submitButton, this.creationDialog);
111
+ await this.waitForProcessApplication();
112
+ return;
113
+ }
114
+ await (0, test_1.expect)(this.inlineNameInput).toBeVisible({ timeout: 30000 });
115
+ await this.inlineNameInput.click({ timeout: 60000 });
116
+ await this.inlineNameInput.fill(name);
117
+ await this.inlineNameInput.press('Enter');
118
+ await this.waitForProcessApplication();
119
+ }
120
+ }
121
+ exports.ProcessApplicationPage = ProcessApplicationPage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1010",
3
+ "version": "0.0.1012",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",