@camunda/e2e-test-suite 0.0.635 → 0.0.637

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.
@@ -30,6 +30,7 @@ declare class AppsPage {
30
30
  clickOperate(clusterName: string): Promise<void>;
31
31
  clickOptimize(clusterName: string): Promise<void>;
32
32
  clickCamundaApps(maxRetries?: number): Promise<void>;
33
+ private dismissBlockingModal;
33
34
  clickConsoleLink(): Promise<void>;
34
35
  assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
35
36
  assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
@@ -68,23 +68,53 @@ class AppsPage {
68
68
  await cluster.click();
69
69
  }
70
70
  async clickModeler() {
71
- const maxRetries = 3;
71
+ const maxRetries = 5;
72
+ const modelerBanner = this.page.getByRole('banner', {
73
+ name: 'Camunda Modeler',
74
+ });
75
+ const modelerDiagramDropdown = this.page.locator('[data-test="diagram-dropdown"]');
76
+ // If the click is already done and we are sitting on Modeler, return
77
+ // immediately — observed in failing runs where the banner role lookup
78
+ // never resolves because partial app.js 429s prevent the accessible
79
+ // name from being set, even though the project list rendered.
80
+ if (this.page.url().includes('modeler.')) {
81
+ return;
82
+ }
72
83
  for (let retries = 0; retries < maxRetries; retries++) {
73
84
  try {
74
- if (retries === 0) {
75
- await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
76
- await this.modelerLink.click();
77
- }
78
- else {
79
- await this.clickCamundaApps();
80
- await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
81
- await this.modelerLink.click();
82
- }
85
+ await this.clickCamundaApps();
86
+ await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
87
+ await this.modelerLink.click({ timeout: 30000 });
88
+ // Primary success signal: the browser actually landed on the
89
+ // Modeler hostname. Some Modeler renders never expose the banner
90
+ // accessible name when modeler asset requests get 429-throttled
91
+ // by SaaS infra, but the URL transition is reliable.
92
+ await this.page.waitForURL((url) => url.hostname.includes('modeler.'), {
93
+ timeout: 90000,
94
+ });
95
+ // Best-effort: also wait briefly for the banner or the project
96
+ // dropdown so the caller's next interaction has a stable DOM.
97
+ // Tolerate this timing out — the URL is already correct.
98
+ await Promise.race([
99
+ modelerBanner.waitFor({ state: 'visible', timeout: 30000 }),
100
+ modelerDiagramDropdown.waitFor({ state: 'visible', timeout: 30000 }),
101
+ ]).catch(() => { });
83
102
  return;
84
103
  }
85
104
  catch (error) {
86
105
  console.warn(`Click attempt ${retries + 1} failed: ${error}`);
87
- await new Promise((resolve) => setTimeout(resolve, 10000));
106
+ if (this.page.url().includes('modeler.')) {
107
+ // Click registered and navigation actually happened — accept it
108
+ // even though banner/dropdown took too long to settle.
109
+ return;
110
+ }
111
+ if (!this.page.isClosed()) {
112
+ await this.page
113
+ .waitForLoadState('domcontentloaded', { timeout: 20000 })
114
+ .catch(() => { });
115
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
116
+ }
117
+ await (0, sleep_1.sleep)(5000);
88
118
  }
89
119
  }
90
120
  throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
@@ -272,13 +302,44 @@ class AppsPage {
272
302
  this.appSwitcherButton,
273
303
  ];
274
304
  for (let retries = 0; retries < maxRetries; retries++) {
275
- for (const appButton of appButtons) {
276
- if (await appButton.isVisible({ timeout: 20000 })) {
277
- await appButton.click({ timeout: 30000 });
278
- return;
305
+ try {
306
+ await this.dismissBlockingModal();
307
+ for (const appButton of appButtons) {
308
+ if (await appButton.isVisible({ timeout: 5000 }).catch(() => false)) {
309
+ await appButton.click({ timeout: 30000 });
310
+ return;
311
+ }
279
312
  }
280
313
  }
314
+ catch (error) {
315
+ console.warn(`Click attempt ${retries + 1} failed while opening app switcher: ${error}`);
316
+ }
317
+ await this.dismissBlockingModal();
318
+ await (0, sleep_1.sleep)(2000);
319
+ }
320
+ throw new Error(`Failed to open app switcher after ${maxRetries} attempts.`);
321
+ }
322
+ async dismissBlockingModal() {
323
+ const visibleModal = this.page.locator('.cds--modal.is-visible').first();
324
+ if (!(await visibleModal.isVisible().catch(() => false))) {
325
+ return;
281
326
  }
327
+ await this.page.keyboard.press('Escape').catch(() => { });
328
+ await this.page
329
+ .getByRole('button', { name: 'Close' })
330
+ .last()
331
+ .click({ timeout: 3000, force: true })
332
+ .catch(() => { });
333
+ await this.page
334
+ .getByRole('button', { name: 'Got it - Dismiss' })
335
+ .last()
336
+ .click({ timeout: 3000, force: true })
337
+ .catch(() => { });
338
+ await this.page
339
+ .getByRole('button', { name: 'Skip customization' })
340
+ .last()
341
+ .click({ timeout: 3000, force: true })
342
+ .catch(() => { });
282
343
  }
283
344
  async clickConsoleLink() {
284
345
  const maxRetries = 3;
@@ -6,6 +6,8 @@ declare class ConnectorMarketplacePage {
6
6
  readonly snackbar: Locator;
7
7
  readonly closeButton: Locator;
8
8
  readonly replaceResourceButton: Locator;
9
+ readonly addToProjectButton: Locator;
10
+ readonly saveAsCopyButton: Locator;
9
11
  readonly cancelButton: Locator;
10
12
  constructor(page: Page);
11
13
  clickSearchForConnectorTextbox(): Promise<void>;
@@ -8,6 +8,8 @@ class ConnectorMarketplacePage {
8
8
  snackbar;
9
9
  closeButton;
10
10
  replaceResourceButton;
11
+ addToProjectButton;
12
+ saveAsCopyButton;
11
13
  cancelButton;
12
14
  constructor(page) {
13
15
  this.page = page;
@@ -20,6 +22,21 @@ class ConnectorMarketplacePage {
20
22
  this.replaceResourceButton = page.getByRole('button', {
21
23
  name: 'Replace resource',
22
24
  });
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
+ this.addToProjectButton = page.getByRole('button', {
35
+ name: 'Add to project',
36
+ });
37
+ this.saveAsCopyButton = page.getByRole('button', {
38
+ name: 'Save as copy',
39
+ });
23
40
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
24
41
  }
25
42
  async clickSearchForConnectorTextbox() {
@@ -42,15 +59,35 @@ class ConnectorMarketplacePage {
42
59
  }
43
60
  async downloadConnectorToProject() {
44
61
  await this.clickDownloadToProjectButton();
45
- try {
46
- await Promise.race([
47
- this.replaceResourceButton.click({ timeout: 20000 }),
48
- this.cancelButton.click({ timeout: 20000 }),
49
- ]);
50
- }
51
- catch (e) {
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 });
52
84
  return;
53
85
  }
86
+ if (await this.cancelButton.isVisible({ timeout: 5000 }).catch(() => false)) {
87
+ await this.cancelButton.click({ timeout: 30000 });
88
+ }
89
+ // If neither button is visible the import dialog never opened; nothing
90
+ // to dismiss and downstream state-reset will recover.
54
91
  }
55
92
  }
56
93
  exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
@@ -266,9 +266,17 @@ class ConsoleOrganizationPage {
266
266
  }
267
267
  }
268
268
  //Enable Alpha Feature
269
- const toggleText = await alphaFeature
270
- .locator('[class= "cds--toggle__text"]')
271
- .textContent();
269
+ const toggleTextLocator = alphaFeature.locator('[class= "cds--toggle__text"]');
270
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, toggleTextLocator, {
271
+ visibilityTimeout: 15000,
272
+ totalTimeout: 90000,
273
+ maxRetries: 3,
274
+ postAction: async () => {
275
+ await this.page.reload();
276
+ await this.clickSettingsTab();
277
+ },
278
+ });
279
+ const toggleText = await toggleTextLocator.textContent();
272
280
  console.info(`Previous feature(${name}) setting is ${toggleText}.`);
273
281
  if (toggleText == 'Enabled') {
274
282
  console.log(`Feature ${name} is already enabled.`);
@@ -74,7 +74,13 @@ class HomePage {
74
74
  if (!(await next.isVisible({ timeout: visibleTimeout }))) {
75
75
  return;
76
76
  }
77
- await next.click({ timeout: 30000, force: true });
77
+ try {
78
+ await next.click({ timeout: 30000, force: true });
79
+ }
80
+ catch {
81
+ // Dialogs are often re-rendered during fade animations; if the current
82
+ // close button becomes stale or hidden, continue and try the latest one.
83
+ }
78
84
  }
79
85
  }
80
86
  organizationUuid() {
@@ -89,20 +95,15 @@ class HomePage {
89
95
  await this.openOrganizationButton.click({ timeout: 60000 });
90
96
  }
91
97
  async clickSkipCustomization() {
92
- try {
93
- await (0, test_1.expect)(this.gettingStartedHeading).toBeVisible({
94
- timeout: 20000,
95
- });
96
- await (0, test_1.expect)(this.buttonSkipCustomization).toBeVisible({
97
- timeout: 20000,
98
- });
99
- await this.buttonSkipCustomization.click();
100
- await this.closeInformationDialog();
101
- return;
102
- }
103
- catch (error) {
104
- console.error(error);
98
+ // The onboarding modal is not consistently shown for every session; this
99
+ // step must remain non-blocking for the rest of test setup.
100
+ const hasCustomizationModal = await this.buttonSkipCustomization
101
+ .isVisible({ timeout: 20000 })
102
+ .catch(() => false);
103
+ if (hasCustomizationModal) {
104
+ await this.buttonSkipCustomization.click({ timeout: 30000, force: true });
105
105
  }
106
+ await this.closeInformationDialog();
106
107
  }
107
108
  }
108
109
  exports.HomePage = HomePage;
@@ -17,7 +17,10 @@ class LoginPage {
17
17
  this.usernameInput = page
18
18
  .getByLabel('Email address')
19
19
  .and(page.locator(':not([id="c4-invite-email"])'));
20
- this.passwordInput = page.getByLabel('Password *');
20
+ this.passwordInput = page
21
+ .getByLabel(/^Password\s*\*?$/i)
22
+ .or(page.locator('input[type="password"]'))
23
+ .first();
21
24
  this.continueButton = page.getByRole('button', {
22
25
  name: 'Continue',
23
26
  exact: true,
@@ -75,9 +78,23 @@ class LoginPage {
75
78
  await this.page.waitForLoadState('domcontentloaded', { timeout: 30000 });
76
79
  }
77
80
  }
78
- await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 120000 });
79
- await this.fillUsername(username);
80
- await this.clickContinueButton();
81
+ const onPasswordStep = await this.passwordHeading
82
+ .isVisible({ timeout: 5000 })
83
+ .catch(() => false);
84
+ if (!onPasswordStep) {
85
+ // After logout the page can stall on a blank/intermediate Console
86
+ // state and never redirect to Auth0. Wait a shorter window first and
87
+ // reload once if the email field still isn't there.
88
+ try {
89
+ await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 60000 });
90
+ }
91
+ catch {
92
+ await this.page.reload();
93
+ await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 120000 });
94
+ }
95
+ await this.fillUsername(username);
96
+ await this.clickContinueButton();
97
+ }
81
98
  await this.fillPassword(password);
