@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.
- package/out/components/AbstractElement.js +2 -1
- package/out/components/ElementWithContextMenu.d.ts +11 -0
- package/out/components/ElementWithContextMenu.js +63 -15
- package/out/components/WebviewMixin.js +73 -7
- package/out/components/activityBar/ViewControl.d.ts +16 -0
- package/out/components/activityBar/ViewControl.js +45 -8
- package/out/components/bottomBar/BottomBarPanel.js +26 -11
- package/out/components/editor/ContentAssist.d.ts +9 -1
- package/out/components/editor/ContentAssist.js +88 -13
- package/out/components/editor/CustomEditor.js +1 -1
- package/out/components/editor/EditorView.d.ts +6 -0
- package/out/components/editor/EditorView.js +73 -3
- package/out/components/editor/SettingsEditor.d.ts +1 -0
- package/out/components/editor/SettingsEditor.js +17 -1
- package/out/components/editor/TextEditor.d.ts +3 -1
- package/out/components/editor/TextEditor.js +79 -52
- package/out/components/editor/WebView.js +6 -1
- package/out/components/menu/ContextMenu.d.ts +1 -1
- package/out/components/menu/ContextMenu.js +7 -4
- package/out/components/menu/Menu.js +3 -1
- package/out/components/sidebar/ViewItem.js +1 -0
- package/out/components/sidebar/tree/default/DefaultTreeSection.js +6 -0
- package/out/components/workbench/DebugToolbar.js +1 -1
- package/out/components/workbench/NotificationsCenter.js +6 -1
- package/out/components/workbench/Workbench.js +1 -1
- package/out/components/workbench/input/Input.d.ts +18 -0
- package/out/components/workbench/input/Input.js +68 -0
- package/out/components/workbench/input/QuickOpenBox.js +1 -1
- package/out/errors/ExTesterError.js +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/locators/locators.js +26 -3
- package/out/utils/TimeoutConstants.d.ts +59 -0
- package/out/utils/TimeoutConstants.js +63 -0
- package/out/utils/WaitHelper.js +8 -2
- 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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
37
|
-
const klass =
|
|
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.
|
|
44
|
+
await this.safeClick();
|
|
40
45
|
// Wait for view to be marked as active
|
|
41
46
|
await this.getWaitHelper().forCondition(async () => {
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
73
|
+
const klass = await this.safeGetKlass();
|
|
64
74
|
if (klass.indexOf(ViewControl.locators.ViewControl.klass) > -1) {
|
|
65
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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 (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
60
|
+
throw err;
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
@@ -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
|