Bladeren bron

Merge pull request #75 from blink1073/filehandler-tests

Update handler api and add tests
A. Darian 9 jaren geleden
bovenliggende
commit
b7308b7ad2
6 gewijzigde bestanden met toevoegingen van 627 en 29 verwijderingen
  1. 1 1
      src/filehandler/default.ts
  2. 37 28
      src/filehandler/handler.ts
  3. 502 0
      test/src/filehandler/filehandler.spec.ts
  4. 1 0
      test/src/index.ts
  5. 85 0
      test/src/mock.ts
  6. 1 0
      test/src/typings.d.ts

+ 1 - 1
src/filehandler/default.ts

@@ -58,7 +58,7 @@ class FileHandler extends AbstractFileHandler<CodeMirrorWidget> {
     let widget = new CodeMirrorWidget();
     widget.addClass(EDITOR_CLASS);
     CodeMirror.on(widget.editor.getDoc(), 'change', () => {
-      this.setDirty(path);
+      this.setDirty(path, true);
     });
     return widget;
   }

+ 37 - 28
src/filehandler/handler.ts

@@ -54,6 +54,13 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     return Private.openedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when a file finishes opening.
+   */
+  get finished(): ISignal<AbstractFileHandler<T>, T> {
+    return Private.finishedSignal.bind(this);
+  }
+
   /**
    * Get the list of file extensions explicitly supported by the handler.
    */
@@ -81,8 +88,7 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
   }
 
   /**
-   * Find a path given a widget.  The model itself will have a
-   * null `content` field.
+   * Find a path given a widget.
    */
   findPath(widget: T): string {
     return Private.pathProperty.get(widget);
@@ -96,6 +102,7 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     if (!widget) {
       widget = this.createWidget(path);
       widget.title.closable = true;
+      widget.title.text = this.getTitleText(path);
       this._setPath(widget, path);
       this._widgets.push(widget);
       installMessageFilter(widget, this);
@@ -104,10 +111,10 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     // Fetch the contents and populate the widget asynchronously.
     let opts = this.getFetchOptions(path);
     this.manager.get(path, opts).then(contents => {
-      widget.title.text = this.getTitleText(path);
       return this.populateWidget(widget, contents);
     }).then(contents => {
-      this.clearDirty(path);
+      this.setDirty(path, false);
+      this.finished.emit(widget);
     });
     this.opened.emit(widget);
     return widget;
@@ -122,7 +129,7 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
       return false;
     }
     if (newPath === void 0) {
-      this.clearDirty(oldPath);
+      this.setDirty(oldPath, false);
       widget.close();
       return true;
     }
@@ -151,7 +158,7 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     return this.getSaveOptions(widget, model).then(opts => {
       return this.manager.save(path, opts);
     }).then(contents => {
-      this.clearDirty(path);
+      this.setDirty(path, false);
       return contents;
     });
   }
@@ -175,7 +182,7 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     return this.manager.get(path, opts).then(contents => {
       return this.populateWidget(widget, contents);
     }).then(contents => {
-      this.clearDirty(path);
+      this.setDirty(path, false);
       return contents;
     });
   }
@@ -195,17 +202,19 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
     if (this.isDirty(path)) {
       return this._maybeClose(widget);
     }
-    this._close(widget);
-    return Promise.resolve(true);
+    return this._close(widget);
   }
 
   /**
    * Close all files.
    */
