@camunda/e2e-test-suite 0.0.683 → 0.0.685

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.
@@ -1,4 +1,6 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
+ import { LoginPage } from './LoginPage';
3
+ import { AppsPage } from './AppsPage';
2
4
  declare class ModelerHomePage {
3
5
  private defaultFolderName;
4
6
  private page;
@@ -51,5 +53,19 @@ declare class ModelerHomePage {
51
53
  clickUploadFilesButton(): Promise<void>;
52
54
  clickMessageBanner(): Promise<void>;
53
55
  assertFormBreadcrumbVisible(formName: string): Promise<void>;
56
+ /**
57
+ * Handles navigation after clicking an invitation link in email.
58
+ * The invitation link may auto-authenticate the second user and land them in
59
+ * the correct org's Modeler. If the Modeler banner is visible, navigate to
60
+ * the Modeler home within the same org context — clearing cookies and
61
+ * re-logging in via the app switcher lands in the user's personal org where
62
+ * the shared project is not listed.
63
+ * If auto-authentication did not occur (page ended up on Auth0), falls back
64
+ * to the explicit login path.
65
+ */
66
+ navigateAfterInvitationLink(loginPage: LoginPage, appsPage: AppsPage, credentials: {
67
+ username: string;
68
+ password: string;
69
+ }): Promise<void>;
54
70
  }
55
71
  export { ModelerHomePage };
@@ -4,6 +4,7 @@ exports.ModelerHomePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
7
+ const UtilitiesPage_1 = require("./UtilitiesPage");
7
8
  class ModelerHomePage {
8
9
  defaultFolderName = 'Cross Component Test Project';
9
10
  page;
@@ -273,5 +274,36 @@ class ModelerHomePage {
273
274
  timeout: 120000,
274
275
  });
275
276
  }
277
+ /**
278
+ * Handles navigation after clicking an invitation link in email.
279
+ * The invitation link may auto-authenticate the second user and land them in
280
+ * the correct org's Modeler. If the Modeler banner is visible, navigate to
281
+ * the Modeler home within the same org context — clearing cookies and
282
+ * re-logging in via the app switcher lands in the user's personal org where
283
+ * the shared project is not listed.
284
+ * If auto-authentication did not occur (page ended up on Auth0), falls back
285
+ * to the explicit login path.
286
+ */
287
+ async navigateAfterInvitationLink(loginPage, appsPage, credentials) {
288
+ const autoAuthenticated = await this.modelerPageBanner
289
+ .waitFor({ state: 'visible', timeout: 15000 })
290
+ .then(() => true)
291
+ .catch(() => false);
292
+ if (autoAuthenticated) {
293
+ try {
294
+ await this.clickHomeBreadcrumb();
295
+ }
296
+ catch {
297
+ // Already on the Modeler home page
298
+ }
299
+ }
300
+ else {
301
+ await this.page.context().clearCookies();
302
+ await (0, UtilitiesPage_1.loginWithRetry)(this.page, loginPage, credentials, 1000, 3, true);
303
+ await appsPage.clickCamundaApps();
304
+ await appsPage.clickModeler();
305
+ }
306
+ await (0, test_1.expect)(this.modelerPageBanner).toBeVisible({ timeout: 120000 });
307
+ }
276
308
  }
277
309
  exports.ModelerHomePage = ModelerHomePage;
