Browse Source

modernize filebrowser tests

Update filebrowser

update modernize

update modernize

update modernize

update modernize

wip modernize filebrowser tests

wip modernize filebrowser tests

wip modernize filebrowser

wip modernize file browser

wip modernize file browser tests

wip filebrowser test modernization

finish
Steven Silvester 5 years ago
parent
commit
46ec0496d4

+ 2 - 2
lint-staged.config.js

@@ -13,7 +13,7 @@ module.exports = {
     const escapedFileNames = escapeFileNames(filenames);
     return [
       `prettier --write ${escapedFileNames}`,
-      `git add ${escapedFileNames}`
+      `git add -f ${escapedFileNames}`
     ];
   },
   '**/*{.ts,.tsx,.js,.jsx}': filenames => {
@@ -21,7 +21,7 @@ module.exports = {
     return [
       `prettier --write ${escapedFileNames}`,
       `eslint --fix ${escapedFileNames}`,
-      `git add ${escapedFileNames}`
+      `git add -f ${escapedFileNames}`
     ];
   }
 };

+ 1 - 0
packages/console/package.json

@@ -36,6 +36,7 @@
     "test": "jest",
     "test:cov": "jest --collect-coverage",
     "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
     "test:watch": "npm run test -- --watch",
     "watch": "tsc -b --watch"
   },

+ 1 - 0
packages/docmanager/package.json

@@ -36,6 +36,7 @@
     "test": "jest",
     "test:cov": "jest --collect-coverage",
     "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
     "watch": "npm run test -- --watch"
   },
   "dependencies": {

+ 11 - 0
packages/filebrowser/.vscode/launch.json

@@ -0,0 +1,11 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "node",
+            "request": "attach",
+            "name": "Attach",
+            "port": 9229
+        }
+    ]
+}

+ 1 - 0
packages/filebrowser/babel.config.js

@@ -0,0 +1 @@
+module.exports = require('@jupyterlab/testutils/lib/babel.config');

+ 2 - 0
packages/filebrowser/jest.config.js

@@ -0,0 +1,2 @@
+const func = require('@jupyterlab/testutils/lib/jest-config-new');
+module.exports = func(__dirname);

+ 9 - 0
packages/filebrowser/package.json

@@ -29,9 +29,14 @@
   },
   "scripts": {
     "build": "tsc -b",
+    "build:test": "tsc --build tsconfig.test.json",
     "clean": "rimraf lib",
     "docs": "typedoc src",
     "prepublishOnly": "npm run build",
+    "test": "jest",
+    "test:cov": "jest --collect-coverage",
+    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
     "watch": "tsc -b --watch"
   },
   "dependencies": {
@@ -55,7 +60,11 @@
     "react": "~16.9.0"
   },
   "devDependencies": {
+    "@jupyterlab/testutils": "^2.1.0",
+    "@types/jest": "^24.0.23",
+    "jest": "^25.2.3",
     "rimraf": "~3.0.0",
+    "ts-jest": "^25.2.1",
     "typedoc": "^0.15.4",
     "typescript": "~3.7.3"
   },

+ 15 - 30
packages/filebrowser/src/listing.ts

