@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
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
  }
@@ -160,7 +170,8 @@ class AbstractElement extends selenium_webdriver_1.WebElement {
160
170
  catch (err) {
161
171
  const isRecoverable = err.name === 'StaleElementReferenceError' ||
162
172
  err.name === 'ElementNotInteractableError' ||
163
- /element.*(detached|not interactable)/i.test(err.message);
173
+ err.name === 'ElementClickInterceptedError' ||
174
+ /element.*(detached|not interactable|click intercepted)/i.test(err.message);
164
175
  if (isRecoverable && attempt < maxRetries) {
165
176
  try {
166
177
  recoveredElement = await this.reinitialize();
@@ -24,4 +24,15 @@ export declare abstract class ElementWithContextMenu extends AbstractElement {
24
24
  * Open context menu on the element
25
25
  */
26
26
  openContextMenu(): Promise<ContextMenu>;
27
+ /**
28
+ * Since VS Code ~1.133 some context menus (notably the text editor's) render
29
+ * inside an open shadow root on a `.shadow-root-host` element attached
30
+ * directly to the workbench, instead of the document-level `.context-view`
31
+ * container. Which shape a given install uses is not strictly version-bound,
32
+ * so poll for either: resolve the menu from the shadow root when it appears,
33
+ * or return undefined as soon as the legacy container shows up (or the
34
+ * timeout runs out) so the caller can take the regular `.context-view` path.
35
+ */
36
+ protected waitForShadowRootMenu(timeout?: number): Promise<ContextMenu | undefined>;
37
+ private menuContainerInShadowRoot;
27
38
  }
@@ -28,26 +28,74 @@ class ElementWithContextMenu extends AbstractElement_1.AbstractElement {
28
28
  * Open context menu on the element
29
29
  */
30
30
  async openContextMenu() {
31
- const workbench = await this.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
32
- const menus = await workbench.findElements(ElementWithContextMenu.locators.ContextMenu.contextView);
33
- if (menus.length < 1) {
34
- await this.getDriver().actions().contextClick(this).perform();
35
- await this.getDriver().wait(selenium_webdriver_1.until.elementLocated(ElementWithContextMenu.locators.ContextMenu.contextView), 2000);
31
+ return await this.withRecovery(async (self) => {
32
+ const workbench = await self.getDriver().findElement(ElementWithContextMenu.locators.Workbench.constructor);
33
+ const menus = await workbench.findElements(ElementWithContextMenu.locators.ContextMenu.contextView);
34
+ if (menus.length < 1) {
35
+ await self.getDriver().actions().contextClick(self).perform();
36
+ await self.getDriver().wait(selenium_webdriver_1.until.elementLocated(ElementWithContextMenu.locators.ContextMenu.contextView), 2000);
37
+ return new __1.ContextMenu(workbench).wait();
38
+ }
39
+ else if ((await workbench.findElements(ElementWithContextMenu.locators.ContextMenu.viewBlock)).length > 0) {
40
+ await self.getDriver().actions().contextClick(self).perform();
41
+ try {
42
+ await self.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(self), 1000);
43
+ }
44
+ catch (err) {
45
+ if (!(err instanceof selenium_webdriver_1.error.StaleElementReferenceError)) {
46
+ throw err;
47
+ }
48
+ }
49
+ }
50
+ await self.getDriver().actions().contextClick(self).perform();
36
51
  return new __1.ContextMenu(workbench).wait();
37
- }
38
- else if ((await workbench.findElements(ElementWithContextMenu.locators.ContextMenu.viewBlock)).length > 0) {
39
- await this.getDriver().actions().contextClick(this).perform();
40
- try {
41
- await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this), 1000);
52
+ });
53
+ }
54
+ /**
55
+ * Since VS Code ~1.133 some context menus (notably the text editor's) render
56
+ * inside an open shadow root on a `.shadow-root-host` element attached
57
+ * directly to the workbench, instead of the document-level `.context-view`
58
+ * container. Which shape a given install uses is not strictly version-bound,
59
+ * so poll for either: resolve the menu from the shadow root when it appears,
60
+ * or return undefined as soon as the legacy container shows up (or the
61
+ * timeout runs out) so the caller can take the regular `.context-view` path.
62
+ */
63
+ async waitForShadowRootMenu(timeout = 2000) {
64
+ const driver = this.getDriver();
65
+ const end = Date.now() + timeout;
66
+ while (Date.now() < end) {
67
+ const hosts = await driver.findElements(ElementWithContextMenu.locators.ContextMenu.shadowRootHost);
68
+ for (const host of hosts) {
69
+ try {
70
+ const container = await this.menuContainerInShadowRoot(host);
71
+ if (container && (await container.isDisplayed())) {
72
+ return await new __1.ContextMenu(host, container).wait();
73
+ }
74
+ }
75
+ catch (err) {
76
+ // the host or menu got removed mid-check - keep polling
77
+ }
42
78
  }
43
- catch (err) {
44
- if (!(err instanceof selenium_webdriver_1.error.StaleElementReferenceError)) {
45
- throw err;
79
+ const legacy = await driver.findElements(ElementWithContextMenu.locators.ContextMenu.contextView);
80
+ for (const view of legacy) {
81
+ if (await view.isDisplayed().catch(() => false)) {
82
+ return undefined;
46
83
  }
47
84
  }
85
+ await new Promise((res) => setTimeout(res, 100));
86
+ }
87
+ return undefined;
88
+ }
89
+ async menuContainerInShadowRoot(host) {
90
+ const driver = this.getDriver();
91
+ const capabilities = await driver.getCapabilities();
92
+ const chromiumVersion = capabilities.getBrowserVersion();
93
+ if (chromiumVersion && Number.parseInt(chromiumVersion.split('.')[0]) >= 96) {
94
+ const shadowRoot = await host.getShadowRoot();
95
+ const containers = await shadowRoot.findElements(ElementWithContextMenu.locators.ContextMenu.constructor);
96
+ return containers.length > 0 ? containers[0] : undefined;
48
97
  }
49
- await this.getDriver().actions().contextClick(this).perform();
50
- return new __1.ContextMenu(workbench).wait();
98
+ return (await driver.executeScript('return arguments[0].shadowRoot ? arguments[0].shadowRoot.querySelector(".monaco-menu-container") : undefined;', host));
51
99
  }
52
100
  }
53
101
  exports.ElementWithContextMenu = ElementWithContextMenu;
@@ -28,7 +28,9 @@ const AbstractElement_1 = require("./AbstractElement");
28
28
  function default_1(Base) {
29
29
  return class extends Base {
30
30
  /**
31
- * Cannot use static element, since this class is unnamed.
31
+ * Window handle captured once at switchToFrame() time so that switchBack()
32
+ * always returns to the correct top-level context even when called from
33
+ * within a nested frame.
32
34
  */
33
35
  handle;
34
36
  /**
@@ -58,25 +60,89 @@ function default_1(Base) {
58
60
  * This allows using the findWebElement methods.
59
61
  * Note that only elements inside the webview iframe will be accessible.
60
62
  * Use the switchBack method to switch to the original context.
63
+ *
64
+ * The method polls for both the outer iframe and the inner active-frame to
65
+ * appear in the DOM under a single shared deadline, so `timeout` is always
66
+ * honoured regardless of how long each phase takes. The driver is always
67
+ * returned to the top-level context before any error is thrown.
68
+ *
69
+ * @throws Error when the webview iframe cannot be located within the timeout
61
70
  */
62
71
  async switchToFrame(timeout = 5000) {
63
72
  if (!this.handle) {
64
73
  this.handle = await this.getDriver().getWindowHandle();
65
74
  }
66
- const view = await this.getViewToSwitchTo();
75
+ const deadline = Date.now() + timeout;
76
+ const pollInterval = 200;
77
+ const minPhase2Budget = Math.min(5000, Math.floor(timeout / 3));
78
+ // Phase 1 — poll for the outer webview iframe. Stop early to
79
+ // guarantee at least minPhase2Budget ms for the inner-frame lookup.
80
+ let view;
81
+ while (Date.now() < deadline - minPhase2Budget) {
82
+ try {
83
+ view = await this.getViewToSwitchTo();
84
+ }
85
+ catch (e) {
86
+ if (!(e instanceof selenium_webdriver_1.error.StaleElementReferenceError || e instanceof selenium_webdriver_1.error.NoSuchElementError)) {
87
+ throw e;
88
+ }
89
+ }
90
+ if (view) {
91
+ break;
92
+ }
93
+ await this.getDriver().sleep(pollInterval);
94
+ }
67
95
  if (!view) {
68
- return;
96
+ throw new Error(`WebviewMixin.switchToFrame: webview iframe was not found within ${timeout}ms. ` +
97
+ `The webview may not have been opened, or the locator no longer matches the current VS Code version. ` +
98
+ `See https://github.com/redhat-developer/vscode-extension-tester/issues/2450`);
99
+ }
100
+ // Phase 2 — switch into the outer iframe, then poll for the inner
101
+ // active-frame under the remaining budget (at least minPhase2Budget ms).
102
+ try {
103
+ await this.getDriver().switchTo().frame(view);
104
+ }
105
+ catch (e) {
106
+ if (e instanceof selenium_webdriver_1.error.StaleElementReferenceError) {
107
+ throw new Error(`WebviewMixin.switchToFrame: outer iframe became stale during frame switch. ` +
108
+ `See https://github.com/redhat-developer/vscode-extension-tester/issues/2450`);
109
+ }
110
+ throw e;
111
+ }
112
+ let frame;
113
+ while (Date.now() < deadline) {
114
+ try {
115
+ const frames = await this.getDriver().findElements(AbstractElement_1.AbstractElement.locators.WebView.activeFrame);
116
+ if (frames.length > 0) {
117
+ frame = frames[0];
118
+ break;
119
+ }
120
+ }
121
+ catch (e) {
122
+ if (!(e instanceof selenium_webdriver_1.error.StaleElementReferenceError || e instanceof selenium_webdriver_1.error.NoSuchElementError)) {
123
+ await this.getDriver().switchTo().window(this.handle);
124
+ throw e;
125
+ }
126
+ }
127
+ await this.getDriver().sleep(pollInterval);
128
+ }
129
+ if (!frame) {
130
+ // Return to top-level before throwing so callers are not left inside a frame.
131
+ await this.getDriver().switchTo().window(this.handle);
132
+ throw new Error(`WebviewMixin.switchToFrame: active-frame iframe was not found within ${timeout}ms. ` +
133
+ `The webview content may still be loading. ` +
134
+ `See https://github.com/redhat-developer/vscode-extension-tester/issues/2450`);
69
135
  }
70
- await this.getDriver().switchTo().frame(view);
71
- await this.getDriver().wait(selenium_webdriver_1.until.elementLocated(AbstractElement_1.AbstractElement.locators.WebView.activeFrame), timeout);
72
- const frame = await this.getDriver().findElement(AbstractElement_1.AbstractElement.locators.WebView.activeFrame);
73
136
  await this.getDriver().switchTo().frame(frame);
74
137
  }
75
138
  /**
76
- * Switch the underlying webdriver back to the original window
139
+ * Switch the underlying webdriver back to the original window.
140
+ * Uses the handle captured at switchToFrame() time; safe to call even when
141
+ * the driver context is already at the top level.
77
142
  */
78
143
  async switchBack() {
79
144
  if (!this.handle) {
145
+ // switchToFrame() was never called — capture and stay at top level
80
146
  this.handle = await this.getDriver().getWindowHandle();
81
147
  }
82
148
  return await this.getDriver().switchTo().window(this.handle);
@@ -21,6 +21,11 @@ import { WebElement } from 'selenium-webdriver';
21
21
  * Page object representing a view container item in the activity bar
22
22
  */
23
23
  export declare class ViewControl extends ElementWithContextMenu {
24
+ /**
25
+ * Title of this control, cached on the first successful getTitle() call so the
26
+ * control can be re-located in the activity bar after its element goes stale.
27
+ */
28
+ private cachedTitle;
24
29
  constructor(element: WebElement, bar: ActivityBar);
25
30
  /**
26
31
  * Opens the associated view if not already open
@@ -36,4 +41,15 @@ export declare class ViewControl extends ElementWithContextMenu {
36
41
  * Returns the title of the associated view
37
42
  */
38
43
  getTitle(): Promise<string>;
44
+ /**
45
+ * Re-locate this control in the activity bar by its cached title.
46
+ * Used by withRecovery()-based helpers (safeClick, safeGetKlass) when the
47
+ * stored element reference goes stale, e.g. after the activity bar re-renders.
48
+ */
49
+ protected reinitialize(): Promise<this>;
50
+ /**
51
+ * Read the CSS class attribute from this element, recovering transparently if
52
+ * the stored reference has become stale.
53
+ */
54
+ private safeGetKlass;
39
55
  }
@@ -25,6 +25,11 @@ const compare_versions_1 = require("compare-versions");
25
25
  * Page object representing a view container item in the activity bar
26
26
  */
27
27
  class ViewControl extends ElementWithContextMenu_1.ElementWithContextMenu {
28
+ /**
29
+ * Title of this control, cached on the first successful getTitle() call so the
30
+ * control can be re-located in the activity bar after its element goes stale.
31
+ */
32
+ cachedTitle;
28
33
  constructor(element, bar) {
29
34
  super(element, bar);
30
35
  }
@@ -33,14 +38,19 @@ class ViewControl extends ElementWithContextMenu_1.ElementWithContextMenu {
33
38
  * @returns Promise resolving to SideBarView object representing the opened view
34
39
  */
35
40
  async openView() {
36
- // Check whether view is already open
37
- const klass = (await this.getAttribute(ViewControl.locators.ViewControl.attribute));
41
+ // Use safeGetKlass so a stale reference is transparently re-fetched.
42
+ const klass = await this.safeGetKlass();
38
43
  if (klass.indexOf(ViewControl.locators.ViewControl.klass) < 0) {
39
- await this.click();
44
+ await this.safeClick();
40
45
  // Wait for view to be marked as active
41
46
  await this.getWaitHelper().forCondition(async () => {
42
- const newKlass = (await this.getAttribute(ViewControl.locators.ViewControl.attribute));
43
- return newKlass.includes(ViewControl.locators.ViewControl.klass);
47
+ try {
48
+ const newKlass = await this.safeGetKlass();
49
+ return newKlass.includes(ViewControl.locators.ViewControl.klass);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
44
54
  }, { timeout: 2000, pollInterval: 100 });
45
55
  }
46
56
  const view = await new __1.SideBarView().wait();
@@ -60,9 +70,9 @@ class ViewControl extends ElementWithContextMenu_1.ElementWithContextMenu {
60
70
  * @returns Promise resolving when the view closes
61
71
  */
62
72
  async closeView() {
63
- const klass = (await this.getAttribute(ViewControl.locators.ViewControl.attribute));
73
+ const klass = await this.safeGetKlass();
64
74
  if (klass.indexOf(ViewControl.locators.ViewControl.klass) > -1) {
65
- await this.click();
75
+ await this.safeClick();
66
76
  }
67
77
  }
68
78
  /**
@@ -70,7 +80,34 @@ class ViewControl extends ElementWithContextMenu_1.ElementWithContextMenu {
70
80
  */
71
81
  async getTitle() {
72
82
  const badge = await this.findElement(ViewControl.locators.ViewControl.badge);
73
- return (await badge.getAttribute('aria-label'));
83
+ const title = (await badge.getAttribute('aria-label'));
84
+ this.cachedTitle = title;
85
+ return title;
86
+ }
87
+ /**
88
+ * Re-locate this control in the activity bar by its cached title.
89
+ * Used by withRecovery()-based helpers (safeClick, safeGetKlass) when the
90
+ * stored element reference goes stale, e.g. after the activity bar re-renders.
91
+ */
92
+ async reinitialize() {
93
+ if (!this.cachedTitle) {
94
+ throw new Error(`ViewControl: element is stale and no cached title is available to re-locate it`);
95
+ }
96
+ // getViewControls() always queries live elements, so the new reference is fresh.
97
+ const fresh = await new __1.ActivityBar().getViewControl(this.cachedTitle);
98
+ if (!fresh) {
99
+ throw new Error(`ViewControl: could not re-locate stale element '${this.cachedTitle}' in the activity bar`);
100
+ }
101
+ return fresh;
102
+ }
103
+ /**
104
+ * Read the CSS class attribute from this element, recovering transparently if
105
+ * the stored reference has become stale.
106
+ */
107
+ async safeGetKlass() {
108
+ return await this.withRecovery(async (self) => {
109
+ return (await self.getAttribute(ViewControl.locators.ViewControl.attribute));
110
+ });
74
111
  }
75
112
  }
76
113
  exports.ViewControl = ViewControl;
@@ -49,7 +49,7 @@ class BottomBarPanel extends AbstractElement_1.AbstractElement {
49
49
  }
50
50
  else {
51
51
  await this.closePanel();
52
- await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this));
52
+ await this.getDriver().wait(selenium_webdriver_1.until.elementIsNotVisible(this), 5_000);
53
53
  }
54
54
  }
55
55
  }
@@ -124,16 +124,31 @@ class BottomBarPanel extends AbstractElement_1.AbstractElement {
124
124
  }
125
125
  async resize(label) {
126
126
  await this.toggle(true);
127
- let action;
128
- try {
129
- action = await this.findElement(BottomBarPanel.locators.BottomBarPanel.globalActions).findElement(BottomBarPanel.locators.BottomBarPanel.action(label));
130
- }
131
- catch (err) {
132
- // the panel is already maximized
133
- }
134
- if (action) {
135
- await action.click();
136
- }
127
+ // The resize action button may briefly be non-interactable (or get replaced)
128
+ // while the panel's open/maximize animation is running — poll the click
129
+ // instead of failing on the first attempt.
130
+ await this.getDriver()
131
+ .wait(async () => {
132
+ let action;
133
+ try {
134
+ action = await this.findElement(BottomBarPanel.locators.BottomBarPanel.globalActions).findElement(BottomBarPanel.locators.BottomBarPanel.action(label));
135
+ }
136
+ catch (err) {
137
+ // the panel is already maximized/restored — the action is absent
138
+ return true;
139
+ }
140
+ try {
141
+ await action.click();
142
+ return true;
143
+ }
144
+ catch (err) {
145
+ // not interactable or stale mid-animation — retry
146
+ return false;
147
+ }
148
+ }, 5000, `BottomBarPanel: could not click the '${label}' action`)
149
+ .catch(() => {
150
+ // keep previous lenient behavior — resize is best-effort
151
+ });
137
152
  }
138
153
  async closePanel() {
139
154
  const closeButton = await this.findElement(BottomBarPanel.locators.BottomBarPanel.closeAction);
@@ -26,9 +26,17 @@ export declare class ContentAssist extends Menu {
26
26
  * until the item is found, or the end is reached
27
27
  *
28
28
  * @param name name/text to search by
29
+ * @param timeout overall time budget in ms for the search (default 30000)
29
30
  * @returns Promise resolving to ContentAssistItem object if found, undefined otherwise
30
31
  */
31
- getItem(name: string): Promise<ContentAssistItem | undefined>;
32
+ getItem(name: string, timeout?: number): Promise<ContentAssistItem | undefined>;
33
+ /**
34
+ * Read all currently rendered suggestion rows in a single script call.
35
+ * Returns the row elements together with their label text, virtual list index
36
+ * and whether the row is marked as the last element of the list.
37
+ */
38
+ private snapshotVisibleRows;
39
+ private static readonly SNAPSHOT_ROWS_SCRIPT;
32
40
  /**
33
41
  * Get all visible content assist items
34
42
  * @returns Promise resolving to array of ContentAssistItem objects
@@ -31,31 +31,106 @@ class ContentAssist extends __1.Menu {
31
31
  * until the item is found, or the end is reached
32
32
  *
33
33
  * @param name name/text to search by
34
+ * @param timeout overall time budget in ms for the search (default 30000)
34
35
  * @returns Promise resolving to ContentAssistItem object if found, undefined otherwise
35
36
  */
36
- async getItem(name) {
37
- let lastItem = false;
37
+ async getItem(name, timeout = 30000) {
38
+ const deadline = Date.now() + timeout;
38
39
  const scrollable = await this.findElement(ContentAssist.locators.ContentAssist.itemList);
40
+ await this.getDriver().wait(async () => {
41
+ return (await this.isLoaded()) && (await this.findElements(ContentAssist.locators.ContentAssist.itemRow)).length > 0;
42
+ }, Math.min(10000, Math.max(1000, deadline - Date.now())), 'Content assist item list did not populate');
43
+ // Scroll back to the top of the list in case it is not there already
39
44
  let firstItem = await this.findElements(ContentAssist.locators.ContentAssist.firstItem);
40
- while (firstItem.length < 1) {
45
+ while (firstItem.length < 1 && Date.now() < deadline) {
41
46
  await scrollable.sendKeys(selenium_webdriver_1.Key.PAGE_UP, selenium_webdriver_1.Key.NULL);
47
+ await this.getWaitHelper().sleep(100);
42
48
  firstItem = await this.findElements(ContentAssist.locators.ContentAssist.firstItem);
43
49
  }
44
- while (!lastItem) {
45
- const items = await this.getItems();
46
- for (const item of items) {
47
- if ((await item.getLabel()) === name) {
48
- return item;
50
+ while (Date.now() < deadline) {
51
+ let rows;
52
+ try {
53
+ rows = await this.snapshotVisibleRows();
54
+ }
55
+ catch (err) {
56
+ if (err instanceof selenium_webdriver_1.error.StaleElementReferenceError) {
57
+ // The list re-rendered mid-read — take a fresh snapshot
58
+ continue;
49
59
  }
50
- lastItem = lastItem || (await item.getAttribute('data-last-element')) === 'true';
60
+ throw err;
51
61
  }
52
- if (!lastItem) {
53
- await scrollable.sendKeys(selenium_webdriver_1.Key.PAGE_DOWN);
54
- // Minimal delay for scroll to render new items
55
- await this.getWaitHelper().sleep(100);
62
+ const match = rows.find((row) => row.label === name);
63
+ if (match) {
64
+ try {
65
+ return await new ContentAssistItem(match.element, this).wait();
66
+ }
67
+ catch (err) {
68
+ if (err instanceof selenium_webdriver_1.error.StaleElementReferenceError) {
69
+ continue;
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+ if (rows.some((row) => row.last)) {
75
+ // Reached the end of the list without finding the item
76
+ return undefined;
56
77
  }
78
+ const maxIndex = rows.reduce((max, row) => Math.max(max, row.index), -1);
79
+ await scrollable.sendKeys(selenium_webdriver_1.Key.PAGE_DOWN);
80
+ // Wait for the visible range to actually move instead of sleeping a fixed amount
81
+ await this.getDriver()
82
+ .wait(async () => {
83
+ const next = await this.snapshotVisibleRows().catch(() => undefined);
84
+ return next !== undefined && (next.some((row) => row.last) || next.reduce((max, row) => Math.max(max, row.index), -1) > maxIndex);
85
+ }, Math.min(2000, Math.max(200, deadline - Date.now())))
86
+ .catch(() => {
87
+ // The list did not move — keep looping, the deadline will end the search
88
+ });
57
89
  }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * Read all currently rendered suggestion rows in a single script call.
94
+ * Returns the row elements together with their label text, virtual list index
95
+ * and whether the row is marked as the last element of the list.
96
+ */
97
+ async snapshotVisibleRows() {
98
+ const container = await this.findElement(ContentAssist.locators.ContentAssist.itemRows);
99
+ const rowLocator = ContentAssist.locators.ContentAssist.itemRow;
100
+ const labelLocator = ContentAssist.locators.ContentAssist.itemLabel;
101
+ const raw = (await this.getDriver().executeScript(ContentAssist.SNAPSHOT_ROWS_SCRIPT, container, rowLocator.using, rowLocator.value, labelLocator.using, labelLocator.value));
102
+ return raw.map((row) => ({ element: row.element, label: row.label, index: row.index, last: row.last }));
58
103
  }
104
+ static SNAPSHOT_ROWS_SCRIPT = `
105
+ var container = arguments[0];
106
+ var rowUsing = arguments[1], rowValue = arguments[2];
107
+ var labelUsing = arguments[3], labelValue = arguments[4];
108
+ function findAll(root, using, value) {
109
+ if (using === 'xpath') {
110
+ var out = [];
111
+ var it = document.evaluate(value, root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
112
+ for (var i = 0; i < it.snapshotLength; i++) { out.push(it.snapshotItem(i)); }
113
+ return out;
114
+ }
115
+ return Array.prototype.slice.call(root.querySelectorAll(value));
116
+ }
117
+ function findOne(root, using, value) {
118
+ if (using === 'xpath') {
119
+ return document.evaluate(value, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
120
+ }
121
+ return root.querySelector(value);
122
+ }
123
+ return findAll(container, rowUsing, rowValue).map(function (row) {
124
+ var labelEl = findOne(row, labelUsing, labelValue);
125
+ var label = labelEl ? (labelEl.innerText !== undefined ? labelEl.innerText : labelEl.textContent) : '';
126
+ return {
127
+ element: row,
128
+ label: (label || '').trim(),
129
+ index: parseInt(row.getAttribute('data-index') || '-1', 10),
130
+ last: row.getAttribute('data-last-element') === 'true'
131
+ };
132
+ });
133
+ `;
59
134
  /**
60
135
  * Get all visible content assist items
61
136
  * @returns Promise resolving to array of ContentAssistItem objects
@@ -28,7 +28,7 @@ class CustomEditor extends __1.Editor {
28
28
  * @returns WebView page object
29
29
  */
30
30
  getWebView() {
31
- return new __1.WebView();
31
+ return new __1.WebView(this.enclosingItem);
32
32
  }
33
33
  /**
34
34
  * Check if the editor has unsaved changes