@camunda/e2e-test-suite 0.0.1005 → 0.0.1006

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.
@@ -36,13 +36,19 @@ class FormJsPage {
36
36
  });
37
37
  this.documentPreview = page.locator('button[data-field-type="documentPreview"]');
38
38
  this.deployDialog = page.getByRole('dialog');
39
- this.deployFormButton = page.locator('button.cds--btn--primary', {
40
- hasText: 'Deploy',
41
- });
42
- this.deployModalSubmitButton = page.locator('button[data-modal-primary-focus="true"][type="submit"]');
43
- this.deploySuccessNotification = page
44
- .locator('.cds--toast-notification--success')
45
- .filter({ hasText: 'Form deployed!' });
39
+ // The Design System shell renders none of Carbon's cds-- classes, so the
40
+ // editor's Deploy button, the dialog's submit and the success toast are all
41
+ // matched by role and text instead -- which resolves on both shells.
42
+ // .first(): once the deploy dialog is open its submit carries the same
43
+ // accessible name; the editor's button is the earlier one in the DOM.
44
+ this.deployFormButton = page
45
+ .getByRole('button', { name: 'Deploy', exact: true })
46
+ .first();
47
+ this.deployModalSubmitButton = this.deployDialog
48
+ .getByRole('button', { name: 'Deploy', exact: true })
49
+ .or(page.locator('button[data-modal-primary-focus="true"][type="submit"]'))
50
+ .first();
51
+ this.deploySuccessNotification = page.getByText('Form deployed!').first();
46
52
  }
