diff --git a/page-objects/src/components/WebviewMixin.ts b/page-objects/src/components/WebviewMixin.ts new file mode 100644 index 000000000..c82f6379b --- /dev/null +++ b/page-objects/src/components/WebviewMixin.ts @@ -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 = 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; +} + +/** + * The interface that is exposed by applying this mixin. + */ +export interface WebviewMixinType { + findWebElement(locator: Locator): Promise; + findWebElements(locator: Locator): Promise; + switchToFrame(): Promise; + switchBack(): Promise; +} + +/** + * 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 >( + Base: TBase +): Constructor & 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 { + 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 { + 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 { + 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 { + if (!this.handle) { + this.handle = await this.getDriver().getWindowHandle(); + } + return await this.getDriver().switchTo().window(this.handle); + } + } as unknown as Constructor & WebviewMixinType>; +} diff --git a/page-objects/src/components/bottomBar/WebviewView.ts b/page-objects/src/components/bottomBar/WebviewView.ts new file mode 100644 index 000000000..307764638 --- /dev/null +++ b/page-objects/src/components/bottomBar/WebviewView.ts @@ -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 { + return this.getDriver().wait(until.elementLocated(WebviewViewBase.locators.WebView.iframe)); + } + +} + +export const WebviewView = WebviewMixin(WebviewViewBase); +export type WebviewView = InstanceType; \ No newline at end of file diff --git a/page-objects/src/components/editor/CustomEditor.ts b/page-objects/src/components/editor/CustomEditor.ts index 0e98ba708..e176e1e5a 100644 --- a/page-objects/src/components/editor/CustomEditor.ts +++ b/page-objects/src/components/editor/CustomEditor.ts @@ -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 { diff --git a/page-objects/src/components/editor/EditorView.ts b/page-objects/src/components/editor/EditorView.ts index e68ca6a4c..acfea8c02 100644 --- a/page-objects/src/components/editor/EditorView.ts +++ b/page-objects/src/components/editor/EditorView.ts @@ -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) { diff --git a/page-objects/src/components/editor/WebView.ts b/page-objects/src/components/editor/WebView.ts index 4d7c71fb1..1b1447920 100644 --- a/page-objects/src/components/editor/WebView.ts +++ b/page-objects/src/components/editor/WebView.ts @@ -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 { - 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 { - 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 { - if (!WebView.handle) { - WebView.handle = await this.getDriver().getWindowHandle(); - } +class WebViewBase extends Editor { + async getViewToSwitchTo(handle: string): Promise { const handles = await this.getDriver().getAllWindowHandles(); for (const handle of handles) { await this.getDriver().switchTo().window(handle); @@ -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 { - if (!WebView.handle) { - WebView.handle = await this.getDriver().getWindowHandle(); - } - return await this.getDriver().switchTo().window(WebView.handle); - } -} \ No newline at end of file +} + +export const WebView = WebviewMixin(WebViewBase); +export type WebView = InstanceType; \ No newline at end of file diff --git a/page-objects/src/index.ts b/page-objects/src/index.ts index 2c81a5a0e..a89a9b2ca 100644 --- a/page-objects/src/index.ts +++ b/page-objects/src/index.ts @@ -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'; @@ -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 diff --git a/test/test-project/package.json b/test/test-project/package.json index e87ae09a2..ebf16c5bf 100644 --- a/test/test-project/package.json +++ b/test/test-project/package.json @@ -83,6 +83,15 @@ "title": "Disable Codelens" } ], + "viewsContainers": { + "panel": [ + { + "icon": "./media/paw-outline.svg", + "id": "myPanel", + "title": "My Panel" + } + ] + }, "views": { "explorer": [ { @@ -93,6 +102,13 @@ "id": "emptyView", "name": "Empty View" } + ], + "myPanel": [ + { + "id": "myPanelView", + "name": "My Panel View", + "type": "webview" + } ] }, "viewsWelcome": [ diff --git a/test/test-project/src/extension.ts b/test/test-project/src/extension.ts index 8141d5c40..9370e0848 100644 --- a/test/test-project/src/extension.ts +++ b/test/test-project/src/extension.ts @@ -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'; @@ -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( @@ -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() {} @@ -135,4 +138,10 @@ class TestView { `; } +} + +class MyPanelView implements vscode.WebviewViewProvider { + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): void | Thenable { + webviewView.webview.html = "My Panel View

Shopping List

  • Apple
  • Banana
"; + } } \ No newline at end of file diff --git a/test/test-project/src/test/bottomBar/webviewView-test.ts b/test/test-project/src/test/bottomBar/webviewView-test.ts new file mode 100644 index 000000000..458e580fd --- /dev/null +++ b/test/test-project/src/test/bottomBar/webviewView-test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import { BottomBarPanel, By, InputBox, WebviewView, Workbench } from 'vscode-extension-tester'; + +describe('WebviewView', function () { + + let webviewView: InstanceType; + + 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'); + }); + +}); \ No newline at end of file