Skip to content

Commit

Permalink
Add page object for WebviewView
Browse files Browse the repository at this point in the history
Fixes #804

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Oct 4, 2023
1 parent ceda3c4 commit 792655a
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 70 deletions.
105 changes: 105 additions & 0 deletions page-objects/src/components/WebviewMixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Locator, WebElement, until } from "selenium-webdriver";
import { AbstractElement } from "./AbstractElement";

/**
* Heavily inspired by https://stackoverflow.com/a/65418734
*/

type Constructor<T = {}> = new (...args: any[]) => T;

/**
* The interface that a class is required to have in order to use the Webview mixin.
*/
interface WebviewMixable extends AbstractElement {
getViewToSwitchTo(handle: string): Promise<WebElement | undefined>;
}

/**
* The interface that is exposed by applying this mixin.
*/
export interface WebviewMixinType {
findWebElement(locator: Locator): Promise<WebElement>;
findWebElements(locator: Locator): Promise<WebElement[]>;
switchToFrame(): Promise<void>;
switchBack(): Promise<void>;
}

/**
* Returns a class that has the ability to access a webview.
*
* @param Base the class to mixin
* @returns a class that has the ability to access a webview
*/
export default function <TBase extends Constructor<WebviewMixable>>(
Base: TBase
): Constructor<InstanceType<TBase> & WebviewMixinType> {
return class extends Base implements WebviewMixinType {
/**
* Cannot use static element, since this class is unnamed.
*/
private handle: string | undefined;

/**
* Search for an element inside the webview iframe.
* Requires webdriver being switched to the webview iframe first.
* (Will attempt to search from the main DOM root otherwise)
*
* @param locator webdriver locator to search by
* @returns promise resolving to WebElement when found
*/
async findWebElement(locator: Locator): Promise<WebElement> {
return await this.getDriver().findElement(locator);
}

/**
* Search for all element inside the webview iframe by a given locator
* Requires webdriver being switched to the webview iframe first.
* (Will attempt to search from the main DOM root otherwise)
*
* @param locator webdriver locator to search by
* @returns promise resolving to a list of WebElement objects
*/
async findWebElements(locator: Locator): Promise<WebElement[]> {
return await this.getDriver().findElements(locator);
}

/**
* Switch the underlying webdriver context to the webview iframe.
* This allows using the findWebElement methods.
* Note that only elements inside the webview iframe will be accessible.
* Use the switchBack method to switch to the original context.
*/
async switchToFrame(): Promise<void> {
if (!this.handle) {
this.handle = await this.getDriver().getWindowHandle();
}

const view = await this.getViewToSwitchTo(this.handle);

if (!view) {
return;
}

await this.getDriver().switchTo().frame(view);

await this.getDriver().wait(
until.elementLocated(AbstractElement.locators.WebView.activeFrame),
5000
);
const frame = await this.getDriver().findElement(
AbstractElement.locators.WebView.activeFrame
);
await this.getDriver().switchTo().frame(frame);
}

/**
* Switch the underlying webdriver back to the original window
*/
async switchBack(): Promise<void> {
if (!this.handle) {
this.handle = await this.getDriver().getWindowHandle();
}
return await this.getDriver().switchTo().window(this.handle);
}
} as unknown as Constructor<InstanceType<TBase> & WebviewMixinType>;
}
21 changes: 21 additions & 0 deletions page-objects/src/components/bottomBar/WebviewView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { WebElement, until } from "selenium-webdriver";
import { AbstractElement } from "../AbstractElement";
import WebviewMixin from "../WebviewMixin";

/**
* Page object representing a user-contributed panel implemented using a Webview.
*/
class WebviewViewBase extends AbstractElement {

constructor() {
super(WebviewViewBase.locators.Workbench.constructor);
}

getViewToSwitchTo(handle: string): Promise<WebElement | undefined> {
return this.getDriver().wait(until.elementLocated(WebviewViewBase.locators.WebView.iframe));
}

}

