@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
@@ -160,7 +160,8 @@ class AbstractElement extends selenium_webdriver_1.WebElement {
160
160
  catch (err) {
161
161
  const isRecoverable = err.name === 'StaleElementReferenceError' ||
162
162
  err.name === 'ElementNotInteractableError' ||
163
- /element.*(detached|not interactable)/i.test(err.message);
163
+ err.name === 'ElementClickInterceptedError' ||
164
+ /element.*(detached|not interactable|click intercepted)/i.test(err.message);
164
165
  if (isRecoverable && attempt < maxRetries) {
165
166
  try {
166
167
  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
@@ -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