diff --git a/src/sql/base/browser/ui/selectBox/selectBox.ts b/src/sql/base/browser/ui/selectBox/selectBox.ts index 5a6fd9c886..1d66958504 100644 --- a/src/sql/base/browser/ui/selectBox/selectBox.ts +++ b/src/sql/base/browser/ui/selectBox/selectBox.ts @@ -350,6 +350,7 @@ export class SelectBox extends vsSelectBox { super.render(selectContainer); this.selectElement.classList.add('action-item-label'); + this.selectElement.id = selectOptions.id; } else { super.render(container); @@ -364,4 +365,5 @@ export class SelectBox extends vsSelectBox { export interface ISelectBoxOptionsWithLabel extends ISelectBoxOptions { labelText?: string; labelOnTop?: boolean; + id?: string; } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 9180f3c9ed..3e1d7353d9 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -275,12 +275,13 @@ export class CollapseCellsAction extends ToggleableAction { const showAllKernelsConfigName = 'notebook.showAllKernels'; const workbenchPreviewConfigName = 'workbench.enablePreviewFeatures'; export const noKernelName = localize('noKernel', "No Kernel"); +const kernelDropdownElementId = 'kernel-dropdown'; export class KernelsDropdown extends SelectBox { private model: NotebookModel; private _showAllKernels: boolean = false; constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise, @IConfigurationService private _configurationService: IConfigurationService) { - super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel } as ISelectBoxOptionsWithLabel); + super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel, id: kernelDropdownElementId } as ISelectBoxOptionsWithLabel); if (modelReady) { modelReady @@ -353,6 +354,8 @@ export class KernelsDropdown extends SelectBox { } } +const attachToDropdownElementId = 'attach-to-dropdown'; + export class AttachToDropdown extends SelectBox { private model: NotebookModel; @@ -363,7 +366,7 @@ export class AttachToDropdown extends SelectBox { @INotificationService private _notificationService: INotificationService, @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, ) { - super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false, ariaLabel: attachToLabel } as ISelectBoxOptionsWithLabel); + super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false, ariaLabel: attachToLabel, id: attachToDropdownElementId } as ISelectBoxOptionsWithLabel); if (modelReady) { modelReady .then(model => { diff --git a/test/automation/src/sql/configurePythonDialog.ts b/test/automation/src/sql/configurePythonDialog.ts new file mode 100644 index 0000000000..ee19b27bb2 --- /dev/null +++ b/test/automation/src/sql/configurePythonDialog.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Code } from '../code'; +import { Dialog } from './dialog'; + +const CONFIGURE_PYTHON_DIALOG_TITLE = 'Configure Python to run Python 3 kernel'; + +export class ConfigurePythonDialog extends Dialog { + + constructor(code: Code) { + super(CONFIGURE_PYTHON_DIALOG_TITLE, code); + } + + async waitForConfigurePythonDialog(): Promise { + await this.waitForNewDialog(); + } + + async installPython(): Promise { + const dialog = '.modal .modal-dialog'; + await this.code.waitAndClick(dialog); + + const newPythonInstallation = '.modal .modal-body input[aria-label="New Python installation"]'; + await this.code.waitAndClick(newPythonInstallation); + + const nextButton = '.modal-dialog .modal-content .modal-footer .right-footer .footer-button a[aria-label="Next"][aria-disabled="false"]'; + await this.code.waitForElement(nextButton); + await this.code.dispatchKeybinding('enter'); + + const installButton = '.modal-dialog .modal-content .modal-footer .right-footer .footer-button a[aria-label="Install"][aria-disabled="false"]'; + await this.code.waitForElement(installButton); + await this.code.dispatchKeybinding('enter'); + + return this.waitForDialogGone(); + } + +} diff --git a/test/automation/src/sql/notebook.ts b/test/automation/src/sql/notebook.ts index fb98f24041..069460b059 100644 --- a/test/automation/src/sql/notebook.ts +++ b/test/automation/src/sql/notebook.ts @@ -8,17 +8,82 @@ import { QuickAccess } from '../quickaccess'; import { QuickInput } from '../quickinput'; import { Editors } from '../editors'; +const winOrCtrl = process.platform === 'darwin' ? 'ctrl' : 'win'; + export class Notebook { + public readonly toolbar: NotebookToolbar; + constructor(private code: Code, private quickAccess: QuickAccess, private quickInput: QuickInput, private editors: Editors) { + this.toolbar = new NotebookToolbar(code); } async openFile(fileName: string): Promise { await this.quickAccess.openQuickAccess(fileName); - await this.quickInput.waitForQuickInputElements(names => names[0] === fileName); await this.code.dispatchKeybinding('enter'); await this.editors.waitForActiveTab(fileName); await this.code.waitForElement('.notebookEditor'); } + + async newUntitledNotebook(): Promise { + await this.code.dispatchKeybinding(winOrCtrl + '+alt+n'); + await this.editors.waitForActiveTab('Notebook-0'); + await this.code.waitForElement('.notebookEditor'); + } + + async addCell(cellType: 'markdown' | 'code'): Promise { + if (cellType === 'markdown') { + await this.code.dispatchKeybinding(winOrCtrl + '+shift+t'); + } else { + await this.code.dispatchKeybinding(winOrCtrl + '+shift+c'); + } + + await this.code.waitForElement('.notebook-cell.active'); + } + + async changeKernel(kernel: string): Promise { + await this.toolbar.changeKernel(kernel); + } + + async runActiveCell(): Promise { + await this.code.dispatchKeybinding('F5'); + } + + async runAllCells(): Promise { + await this.code.dispatchKeybinding(winOrCtrl + '+shift+F5'); + } + + async waitForTypeInEditor(text: string) { + const editor = '.notebook-cell.active .monaco-editor'; + await this.code.waitAndClick(editor); + + const textarea = `${editor} textarea`; + await this.code.waitForActiveElement(textarea); + + await this.code.waitForTypeInEditor(textarea, text); + await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); + } + + private async _waitForActiveCellEditorContents(accept: (contents: string) => boolean): Promise { + const selector = '.notebook-cell.active .monaco-editor .view-lines'; + return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); + } + + async waitForResults(): Promise { + const outputComponent = '.notebook-cell.active .notebook-output'; + await this.code.waitForElement(outputComponent); + } +} + +export class NotebookToolbar { + + private static readonly toolbarSelector = '.notebookEditor .editor-toolbar .actions-container'; + constructor(private code: Code) { } + + async changeKernel(kernel: string): Promise { + const kernelDropdown = `${NotebookToolbar.toolbarSelector} select[id="kernel-dropdown"]`; + await this.code.waitForSetValue(kernelDropdown, kernel); + await this.code.dispatchKeybinding('enter'); + } } diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index 887103d635..a5d56a1794 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -27,6 +27,7 @@ import { Profiler } from './sql/profiler'; import { QueryEditors } from './sql/queryEditors'; import { QueryEditor } from './sql/queryEditor'; import { Notebook as SqlNotebook } from './sql/notebook'; +import { ConfigurePythonDialog } from './sql/configurePythonDialog'; // {{END}} export interface Commands { @@ -57,7 +58,8 @@ export class Workbench { readonly profiler: Profiler; readonly queryEditors: QueryEditors; readonly queryEditor: QueryEditor; - readonly sqlNotebbok: SqlNotebook; + readonly sqlNotebook: SqlNotebook; + readonly configurePythonDialog: ConfigurePythonDialog; // {{END}} constructor(code: Code, userDataPath: string) { @@ -81,7 +83,8 @@ export class Workbench { this.profiler = new Profiler(code, this.quickaccess); this.queryEditors = new QueryEditors(code, this.editors); this.queryEditor = new QueryEditor(code); - this.sqlNotebbok = new SqlNotebook(code, this.quickaccess, this.quickinput, this.editors); + this.sqlNotebook = new SqlNotebook(code, this.quickaccess, this.quickinput, this.editors); + this.configurePythonDialog = new ConfigurePythonDialog(code); // {{END}} this.notebook = new Notebook(this.quickaccess, code); } diff --git a/test/smoke/src/sql/areas/notebook/notebook.test.ts b/test/smoke/src/sql/areas/notebook/notebook.test.ts index 0baaf1e0ed..ebdf748aba 100644 --- a/test/smoke/src/sql/areas/notebook/notebook.test.ts +++ b/test/smoke/src/sql/areas/notebook/notebook.test.ts @@ -7,9 +7,21 @@ import { Application } from '../../../../../automation'; export function setup() { describe('Notebook', () => { - it('open ', async function () { + + + it('can open new notebook, configure Python, and execute one cell', async function () { const app = this.app as Application; - await app.workbench.sqlNotebbok.openFile('hello.ipynb'); + await app.workbench.sqlNotebook.newUntitledNotebook(); + await app.workbench.sqlNotebook.addCell('code'); + await app.workbench.sqlNotebook.waitForTypeInEditor('print("Hello world!")'); + + await app.workbench.sqlNotebook.changeKernel('Python 3'); + await app.workbench.configurePythonDialog.waitForConfigurePythonDialog(); + await app.workbench.configurePythonDialog.installPython(); + + await app.workbench.sqlNotebook.runActiveCell(); + await app.workbench.sqlNotebook.waitForResults(); + await app.workbench.quickaccess.runCommand('workbench.action.closeActiveEditor'); }); }); }