82
99
  await (0, test_1.expect)(this.loginButton).toBeVisible({ timeout: 120000 });
83
100
  await this.clickLoginButton();
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelerCreatePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
- const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
6
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
8
7
  class ModelerCreatePage {
9
8
  page;
@@ -187,9 +186,17 @@ class ModelerCreatePage {
187
186
  this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
188
187
  this.payloadInput = page.locator('[class="fjs-input"]');
189
188
  this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
190
- this.publicHolidayConnectorOption = page
191
- .locator('[data-test="modeler"]')
192
- .getByText('Worldwide Public Holiday');
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
+ });
193
200
  this.publicHolidayYearOption = page.getByLabel('Year');
194
201
  this.publicHolidayCountryCodeOption = page.getByLabel('Countrycode');
195
202
  this.implementationSection = page.locator('[data-group-id="group-userTaskImplementation"]');
@@ -777,33 +784,77 @@ class ModelerCreatePage {
777
784
  throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
778
785
  }
779
786
  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/.
780
800
  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
+ };
781
834
  for (let retries = 0; retries < maxRetries; retries++) {
782
835
  try {
783
- if (retries <= 2) {
784
- await this.publicHolidayConnectorOption.click({ timeout: 60000 });
785
- }
786
- else {
787
- await this.clickMarketPlaceButton();
788
- const connectorMarketplacePage = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
789
- await connectorMarketplacePage.clickSearchForConnectorTextbox();
790
- await connectorMarketplacePage.fillSearchForConnectorTextbox('Worldwide Public Holiday');
791
- await (0, sleep_1.sleep)(10000);
792
- await connectorMarketplacePage.downloadConnectorToProject();
793
- await this.publicHolidayConnectorOption.click({ timeout: 120000 });
794
- }
836
+ await setupModelerState();
837
+ await this.publicHolidayConnectorOption.click({ timeout: 120000 });
795
838
  return;
796
839
  }
797
840
  catch (error) {
798
841
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
842
+ if (retries >= maxRetries - 1)
843
+ 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
+ }
799
853
  await this.page.reload();
800
- await new Promise((resolve) => setTimeout(resolve, 10000));
801
- await this.clickCanvas();
802
- await this.secondElement.click({ timeout: 60000 });
803
- await this.clickChangeTypeButton();
854
+ await this.page.waitForTimeout(5000);
804
855
  }
