提交 bdd37218 编写于 作者: M Matt Bierner

Add custom editor test extension

Adds a simple set of tests for custom editors in a new extension. This is currently not run during CI since we want more testing to make sure it is reliable
上级 d4d1e3b2
......@@ -176,6 +176,24 @@
"order": 6
}
},
{
"type": "extensionHost",
"request": "launch",
"name": "VS Code Custom Editor Tests",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}/extensions/vscode-custom-editor-tests/test-workspace",
"--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-custom-editor-tests",
"--extensionTestsPath=${workspaceFolder}/extensions/vscode-custom-editor-tests/out/test"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"presentation": {
"group": "5_tests",
"order": 6
}
},
{
"type": "chrome",
"request": "attach",
......
......@@ -230,7 +230,8 @@ const excludedCommonExtensions = [
'vscode-test-resolver',
'ms-vscode.node-debug',
'ms-vscode.node-debug2',
'vscode-notebook-tests'
'vscode-notebook-tests',
'vscode-custom-editor-tests',
];
const excludedDesktopExtensions = excludedCommonExtensions.concat([
'vscode-web-playground',
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
(function () {
// @ts-ignore
const vscode = acquireVsCodeApi();
const textArea = document.querySelector('textarea');
const initialState = vscode.getState();
if (initialState) {
textArea.value = initialState.value;
}
window.addEventListener('message', e => {
switch (e.data.type) {
case 'fakeInput':
{
const value = e.data.value;
textArea.value = value;
onInput();
break;
}
case 'setValue':
{
const value = e.data.value;
textArea.value = value;
vscode.setState({ value });
vscode.postMessage({
type: 'didChangeContent',
value: value
});
break;
}
}
});
const onInput = () => {
const value = textArea.value;
vscode.setState({ value });
vscode.postMessage({
type: 'edit',
value: value
});
vscode.postMessage({
type: 'didChangeContent',
value: value
});
};
textArea.addEventListener('input', onInput);
}());
{
"name": "vscode-custom-editor-tests",
"description": "Custom editor tests for VS Code",
"version": "0.0.1",
"publisher": "vscode",
"license": "MIT",
"private": true,
"activationEvents": [
"onCustomEditor:testWebviewEditor.abc"
],
"main": "./out/extension",
"enableProposedApi": true,
"engines": {
"vscode": "^1.48.0"
},
"scripts": {
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-notebook-tests ./tsconfig.json"
},
"dependencies": {
"p-limit": "^3.0.2"
},
"devDependencies": {
"@types/node": "^12.11.7",
"@types/p-limit": "^2.2.0",
"mocha": "^2.3.3",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"vscode": "^1.1.36"
},
"contributes": {
"customEditors": [
{
"viewType": "testWebviewEditor.abc",
"displayName": "Test ABC editor",
"selector": [
{
"filenamePattern": "*.abc"
}
]
}
]
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as pLimit from 'p-limit';
import * as path from 'path';
import * as vscode from 'vscode';
import { Disposable } from './dispose';
export namespace Testing {
export const abcEditorContentChangeCommand = '_abcEditor.contentChange';
export const abcEditorTypeCommand = '_abcEditor.type';
export interface CustomEditorContentChangeEvent {
readonly content: string;
readonly source: vscode.Uri;
}
}
export class AbcTextEditorProvider implements vscode.CustomTextEditorProvider {
public static readonly viewType = 'testWebviewEditor.abc';
private activeEditor?: AbcEditor;
public constructor(
private readonly context: vscode.ExtensionContext,
) { }
public register(): vscode.Disposable {
const provider = vscode.window.registerCustomEditorProvider(AbcTextEditorProvider.viewType, this);
const commands: vscode.Disposable[] = [];
commands.push(vscode.commands.registerCommand(Testing.abcEditorTypeCommand, (content: string) => {
this.activeEditor?.testing_fakeInput(content);
}));
return vscode.Disposable.from(provider, ...commands);
}
public async resolveCustomTextEditor(document: vscode.TextDocument, panel: vscode.WebviewPanel) {
const editor = new AbcEditor(document, this.context.extensionPath, panel);
this.activeEditor = editor;
panel.onDidChangeViewState(({ webviewPanel }) => {
if (this.activeEditor === editor && !webviewPanel.active) {
this.activeEditor = undefined;
}
if (webviewPanel.active) {
this.activeEditor = editor;
}
});
}
}
class AbcEditor extends Disposable {
public readonly _onDispose = this._register(new vscode.EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
private readonly limit = pLimit(1);
private syncedVersion: number = -1;
private currentWorkspaceEdit?: Thenable<void>;
constructor(
private readonly document: vscode.TextDocument,
private readonly _extensionPath: string,
private readonly panel: vscode.WebviewPanel,
) {
super();
panel.webview.options = {
enableScripts: true,
};
panel.webview.html = this.html;
this._register(vscode.workspace.onDidChangeTextDocument(e => {
if (e.document === this.document) {
this.update();
}
}));
this._register(panel.webview.onDidReceiveMessage(message => {
switch (message.type) {
case 'edit':
this.doEdit(message.value);
break;
case 'didChangeContent':
vscode.commands.executeCommand(Testing.abcEditorContentChangeCommand, {
content: message.value,
source: document.uri,
} as Testing.CustomEditorContentChangeEvent);
break;
}
}));
this._register(panel.onDidDispose(() => { this.dispose(); }));
this.update();
}
public testing_fakeInput(value: string) {
this.panel.webview.postMessage({
type: 'fakeInput',
value: value,
});
}
private async doEdit(value: string) {
const edit = new vscode.WorkspaceEdit();
edit.replace(this.document.uri, this.document.validateRange(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(999999, 999999))), value);
this.limit(() => {
this.currentWorkspaceEdit = vscode.workspace.applyEdit(edit).then(() => {
this.syncedVersion = this.document.version;
this.currentWorkspaceEdit = undefined;
});
return this.currentWorkspaceEdit;
});
}
public dispose() {
if (this.isDisposed) {
return;
}
this._onDispose.fire();
super.dispose();
}
private get html() {
const contentRoot = path.join(this._extensionPath, 'customEditorMedia');
const scriptUri = vscode.Uri.file(path.join(contentRoot, 'textEditor.js'));
const nonce = Date.now() + '';
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="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline';">
<title>Document</title>
</head>
<body>
<textarea style="width: 300px; height: 300px;"></textarea>
<script nonce=${nonce} src="${this.panel.webview.asWebviewUri(scriptUri)}"></script>
</body>
</html>`;
}
public async update() {
await this.currentWorkspaceEdit;
if (this.isDisposed || this.syncedVersion >= this.document.version) {
return;
}
this.panel.webview.postMessage({
type: 'setValue',
value: this.document.getText(),
});
this.syncedVersion = this.document.version;
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]) {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: vscode.Disposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AbcTextEditorProvider } from './customTextEditor';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(new AbcTextEditorProvider(context).register());
}
/*---------------------------------------------------------------------------------------------
* 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 * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { Testing } from '../customTextEditor';
import { closeAllEditors, delay, disposeAll, randomFilePath } from './utils';
assert.ok(vscode.workspace.rootPath);
const testWorkspaceRoot = vscode.Uri.file(path.join(vscode.workspace.rootPath!, 'customEditors'));
const commands = Object.freeze({
open: 'vscode.open',
openWith: 'vscode.openWith',
save: 'workbench.action.files.save',
undo: 'undo',
});
async function writeRandomFile(options: { ext: string; contents: string; }): Promise<vscode.Uri> {
const fakeFile = randomFilePath({ root: testWorkspaceRoot, ext: options.ext });
await fs.promises.writeFile(fakeFile.fsPath, Buffer.from(options.contents));
return fakeFile;
}
const disposables: vscode.Disposable[] = [];
function _register<T extends vscode.Disposable>(disposable: T) {
disposables.push(disposable);
return disposable;
}
class CustomEditorUpdateListener {
public static create() {
return _register(new CustomEditorUpdateListener());
}
private readonly commandSubscription: vscode.Disposable;
private readonly unconsumedResponses: Array<Testing.CustomEditorContentChangeEvent> = [];
private readonly callbackQueue: Array<(data: Testing.CustomEditorContentChangeEvent) => void> = [];
private constructor() {
this.commandSubscription = vscode.commands.registerCommand(Testing.abcEditorContentChangeCommand, (data: Testing.CustomEditorContentChangeEvent) => {
if (this.callbackQueue.length) {
const callback = this.callbackQueue.shift();
assert.ok(callback);
callback!(data);
} else {
this.unconsumedResponses.push(data);
}
});
}
dispose() {
this.commandSubscription.dispose();
}
async nextResponse(): Promise<Testing.CustomEditorContentChangeEvent> {
if (this.unconsumedResponses.length) {
return this.unconsumedResponses.shift()!;
}
return new Promise(resolve => {
this.callbackQueue.push(resolve);
});
}
}
suite('CustomEditor tests', () => {
setup(async () => {
await closeAllEditors();
await resetTestWorkspace();
});
teardown(async () => {
await closeAllEditors();
disposeAll(disposables);
await resetTestWorkspace();
});
test('Should load basic content from disk', async () => {
const startingContent = `load, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
const { content } = await listener.nextResponse();
assert.equal(content, startingContent);
});
test('Should support basic edits', async () => {
const startingContent = `basic edit, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const newContent = `basic edit test`;
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent);
const { content } = await listener.nextResponse();
assert.equal(content, newContent);
});
test('Should support single undo', async () => {
const startingContent = `single undo, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const newContent = `undo test`;
{
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent);
const { content } = await listener.nextResponse();
assert.equal(content, newContent);
}
await delay(100);
{
await vscode.commands.executeCommand(commands.undo);
const { content } = await listener.nextResponse();
assert.equal(content, startingContent);
}
});
test('Should support multiple undo', async () => {
const startingContent = `multiple undo, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const count = 10;
// Make edits
for (let i = 0; i < count; ++i) {
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `${i}`);
const { content } = await listener.nextResponse();
assert.equal(`${i}`, content);
}
// Then undo them in order
for (let i = count - 1; i; --i) {
await delay(100);
await vscode.commands.executeCommand(commands.undo);
const { content } = await listener.nextResponse();
assert.equal(`${i - 1}`, content);
}
{
await delay(100);
await vscode.commands.executeCommand(commands.undo);
const { content } = await listener.nextResponse();
assert.equal(content, startingContent);
}
});
test('Should update custom editor on file move', async () => {
const startingContent = `file move, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const newFileName = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'y.abc'));
const edit = new vscode.WorkspaceEdit();
edit.renameFile(testDocument, newFileName);
await vscode.workspace.applyEdit(edit);
const response = (await listener.nextResponse());
assert.equal(response.content, startingContent);
assert.equal(response.source.toString(), newFileName.toString());
});
test('Should support saving custom editors', async () => {
const startingContent = `save, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const newContent = `save, new`;
{
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent);
const { content } = await listener.nextResponse();
assert.equal(content, newContent);
}
{
await vscode.commands.executeCommand(commands.save);
const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString();
assert.equal(fileContent, newContent);
}
});
test('Should undo after saving custom editor', async () => {
const startingContent = `undo after save, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const newContent = `undo after save, new`;
{
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, newContent);
const { content } = await listener.nextResponse();
assert.equal(content, newContent);
}
{
await vscode.commands.executeCommand(commands.save);
const fileContent = (await fs.promises.readFile(testDocument.fsPath)).toString();
assert.equal(fileContent, newContent);
}
await delay(100);
{
await vscode.commands.executeCommand(commands.undo);
const { content } = await listener.nextResponse();
assert.equal(content, startingContent);
}
});
test.skip('Should support untitled custom editors', async () => {
const listener = CustomEditorUpdateListener.create();
const untitledFile = randomFilePath({ root: testWorkspaceRoot, ext: '.abc' }).with({ scheme: 'untitled' });
await vscode.commands.executeCommand(commands.open, untitledFile);
assert.equal((await listener.nextResponse()).content, '');
await vscode.commands.executeCommand(Testing.abcEditorTypeCommand, `123`);
assert.equal((await listener.nextResponse()).content, '123');
await vscode.commands.executeCommand(commands.save);
const content = await fs.promises.readFile(untitledFile.fsPath);
assert.equal(content.toString(), '123');
});
test.skip('When switching away from a non-default custom editors and then back, we should continue using the non-default editor', async () => {
const startingContent = `switch, init`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
{
await vscode.commands.executeCommand(commands.open, testDocument, { preview: false });
const { content } = await listener.nextResponse();
assert.strictEqual(content, startingContent.toString());
assert.ok(!vscode.window.activeTextEditor);
}
// Switch to non-default editor
await vscode.commands.executeCommand(commands.openWith, testDocument, 'default', { preview: false });
assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString());
// Then open a new document (hiding existing one)
const otherFile = vscode.Uri.file(path.join(testWorkspaceRoot.fsPath, 'other.json'));
await vscode.commands.executeCommand(commands.open, otherFile);
assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), otherFile.toString());
// And then back
await vscode.commands.executeCommand('workbench.action.navigateBack');
await vscode.commands.executeCommand('workbench.action.navigateBack');
// Make sure we have the file on as text
assert.ok(vscode.window.activeTextEditor);
assert.strictEqual(vscode.window.activeTextEditor!?.document.uri.toString(), testDocument.toString());
});
test('Should release the text document when the editor is closed', async () => {
const startingContent = `release document init,`;
const testDocument = await writeRandomFile({ ext: '.abc', contents: startingContent });
const listener = CustomEditorUpdateListener.create();
await vscode.commands.executeCommand(commands.open, testDocument);
await listener.nextResponse();
const doc = vscode.workspace.textDocuments.find(x => x.uri.toString() === testDocument.toString());
assert.ok(doc);
assert.ok(!doc!.isClosed);
await closeAllEditors();
await delay(100);
assert.ok(doc!.isClosed);
});
});
async function resetTestWorkspace() {
try {
await vscode.workspace.fs.delete(testWorkspaceRoot, { recursive: true });
} catch {
// ok if file doesn't exist
}
await vscode.workspace.fs.createDirectory(testWorkspaceRoot);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const suite = 'Custom Editor Tests';
const options: any = {
ui: 'tdd',
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
timeout: 6000000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function randomFilePath(args: { root: vscode.Uri, ext: string }): vscode.Uri {
const fileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
return (vscode.Uri as any).joinPath(args.root, fileName + args.ext);
}
export function closeAllEditors(): Thenable<any> {
return vscode.commands.executeCommand('workbench.action.closeAllEditors');
}
export function disposeAll(disposables: vscode.Disposable[]) {
vscode.Disposable.from(...disposables).dispose();
}
export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path="../../../../src/vs/vscode.d.ts" />
/// <reference types='@types/node'/>
{
"extends": "../shared.tsconfig.json",
"compilerOptions": {
"outDir": "./out"
},
"include": [
"src/**/*"
]
}
\ No newline at end of file
此差异已折叠。
......@@ -23,6 +23,7 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" (
compile-extension:vscode-colorize-tests^
compile-extension:markdown-language-features^
compile-extension:typescript-language-features^
compile-extension:vscode-custom-editor-tests^
compile-extension:vscode-notebook-tests^
compile-extension:emmet^
compile-extension:css-language-features-server^
......
......@@ -27,6 +27,7 @@ else
# and the build bundles extensions into .build webpacked
yarn gulp compile-extension:vscode-api-tests \
compile-extension:vscode-colorize-tests \
compile-extension:vscode-custom-editor-tests \
compile-extension:vscode-notebook-tests \
compile-extension:markdown-language-features \
compile-extension:typescript-language-features \
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册