Skip to content

Commit bde4446

Browse files
datho7561djelinek
authored andcommitted
Add page object for WebviewView
Fixes #804 Signed-off-by: David Thompson <davthomp@redhat.com>
1 parent 2266431 commit bde4446

File tree

13 files changed

+254
-74
lines changed

13 files changed

+254
-74
lines changed

locators/lib/1.37.0.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { By, WebElement } from "selenium-webdriver";
21
import { Locators, ViewSection, fromAttribute, fromText, hasAttribute, hasClass, hasElement, hasNotClass } from "monaco-page-objects";
2+
import { By, WebElement } from "selenium-webdriver";
33

44
const abstractElement = {
55
AbstractElement: {
@@ -79,6 +79,9 @@ const bottomBar = {
7979
OutputView: {
8080
constructor: By.id('workbench.panel.output'),
8181
actionsLabel: 'Output actions'
82+
},
83+
WebviewView: {
84+
iframe: By.xpath(`//div[not(@class)]/iframe[@class='webview ready' and not(@data-parent-flow-to-element-id)]`)
8285
}
8386
}
8487

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Locator, WebElement, until } from "selenium-webdriver";
2+
import { AbstractElement } from "./AbstractElement";
3+
4+
/**
5+
* Heavily inspired by https://stackoverflow.com/a/65418734
6+
*/
7+
8+
type Constructor<T = {}> = new (...args: any[]) => T;
9+
10+
/**
11+
* The interface that a class is required to have in order to use the Webview mixin.
12+
*/
13+
interface WebviewMixable extends AbstractElement {
14+
getViewToSwitchTo(handle: string): Promise<WebElement | undefined>;
15+
}
16+
17+
/**
18+
* The interface that is exposed by applying this mixin.
19+
*/
20+
export interface WebviewMixinType {
21+
findWebElement(locator: Locator): Promise<WebElement>;
22+
findWebElements(locator: Locator): Promise<WebElement[]>;
23+
switchToFrame(timeout?: number): Promise<void>;
24+
switchBack(): Promise<void>;
25+
}
26+
27+
/**
28+
* Returns a class that has the ability to access a webview.
29+
*
30+
* @param Base the class to mixin
31+
* @returns a class that has the ability to access a webview
32+
*/
33+
export default function <TBase extends Constructor<WebviewMixable>>(
34+
Base: TBase
35+
): Constructor<InstanceType<TBase> & WebviewMixinType> {
36+
return class extends Base implements WebviewMixinType {
37+
/**
38+
* Cannot use static element, since this class is unnamed.
39+
*/
40+
private handle: string | undefined;
41+
42+
/**
43+
* Search for an element inside the webview iframe.
44+
* Requires webdriver being switched to the webview iframe first.
45+
* (Will attempt to search from the main DOM root otherwise)
46+
*
47+
* @param locator webdriver locator to search by
48+
* @returns promise resolving to WebElement when found
49+
*/
50+
async findWebElement(locator: Locator): Promise<WebElement> {
51+
return await this.getDriver().findElement(locator);
52+
}
53+
54+
/**
55+
* Search for all element inside the webview iframe by a given locator
56+
* Requires webdriver being switched to the webview iframe first.
57+
* (Will attempt to search from the main DOM root otherwise)
58+
*
59+
* @param locator webdriver locator to search by
60+
* @returns promise resolving to a list of WebElement objects
61+
*/
62+
async findWebElements(locator: Locator): Promise<WebElement[]> {
63+
return await this.getDriver().findElements(locator);
64+
}
65+
66+
/**
67+
* Switch the underlying webdriver context to the webview iframe.
68+
* This allows using the findWebElement methods.
69+
* Note that only elements inside the webview iframe will be accessible.
70+
* Use the switchBack method to switch to the original context.
71+
*/
72+
async switchToFrame(timeout: number = 5000): Promise<void> {
73+
if (!this.handle) {
74+
this.handle = await this.getDriver().getWindowHandle();
75+
}
76+
77+
const view = await this.getViewToSwitchTo(this.handle);
78+
79+
if (!view) {
80+
return;
81+
}
82+
83+
await this.getDriver().switchTo().frame(view);
84+
85+
await this.getDriver().wait(
86+
until.elementLocated(AbstractElement.locators.WebView.activeFrame),
87+
timeout
88+
);
89+
const frame = await this.getDriver().findElement(
90+
AbstractElement.locators.WebView.activeFrame
91+
);
92+
await this.getDriver().switchTo().frame(frame);
93+
}
94+
95+
/**
96+
* Switch the underlying webdriver back to the original window
97+
*/
98+
async switchBack(): Promise<void> {
99+
if (!this.handle) {
100+
this.handle = await this.getDriver().getWindowHandle();
101+
}
102+
return await this.getDriver().switchTo().window(this.handle);
103+
}
104+
} as unknown as Constructor<InstanceType<TBase> & WebviewMixinType>;
105+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { WebElement } from "selenium-webdriver";
2+
import { AbstractElement } from "../AbstractElement";
3+
import WebviewMixin from "../WebviewMixin";
4+
5+
/**
6+
* Page object representing a user-contributed panel implemented using a Webview.
7+
*/
8+
class WebviewViewBase extends AbstractElement {
9+
10+
constructor() {
11+
super(WebviewViewBase.locators.Workbench.constructor);
12+
}
13+
14+
async getViewToSwitchTo(handle: string): Promise<WebElement | undefined> {
15+
return await this.getDriver().findElement(WebviewViewBase.locators.WebviewView.iframe);
16+
}
17+
18+
}
19+
20+
export const WebviewView = WebviewMixin(WebviewViewBase);
21+
export type WebviewView = InstanceType<typeof WebviewView>;

page-objects/src/components/editor/CustomEditor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class CustomEditor extends Editor {
3434

3535
/**
3636
* Open the Save as prompt
37-
*
37+
*
3838
* @returns InputBox serving as a simple file dialog
3939
*/
4040
async saveAs(): Promise<InputBox> {

page-objects/src/components/editor/EditorView.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { AbstractElement } from "../AbstractElement";
2-
import { TextEditor } from "../..";
31
import { error, WebElement } from "selenium-webdriver";
2+
import { TextEditor } from "../..";
3+
import { AbstractElement } from "../AbstractElement";
4+
import { ElementWithContexMenu } from "../ElementWithContextMenu";
5+
import { DiffEditor } from './DiffEditor';
46
import { Editor } from "./Editor";
7+
import { EditorAction } from "./EditorAction";
58
import { SettingsEditor } from "./SettingsEditor";
69
import { WebView } from "./WebView";
7-
import { DiffEditor } from './DiffEditor';
8-
import { ElementWithContexMenu } from "../ElementWithContextMenu";
9-
import { EditorAction } from "./EditorAction";
1010

1111
export class EditorTabNotFound extends Error {
1212
constructor(title: string, group: number) {
Lines changed: 13 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,13 @@
1+
import { until, WebElement } from "selenium-webdriver";
2+
import WebviewMixin from "../WebviewMixin";
13
import { Editor } from "./Editor";
2-
import { Locator, until, WebElement } from "selenium-webdriver";
34

45
/**
56
* Page object representing an open editor containing a web view
67
*/
7-
export class WebView extends Editor {
8-
9-
private static handle: string | undefined;
10-
11-
/**
12-
* Search for an element inside the webview iframe.
13-
* Requires webdriver being switched to the webview iframe first.
14-
* (Will attempt to search from the main DOM root otherwise)
15-
*
16-
* @param locator webdriver locator to search by
17-
* @returns promise resolving to WebElement when found
18-
*/
19-
async findWebElement(locator: Locator): Promise<WebElement> {
20-
return await this.getDriver().findElement(locator);
21-
}
22-
23-
/**
24-
* Search for all element inside the webview iframe by a given locator
25-
* Requires webdriver being switched to the webview iframe first.
26-
* (Will attempt to search from the main DOM root otherwise)
27-
*
28-
* @param locator webdriver locator to search by
29-
* @returns promise resolving to a list of WebElement objects
30-
*/
31-
async findWebElements(locator: Locator): Promise<WebElement[]> {
32-
return await this.getDriver().findElements(locator);
33-
}
34-
35-
/**
36-
* Switch the underlying webdriver context to the webview iframe.
37-
* This allows using the findWebElement methods.
38-
* Note that only elements inside the webview iframe will be accessible.
39-
* Use the switchBack method to switch to the original context.
40-
*/
41-
async switchToFrame(): Promise<void> {
42-
if (!WebView.handle) {
43-
WebView.handle = await this.getDriver().getWindowHandle();
44-
}
8+
class WebViewBase extends Editor {
459

10+
async getViewToSwitchTo(handle: string): Promise<WebElement | undefined> {
4611
const handles = await this.getDriver().getAllWindowHandles();
4712
for (const handle of handles) {
4813
await this.getDriver().switchTo().window(handle);
@@ -52,35 +17,23 @@ export class WebView extends Editor {
5217
return;
5318
}
5419
}
55-
await this.getDriver().switchTo().window(WebView.handle);
20+
await this.getDriver().switchTo().window(handle);
5621

57-
const reference = await this.findElement(WebView.locators.EditorView.webView);
58-
const containers = await this.getDriver().wait(until.elementsLocated(WebView.locators.WebView.container(await reference.getAttribute(WebView.locators.WebView.attribute))), 5000);
22+
const reference = await this.findElement(WebViewBase.locators.EditorView.webView);
23+
const containers = await this.getDriver().wait(until.elementsLocated(WebViewBase.locators.WebView.container(await reference.getAttribute(WebViewBase.locators.WebView.attribute))), 5000);
5924

60-
const view = await containers[0].getDriver().wait(async () => {
25+
return await containers[0].getDriver().wait(async () => {
6126
for (let index = 0; index < containers.length; index++) {
62-
const tries = await containers[index].findElements(WebView.locators.WebView.iframe);
27+
const tries = await containers[index].findElements(WebViewBase.locators.WebView.iframe);
6328
if (tries.length > 0) {
6429
return tries[0];
6530
}
6631
}
6732
return undefined;
6833
}, 5000) as WebElement;
69-
70-
await this.getDriver().switchTo().frame(view);
71-
72-
await this.getDriver().wait(until.elementLocated(WebView.locators.WebView.activeFrame), 5000);
73-
const frame = await this.getDriver().findElement(WebView.locators.WebView.activeFrame);
74-
await this.getDriver().switchTo().frame(frame);
7534
}
7635

77-
/**
78-
* Switch the underlying webdriver back to the original window
79-
*/
80-
async switchBack(): Promise<void> {
81-
if (!WebView.handle) {
82-
WebView.handle = await this.getDriver().getWindowHandle();
83-
}
84-
return await this.getDriver().switchTo().window(WebView.handle);
85-
}
86-
}
36+
}
37+
38+
export const WebView = WebviewMixin(WebViewBase);
39+
export type WebView = InstanceType<typeof WebView>;

page-objects/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export * from './components/sidebar/debug/DebugView';
4141

4242
export * from './components/bottomBar/BottomBarPanel';
4343
export * from './components/bottomBar/ProblemsView';
44+
export * from './components/bottomBar/WebviewView';
4445
export * from './components/bottomBar/Views';
4546
export * from './components/statusBar/StatusBar';
4647

@@ -69,7 +70,7 @@ export * from './conditions/WaitForAttribute';
6970

7071
/**
7172
* Initialize the page objects for your tests
72-
*
73+
*
7374
* @param currentVersion version of the locators to load
7475
* @param baseVersion base version of the locators if you have multiple versions with diffs, otherwise leave the same as currentVersion
7576
* @param locatorFolder folder that contains locator files

page-objects/src/locators/locators.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export interface Locators {
8686
constructor: By
8787
actionsLabel: string
8888
}
89+
WebviewView: {
90+
iframe: By
91+
}
8992

9093
// Editors
9194
EditorView: {

test/test-project/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@
8383
"title": "Disable Codelens"
8484
}
8585
],
86+
"viewsContainers": {
87+
"panel": [
88+
{
89+
"icon": "./media/paw-outline.svg",
90+
"id": "myPanel",
91+
"title": "My Panel"
92+
}
93+
]
94+
},
8695
"views": {
8796
"explorer": [
8897
{
@@ -93,6 +102,13 @@
93102
"id": "emptyView",
94103
"name": "Empty View"
95104
}
105+
],
106+
"myPanel": [
107+
{
108+
"id": "myPanelView",
109+
"name": "My Panel View",
110+
"type": "webview"
111+
}
96112
]
97113
},
98114
"viewsWelcome": [

test/test-project/src/extension.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as vscode from 'vscode';
21
import * as path from 'path';
3-
import { TreeView } from './treeView';
2+
import * as vscode from 'vscode';
43
import { CatScratchEditorProvider } from './catScratchEditor';
54
import { CodelensProvider } from './codelensProvider';
5+
import { TreeView } from './treeView';
66

77
export const ERROR_MESSAGE_COMMAND = 'extension.errorMsg';
88

@@ -60,7 +60,7 @@ export function activate(context: vscode.ExtensionContext) {
6060
"extension.populateTestView",
6161
() => { emptyViewNoContent = false; emitter.fire(undefined); }
6262
));
63-
63+
6464
const codelensProvider = new CodelensProvider();
6565
context.subscriptions.push(vscode.languages.registerCodeLensProvider("*", codelensProvider));
6666
context.subscriptions.push(
@@ -75,6 +75,9 @@ export function activate(context: vscode.ExtensionContext) {
7575
vscode.commands.registerCommand("extension.codelensAction", (args: any) => {
7676
vscode.window.showInformationMessage(`CodeLens action clicked with args=${args}`);
7777
}));
78+
context.subscriptions.push(
79+
vscode.window.registerWebviewViewProvider("myPanelView", new MyPanelView())
80+
);
7881
}
7982

8083
export function deactivate() {}
@@ -135,4 +138,10 @@ class TestView {
135138
</body>
136139
</html>`;
137140
}
141+
}
142+
143+
class MyPanelView implements vscode.WebviewViewProvider {
144+
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext<unknown>, token: vscode.CancellationToken): void | Thenable<void> {
145+
webviewView.webview.html = "<!DOCTYPE html><html><head><title>My Panel View</title></head><body><div><h1>Shopping List</h1><ul><li>Apple</li><li>Banana</li></ul></div></body></html>";
146+
}
138147
}

0 commit comments

Comments
 (0)