@redhat-developer/page-objects 1.22.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.
package/README.md CHANGED
@@ -1,9 +1,64 @@
1
1
  <h1 align="center">
2
- <img alt="ExTester for Visual Studio Code" width="75%" height="75%" src="../../icons/logo-text-side.png">
2
+ <img alt="ExTester for Visual Studio Code" width="75%" src="https://raw.githubusercontent.com/redhat-developer/vscode-extension-tester/main/icons/logo-text-side.png">
3
3
  </h1>
4
4
 
5
5
  <h2 align="center">Page Objects</h2>
6
6
 
7
- <p align="center">
8
- Page Object API implementation for a VS Code editor used by <a href="https://github.com/redhat-developer/vscode-extension-tester">ExTester</a> framework.</br>📄 <a href="https://github.com/redhat-developer/vscode-extension-tester/wiki/Page-Object-APIs">Documentation</a>
9
- </p>
7
+ <p align="center">Page Object API for the Visual Studio Code UI, used by the <a href="https://github.com/redhat-developer/vscode-extension-tester">ExTester</a> framework.</p>
8
+
9
+ ## What it is
10
+
11
+ Page objects wrap the parts of the VS Code workbench in classes with meaningful methods, so a test can say `new ActivityBar().getViewControl('Explorer')` instead of searching the DOM. This package contains:
12
+
13
+ - Page object classes for the activity bar, side bar views and tree sections, editors (text, diff, settings, custom and web view), the bottom panel (problems, output, terminal and debug console), the status bar, title bar and menus, dialogs, notifications and the debug views.
14
+ - `AbstractElement`, the base class every page object extends, including your own custom ones.
15
+ - `LocatorLoader` and `mergeLocators`, which assemble the locator set for the VS Code version under test from [`@redhat-developer/locators`](https://www.npmjs.com/package/@redhat-developer/locators) and an optional custom contribution.
16
+ - `WaitHelper`, timeout constants and a re-export of `selenium-webdriver`, so one import gives you `WebDriver`, `By`, `Key` and the rest.
17
+
18
+ ## Installation
19
+
20
+ [`vscode-extension-tester`](https://www.npmjs.com/package/vscode-extension-tester) depends on this package and re-exports everything from it. In an ExTester project you import page objects from `vscode-extension-tester` and never install this package directly:
21
+
22
+ ```typescript
23
+ import { ActivityBar, EditorView, TextEditor } from "vscode-extension-tester";
24
+ ```
25
+
26
+ Install it on its own only if you drive a VS Code instance with your own WebDriver setup:
27
+
28
+ ```bash
29
+ npm install --save-dev @redhat-developer/page-objects @redhat-developer/locators selenium-webdriver
30
+ ```
31
+
32
+ Peer dependencies: `selenium-webdriver ^4.48.0` and `typescript >= 4.6.2`.
33
+
34
+ ## Standalone usage
35
+
36
+ The page objects must be initialised once before the first one is created. ExTester does this for you when `VSBrowser` starts; without ExTester, call `initPageObjects` yourself:
37
+
38
+ ```typescript
39
+ import { getLocatorsPath } from "@redhat-developer/locators";
40
+ import { initPageObjects } from "@redhat-developer/page-objects";
41
+
42
+ initPageObjects(
43
+ "1.134.0", // VS Code version under test
44
+ "1.37.0", // version of the base locator set
45
+ getLocatorsPath(), // folder with the compiled locators
46
+ driver, // your selenium-webdriver WebDriver attached to VS Code
47
+ browserName, // name of the browser the page objects run in
48
+ "./out/test/pageObjects/locators.js", // optional custom locator contribution
49
+ );
50
+ ```
51
+
52
+ ## Documentation
53
+
54
+ - [Page Object Reference](https://redhat-developer.github.io/vscode-extension-tester/objects/) — every page object with examples
55
+ - [Custom Page Objects](https://redhat-developer.github.io/vscode-extension-tester/guides/custom-page-objects/) — extend the API for your own extension's UI
56
+
57
+ ## Feedback
58
+
59
+ - Bugs and feature requests: [open an issue](https://github.com/redhat-developer/vscode-extension-tester/issues/new/choose)
60
+ - Questions: [GitHub Discussions](https://github.com/redhat-developer/vscode-extension-tester/discussions)
61
+
62
+ ## License
63
+
64
+ [Apache License 2.0](https://github.com/redhat-developer/vscode-extension-tester/blob/main/LICENSE)
@@ -53,6 +53,14 @@ export declare abstract class AbstractElement extends WebElement {
53
53
  */
54
54
  constructor(base: Locator | WebElement, enclosingItem?: WebElement | Locator);
55
55
  isEnabled(): Promise<boolean>;
56
+ /**
57
+ * Click the element, recovering when a transient overlay intercepts the
58
+ * click. VS Code renders hovers/tooltips as DOM overlays that appear while
59
+ * the WebDriver pointer rests on the last click point and never auto-hide,
60
+ * so a plain W3C click deadlocks on its pre-dispatch hit-test. See
61
+ * {@link WaitHelper.clickThroughInterception} for the recovery strategy.
62
+ */
63
+ click(): Promise<void>;
56
64
  isSelected(): Promise<boolean>;
57
65
  /**
58
66
  * Wait for the element to become visible
@@ -76,6 +76,16 @@ class AbstractElement extends selenium_webdriver_1.WebElement {
76
76
  async isEnabled() {
77
77
  return (await super.isEnabled()) && (await AbstractElement.locators.AbstractElement.enabled(this));
78
78
  }
79
+ /**
80
+ * Click the element, recovering when a transient overlay intercepts the
81
+ * click. VS Code renders hovers/tooltips as DOM overlays that appear while
82
+ * the WebDriver pointer rests on the last click point and never auto-hide,
83
+ * so a plain W3C click deadlocks on its pre-dispatch hit-test. See
84
+ * {@link WaitHelper.clickThroughInterception} for the recovery strategy.
85
+ */
86
+ async click() {
87
+ await AbstractElement.waitHelper.clickThroughInterception(this, () => super.click());
88
+ }
79
89
  async isSelected() {
80
90
  return (await super.isSelected()) && (await AbstractElement.locators.AbstractElement.selected(this));
81
91
  }
@@ -252,29 +252,9 @@ class EditorGroup extends AbstractElement_1.AbstractElement {
252
252
  async closeEditor(title) {
253
253
  const tab = await this.getTabByTitle(title);
254
254
  const closeButton = await tab.findElement(EditorView.locators.EditorView.closeTab);
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
- }
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);
278
258
  }
279
259
  /**
280
260
  * Close all open editor tabs
@@ -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
  }
@@ -189,6 +189,37 @@ export declare class WaitHelper {
189
189
  * @param ms Milliseconds to sleep
190
190
  */
191
191
  sleep(ms: number): Promise<void>;
192
+ /**
193
+ * Move the virtual pointer to a quiet spot: the title-bar drag region at the
194
+ * top-center of the window (the command center is disabled by the framework's
195
+ * default settings, and unlike the status bar the area has no hover targets).
196
+ *
197
+ * A WebDriver session has no OS mouse - the pointer rests wherever the last
198
+ * interaction left it. VS Code hovers appear while the pointer rests on an
199
+ * element and are torn down only when it moves away, so parking delivers the
200
+ * mouseout an open hover is waiting for and disarms pending hover timers.
201
+ */
202
+ parkPointer(): Promise<void>;
203
+ /**
204
+ * Click an element, recovering when a transient overlay intercepts the click.
205
+ *
206
+ * VS Code renders rich hovers/tooltips as DOM overlays that appear while the
207
+ * pointer rests on the last click point and never auto-hide. A W3C click
208
+ * hit-tests the click point BEFORE dispatching any event, so a resting hover
209
+ * over the target fails every attempt until the pointer actually moves -
210
+ * waiting alone can never resolve it. On interception this actively parks
211
+ * the pointer (dismissing pointer-rest overlays), waits for hover teardown
212
+ * and retries the native click. As a last resort it falls back to a
213
+ * JS-executor click, which bypasses hit-testing but also mousedown semantics
214
+ * (Monaco lists select on mousedown) - the fallback is logged so the call
215
+ * site's true blocker can be root-caused instead of silently papered over.
216
+ *
217
+ * @param element the element to click
218
+ * @param nativeClick override for the native click action (used by
219
+ * AbstractElement.click() to reach WebElement.prototype.click through its
220
+ * own override)
221
+ */
222
+ clickThroughInterception(element: WebElement, nativeClick?: () => Promise<void>): Promise<void>;
192
223
  /**
193
224
  * Get the WebDriver instance.
194
225
  */
@@ -450,6 +450,76 @@ class WaitHelper {
450
450
  async sleep(ms) {
451
451
  await new Promise((resolve) => setTimeout(resolve, ms));
452
452
  }
453
+ /**
454
+ * Move the virtual pointer to a quiet spot: the title-bar drag region at the
455
+ * top-center of the window (the command center is disabled by the framework's
456
+ * default settings, and unlike the status bar the area has no hover targets).
457
+ *
458
+ * A WebDriver session has no OS mouse - the pointer rests wherever the last
459
+ * interaction left it. VS Code hovers appear while the pointer rests on an
460
+ * element and are torn down only when it moves away, so parking delivers the
461
+ * mouseout an open hover is waiting for and disarms pending hover timers.
462
+ */
463
+ async parkPointer() {
464
+ const width = Number(await this.driver.executeScript('return window.innerWidth')) || 200;
465
+ await this.driver
466
+ .actions()
467
+ .move({ x: Math.floor(width / 2), y: 5 })
468
+ .perform();
469
+ }
470
+ /**
471
+ * Click an element, recovering when a transient overlay intercepts the click.
472
+ *
473
+ * VS Code renders rich hovers/tooltips as DOM overlays that appear while the
474
+ * pointer rests on the last click point and never auto-hide. A W3C click
475
+ * hit-tests the click point BEFORE dispatching any event, so a resting hover
476
+ * over the target fails every attempt until the pointer actually moves -
477
+ * waiting alone can never resolve it. On interception this actively parks
478
+ * the pointer (dismissing pointer-rest overlays), waits for hover teardown
479
+ * and retries the native click. As a last resort it falls back to a
480
+ * JS-executor click, which bypasses hit-testing but also mousedown semantics
481
+ * (Monaco lists select on mousedown) - the fallback is logged so the call
482
+ * site's true blocker can be root-caused instead of silently papered over.
483
+ *
484
+ * @param element the element to click
485
+ * @param nativeClick override for the native click action (used by
486
+ * AbstractElement.click() to reach WebElement.prototype.click through its
487
+ * own override)
488
+ */
489
+ async clickThroughInterception(element, nativeClick = () => element.click()) {
490
+ const maxNativeAttempts = 3;
491
+ for (let attempt = 0;; attempt++) {
492
+ try {
493
+ return await nativeClick();
494
+ }
495
+ catch (e) {
496
+ if (e.name !== 'ElementClickInterceptedError') {
497
+ throw e;
498
+ }
499
+ // a modal dialog blocks the whole workbench by design - neither
500
+ // pointer parking nor a JS click may subvert it: JS-clicking
501
+ // through the blocker would trigger actions the real UI forbids.
502
+ // Fail fast with the original error so modal-aware callers (and
503
+ // test authors) see the true blocker.
504
+ const modalUp = await this.driver
505
+ .executeScript('return Array.from(document.querySelectorAll(".monaco-dialog-modal-block, .monaco-dialog-box")).some((el) => el.checkVisibility());')
506
+ .catch(() => false);
507
+ if (modalUp) {
508
+ throw e;
509
+ }
510
+ if (attempt >= maxNativeAttempts - 1) {
511
+ console.warn(`clickThroughInterception: falling back to a JS click after ${maxNativeAttempts} intercepted attempts: ${e.message}`);
512
+ await this.driver.executeScript('arguments[0].click();', element);
513
+ return;
514
+ }
515
+ await this.parkPointer();
516
+ // hover teardown is event-driven and near-instant once the pointer
517
+ // moved away; a non-hover interceptor (modal, toast) never passes
518
+ // this check, so time-box it and retry regardless
519
+ await this.forCondition(async () => await this.driver.executeScript('return !Array.from(document.querySelectorAll(".monaco-hover")).some((el) => el.checkVisibility());'), { timeout: 1500, pollInterval: 50, message: 'hover overlay did not dismiss after parking the pointer' }).catch(() => undefined);
520
+ }
521
+ }
522
+ }
453
523
  /**
454
524
  * Get the WebDriver instance.
455
525
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redhat-developer/page-objects",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Page Object API implementation for a VS Code editor used by ExTester framework.",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -49,5 +49,5 @@
49
49
  "selenium-webdriver": "^4.48.0",
50
50
  "typescript": ">=4.6.2"
51
51
  },
52
- "gitHead": "46f92f28229b4fd407b7fbb54488abb1bfc2b4a8"
52
+ "gitHead": "2333228475aff111f91b91d06e50a5b98e371886"
53
53
  }