@redhat-developer/page-objects 1.21.0 → 1.23.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 (39) hide show
  1. package/README.md +59 -4
  2. package/out/components/AbstractElement.d.ts +8 -0
  3. package/out/components/AbstractElement.js +12 -1
  4. package/out/components/ElementWithContextMenu.d.ts +11 -0
  5. package/out/components/ElementWithContextMenu.js +63 -15
  6. package/out/components/WebviewMixin.js +73 -7
  7. package/out/components/activityBar/ViewControl.d.ts +16 -0
  8. package/out/components/activityBar/ViewControl.js +45 -8
  9. package/out/components/bottomBar/BottomBarPanel.js +26 -11
  10. package/out/components/editor/ContentAssist.d.ts +9 -1
  11. package/out/components/editor/ContentAssist.js +88 -13
  12. package/out/components/editor/CustomEditor.js +1 -1
  13. package/out/components/editor/EditorView.d.ts +6 -0
  14. package/out/components/editor/EditorView.js +53 -3
  15. package/out/components/editor/SettingsEditor.d.ts +1 -0
  16. package/out/components/editor/SettingsEditor.js +17 -1
  17. package/out/components/editor/TextEditor.d.ts +3 -1
  18. package/out/components/editor/TextEditor.js +95 -56
  19. package/out/components/editor/WebView.js +6 -1
  20. package/out/components/menu/ContextMenu.d.ts +1 -1
  21. package/out/components/menu/ContextMenu.js +7 -4
  22. package/out/components/menu/Menu.js +3 -1
  23. package/out/components/sidebar/ViewItem.js +1 -0
  24. package/out/components/sidebar/tree/default/DefaultTreeSection.js +6 -0
  25. package/out/components/workbench/DebugToolbar.js +1 -1
  26. package/out/components/workbench/NotificationsCenter.js +6 -1
  27. package/out/components/workbench/Workbench.js +1 -1
  28. package/out/components/workbench/input/Input.d.ts +18 -0
  29. package/out/components/workbench/input/Input.js +68 -0
  30. package/out/components/workbench/input/QuickOpenBox.js +1 -1
  31. package/out/errors/ExTesterError.js +1 -1
  32. package/out/index.d.ts +1 -0
  33. package/out/index.js +1 -0
  34. package/out/locators/locators.js +26 -3
  35. package/out/utils/TimeoutConstants.d.ts +59 -0
  36. package/out/utils/TimeoutConstants.js +63 -0
  37. package/out/utils/WaitHelper.d.ts +31 -0
  38. package/out/utils/WaitHelper.js +78 -2
  39. package/package.json +6 -3
