@redhat-developer/page-objects 1.21.0 → 1.22.0
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/out/components/AbstractElement.js +2 -1
- package/out/components/ElementWithContextMenu.d.ts +11 -0
- package/out/components/ElementWithContextMenu.js +63 -15
- package/out/components/WebviewMixin.js +73 -7
- package/out/components/activityBar/ViewControl.d.ts +16 -0
- package/out/components/activityBar/ViewControl.js +45 -8
- package/out/components/bottomBar/BottomBarPanel.js +26 -11
- package/out/components/editor/ContentAssist.d.ts +9 -1
- package/out/components/editor/ContentAssist.js +88 -13
- package/out/components/editor/CustomEditor.js +1 -1
- package/out/components/editor/EditorView.d.ts +6 -0
- package/out/components/editor/EditorView.js +73 -3
- package/out/components/editor/SettingsEditor.d.ts +1 -0
- package/out/components/editor/SettingsEditor.js +17 -1
- package/out/components/editor/TextEditor.d.ts +3 -1
- package/out/components/editor/TextEditor.js +79 -52
- package/out/components/editor/WebView.js +6 -1
- package/out/components/menu/ContextMenu.d.ts +1 -1
- package/out/components/menu/ContextMenu.js +7 -4
- package/out/components/menu/Menu.js +3 -1
- package/out/components/sidebar/ViewItem.js +1 -0
- package/out/components/sidebar/tree/default/DefaultTreeSection.js +6 -0
- package/out/components/workbench/DebugToolbar.js +1 -1
- package/out/components/workbench/NotificationsCenter.js +6 -1
- package/out/components/workbench/Workbench.js +1 -1
- package/out/components/workbench/input/Input.d.ts +18 -0
- package/out/components/workbench/input/Input.js +68 -0
- package/out/components/workbench/input/QuickOpenBox.js +1 -1
- package/out/errors/ExTesterError.js +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/locators/locators.js +26 -3
- package/out/utils/TimeoutConstants.d.ts +59 -0
- package/out/utils/TimeoutConstants.js +63 -0
- package/out/utils/WaitHelper.js +8 -2
- package/package.json +6 -3
|
@@ -25,6 +25,7 @@ const DiffEditor_1 = require("./DiffEditor");
|
|
|
25
25
|
const EditorAction_1 = require("./EditorAction");
|
|
26
26
|
const SettingsEditor_1 = require("./SettingsEditor");
|
|
27
27
|
const WebView_1 = require("./WebView");
|
|
28
|
+
const ModalDialog_1 = require("../dialog/ModalDialog");
|
|
28
29
|
class EditorTabNotFound extends Error {
|
|
29
30
|
constructor(title, group) {
|
|
30
31
|
super(`No editor with title '${title}' in group '${group}' available`);
|
|
@@ -68,7 +69,8 @@ class EditorView extends AbstractElement_1.AbstractElement {
|
|
|
68
69
|
if (groupIndex !== undefined) {
|
|
69
70
|
return groups[groupIndex].closeAllEditors();
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
const deadline = Date.now() + 60000;
|
|
73
|
+
while (groups.length > 0 && (await groups[0].getOpenEditorTitles()).length > 0 && Date.now() < deadline) {
|
|
72
74
|
await groups[0].closeAllEditors();
|
|
73
75
|
// Brief wait for DOM to settle after closing editors
|
|
74
76
|
await this.getWaitHelper().sleep(500);
|
|
@@ -250,15 +252,39 @@ class EditorGroup extends AbstractElement_1.AbstractElement {
|
|
|
250
252
|
async closeEditor(title) {
|
|
251
253
|
const tab = await this.getTabByTitle(title);
|
|
252
254
|
const closeButton = await tab.findElement(EditorView.locators.EditorView.closeTab);
|
|
253
|
-
|
|
255
|
+
// VS Code renders hover tooltip overlays (<div class="hover-contents">) that can
|
|
256
|
+
// intercept the click (ElementClickInterceptedError) on CI. Retry with an
|
|
257
|
+
// increasing delay; fall back to a JS-executor click on the last attempt which
|
|
258
|
+
// bypasses overlay elements entirely.
|
|
259
|
+
const maxAttempts = 3;
|
|
260
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
261
|
+
try {
|
|
262
|
+
await closeButton.click();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
if (err.name !== 'ElementClickInterceptedError' || attempt === maxAttempts - 1) {
|
|
267
|
+
// On the final attempt use executeScript to bypass the overlay, then return.
|
|
268
|
+
if (err.name === 'ElementClickInterceptedError') {
|
|
269
|
+
await this.getDriver().executeScript('arguments[0].click()', closeButton);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
// Brief back-off so the tooltip has time to dismiss before the next attempt.
|
|
275
|
+
await this.getWaitHelper().sleep(300 * (attempt + 1));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
254
278
|
}
|
|
255
279
|
/**
|
|
256
280
|
* Close all open editor tabs
|
|
257
281
|
* @returns Promise resolving once all tabs have had their close button pressed
|
|
258
282
|
*/
|
|
259
283
|
async closeAllEditors() {
|
|
284
|
+
const deadline = Date.now() + 30000;
|
|
260
285
|
let titles = await this.getOpenEditorTitles();
|
|
261
|
-
while (titles.length > 0) {
|
|
286
|
+
while (titles.length > 0 && Date.now() < deadline) {
|
|
287
|
+
const previousCount = titles.length;
|
|
262
288
|
await this.closeEditor(titles[0]);
|
|
263
289
|
try {
|
|
264
290
|
// check if the group still exists
|
|
@@ -268,6 +294,50 @@ class EditorGroup extends AbstractElement_1.AbstractElement {
|
|
|
268
294
|
break;
|
|
269
295
|
}
|
|
270
296
|
titles = await this.getOpenEditorTitles();
|
|
297
|
+
if (titles.length >= previousCount) {
|
|
298
|
+
// The tab did not close: a dirty editor raises a "save changes?" modal
|
|
299
|
+
// that blocks the close. Discard the changes so the editor actually
|
|
300
|
+
// closes — otherwise the modal is left intercepting every subsequent
|
|
301
|
+
// click and cascades the rest of the suite into failure. Fall back to
|
|
302
|
+
// Escape (which cancels the modal) if the dialog cannot be handled.
|
|
303
|
+
if (!(await this.discardDirtyEditorDialog())) {
|
|
304
|
+
try {
|
|
305
|
+
await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
/* ignore */
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
await this.getWaitHelper().sleep(500);
|
|
312
|
+
titles = await this.getOpenEditorTitles();
|
|
313
|
+
if (titles.length >= previousCount) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* If a "save changes?" modal dialog is currently blocking the workbench,
|
|
321
|
+
* dismiss it by discarding the changes ("Don't Save") so the underlying
|
|
322
|
+
* dirty editor closes. Returns true when a dialog was found and handled.
|
|
323
|
+
*/
|
|
324
|
+
async discardDirtyEditorDialog() {
|
|
325
|
+
const dialogs = await this.getDriver().findElements(EditorView.locators.Dialog.constructor);
|
|
326
|
+
if (dialogs.length === 0) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const dialog = new ModalDialog_1.ModalDialog();
|
|
331
|
+
await dialog.pushButton(`Don't Save`);
|
|
332
|
+
await this.getDriver()
|
|
333
|
+
.wait(selenium_webdriver_1.until.stalenessOf(dialogs[0]), 5000)
|
|
334
|
+
.catch(() => {
|
|
335
|
+
/* dialog may already be gone */
|
|
336
|
+
});
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
271
341
|
}
|
|
272
342
|
}
|
|
273
343
|
/**
|
|
@@ -49,6 +49,7 @@ export declare class SettingsEditor extends Editor {
|
|
|
49
49
|
*/
|
|
50
50
|
findSettingByID(id: string): Promise<Setting>;
|
|
51
51
|
private _getSettingItem;
|
|
52
|
+
private scanForSettingItem;
|
|
52
53
|
/**
|
|
53
54
|
* Switch between settings perspectives
|
|
54
55
|
* Works only if your vscode instance has both user and workspace settings available
|
|
@@ -82,7 +82,23 @@ class SettingsEditor extends Editor_1.Editor {
|
|
|
82
82
|
const match = new RegExp(/\d+/).exec(text);
|
|
83
83
|
return match ? Number.parseInt(match[0], 10) : 0;
|
|
84
84
|
}, { timeout: 5000, stabilityInterval: 300, stableChecks: 2 });
|
|
85
|
+
// The count can stabilize before the search filter has been applied on slow
|
|
86
|
+
// machines — poll the rendered rows until the requested setting shows up
|
|
87
|
+
// instead of sampling them a single time.
|
|
88
|
+
const deadline = Date.now() + 10_000;
|
|
85
89
|
let setting;
|
|
90
|
+
for (;;) {
|
|
91
|
+
const found = await this.scanForSettingItem(title, category);
|
|
92
|
+
if (found) {
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
if (Date.now() >= deadline) {
|
|
96
|
+
return setting;
|
|
97
|
+
}
|
|
98
|
+
await this.getWaitHelper().sleep(500);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async scanForSettingItem(title, category) {
|
|
86
102
|
const items = await this.findElements(SettingsEditor.locators.SettingsEditor.itemRow);
|
|
87
103
|
for (const item of items) {
|
|
88
104
|
try {
|
|
@@ -105,7 +121,7 @@ class SettingsEditor extends Editor_1.Editor {
|
|
|
105
121
|
// do nothing
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
|
-
return
|
|
124
|
+
return undefined;
|
|
109
125
|
}
|
|
110
126
|
/**
|
|
111
127
|
* Switch between settings perspectives
|
|
@@ -221,8 +221,10 @@ export declare class TextEditor extends Editor {
|
|
|
221
221
|
* Get the cursor's coordinates as an array of two numbers: `[line, column]`
|
|
222
222
|
*
|
|
223
223
|
* **Caution** line & column coordinates do not start at `0` but at `1`!
|
|
224
|
+
*
|
|
225
|
+
* @param timeout ms to wait for the status bar to show a valid position (default 5000)
|
|
224
226
|
*/
|
|
225
|
-
getCoordinates(): Promise<[number, number]>;
|
|
227
|
+
getCoordinates(timeout?: number): Promise<[number, number]>;
|
|
226
228
|
private getTabSize;
|
|
227
229
|
/**
|
|
228
230
|
* Toggle breakpoint on a given line
|
|
@@ -19,7 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.FindWidget = exports.CodeLens = exports.TextEditor = exports.BreakpointError = void 0;
|
|
20
20
|
const __1 = require("../..");
|
|
21
21
|
const selenium_webdriver_1 = require("selenium-webdriver");
|
|
22
|
-
const
|
|
22
|
+
const node_url_1 = require("node:url");
|
|
23
23
|
const StatusBar_1 = require("../statusBar/StatusBar");
|
|
24
24
|
const Editor_1 = require("./Editor");
|
|
25
25
|
const ElementWithContextMenu_1 = require("../ElementWithContextMenu");
|
|
@@ -41,7 +41,7 @@ class TextEditor extends Editor_1.Editor {
|
|
|
41
41
|
async isDirty() {
|
|
42
42
|
const tab = await this.enclosingItem.findElement(TextEditor.locators.TextEditor.activeTab);
|
|
43
43
|
const klass = (await tab.getAttribute('class'));
|
|
44
|
-
return klass.
|
|
44
|
+
return klass.includes('dirty');
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Saves the active editor
|
|
@@ -74,7 +74,7 @@ class TextEditor extends Editor_1.Editor {
|
|
|
74
74
|
* @returns Promise resolving to editor's underlying file path
|
|
75
75
|
*/
|
|
76
76
|
async getFilePath() {
|
|
77
|
-
return (0,
|
|
77
|
+
return (0, node_url_1.fileURLToPath)(await this.getFileUri());
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* Open/Close the content assistant at the current position in the editor by sending the default
|
|
@@ -179,6 +179,13 @@ class TextEditor extends Editor_1.Editor {
|
|
|
179
179
|
* @returns Promise resolving once the text is deleted
|
|
180
180
|
*/
|
|
181
181
|
async clearText() {
|
|
182
|
+
// Dismiss any open menu or overlay first (e.g. context menu left open after
|
|
183
|
+
// formatDocument), then focus the editor container so the inputArea becomes
|
|
184
|
+
// interactable. We press Escape via the driver (safe regardless of focus) and
|
|
185
|
+
// click the outer editor element — NOT the native-edit-context, which cannot
|
|
186
|
+
// receive a synthetic click when it is not the active focus target.
|
|
187
|
+
await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
|
|
188
|
+
await this.enclosingItem.click();
|
|
182
189
|
return this.withRecovery(async (self) => {
|
|
183
190
|
const input = await self.findElement(TextEditor.locators.Editor.inputArea);
|
|
184
191
|
await input.sendKeys(selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'a'));
|
|
@@ -341,12 +348,16 @@ class TextEditor extends Editor_1.Editor {
|
|
|
341
348
|
async setCursor(line, column, timeout = 2_500) {
|
|
342
349
|
const input = await new __1.Workbench().openCommandPrompt();
|
|
343
350
|
await input.setText(`:${line},${column}`);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
351
|
+
try {
|
|
352
|
+
await this.getWaitHelper().forCondition(async () => {
|
|
353
|
+
const picks = await input.getQuickPicks().catch(() => []);
|
|
354
|
+
return picks.length > 0;
|
|
355
|
+
}, { timeout: 2000, pollInterval: 50 });
|
|
356
|
+
await input.selectQuickPick(0);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
await input.confirm();
|
|
360
|
+
}
|
|
350
361
|
await this.waitForCursorPositionAt(line, column, timeout);
|
|
351
362
|
}
|
|
352
363
|
/**
|
|
@@ -533,19 +544,9 @@ class TextEditor extends Editor_1.Editor {
|
|
|
533
544
|
}
|
|
534
545
|
async openContextMenu() {
|
|
535
546
|
await this.getDriver().actions().contextClick(this).perform();
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
const webdriverCapabilities = await this.getDriver().getCapabilities();
|
|
540
|
-
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
|
|
541
|
-
if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 96) {
|
|
542
|
-
shadowRoot = await shadowRootHost[0].getShadowRoot();
|
|
543
|
-
return new __1.ContextMenu(await shadowRoot.findElement(TextEditor.locators.TextEditor.monacoMenuContainer)).wait();
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
shadowRoot = (await this.getDriver().executeScript('return arguments[0].shadowRoot', shadowRootHost[0]));
|
|
547
|
-
return new __1.ContextMenu(shadowRoot).wait();
|
|
548
|
-
}
|
|
547
|
+
const menu = await this.waitForShadowRootMenu();
|
|
548
|
+
if (menu) {
|
|
549
|
+
return menu;
|
|
549
550
|
}
|
|
550
551
|
return await super.openContextMenu();
|
|
551
552
|
}
|
|
@@ -553,15 +554,30 @@ class TextEditor extends Editor_1.Editor {
|
|
|
553
554
|
* Get the cursor's coordinates as an array of two numbers: `[line, column]`
|
|
554
555
|
*
|
|
555
556
|
* **Caution** line & column coordinates do not start at `0` but at `1`!
|
|
557
|
+
*
|
|
558
|
+
* @param timeout ms to wait for the status bar to show a valid position (default 5000)
|
|
556
559
|
*/
|
|
557
|
-
async getCoordinates() {
|
|
558
|
-
const coords = [];
|
|
560
|
+
async getCoordinates(timeout = 5_000) {
|
|
559
561
|
const statusBar = new StatusBar_1.StatusBar();
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
562
|
+
// The status bar position item may not update immediately after a cursor move,
|
|
563
|
+
// especially on slow CI runners. Poll until it returns a valid "Ln X, Col Y" string.
|
|
564
|
+
let positionText = '';
|
|
565
|
+
await this.getDriver().wait(async () => {
|
|
566
|
+
try {
|
|
567
|
+
const text = await statusBar.getCurrentPosition();
|
|
568
|
+
const match = text.match(/\d+/g);
|
|
569
|
+
if (match && match.length >= 2) {
|
|
570
|
+
positionText = text;
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Status bar item not yet available — keep polling
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}, timeout, `Status bar did not show a valid cursor position within ${timeout}ms`);
|
|
579
|
+
const coordinates = positionText.match(/\d+/g);
|
|
580
|
+
return [+coordinates[0], +coordinates[1]];
|
|
565
581
|
}
|
|
566
582
|
async getTabSize() {
|
|
567
583
|
const statusBar = new StatusBar_1.StatusBar();
|
|
@@ -585,7 +601,7 @@ class TextEditor extends Editor_1.Editor {
|
|
|
585
601
|
: lineOverlay;
|
|
586
602
|
const breakPoint = await breakpointContainer.findElements(TextEditor.locators.TextEditor.breakpoint.generalSelector);
|
|
587
603
|
if (breakPoint.length > 0) {
|
|
588
|
-
if (this.breakPoints.
|
|
604
|
+
if (this.breakPoints.includes(line)) {
|
|
589
605
|
await breakPoint[this.breakPoints.indexOf(line)].click();
|
|
590
606
|
// Wait for breakpoint to be removed
|
|
591
607
|
await this.getWaitHelper().forCondition(async () => {
|
|
@@ -665,15 +681,23 @@ class TextEditor extends Editor_1.Editor {
|
|
|
665
681
|
async getBreakpoints() {
|
|
666
682
|
const breakpoints = [];
|
|
667
683
|
const breakpointLocators = Breakpoint_1.Breakpoint.locators.TextEditor.breakpoint;
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
: this;
|
|
684
|
+
const isNewVersion = (0, compare_versions_1.satisfies)(TextEditor.versionInfo.version, '>=1.80.0');
|
|
685
|
+
const breakpointContainer = isNewVersion ? await this.findElement(TextEditor.locators.TextEditor.glyphMarginWidget) : this;
|
|
671
686
|
const breakpointsSelectors = await breakpointContainer.findElements(breakpointLocators.generalSelector);
|
|
687
|
+
// When a debug session is paused at a breakpoint, VS Code renders two overlapping
|
|
688
|
+
// elements with the `codicon-debug-breakpoint` class at the same vertical position
|
|
689
|
+
// (the regular breakpoint dot and the stackframe arrow). Deduplicate by `top` so
|
|
690
|
+
// that each line contributes at most one Breakpoint.
|
|
691
|
+
const seenTops = new Set();
|
|
672
692
|
for (const breakpointSelector of breakpointsSelectors) {
|
|
673
693
|
try {
|
|
674
694
|
let lineElement;
|
|
675
|
-
if (
|
|
695
|
+
if (isNewVersion) {
|
|
676
696
|
const styleTopAttr = await breakpointSelector.getCssValue('top');
|
|
697
|
+
if (seenTops.has(styleTopAttr)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
seenTops.add(styleTopAttr);
|
|
677
701
|
lineElement = await this.findElement(TextEditor.locators.TextEditor.marginArea).findElement(TextEditor.locators.TextEditor.lineElement(styleTopAttr));
|
|
678
702
|
}
|
|
679
703
|
else {
|
|
@@ -696,14 +720,23 @@ class TextEditor extends Editor_1.Editor {
|
|
|
696
720
|
*/
|
|
697
721
|
async getCodeLenses() {
|
|
698
722
|
const lenses = [];
|
|
699
|
-
|
|
700
|
-
|
|
723
|
+
// Use findElements so that a missing/not-yet-visible contentWidgets container
|
|
724
|
+
// returns an empty array rather than throwing a TimeoutError. The caller
|
|
725
|
+
// (or the before-hook poll loop) is responsible for retrying until lenses appear.
|
|
726
|
+
const widgetContainers = await this.findElements(TextEditor.locators.TextEditor.contentWidgets);
|
|
727
|
+
if (widgetContainers.length === 0) {
|
|
728
|
+
return lenses;
|
|
729
|
+
}
|
|
730
|
+
const items = await widgetContainers[0].findElements(TextEditor.locators.TextEditor.contentWidgetsElements);
|
|
701
731
|
for (const item of items) {
|
|
702
732
|
try {
|
|
703
733
|
lenses.push(await new CodeLens(item, this).wait());
|
|
704
734
|
}
|
|
705
735
|
catch (e) {
|
|
706
|
-
|
|
736
|
+
// Skip stale or not-yet-visible lens elements. TimeoutError means the
|
|
737
|
+
// lens widget exists in the DOM but is not visible yet (e.g. the editor
|
|
738
|
+
// is still rendering); the caller's poll loop will retry the whole call.
|
|
739
|
+
if (e.name === 'StaleElementReferenceError' || e.name === 'TimeoutError') {
|
|
707
740
|
continue;
|
|
708
741
|
}
|
|
709
742
|
throw e;
|
|
@@ -779,19 +812,9 @@ class Selection extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
779
812
|
}
|
|
780
813
|
}
|
|
781
814
|
}
|
|
782
|
-
const
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
const webdriverCapabilities = await this.getDriver().getCapabilities();
|
|
786
|
-
const chromiumVersion = webdriverCapabilities.getBrowserVersion();
|
|
787
|
-
if (chromiumVersion && parseInt(chromiumVersion.split('.')[0]) >= 96) {
|
|
788
|
-
shadowRoot = await shadowRootHost[0].getShadowRoot();
|
|
789
|
-
return new __1.ContextMenu(await shadowRoot.findElement(TextEditor.locators.TextEditor.monacoMenuContainer)).wait();
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
shadowRoot = (await this.getDriver().executeScript('return arguments[0].shadowRoot', shadowRootHost[0]));
|
|
793
|
-
return new __1.ContextMenu(shadowRoot).wait();
|
|
794
|
-
}
|
|
815
|
+
const menu = await this.waitForShadowRootMenu();
|
|
816
|
+
if (menu) {
|
|
817
|
+
return menu;
|
|
795
818
|
}
|
|
796
819
|
return await super.openContextMenu();
|
|
797
820
|
}
|
|
@@ -973,8 +996,12 @@ class FindWidget extends AbstractElement_1.AbstractElement {
|
|
|
973
996
|
}
|
|
974
997
|
const btn = await element.findElement(FindWidget.locators.FindWidget.button(title));
|
|
975
998
|
await btn.click();
|
|
976
|
-
//
|
|
977
|
-
|
|
999
|
+
// Give the widget a moment to settle after the action (animations, relayout).
|
|
1000
|
+
// Best-effort: a slow animation must not fail the interaction that already
|
|
1001
|
+
// happened, so a stability timeout is not an error here.
|
|
1002
|
+
await this.getWaitHelper()
|
|
1003
|
+
.forStable(element, { timeout: 2000 })
|
|
1004
|
+
.catch(() => undefined);
|
|
978
1005
|
}
|
|
979
1006
|
async setText(text, composite) {
|
|
980
1007
|
const input = await composite.findElement(FindWidget.locators.FindWidget.input);
|
|
@@ -29,7 +29,12 @@ const locators_1 = require("../../locators/locators");
|
|
|
29
29
|
class WebViewBase extends Editor_1.Editor {
|
|
30
30
|
async getViewToSwitchTo() {
|
|
31
31
|
const frames = await this.getDriver().findElements(WebViewBase.locators.WebView.iframe);
|
|
32
|
-
|
|
32
|
+
if (frames.length === 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
// Try geometry-based selection first; fall back to the first available frame
|
|
36
|
+
// when the editor container has not been laid out yet (common on slow CI runners).
|
|
37
|
+
return (await (0, locators_1.findBestContainingElement)(await this.getRect(), frames)) ?? frames[0];
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
exports.WebView = (0, WebviewMixin_1.default)(WebViewBase);
|
|
@@ -20,7 +20,7 @@ import { WebElement } from 'selenium-webdriver';
|
|
|
20
20
|
* Object representing a context menu
|
|
21
21
|
*/
|
|
22
22
|
export declare class ContextMenu extends Menu {
|
|
23
|
-
constructor(containingElement: WebElement);
|
|
23
|
+
constructor(containingElement: WebElement, menuElement?: WebElement);
|
|
24
24
|
/**
|
|
25
25
|
* Get context menu item by name
|
|
26
26
|
* @param name name of the item to search by
|
|
@@ -23,8 +23,11 @@ const selenium_webdriver_1 = require("selenium-webdriver");
|
|
|
23
23
|
* Object representing a context menu
|
|
24
24
|
*/
|
|
25
25
|
class ContextMenu extends __1.Menu {
|
|
26
|
-
constructor(containingElement) {
|
|
27
|
-
|
|
26
|
+
constructor(containingElement, menuElement) {
|
|
27
|
+
// menuElement covers menus whose container cannot be found by locator
|
|
28
|
+
// from the containing element, e.g. the menu inside a shadow root where
|
|
29
|
+
// the root's only child IS the menu container.
|
|
30
|
+
super(menuElement ?? ContextMenu.locators.ContextMenu.constructor, containingElement);
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* Get context menu item by name
|
|
@@ -84,7 +87,7 @@ class ContextMenu extends __1.Menu {
|
|
|
84
87
|
await actions.clear();
|
|
85
88
|
await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
|
|
86
89
|
try {
|
|
87
|
-
await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this));
|
|
90
|
+
await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this), 5_000);
|
|
88
91
|
}
|
|
89
92
|
catch (err) {
|
|
90
93
|
if (!(err instanceof selenium_webdriver_1.error.StaleElementReferenceError)) {
|
|
@@ -146,7 +149,7 @@ class ContextMenuItem extends __1.MenuItem {
|
|
|
146
149
|
const actions = this.getDriver().actions();
|
|
147
150
|
await actions.clear();
|
|
148
151
|
await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
|
|
149
|
-
await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this));
|
|
152
|
+
await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this), 5_000);
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
catch (err) {
|
|
@@ -52,9 +52,11 @@ class Menu extends AbstractElement_1.AbstractElement {
|
|
|
52
52
|
if (!item) {
|
|
53
53
|
return parent;
|
|
54
54
|
}
|
|
55
|
+
// Guard with an explicit timeout so the wait never hangs indefinitely
|
|
56
|
+
// on CI where the implicit/default Selenium timeout may be 0 or very low.
|
|
55
57
|
await Menu.driver.wait(async function () {
|
|
56
58
|
return (await item.isDisplayed()) && (await item.isEnabled());
|
|
57
|
-
});
|
|
59
|
+
}, 5000);
|
|
58
60
|
const submenu = await item.select();
|
|
59
61
|
if (submenu) {
|
|
60
62
|
parent = submenu;
|
|
@@ -68,6 +68,7 @@ class TreeItem extends ViewItem {
|
|
|
68
68
|
async expand() {
|
|
69
69
|
if ((await this.isExpandable()) && !(await this.isExpanded())) {
|
|
70
70
|
await (await this.findTwistie()).click();
|
|
71
|
+
await this.getDriver().wait(() => this.isExpanded(), 5000, 'Tree item did not expand within 5s');
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
@@ -45,7 +45,13 @@ class DefaultTreeSection extends TreeSection_1.TreeSection {
|
|
|
45
45
|
const container = await this.findElement(DefaultTreeSection.locators.DefaultTreeSection.rowContainer);
|
|
46
46
|
await container.sendKeys(selenium_webdriver_1.Key.HOME);
|
|
47
47
|
let item = undefined;
|
|
48
|
+
const maxIterations = 500;
|
|
49
|
+
let iteration = 0;
|
|
48
50
|
do {
|
|
51
|
+
if (iteration++ >= maxIterations) {
|
|
52
|
+
console.warn(`findItem('${label}'): exceeded ${maxIterations} scroll iterations, giving up`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
49
55
|
const temp = await container.findElements(DefaultTreeSection.locators.DefaultTreeItem.ctor(label));
|
|
50
56
|
if (temp.length > 0) {
|
|
51
57
|
const level = +(await temp[0].getAttribute(DefaultTreeSection.locators.ViewSection.level));
|
|
@@ -42,7 +42,7 @@ class DebugToolbar extends AbstractElement_1.AbstractElement {
|
|
|
42
42
|
* Wait for the execution to pause at the next breakpoint
|
|
43
43
|
*/
|
|
44
44
|
async waitForBreakPoint(timeout = 10_000) {
|
|
45
|
-
let btn = await this.getDriver().wait(selenium_webdriver_1.until.elementLocated(DebugToolbar.locators.DebugToolbar.button('continue')));
|
|
45
|
+
let btn = await this.getDriver().wait(selenium_webdriver_1.until.elementLocated(DebugToolbar.locators.DebugToolbar.button('continue')), timeout);
|
|
46
46
|
await this.getDriver().wait(async () => {
|
|
47
47
|
try {
|
|
48
48
|
const enabled = await btn.isEnabled();
|
|
@@ -48,7 +48,12 @@ class NotificationsCenter extends AbstractElement_1.AbstractElement {
|
|
|
48
48
|
* @returns Promise resolving when the clear all button is pressed
|
|
49
49
|
*/
|
|
50
50
|
async clearAllNotifications() {
|
|
51
|
-
await this.findElement(NotificationsCenter.locators.NotificationsCenter.clear)
|
|
51
|
+
const btn = await this.findElement(NotificationsCenter.locators.NotificationsCenter.clear);
|
|
52
|
+
const classes = (await btn.getAttribute('class')) ?? '';
|
|
53
|
+
if (classes.includes('disabled')) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await this.getDriver().executeScript('arguments[0].click()', btn);
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Get all notifications of a given type
|
|
@@ -115,7 +115,7 @@ class Workbench extends AbstractElement_1.AbstractElement {
|
|
|
115
115
|
async openSettings() {
|
|
116
116
|
await this.executeCommand('Preferences: Open User Settings');
|
|
117
117
|
await new EditorView_1.EditorView().openEditor('Settings');
|
|
118
|
-
await Workbench.driver.wait(selenium_webdriver_1.until.elementLocated(Workbench.locators.Editor.constructor));
|
|
118
|
+
await Workbench.driver.wait(selenium_webdriver_1.until.elementLocated(Workbench.locators.Editor.constructor), 10_000);
|
|
119
119
|
// Wait for settings editor to fully load
|
|
120
120
|
const editor = new SettingsEditor_1.SettingsEditor();
|
|
121
121
|
await this.getWaitHelper().forStable(editor, { timeout: 2000 });
|
|
@@ -41,6 +41,24 @@ export declare abstract class Input extends AbstractElement {
|
|
|
41
41
|
* @returns Promise resolving when the input is confirmed
|
|
42
42
|
*/
|
|
43
43
|
confirm(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Confirm path selection for file/folder dialog.
|
|
46
|
+
*
|
|
47
|
+
* VS Code's file picker auto-navigates as text is typed: when a full path such as
|
|
48
|
+
* `/dir/file.txt` is in the input, the picker shows the parent directory contents
|
|
49
|
+
* with the filename filtered in the list. A single Enter press navigates into the
|
|
50
|
+
* matched list entry. A subsequent click on the OK button (or a second Enter) then
|
|
51
|
+
* confirms the selection and closes the dialog.
|
|
52
|
+
*
|
|
53
|
+
* When the path is not formatted to OS specifics there may first be a 'Select' step.
|
|
54
|
+
* See also https://github.com/redhat-developer/vscode-extension-tester/issues/1778
|
|
55
|
+
*/
|
|
56
|
+
confirmPath(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort check whether this input/picker widget is still displayed.
|
|
59
|
+
* A hidden, detached or stale widget counts as closed.
|
|
60
|
+
*/
|
|
61
|
+
private isPickerOpen;
|
|
44
62
|
/**
|
|
45
63
|
* Cancel the input field by pressing Escape
|
|
46
64
|
* @returns Promise resolving when the input is cancelled
|
|
@@ -86,6 +86,74 @@ class Input extends AbstractElement_1.AbstractElement {
|
|
|
86
86
|
await input.click();
|
|
87
87
|
await input.sendKeys(selenium_webdriver_1.Key.ENTER);
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Confirm path selection for file/folder dialog.
|
|
91
|
+
*
|
|
92
|
+
* VS Code's file picker auto-navigates as text is typed: when a full path such as
|
|
93
|
+
* `/dir/file.txt` is in the input, the picker shows the parent directory contents
|
|
94
|
+
* with the filename filtered in the list. A single Enter press navigates into the
|
|
95
|
+
* matched list entry. A subsequent click on the OK button (or a second Enter) then
|
|
96
|
+
* confirms the selection and closes the dialog.
|
|
97
|
+
*
|
|
98
|
+
* When the path is not formatted to OS specifics there may first be a 'Select' step.
|
|
99
|
+
* See also https://github.com/redhat-developer/vscode-extension-tester/issues/1778
|
|
100
|
+
*/
|
|
101
|
+
async confirmPath() {
|
|
102
|
+
// Step 1 — drive VS Code's file picker to navigate into the typed path.
|
|
103
|
+
// Sending Enter selects the highlighted entry in the suggestion list, which
|
|
104
|
+
// causes the picker to consume the directory portion and reveal the target file.
|
|
105
|
+
const inputEl = await this.findElement(Input.locators.Input.inputBox).findElement(Input.locators.Input.input);
|
|
106
|
+
await inputEl.sendKeys(selenium_webdriver_1.Key.ENTER);
|
|
107
|
+
// When the typed path uniquely resolves the target, the first Enter both
|
|
108
|
+
// navigates and confirms — the picker closes on its own and no second step
|
|
109
|
+
// is needed (attempting one would hit a hidden input).
|
|
110
|
+
if (!(await this.isPickerOpen())) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Step 2 — click the OK button to confirm and close the dialog.
|
|
114
|
+
// Use a JS click to bypass any overlay that may intercept a normal WebDriver click.
|
|
115
|
+
// Falls back to a second Enter if no matching button is found.
|
|
116
|
+
try {
|
|
117
|
+
const buttons = await this.findElements(selenium_webdriver_1.By.className('monaco-button'));
|
|
118
|
+
for (const btn of buttons) {
|
|
119
|
+
if (await btn.isDisplayed()) {
|
|
120
|
+
const label = ((await btn.getText()) || (await btn.getAttribute('aria-label')) || '').trim();
|
|
121
|
+
if (label === 'Select' || label === 'Ok' || label === 'OK') {
|
|
122
|
+
await Input.driver.executeScript('arguments[0].click()', btn);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// ignore if button is not found or stale
|
|
130
|
+
}
|
|
131
|
+
if (!(await this.isPickerOpen())) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await inputEl.sendKeys(selenium_webdriver_1.Key.ENTER);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// The picker can close in the instant between the check above and the
|
|
139
|
+
// keystroke — a failure only matters while the picker is still open.
|
|
140
|
+
if (await this.isPickerOpen()) {
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Best-effort check whether this input/picker widget is still displayed.
|
|
147
|
+
* A hidden, detached or stale widget counts as closed.
|
|
148
|
+
*/
|
|
149
|
+
async isPickerOpen() {
|
|
150
|
+
try {
|
|
151
|
+
return await this.isDisplayed();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
89
157
|
/**
|
|
90
158
|
* Cancel the input field by pressing Escape
|
|
91
159
|
* @returns Promise resolving when the input is cancelled
|
|
@@ -32,7 +32,7 @@ class QuickOpenBox extends __1.Input {
|
|
|
32
32
|
* Use when a quick open box is scheduled to appear.
|
|
33
33
|
*/
|
|
34
34
|
static async create() {
|
|
35
|
-
await QuickOpenBox.driver.wait(selenium_webdriver_1.until.elementLocated(QuickOpenBox.locators.QuickOpenBox.constructor));
|
|
35
|
+
await QuickOpenBox.driver.wait(selenium_webdriver_1.until.elementLocated(QuickOpenBox.locators.QuickOpenBox.constructor), 10_000);
|
|
36
36
|
return new QuickOpenBox().wait();
|
|
37
37
|
}
|
|
38
38
|
async hasProgress() {
|
|
@@ -101,7 +101,7 @@ exports.ElementNotInteractableError = ElementNotInteractableError;
|
|
|
101
101
|
class StaleElementError extends ExTesterError {
|
|
102
102
|
constructor(message, context = {}) {
|
|
103
103
|
super(message, context);
|
|
104
|
-
this.name = '
|
|
104
|
+
this.name = 'StaleElementReferenceError';
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
exports.StaleElementError = StaleElementError;
|
package/out/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './locators/locators';
|
|
|
21
21
|
export * from './errors/NullAttributeError';
|
|
22
22
|
export * from './errors/ExTesterError';
|
|
23
23
|
export * from './utils/WaitHelper';
|
|
24
|
+
export * from './utils/TimeoutConstants';
|
|
24
25
|
export * from './components/menu/Menu';
|
|
25
26
|
export * from './components/menu/MenuItem';
|
|
26
27
|
export * from './components/menu/TitleBar';
|