@@ -560,11 +560,7 @@ export class DirListing extends Widget {
    */
   modelForClick(event: MouseEvent): Contents.IModel | undefined {
     const items = this._sortedItems;
-    const index = Private.hitTestNodes(
-      this._items,
-      event.clientX,
-      event.clientY
-    );
+    const index = Private.hitTestNodes(this._items, event);
     if (index !== -1) {
       return items[index];
     }
@@ -855,14 +851,12 @@ export class DirListing extends Widget {
       }
     }
 
-    const index = Private.hitTestNodes(
-      this._items,
-      event.clientX,
-      event.clientY
-    );
+    let index = Private.hitTestNodes(this._items, event);
+
     if (index === -1) {
       return;
     }
+
     this._handleFileSelect(event);
 
     if (event.button !== 0) {
@@ -1058,11 +1052,7 @@ export class DirListing extends Widget {
    */
   private _evtDragEnter(event: IDragEvent): void {
     if (event.mimeData.hasData(CONTENTS_MIME)) {
-      const index = Private.hitTestNodes(
-        this._items,
-        event.clientX,
-        event.clientY
-      );
+      const index = Private.hitTestNodes(this._items, event);
       if (index === -1) {
         return;
       }
@@ -1100,11 +1090,7 @@ export class DirListing extends Widget {
     if (dropTarget) {
       dropTarget.classList.remove(DROP_TARGET_CLASS);
     }
-    const index = Private.hitTestNodes(
-      this._items,
-      event.clientX,
-      event.clientY
-    );
+    const index = Private.hitTestNodes(this._items, event);
     this._items[index].classList.add(DROP_TARGET_CLASS);
   }
 
@@ -1284,11 +1270,7 @@ export class DirListing extends Widget {
   private _handleFileSelect(event: MouseEvent): void {
     // Fetch common variables.
     const items = this._sortedItems;
-    const index = Private.hitTestNodes(
-      this._items,
-      event.clientX,
-      event.clientY
-    );
+    const index = Private.hitTestNodes(this._items, event);
 
     clearTimeout(this._selectTimer);
 
@@ -1847,7 +1829,7 @@ export namespace DirListing {
       model: Contents.IModel,
       fileType: DocumentRegistry.IFileType = DocumentRegistry.defaultTextFileType
     ): void {
-      const { icon, iconClass } = fileType;
+      const { icon, iconClass, name } = fileType;
 
       const iconContainer = DOMUtils.findElement(node, ITEM_ICON_CLASS);
       const text = DOMUtils.findElement(node, ITEM_TEXT_CLASS);
@@ -1888,6 +1870,7 @@ export namespace DirListing {
       }
 
       node.title = hoverText;
+      node.setAttribute('data-file-type', name);
 
       // If an item is being edited currently, its text node is unavailable.
       if (text && text.textContent !== model.name) {
@@ -2074,11 +2057,13 @@ namespace Private {
    */
   export function hitTestNodes(
     nodes: HTMLElement[],
-    x: number,
-    y: number
+    event: MouseEvent
   ): number {
-    return ArrayExt.findFirstIndex(nodes, node =>
-      ElementExt.hitTest(node, x, y)
+    return ArrayExt.findFirstIndex(
+      nodes,
+      node =>
+        ElementExt.hitTest(node, event.clientX, event.clientY) ||
+        event.target === node
     );
   }
 

+ 4 - 1
packages/filebrowser/src/model.ts

@@ -734,9 +734,12 @@ namespace Private {
     root: string,
     path: string
   ): string {
+    if (path === '/') {
+      return '';
+    }
     const driveName = contents.driveName(root);
     const localPath = contents.localPath(root);
-    const resolved = PathExt.resolve(localPath, path);
+    let resolved = PathExt.normalize(PathExt.join(localPath, path));
     return driveName ? `${driveName}:${resolved}` : resolved;
   }
 }

+ 35 - 25
tests/test-filebrowser/src/crumbs.spec.ts → packages/filebrowser/test/crumbs.spec.ts

@@ -1,18 +1,22 @@
+import 'jest';
+
+import expect from 'expect';
+
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { expect } from 'chai';
-
 import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
 
 import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
 
-import { BreadCrumbs, FileBrowserModel } from '@jupyterlab/filebrowser';
+import { BreadCrumbs, FileBrowserModel } from '../src';
 
 import { ServiceManager } from '@jupyterlab/services';
 
 import { framePromise, signalToPromise } from '@jupyterlab/testutils';
 
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
 import { Message, MessageLoop } from '@lumino/messaging';
 
 import { Widget } from '@lumino/widgets';
@@ -59,7 +63,7 @@ describe('filebrowser/model', () => {
   let third: string;
   let path: string;
 
-  before(async () => {
+  beforeAll(async () => {
     const opener: DocumentManager.IWidgetOpener = {
       open: widget => {
         /* no op */
@@ -69,7 +73,7 @@ describe('filebrowser/model', () => {
     registry = new DocumentRegistry({
       textModelFactory: new TextModelFactory()
     });
-    serviceManager = new ServiceManager({ standby: 'never' });
+    serviceManager = new Mock.ServiceManagerMock();
     manager = new DocumentManager({
       registry,
       opener,
@@ -106,30 +110,30 @@ describe('filebrowser/model', () => {
     describe('#constructor()', () => {
       it('should create a new BreadCrumbs instance', () => {
         const bread = new BreadCrumbs({ model });
-        expect(bread).to.be.an.instanceof(BreadCrumbs);
+        expect(bread).toBeInstanceOf(BreadCrumbs);
         const items = crumbs.node.querySelectorAll(ITEM_QUERY);
-        expect(items.length).to.equal(1);
+        expect(items.length).toBe(1);
       });
 
       it('should add the jp-BreadCrumbs class', () => {
-        expect(crumbs.hasClass('jp-BreadCrumbs')).to.equal(true);
+        expect(crumbs.hasClass('jp-BreadCrumbs')).toBe(true);
       });
     });
 
     describe('#handleEvent()', () => {
-      context('click', () => {
+      describe('click', () => {
         it('should switch to the parent directory', async () => {
           Widget.attach(crumbs, document.body);
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           let items = crumbs.node.querySelectorAll(ITEM_QUERY);
-          expect(items.length).to.equal(4);
+          expect(items.length).toBe(4);
           const promise = signalToPromise(model.pathChanged);
-          expect(items[2].textContent).to.equal(second);
+          expect(items[2].textContent).toBe(second);
           simulate(items[2], 'click');
           await promise;
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           items = crumbs.node.querySelectorAll(ITEM_QUERY);
-          expect(items.length).to.equal(3);
+          expect(items.length).toBe(3);
         });
 
         it('should switch to the home directory', async () => {
@@ -141,8 +145,8 @@ describe('filebrowser/model', () => {
           await promise;
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           items = crumbs.node.querySelectorAll(ITEM_QUERY);
-          expect(items.length).to.equal(1);
-          expect(model.path).to.equal('');
+          expect(items.length).toBe(1);
+          expect(model.path).toBe('');
         });
 
         it('should switch to the grandparent directory', async () => {
@@ -154,8 +158,8 @@ describe('filebrowser/model', () => {
           await promise;
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           items = crumbs.node.querySelectorAll(ITEM_QUERY);
-          expect(items.length).to.equal(2);
-          expect(model.path).to.equal(first);
+          expect(items.length).toBe(2);
+          expect(model.path).toBe(first);
         });
 
         it('should refresh the current directory', async () => {
@@ -163,13 +167,13 @@ describe('filebrowser/model', () => {
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           let items = crumbs.node.querySelectorAll(ITEM_QUERY);
           const promise = signalToPromise(model.refreshed);
-          expect(items[3].textContent).to.equal(third);
+          expect(items[3].textContent).toBe(third);
           simulate(items[3], 'click');
           await promise;
           MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest);
           items = crumbs.node.querySelectorAll(ITEM_QUERY);
-          expect(items.length).to.equal(4);
-          expect(model.path).to.equal(path);
+          expect(items.length).toBe(4);
+          expect(model.path).toBe(path);
         });
       });
     });
@@ -177,15 +181,19 @@ describe('filebrowser/model', () => {
     describe('#onAfterAttach()', () => {
       it('should post an update request', async () => {
         Widget.attach(crumbs, document.body);
-        expect(crumbs.methods).to.contain('onAfterAttach');
+        expect(crumbs.methods).toEqual(
+          expect.arrayContaining(['onAfterAttach'])
+        );
         await framePromise();
-        expect(crumbs.methods).to.contain('onUpdateRequest');
+        expect(crumbs.methods).toEqual(
+          expect.arrayContaining(['onUpdateRequest'])
+        );
       });
 
       it('should add event listeners', () => {
         Widget.attach(crumbs, document.body);
         simulate(crumbs.node, 'click');
-        expect(crumbs.events).to.contain('click');
+        expect(crumbs.events).toEqual(expect.arrayContaining(['click']));
       });
     });
 
@@ -194,7 +202,7 @@ describe('filebrowser/model', () => {
         Widget.attach(crumbs, document.body);
         Widget.detach(crumbs);
         simulate(crumbs.node, 'click');
-        expect(crumbs.events).to.not.contain('click');
+        expect(crumbs.events).not.toEqual(expect.arrayContaining(['click']));
       });
     });
 
@@ -206,9 +214,11 @@ describe('filebrowser/model', () => {
         await model.cd('..');
         await framePromise();
 
-        expect(crumbs.methods).to.contain('onUpdateRequest');
+        expect(crumbs.methods).toEqual(
+          expect.arrayContaining(['onUpdateRequest'])
+        );
         const items = crumbs.node.querySelectorAll(ITEM_QUERY);
-        expect(items.length).to.equal(3);
+        expect(items.length).toBe(3);
         model.dispose();
       });
     });

+ 506 - 0
packages/filebrowser/test/model.spec.ts

@@ -0,0 +1,506 @@
+import 'jest';
+
+import expect from 'expect';
+
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+//import { PageConfig } from '@jupyterlab/coreutils';
+
+import { UUID } from '@lumino/coreutils';
+
+import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
+
+import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
+
+import { StateDB } from '@jupyterlab/statedb';
+
+import { FileBrowserModel } from '../src';
+
+import { Contents, ServiceManager } from '@jupyterlab/services';
+
+import {
+  acceptDialog,
+  dismissDialog,
+  //signalToPromises,
+  sleep
+} from '@jupyterlab/testutils';
+
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
+//import { toArray } from '@lumino/algorithm';
+
+/**
+ * A contents manager that delays requests by less each time it is called
+ * in order to simulate out-of-order responses from the server.
+ */
+class DelayedContentsManager extends Mock.ContentsManagerMock {
+  get(
+    path: string,
+    options?: Contents.IFetchOptions
+  ): Promise<Contents.IModel> {
+    return new Promise<Contents.IModel>(resolve => {
+      const delay = this._delay;
+      this._delay -= 500;
+      void super.get(path, options).then(contents => {
+        setTimeout(() => {
+          resolve(contents);
+        }, Math.max(delay, 0));
+      });
+    });
+  }
+
+  private _delay = 1000;
+}
+
+describe('filebrowser/model', () => {
+  let manager: IDocumentManager;
+  let serviceManager: ServiceManager.IManager;
+  let registry: DocumentRegistry;
+  let model: FileBrowserModel;
+  let name: string;
+  let subDir: string;
+  let state: StateDB;
+  const opener: DocumentManager.IWidgetOpener = {
+    open: widget => {
+      /* no op */
+    }
+  };
+
+  beforeAll(() => {
+    registry = new DocumentRegistry({
+      textModelFactory: new TextModelFactory()
+    });
+    serviceManager = new Mock.ServiceManagerMock();
+    manager = new DocumentManager({
+      registry,
+      opener,
+      manager: serviceManager
+    });
+    state = new StateDB();
+  });
+
+  beforeEach(async () => {
+    await state.clear();
+    model = new FileBrowserModel({ manager, state });
+    let contents = await manager.newUntitled({ type: 'file' });
+    name = contents.name;
+    contents = await manager.newUntitled({ type: 'directory' });
+    subDir = contents.path;
+    return model.cd();
+  });
+
+  afterEach(() => {
+    model.dispose();
+  });
+
+  describe('FileBrowserModel', () => {
+    describe('#constructor()', () => {
+      it('should construct a new file browser model', () => {
+        model = new FileBrowserModel({ manager });
+        expect(model).toBeInstanceOf(FileBrowserModel);
+      });
+    });
+
+    describe('#pathChanged', () => {
+      it('should be emitted when the path changes', async () => {
+        let called = false;
+        model.pathChanged.connect((sender, args) => {
+          expect(sender).toBe(model);
+          expect(args.name).toBe('path');
+          expect(args.oldValue).toBe('');
+          expect(args.newValue).toBe(subDir);
+          called = true;
+        });
+        await model.cd(subDir);
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('#refreshed', () => {
+      it('should be emitted after a refresh', async () => {
+        let called = false;
+        model.refreshed.connect((sender, arg) => {
+          expect(sender).toBe(model);
+          expect(arg).toBeUndefined();
+          called = true;
+        });
+        await model.cd();
+        expect(called).toBe(true);
+      });
+
+      it('should be emitted when the path changes', async () => {
+        let called = false;
+        model.refreshed.connect((sender, arg) => {
+          expect(sender).toBe(model);
+          expect(arg).toBeUndefined();
+          called = true;
+        });
+        await model.cd(subDir);
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('#fileChanged', () => {
+      it('should be emitted when a file is created', async () => {
+        let called = false;
+        model.fileChanged.connect((sender, args) => {
+          expect(sender).toBe(model);
+          expect(args.type).toBe('new');
+          expect(args.oldValue).toBeNull();
+          expect(args.newValue!.type).toBe('file');
+          called = true;
+        });
+        await manager.newUntitled({ type: 'file' });
+        expect(called).toBe(true);
+      });
+
+      it('should be emitted when a file is renamed', async () => {
+        let called = false;
+        model.fileChanged.connect((sender, args) => {
+          expect(sender).toBe(model);
+          expect(args.type).toBe('rename');
+          expect(args.oldValue!.path).toBe(name);
+          expect(args.newValue!.path).toBe(name + '.bak');
+          called = true;
+        });
+        await manager.rename(name, name + '.bak');
+        expect(called).toBe(true);
+      });
+
+      it('should be emitted when a file is deleted', async () => {
+        let called = false;
+        model.fileChanged.connect((sender, args) => {
+          expect(sender).toBe(model);
+          expect(args.type).toBe('delete');
+          expect(args.oldValue!.path).toBe(name);
+          expect(args.newValue).toBeNull();
+          called = true;
+        });
+        await manager.deleteFile(name);
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('#path', () => {
+      it('should be the current path of the model', async () => {
+        expect(model.path).toBe('');
+        await model.cd(subDir);
+        expect(model.path).toBe(subDir);
+      });
+    });
+
+    describe('#items()', () => {
+      it('should get an iterator of items in the current path', () => {
+        const items = model.items();
+        expect(items.next()).toBeTruthy();
+      });
+    });
+
+    describe('#isDisposed', () => {
+      it('should test whether the model is disposed', () => {
+        expect(model.isDisposed).toBe(false);
+        model.dispose();
+        expect(model.isDisposed).toBe(true);
+      });
+    });
+
+    describe('#sessions()', () => {
+      it('should be the session models for the active notebooks', async () => {
+        const contents = await manager.newUntitled({ type: 'notebook' });
+        const session = await serviceManager.sessions.startNew({
+          name: '',
+          path: contents.path,
+          type: 'test'
+        });
+        await model.cd();
+        expect(model.sessions().next()).toBeTruthy();
+        await session.shutdown();
+      });
+    });
+
+    describe('#dispose()', () => {
+      it('should dispose of the resources held by the model', () => {
+        model.dispose();
+        expect(model.isDisposed).toBe(true);
+      });
+
+      it('should be safe to call more than once', () => {
+        model.dispose();
+        model.dispose();
+        expect(model.isDisposed).toBe(true);
+      });
+    });
+
+    describe('#refresh()', () => {
+      it('should refresh the contents', () => {
+        return model.refresh();
+      });
+    });
+
+    describe('#cd()', () => {
+      it('should change directory', async () => {
+        await model.cd(subDir);
+        expect(model.path).toBe(subDir);
+      });
+
+      it('should accept a relative path', async () => {
+        await model.cd(subDir);
+        expect(model.path).toBe(subDir);
+      });
+
+      it('should accept a parent directory', async () => {
+        await model.cd(subDir);
+        await model.cd('..');
+        expect(model.path).toBe('');
+      });
+
+      it('should be resilient to a slow initial fetch', async () => {
+        const delayedServiceManager = new Mock.ServiceManagerMock();
+        (delayedServiceManager as any).contents = new DelayedContentsManager();
+        const contents = await delayedServiceManager.contents.newUntitled({
+          type: 'directory'
+        });
+        subDir = contents.path;
+
+        const manager = new DocumentManager({
+          registry,
+          opener,
+          manager: delayedServiceManager
+        });
+        model = new FileBrowserModel({ manager, state }); // Should delay 1000ms
+
+        // An initial refresh is called in the constructor.
+        // If it is too slow, it can come in after the directory change,
+        // causing a directory set by, e.g., the tree handler to be wrong.
+        // This checks to make sure we are handling that case correctly.
+        await model.cd(subDir); // should delay 500ms
+        await sleep(2000);
+        expect(model.path).toBe(subDir);
+
+        manager.dispose();
+        delayedServiceManager.contents.dispose();
+        delayedServiceManager.dispose();
+        model.dispose();
+      });
+    });
+
+    describe('#restore()', () => {
+      it('should restore based on ID', async () => {
+        const id = 'foo';
+        const model2 = new FileBrowserModel({ manager, state });
+        await model.restore(id);
+        await model.cd(subDir);
+        expect(model.path).toBe(subDir);
+        expect(model2.path).toBe('');
+        await model2.restore(id);
+        expect(model2.path).toBe(subDir);
+        model2.dispose();
+      });
+
+      it('should be safe to call multiple times', async () => {
+        const id = 'bar';
+        const model2 = new FileBrowserModel({ manager, state });
+        await model.restore(id);
+        await model.cd(subDir);
+        expect(model.path).toBe(subDir);
+        expect(model2.path).toBe('');
+        await model2.restore(id);
+        await model2.restore(id);
+        expect(model2.path).toBe(subDir);
+        model2.dispose();
+      });
+    });
+
+    describe('#download()', () => {
+      it('should download the file without error', () => {
+        // TODO: how to test this?
+      });
+    });
+
+    describe('#upload()', () => {
+      it('should upload a file object', async () => {
+        const fname = UUID.uuid4() + '.html';
+        const file = new File(['<p>Hello world!</p>'], fname, {
+          type: 'text/html'
+        });
+        const contents = await model.upload(file);
+        expect(contents.name).toBe(fname);
+      });
+
+      it('should overwrite', async () => {
+        const fname = UUID.uuid4() + '.html';
+        const file = new File(['<p>Hello world!</p>'], fname, {
+          type: 'text/html'
+        });
+        const contents = await model.upload(file);
+        expect(contents.name).toBe(fname);
+        const promise = model.upload(file);
+        await acceptDialog();
+        await promise;
+        expect(contents.name).toBe(fname);
+      });
+
+      it('should not overwrite', async () => {
+        const fname = UUID.uuid4() + '.html';
+        const file = new File(['<p>Hello world!</p>'], fname, {
+          type: 'text/html'
+        });
+        const contents = await model.upload(file);
+        expect(contents.name).toBe(fname);
+        const promise = model.upload(file);
+        await dismissDialog();
+        try {
+          await promise;
+        } catch (e) {
+          expect(e).toBe('File not uploaded');
+        }
+      });
+
+      it('should emit the fileChanged signal', async () => {
+        const fname = UUID.uuid4() + '.html';
+        let called = false;
+        model.fileChanged.connect((sender, args) => {
+          expect(sender).toBe(model);
+          expect(args.type).toBe('save');
+          expect(args.oldValue).toBeNull();
+          expect(args.newValue!.path).toBe(fname);
+          called = true;
+        });
+        const file = new File(['<p>Hello world!</p>'], fname, {
+          type: 'text/html'
+        });
+        await model.upload(file);
+        expect(called).toBe(true);
+      });
+
+      //   describe('older notebook version', () => {
+      //     let prevNotebookVersion: string;
+
+      //     beforeAll(() => {
+      //       prevNotebookVersion = PageConfig.setOption(
+      //         'notebookVersion',
+      //         JSON.stringify([5, 0, 0])
+      //       );
+      //     });
+
+      //     it('should not upload large file', async () => {
+      //       const fname = UUID.uuid4() + '.html';
+      //       const file = new File([new ArrayBuffer(LARGE_FILE_SIZE + 1)], fname);
+      //       try {
+      //         await model.upload(file);
+      //         throw new Error('Upload should have failed');
+      //       } catch (err) {
+      //         expect(err).toBe(`Cannot upload file (>15 MB). ${fname}`);
+      //       }
+      //     });
+
+      //     afterAll(() => {
+      //       PageConfig.setOption('notebookVersion', prevNotebookVersion);
+      //     });
+      //   });
+
+      //   describe('newer notebook version', () => {
+      //     let prevNotebookVersion: string;
+
+      //     beforeAll(() => {
+      //       prevNotebookVersion = PageConfig.setOption(
+      //         'notebookVersion',
+      //         JSON.stringify([5, 1, 0])
+      //       );
+      //     });
+
+      //     for (const ending of ['.txt', '.ipynb']) {
+      //       for (const size of [
+      //         CHUNK_SIZE - 1,
+      //         CHUNK_SIZE,
+      //         CHUNK_SIZE + 1,
+      //         2 * CHUNK_SIZE
+      //       ]) {
+      //         it(`should upload a large ${ending} file of size ${size}`, async () => {
+      //           const fname = UUID.uuid4() + ending;
+      //           // minimal valid (according to server) notebook
+      //           let content =
+      //             '{"nbformat": 4, "metadata": {"_": ""}, "nbformat_minor": 2, "cells": []}';
+      //           // make metadata longer so that total document is `size` long
+      //           content = content.replace(
+      //             '"_": ""',
+      //             `"_": "${' '.repeat(size - content.length)}"`
+      //           );
+      //           const file = new File([content], fname, { type: 'text/plain' });
+      //           await model.upload(file);
+      //           const {
+      //             content: newContent
+      //           } = await model.manager.services.contents.get(fname);
+      //           // the contents of notebooks are returned as objects instead of strings
+      //           if (ending === '.ipynb') {
+      //             expect(newContent).toEqual(JSON.parse(content));
+      //           } else {
+      //             expect(newContent).toBe(content);
+      //           }
+      //         });
+      //       }
+      //     }
+      //     it(`should produce progress as a large file uploads`, async () => {
+      //       const fname = UUID.uuid4() + '.txt';
+      //       const file = new File([new ArrayBuffer(2 * CHUNK_SIZE)], fname);
+
+      //       const [start, first, second, finished] = signalToPromises(
+      //         model.uploadChanged,
+      //         4
+      //       );
+
+      //       const uploaded = model.upload(file);
+      //       expect(toArray(model.uploads())).toEqual([]);
+      //       expect(await start).toEqual([
+      //         model,
+      //         {
+      //           name: 'start',
+      //           oldValue: null,
+      //           newValue: { path: fname, progress: 0 }
+      //         }
+      //       ]);
+      //       expect(toArray(model.uploads())).toEqual([
+      //         { path: fname, progress: 0 }
+      //       ]);
+      //       expect(await first).toEqual([
+      //         model,
+      //         {
+      //           name: 'update',
+      //           oldValue: { path: fname, progress: 0 },
+      //           newValue: { path: fname, progress: 0 }
+      //         }
+      //       ]);
+      //       expect(toArray(model.uploads())).toEqual([
+      //         { path: fname, progress: 0 }
+      //       ]);
+      //       expect(await second).toEqual([
+      //         model,
+      //         {
+      //           name: 'update',
+      //           oldValue: { path: fname, progress: 0 },
+      //           newValue: { path: fname, progress: 1 / 2 }
+      //         }
+      //       ]);
+      //       expect(toArray(model.uploads())).toEqual([
+      //         { path: fname, progress: 1 / 2 }
+      //       ]);
+      //       expect(await finished).toEqual([
+      //         model,
+      //         {
+      //           name: 'finish',
+      //           oldValue: { path: fname, progress: 1 / 2 },
+      //           newValue: null
+      //         }
+      //       ]);
+      //       expect(toArray(model.uploads())).toEqual([]);
+      //       await uploaded;
+      //     });
+
+      //     afterAll(() => {
+      //       PageConfig.setOption('notebookVersion', prevNotebookVersion);
+      //     });
+      //   });
+    });
+  });
+});

+ 46 - 54
tests/test-filebrowser/src/openfiledialog.spec.ts → packages/filebrowser/test/openfiledialog.spec.ts

@@ -1,18 +1,16 @@
+import 'jest';
+
+import expect from 'expect';
+
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
 import { toArray } from '@lumino/algorithm';
-
 import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
 import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
-import {
-  FileDialog,
-  FilterFileBrowserModel,
-  FileBrowserModel
-} from '@jupyterlab/filebrowser';
-import { ServiceManager, Contents } from '@jupyterlab/services';
+import { FileDialog, FilterFileBrowserModel, FileBrowserModel } from '../src';
 
-import { expect } from 'chai';
+import { ServiceManager, Contents } from '@jupyterlab/services';
 import {
   acceptDialog,
   dismissDialog,
@@ -20,6 +18,9 @@ import {
   sleep,
   framePromise
 } from '@jupyterlab/testutils';
+
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
 import { simulate } from 'simulate-event';
 
 describe('@jupyterlab/filebrowser', () => {
@@ -27,7 +28,7 @@ describe('@jupyterlab/filebrowser', () => {
   let serviceManager: ServiceManager.IManager;
   let registry: DocumentRegistry;
 
-  before(async () => {
+  beforeAll(async () => {
     const opener: DocumentManager.IWidgetOpener = {
       open: widget => {
         /* no op */
@@ -37,7 +38,7 @@ describe('@jupyterlab/filebrowser', () => {
     registry = new DocumentRegistry({
       textModelFactory: new TextModelFactory()
     });
-    serviceManager = new ServiceManager({ standby: 'never' });
+    serviceManager = new Mock.ServiceManagerMock();
     manager = new DocumentManager({
       registry,
       opener,
@@ -54,7 +55,7 @@ describe('@jupyterlab/filebrowser', () => {
     describe('#constructor()', () => {
       it('should construct a new filtered file browser model', () => {
         const model = new FilterFileBrowserModel({ manager });
-        expect(model).to.be.an.instanceof(FilterFileBrowserModel);
+        expect(model).toBeInstanceOf(FilterFileBrowserModel);
       });
 
       it('should accept filter option', () => {
@@ -62,7 +63,7 @@ describe('@jupyterlab/filebrowser', () => {
           manager,
           filter: (model: Contents.IModel) => false
         });
-        expect(model).to.be.an.instanceof(FilterFileBrowserModel);
+        expect(model).toBeInstanceOf(FilterFileBrowserModel);
       });
     });
 
@@ -77,7 +78,7 @@ describe('@jupyterlab/filebrowser', () => {
 
         const filteredItems = toArray(filteredModel.items());
         const items = toArray(model.items());
-        expect(filteredItems.length).equal(items.length);
+        expect(filteredItems.length).toBe(items.length);
       });
 
       it('should list all directories whatever the filter', async () => {
@@ -92,7 +93,7 @@ describe('@jupyterlab/filebrowser', () => {
         const filteredItems = toArray(filteredModel.items());
         const items = toArray(model.items());
         const folders = items.filter(item => item.type === 'directory');
-        expect(filteredItems.length).equal(folders.length);
+        expect(filteredItems.length).toBe(folders.length);
       });
 
       it('should respect the filter', async () => {
@@ -111,11 +112,11 @@ describe('@jupyterlab/filebrowser', () => {
         const shownItems = items.filter(
           item => item.type === 'directory' || item.type === 'notebook'
         );
-        expect(filteredItems.length).equal(shownItems.length);
+        expect(filteredItems.length).toBe(shownItems.length);
         const notebooks = filteredItems.filter(
           item => item.type === 'notebook'
         );
-        expect(notebooks.length).to.be.greaterThan(0);
+        expect(notebooks.length).toBeGreaterThan(0);
       });
     });
   });
@@ -130,8 +131,8 @@ describe('@jupyterlab/filebrowser', () => {
 
       const result = await dialog;
 
-      expect(result.button.accept).false;
-      expect(result.value).null;
+      expect(result.button.accept).toBe(false);
+      expect(result.value).toBeNull();
     });
 
     it('should accept options', async () => {
@@ -150,9 +151,9 @@ describe('@jupyterlab/filebrowser', () => {
 
       const result = await dialog;
 
-      expect(result.button.accept).true;
+      expect(result.button.accept).toBe(true);
       const items = result.value!;
-      expect(items.length).equal(1);
+      expect(items.length).toBe(1);
 
       document.body.removeChild(node);
     });
@@ -174,7 +175,7 @@ describe('@jupyterlab/filebrowser', () => {
 
       let counter = 0;
       const listing = node.getElementsByClassName('jp-DirListing-content')[0];
-      expect(listing).to.be.ok;
+      expect(listing).toBeTruthy();
 
       let items = listing.getElementsByTagName('li');
       counter = 0;
@@ -186,23 +187,18 @@ describe('@jupyterlab/filebrowser', () => {
       }
 
       // Fails if there is no items shown
-      expect(items.length).to.be.greaterThan(0);
+      expect(items.length).toBeGreaterThan(0);
 
       // Emulate notebook file selection
-      // Get node coordinates we need to be precised as code test for hit position
-      const rect = items.item(items.length - 1)!.getBoundingClientRect();
-
-      simulate(items.item(items.length - 1)!, 'mousedown', {
-        clientX: 0.5 * (rect.left + rect.right),
-        clientY: 0.5 * (rect.bottom + rect.top)
-      });
+      const item = listing.querySelector('li[data-file-type="notebook"]')!;
+      simulate(item, 'mousedown');
 
       await acceptDialog();
       const result = await dialog;
       const files = result.value!;
-      expect(files.length).equal(1);
-      expect(files[0].type).equal('notebook');
-      expect(files[0].name).matches(/Untitled\d*.ipynb/);
+      expect(files.length).toBe(1);
+      expect(files[0].type).toBe('notebook');
+      expect(files[0].name).toEqual(expect.stringMatching(/Untitled.*.ipynb/));
 
       document.body.removeChild(node);
     });
@@ -217,9 +213,9 @@ describe('@jupyterlab/filebrowser', () => {
       const result = await dialog;
       const items = result.value!;
 
-      expect(items.length).equal(1);
-      expect(items[0].type).equal('directory');
-      expect(items[0].path).equal('');
+      expect(items.length).toBe(1);
+      expect(items[0].type).toBe('directory');
+      expect(items[0].path).toBe('');
     });
   });
 
@@ -233,8 +229,8 @@ describe('@jupyterlab/filebrowser', () => {
 
       const result = await dialog;
 
-      expect(result.button.accept).false;
-      expect(result.value).null;
+      expect(result.button.accept).toBe(false);
+      expect(result.value).toBeNull();
     });
 
     it('should accept options', async () => {
@@ -252,8 +248,8 @@ describe('@jupyterlab/filebrowser', () => {
 
       const result = await dialog;
 
-      expect(result.button.accept).true;
-      expect(result.value!.length).equal(1);
+      expect(result.button.accept).toBe(true);
+      expect(result.value!.length).toBe(1);
 
       document.body.removeChild(node);
     });
@@ -274,7 +270,7 @@ describe('@jupyterlab/filebrowser', () => {
 
       let counter = 0;
       const listing = node.getElementsByClassName('jp-DirListing-content')[0];
-      expect(listing).to.be.ok;
+      expect(listing).toBeTruthy();
 
       let items = listing.getElementsByTagName('li');
       // Wait for the directory listing to be populated
@@ -285,23 +281,19 @@ describe('@jupyterlab/filebrowser', () => {
       }
 
       // Fails if there is no items shown
-      expect(items.length).to.be.greaterThan(0);
+      expect(items.length).toBeGreaterThan(0);
 
       // Emulate notebook file selection
-      // Get node coordinates we need to be precised as code test for hit position
-      const rect = items.item(items.length - 1)!.getBoundingClientRect();
-
-      simulate(items.item(items.length - 1)!, 'mousedown', {
-        clientX: 0.5 * (rect.left + rect.right),
-        clientY: 0.5 * (rect.bottom + rect.top)
-      });
+      simulate(items.item(items.length - 1)!, 'mousedown');
 
       await acceptDialog();
       const result = await dialog;
       const files = result.value!;
-      expect(files.length).equal(1);
-      expect(files[0].type).equal('directory');
-      expect(files[0].name).matches(/Untitled Folder( \d+)?/);
+      expect(files.length).toBe(1);
+      expect(files[0].type).toBe('directory');
+      expect(files[0].name).toEqual(
+        expect.stringMatching(/Untitled Folder( \d+)?/)
+      );
 
       document.body.removeChild(node);
     });
@@ -316,9 +308,9 @@ describe('@jupyterlab/filebrowser', () => {
       const result = await dialog;
       const items = result.value!;
 
-      expect(items.length).equal(1);
-      expect(items[0].type).equal('directory');
-      expect(items[0].path).equal('');
+      expect(items.length).toBe(1);
+      expect(items[0].type).toBe('directory');
+      expect(items[0].path).toBe('');
     });
   });
 });

+ 33 - 0
packages/filebrowser/tsconfig.test.json

@@ -0,0 +1,33 @@
+{
+  "extends": "../../tsconfigbase.test",
+  "include": ["src/*", "test/*"],
+  "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../docmanager"
+    },
+    {
+      "path": "../docregistry"
+    },
+    {
+      "path": "../services"
+    },
+    {
+      "path": "../statedb"
+    },
+    {
+      "path": "../statusbar"
+    },
+    {
+      "path": "../ui-components"
+    },
+    {
+      "path": "../../testutils"
+    }
+  ]
+}

+ 11 - 4
tests/modernize.js

@@ -46,7 +46,14 @@ const targetData = utils.readJSONFile(path.join(pkgPath, name, 'package.json'));
   targetData['devDependencies'][dep] = sourceData['devDependencies'][dep];
 });
 // Update scripts
-['build:test', 'test', 'test:cov', 'test:debug', 'watch'].forEach(script => {
+[
+  'build:test',
+  'test',
+  'test:cov',
+  'test:debug',
+  'test:debug:watch',
+  'watch'
+].forEach(script => {
   targetData['scripts'][script] = sourceData['scripts'][script];
 });
 utils.writeJSONFile(path.join(pkgPath, name, 'package.json'), targetData);
@@ -66,7 +73,7 @@ glob.sync(path.join(testSrc, 'src', '**', '*.ts*')).forEach(function(filePath) {
 
 // Commit changes (needed for jlpm jest-codemods)
 utils.run(
-  `git add test-${name} && git commit -m "wip modernize ${name} tests"`
+  `git add test-${name}; git add ../packages/${name}; git commit -m "wip modernize ${name} tests"; true`
 );
 
 // Run jest-codemods to convert from chai to jest.
@@ -93,9 +100,9 @@ utils.run(
 );
 utils.run(`git add -f ${pkgPath}/${name}/.vscode/launch.json`);
 
-// Run integrity and build the new tests
+// Run integrity
 const rootDir = path.resolve('..');
-utils.run(`jlpm integrity && cd packages/${name} && jlpm run build:test`, {
+utils.run(`jlpm integrity`, {
   cwd: rootDir
 });
 

+ 0 - 1
tests/test-filebrowser/karma-cov.conf.js

@@ -1 +0,0 @@
-module.exports = require('../karma-cov.conf');

+ 0 - 1
tests/test-filebrowser/karma.conf.js

@@ -1 +0,0 @@
-module.exports = require('../karma.conf');

+ 0 - 43
tests/test-filebrowser/package.json

@@ -1,43 +0,0 @@
-{
-  "name": "@jupyterlab/test-filebrowser",
-  "version": "2.1.0",
-  "private": true,
-  "scripts": {
-    "build": "tsc -b",
-    "clean": "rimraf build && rimraf coverage",
-    "coverage": "python run-test.py --browsers=ChromeHeadless karma-cov.conf.js",
-    "test": "jlpm run test:firefox-headless",
-    "test:chrome": "python run-test.py --browsers=Chrome karma.conf.js",
-    "test:chrome-headless": "python run-test.py --browsers=ChromeHeadless karma.conf.js",
-    "test:debug": "python run-test.py  --browsers=Chrome --singleRun=false --debug=true --browserNoActivityTimeout=10000000 karma.conf.js",
-    "test:firefox": "python run-test.py --browsers=Firefox karma.conf.js",
-    "test:firefox-headless": "python run-test.py --browsers=FirefoxHeadless karma.conf.js",
-    "test:ie": "python run-test.py  --browsers=IE karma.conf.js",
-    "watch": "tsc -b --watch",
-    "watch:src": "tsc -p src --watch"
-  },
-  "dependencies": {
-    "@jupyterlab/coreutils": "^4.1.0",
-    "@jupyterlab/docmanager": "^2.1.0",
-    "@jupyterlab/docregistry": "^2.1.0",
-    "@jupyterlab/filebrowser": "^2.1.0",
-    "@jupyterlab/services": "^5.1.0",
-    "@jupyterlab/statedb": "^2.1.0",
-    "@jupyterlab/testutils": "^2.1.0",
-    "@lumino/algorithm": "^1.2.3",
-    "@lumino/coreutils": "^1.4.2",
-    "@lumino/messaging": "^1.3.3",
-    "@lumino/widgets": "^1.11.1",
-    "chai": "^4.2.0",
-    "simulate-event": "~1.4.0"
-  },
-  "devDependencies": {
-    "@types/chai": "^4.2.7",
-    "@types/mocha": "^7.0.2",
-    "karma": "^4.4.1",
-    "karma-chrome-launcher": "~3.1.0",
-    "puppeteer": "~2.0.0",
-    "rimraf": "~3.0.0",
-    "typescript": "~3.7.3"
-  }
-}

+ 0 - 10
tests/test-filebrowser/run-test.py

@@ -1,10 +0,0 @@
-# Copyright (c) Jupyter Development Team.
-# Distributed under the terms of the Modified BSD License.
-
-import os
-from jupyterlab.tests.test_app import run_karma
-
-HERE = os.path.realpath(os.path.dirname(__file__))
-
-if __name__ == '__main__':
-    run_karma(HERE)

+ 0 - 503
tests/test-filebrowser/src/model.spec.ts

@@ -1,503 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import { expect } from 'chai';
-
-import { PageConfig } from '@jupyterlab/coreutils';
-
-import { UUID } from '@lumino/coreutils';
-
-import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
-
-import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
-
-import { StateDB } from '@jupyterlab/statedb';
-
-import {
-  FileBrowserModel,
-  LARGE_FILE_SIZE,
-  CHUNK_SIZE
-} from '@jupyterlab/filebrowser';
-
-import {
-  Contents,
-  ContentsManager,
-  ServiceManager
-} from '@jupyterlab/services';
-
-import {
-  acceptDialog,
-  dismissDialog,
-  signalToPromises,
-  sleep
-} from '@jupyterlab/testutils';
-
-import { toArray } from '@lumino/algorithm';
-
-/**
- * A contents manager that delays requests by less each time it is called
- * in order to simulate out-of-order responses from the server.
- */
-class DelayedContentsManager extends ContentsManager {
-  get(
-    path: string,
-    options?: Contents.IFetchOptions
-  ): Promise<Contents.IModel> {
-    return new Promise<Contents.IModel>(resolve => {
-      const delay = this._delay;
-      this._delay -= 500;
-      void super.get(path, options).then(contents => {
-        setTimeout(() => {
-          resolve(contents);
-        }, Math.max(delay, 0));
-      });
-    });
-  }
-
-  private _delay = 1000;
-}
-
-describe('filebrowser/model', () => {
-  let manager: IDocumentManager;
-  let serviceManager: ServiceManager.IManager;
-  let registry: DocumentRegistry;
-  let model: FileBrowserModel;
-  let name: string;
-  let state: StateDB;
-
-  before(() => {
-    const opener: DocumentManager.IWidgetOpener = {
-      open: widget => {
-        /* no op */
-      }
-    };
-
-    registry = new DocumentRegistry({
-      textModelFactory: new TextModelFactory()
-    });
-    serviceManager = new ServiceManager({ standby: 'never' });
-    manager = new DocumentManager({
-      registry,
-      opener,
-      manager: serviceManager
-    });
-    state = new StateDB();
-  });
-
-  beforeEach(async () => {
-    await state.clear();
-    model = new FileBrowserModel({ manager, state });
-    const contents = await manager.newUntitled({ type: 'file' });
-    name = contents.name;
-    return model.cd();
-  });
-
-  afterEach(() => {
-    model.dispose();
-  });
-
-  describe('FileBrowserModel', () => {
-    describe('#constructor()', () => {
-      it('should construct a new file browser model', () => {
-        model = new FileBrowserModel({ manager });
-        expect(model).to.be.an.instanceof(FileBrowserModel);
-      });
-    });
-
-    describe('#pathChanged', () => {
-      it('should be emitted when the path changes', async () => {
-        let called = false;
-        model.pathChanged.connect((sender, args) => {
-          expect(sender).to.equal(model);
-          expect(args.name).to.equal('path');
-          expect(args.oldValue).to.equal('');
-          expect(args.newValue).to.equal('src');
-          called = true;
-        });
-        await model.cd('src');
-        expect(called).to.equal(true);
-      });
-    });
-
-    describe('#refreshed', () => {
-      it('should be emitted after a refresh', async () => {
-        let called = false;
-        model.refreshed.connect((sender, arg) => {
-          expect(sender).to.equal(model);
-          expect(arg).to.be.undefined;
-          called = true;
-        });
-        await model.cd();
-        expect(called).to.equal(true);
-      });
-
-      it('should be emitted when the path changes', async () => {
-        let called = false;
-        model.refreshed.connect((sender, arg) => {
-          expect(sender).to.equal(model);
-          expect(arg).to.be.undefined;
-          called = true;
-        });
-        await model.cd('src');
-        expect(called).to.equal(true);
-      });
-    });
-
-    describe('#fileChanged', () => {
-      it('should be emitted when a file is created', async () => {
-        let called = false;
-        model.fileChanged.connect((sender, args) => {
-          expect(sender).to.equal(model);
-          expect(args.type).to.equal('new');
-          expect(args.oldValue).to.be.null;
-          expect(args.newValue!.type).to.equal('file');
-          called = true;
-        });
-        await manager.newUntitled({ type: 'file' });
-        expect(called).to.equal(true);
-      });
-
-      it('should be emitted when a file is renamed', async () => {
-        let called = false;
-        model.fileChanged.connect((sender, args) => {
-          expect(sender).to.equal(model);
-          expect(args.type).to.equal('rename');
-          expect(args.oldValue!.path).to.equal(name);
-          expect(args.newValue!.path).to.equal(name + '.bak');
-          called = true;
-        });
-        await manager.rename(name, name + '.bak');
-        expect(called).to.equal(true);
-      });
-
-      it('should be emitted when a file is deleted', async () => {
-        let called = false;
-        model.fileChanged.connect((sender, args) => {
-          expect(sender).to.equal(model);
-          expect(args.type).to.equal('delete');
-          expect(args.oldValue!.path).to.equal(name);
-          expect(args.newValue).to.be.null;
-          called = true;
-        });
-        await manager.deleteFile(name);
-        expect(called).to.equal(true);
-      });
-    });
-
-    describe('#path', () => {
-      it('should be the current path of the model', async () => {
-        expect(model.path).to.equal('');
-        await model.cd('src/');
-        expect(model.path).to.equal('src');
-      });
-    });
-
-    describe('#items()', () => {
-      it('should get an iterator of items in the current path', () => {
-        const items = model.items();
-        expect(items.next()).to.be.ok;
-      });
-    });
-
-    describe('#isDisposed', () => {
-      it('should test whether the model is disposed', () => {
-        expect(model.isDisposed).to.equal(false);
-        model.dispose();
-        expect(model.isDisposed).to.equal(true);
-      });
-    });
-
-    describe('#sessions()', () => {
-      it('should be the session models for the active notebooks', async () => {
-        const contents = await manager.newUntitled({ type: 'notebook' });
-        const session = await serviceManager.sessions.startNew({
-          name: '',
-          path: contents.path,
-          type: 'test'
-        });
-        await model.cd();
-        expect(model.sessions().next()).to.be.ok;
-        await session.shutdown();
-      });
-    });
-
-    describe('#dispose()', () => {
-      it('should dispose of the resources held by the model', () => {
-        model.dispose();
-        expect(model.isDisposed).to.equal(true);
-      });
-
-      it('should be safe to call more than once', () => {
-        model.dispose();
-        model.dispose();
-        expect(model.isDisposed).to.equal(true);
-      });
-    });
-
-    describe('#refresh()', () => {
-      it('should refresh the contents', () => {
-        return model.refresh();
-      });
-    });
-
-    describe('#cd()', () => {
-      it('should change directory', async () => {
-        await model.cd('src');
-        expect(model.path).to.equal('src');
-      });
-
-      it('should accept a relative path', async () => {
-        await model.cd('./src');
-        expect(model.path).to.equal('src');
-      });
-
-      it('should accept a parent directory', async () => {
-        await model.cd('src');
-        await model.cd('..');
-        expect(model.path).to.equal('');
-      });
-
-      it('should be resilient to a slow initial fetch', async () => {
-        const delayedServiceManager = new ServiceManager({ standby: 'never' });
-        (delayedServiceManager as any).contents = new DelayedContentsManager();
-        const manager = new DocumentManager({
-          registry,
-          opener,
-          manager: delayedServiceManager
-        });
-        model = new FileBrowserModel({ manager, state }); // Should delay 1000ms
-
-        // An initial refresh is called in the constructor.
-        // If it is too slow, it can come in after the directory change,
-        // causing a directory set by, e.g., the tree handler to be wrong.
-        // This checks to make sure we are handling that case correctly.
-        await model.cd('src'); // should delay 500ms
-        await sleep(2000);
-        expect(model.path).to.equal('src');
-
-        manager.dispose();
-        delayedServiceManager.contents.dispose();
-        delayedServiceManager.dispose();
-        model.dispose();
-      });
-    });
-
-    describe('#restore()', () => {
-      it('should restore based on ID', async () => {
-        const id = 'foo';
-        const model2 = new FileBrowserModel({ manager, state });
-        await model.restore(id);
-        await model.cd('src');
-        expect(model.path).to.equal('src');
-        expect(model2.path).to.equal('');
-        await model2.restore(id);
-        expect(model2.path).to.equal('src');
-        model2.dispose();
-      });
-
-      it('should be safe to call multiple times', async () => {
-        const id = 'bar';
-        const model2 = new FileBrowserModel({ manager, state });
-        await model.restore(id);
-        await model.cd('src');
-        expect(model.path).to.equal('src');
-        expect(model2.path).to.equal('');
-        await model2.restore(id);
-        await model2.restore(id);
-        expect(model2.path).to.equal('src');
-        model2.dispose();
-      });
-    });
-
-    describe('#download()', () => {
-      it('should download the file without error', () => {
-        // TODO: how to test this?
-      });
-    });
-
-    describe('#upload()', () => {
-      it('should upload a file object', async () => {
-        const fname = UUID.uuid4() + '.html';
-        const file = new File(['<p>Hello world!</p>'], fname, {
-          type: 'text/html'
-        });
-        const contents = await model.upload(file);
-        expect(contents.name).to.equal(fname);
-      });
-
-      it('should overwrite', async () => {
-        const fname = UUID.uuid4() + '.html';
-        const file = new File(['<p>Hello world!</p>'], fname, {
-          type: 'text/html'
-        });
-        const contents = await model.upload(file);
-        expect(contents.name).to.equal(fname);
-        const promise = model.upload(file);
-        await acceptDialog();
-        await promise;
-        expect(contents.name).to.equal(fname);
-      });
-
-      it('should not overwrite', async () => {
-        const fname = UUID.uuid4() + '.html';
-        const file = new File(['<p>Hello world!</p>'], fname, {
-          type: 'text/html'
-        });
-        const contents = await model.upload(file);
-        expect(contents.name).to.equal(fname);
-        const promise = model.upload(file);
-        await dismissDialog();
-        try {
-          await promise;
-        } catch (e) {
-          expect(e).to.equal('File not uploaded');
-        }
-      });
-
-      it('should emit the fileChanged signal', async () => {
-        const fname = UUID.uuid4() + '.html';
-        let called = false;
-        model.fileChanged.connect((sender, args) => {
-          expect(sender).to.equal(model);
-          expect(args.type).to.equal('save');
-          expect(args.oldValue).to.be.null;
-          expect(args.newValue!.path).to.equal(fname);
-          called = true;
-        });
-        const file = new File(['<p>Hello world!</p>'], fname, {
-          type: 'text/html'
-        });
-        await model.upload(file);
-        expect(called).to.equal(true);
-      });
-
-      describe('older notebook version', () => {
-        let prevNotebookVersion: string;
-
-        before(() => {
-          prevNotebookVersion = PageConfig.setOption(
-            'notebookVersion',
-            JSON.stringify([5, 0, 0])
-          );
-        });
-
-        it('should not upload large file', async () => {
-          const fname = UUID.uuid4() + '.html';
-          const file = new File([new ArrayBuffer(LARGE_FILE_SIZE + 1)], fname);
-          try {
-            await model.upload(file);
-            throw new Error('Upload should have failed');
-          } catch (err) {
-            expect(err).to.equal(`Cannot upload file (>15 MB). ${fname}`);
-          }
-        });
-
-        after(() => {
-          PageConfig.setOption('notebookVersion', prevNotebookVersion);
-        });
-      });
-
-      describe('newer notebook version', () => {
-        let prevNotebookVersion: string;
-
-        before(() => {
-          prevNotebookVersion = PageConfig.setOption(
-            'notebookVersion',
-            JSON.stringify([5, 1, 0])
-          );
-        });
-
-        for (const ending of ['.txt', '.ipynb']) {
-          for (const size of [
-            CHUNK_SIZE - 1,
-            CHUNK_SIZE,
-            CHUNK_SIZE + 1,
-            2 * CHUNK_SIZE
-          ]) {
-            it(`should upload a large ${ending} file of size ${size}`, async () => {
-              const fname = UUID.uuid4() + ending;
-              // minimal valid (according to server) notebook
-              let content =
-                '{"nbformat": 4, "metadata": {"_": ""}, "nbformat_minor": 2, "cells": []}';
-              // make metadata longer so that total document is `size` long
-              content = content.replace(
-                '"_": ""',
-                `"_": "${' '.repeat(size - content.length)}"`
-              );
-              const file = new File([content], fname, { type: 'text/plain' });
-              await model.upload(file);
-              const {
-                content: newContent
-              } = await model.manager.services.contents.get(fname);
-              // the contents of notebooks are returned as objects instead of strings
-              if (ending === '.ipynb') {
-                expect(newContent).to.deep.equal(JSON.parse(content));
-              } else {
-                expect(newContent).to.equal(content);
-              }
-            });
-          }
-        }
-        it(`should produce progress as a large file uploads`, async () => {
-          const fname = UUID.uuid4() + '.txt';
-          const file = new File([new ArrayBuffer(2 * CHUNK_SIZE)], fname);
-
-          const [start, first, second, finished] = signalToPromises(
-            model.uploadChanged,
-            4
-          );
-
-          const uploaded = model.upload(file);
-          expect(toArray(model.uploads())).to.deep.equal([]);
-          expect(await start).to.deep.equal([
-            model,
-            {
-              name: 'start',
-              oldValue: null,
-              newValue: { path: fname, progress: 0 }
-            }
-          ]);
-          expect(toArray(model.uploads())).to.deep.equal([
-            { path: fname, progress: 0 }
-          ]);
-          expect(await first).to.deep.equal([
-            model,
-            {
-              name: 'update',
-              oldValue: { path: fname, progress: 0 },
-              newValue: { path: fname, progress: 0 }
-            }
-          ]);
-          expect(toArray(model.uploads())).to.deep.equal([
-            { path: fname, progress: 0 }
-          ]);
-          expect(await second).to.deep.equal([
-            model,
-            {
-              name: 'update',
-              oldValue: { path: fname, progress: 0 },
-              newValue: { path: fname, progress: 1 / 2 }
-            }
-          ]);
-          expect(toArray(model.uploads())).to.deep.equal([
-            { path: fname, progress: 1 / 2 }
-          ]);
-          expect(await finished).to.deep.equal([
-            model,
-            {
-              name: 'finish',
-              oldValue: { path: fname, progress: 1 / 2 },
-              newValue: null
-            }
-          ]);
-          expect(toArray(model.uploads())).to.deep.equal([]);
-          await uploaded;
-        });
-
-        after(() => {
-          PageConfig.setOption('notebookVersion', prevNotebookVersion);
-        });
-      });
-    });
-  });
-});

+ 0 - 34
tests/test-filebrowser/tsconfig.json

@@ -1,34 +0,0 @@
-{
-  "extends": "../../tsconfigbase",
-  "compilerOptions": {
-    "outDir": "build",
-    "types": ["mocha"],
-    "composite": false,
-    "rootDir": "src",
-    "skipLibCheck": true
-  },
-  "include": ["src/*"],
-  "references": [
-    {
-      "path": "../../packages/coreutils"
-    },
-    {
-      "path": "../../packages/docmanager"
-    },
-    {
-      "path": "../../packages/docregistry"
-    },
-    {
-      "path": "../../packages/filebrowser"
-    },
-    {
-      "path": "../../packages/services"
-    },
-    {
-      "path": "../../packages/statedb"
-    },
-    {
-      "path": "../../testutils"
-    }
-  ]
-}

+ 147 - 38
testutils/src/mock.ts

@@ -12,7 +12,8 @@ import {
   Session,
   ServiceManager,
   Contents,
-  ServerConnection
+  ServerConnection,
+  ContentsManager
 } from '@jupyterlab/services';
 
 import { ArrayIterator } from '@lumino/algorithm';
@@ -401,33 +402,19 @@ export const SessionContextMock = jest.fn<
  * A mock contents manager.
  */
 export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
-  const files: { [key: string]: Contents.IModel } = {};
+  const files = new Map<string, Contents.IModel>();
+  const dummy = new ContentsManager();
   const checkpoints: { [key: string]: Contents.ICheckpointModel } = {};
 
+  const baseModel = Private.createFile({ type: 'directory' });
+  files.set('', { ...baseModel, path: '', name: '' });
+
   const thisObject: Contents.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
     newUntitled: jest.fn(options => {
-      options = options || {};
-      const name = UUID.uuid4() + options.ext || '.txt';
-      const path = PathExt.join(options.path || '', name);
-      let content = '';
-      if (options.type === 'notebook') {
-        content = JSON.stringify({});
-      }
-      const timeStamp = new Date().toISOString();
-      const model: Contents.IModel = {
-        path,
-        content,
-        name,
-        last_modified: timeStamp,
-        writable: true,
-        created: timeStamp,
-        type: options.type || 'file',
-        format: 'text',
-        mimetype: 'plain/text'
-      };
-      files[path] = model;
+      const model = Private.createFile(options || {});
+      files.set(model.path, model);
       fileChangedSignal.emit({
         type: 'new',
         oldValue: null,
@@ -450,24 +437,80 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
       return null;
     }),
     normalize: jest.fn(path => {
-      return path;
+      return dummy.normalize(path);
     }),
     localPath: jest.fn(path => {
-      return path;
+      return dummy.localPath(path);
+    }),
+    get: jest.fn((path, options) => {
+      path = Private.fixSlash(path);
+      if (!files.has(path)) {
+        return Private.makeResponseError(404);
+      }
+      const model = files.get(path)!;
+      if (model.type === 'directory') {
+        if (options?.content == true) {
+          const content: Contents.IModel[] = [];
+          files.forEach(fileModel => {
+            if (PathExt.dirname(fileModel.path) == model.path) {
+              content.push(fileModel);
+            }
+          });
+          return Promise.resolve({ ...model, content });
+        }
+        return Promise.resolve(model);
+      }
+      if (options?.content == true) {
+        return Promise.resolve(model);
+      }
+      return Promise.resolve({ ...model, content: '' });
+    }),
+    driveName: jest.fn(path => {
+      return dummy.driveName(path);
     }),
-    get: jest.fn((path, _) => {
-      if (!files[path]) {
-        const resp = new Response(void 0, { status: 404 });
-        return Promise.reject(new ServerConnection.ResponseError(resp));
+    rename: jest.fn((oldPath, newPath) => {
+      oldPath = Private.fixSlash(oldPath);
+      newPath = Private.fixSlash(newPath);
+      if (!files.has(oldPath)) {
+        return Private.makeResponseError(404);
       }
-      return Promise.resolve(files[path]);
+      const oldValue = files.get(oldPath)!;
+      files.delete(oldPath);
+      const name = PathExt.basename(newPath);
+      const newValue = { ...oldValue, name, path: newPath };
+      files.set(newPath, newValue);
+      fileChangedSignal.emit({
+        type: 'rename',
+        oldValue,
+        newValue
+      });
+      return Promise.resolve(newValue);
+    }),
+    delete: jest.fn(path => {
+      path = Private.fixSlash(path);
+      if (!files.has(path)) {
+        return Private.makeResponseError(404);
+      }
+      const oldValue = files.get(path)!;
+      files.delete(path);
+      fileChangedSignal.emit({
+        type: 'delete',
+        oldValue,
+        newValue: null
+      });
+      return Promise.resolve(void 0);
     }),
     save: jest.fn((path, options) => {
+      path = Private.fixSlash(path);
       const timeStamp = new Date().toISOString();
-      if (files[path]) {
-        files[path] = { ...files[path], ...options, last_modified: timeStamp };
+      if (files.has(path)) {
+        files.set(path, {
+          ...files.get(path)!,
+          ...options,
+          last_modified: timeStamp
+        });
       } else {
-        files[path] = {
+        files.set(path, {
           path,
           name: PathExt.basename(path),
           content: '',
@@ -478,16 +521,18 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
           mimetype: 'plain/text',
           ...options,
           last_modified: timeStamp
-        };
+        });
       }
       fileChangedSignal.emit({
         type: 'save',
         oldValue: null,
-        newValue: files[path]
+        newValue: files.get(path)!
       });
-      return Promise.resolve(files[path]);
-    })
+      return Promise.resolve(files.get(path)!);
+    }),
+    dispose: jest.fn()
   };
+
   const fileChangedSignal = new Signal<
     Contents.IManager,
     Contents.IChangedArgs
@@ -500,21 +545,35 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
  * A mock sessions manager.
  */
 export const SessionManagerMock = jest.fn<Session.IManager, []>(() => {
-  const sessions: Session.IModel[] = [];
+  let sessions: Session.IModel[] = [];
   const thisObject: Session.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
     startNew: jest.fn(options => {
       const session = new SessionConnectionMock({ model: options }, null);
       sessions.push(session.model);
+      runningChangedSignal.emit(sessions);
       return session;
     }),
     connectTo: jest.fn(options => {
       return new SessionConnectionMock(options, null);
     }),
+    stopIfNeeded: jest.fn(path => {
+      const length = sessions.length;
+      sessions = sessions.filter(model => model.path !== path);
+      if (sessions.length !== length) {
+        runningChangedSignal.emit(sessions);
+      }
+      return Promise.resolve(void 0);
+    }),
     refreshRunning: jest.fn(() => Promise.resolve(void 0)),
     running: jest.fn(() => new ArrayIterator(sessions))
   };
+
+  const runningChangedSignal = new Signal<Session.IManager, Session.IModel[]>(
+    thisObject
+  );
+  (thisObject as any).runningChanged = runningChangedSignal;
   return thisObject;
 });
 
@@ -539,7 +598,8 @@ export const ServiceManagerMock = jest.fn<ServiceManager.IManager, []>(() => {
     ready: Promise.resolve(void 0),
     contents: new ContentsManagerMock(),
     sessions: new SessionManagerMock(),
-    kernelspecs: new KernelSpecManagerMock()
+    kernelspecs: new KernelSpecManagerMock(),
+    dispose: jest.fn()
   };
   return thisObject;
 });
@@ -575,6 +635,55 @@ namespace Private {
     [P in keyof T]?: RecursivePartial<T[P]>;
   };
 
+  export function createFile(
+    options?: Contents.ICreateOptions
+  ): Contents.IModel {
+    options = options || {};
+    let name = UUID.uuid4();
+    switch (options.type) {
+      case 'directory':
+        name = `Untitled Folder_${name}`;
+        break;
+      case 'notebook':
+        name = `Untitled_${name}.ipynb`;
+        break;
+      default:
+        name = `untitled_${name}${options.ext || '.txt'}`;
+    }
+
+    const path = PathExt.join(options.path || '', name);
+    let content = '';
+    if (options.type === 'notebook') {
+      content = JSON.stringify({});
+    }
+    const timeStamp = new Date().toISOString();
+    return {
+      path,
+      content,
+      name,
+      last_modified: timeStamp,
+      writable: true,
+      created: timeStamp,
+      type: options.type || 'file',
+      format: 'text',
+      mimetype: 'plain/text'
+    };
+  }
+
+  export function fixSlash(path: string): string {
+    if (path.endsWith('/')) {
+      path = path.slice(0, path.length - 1);
+    }
+    return path;
+  }
+
+  export function makeResponseError(
+    status: number
+  ): Promise<ServerConnection.ResponseError> {
+    const resp = new Response(void 0, { status });
+    return Promise.reject(new ServerConnection.ResponseError(resp));
+  }
+
   export function cloneKernel(
     options: RecursivePartial<Kernel.IKernelConnection.IOptions>
   ): Kernel.IKernelConnection {