@@ -120,6 +120,12 @@ export declare class EditorGroup extends AbstractElement {
120
120
  * @returns Promise resolving once all tabs have had their close button pressed
121
121
  */
122
122
  closeAllEditors(): Promise<void>;
123
+ /**
124
+ * If a "save changes?" modal dialog is currently blocking the workbench,
125
+ * dismiss it by discarding the changes ("Don't Save") so the underlying
126
+ * dirty editor closes. Returns true when a dialog was found and handled.
127
+ */
128
+ private discardDirtyEditorDialog;
123
129
  /**
124
130
  * Retrieve all open editor tab titles in an array
125
131
  * @returns Promise resolving to array of editor titles
@@ -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,19 @@ 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
+ // hover tooltip overlays can intercept the click on CI - the centralized
256
+ // recovery parks the pointer to dismiss them and retries the native click
257
+ await this.getWaitHelper().clickThroughInterception(closeButton);
254
258
  }
255
259
  /**
256
260
  * Close all open editor tabs
257
261
  * @returns Promise resolving once all tabs have had their close button pressed
258
262
  */
259
263
  async closeAllEditors() {
264
+ const deadline = Date.now() + 30000;
260
265
  let titles = await this.getOpenEditorTitles();
261
- while (titles.length > 0) {
266
+ while (titles.length > 0 && Date.now() < deadline) {
267
+ const previousCount = titles.length;
262
268
  await this.closeEditor(titles[0]);
263
269
  try {
264
270
  // check if the group still exists
@@ -268,6 +274,50 @@ class EditorGroup extends AbstractElement_1.AbstractElement {
268
274
  break;
269
275
  }
270
276
  titles = await this.getOpenEditorTitles();
277
+ if (titles.length >= previousCount) {
278
+ // The tab did not close: a dirty editor raises a "save changes?" modal
279
+ // that blocks the close. Discard the changes so the editor actually
280
+ // closes — otherwise the modal is left intercepting every subsequent
281
+ // click and cascades the rest of the suite into failure. Fall back to
282
+ // Escape (which cancels the modal) if the dialog cannot be handled.
283
+ if (!(await this.discardDirtyEditorDialog())) {
284
+ try {
285
+ await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
286
+ }
287
+ catch {
288
+ /* ignore */
289
+ }
290
+ }
291
+ await this.getWaitHelper().sleep(500);
292
+ titles = await this.getOpenEditorTitles();
293
+ if (titles.length >= previousCount) {
294
+ break;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ /**
300
+ * If a "save changes?" modal dialog is currently blocking the workbench,
301
+ * dismiss it by discarding the changes ("Don't Save") so the underlying
302
+ * dirty editor closes. Returns true when a dialog was found and handled.
303
+ */
304
+ async discardDirtyEditorDialog() {
305
+ const dialogs = await this.getDriver().findElements(EditorView.locators.Dialog.constructor);
306
+ if (dialogs.length === 0) {
307
+ return false;
308
+ }
309
+ try {
310
+ const dialog = new ModalDialog_1.ModalDialog();
311
+ await dialog.pushButton(`Don't Save`);
312
+ await this.getDriver()
313
+ .wait(selenium_webdriver_1.until.stalenessOf(dialogs[0]), 5000)
314
+ .catch(() => {
315
+ /* dialog may already be gone */
316
+ });
317
+ return true;
318
+ }
319
+ catch {
320
+ return false;
271
321
  }
272
322
  }
273
323
  /**
@@ -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
@@ -128,8 +128,12 @@ class TextEditor extends Editor_1.Editor {
128
128
  // workaround issue https://github.com/redhat-developer/vscode-extension-tester/issues/835
129
129
  // do not fail if clipboard is empty
130
130
  }
131
- const inputarea = await self.findElement(TextEditor.locators.Editor.inputArea);
132
- await inputarea.sendKeys(selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'a'), selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'c'));
131
+ // Store current position
132
+ const [line, col] = await this.getCoordinates();
133
+ // Select/copy contents
134
+ const bench = new __1.Workbench();
135
+ await bench.executeCommand('editor.action.selectAll');
136
+ await bench.executeCommand('editor.action.clipboardCopyAction');
133
137
  // Wait for clipboard operation to complete
134
138
  await self.getWaitHelper().forCondition(async () => {
135
139
  try {
@@ -141,10 +145,16 @@ class TextEditor extends Editor_1.Editor {
141
145
  }
142
146
  }, { timeout: 2000, pollInterval: 50, message: 'Clipboard copy operation did not complete' });
143
147
  const text = clipboard.readSync();
144
- await inputarea.sendKeys(selenium_webdriver_1.Key.UP);
145
148
  if (originalClipboard.length > 0) {
146
149
  clipboard.writeSync(originalClipboard);
147
150
  }
151
+ try {
152
+ // Restore original cursor position
153
+ await this.setCursor(line, col);
154
+ }
155
+ catch {
156
+ await this.sendKeys(selenium_webdriver_1.Key.UP);
157
+ }
148
158
  return text;
149
159
  });
150
160
  }
@@ -166,7 +176,9 @@ class TextEditor extends Editor_1.Editor {
166
176
  }
167
177
  const inputarea = await this.findElement(TextEditor.locators.Editor.inputArea);
168
178
  clipboard.writeSync(text);
169
- await inputarea.sendKeys(selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'a'), selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'v'));
179
+ const bench = new __1.Workbench();
180
+ await bench.executeCommand('editor.action.selectAll');
181
+ await inputarea.sendKeys(selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'v'));
170
182
  if (originalClipboard.length > 0) {
171
183
  clipboard.writeSync(originalClipboard);
172
184
  }
@@ -179,6 +191,13 @@ class TextEditor extends Editor_1.Editor {
179
191
  * @returns Promise resolving once the text is deleted
180
192
  */
181
193
  async clearText() {
194
+ // Dismiss any open menu or overlay first (e.g. context menu left open after
195
+ // formatDocument), then focus the editor container so the inputArea becomes
196
+ // interactable. We press Escape via the driver (safe regardless of focus) and
197
+ // click the outer editor element — NOT the native-edit-context, which cannot
198
+ // receive a synthetic click when it is not the active focus target.
199
+ await this.getDriver().actions().sendKeys(selenium_webdriver_1.Key.ESCAPE).perform();
200
+ await this.enclosingItem.click();
182
201
  return this.withRecovery(async (self) => {
183
202
  const input = await self.findElement(TextEditor.locators.Editor.inputArea);
184
203
  await input.sendKeys(selenium_webdriver_1.Key.chord(TextEditor.ctlKey, 'a'));
@@ -341,12 +360,16 @@ class TextEditor extends Editor_1.Editor {
341
360
  async setCursor(line, column, timeout = 2_500) {
342
361
  const input = await new __1.Workbench().openCommandPrompt();
343
362
  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);
363
+ try {
364
+ await this.getWaitHelper().forCondition(async () => {
365
+ const picks = await input.getQuickPicks().catch(() => []);
366
+ return picks.length > 0;
367
+ }, { timeout: 2000, pollInterval: 50 });
368
+ await input.selectQuickPick(0);
369
+ }
370
+ catch {
371
+ await input.confirm();
372
+ }
350
373
  await this.waitForCursorPositionAt(line, column, timeout);
351
374
  }
352
375
  /**
@@ -533,19 +556,9 @@ class TextEditor extends Editor_1.Editor {
533
556
  }
534
557
  async openContextMenu() {
535
558
  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
- }
559
+ const menu = await this.waitForShadowRootMenu();
560
+ if (menu) {
561
+ return menu;
549
562
  }
550
563
  return await super.openContextMenu();
551
564
  }
@@ -553,15 +566,30 @@ class TextEditor extends Editor_1.Editor {
553
566
  * Get the cursor's coordinates as an array of two numbers: `[line, column]`
554
567
  *
555
568
  * **Caution** line & column coordinates do not start at `0` but at `1`!
569
+ *
570
+ * @param timeout ms to wait for the status bar to show a valid position (default 5000)
556
571
  */
557
- async getCoordinates() {
558
- const coords = [];
572
+ async getCoordinates(timeout = 5_000) {
559
573
  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]];
574
+ // The status bar position item may not update immediately after a cursor move,
575
+ // especially on slow CI runners. Poll until it returns a valid "Ln X, Col Y" string.
576
+ let positionText = '';
577
+ await this.getDriver().wait(async () => {
578
+ try {
579
+ const text = await statusBar.getCurrentPosition();
580
+ const match = text.match(/\d+/g);
581
+ if (match && match.length >= 2) {
582
+ positionText = text;
583
+ return true;
584
+ }
585
+ }
586
+ catch {
587
+ // Status bar item not yet available — keep polling
588
+ }
589
+ return false;
590
+ }, timeout, `Status bar did not show a valid cursor position within ${timeout}ms`);
591
+ const coordinates = positionText.match(/\d+/g);
592
+ return [+coordinates[0], +coordinates[1]];
565
593
  }
566
594
  async getTabSize() {
567
595
  const statusBar = new StatusBar_1.StatusBar();
@@ -585,7 +613,7 @@ class TextEditor extends Editor_1.Editor {
585
613
  : lineOverlay;
586
614
  const breakPoint = await breakpointContainer.findElements(TextEditor.locators.TextEditor.breakpoint.generalSelector);
587
615
  if (breakPoint.length > 0) {
588
- if (this.breakPoints.indexOf(line) !== -1) {
616
+ if (this.breakPoints.includes(line)) {
589
617
  await breakPoint[this.breakPoints.indexOf(line)].click();
590
618
  // Wait for breakpoint to be removed
591
619
  await this.getWaitHelper().forCondition(async () => {
@@ -665,15 +693,23 @@ class TextEditor extends Editor_1.Editor {
665
693
  async getBreakpoints() {
666
694
  const breakpoints = [];
667
695
  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;
696
+ const isNewVersion = (0, compare_versions_1.satisfies)(TextEditor.versionInfo.version, '>=1.80.0');
697
+ const breakpointContainer = isNewVersion ? await this.findElement(TextEditor.locators.TextEditor.glyphMarginWidget) : this;
671
698
  const breakpointsSelectors = await breakpointContainer.findElements(breakpointLocators.generalSelector);
699
+ // When a debug session is paused at a breakpoint, VS Code renders two overlapping
700
+ // elements with the `codicon-debug-breakpoint` class at the same vertical position
701
+ // (the regular breakpoint dot and the stackframe arrow). Deduplicate by `top` so
702
+ // that each line contributes at most one Breakpoint.
703
+ const seenTops = new Set();
672
704
  for (const breakpointSelector of breakpointsSelectors) {
673
705
  try {
674
706
  let lineElement;
675
- if ((0, compare_versions_1.satisfies)(TextEditor.versionInfo.version, '>=1.80.0')) {
707
+ if (isNewVersion) {
676
708
  const styleTopAttr = await breakpointSelector.getCssValue('top');
709
+ if (seenTops.has(styleTopAttr)) {
710
+ continue;
711
+ }
712
+ seenTops.add(styleTopAttr);
677
713
  lineElement = await this.findElement(TextEditor.locators.TextEditor.marginArea).findElement(TextEditor.locators.TextEditor.lineElement(styleTopAttr));
678
714
  }
679
715
  else {
@@ -696,14 +732,23 @@ class TextEditor extends Editor_1.Editor {
696
732
  */
697
733
  async getCodeLenses() {
698
734
  const lenses = [];
699
- const widgets = await this.findElement(TextEditor.locators.TextEditor.contentWidgets);
700
- const items = await widgets.findElements(TextEditor.locators.TextEditor.contentWidgetsElements);
735
+ // Use findElements so that a missing/not-yet-visible contentWidgets container
736
+ // returns an empty array rather than throwing a TimeoutError. The caller
737
+ // (or the before-hook poll loop) is responsible for retrying until lenses appear.
738
+ const widgetContainers = await this.findElements(TextEditor.locators.TextEditor.contentWidgets);
739
+ if (widgetContainers.length === 0) {
740
+ return lenses;
741
+ }
742
+ const items = await widgetContainers[0].findElements(TextEditor.locators.TextEditor.contentWidgetsElements);
701
743
  for (const item of items) {
702
744
  try {
703
745
  lenses.push(await new CodeLens(item, this).wait());
704
746
  }
705
747
  catch (e) {
706
- if (e.name === 'StaleElementReferenceError') {
748
+ // Skip stale or not-yet-visible lens elements. TimeoutError means the
749
+ // lens widget exists in the DOM but is not visible yet (e.g. the editor
750
+ // is still rendering); the caller's poll loop will retry the whole call.
751
+ if (e.name === 'StaleElementReferenceError' || e.name === 'TimeoutError') {
707
752
  continue;
708
753
  }
709
754
  throw e;
@@ -779,19 +824,9 @@ class Selection extends ElementWithContextMenu_1.ElementWithContextMenu {
779
824
  }
780
825
  }
781
826
  }
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
- }
827
+ const menu = await this.waitForShadowRootMenu();
828
+ if (menu) {
829
+ return menu;
795
830
  }
796
831
  return await super.openContextMenu();
797
832
  }
@@ -973,8 +1008,12 @@ class FindWidget extends AbstractElement_1.AbstractElement {
973
1008
  }
974
1009
  const btn = await element.findElement(FindWidget.locators.FindWidget.button(title));
975
1010
  await btn.click();
976
- // Wait for button action to complete
977
- await this.getWaitHelper().forStable(element, { timeout: 500 });
1011
+ // Give the widget a moment to settle after the action (animations, relayout).
1012
+ // Best-effort: a slow animation must not fail the interaction that already
1013
+ // happened, so a stability timeout is not an error here.
1014
+ await this.getWaitHelper()
1015
+ .forStable(element, { timeout: 2000 })
1016
+ .catch(() => undefined);
978
1017
  }
979
1018
  async setText(text, composite) {
980
1019
  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