@redhat-developer/page-objects 1.20.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.d.ts +1 -1
- package/out/components/AbstractElement.js +3 -2
- 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/ActionsControl.js +1 -1
- package/out/components/activityBar/ViewControl.d.ts +16 -0
- package/out/components/activityBar/ViewControl.js +45 -8
- package/out/components/bottomBar/AbstractViews.js +2 -2
- package/out/components/bottomBar/BottomBarPanel.js +26 -11
- package/out/components/bottomBar/ProblemsView.js +2 -2
- package/out/components/bottomBar/Views.js +1 -1
- package/out/components/editor/ContentAssist.d.ts +9 -1
- package/out/components/editor/ContentAssist.js +98 -20
- package/out/components/editor/CustomEditor.js +2 -2
- package/out/components/editor/EditorAction.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 +20 -4
- package/out/components/editor/TextEditor.d.ts +4 -1
- package/out/components/editor/TextEditor.js +99 -62
- package/out/components/editor/WebView.js +6 -1
- package/out/components/menu/ContextMenu.d.ts +1 -1
- package/out/components/menu/ContextMenu.js +9 -6
- package/out/components/menu/Menu.js +3 -1
- package/out/components/menu/TitleBar.js +1 -1
- package/out/components/sidebar/ViewItem.js +1 -0
- package/out/components/sidebar/ViewSection.js +2 -2
- package/out/components/sidebar/ViewTitlePart.js +2 -2
- package/out/components/sidebar/debug/DebugView.d.ts +10 -0
- package/out/components/sidebar/debug/DebugView.js +45 -13
- package/out/components/sidebar/extensions/ExtensionsViewItem.js +1 -1
- package/out/components/sidebar/scm/NewScmView.js +1 -1
- package/out/components/sidebar/scm/ScmView.js +2 -2
- package/out/components/sidebar/tree/custom/CustomTreeItem.js +2 -1
- package/out/components/sidebar/tree/debug/CallStackItem.js +1 -1
- package/out/components/sidebar/tree/debug/WatchSectionItem.js +1 -1
- package/out/components/sidebar/tree/default/DefaultTreeItem.js +3 -3
- package/out/components/sidebar/tree/default/DefaultTreeSection.js +6 -0
- package/out/components/statusBar/StatusBar.js +2 -2
- package/out/components/workbench/DebugToolbar.js +1 -1
- package/out/components/workbench/Notification.js +3 -3
- 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 +70 -2
- package/out/components/workbench/input/InputBox.js +2 -2
- package/out/components/workbench/input/QuickOpenBox.js +2 -2
- package/out/errors/ExTesterError.js +1 -1
- package/out/index.d.ts +3 -1
- package/out/index.js +33 -3
- package/out/locators/loader.d.ts +2 -1
- package/out/locators/loader.js +2 -2
- package/out/locators/locators.d.ts +5 -2
- package/out/locators/locators.js +28 -5
- 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 +8 -6
|
@@ -33,7 +33,7 @@ export interface LocatorInfo {
|
|
|
33
33
|
export declare abstract class AbstractElement extends WebElement {
|
|
34
34
|
static readonly ctlKey: string;
|
|
35
35
|
protected static driver: WebDriver;
|
|
36
|
-
protected static locators: Locators
|
|
36
|
+
protected static locators: Locators & Record<string, Record<string, unknown>>;
|
|
37
37
|
protected static versionInfo: {
|
|
38
38
|
version: string;
|
|
39
39
|
browser: string;
|
|
@@ -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();
|
|
@@ -252,7 +253,7 @@ class AbstractElement extends selenium_webdriver_1.WebElement {
|
|
|
252
253
|
*/
|
|
253
254
|
async safeGetAttribute(name) {
|
|
254
255
|
return await this.withRecovery(async (self) => {
|
|
255
|
-
return await self.getAttribute(name);
|
|
256
|
+
return (await self.getAttribute(name));
|
|
256
257
|
});
|
|
257
258
|
}
|
|
258
259
|
}
|
|
@@ -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);
|
|
@@ -36,7 +36,7 @@ class ActionsControl extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
36
36
|
* Returns the title of the associated action
|
|
37
37
|
*/
|
|
38
38
|
async getTitle() {
|
|
39
|
-
return await this.getAttribute('aria-label');
|
|
39
|
+
return (await this.getAttribute('aria-label'));
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
exports.ActionsControl = ActionsControl;
|
|
@@ -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 = await this.
|
|
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 = await this.
|
|
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;
|
|
@@ -35,7 +35,7 @@ class ChannelView extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
35
35
|
await Promise.all(elements.map(async (element) => {
|
|
36
36
|
const disabled = await element.getAttribute('disabled');
|
|
37
37
|
if (!disabled) {
|
|
38
|
-
names.push(await element.getAttribute('value'));
|
|
38
|
+
names.push((await element.getAttribute('value')));
|
|
39
39
|
}
|
|
40
40
|
}));
|
|
41
41
|
return names;
|
|
@@ -50,7 +50,7 @@ class ChannelView extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
50
50
|
throw Error(`DEPRECATED METHOD! The 'ChannelView.getCurrentChannel' method is broken! Read more information in 'Known Issues > Limitations in testing with VS Code 1.87+' - https://github.com/microsoft/vscode/issues/206897.`);
|
|
51
51
|
}
|
|
52
52
|
const combo = await this.enclosingItem.findElement(ChannelView.locators.BottomBarViews.channelCombo);
|
|
53
|
-
return await combo.getAttribute(ChannelView.locators.OutputView.currentChannelAttribute);
|
|
53
|
+
return (await combo.getAttribute(ChannelView.locators.OutputView.currentChannelAttribute));
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Select a channel using the selector combo
|
|
@@ -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);
|
|
@@ -125,7 +125,7 @@ class Marker extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
125
125
|
* @returns Promise resolving to a Marker row text
|
|
126
126
|
*/
|
|
127
127
|
async getText() {
|
|
128
|
-
return await this.getAttribute(ProblemsView.locators.ProblemsView.rowLabel);
|
|
128
|
+
return (await this.getAttribute(ProblemsView.locators.ProblemsView.rowLabel));
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Get the Marker label text
|
|
@@ -141,7 +141,7 @@ class Marker extends ElementWithContextMenu_1.ElementWithContextMenu {
|
|
|
141
141
|
*/
|
|
142
142
|
async toggleExpand(expand) {
|
|
143
143
|
if ((await this.getType()) === MarkerType.File) {
|
|
144
|
-
const klass = await this.findElement(ProblemsView.locators.ProblemsView.markerTwistie).getAttribute('class');
|
|
144
|
+
const klass = (await this.findElement(ProblemsView.locators.ProblemsView.markerTwistie).getAttribute('class'));
|
|
145
145
|
if (klass.indexOf('collapsed') > -1 === expand) {
|
|
146
146
|
await this.click();
|
|
147
147
|
}
|
|
@@ -226,7 +226,7 @@ class TerminalView extends AbstractViews_1.ChannelView {
|
|
|
226
226
|
const rows = await list.findElements(TerminalView.locators.TerminalView.row);
|
|
227
227
|
for (const row of rows) {
|
|
228
228
|
const label = await row.getAttribute('aria-label');
|
|
229
|
-
if (label
|
|
229
|
+
if (label?.includes(channelNumber)) {
|
|
230
230
|
await row.click();
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
@@ -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
|
+
}
|
|
56
73
|
}
|
|
74
|
+
if (rows.some((row) => row.last)) {
|
|
75
|
+
// Reached the end of the list without finding the item
|
|
76
|
+
return undefined;
|
|
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
|
|
@@ -63,7 +138,7 @@ class ContentAssist extends __1.Menu {
|
|
|
63
138
|
async getItems() {
|
|
64
139
|
await this.getDriver().wait(async () => {
|
|
65
140
|
return await this.isLoaded();
|
|
66
|
-
});
|
|
141
|
+
}, 10000);
|
|
67
142
|
const elements = await this.findElement(ContentAssist.locators.ContentAssist.itemRows).findElements(ContentAssist.locators.ContentAssist.itemRow);
|
|
68
143
|
const items = [];
|
|
69
144
|
for (const item of elements) {
|
|
@@ -84,12 +159,15 @@ class ContentAssist extends __1.Menu {
|
|
|
84
159
|
* to false otherwise
|
|
85
160
|
*/
|
|
86
161
|
async isLoaded() {
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
const messages = await this.findElements(ContentAssist.locators.ContentAssist.message);
|
|
163
|
+
if (messages.length === 0) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (await messages[0].isDisplayed()) {
|
|
167
|
+
const text = await messages[0].getText();
|
|
168
|
+
// Empty message means loaded; "No suggestions" is also a terminal state.
|
|
169
|
+
// Any other non-empty text (e.g. "Loading...") means still loading.
|
|
170
|
+
return text === '' || text.startsWith('No suggestions');
|
|
93
171
|
}
|
|
94
172
|
return true;
|
|
95
173
|
}
|