805
856
  }
806
- throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
857
+ throw new Error(`Failed to click the public holiday connector after ${maxRetries} attempts.`);
807
858
  }
808
859
  async clickPublicHolidayYearOption() {
809
860
  await this.publicHolidayYearOption.click({ timeout: 60000 });
@@ -103,9 +103,28 @@ class ModelerHomePage {
103
103
  });
104
104
  }
105
105
  async enterNewProjectName(name) {
106
- await this.projectNameInput.click({ timeout: 60000 });
107
- await this.projectNameInput.fill(name);
108
- await this.projectNameInput.press('Enter');
106
+ for (let attempt = 0; attempt < 3; attempt++) {
107
+ try {
108
+ await (0, test_1.expect)(this.projectNameInput.first()).toBeVisible({
109
+ timeout: 90000,
110
+ });
111
+ await this.projectNameInput
112
+ .first()
113
+ .click({ timeout: 60000, force: true });
114
+ await this.projectNameInput.first().fill(name);
115
+ await this.projectNameInput.first().press('Enter');
116
+ return;
117
+ }
118
+ catch (error) {
119
+ if (attempt >= 2) {
120
+ throw error;
121
+ }
122
+ await this.dismissOverlays();
123
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
124
+ await this.page.waitForLoadState('domcontentloaded');
125
+ await this.clickCreateNewProjectButton();
126
+ }
127
+ }
109
128
  }