47
53
  async clickAIFormGeneratorButton() {
48
54
  await this.aiFormGeneratorButton.click();
@@ -94,10 +94,26 @@ class ManagementIdentityPage {
94
94
  await (0, test_1.expect)(this.demoUser).toBeVisible({ timeout: 60000 });
95
95
  await this.demoUser.click({ timeout: 180000 });
96
96
  }
97
+ // The assigned-roles table can come up with a stale row set shortly after the
98
+ // roles were assigned — run 32920068272 recorded `/api/users/<id>/roles`
99
+ // returning Optimize while the table repeated the neighbouring row's label —
100
+ // and it does not re-render on its own. Rebuild it from a fresh response
101
+ // between attempts before reporting the role as missing.
97
102
  async clickDeleteAccessButton(accessToDelete) {
98
103
  const roleRow = this.page
99
104
  .locator(':text("' + accessToDelete + '")')
100
105
  .first();
106
+ for (let attempt = 1; attempt <= 2; attempt++) {
107
+ const rowVisible = await roleRow
108
+ .waitFor({ state: 'visible', timeout: 30000 })
109
+ .then(() => true)
110
+ .catch(() => false);
111
+ if (rowVisible) {
112
+ break;
113
+ }
114
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
115
+ await this.clickAssignedRolesTab();
116
+ }
101
117
  await (0, test_1.expect)(roleRow, `No assigned role matching "${accessToDelete}" was found. The role may ` +
102
118
  `already be removed, or its label may have changed in this version.`).toBeVisible({ timeout: 60000 });
103
119
  await this.page
@@ -21,6 +21,10 @@ declare class ModelerCreatePage {
21
21
  readonly diagramBreadcrumb: Locator;
22
22
  readonly renameDiagramNameButton: Locator;
23
23
  readonly diagramNameInput: Locator;
24
+ readonly moreActionsButton: Locator;
25
+ readonly renameMenuItem: Locator;
26
+ readonly renameInput: Locator;
27
+ readonly renameSubmitButton: Locator;
24
28
  readonly variableInput: Locator;
25
29
  readonly businessIdInput: Locator;
26
30
  readonly embedFormButton: Locator;
@@ -113,6 +117,7 @@ declare class ModelerCreatePage {
113
117
  private clickCreatePadEntry;
114
118
  private appendViaPopup;
115
119
  clickAppendTaskButton(): Promise<void>;
120
+ private padHoverTargets;
116
121
  hoverOnLocator(locator: Locator, selector?: string): Promise<void>;
117
122
  clickAppendGatewayButton(): Promise<void>;
118
123
  setEngineVersion(version: string): Promise<void>;
@@ -136,6 +141,7 @@ declare class ModelerCreatePage {
136
141
  fillJobTypeInput(name: string): Promise<void>;
137
142
  clickDiagramBreadcrumb(): Promise<void>;
138
143
  clickEditDiagramNameButton(): Promise<void>;
144
+ private legacyBreadcrumbPresent;
139
145
  enterDiagramName(name: string): Promise<void>;
140
146
  clickVariableInput(): Promise<void>;
141
147
  fillVariableInput(variable: string): Promise<void>;
@@ -27,6 +27,10 @@ class ModelerCreatePage {
27
27
  diagramBreadcrumb;
28
28
  renameDiagramNameButton;
29
29
  diagramNameInput;
30
+ moreActionsButton;
31
+ renameMenuItem;
32
+ renameInput;
33
+ renameSubmitButton;
30
34
  variableInput;
31
35
  businessIdInput;
32
36
  embedFormButton;
@@ -107,7 +111,13 @@ class ModelerCreatePage {
107
111
  this.processIdInput = page.getByLabel('ID', { exact: true });
108
112
  this.elemendIdInput = page.getByLabel('ID', { exact: true });
109
113
  this.startEventElement = page.locator('.djs-hit').first();
110
- this.appendPreButton = page.locator('.djs-create-pad-icon > svg').first();
114
+ // The improved canvas exposes the append affordance of the selected element
115
+ // as an overlay indicator ("+" beside the shape) that stands in for the
116
+ // create pad's call-to-action entry; the `.djs-create-pad-icon` wrapper this
117
+ // used to hover is no longer rendered.
118
+ this.appendPreButton = page
119
+ .locator('.djs-append-indicator.selected')
120
+ .first();
111
121
  this.appendTaskButton = page.getByRole('button', { name: 'Append task' });
112
122
  this.appendAnythingEntry = page.locator('.djs-create-pad-entries [data-entry-id="append"]');
113
123
  this.appendPopup = page.locator('.djs-popup.bpmn-append');
@@ -123,7 +133,18 @@ class ModelerCreatePage {
123
133
  this.appendGatewayButton = page.getByRole('button', {
124
134
  name: 'Append gateway',
125
135
  });
126
- this.parallelGatewayOption = page.getByText('parallel gateway');
136
+ // bpmn-js renders popup entries as <li role="option" data-id="..."> and
137
+ // binds the click handler to that li. getByText resolved the nested
138
+ // <span class="djs-popup-label"> instead, whose box kept failing the
139
+ // visible/stable actionability check inside the scrolling results list --
140
+ // which is why this timed out on some runs and not others. Target the li,
141
+ // by the data-id bpmn-js owns, with the role as a fallback. The suffix
142
+ // match covers both the append and change-element popups
143
+ // (append-* / replace-with-parallel-gateway).
144
+ this.parallelGatewayOption = page
145
+ .locator('.djs-popup-results [data-id$="parallel-gateway"]')
146
+ .or(page.getByRole('option', { name: /parallel gateway/i }))
147
+ .first();
127
148
  this.dialog = page.getByRole('dialog');
128
149
  this.startInstanceMainButton = page.getByRole('button', {
129
150
  name: 'Run',
@@ -145,6 +166,10 @@ class ModelerCreatePage {
145
166
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
146
167
  this.renameDiagramNameButton = page.getByText('Rename');
147
168
  this.diagramNameInput = page.locator('[data-test="editable-input"]');
169
+ this.moreActionsButton = page.getByRole('button', { name: 'More actions' });
170
+ this.renameMenuItem = page.getByRole('menuitem', { name: 'Rename' });
171
+ this.renameInput = page.locator('[data-test="rename-input"]');
172
+ this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
148
173
  this.variableInput = page.locator('[id="variables-json"]');
149
174
  this.businessIdInput = page.locator('[id^="business-id-input"]');
150
175
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
@@ -529,25 +554,75 @@ class ModelerCreatePage {
529
554
  }
530
555
  await this.appendViaPopup('append-task', ['append-tasks'], 'Task');
531
556
  }
532
- async hoverOnLocator(locator, selector = '.djs-create-pad-entry') {
533
- const canvasState = await Promise.race([
557
+ // The create pad is only mounted while the append affordance is engaged. An
558
+ // element without an outgoing flow gets the pad on selection, but one that
559
+ // already has an outgoing flow (the first gateway of a parallel branch) only
560
+ // gets the overlay indicator ("+" beside the shape), and the pad appears once
561
+ // that indicator is hovered. The indicator carries its element id in
562
+ // `data-element`, so key the hover on the current selection: hovering another
563
+ // element's indicator would open its pad and append to the wrong shape.
564
+ async padHoverTargets(locator, selector) {
565
+ if (!selector.includes('create-pad')) {
566
+ return [locator];
567
+ }
568
+ const selectedElementId = await this.page
569
+ .locator('.djs-element.selected')
570
+ .first()
571
+ .getAttribute('data-element-id', { timeout: 5000 })
572
+ .catch(() => null);
573
+ if (!selectedElementId) {
574
+ return [locator];
575
+ }
576
+ return [
534
577
  this.page
535
- .waitForSelector(selector, { state: 'visible' })
536
- .then(() => 'pad')
537
- .catch(() => {
538
- throw new Error('Canvas not visible');
539
- }),
540
- locator
541
- .waitFor({ state: 'visible' })
542
- .then(() => 'hover')
543
- .catch(() => {
544
- throw new Error('Locator not visible');
545
- }),
546
- ]);
547
- if (canvasState === 'hover') {
548
- await locator.hover({ timeout: 5000 });
549
- await this.page.waitForSelector(selector, { state: 'visible' });
578
+ .locator(`.djs-append-indicator[data-element="${selectedElementId}"]`)
579
+ .first(),
580
+ locator,
581
+ ];
582
+ }
583
+ async hoverOnLocator(locator, selector = '.djs-create-pad-entry') {
584
+ // Only visible entries count: both `page.waitForSelector` and `.first()`
585
+ // resolve a single DOM match, so an entry belonging to another element's
586
+ // closed pad would never satisfy `visible`.
587
+ const padEntry = this.page.locator(`${selector}:visible`).first();
588
+ let lastError = null;
589
+ // Promise.race leaves the losing branch pending, so when that branch later
590
+ // rejects nobody awaits it: the unhandled rejection fails the whole test
591
+ // with the loser's message ("Canvas not visible") before the callers'
592
+ // retries can recover. Poll the two states sequentially instead.
593
+ for (let attempt = 1; attempt <= 3; attempt++) {
594
+ if (await padEntry.isVisible()) {
595
+ return;
596
+ }
597
+ // bpmn-js opens the pad on mouseover, which does not re-fire while the
598
+ // pointer already rests on the trigger, so park it off-element on retries.
599
+ if (attempt > 1) {
600
+ await this.page.mouse.move(0, 0);
601
+ }
602
+ for (const target of await this.padHoverTargets(locator, selector)) {
603
+ const targetVisible = await target
604
+ .waitFor({ state: 'visible', timeout: 5000 })
605
+ .then(() => true)
606
+ .catch(() => false);
607
+ if (!targetVisible) {
608
+ continue;
609
+ }
610
+ // force: the append affordance of an element that already has an
611
+ // outgoing flow is drawn on top of that sequence flow, so the
612
+ // hit-target check resolves to the connection instead of the trigger.
613
+ await target.hover({ timeout: 10000, force: true }).catch((error) => {
614
+ lastError = error;
615
+ });
616
+ const padOpen = await padEntry
617
+ .waitFor({ state: 'visible', timeout: 10000 })
618
+ .then(() => true)
619
+ .catch(() => false);
620
+ if (padOpen) {
621
+ return;
622
+ }
623
+ }
550
624
  }
625
+ throw new Error(`Canvas pad entries "${selector}" did not open${lastError ? `: ${lastError}` : ''}`);
551
626
  }
552
627
  async clickAppendGatewayButton() {
553
628
  await this.appendGatewayButton.click({ timeout: 90000 });
@@ -725,15 +800,51 @@ class ModelerCreatePage {
725
800
  async fillJobTypeInput(name) {
726
801
  await this.jobTypeInput.fill(name);
727
802
  }
803
+ // Carbon renamed a diagram in three steps: click the diagram crumb, click
804
+ // "Rename" in the menu it opens, then type into the inline input. The Design
805
+ // System shell has no breadcrumb-diagram hook and drives the whole rename
806
+ // from enterDiagramName()'s dialog path, so these two steps have nothing to
807
+ // do there -- skip them instead of timing out on a hook that is gone.
728
808
  async clickDiagramBreadcrumb() {
809
+ if (!(await this.legacyBreadcrumbPresent())) {
810
+ return;
811
+ }
729
812
  await this.diagramBreadcrumb.click({ timeout: 60000 });
730
813
  }
731
814
  async clickEditDiagramNameButton() {
815
+ if (!(await this.legacyBreadcrumbPresent())) {
816
+ return;
817
+ }
732
818
  await this.renameDiagramNameButton.click();
733
819
  }
820
+ async legacyBreadcrumbPresent() {
821
+ return this.diagramBreadcrumb
822
+ .waitFor({ state: 'visible', timeout: 5000 })
823
+ .then(() => true)
824
+ .catch(() => false);
825
+ }
826
+ // The Carbon editor renames the diagram through an inline editable input. The
827
+ // Design System editor has none: renaming moved to the breadcrumb's "More
828
+ // actions" > "Rename" dialog. Use whichever the deployed shell offers.
734
829
  async enterDiagramName(name) {
735
- await this.diagramNameInput.fill(name, { timeout: 60000 });
736
- await this.diagramNameInput.press('Enter', { timeout: 60000 });
830
+ await this.diagramNameInput
831
+ .or(this.moreActionsButton)
832
+ .first()
833
+ .waitFor({ state: 'visible', timeout: 60000 });
834
+ if (await this.diagramNameInput.isVisible()) {
835
+ await this.diagramNameInput.fill(name, { timeout: 60000 });
836
+ await this.diagramNameInput.press('Enter', { timeout: 60000 });
837
+ return;
838
+ }
839
+ // Two "More actions" buttons render here -- the project crumb's and the
840
+ // diagram's. first() is the diagram's; the Rename click fails loudly
841
+ // rather than renaming the wrong entity if that order ever changes.
842
+ await this.moreActionsButton.first().click();
843
+ await this.renameMenuItem.click();
844
+ await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
845
+ await this.renameInput.fill(name);
846
+ await this.renameSubmitButton.click();
847
+ await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
737
848
  }
738
849
  async clickVariableInput() {
739
850
  await this.variableInput.click();
@@ -1092,16 +1203,9 @@ class ModelerCreatePage {
1092
1203
  }
1093
1204
  }
1094
1205
  async clickAppendElementButton() {
1095
- for (let attempt = 1; attempt <= 3; attempt++) {
1096
- try {
1097
- await this.hoverOnLocator(this.appendPreButton);
1098
- return; // Exit loop if successful
1099
- }
1100
- catch (error) {
1101
- if (attempt === 3)
1102
- throw error; // Rethrow after 3 failed attempts
1103
- }
1104
- }
1206
+ // hoverOnLocator already retries the hover; a second retry layer here only
1207
+ // multiplied the worst-case wait.
1208
+ await this.hoverOnLocator(this.appendPreButton);
1105
1209
  }
1106
1210
  async clickParallelGatewayOption() {
1107
1211
  await this.parallelGatewayOption.click({ timeout: 90000 });
@@ -5,6 +5,10 @@ declare class ModelerHomePage {
5
5
  readonly modelerPageBanner: Locator;
6
6
  readonly createNewProjectButton: Locator;
7
7
  readonly projectNameInput: Locator;
8
+ readonly moreActionsButton: Locator;
9
+ readonly renameMenuItem: Locator;
10
+ readonly renameInput: Locator;
11
+ readonly renameSubmitButton: Locator;
8
12
  readonly chooseBpmnTemplateButton: Locator;
9
13
  readonly diagramTypeDropdown: Locator;
10
14
  readonly bpmnTemplateOption: Locator;
@@ -37,6 +41,7 @@ declare class ModelerHomePage {
37
41
  clickBpmnTemplateOption(): Promise<void>;
38
42
  clickFormOption(): Promise<void>;
39
43
  enterFormName(name: string): Promise<void>;
44
+ private renameViaMoreActions;
40
45
  clickProjectBreadcrumb(): Promise<void>;
41
46
  clickOpenOrganizationsButton(): Promise<void>;
42
47
  clickManageButton(): Promise<void>;
@@ -10,6 +10,10 @@ class ModelerHomePage {
10
10
  modelerPageBanner;
11
11
  createNewProjectButton;
12
12
  projectNameInput;
13
+ moreActionsButton;
14
+ renameMenuItem;
15
+ renameInput;
16
+ renameSubmitButton;
13
17
  chooseBpmnTemplateButton;
14
18
  diagramTypeDropdown;
15
19
  bpmnTemplateOption;
@@ -31,9 +35,13 @@ class ModelerHomePage {
31
35
  modalCloseButton;
32
36
  constructor(page) {
33
37
  this.page = page;
38
+ // Matches both Modeler shells -- see NavigationPage.modelerPageBanner for
39
+ // why the Design System branch excludes the /login href.
34
40
  this.modelerPageBanner = page
35
- .locator('a')
36
- .filter({ hasText: 'Camunda logoModeler' });
41
+ .getByRole('link', { name: 'Camunda Modeler home' })
42
+ .and(page.locator(':not([href$="/login"])'))
43
+ .or(page.locator('a').filter({ hasText: 'Camunda logoModeler' }))
44
+ .first();
37
45
  this.createNewProjectButton = page
38
46
  .getByRole('button', {
39
47
  name: 'Create new project',
@@ -42,6 +50,10 @@ class ModelerHomePage {
42
50
  name: 'New project',
43
51
  }));
44
52
  this.projectNameInput = page.locator('[data-test="editable-input"]');
53
+ this.moreActionsButton = page.getByRole('button', { name: 'More actions' });
54
+ this.renameMenuItem = page.getByRole('menuitem', { name: 'Rename' });
55
+ this.renameInput = page.locator('[data-test="rename-input"]');
56
+ this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
45
57
  this.chooseBpmnTemplateButton = page.getByRole('button', {
46
58
  name: 'Choose BPMN template',
47
59
  });
@@ -52,7 +64,17 @@ class ModelerHomePage {
52
64
  this.formTemplateOption = page
53
65
  .locator('[data-test="create-form"]')
54
66
  .getByText('Form');
55
- this.projectBreadcrumb = page.locator('[data-test="breadcrumb-project"]');
67
+ // The Design System shell drops the breadcrumb-project hook. Its context
68
+ // breadcrumb is org / project / entity, so the project crumb is the second
69
+ // item -- scoped to the breadcrumb nav, never a bare nth() on the page.
70
+ this.projectBreadcrumb = page
71
+ .locator('[data-test="breadcrumb-project"]')
72
+ .or(page
73
+ .locator('nav[aria-label="Context navigation"] [data-slot="nav-breadcrumb-item"]')
74
+ .nth(1)
75
+ .getByRole('button')
76
+ .first())
77
+ .first();
56
78
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
57
79
  this.manageButton = page.getByRole('button', { name: 'Manage' });
58
80
  this.crossComponentProjectFolder = page
@@ -60,7 +82,12 @@ class ModelerHomePage {
60
82
  exact: true,
61
83
  })
62
84
  .first();
63
- this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
85
+ // The Design System shell drops the breadcrumb-home hook and labels the
86
+ // same crumb "My organisation".
87
+ this.homeBreadcrumb = page
88
+ .locator('[data-test="breadcrumb-home"]')
89
+ .or(page.getByRole('button', { name: 'My organisation' }))
90
+ .first();
64
91
  this.formNameInput = page.locator('[data-test="editable-input"]');
65
92
  this.rows = page.getByRole('row');
66
93
  this.licenseKeyTagNonProduction = page
@@ -90,11 +117,24 @@ class ModelerHomePage {
90
117
  .catch(() => { });
91
118
  await this.createNewProjectButton.click();
92
119
  }
120
+ // The Carbon shell renames a project through an inline editable input on the
121
+ // project page. The Design System shell renders the name as a plain span and
122
+ // moved renaming behind "More actions" > "Rename", which opens a dialog. Wait
123
+ // for whichever affordance the deployed shell offers, then use that one --
124
+ // both images are still in circulation.
93
125
  async enterNewProjectName(name) {
94
- await (0, test_1.expect)(this.projectNameInput).toBeVisible({ timeout: 10000 });
95
- await this.projectNameInput.click({ timeout: 60000 });
96
- await this.projectNameInput.fill(name);
97
- await this.projectNameInput.press('Enter');
126
+ await this.projectNameInput
127
+ .or(this.moreActionsButton)
128
+ .first()
129
+ .waitFor({ state: 'visible', timeout: 30000 });
130
+ // Inline first: when the Carbon shell renders it, it is the whole flow.
131
+ if (await this.projectNameInput.isVisible()) {
132
+ await this.projectNameInput.click({ timeout: 60000 });
133
+ await this.projectNameInput.fill(name);
134
+ await this.projectNameInput.press('Enter');
135
+ return;
136
+ }
137
+ await this.renameViaMoreActions(name);
98
138
  }
99
139
  // Wait for the Modeler project workspace to become interactive. The caller
100
140
  // arrives here via goToModeler(), which has already loaded and authenticated
@@ -242,11 +282,32 @@ class ModelerHomePage {
242
282
  async clickFormOption() {
243
283
  await this.formTemplateOption.click();
244
284
  }
285
+ // Same shell split as enterNewProjectName: the Design System form editor has
286
+ // no inline editable input and renames through the breadcrumb's "More
287
+ // actions" > "Rename" dialog.
245
288
  async enterFormName(name) {
246
- await (0, test_1.expect)(this.formNameInput).toBeVisible({ timeout: 10000 });
247
- await this.formNameInput.click({ timeout: 60000 });
248
- await this.formNameInput.fill(name);
249
- await this.formNameInput.press('Enter');
289
+ await this.formNameInput
290
+ .or(this.moreActionsButton)
291
+ .first()
292
+ .waitFor({ state: 'visible', timeout: 30000 });
293
+ if (await this.formNameInput.isVisible()) {
294
+ await this.formNameInput.click({ timeout: 60000 });
295
+ await this.formNameInput.fill(name);
296
+ await this.formNameInput.press('Enter');
297
+ return;
298
+ }
299
+ await this.renameViaMoreActions(name);
300
+ }
301
+ // The editors show two "More actions" buttons -- one for the project crumb
302
+ // and one for the entity being edited. first() is the entity's; the Rename
303
+ // click fails loudly rather than renaming the wrong thing if that changes.
304
+ async renameViaMoreActions(name) {
305
+ await this.moreActionsButton.first().click();
306
+ await this.renameMenuItem.click();
307
+ await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
308
+ await this.renameInput.fill(name);
309
+ await this.renameSubmitButton.click();
310
+ await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
250
311
  }
251
312
  async clickProjectBreadcrumb() {
252
313
  await this.projectBreadcrumb.click({ timeout: 60000 });
@@ -35,6 +35,10 @@ declare class NavigationPage {
35
35
  username?: string;
36
36
  password?: string;
37
37
  }): Promise<void>;
38
+ goToModelerWithoutAccess({ username, password, }?: {
39
+ username?: string;
40
+ password?: string;
41
+ }): Promise<void>;
38
42
  goToKeycloak(maxRetries?: number): Promise<void>;
39
43
  goToOCAdmin(sleepTimeout?: number, credentials?: {
40
44
  username: string;
@@ -35,9 +35,19 @@ class NavigationPage {
35
35
  .getByRole('link', { name: 'Camunda logo Operate' })
36
36
  .or(page.getByRole('link', { name: 'Camunda Operate', exact: true }))
37
37
  .first();
38
+ // Web Modeler renders the Design System nav shell (NavShellDs) whenever
39
+ // `design-system-enabled` is on -- its home link is
40
+ // aria-label="Camunda Modeler home" wrapping an aria-hidden logo, so the
41
+ // legacy shell's "Camunda logoModeler" text no longer exists. The
42
+ // unauthenticated shell reuses that aria-label pointing at /login, and
43
+ // waitForBanner() gates only on visibility, so exclude that href here:
44
+ // callers use this banner as the logged-in gate.
45
+ // Match either shell so the banner resolves with the flag on or off.
38
46
  this.modelerPageBanner = page
39
- .locator('a')
40
- .filter({ hasText: 'Camunda logoModeler' });
47
+ .getByRole('link', { name: 'Camunda Modeler home' })
48
+ .and(page.locator(':not([href$="/login"])'))
49
+ .or(page.locator('a').filter({ hasText: 'Camunda logoModeler' }))
50
+ .first();
41
51
  this.tasklistPageBanner = page.getByRole('link', {
42
52
  name: 'Camunda logo Tasklist',
43
53
  });
@@ -258,6 +268,27 @@ class NavigationPage {
258
268
  }
259
269
  await new LoginPage_1.LoginPage(this.page).login(username, password);
260
270
  }
271
+ // Same reason as `goToOptimizeWithoutAccess`: a user whose Web Modeler role
272
+ // was removed never reaches a page carrying the Modeler banner — the OAuth
273
+ // round trip ends back on the Keycloak authorize URL — so `goTo` burns all 8
274
+ // attempts (~570s) and throws instead of letting the caller assert the denial.
275
+ // Navigate and log in only; the caller asserts what the denied page shows.
276
+ async goToModelerWithoutAccess({ username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}) {
277
+ await this.page.goto(MODELER_CONTEXT_PATH, {
278
+ timeout: 60000,
279
+ waitUntil: 'domcontentloaded',
280
+ });
281
+ let gatewayReloads = 0;
282
+ while (gatewayReloads < 8 && (await this.isGatewayErrorPage())) {
283
+ await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
284
+ await this.page.goto(MODELER_CONTEXT_PATH, {
285
+ timeout: 60000,
286
+ waitUntil: 'domcontentloaded',
287
+ });
288
+ gatewayReloads++;
289
+ }
290
+ await new LoginPage_1.LoginPage(this.page).login(username, password);
291
+ }
261
292
  async goToKeycloak(maxRetries = 5) {
262
293
  const keycloakUrl = process.env.KEYCLOAK_URL
263
294
  ? `${process.env.KEYCLOAK_URL}/auth`
@@ -30,9 +30,15 @@ class PlayPage {
30
30
  constructor(page) {
31
31
  this.page = page;
32
32
  this.dialog = page.getByRole('dialog');
33
+ // Test mode labels this button with a <span class="sr-only">Complete job</span>
34
+ // rather than an aria-label, so getByLabel cannot see it -- its data-testid
35
+ // is element-scoped ("<elementId>-Complete job"). getByRole reads the
36
+ // accessible name, which the sr-only text and the legacy aria-label both
37
+ // supply, so this resolves on either shell.
33
38
  this.completeJobButton = page
34
39
  .getByTestId('diagram')
35
- .getByLabel('Complete job');
40
+ .getByRole('button', { name: 'Complete job' })
41
+ .first();
36
42
  // Test mode renamed "scenario" to "test case" across this panel
37
43
  // (camunda-hub a90fd06). Each label below accepts both wordings so the
38
44
  // page object works either side of the rename.
@@ -42,8 +48,18 @@ class PlayPage {
42
48
  this.runAllScenariosButton = page.getByRole('button', {
43
49
  name: /^Run all (scenarios|test cases)$/i,
44
50
  });
51
+ // The Design System confirmation is a Radix AlertDialog, so its role is
52
+ // "alertdialog" -- a distinct ARIA role that getByRole('dialog') does not
53
+ // match. Accept either role and identify it by its heading rather than an
54
+ // accessible name, which the DS dialog does not always expose.
45
55
  this.confirmDeleteScenarioButton = page
46
- .getByRole('dialog', { name: /^Delete (scenario|test case)$/i })
56
+ .getByRole('alertdialog')
57
+ .or(page.getByRole('dialog'))
58
+ .filter({
59
+ has: page.getByRole('heading', {
60
+ name: /^Delete (scenario|test case)$/i,
61
+ }),
62
+ })
47
63
  .getByRole('button', { name: 'Delete', exact: true });
48
64
  // After saving a scenario, the Scenarios panel is in detail-view mode.
49
65
  // To get back to the list (where "Run all scenarios" lives in
@@ -82,9 +98,14 @@ class PlayPage {
82
98
  // non-actionable toasts (see Notification.tsx in @camunda/c4-notifications).
83
99
  // Anchor on the accessible role instead of the Carbon CSS class so the
84
100
  // locator is resilient to Carbon version bumps.
101
+ // The Design System toast stack is an <ol> of plain <li> items with no
102
+ // role="alert" (the only alert role in Test mode belongs to the Copilot
103
+ // panel), so match the text as well as the legacy alert role.
85
104
  this.scenarioSavedToast = page
86
105
  .getByRole('alert')
87
- .filter({ hasText: /(Scenario|Test case) saved/i });
106
+ .filter({ hasText: /(Scenario|Test case) saved/i })
107
+ .or(page.getByText(/(Scenario|Test case) saved/i))
108
+ .first();
88
109
  this.loadingInstanceDetailsText = this.page.getByText('Loading instance details...');
89
110
  this.retryButton = this.page.getByRole('button', { name: 'Retry' });
90
111
  // New Play UI (8.10+): "Configure scenario" opens an input panel with a plain "Start" button.
@@ -46,10 +46,7 @@ SM_8_10_1.test.describe('HTO User Flow Tests', () => {
46
46
  await operateProcessInstancePage.completedIconAssertion();
47
47
  });
48
48
  });
49
- // Temporarily skipped pending fix PR #3144:
50
- // https://github.com/camunda/c8-cross-component-e2e-tests/pull/3144
51
- // Re-enable in the same change that lands #3144.
52
- SM_8_10_1.test.skip('Zeebe User Task With Priority', async ({ page, modelerHomePage, navigationPage, modelerCreatePage, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskDetailsPage, taskPanelPage, }) => {
49
+ (0, SM_8_10_1.test)('Zeebe User Task With Priority', async ({ page, modelerHomePage, navigationPage, modelerCreatePage, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskDetailsPage, taskPanelPage, }) => {
53
50
  SM_8_10_1.test.slow();
54
51
  const randomName = await (0, _setup_1.generateRandomStringAsync)(3);
55
52
  const processName = 'Zeebe_User_Task_Process_With_Priority' + randomName;
@@ -61,7 +61,10 @@ SM_8_10_1.test.describe('Identity User Flow Tests', () => {
61
61
  await (0, sleep_1.sleep)(40000);
62
62
  });
63
63
  await SM_8_10_1.test.step('Ensure Modeler is not Accessible', async () => {
64
- await navigationPage.goToModeler(undefined, credentials);
64
+ // goToModeler asserts the Modeler banner, which is exactly what a user
65
+ // without the Hub role never gets: it exhausts its 8 login attempts on
66
+ // the Keycloak authorize URL and throws before this assertion can run.
67
+ await navigationPage.goToModelerWithoutAccess(credentials);
65
68
  await (0, test_1.expect)(modelerHomePage.modelerPageBanner).not.toBeVisible({
66
69
  timeout: 60000,
67
70
  });
@@ -115,8 +115,8 @@ if (process.env.IS_MIGRATION === 'true') {
115
115
  'ManagementIdentity',
116
116
  'Optimize',
117
117
  'Orchestration',
118
- 'Web Modeler',
119
- 'Web Modeler Admin',
118
+ 'Hub',
119
+ 'Hub Admin',
120
120
  ]);
121
121
  await managementIdentityPage.clickUsersLink();
122
122
  await identityUsersPage.assertUserIsAssignedCorrectRole('bart', [
@@ -124,8 +124,8 @@ if (process.env.IS_MIGRATION === 'true') {
124
124
  'ManagementIdentity',
125
125
  'Optimize',
126
126
  'Orchestration',
127
- 'Web Modeler',
128
- 'Web Modeler Admin',
127
+ 'Hub',
128
+ 'Hub Admin',
129
129
  ]);
130
130
  await managementIdentityPage.clickUsersLink();
131
131
  await identityUsersPage.assertUserIsAssignedCorrectRole('lisa', [
@@ -133,8 +133,8 @@ if (process.env.IS_MIGRATION === 'true') {
133
133
  'ManagementIdentity',
134
134
  'Optimize',
135
135
  'Orchestration',
136
- 'Web Modeler',
137
- 'Web Modeler Admin',
136
+ 'Hub',
137
+ 'Hub Admin',
138
138
  ]);
139
139
  });
140
140
  (0, SM_8_10_1.test)('Assert Demo User Can Deploy Processes', async ({ page, modelerHomePage, navigationPage, modelerCreatePage, operateHomePage, operateProcessesPage, operateProcessInstancePage, taskDetailsPage, taskPanelPage, }) => {
@@ -41,10 +41,7 @@ SM_8_10_1.test.describe('Deploy and run a process in Play', () => {
41
41
  await playPage.waitForProcessToBeCompleted();
42
42
  });
43
43
  });
44
- // Temporarily skipped pending fix PR #3144:
45
- // https://github.com/camunda/c8-cross-component-e2e-tests/pull/3144
46
- // Re-enable in the same change that lands #3144.
47
- SM_8_10_1.test.skip('Create,update and delete test scenarios in play', async ({ modelerHomePage, modelerCreatePage, playPage, }) => {
44
+ (0, SM_8_10_1.test)('Create,update and delete test scenarios in play', async ({ modelerHomePage, modelerCreatePage, playPage, }) => {
48
45
  SM_8_10_1.test.slow();
49
46
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
50
47
  const processName = 'Play_Test_Process' + randomString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1005",
3
+ "version": "0.0.1006",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",