@camunda/e2e-test-suite 0.0.1030 → 0.0.1032
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.
- package/dist/pages/8.10/Authorization.js +28 -25
- package/dist/pages/8.10/ModelerCreatePage.js +6 -1
- package/dist/pages/8.10/ModelerHomePage.js +36 -5
- package/dist/pages/8.10/ProcessApplicationPage.js +19 -17
- package/dist/pages/8.8/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.8/ModelerHomePage.js +70 -21
- package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.9/ModelerHomePage.js +69 -20
- package/package.json +1 -1
|
@@ -40,11 +40,17 @@ class Authorization {
|
|
|
40
40
|
name,
|
|
41
41
|
exact: true,
|
|
42
42
|
});
|
|
43
|
+
// The Owner field is no longer a Carbon ComboBox labelled "Owner".
|
|
44
|
+
// Identity replaced it with an entity-search dropdown
|
|
45
|
+
// (identity/client/src/components/form/EntitySearchSingleSelect.tsx ->
|
|
46
|
+
// EntitySearchDropdown -> DropdownSearch), which renders a bare
|
|
47
|
+
// `FormLabel` plus a Carbon `Search` input carrying only the
|
|
48
|
+
// "Search by owner ID" placeholder — nothing in the modal exposes the
|
|
49
|
+
// accessible name "Owner" any more, so the old combobox click waited out
|
|
50
|
+
// its full timeout. Anchor on the placeholder, which is what the
|
|
51
|
+
// component actually renders for every searchable owner type.
|
|
43
52
|
this.createAuthorizationOwnerComboBox =
|
|
44
|
-
this.createAuthorizationModal.
|
|
45
|
-
name: 'Owner',
|
|
46
|
-
exact: true,
|
|
47
|
-
});
|
|
53
|
+
this.createAuthorizationModal.getByPlaceholder('Search by owner ID');
|
|
48
54
|
this.createAuthorizationOwnerOption = (name) => this.createAuthorizationModal.getByRole('option', {
|
|
49
55
|
name,
|
|
50
56
|
exact: true,
|
|
@@ -90,33 +96,30 @@ class Authorization {
|
|
|
90
96
|
await (0, test_1.expect)(this.createAuthorizationModal).toBeVisible();
|
|
91
97
|
await this.createAuthorizationOwnerTypeCombobox.click();
|
|
92
98
|
await this.createAuthorizationOwnerTypeOption(authorization.ownerType).click();
|
|
93
|
-
await this.createAuthorizationOwnerComboBox.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// 26632778543 shows the input value populated but Resource type
|
|
105
|
-
// still disabled). Scoping to the Carbon menu-item class — the same
|
|
106
|
-
// approach used in the product repo's qa-suite
|
|
107
|
-
// (camunda/camunda qa/c8-orchestration-cluster-e2e-test-suite/
|
|
108
|
-
// pages/IdentityAuthorizationsPage.ts selectAuthorizationOwner) —
|
|
109
|
-
// selects the actual Downshift menu item and fires the handler.
|
|
110
|
-
const ownerSearchInput = this.createAuthorizationModal.getByPlaceholder('Search by owner ID');
|
|
111
|
-
if (await ownerSearchInput.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
112
|
-
await ownerSearchInput.fill(authorization.ownerId);
|
|
113
|
-
}
|
|
99
|
+
await (0, test_1.expect)(this.createAuthorizationOwnerComboBox).toBeVisible({
|
|
100
|
+
timeout: 30000,
|
|
101
|
+
});
|
|
102
|
+
await this.createAuthorizationOwnerComboBox.fill(authorization.ownerId);
|
|
103
|
+
// The dropdown renders results only while the search box holds text, and
|
|
104
|
+
// the list is fetched from the server on a debounce rather than filtered
|
|
105
|
+
// client-side, so the matching entry appears a beat later. Clicking the
|
|
106
|
+
// Carbon menu item is what commits the selection: DropdownSearch wires
|
|
107
|
+
// `onSelect` to `ListBox.MenuItem`'s onClick
|
|
108
|
+
// (identity/client/src/components/form/DropdownSearch.tsx), and that is
|
|
109
|
+
// the only path that promotes an entry into the form's owner value.
|
|
114
110
|
const ownerMenuItem = this.createAuthorizationModal
|
|
115
111
|
.locator('.cds--list-box__menu-item')
|
|
116
112
|
.filter({ hasText: authorization.ownerId })
|
|
117
113
|
.first();
|
|
118
114
|
await (0, test_1.expect)(ownerMenuItem).toBeVisible({ timeout: 60000 });
|
|
119
115
|
await ownerMenuItem.click({ timeout: 20000 });
|
|
116
|
+
// A committed selection swaps the search box for a filter Tag
|
|
117
|
+
// (EntitySearchSingleSelect renders one or the other), so the box going
|
|
118
|
+
// away is how we know the owner actually landed on the form rather than
|
|
119
|
+
// the click being swallowed.
|
|
120
|
+
await (0, test_1.expect)(this.createAuthorizationOwnerComboBox).toBeHidden({
|
|
121
|
+
timeout: 15000,
|
|
122
|
+
});
|
|
120
123
|
await this.selectResourceTypeFromDialog(authorization.resourceType);
|
|
121
124
|
await this.createAuthorizationResourceIdField.fill(authorization.resourceId);
|
|
122
125
|
for (const permission of authorization.accessPermissions) {
|
|
@@ -739,8 +739,13 @@ class ModelerCreatePage {
|
|
|
739
739
|
async fillNamedInput(name) {
|
|
740
740
|
await this.nameInput.fill(name, { timeout: 60000 });
|
|
741
741
|
}
|
|
742
|
+
// 10s was too tight for the first click after the editor opens: the button
|
|
743
|
+
// is already resolved, visible and stable, and the click then waits out its
|
|
744
|
+
// whole budget on the actionability check while the bpmn-js canvas is still
|
|
745
|
+
// settling ("done scrolling" is the last line before the timeout). Every
|
|
746
|
+
// other breadcrumb interaction in this file budgets 30-60s; match them.
|
|
742
747
|
async clickDiagramBreadcrumb() {
|
|
743
|
-
await this.diagramBreadcrumb.click({ timeout:
|
|
748
|
+
await this.diagramBreadcrumb.click({ timeout: 60000 });
|
|
744
749
|
}
|
|
745
750
|
// Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
|
|
746
751
|
// and hub renders that button's truncated-name tooltip on top of the menu,
|
|
@@ -105,7 +105,20 @@ class ModelerHomePage {
|
|
|
105
105
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
106
106
|
this.messageBanner = page.locator('[data-test="close-top-banner"]');
|
|
107
107
|
this.dialog = page.getByRole('dialog');
|
|
108
|
-
|
|
108
|
+
// Both announcement modals hub stacks over the projects list dismiss
|
|
109
|
+
// through a "Got it…" footer button: the What's New modal's reads
|
|
110
|
+
// "Got it - Dismiss", and the migration notice added in camunda-hub
|
|
111
|
+
// #27819 ("Your projects have a new structure") reads just "Got it".
|
|
112
|
+
//
|
|
113
|
+
// Matched as a visible button rather than by role/name: the What's New
|
|
114
|
+
// modal stays mounted with `open={false}` after it is closed, so its
|
|
115
|
+
// dismiss button remains in the DOM but hidden, and an `.or(...).first()`
|
|
116
|
+
// union would resolve to that hidden button in DOM order and report
|
|
117
|
+
// "nothing to dismiss" while the migration notice is still covering the
|
|
118
|
+
// projects list.
|
|
119
|
+
this.closeButton = page
|
|
120
|
+
.locator('button:visible', { hasText: /^Got it/ })
|
|
121
|
+
.first();
|
|
109
122
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
110
123
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
111
124
|
hasText: formName,
|
|
@@ -120,6 +133,12 @@ class ModelerHomePage {
|
|
|
120
133
|
this.renameMenuItem = page.getByRole('menuitem', { name: 'Rename' });
|
|
121
134
|
}
|
|
122
135
|
async dismissOverlays() {
|
|
136
|
+
// Before the generic pass, for the same reason
|
|
137
|
+
// clickCrossComponentProjectFolder dismisses the dialog first: the loop
|
|
138
|
+
// below closes whichever overlay it finds and moves on, so a single pass
|
|
139
|
+
// leaves a stacked announcement dialog open, and every caller that relies
|
|
140
|
+
// on dismissOverlays() to clear the page then clicks into it.
|
|
141
|
+
await this.dismissBlockingDialog();
|
|
123
142
|
for (const locator of [
|
|
124
143
|
this.closeButton,
|
|
125
144
|
this.page.getByRole('button', { name: 'Close' }).first(),
|
|
@@ -163,6 +182,12 @@ class ModelerHomePage {
|
|
|
163
182
|
if (await committedProjectName.first().isVisible()) {
|
|
164
183
|
return;
|
|
165
184
|
}
|
|
185
|
+
// The rename runs on a freshly created project, which is exactly when an
|
|
186
|
+
// announcement dialog mounts on top of it: it takes focus, the inline
|
|
187
|
+
// editor unmounts, and the probe below then reads "closed" and sends the
|
|
188
|
+
// recovery down a path that cannot reach an editor the dialog is
|
|
189
|
+
// covering.
|
|
190
|
+
await this.dismissBlockingDialog();
|
|
166
191
|
// Bounded, not instantaneous: the editor can still be mounting, and
|
|
167
192
|
// reading that as "closed" would send the recovery to the projects-list
|
|
168
193
|
// branch while the browser sits on the project page, where the row it
|
|
@@ -413,12 +438,18 @@ class ModelerHomePage {
|
|
|
413
438
|
await this.page.getByText('Home', { exact: true }).first().click();
|
|
414
439
|
}
|
|
415
440
|
}
|
|
441
|
+
// An announcement dialog covering the Modeler header makes every click on the
|
|
442
|
+
// org button bounce off the modal layer ("intercepts pointer events") until
|
|
443
|
+
// the timeout, so dismiss it before each attempt instead of waiting out a
|
|
444
|
+
// fixed sleep and hoping the header is clear.
|
|
416
445
|
async clickOpenOrganizationsButton() {
|
|
417
|
-
await (0,
|
|
418
|
-
|
|
419
|
-
|
|
446
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
|
|
447
|
+
preAction: async () => {
|
|
448
|
+
await this.dismissBlockingDialog();
|
|
449
|
+
},
|
|
450
|
+
totalTimeout: 120000,
|
|
451
|
+
visibilityTimeout: 30000,
|
|
420
452
|
});
|
|
421
|
-
await this.openOrganizationsButton.click({ timeout: 30000 });
|
|
422
453
|
}
|
|
423
454
|
async createForm(formName) {
|
|
424
455
|
await this.clickDiagramTypeDropdown();
|
|
@@ -199,29 +199,31 @@ class ProcessApplicationPage {
|
|
|
199
199
|
// Worth the noise: an unbound cluster otherwise surfaces as a deploy dialog
|
|
200
200
|
// that silently offers nothing, which says nothing about its own cause.
|
|
201
201
|
async openStageDefinition() {
|
|
202
|
-
const opened = await this.defineStagesDialog
|
|
203
|
-
.waitFor({ state: 'visible', timeout: 15000 })
|
|
204
|
-
.then(() => true)
|
|
205
|
-
.catch(() => false);
|
|
206
|
-
if (opened) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
202
|
// Page-scoped .first(): hub renders one such control on this page, and a
|
|
210
203
|
// second placement must not turn every rebinding test into a strict-mode
|
|
211
204
|
// violation.
|
|
212
205
|
const reopen = this.page
|
|
213
206
|
.getByRole('button', { name: /define stages|edit stages|configure/i })
|
|
214
207
|
.first();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
208
|
+
// Retried as a whole, because the two ways in race each other. On a slow
|
|
209
|
+
// load the deep link's own auto-open lands AFTER the first wait below has
|
|
210
|
+
// given up; the dialog then sits on top of the reopen control, so the
|
|
211
|
+
// fallback click waits out its timeout on an intercepted pointer while the
|
|
212
|
+
// dialog it was meant to open is already on screen. Re-probing for the
|
|
213
|
+
// dialog at the top of every attempt settles that: whichever route got it
|
|
214
|
+
// there, the next pass sees it and returns.
|
|
215
|
+
await (0, test_1.expect)(async () => {
|
|
216
|
+
const opened = await this.defineStagesDialog
|
|
217
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
218
|
+
.then(() => true)
|
|
219
|
+
.catch(() => false);
|
|
220
|
+
if (opened) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await reopen.waitFor({ state: 'visible', timeout: 10000 });
|
|
224
|
+
await reopen.click({ timeout: 15000 });
|
|
225
|
+
await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 15000 });
|
|
226
|
+
}).toPass({ timeout: 150000 });
|
|
225
227
|
}
|
|
226
228
|
async openByName(name) {
|
|
227
229
|
const processApplication = this.page
|
|
@@ -32,6 +32,7 @@ declare class ModelerHomePage {
|
|
|
32
32
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
33
33
|
readonly buttonSkipCustomization: Locator;
|
|
34
34
|
constructor(page: Page);
|
|
35
|
+
dismissBlockingDialog(): Promise<void>;
|
|
35
36
|
dismissOverlays(): Promise<void>;
|
|
36
37
|
clickCreateNewProjectButton(): Promise<void>;
|
|
37
38
|
enterNewProjectName(name: string): Promise<void>;
|
|
@@ -96,7 +96,19 @@ class ModelerHomePage {
|
|
|
96
96
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
97
97
|
this.messageBanner = page.locator('[data-test="close-top-banner"]');
|
|
98
98
|
this.dialog = page.getByRole('dialog');
|
|
99
|
-
|
|
99
|
+
// Both announcement modals hub stacks over the projects list dismiss
|
|
100
|
+
// through a "Got it…" footer button: the What's New modal's reads
|
|
101
|
+
// "Got it - Dismiss", and the migration notice ("Your projects have a new
|
|
102
|
+
// structure") reads just "Got it".
|
|
103
|
+
//
|
|
104
|
+
// Matched as a visible button rather than by role/name: the What's New
|
|
105
|
+
// modal stays mounted with `open={false}` after it is closed, so its
|
|
106
|
+
// dismiss button remains in the DOM but hidden, and a role/name match
|
|
107
|
+
// would resolve both and fail strict mode -- reporting "nothing to
|
|
108
|
+
// dismiss" while the migration notice is still covering the projects list.
|
|
109
|
+
this.closeButton = page
|
|
110
|
+
.locator('button:visible', { hasText: /^Got it/ })
|
|
111
|
+
.first();
|
|
100
112
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
101
113
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
102
114
|
hasText: formName,
|
|
@@ -105,7 +117,39 @@ class ModelerHomePage {
|
|
|
105
117
|
name: 'Skip customization',
|
|
106
118
|
});
|
|
107
119
|
}
|
|
120
|
+
// An announcement dialog renders over the projects list and swallows every
|
|
121
|
+
// click on it: Playwright reports the project link as visible, enabled and
|
|
122
|
+
// stable, then times out with the modal overlay "intercepts pointer events".
|
|
123
|
+
// Pressing Escape blindly does not help -- Carbon only closes the dialog when
|
|
124
|
+
// focus is inside it, and focus sits on the body after a page load -- so
|
|
125
|
+
// close it via its own dismiss button and confirm it is gone.
|
|
126
|
+
//
|
|
127
|
+
// The dialog is mounted a beat after the projects list, so let the page
|
|
128
|
+
// settle before looking for it, and re-probe after each close: hub can stack
|
|
129
|
+
// the What's New modal and the migration notice, and closing one reveals the
|
|
130
|
+
// next.
|
|
131
|
+
async dismissBlockingDialog() {
|
|
132
|
+
await this.page
|
|
133
|
+
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
134
|
+
.catch(() => false);
|
|
135
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
136
|
+
const isOpen = await this.closeButton
|
|
137
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
138
|
+
.then(() => true)
|
|
139
|
+
.catch(() => false);
|
|
140
|
+
if (!isOpen)
|
|
141
|
+
return;
|
|
142
|
+
await this.closeButton.click({ timeout: 5000 }).catch(() => { });
|
|
143
|
+
await this.closeButton
|
|
144
|
+
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
145
|
+
.catch(() => false);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
108
148
|
async dismissOverlays() {
|
|
149
|
+
// Before the generic pass: clickMessageBanner and the loop below resolve
|
|
150
|
+
// `.or(...).first()` unions in DOM order, where the top banner comes first,
|
|
151
|
+
// so on their own they close the banner and leave the dialog open.
|
|
152
|
+
await this.dismissBlockingDialog();
|
|
109
153
|
for (const locator of [
|
|
110
154
|
this.closeButton,
|
|
111
155
|
this.page.getByRole('button', { name: 'Close' }).first(),
|
|
@@ -147,6 +191,11 @@ class ModelerHomePage {
|
|
|
147
191
|
if (await committedProjectName.first().isVisible()) {
|
|
148
192
|
return;
|
|
149
193
|
}
|
|
194
|
+
// The rename runs on a freshly created project, which is exactly when an
|
|
195
|
+
// announcement dialog mounts on top of it: it takes focus, the inline
|
|
196
|
+
// editor unmounts, and the click below then waits out its whole timeout
|
|
197
|
+
// on an element that never comes back.
|
|
198
|
+
await this.dismissOverlays();
|
|
150
199
|
await this.projectNameInput.first().click({ timeout: 15000 });
|
|
151
200
|
await this.projectNameInput.first().fill(name);
|
|
152
201
|
// Confirm the value actually landed in the edit field before committing;
|
|
@@ -397,12 +446,17 @@ class ModelerHomePage {
|
|
|
397
446
|
.catch(() => { });
|
|
398
447
|
}
|
|
399
448
|
}
|
|
449
|
+
// An announcement dialog covering the Modeler header makes every click on the
|
|
450
|
+
// org button bounce off the modal layer ("intercepts pointer events") until
|
|
451
|
+
// the timeout, so dismiss overlays before each attempt.
|
|
400
452
|
async clickOpenOrganizationsButton() {
|
|
401
|
-
await (0,
|
|
402
|
-
|
|
403
|
-
|
|
453
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
|
|
454
|
+
preAction: async () => {
|
|
455
|
+
await this.dismissOverlays();
|
|
456
|
+
},
|
|
457
|
+
totalTimeout: 120000,
|
|
458
|
+
visibilityTimeout: 30000,
|
|
404
459
|
});
|
|
405
|
-
await this.openOrganizationsButton.click({ timeout: 30000 });
|
|
406
460
|
}
|
|
407
461
|
async createForm(formName) {
|
|
408
462
|
const maxRetries = 3;
|
|
@@ -429,24 +483,19 @@ class ModelerHomePage {
|
|
|
429
483
|
async clickCrossComponentProjectFolder() {
|
|
430
484
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.crossComponentProjectFolder, {
|
|
431
485
|
visibilityTimeout: 60000,
|
|
432
|
-
|
|
486
|
+
// Widened from 180s: dismissing the announcement dialog costs up to ~25s
|
|
487
|
+
// of settle-and-probe per attempt, which at 180s left room for barely one
|
|
488
|
+
// retry after an intercepted click had already burnt its 60s.
|
|
489
|
+
totalTimeout: 240000,
|
|
433
490
|
maxRetries: 5,
|
|
434
491
|
preAction: async () => {
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if ((await blockingModal.count()) > 0) {
|
|
443
|
-
await this.page.keyboard.press('Escape');
|
|
444
|
-
await (0, sleep_1.sleep)(1000);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
// No blocking modal to dismiss
|
|
449
|
-
}
|
|
492
|
+
// Dismiss the dialog BEFORE the banner: the Modeler home page can
|
|
493
|
+
// stack both at once, and clickMessageBanner resolves an
|
|
494
|
+
// `.or(...).first()` union in DOM order — the top banner comes first,
|
|
495
|
+
// so on its own it closes the banner and leaves the dialog open. The
|
|
496
|
+
// previous press-Escape-on-any-visible-modal handling could not close
|
|
497
|
+
// it at all: the migration notice ignores Escape.
|
|
498
|
+
await this.dismissBlockingDialog();
|
|
450
499
|
await this.clickMessageBanner();
|
|
451
500
|
},
|
|
452
501
|
postAction: async () => {
|
|
@@ -31,6 +31,7 @@ declare class ModelerHomePage {
|
|
|
31
31
|
readonly optionsButton: Locator;
|
|
32
32
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
33
33
|
constructor(page: Page);
|
|
34
|
+
dismissBlockingDialog(): Promise<void>;
|
|
34
35
|
dismissOverlays(): Promise<void>;
|
|
35
36
|
clickCreateNewProjectButton(): Promise<void>;
|
|
36
37
|
enterNewProjectName(name: string): Promise<void>;
|
|
@@ -100,13 +100,58 @@ class ModelerHomePage {
|
|
|
100
100
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
101
101
|
this.messageBanner = page.locator('[data-test="close-top-banner"]');
|
|
102
102
|
this.dialog = page.getByRole('dialog');
|
|
103
|
-
|
|
103
|
+
// Both announcement modals hub stacks over the projects list dismiss
|
|
104
|
+
// through a "Got it…" footer button: the What's New modal's reads
|
|
105
|
+
// "Got it - Dismiss", and the migration notice ("Your projects have a new
|
|
106
|
+
// structure") reads just "Got it".
|
|
107
|
+
//
|
|
108
|
+
// Matched as a visible button rather than by role/name: the What's New
|
|
109
|
+
// modal stays mounted with `open={false}` after it is closed, so its
|
|
110
|
+
// dismiss button remains in the DOM but hidden, and a role/name match
|
|
111
|
+
// would resolve both and fail strict mode -- reporting "nothing to
|
|
112
|
+
// dismiss" while the migration notice is still covering the projects list.
|
|
113
|
+
this.closeButton = page
|
|
114
|
+
.locator('button:visible', { hasText: /^Got it/ })
|
|
115
|
+
.first();
|
|
104
116
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
105
117
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
106
118
|
hasText: formName,
|
|
107
119
|
});
|
|
108
120
|
}
|
|
121
|
+
// An announcement dialog renders over the projects list and swallows every
|
|
122
|
+
// click on it: Playwright reports the project link as visible, enabled and
|
|
123
|
+
// stable, then times out with the modal overlay "intercepts pointer events".
|
|
124
|
+
// Pressing Escape blindly does not help -- Carbon only closes the dialog when
|
|
125
|
+
// focus is inside it, and focus sits on the body after a page load -- so
|
|
126
|
+
// close it via its own dismiss button and confirm it is gone.
|
|
127
|
+
//
|
|
128
|
+
// The dialog is mounted a beat after the projects list, so let the page
|
|
129
|
+
// settle before looking for it, and re-probe after each close: hub can stack
|
|
130
|
+
// the What's New modal and the migration notice, and closing one reveals the
|
|
131
|
+
// next.
|
|
132
|
+
async dismissBlockingDialog() {
|
|
133
|
+
await this.page
|
|
134
|
+
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
135
|
+
.catch(() => false);
|
|
136
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
137
|
+
const isOpen = await this.closeButton
|
|
138
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
139
|
+
.then(() => true)
|
|
140
|
+
.catch(() => false);
|
|
141
|
+
if (!isOpen)
|
|
142
|
+
return;
|
|
143
|
+
await this.closeButton.click({ timeout: 5000 }).catch(() => { });
|
|
144
|
+
await this.closeButton
|
|
145
|
+
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
146
|
+
.catch(() => false);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
109
149
|
async dismissOverlays() {
|
|
150
|
+
// Before the generic pass: the loop below closes whichever overlay it finds
|
|
151
|
+
// and moves on, so a single pass leaves a stacked announcement dialog open,
|
|
152
|
+
// and every caller that relies on dismissOverlays() to clear the page then
|
|
153
|
+
// clicks into it.
|
|
154
|
+
await this.dismissBlockingDialog();
|
|
110
155
|
for (const locator of [
|
|
111
156
|
this.closeButton,
|
|
112
157
|
this.page.getByRole('button', { name: 'Close' }).first(),
|
|
@@ -141,6 +186,11 @@ class ModelerHomePage {
|
|
|
141
186
|
if (await this.page.getByTitle(name, { exact: true }).isVisible()) {
|
|
142
187
|
return;
|
|
143
188
|
}
|
|
189
|
+
// The rename runs on a freshly created project, which is exactly when an
|
|
190
|
+
// announcement dialog mounts on top of it: it takes focus, the inline
|
|
191
|
+
// editor unmounts, and the click below then waits out its whole timeout
|
|
192
|
+
// on an element that never comes back.
|
|
193
|
+
await this.dismissBlockingDialog();
|
|
144
194
|
await this.projectNameInput.click({ timeout: 60000 });
|
|
145
195
|
await this.projectNameInput.fill(name);
|
|
146
196
|
await this.projectNameInput.press('Enter');
|
|
@@ -342,12 +392,18 @@ class ModelerHomePage {
|
|
|
342
392
|
await this.page.getByText('Home', { exact: true }).first().click();
|
|
343
393
|
}
|
|
344
394
|
}
|
|
395
|
+
// An announcement dialog covering the Modeler header makes every click on the
|
|
396
|
+
// org button bounce off the modal layer ("intercepts pointer events") until
|
|
397
|
+
// the timeout, so dismiss it before each attempt instead of waiting out a
|
|
398
|
+
// fixed sleep and hoping the header is clear.
|
|
345
399
|
async clickOpenOrganizationsButton() {
|
|
346
|
-
await (0,
|
|
347
|
-
|
|
348
|
-
|
|
400
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
|
|
401
|
+
preAction: async () => {
|
|
402
|
+
await this.dismissBlockingDialog();
|
|
403
|
+
},
|
|
404
|
+
totalTimeout: 120000,
|
|
405
|
+
visibilityTimeout: 30000,
|
|
349
406
|
});
|
|
350
|
-
await this.openOrganizationsButton.click({ timeout: 30000 });
|
|
351
407
|
}
|
|
352
408
|
async createForm(formName) {
|
|
353
409
|
await this.clickDiagramTypeDropdown();
|
|
@@ -361,26 +417,19 @@ class ModelerHomePage {
|
|
|
361
417
|
async clickCrossComponentProjectFolder() {
|
|
362
418
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.crossComponentProjectFolder, {
|
|
363
419
|
visibilityTimeout: 60000,
|
|
364
|
-
|
|
420
|
+
// Widened from 180s: dismissing the announcement dialog costs up to ~25s
|
|
421
|
+
// of settle-and-probe per attempt, which at 180s left room for barely one
|
|
422
|
+
// retry after an intercepted click had already burnt its 60s.
|
|
423
|
+
totalTimeout: 240000,
|
|
365
424
|
maxRetries: 5,
|
|
366
425
|
preAction: async () => {
|
|
367
|
-
// A Carbon modal (`.cds--modal.is-visible`) sometimes covers the
|
|
368
|
-
// project folder and intercepts pointer events. Escape it explicitly
|
|
369
|
-
// first — same pattern 8.7 already uses for the same symptom.
|
|
370
|
-
try {
|
|
371
|
-
const blockingModal = this.page.locator('.cds--modal.is-visible');
|
|
372
|
-
if ((await blockingModal.count()) > 0) {
|
|
373
|
-
await this.page.keyboard.press('Escape');
|
|
374
|
-
await (0, sleep_1.sleep)(1000);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// No blocking modal to dismiss
|
|
379
|
-
}
|
|
380
426
|
// dismissOverlays, not clickMessageBanner: the latter resolves
|
|
381
427
|
// `messageBanner.or(closeButton).first()`, so with a top banner on
|
|
382
428
|
// screen it always picks the banner and leaves any dialog in place.
|
|
383
|
-
// dismissOverlays
|
|
429
|
+
// dismissOverlays closes the announcement dialog first, then walks
|
|
430
|
+
// every other overlay control in turn. The previous
|
|
431
|
+
// press-Escape-on-any-visible-modal handling could not close the
|
|
432
|
+
// migration notice at all: it ignores Escape.
|
|
384
433
|
await this.dismissOverlays();
|
|
385
434
|
},
|
|
386
435
|
postAction: async () => {
|