diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index fc5f48d130..5d5137f00a 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -28,6 +28,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider = new vscode.EventEmitter(); private _openAsUntitled: boolean; @@ -194,8 +196,12 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider Array.isArray(val.sections) ? acc.concat(val).concat(this.flattenArray(val.sections)) : acc.concat(val), []); + private flattenArray(array: any[], title: string): any[] { + try { + return array.reduce((acc, val) => Array.isArray(val.sections) ? acc.concat(val).concat(this.flattenArray(val.sections, title)) : acc.concat(val), []); + } catch (e) { + throw localize('Invalid toc.yml', 'Error: {0} has an incorrect toc.yml file', title); + } } public getBooks(): BookTreeItem[] { @@ -208,7 +214,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + let dataFolderPath = path.join(rootFolderPath, '_data'); + tableOfContentsFile = path.join(dataFolderPath, 'toc.yml'); + let tableOfContentsFileIgnore = path.join(rootFolderPath, 'toc.yml'); + await fs.mkdir(rootFolderPath); + await fs.mkdir(dataFolderPath); + await fs.writeFile(tableOfContentsFile, ''); + await fs.writeFile(tableOfContentsFileIgnore, ''); + let mockExtensionContext = TypeMoq.Mock.ofType(); + folder = { + uri: vscode.Uri.file(rootFolderPath), + name: '', + index: 0 + }; + bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object); + let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve())); + let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); + await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]); + }); + + it('should ignore toc.yml files not in _data folder', function(): void { + bookTreeViewProvider.getTableOfContentFiles([folder.uri.toString()]); + for (let p of bookTreeViewProvider.tableOfContentPaths) { + should(p.toLocaleLowerCase()).equal(tableOfContentsFile.replace(/\\/g, '/').toLocaleLowerCase()); + } + }); + + this.afterAll(async function () { + if (fs.existsSync(rootFolderPath)) { + rimraf.sync(rootFolderPath); + } + }); +}); + + +describe('BookTreeViewProvider.getBooks', function (): void { + let rootFolderPath: string; + let configFile: string; + let folder: vscode.WorkspaceFolder; + let bookTreeViewProvider: BookTreeViewProvider; + let mockExtensionContext: TypeMoq.IMock; + + this.beforeAll(async () => { + rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + let dataFolderPath = path.join(rootFolderPath, '_data'); + configFile = path.join(rootFolderPath, '_config.yml'); + let tableOfContentsFile = path.join(dataFolderPath, 'toc.yml'); + await fs.mkdir(rootFolderPath); + await fs.mkdir(dataFolderPath); + await fs.writeFile(tableOfContentsFile, 'title: Test'); + mockExtensionContext = TypeMoq.Mock.ofType(); + folder = { + uri: vscode.Uri.file(rootFolderPath), + name: '', + index: 0 + }; + bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object); + let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve())); + let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); + await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]); + }); + + it('should show error message if config.yml file not found', function(): void { + bookTreeViewProvider.getBooks(); + should(bookTreeViewProvider.errorMessage.toLocaleLowerCase()).equal(('ENOENT: no such file or directory, open \'' + configFile + '\'').toLocaleLowerCase()); + }); + it('should show error if toc.yml file format is invalid', async function(): Promise { + await fs.writeFile(configFile, 'title: Test Book'); + bookTreeViewProvider.getBooks(); + should(bookTreeViewProvider.errorMessage).equal('Error: Test Book has an incorrect toc.yml file'); + }); + + this.afterAll(async function () { + if (fs.existsSync(rootFolderPath)) { + rimraf.sync(rootFolderPath); + } + }); +}); + + +describe('BookTreeViewProvider.getSections', function (): void { + let rootFolderPath: string; + let tableOfContentsFile: string; + let bookTreeViewProvider: BookTreeViewProvider; + let folder: vscode.WorkspaceFolder; + let expectedNotebook2: ExpectedBookItem; + + this.beforeAll(async () => { + rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + let dataFolderPath = path.join(rootFolderPath, '_data'); + let contentFolderPath = path.join(rootFolderPath, 'content'); + let configFile = path.join(rootFolderPath, '_config.yml'); + tableOfContentsFile = path.join(dataFolderPath, 'toc.yml'); + let notebook2File = path.join(contentFolderPath, 'notebook2.ipynb'); + expectedNotebook2 = { + title: 'Notebook2', + url: '/notebook2', + previousUri: undefined, + nextUri: undefined + }; + await fs.mkdir(rootFolderPath); + await fs.mkdir(dataFolderPath); + await fs.mkdir(contentFolderPath); + await fs.writeFile(configFile, 'title: Test Book'); + await fs.writeFile(tableOfContentsFile, '- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'); + await fs.writeFile(notebook2File, ''); + + let mockExtensionContext = TypeMoq.Mock.ofType(); + folder = { + uri: vscode.Uri.file(rootFolderPath), + name: '', + index: 0 + }; + bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext.object); + let tocRead = new Promise((resolve, reject) => bookTreeViewProvider.onReadAllTOCFiles(() => resolve())); + let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000)); + await Promise.race([tocRead, errorCase.then(() => { throw new Error('Table of Contents were not ready in time'); })]); + }); + + it('should show error if notebook or markdown file is missing', function(): void { + let books = bookTreeViewProvider.getBooks(); + let children = bookTreeViewProvider.getSections([], books[0].sections, rootFolderPath); + should(bookTreeViewProvider.errorMessage).equal('Missing file : Notebook1'); + // Rest of book should be detected correctly even with a missing file + equalBookItems(children[0], expectedNotebook2); + }); + + this.afterAll(async function () { + if (fs.existsSync(rootFolderPath)) { + rimraf.sync(rootFolderPath); + } + }); +});