110
129
  async enterIdpApplicationName(name) {
111
130
  await this.idpApplicationNameInput.click({ timeout: 60000 });
@@ -131,10 +150,24 @@ class ModelerHomePage {
131
150
  await process.click();
132
151
  }
133
152
  async clickDiagramTypeDropdown() {
134
- await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
135
- timeout: 30000,
136
- });
137
- await this.diagramTypeDropdown.click({ timeout: 30000 });
153
+ for (let attempt = 0; attempt < 4; attempt++) {
154
+ try {
155
+ await this.dismissOverlays();
156
+ await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
157
+ timeout: 45000,
158
+ });
159
+ await this.diagramTypeDropdown.click({ timeout: 30000 });
160
+ return;
161
+ }
162
+ catch (error) {
163
+ if (attempt >= 3) {
164
+ throw error;
165
+ }
166
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
167
+ await this.page.waitForLoadState('domcontentloaded');
168
+ await this.clickCrossComponentProjectFolder();
169
+ }
170
+ }
138
171
  }
139
172
  async clickBpmnTemplateOption() {
140
173
  await this.bpmnTemplateOption.click({ timeout: 120000 });
@@ -143,8 +176,9 @@ class ModelerHomePage {
143
176
  await this.formTemplateOption.click();
144
177
  }
145
178
  async enterFormName(name) {
146
- await this.formNameInput.click({ timeout: 60000 });
147
- await this.formNameInput.fill(name);
179
+ await (0, test_1.expect)(this.formNameInput).toBeVisible({ timeout: 120000 });
180
+ await this.formNameInput.click({ timeout: 60000, force: true });
181
+ await this.formNameInput.fill(name, { timeout: 60000 });
148
182
  await this.formNameInput.press('Enter');
149
183
  }
150
184
  async clickProjectBreadcrumb() {
@@ -166,10 +200,23 @@ class ModelerHomePage {
166
200
  await this.openOrganizationsButton.click({ timeout: 30000 });
167
201
  }
168
202
  async createForm(formName) {
169
- await this.clickDiagramTypeDropdown();
170
- await this.clickFormOption();
171
- await this.enterFormName(formName);
172
- await (0, sleep_1.sleep)(10000);
203
+ const maxRetries = 3;
204
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
205
+ try {
206
+ await this.clickDiagramTypeDropdown();
207
+ await this.clickFormOption();
208
+ await this.enterFormName(formName);
209
+ await (0, sleep_1.sleep)(10000);
210
+ return;
211
+ }
212
+ catch (error) {
213
+ if (attempt >= maxRetries - 1) {
214
+ throw error;
215
+ }
216
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
217
+ await this.clickCrossComponentProjectFolder();
218
+ }
219
+ }
173
220
  }
174
221
  async clickCreateIdpApplicationButton() {
175
222
  await this.createIdpApplicationButton.click();
@@ -120,7 +120,16 @@ class OCIdentityRolesPage {
120
120
  await (0, test_1.expect)(this.assignUserModal).toBeVisible({ timeout: 30000 });
121
121
  await this.assignUserModalInputField.fill(email);
122
122
  await this.assignUserModalAssignButton.click({ timeout: 30000 });
123
- await (0, test_1.expect)(this.assignUserModal).not.toBeVisible({ timeout: 30000 });
123
+ try {
124
+ await (0, test_1.expect)(this.assignUserModal).not.toBeVisible({ timeout: 30000 });
125
+ }
126
+ catch {
127
+ // The Assign click occasionally lands while the modal is mid-render
128
+ // and the submission is silently dropped — the modal stays open with
129
+ // no error surfaced. Re-click Assign once before failing the test.
130
+ await this.assignUserModalAssignButton.click({ timeout: 30000 });
131
+ await (0, test_1.expect)(this.assignUserModal).not.toBeVisible({ timeout: 30000 });
132
+ }
124
133
  }
125
134
  async createRole(role) {
126
135
  await (0, test_1.expect)(this.createRoleButton).toBeVisible({ timeout: 30000 });
@@ -2,6 +2,9 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class PlayPage {
3
3
  private page;
4
4
  readonly completeJobButton: Locator;
5
+ readonly configureTestPanel: Locator;
6
+ readonly configureTestPanelStartButton: Locator;
7
+ readonly startInstanceOverlayButton: Locator;
5
8
  constructor(page: Page);
6
9
  waitForCompleteJobButtonToBeAvailable(): Promise<void>;
7
10
  clickCompleteJobButton(): Promise<void>;
@@ -2,15 +2,35 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PlayPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
5
6
  const maxWaitTimeSeconds = 120000;
6
7
  class PlayPage {
7
8
  page;
8
9
  completeJobButton;
10
+ configureTestPanel;
11
+ configureTestPanelStartButton;
12
+ startInstanceOverlayButton;
9
13
  constructor(page) {
10
14
  this.page = page;
11
15
  this.completeJobButton = page
12
16
  .getByTestId('diagram')
13
17
  .getByLabel('Complete job');
18
+ // Newer Modeler Play UI: the "Configure Scenario" floating card hosts a
19
+ // primary "Start" Carbon button (variants: "Start", "Start with
20
+ // variables", "Start with a form"). Source:
21
+ // camunda-hub frontend/packages/modeler/play/src/Definition/
22
+ // ConfigureTestPanel/index.tsx — renders <FloatingCard
23
+ // data-testid="configure-test-panel"> with a <Button> whose visible
24
+ // text starts with "Start".
25
+ this.configureTestPanel = page.getByTestId('configure-test-panel');
26
+ this.configureTestPanelStartButton = this.configureTestPanel.getByRole('button', { name: /^Start( with .*)?$/, exact: false });
27
+ // Legacy bpmn-js canvas overlay button (still emitted by older Modeler
28
+ // builds, also relabeled with cached/example-data suffixes). Kept as a
29
+ // fallback for environments that haven't shipped the ConfigureTestPanel
30
+ // yet.
31
+ this.startInstanceOverlayButton = page
32
+ .getByTestId('diagram')
33
+ .getByLabel('Start instance', { exact: false });
14
34
  }
15
35
  async waitForCompleteJobButtonToBeAvailable() {
16
36
  await (0, test_1.expect)(this.completeJobButton).toBeVisible({
@@ -21,17 +41,31 @@ class PlayPage {
21
41
  await this.completeJobButton.click();
22
42
  }
23
43
  async clickStartInstanceButton() {
24
- await this.page
25
- .getByTestId('diagram')
26
- .getByLabel('Start instance', { exact: true })
27
- .click();
44
+ const startTrigger = this.configureTestPanelStartButton
45
+ .or(this.startInstanceOverlayButton)
46
+ .first();
47
+ await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, startTrigger, {
48
+ totalTimeout: 240000,
49
+ visibilityTimeout: 60000,
50
+ maxRetries: 8,
51
+ retryDelayMs: 5000,
52
+ });
28
53
  }
29
54
  async dismissStartModal() {
30
- await this.page
31
- .getByRole('button', {
32
- name: 'Start a process instance',
33
- })
34
- .click();
55
+ // The intro dialog cycles labels across Modeler versions/states.
56
+ const buttonVariations = [
57
+ 'Start a process instance',
58
+ 'Start another instance',
59
+ 'Start new instance',
60
+ 'Start instance',
61
+ ];
62
+ for (const buttonName of buttonVariations) {
63
+ const button = this.page.getByRole('button', { name: buttonName });
64
+ if ((await button.count()) > 0) {
65
+ await button.first().click();
66
+ return;
67
+ }
68
+ }
35
69
  }
36
70
  async waitForInstanceDetailsToBeLoaded() {
37
71
  const maxRetries = 2;
@@ -33,21 +33,45 @@ async function forceLogoutIfNeeded(page) {
33
33
  }
34
34
  exports.forceLogoutIfNeeded = forceLogoutIfNeeded;
35
35
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3) {
36
+ let lastError;
36
37
  for (let attempt = 0; attempt < maxRetries; attempt++) {
37
38
  try {
38
- await page.goto('/');
39
+ await page.context().clearCookies();
40
+ await page.goto('about:blank');
41
+ await page
42
+ .evaluate(() => {
43
+ try {
44
+ localStorage.clear();
45
+ }
46
+ catch (_) {
47
+ // storage may be unavailable in some contexts
48
+ }
49
+ try {
50
+ sessionStorage.clear();
51
+ }
52
+ catch (_) {
53
+ // storage may be unavailable in some contexts
54
+ }
55
+ })
56
+ .catch(() => { });
57
+ await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
58
+ await page
59
+ .waitForLoadState('networkidle', { timeout: 30000 })
60
+ .catch(() => { });
39
61
  await (0, sleep_1.sleep)(timeout);
62
+ await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
40
63
  await loginPage.loginWithTestUser(testUser);
41
64
  return;
42
65
  }
43
66
  catch (error) {
67
+ lastError = error;
44
68
  if (attempt < maxRetries - 1) {
45
- console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying...`);
69
+ console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`);
46
70
  await (0, randomSleep_1.randomSleep)(10000, 20000);
47
71
  }
48
72
  else {
49
- console.error(error);
50
- throw new Error(`Login failed after ${maxRetries} attempts`);
73
+ console.error(lastError);
74
+ throw new Error(`Login failed after ${maxRetries} attempts: ${String(lastError)}`);
51
75
  }
52
76
  }
53
77
  }
@@ -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 expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
+ const LoginPage_1 = require("./LoginPage");
7
8
  class ModelerHomePage {
8
9
  page;
9
10
  defaultFolderName = 'Cross Component Test Project';
@@ -143,6 +144,16 @@ class ModelerHomePage {
143
144
  console.log(`Attempt ${attempts} failed. Reloading page and retrying...`);
144
145
  await this.page.reload();
145
146
  await (0, sleep_1.sleep)(10000);
147
+ // A reload can bounce the Modeler SPA back to the Identity login
148
+ // screen (the session is re-validated on reload). When that happens
149
+ // the project folder can never appear, so re-authenticate before the
150
+ // next attempt instead of looping uselessly against the login page.
151
+ const loginPage = new LoginPage_1.LoginPage(this.page);
152
+ if (await loginPage.usernameInput.isVisible().catch(() => false)) {
153
+ await loginPage.login('demo', process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD ??
154
+ 'demo');
155
+ await this.page.waitForLoadState('load').catch(() => { });
156
+ }
146
157
  }
147
158
  else {
148
159
  throw new Error('Failed to click crossComponentProjectFolder after 3 attempts.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.635",
3
+ "version": "0.0.637",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",