diff --git a/package.json b/package.json index 89c56c065d..fae38536c1 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,11 @@ "devDependencies": { "@types/mocha": "~5.2.6", "@types/node": "~11.13.7", + "@types/rewire": "^2.5.28", "mocha": "~5.2.0", "mocha-junit-reporter": "~1.22.0", "mocha-multi-reporters": "~1.1.7", + "rewire": "~4.0.1", "tslint": "~5.16.0", "typescript": "~3.4.5", "vsce": "~1.59.0", diff --git a/src/features/CustomViews.ts b/src/features/CustomViews.ts index 336d097ead..f5999877a7 100644 --- a/src/features/CustomViews.ts +++ b/src/features/CustomViews.ts @@ -2,8 +2,9 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import vscode = require("vscode"); -import { LanguageClient, NotificationType, RequestType } from "vscode-languageclient"; +import * as path from "path"; +import * as vscode from "vscode"; +import { LanguageClient, RequestType } from "vscode-languageclient"; import { IFeature } from "../feature"; export class CustomViewsFeature implements IFeature { @@ -94,13 +95,7 @@ class PowerShellContentProvider implements vscode.TextDocumentContentProvider { public showView(id: string, viewColumn: vscode.ViewColumn) { const uriString = this.getUri(id); - const view: CustomView = this.viewIndex[uriString]; - - vscode.commands.executeCommand( - "vscode.previewHtml", - uriString, - viewColumn, - view.title); + (this.viewIndex[uriString] as HtmlContentView).showContent(viewColumn); } public closeView(id: string) { @@ -164,6 +159,8 @@ class HtmlContentView extends CustomView { styleSheetPaths: [], }; + private webviewPanel: vscode.WebviewPanel; + constructor( id: string, title: string) { @@ -179,42 +176,63 @@ class HtmlContentView extends CustomView { } public getContent(): string { - let styleSrc = "none"; let styleTags = ""; - - function getNonce(): number { - return Math.floor(Math.random() * 100000) + 100000; - } - if (this.htmlContent.styleSheetPaths && this.htmlContent.styleSheetPaths.length > 0) { - styleSrc = ""; this.htmlContent.styleSheetPaths.forEach( - (p) => { - const nonce = getNonce(); - styleSrc += `'nonce-${nonce}' `; - styleTags += `\n`; + (styleSheetPath) => { + styleTags += `\n`; }); } - let scriptSrc = "none"; let scriptTags = ""; - if (this.htmlContent.javaScriptPaths && this.htmlContent.javaScriptPaths.length > 0) { - scriptSrc = ""; this.htmlContent.javaScriptPaths.forEach( - (p) => { - const nonce = getNonce(); - scriptSrc += `'nonce-${nonce}' `; - scriptTags += `\n`; + (javaScriptPath) => { + scriptTags += `\n`; }); } // Return an HTML page with the specified content - return `
` + - `${styleTags}\n${this.htmlContent.bodyContent}\n${scriptTags}`; + return `${styleTags}\n${this.htmlContent.bodyContent}\n${scriptTags}`; + } + + public showContent(viewColumn: vscode.ViewColumn): void { + if (this.webviewPanel) { + this.webviewPanel.dispose(); + } + + let localResourceRoots: vscode.Uri[] = []; + if (this.htmlContent.javaScriptPaths) { + localResourceRoots = localResourceRoots.concat(this.htmlContent.javaScriptPaths.map((p) => { + return vscode.Uri.parse(path.dirname(p)); + })); + } + + if (this.htmlContent.styleSheetPaths) { + localResourceRoots = localResourceRoots.concat(this.htmlContent.styleSheetPaths.map((p) => { + return vscode.Uri.parse(path.dirname(p)); + })); + } + + this.webviewPanel = vscode.window.createWebviewPanel( + this.id, + this.title, + viewColumn, + { + enableScripts: true, + enableFindWidget: true, + enableCommandUris: true, + retainContextWhenHidden: true, + localResourceRoots, + }); + this.webviewPanel.webview.html = this.getContent(); + this.webviewPanel.reveal(viewColumn); } } diff --git a/test/features/CustomViews.test.ts b/test/features/CustomViews.test.ts new file mode 100644 index 0000000000..c9eed44e11 --- /dev/null +++ b/test/features/CustomViews.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from "assert"; +import fs = require("fs"); +import path = require("path"); +import rewire = require("rewire"); +import vscode = require("vscode"); + +// Setup types that are not exported. +const customViews = rewire("../../src/features/CustomViews"); +const htmlContentViewClass = customViews.__get__("HtmlContentView"); +const HtmlContentView: typeof htmlContentViewClass = htmlContentViewClass; + +// interfaces for tests +interface ITestFile { + fileName: string; + content: string; +} + +interface IHtmlContentViewTestCase { + name: string; + htmlContent: string; + javaScriptFiles: ITestFile[]; + cssFiles: ITestFile[]; + expectedHtmlString: string; +} + +function convertToVSCodeResourceScheme(filePath: string): string { + return vscode.Uri.file(filePath).toString().replace("file://", "vscode-resource://"); +} + +suite("CustomViews tests", () => { + const testCases: IHtmlContentViewTestCase[] = [ + // Basic test that has no js or css. + { + name: "Basic", + htmlContent: "hello", + javaScriptFiles: [], + cssFiles: [], + expectedHtmlString: ` +hello +`, + }, + + // A test that adds a js file. + { + name: "With JavaScript file", + htmlContent: "hello", + javaScriptFiles: [ + { + fileName: "testCustomViews.js", + content: "console.log('asdf');", + }, + ], + cssFiles: [], + expectedHtmlString: ` +hello + +`, + }, + + // A test that adds a js file in the current directory, and the parent directory. + { + name: "With 2 JavaScript files in two different locations", + htmlContent: "hello", + javaScriptFiles: [ + { + fileName: "testCustomViews.js", + content: "console.log('asdf');", + }, + { + fileName: "../testCustomViews.js", + content: "console.log('asdf');", + }, + ], + cssFiles: [], + expectedHtmlString: ` +hello + + +`, + }, + + // A test that adds a js file and a css file. + { + name: "With JavaScript and CSS file", + htmlContent: "hello", + javaScriptFiles: [ + { + fileName: "testCustomViews.js", + content: "console.log('asdf');", + }, + ], + cssFiles: [ + { + fileName: "testCustomViews.css", + content: "body: { background-color: green; }", + }, + ], + expectedHtmlString: ` + +hello + +`, + }, + ]; + + for (const testCase of testCases) { + test(`Can create an HtmlContentView and get its content - ${testCase.name}`, () => { + const htmlContentView = new HtmlContentView(); + + const jsPaths = testCase.javaScriptFiles.map((jsFile) => { + const jsPath: string = path.join(__dirname, jsFile.fileName); + fs.writeFileSync(jsPath, jsFile.content); + return vscode.Uri.file(jsPath).toString(); + }); + + const cssPaths = testCase.cssFiles.map((cssFile) => { + const cssPath: string = path.join(__dirname, cssFile.fileName); + fs.writeFileSync(cssPath, cssFile.content); + return vscode.Uri.file(cssPath).toString(); + }); + + htmlContentView.htmlContent = { + bodyContent: testCase.htmlContent, + javaScriptPaths: jsPaths, + styleSheetPaths: cssPaths, + }; + try { + assert.equal(htmlContentView.getContent(), testCase.expectedHtmlString); + } finally { + jsPaths.forEach((jsPath) => fs.unlinkSync(vscode.Uri.parse(jsPath).fsPath)); + cssPaths.forEach((cssPath) => fs.unlinkSync(vscode.Uri.parse(cssPath).fsPath)); + } + }); + } +});