-  closeAll(): void {
+  closeAll(): Promise<void> {
+    let promises: Promise<boolean>[] = [];
     for (let w of this._widgets) {
-      w.close();
+      let path = this.findPath(w);
+      promises.push(this.close(path));
     }
+    return Promise.all(promises).then(() => { return void 0; });
   }
 
   /**
@@ -213,23 +222,17 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
    */
   isDirty(path: string): boolean {
     let widget = this.findWidget(path);
-    return Private.dirtyProperty.get(widget);
+    if (widget) {
+      return Private.dirtyProperty.get(widget);
+    }
   }
 
   /**
    * Set the dirty state of a file.
    */
-  setDirty(path: string): void {
+  setDirty(path: string, value: boolean): void {
     let widget = this.findWidget(path);
-    Private.dirtyProperty.set(widget, true);
-  }
-
-  /**
-   * Clear the dirty state of a file.
-   */
-  clearDirty(path: string): void {
-    let widget = this.findWidget(path);
-    Private.dirtyProperty.set(widget, false);
+    if (widget) Private.dirtyProperty.set(widget, value);
   }
 
   /**
@@ -325,13 +328,14 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
   /**
    * Actually close the file.
    */
-  private _close(widget: T): void {
-    this.beforeClose(widget).then(() => {
+  private _close(widget: T): Promise<boolean> {
+    return this.beforeClose(widget).then(() => {
       let model = Private.pathProperty.get(widget);
       let index = this._widgets.indexOf(widget);
       this._widgets.splice(index, 1);
-      Private.pathProperty.set(widget, null);
+      Private.pathProperty.set(widget, void 0);
       widget.close();
+      return true;
     });
   }
 
@@ -346,18 +350,23 @@ abstract class AbstractFileHandler<T extends Widget> implements IMessageFilter {
  */
 namespace Private {
   /**
-   * A signal emitted when a model is opened.
+   * A signal emitted when a path is opened.
    */
   export
   const openedSignal = new Signal<AbstractFileHandler<Widget>, Widget>();
 
+  /**
+   * A signal emitted when a model is populated.
+   */
+  export
+  const finishedSignal = new Signal<AbstractFileHandler<Widget>, Widget>();
+
   /**
    * An attached property with the widget path.
    */
   export
   const pathProperty = new Property<Widget, string>({
-    name: 'path',
-    value: null
+    name: 'path'
   });
 
   /**

+ 502 - 0
test/src/filehandler/filehandler.spec.ts

@@ -0,0 +1,502 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import expect = require('expect.js');
+
+import {
+  IContentsModel, IContentsManager, IContentsOpts, ICheckpointModel,
+  IAjaxSettings, ContentsManager
+} from 'jupyter-js-services';
+
+import {
+  sendMessage
+} from 'phosphor-messaging';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+import {
+  AbstractFileHandler
+} from '../../../lib/filehandler/handler';
+
+import {
+  MockContentsManager
+} from '../mock';
+
+
+class FileHandler extends AbstractFileHandler<Widget> {
+
+  methods: string[] = [];
+
+  protected getSaveOptions(widget: Widget, path: string): Promise<IContentsOpts> {
+    this.methods.push('getSaveOptions');
+    return Promise.resolve({ path, content: 'baz', name,
+                             type: 'file', format: 'text' });
+  }
+
+  protected createWidget(path: string): Widget {
+    this.methods.push('createWidget');
+    return new Widget();
+  }
+
+  protected populateWidget(widget: Widget, model: IContentsModel): Promise<IContentsModel> {
+    this.methods.push('populateWidget');
+    return Promise.resolve(model);
+  }
+
+  protected getFetchOptions(path: string): IContentsOpts {
+    this.methods.push('getFetchOptions');
+    return super.getFetchOptions(path);
+  }
+
+  protected getTitleText(path: string): string {
+    this.methods.push('getTitleText');
+    return super.getTitleText(path);
+  }
+
+  protected beforeClose(widget: Widget): Promise<void> {
+    this.methods.push('beforeClose');
+    return super.beforeClose(widget);
+  }
+}
+
+
+describe('jupyter-ui', () => {
+
+  describe('AbstractFileHandler', () => {
+
+    describe('#constructor()', () => {
+
+      it('should accept a contents manager', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        expect(handler instanceof AbstractFileHandler).to.be(true);
+      });
+
+    });
+
+    describe('#opened', () => {
+
+      it('should be emitted when an item is opened', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let called = false;
+        handler.opened.connect((h, widget) => {
+          expect(widget instanceof Widget).to.be(true);
+          called = true;
+        });
+        handler.open('foo.txt');
+        expect(called).to.be(true);
+      });
+
+    });
+
+    describe('#finished', () => {
+
+      it('should be emitted when a widget is populated', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        handler.finished.connect((h, widget) => {
+          expect(widget instanceof Widget).to.be(true);
+          done();
+        });
+        handler.open('foo.txt');
+      });
+
+    });
+
+    describe('#fileExtensions', () => {
+
+      it('should be an empty list by default', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        expect(handler.fileExtensions).to.eql([]);
+      });
+
+      it('should be read only', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        expect(() => { handler.fileExtensions = []; }).to.throwError();
+      });
+
+    });
+
+    describe('#manager', () => {
+
+      it('should be the contents manager used by the handler', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        expect(handler.manager).to.be(manager);
+      });
+
+      it('should be read only', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        expect(() => { handler.manager = null; }).to.throwError();
+      });
+
+    });
+
+    describe('#findWidget()', () => {
+
+      it('should find a widget given a path', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(handler.findWidget('foo.txt')).to.be(widget);
+      });
+
+      it('should return `undefined` if the path is invalid', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(handler.findWidget('bar.txt')).to.be(void 0);
+      });
+
+    });
+
+    describe('#findPath()', () => {
+
+      it('should find a path given a widget', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(handler.findPath(widget)).to.be('foo.txt');
+      });
+
+      it('should return `undefined` if the widget is invalid', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.close('foo.txt').then(() => {
+          expect(handler.findPath(widget)).to.be(void 0);
+          done();
+        });
+      });
+
+    });
+
+    describe('#open()', () => {
+
+      it('should open a file by path and return a widget', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(widget instanceof Widget).to.be(true);
+      });
+
+      it('should return an existing widget if it is already open', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(handler.open('foo.txt')).to.be(widget);
+      });
+
+      it('should clear the dirty state when finished', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.finished.connect(() => {
+          expect(handler.isDirty('foo.txt')).to.be(false);
+          done();
+        });
+      });
+
+      it('should set the title', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(widget.title.text).to.be('foo.txt');
+      });
+
+    });
+
+    describe('#rename()', () => {
+
+      it('should rename the file', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.rename('foo.txt', 'bar.txt');
+        expect(handler.findWidget('bar.txt')).to.be(widget);
+      });
+
+      it('should update the title', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.rename('foo.txt', 'bar.txt');
+        expect(widget.title.text).to.be('bar.txt');
+      });
+
+    });
+
+    describe('#save()', () => {
+
+      it('should resolve to the file contents', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.save('foo.txt').then(contents => {
+          expect(contents.content).to.be('baz');
+          done();
+        });
+      });
+
+      it('should clear the dirty flag', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.setDirty('foo.txt', true);
+        handler.save('foo.txt').then(contents => {
+          expect(handler.isDirty('foo.txt')).to.be(false);
+          done();
+        });
+      });
+
+    });
+
+    describe('#revert()', () => {
+
+      it('should resolve to the original file contents', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.revert('foo.txt').then(contents => {
+          expect(contents.content).to.be('bar');
+          done();
+        });
+      });
+
+      it('should clear the dirty flag', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        handler.setDirty('foo.txt', true);
+        handler.revert('foo.txt').then(contents => {
+          expect(handler.isDirty('foo.txt')).to.be(false);
+          done();
+        });
+      });
+
+    });
+
+    describe('#close()', () => {
+
+      it('should close a file by path', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        widget.attach(document.body);
+        handler.close('foo.txt').then(result => {
+          expect(result).to.be(true);
+          expect(widget.isAttached).to.be(false);
+          done();
+        });
+      });
+
+      it('should return false if the path is invalid', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        handler.close('foo.txt').then(result => {
+          expect(result).to.be(false);
+          done();
+        });
+      });
+
+      it('should prompt the user if the file is dirty', () => {
+        // TODO
+      });
+
+    });
+
+    describe('#closeAll()', () => {
+
+      it('should class all files', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        let widget1 = handler.open('bar.txt');
+        widget0.attach(document.body);
+        handler.closeAll().then(() => {
+          expect(widget0.isAttached).to.be(false);
+          expect(handler.findWidget('bar.txt')).to.be(void 0);
+          done();
+        });
+      });
+
+    });
+
+    describe('#isDirty()', () => {
+
+      it('should default to false', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        expect(handler.isDirty('foo.txt')).to.be(false);
+      });
+
+      it('should return `undefined` if the path is invalid', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        expect(handler.isDirty('bar.txt')).to.be(void 0);
+      });
+
+    });
+
+    describe('#setDirty()', () => {
+
+      it('should set the dirty state of a file', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.setDirty('foo.txt', true);
+        expect(handler.isDirty('foo.txt')).to.be(true);
+        handler.setDirty('foo.txt', false);
+        expect(handler.isDirty('foo.txt')).to.be(false);
+      });
+
+      it('should affect the className of the title', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        expect(widget.title.className.indexOf('jp-mod-dirty')).to.be(-1);
+        handler.setDirty('foo.txt', true);
+        expect(widget.title.className.indexOf('jp-mod-dirty')).to.not.be(-1);
+      });
+
+      it('should be a no-op for an invalid path', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.setDirty('bar.txt', true);
+      });
+
+    });
+
+    describe('#filterMessage()', () => {
+
+      it('should filter close messages for contained widgets', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        let value = handler.filterMessage(widget, Widget.MsgCloseRequest);
+        expect(value).to.be(true);
+        value = handler.filterMessage(widget, Widget.MsgUpdateRequest);
+        expect(value).to.be(false);
+      });
+
+    });
+
+    describe('#getFetchOptions()', () => {
+
+      it('should get the options use to fetch contents from disk', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        expect(handler.methods.indexOf('getFetchOptions')).to.not.be(-1);
+      });
+
+      it('should be called during a revert', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.methods = [];
+        handler.revert('foo.txt');
+        expect(handler.methods.indexOf('getFetchOptions')).to.not.be(-1);
+      });
+
+    });
+
+    describe('#getSaveOptions()', () => {
+
+      it('should get the options used to save the widget', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.save('foo.txt');
+        expect(handler.methods.indexOf('getSaveOptions')).to.not.be(-1);
+      });
+
+    });
+
+    describe('#createWidget()', () => {
+
+      it('should be used to create the initial widget given a path', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        expect(handler.methods.indexOf('createWidget')).to.not.be(-1);
+      });
+
+    });
+
+    describe('#populateWidget()', () => {
+
+      it('should be called to populate a widget while opening', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.finished.connect(() => {
+          expect(handler.methods.indexOf('populateWidget')).to.not.be(-1);
+          done();
+        });
+      });
+
+      it('should be called when reverting', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        let called = false;
+        handler.finished.connect(() => {
+          handler.methods = [];
+          handler.revert('foo.txt').then(() => {
+            expect(handler.methods.indexOf('populateWidget')).to.not.be(-1);
+            done();
+          });
+        });
+      });
+
+    });
+
+    describe('#getTitleText()', () => {
+
+      it('should set the appropriate title text based on a path', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        expect(handler.methods.indexOf('getTitleText')).to.not.be(-1);
+      });
+
+      it('should be called when renaming', () => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget0 = handler.open('foo.txt');
+        handler.methods = [];
+        handler.rename('foo.txt', 'bar.txt');
+        expect(handler.methods.indexOf('getTitleText')).to.not.be(-1);
+      });
+    });
+
+    describe('#beforeClose()', () => {
+
+      it('should call before closing', (done) => {
+        let manager = new MockContentsManager();
+        let handler = new FileHandler(manager);
+        let widget = handler.open('foo.txt');
+        widget.attach(document.body);
+        handler.close('foo.txt').then(result => {
+          expect(result).to.be(true);
+          expect(handler.methods.indexOf('beforeClose')).to.not.be(-1);
+          done();
+        });
+      });
+
+    });
+
+  });
+
+});

+ 1 - 0
test/src/index.ts

@@ -2,5 +2,6 @@
 // Distributed under the terms of the Modified BSD License.
 'use strict';
 
+import './filehandler/filehandler.spec';
 import './renderers/renderers.spec';
 import './rendermime/rendermime.spec';

+ 85 - 0
test/src/mock.ts

@@ -0,0 +1,85 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+'use strict';
+
+import {
+  IContentsModel, IContentsManager, IContentsOpts, ICheckpointModel,
+  IAjaxSettings, ContentsManager
+} from 'jupyter-js-services';
+
+
+export
+class MockContentsManager implements IContentsManager {
+
+  get(path: string, options?: IContentsOpts): Promise<IContentsModel> {
+    return Promise.resolve({
+      name: path.split('/').pop(),
+      path: path,
+      type: 'file',
+      content: 'bar'
+    });
+  }
+
+  newUntitled(path: string, options: IContentsOpts): Promise<IContentsModel> {
+    return Promise.resolve({
+      name: 'untitled',
+      path: `${path}/untitled`,
+      type: 'file',
+      content: 'bar'
+    });
+  }
+
+  delete(path: string): Promise<void> {
+    return Promise.resolve(void 0);
+  }
+
+  rename(path: string, newPath: string): Promise<IContentsModel> {
+    return Promise.resolve({
+      name: newPath.split('/').pop(),
+      path: newPath,
+      type: 'file',
+      content: 'bar'
+    });
+  }
+
+  save(path: string, model: IContentsModel): Promise<IContentsModel> {
+    return Promise.resolve(model);
+  }
+
+  copy(path: string, toDir: string): Promise<IContentsModel> {
+    let name = path.split('/').pop();
+    return Promise.resolve({
+      name,
+      path: `${toDir}/${name}`,
+      type: 'file',
+      content: 'bar'
+    });
+  }
+
+  listContents(path: string): Promise<IContentsModel> {
+    return Promise.resolve({
+      name: path.split('/').pop(),
+      path,
+      type: 'dirty',
+      content: []
+    });
+  }
+
+  createCheckpoint(path: string): Promise<ICheckpointModel> {
+    return Promise.resolve(void 0);
+  }
+
+  listCheckpoints(path: string): Promise<ICheckpointModel[]> {
+    return Promise.resolve(void 0);
+  }
+
+  restoreCheckpoint(path: string, checkpointID: string): Promise<void> {
+    return Promise.resolve(void 0);
+  }
+
+  deleteCheckpoint(path: string, checkpointID: string): Promise<void> {
+    return Promise.resolve(void 0);
+  }
+
+  ajaxSettings: IAjaxSettings = {};
+}

+ 1 - 0
test/src/typings.d.ts

@@ -1,3 +1,4 @@
 /// <reference path="../../typings/expect.js/expect.js.d.ts"/>
 /// <reference path="../../typings/mocha/mocha.d.ts"/>
 /// <reference path="../../typings/require/require.d.ts"/>
+/// <reference path="../../typings/es6-promise/es6-promise.d.ts"/>