From 9bc11824c39c11a1c82a0fa3dae146d3556f942f Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 20 May 2020 12:01:16 -0500 Subject: [PATCH] Add notebook smoketests --- extensions/vscode-notebook-tests/package.json | 16 ++++ .../src/notebookSmokeTestMain.ts | 73 +++++++++++++++ .../src/notebookTestMain.ts | 3 + test/automation/src/code.ts | 17 +++- test/automation/src/notebook.ts | 92 +++++++++++++++++++ test/automation/src/workbench.ts | 3 + .../smoke/src/areas/notebook/notebook.test.ts | 66 +++++++++++++ test/smoke/src/main.ts | 2 + 8 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts create mode 100644 test/automation/src/notebook.ts create mode 100644 test/smoke/src/areas/notebook/notebook.test.ts diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 8d7e92bd255..0a6b5ee625d 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -27,6 +27,12 @@ "mocha": "^2.3.3" }, "contributes": { + "commands": [ + { + "command": "vscode-notebook-tests.createNewNotebook", + "title": "Create New Notebook" + } + ], "notebookProvider": [ { "viewType": "notebookCoreTest", @@ -37,6 +43,16 @@ "excludeFileNamePattern": "" } ] + }, + { + "viewType": "notebookSmokeTest", + "displayName": "Notebook Smoke Test", + "selector": [ + { + "filenamePattern": "*.smoke-nb", + "excludeFileNamePattern": "" + } + ] } ] } diff --git a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts new file mode 100644 index 00000000000..0018c0e491b --- /dev/null +++ b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as child_process from 'child_process'; +import * as path from 'path'; + +function wait(ms: number): Promise { + return new Promise(r => setTimeout(r, ms)); +} + +export function smokeTestActivate(context: vscode.ExtensionContext): any { + context.subscriptions.push(vscode.commands.registerCommand('vscode-notebook-tests.createNewNotebook', async () => { + const workspacePath = vscode.workspace.workspaceFolders![0].uri.fsPath; + const notebookPath = path.join(workspacePath, 'test.smoke-nb'); + child_process.execSync('echo \'\' > ' + notebookPath); + await wait(500); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(notebookPath)); + })); + + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookSmokeTest', { + onDidChangeNotebook: new vscode.EventEmitter().event, + openNotebook: async (_resource: vscode.Uri) => { + const dto: vscode.NotebookData = { + languages: ['typescript'], + metadata: {}, + cells: [ + { + source: 'code()', + language: 'typescript', + cellKind: vscode.CellKind.Code, + outputs: [], + metadata: { + custom: { testCellMetadata: 123 } + } + }, + { + source: 'Markdown Cell', + language: 'markdown', + cellKind: vscode.CellKind.Markdown, + outputs: [], + metadata: { + custom: { testCellMetadata: 123 } + } + } + ] + }; + + return dto; + }, + executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { + if (!_cell) { + _cell = _document.cells[0]; + } + + _cell.outputs = [{ + outputKind: vscode.CellOutputKind.Rich, + data: { + 'text/html': ['test output'] + } + }]; + return; + }, + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + } + })); +} diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index 38ee07f1a71..155a8e2c847 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -4,8 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { + smokeTestActivate(context); + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { onDidChangeNotebook: new vscode.EventEmitter().event, openNotebook: async (_resource: vscode.Uri) => { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index c9382b9fb23..35f2a4c214a 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -148,11 +148,7 @@ export async function spawn(options: SpawnOptions): Promise { if (codePath) { // running against a build: copy the test resolver extension - const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver'); - if (!fs.existsSync(testResolverExtPath)) { - const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver'); - await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); - } + copyExtension(options, 'vscode-test-resolver'); } args.push('--enable-proposed-api=vscode.vscode-test-resolver'); const remoteDataDir = `${options.userDataDir}-server`; @@ -160,6 +156,9 @@ export async function spawn(options: SpawnOptions): Promise { env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; } + copyExtension(options, 'vscode-notebook-tests'); + args.push('--enable-proposed-api=vscode.vscode-notebook-tests'); + if (!codePath) { args.unshift(repoPath); } @@ -185,6 +184,14 @@ export async function spawn(options: SpawnOptions): Promise { return connect(connectDriver, child, outPath, handle, options.logger); } +async function copyExtension(options: SpawnOptions, extId: string): Promise { + const testResolverExtPath = path.join(options.extensionsPath, extId); + if (!fs.existsSync(testResolverExtPath)) { + const orig = path.join(repoPath, 'extensions', extId); + await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); + } +} + async function poll( fn: () => Thenable, acceptFn: (result: T) => boolean, diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts new file mode 100644 index 00000000000..c0b2fc5ed7c --- /dev/null +++ b/test/automation/src/notebook.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Code } from './code'; +import { QuickAccess } from './quickaccess'; + +const activeRowSelector = `.notebook-editor .monaco-list-row.focused`; + +export class Notebook { + + constructor( + private readonly quickAccess: QuickAccess, + private readonly code: Code) { + } + + async openNotebook() { + await this.quickAccess.runCommand('vscode-notebook-tests.createNewNotebook'); + await this.code.waitForElement(activeRowSelector); + await this.focusFirstCell(); + await this.waitForActiveCellEditorContents('code()'); + } + + async focusNextCell() { + await this.code.dispatchKeybinding('down'); + } + + async focusFirstCell() { + await this.quickAccess.runCommand('notebook.focusTop'); + } + + async editCell() { + await this.code.dispatchKeybinding('enter'); + } + + async stopEditingCell() { + await this.code.dispatchKeybinding('esc'); + } + + async waitForTypeInEditor(text: string): Promise { + const editor = `${activeRowSelector} .monaco-editor`; + + await this.code.waitForElement(editor); + + const textarea = `${editor} textarea`; + await this.code.waitForActiveElement(textarea); + + await this.code.waitForTypeInEditor(textarea, text); + + await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); + } + + async waitForActiveCellEditorContents(contents: string): Promise { + return this._waitForActiveCellEditorContents(str => str === contents); + } + + private async _waitForActiveCellEditorContents(accept: (contents: string) => boolean): Promise { + const selector = `${activeRowSelector} .monaco-editor .view-lines`; + return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); + } + + async waitForMarkdownContents(markdownSelector: string, text: string): Promise { + const selector = `${activeRowSelector} .markdown ${markdownSelector}`; + await this.code.waitForTextContent(selector, text); + } + + async insertNotebookCell(kind: 'markdown' | 'code'): Promise { + if (kind === 'markdown') { + await this.quickAccess.runCommand('notebook.cell.insertMarkdownCellBelow'); + } else { + await this.quickAccess.runCommand('notebook.cell.insertCodeCellBelow'); + } + } + + async deleteActiveCell(): Promise { + await this.quickAccess.runCommand('notebook.cell.delete'); + } + + async focusInCellOutput(): Promise { + await this.quickAccess.runCommand('notebook.cell.focusInOutput'); + await this.code.waitForActiveElement('webview'); + } + + async focusOutCellOutput(): Promise { + await this.quickAccess.runCommand('notebook.cell.focusOutOutput'); + } + + async executeActiveCell(): Promise { + await this.quickAccess.runCommand('notebook.cell.execute'); + } +} diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index e2373948924..9b46d78f8fe 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -19,6 +19,7 @@ import { KeybindingsEditor } from './keybindings'; import { Editors } from './editors'; import { Code } from './code'; import { Terminal } from './terminal'; +import { Notebook } from './notebook'; export interface Commands { runCommand(command: string): Promise; @@ -41,6 +42,7 @@ export class Workbench { readonly settingsEditor: SettingsEditor; readonly keybindingsEditor: KeybindingsEditor; readonly terminal: Terminal; + readonly notebook: Notebook; constructor(code: Code, userDataPath: string) { this.editors = new Editors(code); @@ -58,5 +60,6 @@ export class Workbench { this.settingsEditor = new SettingsEditor(code, userDataPath, this.editors, this.editor, this.quickaccess); this.keybindingsEditor = new KeybindingsEditor(code); this.terminal = new Terminal(code, this.quickaccess); + this.notebook = new Notebook(this.quickaccess, code); } } diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts new file mode 100644 index 00000000000..f052e77d009 --- /dev/null +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import { Application } from '../../../../automation'; + +// function wait(ms: number): Promise { +// return new Promise(r => setTimeout(r, ms)); +// } + + +export function setup() { + describe.only('Notebooks', () => { + after(async function () { + const app = this.app as Application; + cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder }); + cp.execSync('git reset --hard origin/master --quiet', { cwd: app.workspacePathOrFolder }); + }); + + afterEach(async function () { + const app = this.app as Application; + await app.workbench.quickaccess.runCommand('workbench.action.files.save'); + await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor'); + }); + + it('inserts/edits code cell', async function () { + const app = this.app as Application; + await app.workbench.notebook.openNotebook(); + await app.workbench.notebook.focusNextCell(); + await app.workbench.notebook.insertNotebookCell('code'); + await app.workbench.notebook.waitForTypeInEditor('// some code'); + await app.workbench.notebook.stopEditingCell(); + }); + + it('inserts/edits markdown cell', async function () { + const app = this.app as Application; + await app.workbench.notebook.openNotebook(); + await app.workbench.notebook.focusNextCell(); + await app.workbench.notebook.insertNotebookCell('markdown'); + await app.workbench.notebook.waitForTypeInEditor('## hello2! '); + await app.workbench.notebook.stopEditingCell(); + await app.workbench.notebook.waitForMarkdownContents('h2', 'hello2!'); + }); + + it('moves focus as it inserts/deletes a cell', async function () { + const app = this.app as Application; + await app.workbench.notebook.openNotebook(); + await app.workbench.notebook.insertNotebookCell('code'); + await app.workbench.notebook.waitForActiveCellEditorContents(' '); + await app.workbench.notebook.stopEditingCell(); + await app.workbench.notebook.deleteActiveCell(); + await app.workbench.notebook.waitForMarkdownContents('p', 'Markdown Cell'); + }); + + it('moves focus in and out of output', async function () { + const app = this.app as Application; + await app.workbench.notebook.openNotebook(); + await app.workbench.notebook.executeActiveCell(); + await app.workbench.notebook.focusInCellOutput(); + await app.workbench.notebook.focusOutCellOutput(); + await app.workbench.notebook.waitForActiveCellEditorContents('code()'); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 72b768a6e01..51c085de716 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -25,6 +25,7 @@ import { setup as setupDataMigrationTests } from './areas/workbench/data-migrati import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test'; import { setup as setupDataSearchTests } from './areas/search/search.test'; +import { setup as setupDataNotebookTests } from './areas/notebook/notebook.test'; import { setup as setupDataLanguagesTests } from './areas/languages/languages.test'; import { setup as setupDataEditorTests } from './areas/editor/editor.test'; import { setup as setupDataStatusbarTests } from './areas/statusbar/statusbar.test'; @@ -315,6 +316,7 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web) { setupDataLossTests(); } if (!opts.web) { setupDataPreferencesTests(); } setupDataSearchTests(); + setupDataNotebookTests(); setupDataLanguagesTests(); setupDataEditorTests(); setupDataStatusbarTests(!!opts.web);