/*--------------------------------------------------------------------------------------------- * 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 { QuickAccess } from '../quickaccess'; import { QuickInput } from '../quickinput'; import { Editors } from '../editors'; import { IElement } from '..'; const winOrCtrl = process.platform === 'darwin' ? 'ctrl' : 'win'; const ctrlOrCmd = process.platform === 'darwin' ? 'cmd' : 'ctrl'; export class Notebook { public readonly notebookToolbar: NotebookToolbar; public readonly textCellToolbar: TextCellToolbar; public readonly view: NotebookTreeView; constructor(private code: Code, private quickAccess: QuickAccess, private quickInput: QuickInput, private editors: Editors) { this.notebookToolbar = new NotebookToolbar(code); this.textCellToolbar = new TextCellToolbar(code); this.view = new NotebookTreeView(code, quickAccess); } async openFile(fileName: string): Promise { await this.quickAccess.openQuickAccess(fileName); await this.quickInput.waitForQuickInputElements(names => names[0] === fileName); await this.code.waitAndClick('.quick-input-widget .quick-input-list .monaco-list-row'); 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'); } // Notebook Toolbar Actions (keyboard shortcuts) async addCell(cellType: 'markdown' | 'code'): Promise { if (cellType === 'markdown') { await this.code.dispatchKeybinding('ctrl+shift+t'); } else { await this.code.dispatchKeybinding('ctrl+shift+c'); } await this.code.waitForElement('.notebook-cell.active'); } async runActiveCell(): Promise { await this.code.dispatchKeybinding('F5'); } async runAllCells(): Promise { await this.code.dispatchKeybinding('ctrl+shift+F5'); } // Cell Actions 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 waitForColorization(spanNumber: string, color: string): Promise { const span = `span:nth-child(${spanNumber})[class="${color}"]`; await this.code.waitForElement(span); } public async selectAllTextInEditor(): Promise { const editor = '.notebook-cell.active .monaco-editor'; await this.code.waitAndClick(editor); await this.code.dispatchKeybinding(ctrlOrCmd + '+a'); } private static readonly placeholderSelector = 'div.placeholder-cell-component'; async addCellFromPlaceholder(cellType: 'Markdown' | 'Code'): Promise { await this.code.waitAndClick(`${Notebook.placeholderSelector} p a[id="add${cellType}"]`); await this.code.waitForElement('.notebook-cell.active'); } async waitForPlaceholderGone(): Promise { await this.code.waitForElementGone(Notebook.placeholderSelector); } async waitForCollapseIconInCells(): Promise { let cellIds = await this.getCellIds(); for (let i of cellIds) { const editor = `.notebook-cell[id="${i}"] code-cell-component code-component collapse-component`; await this.code.waitForElement(`${editor} [title="Collapse code cell contents"]`); } } async waitForExpandIconInCells(): Promise { let cellIds = await this.getCellIds(); for (let i of cellIds) { const editor = `.notebook-cell[id="${i}"] code-cell-component code-component collapse-component`; await this.code.waitForElement(`${editor} [title="Expand code cell contents"]`); } } /** * Helper function * @returns cell ids for the notebook */ async getCellIds(): Promise { return (await this.code.waitForElements('div.notebook-cell', false)).map(cell => cell.attributes['id']); } // Code Cell Actions async waitForSuggestionWidget(): Promise { const suggestionWidgetSelector = 'div.editor-widget.suggest-widget'; await this.code.waitForElement(suggestionWidgetSelector); } async waitForSuggestionResult(expectedResult: string): Promise { const expectedResultSelector = `div.editor-widget.suggest-widget div.monaco-list-row.focused[aria-label="${expectedResult}"]`; await this.code.waitForElement(expectedResultSelector); } // Text Cell Actions private static readonly textCellPreviewSelector = 'div.notebook-preview'; private static readonly doubleClickToEditSelector = `${Notebook.textCellPreviewSelector} p i`; async waitForDoubleClickToEdit(): Promise { await this.code.waitForElement(Notebook.doubleClickToEditSelector); } async doubleClickTextCell(): Promise { await this.code.waitAndClick(Notebook.textCellPreviewSelector); await this.code.waitAndDoubleClick(`${Notebook.textCellPreviewSelector}.actionselect`); } async waitForDoubleClickToEditGone(): Promise { await this.code.waitForElementGone(Notebook.doubleClickToEditSelector); } async waitForTextCellPreviewContent(text: string, selector: string): Promise { let textSelector = `${Notebook.textCellPreviewSelector} ${selector}`; await this.code.waitForElement(textSelector, result => !!result?.textContent?.includes(text)); // Use includes to handle whitespace/quote edge cases } async waitForTextCellPreviewContentGone(selector: string): Promise { let textSelector = `${Notebook.textCellPreviewSelector} ${selector}`; await this.code.waitForElementGone(textSelector); } // 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); } async waitForResults(cellIds: string[]): Promise { for (let i of cellIds) { await this.code.waitForElement(`div.notebook-cell[id="${i}"] .notebook-output`); } } async waitForAllResults(): Promise { await this.waitForResults(await this.getCellIds()); } async waitForActiveCellResultsGone(): Promise { const outputComponent = '.notebook-cell.active .notebook-output'; await this.code.waitForElementGone(outputComponent); } async waitForResultsGone(cellIds: string[]): Promise { for (let i of cellIds) { await this.code.waitForElementGone(`div.notebook-cell[id="${i}"] .notebook-output`); } } async waitForAllResultsGone(): Promise { await this.waitForResultsGone(await this.getCellIds()); } async waitForTrustedElements(): Promise { const cellSelector = '.notebookEditor .notebook-cell'; await this.code.waitForElement(`${cellSelector} iframe`); await this.code.waitForElement(`${cellSelector} dialog`); await this.code.waitForElement(`${cellSelector} embed`); await this.code.waitForElement(`${cellSelector} svg`); } async waitForTrustedElementsGone(): Promise { const cellSelector = '.notebookEditor .notebook-cell'; await this.code.waitForElementGone(`${cellSelector} iframe`); await this.code.waitForElementGone(`${cellSelector} dialog`); await this.code.waitForElementGone(`${cellSelector} embed`); await this.code.waitForElementGone(`${cellSelector} svg`); } } export class TextCellToolbar { private static readonly textCellToolbar = 'text-cell-component markdown-toolbar-component ul.actions-container li.action-item'; constructor(private code: Code) { } public async changeTextCellView(view: 'Rich Text View' | 'Split View' | 'Markdown View'): Promise { await this.clickToolbarButton(view); } public async boldSelectedText(): Promise { await this.clickToolbarButton('Bold'); } public async italicizeSelectedText(): Promise { await this.clickToolbarButton('Italic'); } public async underlineSelectedText(): Promise { await this.clickToolbarButton('Underline'); } public async highlightSelectedText(): Promise { await this.clickToolbarButton('Highlight'); } public async codifySelectedText(): Promise { await this.clickToolbarButton('Insert code'); } public async insertLink(linkLabel: string, linkAddress: string): Promise { await this.clickToolbarButton('Insert link'); const linkDialogSelector = 'div.modal.callout-dialog[aria-label="Insert link"]'; const displayTextSelector = `${linkDialogSelector} input[aria-label="Text to display"]`; await this.code.waitForSetValue(displayTextSelector, linkLabel); const addressTextSelector = `${linkDialogSelector} input[aria-label="Address"]`; await this.code.waitForSetValue(addressTextSelector, linkAddress); await this.code.dispatchKeybinding('enter'); } public async insertList(): Promise { await this.clickToolbarButton('Insert list'); } public async insertOrderedList(): Promise { await this.clickToolbarButton('Insert ordered list'); } // Disabled since the text size dropdown is not clickable on Unix from smoke tests // public async changeSelectedTextSize(textSize: 'Heading 1' | 'Heading 2' | 'Heading 3' | 'Paragraph'): Promise { // const actionSelector = `${TextCellToolbar.textCellToolbar} .monaco-dropdown a.heading-dropdown`; // await this.code.waitAndClick(actionSelector); // const menuItemSelector = `.context-view.monaco-menu-container .monaco-menu .action-menu-item[title="${textSize}"]`; // await this.code.waitAndClick(menuItemSelector); // } private async clickToolbarButton(buttonTitle: string) { const actionSelector = `${TextCellToolbar.textCellToolbar} a[title="${buttonTitle}"]`; await this.code.waitAndClick(actionSelector); } } export class NotebookToolbar { private static readonly toolbarSelector = '.notebookEditor .editor-toolbar .actions-container'; private static readonly toolbarButtonSelector = `${NotebookToolbar.toolbarSelector} a.action-label.codicon.masked-icon`; private static readonly trustedButtonSelector = `${NotebookToolbar.toolbarButtonSelector}.icon-shield`; private static readonly notTrustedButtonSelector = `${NotebookToolbar.toolbarButtonSelector}.icon-shield-x`; 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) { } async changeKernel(kernel: string): Promise { const kernelDropdown = `${NotebookToolbar.toolbarSelector} select[id="kernel-dropdown"]`; await this.code.waitForSetValue(kernelDropdown, kernel); await this.code.dispatchKeybinding('enter'); } async waitForKernel(kernel: string): Promise { const kernelDropdownValue = `${NotebookToolbar.toolbarSelector} select[id="kernel-dropdown"][title="${kernel}"]`; await this.code.waitForElement(kernelDropdownValue, undefined, 3000); // wait up to 5 minutes for kernel change } async trustNotebook(): Promise { await this.code.waitAndClick(NotebookToolbar.toolbarSelector); let buttons: IElement[] = await this.code.waitForElements(NotebookToolbar.toolbarButtonSelector, false); buttons.forEach(async button => { if (button.className.includes('icon-shield-x')) { await this.code.waitAndClick(NotebookToolbar.notTrustedButtonSelector); return; } else if (button.className.includes('icon-shield')) { // notebook is already trusted return; } }); } async waitForTrustedIcon(): Promise { await this.code.waitForElement(NotebookToolbar.trustedButtonSelector); } async waitForNotTrustedIcon(): Promise { await this.code.waitForElement(NotebookToolbar.notTrustedButtonSelector); } async collapseCells(): Promise { let buttons: IElement[] = await this.code.waitForElements(NotebookToolbar.toolbarButtonSelector, false); let collapseButton = buttons.find(button => button.className.includes('icon-collapse-cells')); if (collapseButton) { await this.code.waitAndClick(NotebookToolbar.collapseCellsButtonSelector); } } async expandCells(): Promise { let buttons: IElement[] = await this.code.waitForElements(NotebookToolbar.toolbarButtonSelector, false); let expandButton = buttons.find(button => button.className.includes('icon-expand-cells')); if (expandButton) { await this.code.waitAndClick(NotebookToolbar.expandCellsButtonSelector); } } async waitForCollapseCellsNotebookIcon(): Promise { await this.code.waitForElement(NotebookToolbar.collapseCellsButtonSelector); } async waitForExpandCellsNotebookIcon(): Promise { await this.code.waitForElement(NotebookToolbar.expandCellsButtonSelector); } async clearResults(): Promise { await this.code.waitAndClick(NotebookToolbar.clearResultsButtonSelector); } async managePackages(): Promise { await this.code.waitAndClick(NotebookToolbar.managePackagesButtonSelector); } } export class NotebookTreeView { private static readonly inputBox = '.notebookExplorer-viewlet .search-widget .input-box'; private static searchResult = '.search-view .result-messages'; private static notebookTreeItem = '.split-view-view .tree-explorer-viewlet-tree-view .monaco-list-row'; private static selectedItem = '.focused.selected'; private static pinnedNotebooksSelector = '.split-view-view .tree-explorer-viewlet-tree-view .monaco-list[aria-label="Pinned notebooks"] .monaco-list-row'; constructor(private code: Code, private quickAccess: QuickAccess) { } async focusSearchResultsView(): Promise { return this.quickAccess.runCommand('Notebooks: Focus on Search Results View'); } async focusNotebooksView(): Promise { return this.quickAccess.runCommand('Notebooks: Focus on Notebooks View'); } async focusPinnedNotebooksView(): Promise { return this.quickAccess.runCommand('Notebooks: Focus on Pinned notebooks View'); } async searchInNotebook(expr: string): Promise { await this.waitForSetSearchValue(expr); await this.code.dispatchKeybinding('enter'); let selector = NotebookTreeView.searchResult; if (expr) { selector += ' .message'; } return this.code.waitForElement(selector, undefined); } async waitForSetSearchValue(text: string): Promise { const textArea = `${NotebookTreeView.inputBox} textarea`; await this.code.waitForTypeInEditor(textArea, text); } /** * Gets tree items from Notebooks Tree View * @returns tree item from Notebooks View */ async getNotebookTreeItems(): Promise { return this.code.waitForElements(NotebookTreeView.notebookTreeItem, false); } /** * Gets tree items from Pinned Notebooks View * @returns tree item from Pinned Notebooks View */ async getPinnedNotebookTreeItems(): Promise { return this.code.waitForElements(NotebookTreeView.pinnedNotebooksSelector, false); } async pinNotebook(notebookId: string): Promise { await this.code.waitAndDoubleClick(`${NotebookTreeView.notebookTreeItem}[id="${notebookId}"]`); await this.code.waitAndClick(`${NotebookTreeView.notebookTreeItem}${NotebookTreeView.selectedItem} .codicon-pinned`); } async unpinNotebook(notebookId: string): Promise { await this.code.waitAndClick(NotebookTreeView.pinnedNotebooksSelector); await this.code.waitAndClick(`${NotebookTreeView.pinnedNotebooksSelector}[id="${notebookId}"] .actions a[title="Unpin Notebook"]`); } /** * When pinning a notebook, the pinned notebook view will show. */ async waitForPinnedNotebookTreeView(): Promise { await this.code.waitForElement(NotebookTreeView.pinnedNotebooksSelector); } async waitForPinnedNotebookTreeViewGone(): Promise { await this.code.waitForElementGone(NotebookTreeView.pinnedNotebooksSelector); } }