@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.
Files changed (36) hide show
  1. package/out/components/AbstractElement.js +2 -1
  2. package/out/components/ElementWithContextMenu.d.ts +11 -0
  3. package/out/components/ElementWithContextMenu.js +63 -15
  4. package/out/components/WebviewMixin.js +73 -7
  5. package/out/components/activityBar/ViewControl.d.ts +16 -0
  6. package/out/components/activityBar/ViewControl.js +45 -8
  7. package/out/components/bottomBar/BottomBarPanel.js +26 -11
  8. package/out/components/editor/ContentAssist.d.ts +9 -1
  9. package/out/components/editor/ContentAssist.js +88 -13
  10. package/out/components/editor/CustomEditor.js +1 -1
  11. package/out/components/editor/EditorView.d.ts +6 -0
  12. package/out/components/editor/EditorView.js +73 -3
  13. package/out/components/editor/SettingsEditor.d.ts +1 -0
  14. package/out/components/editor/SettingsEditor.js +17 -1
  15. package/out/components/editor/TextEditor.d.ts +3 -1
  16. package/out/components/editor/TextEditor.js +79 -52
  17. package/out/components/editor/WebView.js +6 -1
  18. package/out/components/menu/ContextMenu.d.ts +1 -1
  19. package/out/components/menu/ContextMenu.js +7 -4
  20. package/out/components/menu/Menu.js +3 -1
  21. package/out/components/sidebar/ViewItem.js +1 -0
  22. package/out/components/sidebar/tree/default/DefaultTreeSection.js +6 -0
  23. package/out/components/workbench/DebugToolbar.js +1 -1
  24. package/out/components/workbench/NotificationsCenter.js +6 -1
  25. package/out/components/workbench/Workbench.js +1 -1
  26. package/out/components/workbench/input/Input.d.ts +18 -0
  27. package/out/components/workbench/input/Input.js +68 -0
  28. package/out/components/workbench/input/QuickOpenBox.js +1 -1
  29. package/out/errors/ExTesterError.js +1 -1
  30. package/out/index.d.ts +1 -0
  31. package/out/index.js +1 -0
  32. package/out/locators/locators.js +26 -3
  33. package/out/utils/TimeoutConstants.d.ts +59 -0
  34. package/out/utils/TimeoutConstants.js +63 -0
  35. package/out/utils/WaitHelper.js +8 -2
  36. 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
- while (groups.length > 0 && (await groups[0].getOpenEditorTitles()).length > 0) {
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
- await closeButton.click();
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 setting;
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 url_1 = require("url");
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.indexOf('dirty') >= 0;
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, url_1.fileURLToPath)(await this.getFileUri());
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
- // Wait for input to be processed - check for quick picks appearing
345
- await this.getWaitHelper().forCondition(async () => {
346
- const picks = await input.getQuickPicks().catch(() => []);
347
- return picks.length > 0;
348
- }, { timeout: 1000, pollInterval: 50 });
349
- await input.selectQuickPick(0);
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 shadowRootHost = await this.enclosingItem.findElements(TextEditor.locators.TextEditor.shadowRootHost);
537
- if (shadowRootHost.length > 0) {
538
- let shadowRoot;
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
- const coordinates = (await statusBar.getCurrentPosition()).match(/\d+/g);
561
- for (const c of coordinates) {
562
- coords.push(+c);
563
- }
564
- return [coords[0], coords[1]];
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.indexOf(line) !== -1) {
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 breakpointContainer = (0, compare_versions_1.satisfies)(TextEditor.versionInfo.version, '>=1.80.0')
669
- ? await this.findElement(TextEditor.locators.TextEditor.glyphMarginWidget)
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 ((0, compare_versions_1.satisfies)(TextEditor.versionInfo.version, '>=1.80.0')) {
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
- const widgets = await this.findElement(TextEditor.locators.TextEditor.contentWidgets);
700
- const items = await widgets.findElements(TextEditor.locators.TextEditor.contentWidgetsElements);
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
- if (e.name === 'StaleElementReferenceError') {
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 shadowRootHost = await ed.getEnclosingElement().findElements(TextEditor.locators.TextEditor.shadowRootHost);
783
- if (shadowRootHost.length > 0) {
784
- let shadowRoot;
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
- // Wait for button action to complete
977
- await this.getWaitHelper().forStable(element, { timeout: 500 });
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
- return (0, locators_1.findBestContainingElement)(await this.getRect(), frames);
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
- super(ContextMenu.locators.ContextMenu.constructor, containingElement);
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).click();
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 = 'StaleElementError';
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';