@@ -7,7 +7,6 @@ declare class ConnectorMarketplacePage {
7
7
  readonly closeButton: Locator;
8
8
  readonly replaceResourceButton: Locator;
9
9
  readonly addToProjectButton: Locator;
10
- readonly saveAsCopyButton: Locator;
11
10
  readonly cancelButton: Locator;
12
11
  constructor(page: Page);
13
12
  clickSearchForConnectorTextbox(): Promise<void>;
@@ -9,7 +9,6 @@ class ConnectorMarketplacePage {
9
9
  closeButton;
10
10
  replaceResourceButton;
11
11
  addToProjectButton;
12
- saveAsCopyButton;
13
12
  cancelButton;
14
13
  constructor(page) {
15
14
  this.page = page;
@@ -22,21 +21,9 @@ class ConnectorMarketplacePage {
22
21
  this.replaceResourceButton = page.getByRole('button', {
23
22
  name: 'Replace resource',
24
23
  });
25
- // Camunda Hub's ImportModal renders one of three primary buttons depending
26
- // on state. Source: camunda-hub
27
- // frontend/apps/hub/src/components/ImportModal/get-action-buttons-props.js
28
- // - "Add to project" for a brand-new resource (showPublish)
29
- // - "Replace resource" when a duplicate exists and replace is allowed
30
- // - "Save as copy" when a duplicate exists but replace is NOT
31
- // allowed (e.g. an existing project template
32
- // conflict — user must save the import under a
33
- // new ID to keep both)
34
24
  this.addToProjectButton = page.getByRole('button', {
35
25
  name: 'Add to project',
36
26
  });
37
- this.saveAsCopyButton = page.getByRole('button', {
38
- name: 'Save as copy',
39
- });
40
27
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
41
28
  }
42
29
  async clickSearchForConnectorTextbox() {
@@ -59,35 +46,31 @@ class ConnectorMarketplacePage {
59
46
  }
60
47
  async downloadConnectorToProject() {
61
48
  await this.clickDownloadToProjectButton();
62
- // After "Download to project", the Hub ImportModal opens. Per
63
- // camunda-hub
64
- // frontend/apps/hub/src/components/ImportModal/get-action-buttons-props.js
65
- // the primary button is one of:
66
- // - "Add to project" fresh import — we want to confirm
67
- // - "Replace resource" duplicate exists, replace allowed
68
- // - "Save as copy" duplicate exists, replace NOT allowed
69
- //
70
- // Strategy:
71
- // - If "Add to project" is visible, click it: the connector wasn't
72
- // in the project, this commits the fresh import.
73
- // - Otherwise the connector is already in the project (conflict).
74
- // Cancel the modal: Save as copy would create a duplicate under a
75
- // different name (and the test then can't find "Worldwide Public
76
- // Holiday"), and Replace risks other tests' state. The connector
77
- // already exists — clickPublicHolidayConnectorOption just needs
78
- // to find it in the change-element popup, which it does via a
79
- // state reset.
80
- if (await this.addToProjectButton
81
- .isVisible({ timeout: 20000 })
82
- .catch(() => false)) {
83
- await this.addToProjectButton.click({ timeout: 30000 });
84
- return;
49
+ let imported = false;
50
+ try {
51
+ await Promise.race([
52
+ this.addToProjectButton.click({ timeout: 20000 }),
53
+ this.replaceResourceButton.click({ timeout: 20000 }),
54
+ ]);
55
+ imported = true;
56
+ }
57
+ catch (e) {
58
+ if (await this.cancelButton.isVisible({ timeout: 3000 }).catch(() => false)) {
59
+ await this.cancelButton.click({ timeout: 10000 });
60
+ }
85
61
  }
86
- if (await this.cancelButton.isVisible({ timeout: 5000 }).catch(() => false)) {
87
- await this.cancelButton.click({ timeout: 30000 });
62
+ // After a successful import, wait for the snackbar (confirms the connector
63
+ // template was received by the modeler) then close the marketplace panel so
64
+ // the diagram canvas is unblocked before the caller tries to select the
65
+ // newly installed connector.
66
+ if (imported) {
67
+ await this.snackbar
68
+ .waitFor({ state: 'visible', timeout: 60000 })
69
+ .catch(() => { });
70
+ if (await this.closeButton.isVisible({ timeout: 5000 }).catch(() => false)) {
71
+ await this.closeButton.click({ timeout: 10000 });
72
+ }
88
73
  }
89
- // If neither button is visible the import dialog never opened; nothing
90
- // to dismiss and downstream state-reset will recover.
91
74
  }
92
75
  }
93
76
  exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
@@ -4,6 +4,7 @@ exports.ModelerCreatePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
7
+ const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
8
  class ModelerCreatePage {
8
9
  page;
9
10
  generalPanel;
@@ -186,17 +187,9 @@ class ModelerCreatePage {
186
187
  this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
187
188
  this.payloadInput = page.locator('[class="fjs-input"]');
188
189
  this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
189
- // Match the listitem rendered by bpmn-js's change-element popup. The
190
- // previous getByText-inside-[data-test="modeler"] resolver finds the
191
- // text node anywhere in the modeler, including off-screen positions in
192
- // the virtualized list — which never satisfies toBeVisible. Targeting
193
- // the listitem by accessible name mirrors the SM-8.8 pattern
194
- // (pages/SM-8.8/ModelerCreatePage.ts:165) and works whether the
195
- // connector is at the top of the list or filtered via search.
196
- this.publicHolidayConnectorOption = page.getByRole('listitem', {
197
- name: 'Worldwide Public Holiday',
198
- exact: false,
199
- });
190
+ this.publicHolidayConnectorOption = page
191
+ .locator('[data-test="modeler"]')
192
+ .getByText('Worldwide Public Holiday');
200
193
  this.publicHolidayYearOption = page.getByLabel('Year');
201
194
  this.publicHolidayCountryCodeOption = page.getByLabel('Countrycode');
202
195
  this.implementationSection = page.locator('[data-group-id="group-userTaskImplementation"]');
@@ -784,77 +777,49 @@ class ModelerCreatePage {
784
777
  throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
785
778
  }
786
779
  async clickPublicHolidayConnectorOption() {
787
- // Port of the 8.7 state-reset approach
788
- // (pages/8.7/ModelerCreatePage.ts). After the test's marketplace flow
789
- // completes, the connector is on the project (whether freshly added or
790
- // already there from a prior run) but the modeler may be in any of
791
- // several states: change-element popup closed, residual import modal
792
- // up, virtualized list with the connector off-screen. Instead of
793
- // trying to manage those states, dismiss any overlay, reload the page,
794
- // then re-open the change-element popup from a clean state. The
795
- // downloaded connector persists across reloads (server-side project
796
- // state). Filter the popup so the listitem comes into view —
797
- // `.djs-popup-search input` is the popup's own search, same selector
798
- // the camunda-hub e2e tests use under
799
- // e2e/cypress/e2e/bpmn/browse-market-place/.
800
780
  const maxRetries = 4;
801
- const setupModelerState = async () => {
802
- const onModeler = await this.page
803
- .locator('[data-test="modeler"]')
804
- .waitFor({ state: 'visible', timeout: 5000 })
805
- .then(() => true)
806
- .catch(() => false);
807
- if (!onModeler) {
808
- await this.page.keyboard.press('Escape');
809
- await this.page.waitForTimeout(2000);
810
- const afterEscape = await this.page
811
- .locator('[data-test="modeler"]')
812
- .waitFor({ state: 'visible', timeout: 3000 })
813
- .then(() => true)
814
- .catch(() => false);
815
- if (!afterEscape) {
816
- await this.page.goBack();
817
- await this.page.waitForTimeout(5000);
818
- }
819
- await this.page.reload();
820
- await this.page.waitForTimeout(5000);
821
- }
822
- await this.clickCanvas();
823
- await this.secondElement.click({ timeout: 60000 });
824
- await this.clickChangeTypeButton();
825
- // Wait for the change-element popup to be fully loaded.
826
- await (0, test_1.expect)(this.marketPlaceButton).toBeVisible({ timeout: 60000 });
827
- // Filter the change-element popup so the connector listitem comes
828
- // into the rendered slice of the virtualized list.
829
- const changeElementSearch = this.page.locator('.djs-popup-search input');
830
- if (await changeElementSearch.isVisible({ timeout: 5000 }).catch(() => false)) {
831
- await changeElementSearch.fill('Worldwide Public Holiday');
832
- }
833
- };
834
781
  for (let retries = 0; retries < maxRetries; retries++) {
835
782
  try {
836
- await setupModelerState();
837
- await this.publicHolidayConnectorOption.click({ timeout: 120000 });
783
+ if (retries === 2) {
784
+ // Re-download connector on second retry in case the first download failed
785
+ await this.clickMarketPlaceButton();
786
+ const connectorMarketplacePage = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
787
+ await connectorMarketplacePage.clickSearchForConnectorTextbox();
788
+ await connectorMarketplacePage.fillSearchForConnectorTextbox('Public Holiday Connector');
789
+ await (0, sleep_1.sleep)(10000);
790
+ await connectorMarketplacePage.downloadConnectorToProject();
791
+ await this.clickCanvas();
792
+ await this.secondElement.click({ timeout: 60000 });
793
+ await this.clickChangeTypeButton();
794
+ }
795
+ // Filter the virtualized change-element popup so the option scrolls into view
796
+ const changeElementSearch = this.page.locator('.djs-popup-search input');
797
+ if (await changeElementSearch
798
+ .isVisible({ timeout: 5000 })
799
+ .catch(() => false)) {
800
+ await changeElementSearch.pressSequentially('Worldwide Public Holiday', {
801
+ delay: 50,
802
+ });
803
+ }
804
+ await this.publicHolidayConnectorOption.waitFor({
805
+ state: 'visible',
806
+ timeout: 60000,
807
+ });
808
+ await this.publicHolidayConnectorOption.click({ timeout: 30000 });
838
809
  return;
839
810
  }
840
811
  catch (error) {
841
812
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
842
813
  if (retries >= maxRetries - 1)
843
814
  break;
844
- const onModeler = await this.page
845
- .locator('[data-test="modeler"]')
846
- .waitFor({ state: 'visible', timeout: 3000 })
847
- .then(() => true)
848
- .catch(() => false);
849
- if (!onModeler) {
850
- await this.page.goBack();
851
- await this.page.waitForTimeout(3000);
852
- }
853
815
  await this.page.reload();
854
- await this.page.waitForTimeout(5000);
816
+ await new Promise((resolve) => setTimeout(resolve, 10000));
817
+ await this.clickCanvas();
818
+ await this.secondElement.click({ timeout: 60000 });
819
+ await this.clickChangeTypeButton();
855
820
  }
856
821
  }
857
- throw new Error(`Failed to click the public holiday connector after ${maxRetries} attempts.`);
822
+ throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
858
823
  }
859
824
  async clickPublicHolidayYearOption() {
860
825
  await this.publicHolidayYearOption.click({ timeout: 60000 });
@@ -6,6 +6,7 @@ declare class ConnectorMarketplacePage {
6
6
  readonly snackbar: Locator;
7
7
  readonly closeButton: Locator;
8
8
  readonly replaceResourceButton: Locator;
9
+ readonly addToProjectButton: Locator;
9
10
  readonly cancelButton: Locator;
10
11
  constructor(page: Page);
11
12
  clickSearchForConnectorTextbox(): Promise<void>;
@@ -8,6 +8,7 @@ class ConnectorMarketplacePage {
8
8
  snackbar;
9
9
  closeButton;
10
10
  replaceResourceButton;
11
+ addToProjectButton;
11
12
  cancelButton;
12
13
  constructor(page) {
13
14
  this.page = page;
@@ -20,6 +21,9 @@ class ConnectorMarketplacePage {
20
21
  this.replaceResourceButton = page.getByRole('button', {
21
22
  name: 'Replace resource',
22
23
  });
24
+ this.addToProjectButton = page.getByRole('button', {
25
+ name: 'Add to project',
26
+ });
23
27
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
24
28
  }
25
29
  async clickSearchForConnectorTextbox() {
@@ -42,14 +46,30 @@ class ConnectorMarketplacePage {
42
46
  }
43
47
  async downloadConnectorToProject() {
44
48
  await this.clickDownloadToProjectButton();
49
+ let imported = false;
45
50
  try {
46
51
  await Promise.race([
52
+ this.addToProjectButton.click({ timeout: 20000 }),
47
53
  this.replaceResourceButton.click({ timeout: 20000 }),
48
- this.cancelButton.click({ timeout: 20000 }),
49
54
  ]);
55
+ imported = true;
50
56
  }
51
57
  catch (e) {
52
- return;
58
+ if (await this.cancelButton.isVisible({ timeout: 3000 }).catch(() => false)) {
59
+ await this.cancelButton.click({ timeout: 10000 });
60
+ }
61
+ }
62
+ // After a successful import, wait for the snackbar (confirms the connector
63
+ // template was received by the modeler) then close the marketplace panel so
64
+ // the diagram canvas is unblocked before the caller tries to select the
65
+ // newly installed connector.
66
+ if (imported) {
67
+ await this.snackbar
68
+ .waitFor({ state: 'visible', timeout: 60000 })
69
+ .catch(() => { });
70
+ if (await this.closeButton.isVisible({ timeout: 5000 }).catch(() => false)) {
71
+ await this.closeButton.click({ timeout: 10000 });
72
+ }
53
73
  }
54
74
  }
55
75
  }
@@ -767,38 +767,29 @@ class ModelerCreatePage {
767
767
  const maxRetries = 4;
768
768
  for (let retries = 0; retries < maxRetries; retries++) {
769
769
  try {
770
- if (retries <= 2) {
771
- // Wait explicitly for the option to render in the change-type
772
- // panel before clicking. Without this we burn an attempt every
773
- // time the panel is still loading after download.
774
- await this.publicHolidayConnectorOption.waitFor({
775
- state: 'visible',
776
- timeout: 60000,
777
- });
778
- await this.publicHolidayConnectorOption.click({ timeout: 30000 });
779
- }
780
- else {
770
+ if (retries === 2) {
771
+ // Re-download connector on second retry in case the first download failed
781
772
  await this.clickMarketPlaceButton();
782
773
  const connectorMarketplacePage = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
783
774
  await connectorMarketplacePage.clickSearchForConnectorTextbox();
784
775
  await connectorMarketplacePage.fillSearchForConnectorTextbox('Public Holiday Connector');
785
776
  await (0, sleep_1.sleep)(10000);
786
777
  await connectorMarketplacePage.downloadConnectorToProject();
787
- await this.publicHolidayConnectorOption.waitFor({
788
- state: 'visible',
789
- timeout: 120000,
790
- });
791
- await this.publicHolidayConnectorOption.click({ timeout: 30000 });
792
778
  }
779
+ // Always open the change-type popup before trying to click the option
780
+ await this.clickCanvas();
781
+ await this.secondElement.click({ timeout: 60000 });
782
+ await this.clickChangeTypeButton();
783
+ await (0, test_1.expect)(this.marketPlaceButton).toBeVisible({ timeout: 60000 });
784
+ await this.publicHolidayConnectorOption.click({ timeout: 120000 });
793
785
  return;
794
786
  }
795
787
  catch (error) {
796
788
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
789
+ if (retries >= maxRetries - 1)
790
+ break;
797
791
  await this.page.reload();
798
792
  await new Promise((resolve) => setTimeout(resolve, 10000));
799
- await this.clickCanvas();
800
- await this.secondElement.click({ timeout: 60000 });
801
- await this.clickChangeTypeButton();
802
793
  }
803
794
  }
804
795
  throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
@@ -515,37 +515,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
515
515
  await _8_7_1.test.step('Log in with Second User and Navigate to Cross Component Test project as Collaborator', async () => {
516
516
  await (0, UtilitiesPage_1.clickInvitationLinkInEmail)(page, id);
517
517
  await (0, sleep_1.sleep)(60000);
518
- // The invitation link auto-authenticates the second user and lands
519
- // them in the correct org's Modeler. If the Modeler banner is visible
520
- // after the invitation, navigate to the Modeler home within the same
521
- // org context — clearing cookies and re-logging in via the app
522
- // switcher lands in the user's personal org where the shared project
523
- // is not listed.
524
- // If the invitation did not auto-authenticate (page ended up on Auth0),
525
- // fall back to the explicit login path.
526
- const autoAuthenticated = await modelerHomePage.modelerPageBanner
527
- .waitFor({ state: 'visible', timeout: 15000 })
528
- .then(() => true)
529
- .catch(() => false);
530
- if (autoAuthenticated) {
531
- try {
532
- await modelerHomePage.clickHomeBreadcrumb();
533
- }
534
- catch {
535
- // Already on the Modeler home page
536
- }
537
- }
538
- else {
539
- await page.context().clearCookies();
540
- await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, {
541
- username: emailAddress,
542
- password: password,
543
- }, 1000, 3, true);
544
- await appsPage.clickCamundaApps();
545
- await appsPage.clickModeler();
546
- }
547
- await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
548
- timeout: 120000,
518
+ await modelerHomePage.navigateAfterInvitationLink(loginPage, appsPage, {
519
+ username: emailAddress,
520
+ password: password,
549
521
  });
550
522
  await (0, UtilitiesPage_1.assertLocatorVisibleWithRetry)(modelerHomePage, modelerHomePage.crossComponentProjectFolder, 'Cross Component Test Project', 90000, 20);
551
523
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.683",
3
+ "version": "0.0.685",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",