566 lines
19 KiB
TypeScript
566 lines
19 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as assert from 'assert';
|
|
import 'mocha';
|
|
import * as os from 'os';
|
|
import * as vscode from 'vscode';
|
|
import { assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils';
|
|
|
|
const webviewId = 'myWebview';
|
|
|
|
function workspaceFile(...segments: string[]) {
|
|
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
|
|
}
|
|
|
|
const testDocument = workspaceFile('bower.json');
|
|
|
|
suite.skip('vscode API - webview', () => {
|
|
const disposables: vscode.Disposable[] = [];
|
|
|
|
function _register<T extends vscode.Disposable>(disposable: T) {
|
|
disposables.push(disposable);
|
|
return disposable;
|
|
}
|
|
|
|
teardown(async () => {
|
|
assertNoRpc();
|
|
await closeAllEditors();
|
|
disposeAll(disposables);
|
|
});
|
|
|
|
test('webviews should be able to send and receive messages', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true }));
|
|
const firstResponse = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
window.addEventListener('message', (message) => {
|
|
vscode.postMessage({ value: message.data.value + 1 });
|
|
});
|
|
</script>`);
|
|
|
|
webview.webview.postMessage({ value: 1 });
|
|
assert.strictEqual((await firstResponse).value, 2);
|
|
});
|
|
|
|
test('webviews should not have scripts enabled by default', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {}));
|
|
const response = Promise.race<any>([
|
|
getMessage(webview),
|
|
new Promise<{}>(resolve => setTimeout(() => resolve({ value: '🎉' }), 1000))
|
|
]);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
vscode.postMessage({ value: '💉' });
|
|
</script>`);
|
|
|
|
assert.strictEqual((await response).value, '🎉');
|
|
});
|
|
|
|
test('webviews should update html', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true }));
|
|
|
|
{
|
|
const response = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
vscode.postMessage({ value: 'first' });
|
|
</script>`);
|
|
|
|
assert.strictEqual((await response).value, 'first');
|
|
}
|
|
{
|
|
const response = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
vscode.postMessage({ value: 'second' });
|
|
</script>`);
|
|
|
|
assert.strictEqual((await response).value, 'second');
|
|
}
|
|
});
|
|
|
|
test.skip('webviews should preserve vscode API state when they are hidden', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true }));
|
|
const ready = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
let value = (vscode.getState() || {}).value || 0;
|
|
|
|
window.addEventListener('message', (message) => {
|
|
switch (message.data.type) {
|
|
case 'get':
|
|
vscode.postMessage({ value });
|
|
break;
|
|
|
|
case 'add':
|
|
++value;;
|
|
vscode.setState({ value });
|
|
vscode.postMessage({ value });
|
|
break;
|
|
}
|
|
});
|
|
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
await ready;
|
|
|
|
const firstResponse = await sendRecieveMessage(webview, { type: 'add' });
|
|
assert.strictEqual(firstResponse.value, 1);
|
|
|
|
// Swap away from the webview
|
|
const doc = await vscode.workspace.openTextDocument(testDocument);
|
|
await vscode.window.showTextDocument(doc);
|
|
|
|
// And then back
|
|
const ready2 = getMessage(webview);
|
|
webview.reveal(vscode.ViewColumn.One);
|
|
await ready2;
|
|
|
|
// We should still have old state
|
|
const secondResponse = await sendRecieveMessage(webview, { type: 'get' });
|
|
assert.strictEqual(secondResponse.value, 1);
|
|
});
|
|
|
|
test('webviews should preserve their context when they are moved between view columns', async () => {
|
|
const doc = await vscode.workspace.openTextDocument(testDocument);
|
|
await vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
|
|
|
|
// Open webview in same column
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true }));
|
|
const ready = getMessage(webview);
|
|
webview.webview.html = statefulWebviewHtml;
|
|
await ready;
|
|
|
|
const firstResponse = await sendRecieveMessage(webview, { type: 'add' });
|
|
assert.strictEqual(firstResponse.value, 1);
|
|
|
|
// Now move webview to new view column
|
|
webview.reveal(vscode.ViewColumn.Two);
|
|
|
|
// We should still have old state
|
|
const secondResponse = await sendRecieveMessage(webview, { type: 'get' });
|
|
assert.strictEqual(secondResponse.value, 1);
|
|
});
|
|
|
|
test('webviews with retainContextWhenHidden should preserve their context when they are hidden', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
|
|
webview.webview.html = statefulWebviewHtml;
|
|
await ready;
|
|
|
|
const firstResponse = await sendRecieveMessage(webview, { type: 'add' });
|
|
assert.strictEqual((await firstResponse).value, 1);
|
|
|
|
// Swap away from the webview
|
|
const doc = await vscode.workspace.openTextDocument(testDocument);
|
|
await vscode.window.showTextDocument(doc);
|
|
|
|
// And then back
|
|
webview.reveal(vscode.ViewColumn.One);
|
|
|
|
// We should still have old state
|
|
const secondResponse = await sendRecieveMessage(webview, { type: 'get' });
|
|
assert.strictEqual(secondResponse.value, 1);
|
|
});
|
|
|
|
test('webviews with retainContextWhenHidden should preserve their page position when hidden', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
${'<h1>Header</h1>'.repeat(200)}
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
setTimeout(() => {
|
|
window.scroll(0, 100);
|
|
vscode.postMessage({ value: window.scrollY });
|
|
}, 500);
|
|
|
|
window.addEventListener('message', (message) => {
|
|
switch (message.data.type) {
|
|
case 'get':
|
|
vscode.postMessage({ value: window.scrollY });
|
|
break;
|
|
}
|
|
});
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
await ready;
|
|
|
|
const firstResponse = getMessage(webview);
|
|
|
|
assert.strictEqual(Math.round((await firstResponse).value), 100);
|
|
|
|
// Swap away from the webview
|
|
const doc = await vscode.workspace.openTextDocument(testDocument);
|
|
await vscode.window.showTextDocument(doc);
|
|
|
|
// And then back
|
|
webview.reveal(vscode.ViewColumn.One);
|
|
|
|
// We should still have old scroll pos
|
|
const secondResponse = await sendRecieveMessage(webview, { type: 'get' });
|
|
assert.strictEqual(Math.round(secondResponse.value), 100);
|
|
});
|
|
|
|
test('webviews with retainContextWhenHidden should be able to recive messages while hidden', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
|
|
webview.webview.html = statefulWebviewHtml;
|
|
await ready;
|
|
|
|
const firstResponse = await sendRecieveMessage(webview, { type: 'add' });
|
|
assert.strictEqual((await firstResponse).value, 1);
|
|
|
|
// Swap away from the webview
|
|
const doc = await vscode.workspace.openTextDocument(testDocument);
|
|
await vscode.window.showTextDocument(doc);
|
|
|
|
// Try posting a message to our hidden webview
|
|
const secondResponse = await sendRecieveMessage(webview, { type: 'add' });
|
|
assert.strictEqual((await secondResponse).value, 2);
|
|
|
|
// Now show webview again
|
|
webview.reveal(vscode.ViewColumn.One);
|
|
|
|
// We should still have old state
|
|
const thirdResponse = await sendRecieveMessage(webview, { type: 'get' });
|
|
assert.strictEqual(thirdResponse.value, 2);
|
|
});
|
|
|
|
|
|
test.skip('webviews should only be able to load resources from workspace by default', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', {
|
|
viewColumn: vscode.ViewColumn.One
|
|
}, {
|
|
enableScripts: true
|
|
}));
|
|
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
window.addEventListener('message', (message) => {
|
|
const img = document.createElement('img');
|
|
img.addEventListener('load', () => {
|
|
vscode.postMessage({ value: true });
|
|
});
|
|
img.addEventListener('error', () => {
|
|
vscode.postMessage({ value: false });
|
|
});
|
|
img.src = message.data.src;
|
|
document.body.appendChild(img);
|
|
});
|
|
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
|
|
const ready = getMessage(webview);
|
|
await ready;
|
|
|
|
{
|
|
const imagePath = webview.webview.asWebviewUri(workspaceFile('image.png'));
|
|
const response = await sendRecieveMessage(webview, { src: imagePath.toString() });
|
|
assert.strictEqual(response.value, true);
|
|
}
|
|
// {
|
|
// // #102188. Resource filename containing special characters like '%', '#', '?'.
|
|
// const imagePath = webview.webview.asWebviewUri(workspaceFile('image%02.png'));
|
|
// const response = await sendRecieveMessage(webview, { src: imagePath.toString() });
|
|
// assert.strictEqual(response.value, true);
|
|
// }
|
|
// {
|
|
// // #102188. Resource filename containing special characters like '%', '#', '?'.
|
|
// const imagePath = webview.webview.asWebviewUri(workspaceFile('image%.png'));
|
|
// const response = await sendRecieveMessage(webview, { src: imagePath.toString() });
|
|
// assert.strictEqual(response.value, true);
|
|
// }
|
|
{
|
|
const imagePath = webview.webview.asWebviewUri(workspaceFile('no-such-image.png'));
|
|
const response = await sendRecieveMessage(webview, { src: imagePath.toString() });
|
|
assert.strictEqual(response.value, false);
|
|
}
|
|
{
|
|
const imagePath = webview.webview.asWebviewUri(workspaceFile('..', '..', '..', 'resources', 'linux', 'code.png'));
|
|
const response = await sendRecieveMessage(webview, { src: imagePath.toString() });
|
|
assert.strictEqual(response.value, false);
|
|
}
|
|
});
|
|
|
|
test.skip('webviews should allow overriding allowed resource paths using localResourceRoots', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {
|
|
enableScripts: true,
|
|
localResourceRoots: [workspaceFile('sub')]
|
|
}));
|
|
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
window.addEventListener('message', (message) => {
|
|
const img = document.createElement('img');
|
|
img.addEventListener('load', () => { vscode.postMessage({ value: true }); });
|
|
img.addEventListener('error', () => { vscode.postMessage({ value: false }); });
|
|
img.src = message.data.src;
|
|
document.body.appendChild(img);
|
|
});
|
|
</script>`);
|
|
|
|
{
|
|
const response = sendRecieveMessage(webview, { src: webview.webview.asWebviewUri(workspaceFile('sub', 'image.png')).toString() });
|
|
assert.strictEqual((await response).value, true);
|
|
}
|
|
{
|
|
const response = sendRecieveMessage(webview, { src: webview.webview.asWebviewUri(workspaceFile('image.png')).toString() });
|
|
assert.strictEqual((await response).value, false);
|
|
}
|
|
});
|
|
|
|
test.skip('webviews using hard-coded old style vscode-resource uri should work', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, {
|
|
enableScripts: true,
|
|
localResourceRoots: [workspaceFile('sub')]
|
|
}));
|
|
|
|
const imagePath = workspaceFile('sub', 'image.png').with({ scheme: 'vscode-resource' }).toString();
|
|
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<img src="${imagePath}">
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
const img = document.getElementsByTagName('img')[0];
|
|
img.addEventListener('load', () => { vscode.postMessage({ value: true }); });
|
|
img.addEventListener('error', () => { vscode.postMessage({ value: false }); });
|
|
</script>`);
|
|
|
|
const firstResponse = getMessage(webview);
|
|
|
|
assert.strictEqual((await firstResponse).value, true);
|
|
});
|
|
|
|
test('webviews should have real view column after they are created, #56097', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.Active }, { enableScripts: true }));
|
|
|
|
// Since we used a symbolic column, we don't know what view column the webview will actually show in at first
|
|
assert.strictEqual(webview.viewColumn, undefined);
|
|
|
|
let changed = false;
|
|
const viewStateChanged = new Promise<vscode.WebviewPanelOnDidChangeViewStateEvent>((resolve) => {
|
|
webview.onDidChangeViewState(e => {
|
|
if (changed) {
|
|
throw new Error('Only expected a single view state change');
|
|
}
|
|
changed = true;
|
|
resolve(e);
|
|
}, undefined, disposables);
|
|
});
|
|
|
|
assert.strictEqual((await viewStateChanged).webviewPanel.viewColumn, vscode.ViewColumn.One);
|
|
|
|
const firstResponse = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
vscode.postMessage({ });
|
|
</script>`);
|
|
|
|
webview.webview.postMessage({ value: 1 });
|
|
await firstResponse;
|
|
assert.strictEqual(webview.viewColumn, vscode.ViewColumn.One);
|
|
});
|
|
|
|
if (os.platform() === 'darwin') {
|
|
test.skip('webview can copy text from webview', async () => {
|
|
const expectedText = `webview text from: ${Date.now()}!`;
|
|
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
|
|
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<b>${expectedText}</b>
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
document.execCommand('selectAll');
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
await ready;
|
|
|
|
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
|
await delay(200); // Make sure copy has time to reach webview
|
|
assert.strictEqual(await vscode.env.clipboard.readText(), expectedText);
|
|
});
|
|
}
|
|
|
|
test('webviews should transfer ArrayBuffers to and from webviews', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
window.addEventListener('message', (message) => {
|
|
switch (message.data.type) {
|
|
case 'add1':
|
|
const arrayBuffer = message.data.array;
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
for (let i = 0; i < uint8Array.length; ++i) {
|
|
uint8Array[i] = uint8Array[i] + 1;
|
|
}
|
|
|
|
vscode.postMessage({ array: arrayBuffer }, [arrayBuffer]);
|
|
break;
|
|
}
|
|
});
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
await ready;
|
|
|
|
const responsePromise = getMessage(webview);
|
|
|
|
const bufferLen = 100;
|
|
|
|
{
|
|
const arrayBuffer = new ArrayBuffer(bufferLen);
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
for (let i = 0; i < bufferLen; ++i) {
|
|
uint8Array[i] = i;
|
|
}
|
|
webview.webview.postMessage({
|
|
type: 'add1',
|
|
array: arrayBuffer
|
|
});
|
|
}
|
|
{
|
|
const response = await responsePromise;
|
|
assert.ok(response.array instanceof ArrayBuffer);
|
|
|
|
const uint8Array = new Uint8Array(response.array);
|
|
for (let i = 0; i < bufferLen; ++i) {
|
|
assert.strictEqual(uint8Array[i], i + 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('webviews should transfer Typed arrays to and from webviews', async () => {
|
|
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
|
|
const ready = getMessage(webview);
|
|
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
|
|
window.addEventListener('message', (message) => {
|
|
switch (message.data.type) {
|
|
case 'add1':
|
|
const uint8Array = message.data.array1;
|
|
|
|
// This should update both buffers since they use the same ArrayBuffer storage
|
|
const uint16Array = message.data.array2;
|
|
for (let i = 0; i < uint16Array.length; ++i) {
|
|
uint16Array[i] = uint16Array[i] + 1;
|
|
}
|
|
|
|
vscode.postMessage({ array1: uint8Array, array2: uint16Array, }, [uint16Array.buffer]);
|
|
break;
|
|
}
|
|
});
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
await ready;
|
|
|
|
const responsePromise = getMessage(webview);
|
|
|
|
const bufferLen = 100;
|
|
{
|
|
const arrayBuffer = new ArrayBuffer(bufferLen);
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
const uint16Array = new Uint16Array(arrayBuffer);
|
|
for (let i = 0; i < uint16Array.length; ++i) {
|
|
uint16Array[i] = i;
|
|
}
|
|
|
|
webview.webview.postMessage({
|
|
type: 'add1',
|
|
array1: uint8Array,
|
|
array2: uint16Array,
|
|
});
|
|
}
|
|
{
|
|
const response = await responsePromise;
|
|
|
|
assert.ok(response.array1 instanceof Uint8Array);
|
|
assert.ok(response.array2 instanceof Uint16Array);
|
|
assert.ok(response.array1.buffer === response.array2.buffer);
|
|
|
|
const uint8Array = response.array1;
|
|
for (let i = 0; i < bufferLen; ++i) {
|
|
if (i % 2 === 0) {
|
|
assert.strictEqual(uint8Array[i], Math.floor(i / 2) + 1);
|
|
} else {
|
|
assert.strictEqual(uint8Array[i], 0);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
function createHtmlDocumentWithBody(body: string): string {
|
|
return /*html*/`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title>Document</title>
|
|
</head>
|
|
<body>
|
|
${body}
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
const statefulWebviewHtml = createHtmlDocumentWithBody(/*html*/ `
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
let value = 0;
|
|
window.addEventListener('message', (message) => {
|
|
switch (message.data.type) {
|
|
case 'get':
|
|
vscode.postMessage({ value });
|
|
break;
|
|
|
|
case 'add':
|
|
++value;;
|
|
vscode.setState({ value });
|
|
vscode.postMessage({ value });
|
|
break;
|
|
}
|
|
});
|
|
vscode.postMessage({ type: 'ready' });
|
|
</script>`);
|
|
|
|
|
|
function getMessage<R = any>(webview: vscode.WebviewPanel): Promise<R> {
|
|
return new Promise<R>(resolve => {
|
|
const sub = webview.webview.onDidReceiveMessage(message => {
|
|
sub.dispose();
|
|
resolve(message);
|
|
});
|
|
});
|
|
}
|
|
|
|
function sendRecieveMessage<T = {}, R = any>(webview: vscode.WebviewPanel, message: T): Promise<R> {
|
|
const p = getMessage<R>(webview);
|
|
webview.webview.postMessage(message);
|
|
return p;
|
|
}
|