export const WebviewView = WebviewMixin(WebviewViewBase);
export type WebviewView = InstanceType<typeof WebviewView>;
2 changes: 1 addition & 1 deletion page-objects/src/components/editor/CustomEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class CustomEditor extends Editor {

/**
* Open the Save as prompt
*
*
* @returns InputBox serving as a simple file dialog
*/
async saveAs(): Promise<InputBox> {
Expand Down
10 changes: 5 additions & 5 deletions page-objects/src/components/editor/EditorView.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { AbstractElement } from "../AbstractElement";
import { TextEditor } from "../..";
import { error, WebElement } from "selenium-webdriver";
import { TextEditor } from "../..";
import { AbstractElement } from "../AbstractElement";
import { ElementWithContexMenu } from "../ElementWithContextMenu";
import { DiffEditor } from './DiffEditor';
import { Editor } from "./Editor";
import { EditorAction } from "./EditorAction";
import { SettingsEditor } from "./SettingsEditor";
import { WebView } from "./WebView";
import { DiffEditor } from './DiffEditor';
import { ElementWithContexMenu } from "../ElementWithContextMenu";
import { EditorAction } from "./EditorAction";

export class EditorTabNotFound extends Error {
constructor(title: string, group: number) {
Expand Down
73 changes: 13 additions & 60 deletions page-objects/src/components/editor/WebView.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,13 @@
import { until, WebElement } from "selenium-webdriver";
import WebviewMixin from "../WebviewMixin";
import { Editor } from "./Editor";
import { Locator, until, WebElement } from "selenium-webdriver";

/**
* Page object representing an open editor containing a web view
*/
export class WebView extends Editor {

private static handle: string | undefined;

/**
* Search for an element inside the webview iframe.
* Requires webdriver being switched to the webview iframe first.
* (Will attempt to search from the main DOM root otherwise)
*
* @param locator webdriver locator to search by
* @returns promise resolving to WebElement when found
*/
async findWebElement(locator: Locator): Promise<WebElement> {
return await this.getDriver().findElement(locator);
}

/**
* Search for all element inside the webview iframe by a given locator
* Requires webdriver being switched to the webview iframe first.
* (Will attempt to search from the main DOM root otherwise)
*
* @param locator webdriver locator to search by
* @returns promise resolving to a list of WebElement objects
*/
async findWebElements(locator: Locator): Promise<WebElement[]> {
return await this.getDriver().findElements(locator);
}

/**
* Switch the underlying webdriver context to the webview iframe.
* This allows using the findWebElement methods.
* Note that only elements inside the webview iframe will be accessible.
* Use the switchBack method to switch to the original context.
*/
async switchToFrame(): Promise<void> {
if (!WebView.handle) {
WebView.handle = await this.getDriver().getWindowHandle();
}
class WebViewBase extends Editor {

async getViewToSwitchTo(handle: string): Promise<WebElement | undefined> {
const handles = await this.getDriver().getAllWindowHandles();
for (const handle of handles) {
await this.getDriver().switchTo().window(handle);
Expand All @@ -52,35 +17,23 @@ export class WebView extends Editor {
return;
}
}
await this.getDriver().switchTo().window(WebView.handle);
await this.getDriver().switchTo().window(handle);

const reference = await this.findElement(WebView.locators.EditorView.webView);
const containers = await this.getDriver().wait(until.elementsLocated(WebView.locators.WebView.container(await reference.getAttribute(WebView.locators.WebView.attribute))), 5000);
const reference = await this.findElement(WebViewBase.locators.EditorView.webView);
const containers = await this.getDriver().wait(until.elementsLocated(WebViewBase.locators.WebView.container(await reference.getAttribute(WebViewBase.locators.WebView.attribute))), 5000);

const view = await containers[0].getDriver().wait(async () => {
return await containers[0].getDriver().wait(async () => {
for (let index = 0; index < containers.length; index++) {
const tries = await containers[index].findElements(WebView.locators.WebView.iframe);
const tries = await containers[index].findElements(WebViewBase.locators.WebView.iframe);
if (tries.length > 0) {
return tries[0];
}
}
return undefined;
}, 5000) as WebElement;

await this.getDriver().switchTo().frame(view);

await this.getDriver().wait(until.elementLocated(WebView.locators.WebView.activeFrame), 5000);
const frame = await this.getDriver().findElement(WebView.locators.WebView.activeFrame);
await this.getDriver().switchTo().frame(frame);
}

/**
* Switch the underlying webdriver back to the original window
*/
async switchBack(): Promise<void> {
if (!WebView.handle) {
WebView.handle = await this.getDriver().getWindowHandle();
}
return await this.getDriver().switchTo().window(WebView.handle);
}
}
}

export const WebView = WebviewMixin(WebViewBase);
export type WebView = InstanceType<typeof WebView>;
3 changes: 2 additions & 1 deletion page-objects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from './components/sidebar/debug/DebugView';

export * from './components/bottomBar/BottomBarPanel';
export * from './components/bottomBar/ProblemsView';
export * from './components/bottomBar/WebviewView';
export * from './components/bottomBar/Views';
export * from './components/statusBar/StatusBar';

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

/**
* Initialize the page objects for your tests
*
*
* @param currentVersion version of the locators to load
* @param baseVersion base version of the locators if you have multiple versions with diffs, otherwise leave the same as currentVersion
* @param locatorFolder folder that contains locator files
Expand Down
16 changes: 16 additions & 0 deletions test/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@
"title": "Disable Codelens"
}
],
"viewsContainers": {
"panel": [
{
"icon": "./media/paw-outline.svg",
"id": "myPanel",
"title": "My Panel"
}
]
},
"views": {
"explorer": [
{
Expand All @@ -93,6 +102,13 @@
"id": "emptyView",
"name": "Empty View"
}
],
"myPanel": [
{
"id": "myPanelView",
"name": "My Panel View",
"type": "webview"
}
]
},
"viewsWelcome": [
Expand Down
15 changes: 12 additions & 3 deletions test/test-project/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { TreeView } from './treeView';
import * as vscode from 'vscode';
import { CatScratchEditorProvider } from './catScratchEditor';
import { CodelensProvider } from './codelensProvider';
import { TreeView } from './treeView';

export const ERROR_MESSAGE_COMMAND = 'extension.errorMsg';

Expand Down Expand Up @@ -60,7 +60,7 @@ export function activate(context: vscode.ExtensionContext) {
"extension.populateTestView",
() => { emptyViewNoContent = false; emitter.fire(undefined); }
));

const codelensProvider = new CodelensProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider("*", codelensProvider));
context.subscriptions.push(
Expand All @@ -75,6 +75,9 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("extension.codelensAction", (args: any) => {
vscode.window.showInformationMessage(`CodeLens action clicked with args=${args}`);
}));
context.subscriptions.push(
vscode.window.registerWebviewViewProvider("myPanelView", new MyPanelView())
);
}

export function deactivate() {}
Expand Down Expand Up @@ -135,4 +138,10 @@ class TestView {
</body>
</html>`;
}
}

class MyPanelView implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext<unknown>, token: vscode.CancellationToken): void | Thenable<void> {
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>";
}
}
35 changes: 35 additions & 0 deletions test/test-project/src/test/bottomBar/webviewView-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from 'chai';
import { BottomBarPanel, By, InputBox, WebviewView, Workbench } from 'vscode-extension-tester';

describe('WebviewView', function () {

let webviewView: InstanceType<typeof WebviewView>;

before(async function () {
const prompt = await new Workbench().openCommandPrompt() as InputBox;
await prompt.setText('>My Panel: Focus on My Panel View View');
await prompt.confirm();
});

after(async function () {
if (webviewView) {
await webviewView.switchBack();
webviewView = undefined;
}
await new BottomBarPanel().toggle(false);
});

it('contains apple and banana', async () => {
webviewView = new WebviewView();
await webviewView.switchToFrame();
const elts = await webviewView.findWebElements(By.xpath('//div/ul/li'));
const listContent: string[] = [];
await Promise.all(elts.map(async elt => {
listContent.push(await elt.getText());
}));
expect(listContent).to.have.length(2);
expect(listContent).to.contain('Apple');
expect(listContent).to.contain('Banana');
});

});

0 comments on commit 792655a

Please sign in to comment.