diff --git a/test/automation/src/sql/managePackagesDialog.ts b/test/automation/src/sql/managePackagesDialog.ts new file mode 100644 index 0000000000..da54107aba --- /dev/null +++ b/test/automation/src/sql/managePackagesDialog.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 MANAGE_PACKAGES_DIALOG_TITLE = 'Manage Packages'; + +export class ManagePackagesDialog extends Dialog { + private static readonly dialogPage = '.modal .modal-body .dialogModal-pane'; + + constructor(code: Code) { + super(MANAGE_PACKAGES_DIALOG_TITLE, code); + } + + async waitForManagePackagesDialog(): Promise { + await this.waitForNewDialog(); + } + + async addNewPackage(packageName: string): Promise { + const addNewTab = `${ManagePackagesDialog.dialogPage} div[class="tab-header"][aria-controls="dialogPane.Manage Packages.1"]`; + await this.code.waitAndClick(addNewTab); + + const loadingSpinner = `${ManagePackagesDialog.dialogPage} div.modelview-loadingComponent-spinner`; + + // Wait for "Search Pip packages" placeholder in the input box to know that the tab has finished initializing + const searchPipPackagesInput = `${ManagePackagesDialog.dialogPage} input[placeholder="Search Pip packages"]`; + await this.code.waitForElement(searchPipPackagesInput); + const searchInputBox = `${ManagePackagesDialog.dialogPage} .monaco-inputbox`; + await this.code.waitAndClick(searchInputBox); + + const searchInputBoxEditor = `${searchInputBox} input.input`; + await this.code.waitForTypeInEditor(searchInputBoxEditor, packageName); + + const searchButton = `${ManagePackagesDialog.dialogPage} a[class="monaco-button monaco-text-button"][aria-label="Search"][aria-disabled="false"]`; + await this.code.waitAndClick(searchButton); + + const installButton = `${ManagePackagesDialog.dialogPage} a[class="monaco-button monaco-text-button"][aria-label="Install"][aria-disabled="false"]`; + await this.code.waitAndClick(installButton); + + const installedTab = `${ManagePackagesDialog.dialogPage} div[class="tab-header"][aria-controls="dialogPane.Manage Packages.0"]`; + await this.code.waitAndClick(installedTab); + + // The installed packages tab will reload once the package has been installed + await this.code.waitForElement(loadingSpinner); + await this.code.waitForElementGone(loadingSpinner); + + const closeButton = '.modal .modal-footer a[class="monaco-button monaco-text-button"][aria-label="Close"][aria-disabled="false"]'; + await this.code.waitAndClick(closeButton); + } +} diff --git a/test/automation/src/sql/notebook.ts b/test/automation/src/sql/notebook.ts index da4ff07b59..39e3380775 100644 --- a/test/automation/src/sql/notebook.ts +++ b/test/automation/src/sql/notebook.ts @@ -162,6 +162,11 @@ export class Notebook { // Cell Output Actions + async waitForJupyterErrorOutput(): Promise { + const jupyterErrorOutput = `.notebook-cell.active .notebook-output mime-output[data-mime-type="application/vnd.jupyter.stderr"]`; + await this.code.waitForElement(jupyterErrorOutput); + } + async waitForActiveCellResults(): Promise { const outputComponent = '.notebook-cell.active .notebook-output'; await this.code.waitForElement(outputComponent); @@ -281,6 +286,7 @@ export class NotebookToolbar { private static readonly collapseCellsButtonSelector = `${NotebookToolbar.toolbarButtonSelector}.icon-collapse-cells`; private static readonly expandCellsButtonSelector = `${NotebookToolbar.toolbarButtonSelector}.icon-expand-cells`; private static readonly clearResultsButtonSelector = `${NotebookToolbar.toolbarButtonSelector}.icon-clear-results`; + private static readonly managePackagesButtonSelector = `${NotebookToolbar.toolbarButtonSelector}[title="Manage Packages"]`; constructor(private code: Code) { } @@ -344,6 +350,10 @@ export class NotebookToolbar { async clearResults(): Promise { await this.code.waitAndClick(NotebookToolbar.clearResultsButtonSelector); } + + async managePackages(): Promise { + await this.code.waitAndClick(NotebookToolbar.managePackagesButtonSelector); + } } export class NotebookTreeView { diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index 42bcad285c..3fc7e6e215 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -29,6 +29,7 @@ import { QueryEditors } from './sql/queryEditors'; import { QueryEditor } from './sql/queryEditor'; import { Notebook as SqlNotebook } from './sql/notebook'; import { ConfigurePythonDialog } from './sql/configurePythonDialog'; +import { ManagePackagesDialog } from './sql/managePackagesDialog'; import { CreateBookDialog } from './sql/createBookDialog'; import { NotificationToast } from './sql/notificationToast'; import { AddRemoteBookDialog } from './sql/addRemoteBookDialog'; @@ -66,6 +67,7 @@ export class Workbench { readonly sqlNotebook: SqlNotebook; readonly createBookDialog: CreateBookDialog; readonly configurePythonDialog: ConfigurePythonDialog; + readonly managePackagesDialog: ManagePackagesDialog; readonly notificationToast: NotificationToast; readonly addRemoteBookDialog: AddRemoteBookDialog; // {{END}} @@ -95,6 +97,7 @@ export class Workbench { this.sqlNotebook = new SqlNotebook(code, this.quickaccess, this.quickinput, this.editors); this.createBookDialog = new CreateBookDialog(code); this.configurePythonDialog = new ConfigurePythonDialog(code); + this.managePackagesDialog = new ManagePackagesDialog(code); this.addRemoteBookDialog = new AddRemoteBookDialog(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 b5e5fd66e0..85efc1407c 100644 --- a/test/smoke/src/sql/areas/notebook/notebook.test.ts +++ b/test/smoke/src/sql/areas/notebook/notebook.test.ts @@ -81,6 +81,27 @@ export function setup(opts: minimist.ParsedArgs) { await app.workbench.sqlNotebook.waitForActiveCellResults(); }); + it('can add a new package from the Manage Packages wizard', async function () { + const app = this.app as Application; + await app.workbench.sqlNotebook.newUntitledNotebook(); + await app.workbench.sqlNotebook.notebookToolbar.waitForKernel('SQL'); + await app.workbench.sqlNotebook.notebookToolbar.changeKernel('Python 3'); + await app.workbench.sqlNotebook.notebookToolbar.waitForKernel('Python 3'); + + await app.workbench.sqlNotebook.addCell('code'); + await app.workbench.sqlNotebook.waitForTypeInEditor('import pyarrow'); + await app.workbench.sqlNotebook.runActiveCell(); + await app.workbench.sqlNotebook.waitForJupyterErrorOutput(); + + await app.workbench.sqlNotebook.notebookToolbar.managePackages(); + await app.workbench.managePackagesDialog.waitForManagePackagesDialog(); + await app.workbench.managePackagesDialog.addNewPackage('pyarrow'); + + // There should be no error output when running the cell after pyarrow has been installed + await app.workbench.sqlNotebook.runActiveCell(); + await app.workbench.sqlNotebook.waitForActiveCellResultsGone(); + }); + it('can open ipynb file, run all, and save notebook with outputs', async function () { const app = this.app as Application; await openAndRunNotebook(app, 'hello.ipynb');