@camunda/e2e-test-suite 0.0.879 → 0.0.880

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.
@@ -4,7 +4,10 @@ declare class ConnectorMarketplacePage {
4
4
  readonly searchForConnectorTextbox: Locator;
5
5
  readonly downloadToProjectButton: Locator;
6
6
  readonly replaceResourceButton: Locator;
7
+ readonly addToProjectButton: Locator;
7
8
  readonly cancelButton: Locator;
9
+ readonly backToMarketplaceButton: Locator;
10
+ readonly downloadErrorMessage: Locator;
8
11
  constructor(page: Page);
9
12
  clickSearchForConnectorTextbox(): Promise<void>;
10
13
  fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
@@ -12,5 +15,6 @@ declare class ConnectorMarketplacePage {
12
15
  clickCancelButton(): Promise<void>;
13
16
  clickReplaceResourceButton(): Promise<void>;
14
17
  downloadConnectorToProject(): Promise<void>;
18
+ searchAndDownloadConnector(connectorName: string, maxRetries?: number): Promise<void>;
15
19
  }
16
20
  export { ConnectorMarketplacePage };
@@ -7,7 +7,10 @@ class ConnectorMarketplacePage {
7
7
  searchForConnectorTextbox;
8
8
  downloadToProjectButton;
9
9
  replaceResourceButton;
10
+ addToProjectButton;
10
11
  cancelButton;
12
+ backToMarketplaceButton;
13
+ downloadErrorMessage;
11
14
  constructor(page) {
12
15
  this.page = page;
13
16
  this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
@@ -18,6 +21,15 @@ class ConnectorMarketplacePage {
18
21
  this.replaceResourceButton = page.getByRole('button', {
19
22
  name: 'Replace resource',
20
23
  });
24
+ this.addToProjectButton = page.getByRole('button', {
25
+ name: 'Add to project',
26
+ });
27
+ this.backToMarketplaceButton = page.getByRole('button', {
28
+ name: 'Back to Marketplace',
29
+ });
30
+ this.downloadErrorMessage = page
31
+ .getByText('Download Error')
32
+ .or(page.getByText('We are sorry, an error occurred'));
21
33
  }
22
34
  async clickSearchForConnectorTextbox() {
23
35
  await this.searchForConnectorTextbox.click({ timeout: 60000 });
@@ -35,17 +47,55 @@ class ConnectorMarketplacePage {
35
47
  async clickReplaceResourceButton() {
36
48
  await this.replaceResourceButton.click({ timeout: 30000 });
37
49
  }
50
+ // The import dialog confirms with either "Add to project" (the resource is new
51
+ // to the project) or "Replace resource" (a file with the same name already
52
+ // exists); on a backend failure it renders a download-error panel with neither
53
+ // button. Probe which one rendered and click only that one - issuing both
54
+ // clicks through Promise.race also fired the Cancel click, so the import was
55
+ // abandoned and the connector template never landed in the project.
38
56
  async downloadConnectorToProject() {
39
57
  await this.clickDownloadToProjectButton();
40
- try {
41
- await Promise.race([
42
- this.replaceResourceButton.click({ timeout: 20000 }),
43
- this.cancelButton.click({ timeout: 20000 }),
44
- ]);
58
+ const importConfirmButton = this.addToProjectButton.or(this.replaceResourceButton);
59
+ const settled = await importConfirmButton
60
+ .or(this.downloadErrorMessage)
61
+ .first()
62
+ .waitFor({ state: 'visible', timeout: 20000 })
63
+ .then(() => true)
64
+ .catch(() => false);
65
+ if (settled && (await importConfirmButton.first().isVisible())) {
66
+ await importConfirmButton.first().click({ timeout: 30000 });
45
67
  }
46
- catch (e) {
47
- return;
68
+ }
69
+ // The marketplace backend transiently fails in two ways: the search returns an
70
+ // error instead of results, or the resource download from the external
71
+ // marketplace host ends in a download-error panel. Either way no template
72
+ // lands in the project, so re-issue the search and download (clearing the
73
+ // field first) with escalating backoff until one attempt completes cleanly.
74
+ async searchAndDownloadConnector(connectorName, maxRetries = 6) {
75
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
76
+ if (await this.backToMarketplaceButton.isVisible()) {
77
+ await this.backToMarketplaceButton.click({ timeout: 30000 });
78
+ }
79
+ await this.clickSearchForConnectorTextbox();
80
+ await this.searchForConnectorTextbox.fill('', { timeout: 60000 });
81
+ await this.fillSearchForConnectorTextbox(connectorName);
82
+ const isDownloadable = await this.downloadToProjectButton
83
+ .waitFor({ state: 'visible', timeout: 30000 })
84
+ .then(() => true)
85
+ .catch(() => false);
86
+ if (isDownloadable) {
87
+ await this.downloadConnectorToProject();
88
+ if (!(await this.downloadErrorMessage.first().isVisible())) {
89
+ return;
90
+ }
91
+ }
92
+ if (attempt < maxRetries) {
93
+ await (0, sleep_1.sleep)(attempt * 5000);
94
+ }
48
95
  }
96
+ throw new Error(`Connector "${connectorName}" could not be downloaded from the ` +
97
+ `marketplace after ${maxRetries} attempts; the marketplace search or ` +
98
+ `download API is returning errors.`);
49
99
  }
50
100
  }
51
101
  exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
@@ -31,6 +31,7 @@ declare class ModelerHomePage {
31
31
  constructor(page: Page);
32
32
  clickCreateNewProjectButton(): Promise<void>;
33
33
  enterNewProjectName(name: string): Promise<void>;
34
+ waitForModelerReady(timeoutMs?: number): Promise<void>;
34
35
  createCrossComponentProjectFolder(): Promise<void>;
35
36
  clickCrossComponentProjectFolder(): Promise<void>;
36
37
  clickProcessDiagram(name: string): Promise<void>;
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
7
  const LoginPage_1 = require("./LoginPage");
8
+ const NavigationPage_1 = require("./NavigationPage");
8
9
  class ModelerHomePage {
9
10
  page;
10
11
  defaultFolderName = 'Cross Component Test Project';
@@ -106,15 +107,53 @@ class ModelerHomePage {
106
107
  await this.projectNameInput.fill(name);
107
108
  await this.projectNameInput.press('Enter');
108
109
  }
110
+ // goToModeler() gates on the nav banner, but that banner also renders on the
111
+ // unauthenticated `/modeler/login` page — so navigation can settle on a shell
112
+ // whose project list never loads. Gate on an actionable element (the "New
113
+ // project" button or an existing project folder) instead.
114
+ async waitForModelerReady(timeoutMs = 120000) {
115
+ // .first(): when the project folder already exists on the shared cluster
116
+ // both elements match, and waitFor() requires a single element.
117
+ await this.createNewProjectButton
118
+ .or(this.crossComponentProjectFolder)
119
+ .first()
120
+ .waitFor({ state: 'visible', timeout: timeoutMs });
121
+ }
109
122
  async createCrossComponentProjectFolder() {
110
- await this.clickMessageBanner();
111
- if (await this.crossComponentProjectFolder.isVisible()) {
112
- console.log('Cross Component Project folder already exists. Clicking into it');
113
- await this.clickCrossComponentProjectFolder();
114
- return;
123
+ const maxAttempts = 3;
124
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
125
+ try {
126
+ await this.waitForModelerReady();
127
+ await this.clickMessageBanner();
128
+ if (await this.crossComponentProjectFolder.isVisible()) {
129
+ console.log('Cross Component Project folder already exists. Clicking into it');
130
+ await this.clickCrossComponentProjectFolder();
131
+ return;
132
+ }
133
+ await this.clickCreateNewProjectButton();
134
+ await this.enterNewProjectName(this.defaultFolderName);
135
+ return;
136
+ }
137
+ catch (error) {
138
+ if (attempt < maxAttempts - 1) {
139
+ console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
140
+ // The Modeler SPA requests the OIDC discovery document once per load;
141
+ // when Identity answers 503 it stops at a spinner and never retries,
142
+ // so waiting longer on the same page cannot help. A bare reload can
143
+ // land back on the empty login form without re-authenticating, so
144
+ // re-run the full navigation+login flow.
145
+ try {
146
+ await new NavigationPage_1.NavigationPage(this.page).goToModeler();
147
+ }
148
+ catch (navError) {
149
+ console.log(`Re-navigation to Modeler failed: ${navError}`);
150
+ }
151
+ }
152
+ else {
153
+ throw error;
154
+ }
155
+ }
115
156
  }
116
- await this.clickCreateNewProjectButton();
117
- await this.enterNewProjectName(this.defaultFolderName);
118
157
  }
119
158
  async clickCrossComponentProjectFolder() {
120
159
  let attempts = 0;
@@ -443,42 +443,13 @@ async function modelAndRunConnectorsDocHandlingDiagram(modelerCreatePage, modele
443
443
  await (0, sleep_1.sleep)(10000);
444
444
  await modelerCreatePage.clickFirstPlaceRESTConnector();
445
445
  await modelerCreatePage.clickChangeTypeButton();
446
- try {
447
- // REST Outbound Connector is bundled in the modeler; apply it directly
448
- // from the change-type panel. The marketplace search for the bundled
449
- // connector no longer returns a match, so only fall back to it when the
450
- // bundled option is genuinely absent. Mirrors modelRestConnector above.
451
- //
452
- // The change-element popup virtualizes its list: connector templates
453
- // render below the built-in BPMN tasks and are not in the queryable DOM
454
- // slice until the list is filtered. Type into the popup search box (the
455
- // same `.djs-popup-search input` selector the bpmn-js/hub e2e tests use)
456
- // so the REST Outbound Connector entry is rendered before asserting on it.
457
- // Wait for the search input to render before filling: a bare isVisible()
458
- // check races the popup re-render and silently skips the fill, leaving the
459
- // list unfiltered so the off-screen connector entry never appears.
460
- await (0, test_1.expect)(modelerCreatePage.changeElementSearchInput).toBeVisible({
461
- timeout: 30000,
462
- });
463
- await modelerCreatePage.changeElementSearchInput.fill('REST Outbound');
464
- await (0, test_1.expect)(modelerCreatePage.restConnectorOption).toBeVisible({
465
- timeout: 15000,
466
- });
467
- await modelerCreatePage.restConnectorOption.scrollIntoViewIfNeeded();
468
- await modelerCreatePage.restConnectorOption.click({
469
- timeout: 60000,
470
- force: true,
471
- });
472
- }
473
- catch {
474
- await modelerCreatePage.clickMarketPlaceButton();
475
- await connectorMarketplacePage.clickSearchForConnectorTextbox();
476
- await connectorMarketplacePage.fillSearchForConnectorTextbox('REST Connector');
477
- await connectorMarketplacePage.downloadConnectorToProject();
478
- }
479
- await (0, sleep_1.sleep)(5000);
446
+ // SM Web Modeler ships no bundled marketplace connectors, so the
447
+ // change-element popup never offers the REST Outbound Connector and the
448
+ // template has to come from the marketplace.
449
+ await modelerCreatePage.clickMarketPlaceButton();
450
+ await connectorMarketplacePage.searchAndDownloadConnector('REST Connector');
480
451
  await page.reload();
481
- await (0, sleep_1.sleep)(5000);
452
+ await page.waitForLoadState('networkidle');
482
453
  await modelerCreatePage.fillStartEventVariablesForDocHandling(zeebeUrl, zeebeClientId, zeebeClientSecret);
483
454
  await modelerCreatePage.setOAuthTokenEndpoint(page.locator('.djs-hit').first(), endpoint);
484
455
  await (0, sleep_1.sleep)(2000);
@@ -114,7 +114,8 @@ if (process.env.IS_DS === 'true') {
114
114
  await operateProcessInstancePage.assertProcessVariableContainsText('Upload_Files', 'aws');
115
115
  });
116
116
  });
117
- (0, SM_8_8_1.test)('Document Handling Connectors User Flow - AWS @tasklistV2', async ({ page, navigationPage, modelerHomePage, modelerCreatePage, operateHomePage, operateProcessesPage, connectorMarketplacePage, operateProcessInstancePage, ocIdentityHomePage, ocIdentityRolesPage, }) => {
117
+ // Skipped due to bug #8162: https://github.com/camunda/connectors/issues/8162
118
+ SM_8_8_1.test.skip('Document Handling Connectors User Flow - AWS @tasklistV2', async ({ page, navigationPage, modelerHomePage, modelerCreatePage, operateHomePage, operateProcessesPage, connectorMarketplacePage, operateProcessInstancePage, ocIdentityHomePage, ocIdentityRolesPage, }) => {
118
119
  const processName = 'Document_Handling_Connectors_User_Flow_AWS_Process' +
119
120
  (await (0, _setup_1.generateRandomStringAsync)(3));
120
121
  const baseURL = process.env.PLAYWRIGHT_BASE_URL ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.879",
3
+ "version": "0.0.880",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",