Pinning Notebooks on Notebooks view (#11963)

* initial commit

* added tests

* code cleanup and more tests

* add missed util test

* changes to address comments

* remove pin from resources
This commit is contained in:
Maddy
2020-08-31 08:53:11 -07:00
committed by GitHub
parent b4a3325a21
commit ae830d9e64
15 changed files with 506 additions and 26 deletions

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as constants from './../common/constants';
import { BookTreeItem } from './bookTreeItem';
import { getPinnedNotebooks } from '../common/utils';
export interface IBookPinManager {
pinNotebook(notebook: BookTreeItem): boolean;
unpinNotebook(notebook: BookTreeItem): boolean;
}
enum PinBookOperation {
Pin,
Unpin
}
export class BookPinManager implements IBookPinManager {
constructor() {
this.setPinnedSectionContext();
}
setPinnedSectionContext(): void {
if (getPinnedNotebooks().length > 0) {
vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, constants.showPinnedBooksContextKey, true);
} else {
vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, constants.showPinnedBooksContextKey, false);
}
}
isNotebookPinned(notebookPath: string): boolean {
if (getPinnedNotebooks().findIndex(x => x === notebookPath) > -1) {
return true;
}
return false;
}
pinNotebook(notebook: BookTreeItem): boolean {
return this.isNotebookPinned(notebook.book.contentPath) ? false : this.updatePinnedBooks(notebook, PinBookOperation.Pin);
}
unpinNotebook(notebook: BookTreeItem): boolean {
return this.updatePinnedBooks(notebook, PinBookOperation.Unpin);
}
getPinnedBookPathsInConfig(): string[] {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(constants.notebookConfigKey);
let pinnedBookDirectories: string[] = config.get(constants.pinnedBooksConfigKey);
return pinnedBookDirectories;
}
setPinnedBookPathsInConfig(bookPaths: string[]) {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(constants.notebookConfigKey);
let storeInWorspace: boolean = this.hasWorkspaceFolders();
config.update(constants.pinnedBooksConfigKey, bookPaths, storeInWorspace ? false : vscode.ConfigurationTarget.Global);
}
hasWorkspaceFolders(): boolean {
let workspaceFolders = vscode.workspace.workspaceFolders;
return workspaceFolders && workspaceFolders.length > 0;
}
updatePinnedBooks(notebook: BookTreeItem, operation: PinBookOperation) {
let modifiedPinnedBooks = false;
let bookPathToChange: string = notebook.book.contentPath;
let pinnedBooks: string[] = this.getPinnedBookPathsInConfig();
let existingBookIndex = pinnedBooks.map(pinnedBookPath => path.normalize(pinnedBookPath)).indexOf(bookPathToChange);
if (existingBookIndex !== -1 && operation === PinBookOperation.Unpin) {
pinnedBooks.splice(existingBookIndex, 1);
modifiedPinnedBooks = true;
} else if (existingBookIndex === -1 && operation === PinBookOperation.Pin) {
pinnedBooks.push(bookPathToChange);
modifiedPinnedBooks = true;
}
this.setPinnedBookPathsInConfig(pinnedBooks);
this.setPinnedSectionContext();
return modifiedPinnedBooks;
}
}

View File

@@ -8,6 +8,7 @@ import * as path from 'path';
import * as fs from 'fs';
import { IJupyterBookSection, IJupyterBookToc } from '../contracts/content';
import * as loc from '../common/localizedConstants';
import { isBookItemPinned } from '../common/utils';
export enum BookTreeItemType {
Book = 'Book',
@@ -54,6 +55,8 @@ export class BookTreeItem extends vscode.TreeItem {
} else {
this.contextValue = 'savedNotebook';
}
} else {
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook') : 'section';
}
this.setPageVariables();
this.setCommand();

View File

@@ -17,7 +17,8 @@ import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
import { isNullOrUndefined } from 'util';
import { debounce } from '../common/utils';
import { debounce, getPinnedNotebooks } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
const Content = 'content';
@@ -34,6 +35,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
private _initializeDeferred: Deferred<void> = new Deferred<void>();
private _openAsUntitled: boolean;
private _bookTrustManager: IBookTrustManager;
public bookPinManager: IBookPinManager;
private _bookViewer: vscode.TreeView<BookTreeItem>;
public viewId: string;
@@ -44,8 +46,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this._openAsUntitled = openAsUntitled;
this._extensionContext = extensionContext;
this.books = [];
this.initialize(workspaceFolders).catch(e => console.error(e));
this.bookPinManager = new BookPinManager();
this.viewId = view;
this.initialize(workspaceFolders).catch(e => console.error(e));
this.prompter = new CodeAdapter();
this._bookTrustManager = new BookTrustManager(this.books);
@@ -53,13 +56,24 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
private async initialize(workspaceFolders: vscode.WorkspaceFolder[]): Promise<void> {
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try {
await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
if (this.viewId === constants.PINNED_BOOKS_VIEWID) {
await Promise.all(getPinnedNotebooks().map(async (notebookPath) => {
try {
await this.createAndAddBookModel(notebookPath, true);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
} else {
await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try {
await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);
} catch {
// no-op, not all workspace folders are going to be valid books
}
}));
}
this._initializeDeferred.resolve();
}
@@ -97,6 +111,26 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
async pinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.pinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'pinnedNotebook';
}
}
}
async unpinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.unpinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'savedNotebook';
}
}
}
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
try {
// Convert path to posix style for easier comparisons
@@ -132,6 +166,20 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
}
}
async addNotebookToPinnedView(bookItem: BookTreeItem): Promise<void> {
let notebookPath: string = bookItem.book.contentPath;
if (notebookPath) {
await this.createAndAddBookModel(notebookPath, true);
}
}
async removeNotebookFromPinnedView(bookItem: BookTreeItem): Promise<void> {
let notebookPath: string = bookItem.book.contentPath;
if (notebookPath) {
await this.closeBook(bookItem);
}
}
@debounce(1500)
async fireBookRefresh(book: BookModel): Promise<void> {
await book.initializeContents().then(() => {
@@ -169,21 +217,23 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
* @param bookPath The path to the book folder to create the model for
*/
private async createAndAddBookModel(bookPath: string, isNotebook: boolean): Promise<void> {
const book: BookModel = new BookModel(bookPath, this._openAsUntitled, isNotebook, this._extensionContext);
await book.initializeContents();
this.books.push(book);
if (!this.currentBook) {
this.currentBook = book;
}
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
this._bookViewer.onDidChangeVisibility(e => {
let openDocument = azdata.nb.activeNotebookEditor;
let notebookPath = openDocument?.document.uri;
// call reveal only once on the correct view
if (e.visible && ((!this._openAsUntitled && notebookPath?.scheme !== 'untitled') || (this._openAsUntitled && notebookPath?.scheme === 'untitled'))) {
this.revealActiveDocumentInViewlet();
if (!this.books.find(x => x.bookPath === bookPath)) {
const book: BookModel = new BookModel(bookPath, this._openAsUntitled, isNotebook, this._extensionContext);
await book.initializeContents();
this.books.push(book);
if (!this.currentBook) {
this.currentBook = book;
}
});
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
this._bookViewer.onDidChangeVisibility(e => {
let openDocument = azdata.nb.activeNotebookEditor;
let notebookPath = openDocument?.document.uri;
// call reveal only once on the correct view
if (e.visible && ((!this._openAsUntitled && notebookPath?.scheme !== 'untitled') || (this._openAsUntitled && notebookPath?.scheme === 'untitled'))) {
this.revealActiveDocumentInViewlet();
}
});
}
}
async showPreviewFile(urlToOpen?: string): Promise<void> {

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { NotebookUtils } from './notebookUtils';
import { BookTreeViewProvider } from '../book/bookTreeView';
import { NavigationProviders, BOOKS_VIEWID, PROVIDED_BOOKS_VIEWID, extensionOutputChannelName } from './constants';
import { NavigationProviders, BOOKS_VIEWID, PROVIDED_BOOKS_VIEWID, PINNED_BOOKS_VIEWID, extensionOutputChannelName } from './constants';
/**
* Global context for the application
@@ -16,6 +16,7 @@ export class AppContext {
public readonly notebookUtils: NotebookUtils;
public readonly bookTreeViewProvider: BookTreeViewProvider;
public readonly providedBookTreeViewProvider: BookTreeViewProvider;
public readonly pinnedBookTreeViewProvider: BookTreeViewProvider;
public readonly outputChannel: vscode.OutputChannel;
constructor(public readonly extensionContext: vscode.ExtensionContext) {
@@ -24,6 +25,7 @@ export class AppContext {
let workspaceFolders = vscode.workspace.workspaceFolders?.slice() ?? [];
this.bookTreeViewProvider = new BookTreeViewProvider(workspaceFolders, extensionContext, false, BOOKS_VIEWID, NavigationProviders.NotebooksNavigator);
this.providedBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, true, PROVIDED_BOOKS_VIEWID, NavigationProviders.ProvidedBooksNavigator);
this.pinnedBookTreeViewProvider = new BookTreeViewProvider([], extensionContext, false, PINNED_BOOKS_VIEWID, NavigationProviders.NotebooksNavigator);
this.outputChannel = vscode.window.createOutputChannel(extensionOutputChannelName);
}
}

View File

@@ -19,6 +19,7 @@ export const pythonPathConfigKey = 'pythonPath';
export const existingPythonConfigKey = 'useExistingPython';
export const notebookConfigKey = 'notebook';
export const trustedBooksConfigKey = 'trustedBooks';
export const pinnedBooksConfigKey = 'pinnedNotebooks';
export const maxBookSearchDepth = 'maxBookSearchDepth';
export const remoteBookDownloadTimeout = 'remoteBookDownloadTimeout';
export const collapseBookItems = 'collapseBookItems';
@@ -45,10 +46,13 @@ export const sparkScalaDisplayName = 'Spark | Scala';
export const sparkRDisplayName = 'Spark | R';
export const powershellDisplayName = 'PowerShell';
export const allKernelsName = 'All Kernels';
export const BOOKS_VIEWID = 'bookTreeView';
export const PROVIDED_BOOKS_VIEWID = 'providedBooksView';
export const PINNED_BOOKS_VIEWID = 'pinnedBooksView';
export const visitedNotebooksMementoKey = 'notebooks.visited';
export const pinnedNotebooksMementoKey = 'notebooks.pinned';
export enum BuiltInCommands {
SetContext = 'setContext'
@@ -69,6 +73,7 @@ export enum NavigationProviders {
}
export const unsavedBooksContextKey = 'unsavedBooks';
export const showPinnedBooksContextKey = 'showPinnedbooks';
export const pythonWindowsInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2110625';
export const pythonMacInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2128152';

View File

@@ -25,6 +25,8 @@ export const msgBookTrusted = localize('msgBookTrusted', "Book is now trusted in
export const msgBookAlreadyTrusted = localize('msgBookAlreadyTrusted', "Book is already trusted in this workspace.");
export const msgBookUntrusted = localize('msgBookUntrusted', "Book is no longer trusted in this workspace");
export const msgBookAlreadyUntrusted = localize('msgBookAlreadyUntrusted', "Book is already untrusted in this workspace.");
export function msgBookPinned(book: string): string { return localize('msgBookPinned', "Book {0} is now pinned in the workspace.", book); }
export function msgBookUnpinned(book: string): string { return localize('msgBookUnpinned', "Book {0} is no longer pinned in this workspace", book); }
export const missingTocError = localize('bookInitializeFailed', "Failed to find a Table of Contents file in the specified book.");
export const noBooksSelectedError = localize('noBooksSelected', "No books are currently selected in the viewlet.");

View File

@@ -9,7 +9,7 @@ import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as crypto from 'crypto';
import { notebookLanguages } from './constants';
import { notebookLanguages, notebookConfigKey, pinnedBooksConfigKey } from './constants';
const localize = nls.loadMessageBundle();
@@ -323,3 +323,18 @@ export async function getRandomToken(size: number = 24): Promise<string> {
});
});
}
export function isBookItemPinned(notebookPath: string): boolean {
let pinnedNotebooks: string[] = getPinnedNotebooks();
if (pinnedNotebooks?.indexOf(notebookPath) > -1) {
return true;
}
return false;
}
export function getPinnedNotebooks(): string[] {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(notebookConfigKey);
let pinnedNotebooks: string[] = config.get(pinnedBooksConfigKey) ?? [];
return pinnedNotebooks;
}

View File

@@ -39,6 +39,15 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: any) => bookTreeViewProvider.closeBook(book)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (book: any) => bookTreeViewProvider.closeBook(book)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openNotebookFolder', (folderPath?: string, urlToOpen?: string, showPreview?: boolean,) => bookTreeViewProvider.openNotebookFolder(folderPath, urlToOpen, showPreview)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.pinNotebook', async (book: any) => {
await bookTreeViewProvider.pinNotebook(book);
await pinnedBookTreeViewProvider.addNotebookToPinnedView(book);
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.unpinNotebook', async (book: any) => {
await bookTreeViewProvider.unpinNotebook(book);
await pinnedBookTreeViewProvider.removeNotebookFromPinnedView(book);
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => {
let untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${createBookPath}`);
await vscode.workspace.openTextDocument(createBookPath).then((document) => {
@@ -128,6 +137,8 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
await bookTreeViewProvider.initialized;
const providedBookTreeViewProvider = appContext.providedBookTreeViewProvider;
await providedBookTreeViewProvider.initialized;
const pinnedBookTreeViewProvider = appContext.pinnedBookTreeViewProvider;
await pinnedBookTreeViewProvider.initialized;
azdata.nb.onDidChangeActiveNotebookEditor(e => {
if (e.document.uri.scheme === 'untitled') {

View File

@@ -124,6 +124,7 @@ describe('BooksTreeViewTests', function () {
should(appContext).not.be.undefined();
should(appContext.bookTreeViewProvider).not.be.undefined();
should(appContext.providedBookTreeViewProvider).not.be.undefined();
should(appContext.pinnedBookTreeViewProvider).not.be.undefined();
});
it('should initialize correctly with empty workspace array', async () => {
@@ -346,6 +347,67 @@ describe('BooksTreeViewTests', function () {
});
describe('pinnedBookTreeViewProvider', function (): void {
let pinnedTreeViewProvider: BookTreeViewProvider;
let bookTreeViewProvider: BookTreeViewProvider;
let bookItem: BookTreeItem;
this.beforeAll(async () => {
pinnedTreeViewProvider = appContext.pinnedBookTreeViewProvider;
bookTreeViewProvider = appContext.bookTreeViewProvider;
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
await Promise.race([pinnedTreeViewProvider.initialized, errorCase.then(() => { throw new Error('PinnedTreeViewProvider did not initialize in time'); })]);
await bookTreeViewProvider.openBook(bookFolderPath, undefined, false, false);
bookItem = bookTreeViewProvider.books[0].bookItems[0];
});
afterEach(function (): void {
sinon.restore();
});
it('pinnedBookTreeViewProvider should not have any books when there are no pinned notebooks', async function (): Promise<void> {
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(0, 'Pinned Notebooks view should not have any notebooks');
});
it('pinNotebook should add notebook to pinnedBookTreeViewProvider', async function (): Promise<void> {
await vscode.commands.executeCommand('notebook.command.pinNotebook', bookItem);
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(1, 'Pinned Notebooks view should have a notebook');
});
it('unpinNotebook should remove notebook from pinnedBookTreeViewProvider', async function (): Promise<void> {
await vscode.commands.executeCommand('notebook.command.unpinNotebook', pinnedTreeViewProvider.books[0].bookItems[0]);
const notebooks = pinnedTreeViewProvider.books;
should(notebooks.length).equal(0, 'Pinned Notebooks view should not have any notebooks');
});
it('pinNotebook should invoke bookPinManagers pinNotebook method', async function (): Promise<void> {
let pinBookSpy = sinon.spy(bookTreeViewProvider.bookPinManager, 'pinNotebook');
await bookTreeViewProvider.pinNotebook(bookItem);
should(pinBookSpy.calledOnce).be.true('Should invoke bookPinManagers pinNotebook to update pinnedNotebooks config');
});
it('unpinNotebook should invoke bookPinManagers unpinNotebook method', async function (): Promise<void> {
let unpinNotebookSpy = sinon.spy(bookTreeViewProvider.bookPinManager, 'unpinNotebook');
await bookTreeViewProvider.unpinNotebook(bookItem);
should(unpinNotebookSpy.calledOnce).be.true('Should invoke bookPinManagers unpinNotebook to update pinnedNotebooks config');
});
it('addNotebookToPinnedView should add notebook to the TreeViewProvider', async function (): Promise<void> {
let notebooks = pinnedTreeViewProvider.books.length;
await pinnedTreeViewProvider.addNotebookToPinnedView(bookItem);
should(pinnedTreeViewProvider.books.length).equal(notebooks + 1, 'Should add the notebook as new item to the TreeViewProvider');
});
it('removeNotebookFromPinnedView should remove notebook from the TreeViewProvider', async function (): Promise<void> {
let notebooks = pinnedTreeViewProvider.books.length;
await pinnedTreeViewProvider.removeNotebookFromPinnedView(pinnedTreeViewProvider.books[0].bookItems[0]);
should(pinnedTreeViewProvider.books.length).equal(notebooks - 1, 'Should remove the notebook from the TreeViewProvider');
});
});
this.afterAll(async function (): Promise<void> {
console.log('Removing temporary files...');
if (await exists(rootFolderPath)) {

View File

@@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import * as constants from '../../common/constants';
import { IBookPinManager, BookPinManager } from '../../book/bookPinManager';
import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem';
import * as vscode from 'vscode';
import { BookModel } from '../../book/bookModel';
import * as sinon from 'sinon';
import { isBookItemPinned } from '../../common/utils';
describe('BookPinManagerTests', function () {
describe('PinningNotebooks', () => {
let bookPinManager: IBookPinManager;
let pinnedNotebooks: string[];
let books: BookModel[];
afterEach(function (): void {
sinon.restore();
});
beforeEach(() => {
pinnedNotebooks = ['/temp/SubFolder/content/sample/notebook1.ipynb', '/temp/SubFolder/content/sample/notebook2.ipynb'];
// Mock Workspace Configuration
let workspaceConfigurtionMock: TypeMoq.IMock<vscode.WorkspaceConfiguration> = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
workspaceConfigurtionMock.setup(config => config.get(TypeMoq.It.isValue(constants.pinnedBooksConfigKey))).returns(() => [].concat(pinnedNotebooks));
workspaceConfigurtionMock.setup(config => config.update(TypeMoq.It.isValue(constants.pinnedBooksConfigKey), TypeMoq.It.isAny(), TypeMoq.It.isValue(false))).returns((key: string, newValues: string[]) => {
pinnedNotebooks.splice(0, pinnedNotebooks.length, ...newValues);
return Promise.resolve();
});
sinon.replaceGetter(vscode.workspace, 'workspaceFolders', () => {
return <vscode.WorkspaceFolder[]>[{
uri: {
fsPath: '/temp/'
},
},
{
uri: {
fsPath: '/temp2/'
}
},
];
});
sinon.stub(vscode.workspace, 'getConfiguration').returns(workspaceConfigurtionMock.object);
// Mock Book Data
let bookTreeItemFormat1: BookTreeItemFormat = {
contentPath: '/temp/SubFolder/content/sample/notebook1.ipynb',
root: '/temp/SubFolder/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook1')
},
{
url: path.join(path.sep, 'sample', 'notebook2')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookTreeItemFormat2: BookTreeItemFormat = {
contentPath: '/temp/SubFolder2/content/sample/notebook.ipynb',
root: '/temp/SubFolder2/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookTreeItemFormat3: BookTreeItemFormat = {
contentPath: '/temp2/SubFolder3/content/sample/notebook.ipynb',
root: '/temp2/SubFolder3/',
tableOfContents: {
sections: [
{
url: path.join(path.sep, 'sample', 'notebook')
}
]
},
isUntitled: undefined,
page: undefined,
title: undefined,
treeItemCollapsibleState: undefined,
type: BookTreeItemType.Book
};
let bookModel1Mock: TypeMoq.IMock<BookModel> = TypeMoq.Mock.ofType<BookModel>();
bookModel1Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat1, undefined), new BookTreeItem(bookTreeItemFormat2, undefined)]);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder', 'content', 'sample', 'notebook2.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp', 'SubFolder2', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel1Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
let bookModel2Mock: TypeMoq.IMock<BookModel> = TypeMoq.Mock.ofType<BookModel>();
bookModel2Mock.setup(model => model.bookItems).returns(() => [new BookTreeItem(bookTreeItemFormat3, undefined)]);
bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isValue(path.join(path.sep, 'temp2', 'SubFolder', 'content', 'sample', 'notebook.ipynb')))).returns((uri: string) => TypeMoq.Mock.ofType<BookTreeItem>().object);
bookModel2Mock.setup(model => model.getNotebook(TypeMoq.It.isAnyString())).returns((uri: string) => undefined);
books = [bookModel1Mock.object, bookModel2Mock.object];
bookPinManager = new BookPinManager();
});
it('should have notebooks in the pinnedBooksConfigKey when pinned within a workspace', async () => {
let notebookUri1 = books[0].bookItems[0].book.contentPath;
let isNotebook1Pinned = isBookItemPinned(notebookUri1);
should(isNotebook1Pinned).be.true('Notebook 1 should be pinned');
});
it('should NOT pin a notebook that is not pinned within a workspace', async () => {
let notebookUri = path.join(path.sep, 'temp', 'SubFolder2', 'content', 'sample', 'notebook.ipynb');
let isNotebookPinned = isBookItemPinned(notebookUri);
should(isNotebookPinned).be.false('Notebook should not be pinned');
});
it('should pin notebook after book has been pinned from viewlet within a workspace', async () => {
let notebookUri = books[0].bookItems[1].book.contentPath;
let isNotebookPinnedBeforeChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedBeforeChange).be.false('Notebook should NOT be pinned');
// mock pin book item from viewlet
bookPinManager.pinNotebook(books[0].bookItems[1]);
let isNotebookPinnedAfterChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedAfterChange).be.true('Notebook should be pinned');
});
it('should NOT pin a notebook when unpinned from viewlet within a workspace', async () => {
let notebookUri = books[0].bookItems[0].book.contentPath;
let isNotebookPinned = isBookItemPinned(notebookUri);
should(isNotebookPinned).be.true('Notebook should be pinned');
bookPinManager.unpinNotebook(books[0].bookItems[0]);
let isNotebookPinnedAfterChange = isBookItemPinned(notebookUri);
should(isNotebookPinnedAfterChange).be.false('Notebook should not be pinned after notebook is unpinned');
});
});
});

View File

@@ -327,4 +327,22 @@ describe('Utils Tests', function () {
}
});
});
describe('isBookItemPinned', function (): void {
it('Should NOT pin an unknown book within a workspace', async function (): Promise<void> {
let notebookUri = path.join(path.sep, 'randomfolder', 'randomsubfolder', 'content', 'randomnotebook.ipynb');
let isNotebookPinned = utils.isBookItemPinned(notebookUri);
should(isNotebookPinned).be.false('Random notebooks should not be pinned');
});
});
describe('getPinnedNotebooks', function (): void {
it('Should NOT have any pinned notebooks', async function (): Promise<void> {
let pinnedNotebooks: string[] = utils.getPinnedNotebooks();
should(pinnedNotebooks.length).equal(0, 'Should not have any pinned notebooks');
});
});
});