소스 검색

Merge pull request #1391 from blink1073/editorwidget-tests

Clean up editor widget and add tests
Steven Silvester 8 년 전
부모
커밋
5bd2d37388
5개의 변경된 파일324개의 추가작업 그리고 66개의 파일을 삭제
  1. 10 7
      examples/filebrowser/src/index.ts
  2. 7 4
      src/editorwidget/plugin.ts
  3. 152 55
      src/editorwidget/widget.ts
  4. 153 0
      test/src/editorwidget/widget.spec.ts
  5. 2 0
      test/src/index.ts

+ 10 - 7
examples/filebrowser/src/index.ts

@@ -95,13 +95,16 @@ function createApp(manager: ServiceManager.IManager): void {
     factory: new CodeMirrorEditorFactory(),
     mimeTypeService: new CodeMirrorMimeTypeService()
   };
-  let wFactory = new EditorWidgetFactory(editorServices, {
-    name: 'Editor',
-    modelName: 'text',
-    fileExtensions: ['*'],
-    defaultFor: ['*'],
-    preferKernel: false,
-    canStartKernel: true
+  let wFactory = new EditorWidgetFactory({
+    editorServices,
+    factoryOptions: {
+      name: 'Editor',
+      modelName: 'text',
+      fileExtensions: ['*'],
+      defaultFor: ['*'],
+      preferKernel: false,
+      canStartKernel: true
+    }
   });
   docRegistry.addModelFactory(mFactory);
   docRegistry.addWidgetFactory(wFactory);

+ 7 - 4
src/editorwidget/plugin.ts

@@ -79,10 +79,13 @@ const plugin: JupyterLabPlugin<IEditorTracker> = {
  * Sets up the editor widget
  */
 function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB, layout: ILayoutRestorer, editorServices: IEditorServices): IEditorTracker {
-  const factory = new EditorWidgetFactory(editorServices, {
-    name: FACTORY,
-    fileExtensions: ['*'],
-    defaultFor: ['*']
+  const factory = new EditorWidgetFactory({
+    editorServices,
+    factoryOptions: {
+      name: FACTORY,
+      fileExtensions: ['*'],
+      defaultFor: ['*']
+    }
   });
   const tracker = new InstanceTracker<EditorWidget>({
     restore: {

+ 152 - 55
src/editorwidget/widget.ts

@@ -9,6 +9,10 @@ import {
   IInstanceTracker
 } from '../common/instancetracker';
 
+import {
+  IChangedArgs
+} from '../common/interfaces';
+
 import {
   ABCWidgetFactory, DocumentRegistry
 } from '../docregistry';
@@ -61,89 +65,182 @@ class EditorWidget extends CodeEditorWidget {
   /**
    * Construct a new editor widget.
    */
-  constructor(
-    editorFactory: (host: Widget) => CodeEditor.IEditor,
-    context: DocumentRegistry.Context,
-    editorMimeTypeService: IEditorMimeTypeService) {
-    super(editorFactory);
+  constructor(options: EditorWidget.IOptions) {
+    super(options.factory);
     this.addClass(EDITOR_CLASS);
-    this._context = context;
-    let model = context.model;
+    let context = this._context = options.context;
+    this._mimeTypeService = options.mimeTypeService;
+    this.editor.model.value.text = context.model.toString();
+    this._onPathChanged();
+    context.pathChanged.connect(this._onPathChanged, this);
+    context.ready.then(() => {
+      this._onContextReady();
+    });
+  }
+
+  /**
+   * Get the context for the editor widget.
+   */
+  get context(): DocumentRegistry.Context {
+    return this._context;
+  }
+
+  /**
+   * Handle actions that should be taken when the context is ready.
+   */
+  private _onContextReady(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    let model = this._context.model;
     let editor = this.editor;
     let value = editor.model.value;
+    value.text = model.toString();
 
     // Prevent the initial loading from disk from being in the editor history.
-    context.ready.then( () => {
-      value.text = model.toString();
-      editor.model.clearHistory();
-    });
+    editor.model.clearHistory();
+    this._handleDirtyState();
 
-    value.text = model.toString();
+    model.stateChanged.connect(this._onModelStateChanged, this);
+    model.contentChanged.connect(this._onContentChanged, this);
+    value.changed.connect(this._onValueChanged, this);
+  }
 
-    this.title.label = context.path.split('/').pop();
-    model.stateChanged.connect((m, args) => {
-      if (args.name === 'dirty') {
-        if (args.newValue) {
-          this.title.className += ` ${DIRTY_CLASS}`;
-        } else {
-          this.title.className = this.title.className.replace(DIRTY_CLASS, '');
-        }
-      }
-    });
-    model.contentChanged.connect(() => {
-      let old = value.text;
-      let text = model.toString();
-      if (old !== text) {
-        value.text = text;
-      }
-    });
-    this.editor.model.value.changed.connect((sender, args) => {
-      model.fromString(value.text);
-    });
-    editor.model.mimeType = editorMimeTypeService.getMimeTypeByFilePath(context.path);
-    context.pathChanged.connect((c, path) => {
-      editor.model.mimeType = editorMimeTypeService.getMimeTypeByFilePath(path);
-      this.title.label = path.split('/').pop();
-    });
+  /**
+   * Handle a change to the model state.
+   */
+  private _onModelStateChanged(sender: DocumentRegistry.IModel, args: IChangedArgs<any>): void {
+    if (args.name === 'dirty') {
+      this._handleDirtyState();
+    }
+  }
 
-    // TODO disconnect on deactivation
+  /**
+   * Handle the dirty state of the model.
+   */
+  private _handleDirtyState(): void {
+    if (this._context.model.dirty) {
+      this.title.className += ` ${DIRTY_CLASS}`;
+    } else {
+      this.title.className = this.title.className.replace(DIRTY_CLASS, '');
+    }
   }
 
   /**
-   * Get the context for the editor widget.
+   * Handle a change in model content.
    */
-  get context(): DocumentRegistry.Context {
-    return this._context;
+  private _onContentChanged(): void {
+    let value = this.editor.model.value;
+    let old = value.text;
+    let text = this._context.model.toString();
+    if (old !== text) {
+      value.text = text;
+    }
+  }
+
+  /**
+   * Handle a change in the editor model value.
+   */
+  private _onValueChanged(): void {
+    this._context.model.fromString(this.editor.model.value.text);
+  }
+
+  /**
+   * Handle a change to the path.
+   */
+  private _onPathChanged(): void {
+    let editor = this.editor;
+    let path = this._context.path;
+    editor.model.mimeType = this._mimeTypeService.getMimeTypeByFilePath(path);
+    this.title.label = path.split('/').pop();
   }
 
   protected _context: DocumentRegistry.Context;
+  private _mimeTypeService: IEditorMimeTypeService;
 }
 
+
+/**
+ * The namespace for editor widget statics.
+ */
+export
+namespace EditorWidget {
+  /**
+   * The options used to create an editor widget.
+   */
+  export
+  interface IOptions {
+    /**
+     * The editor factory used to create the editor.
+     */
+    factory: (host: Widget) => CodeEditor.IEditor;
+
+    /**
+     * The mime type service for the editor.
+     */
+    mimeTypeService: IEditorMimeTypeService;
+
+    /**
+     * The document context associated with the editor.
+     */
+    context: DocumentRegistry.Context;
+  }
+}
+
+
 /**
  * A widget factory for editors.
  */
 export
 class EditorWidgetFactory extends ABCWidgetFactory<EditorWidget, DocumentRegistry.IModel> {
-
-  constructor(editorServices: IEditorServices, options: DocumentRegistry.IWidgetFactoryOptions) {
-    super(options);
-    this._editorServices = editorServices;
+  /**
+   * Construct a new editor widget factory.
+   */
+  constructor(options: EditorWidgetFactory.IOptions) {
+    super(options.factoryOptions);
+    this._mimeTypeService = options.editorServices.mimeTypeService;
+    let factory = options.editorServices.factory;
+    this._factory = (host: Widget) => factory.newDocumentEditor(host.node, {
+      lineNumbers: true,
+      readOnly: false,
+      wordWrap: true
+    });
   }
 
   /**
    * Create a new widget given a context.
    */
   protected createNewWidget(context: DocumentRegistry.Context): EditorWidget {
-    const { factory, mimeTypeService } = this._editorServices;
-    return new EditorWidget((host: Widget) => {
-      let editor = factory.newDocumentEditor(host.node, {
-          lineNumbers: true,
-          readOnly: false,
-          wordWrap: true,
-      });
-      return editor;
-    }, context, mimeTypeService);
+    return new EditorWidget({
+      factory: this._factory,
+      context,
+      mimeTypeService: this._mimeTypeService
+    });
   }
 
-  private _editorServices: IEditorServices;
+  private _mimeTypeService: IEditorMimeTypeService;
+  private _factory: (host: Widget) => CodeEditor.IEditor;
+}
+
+
+/**
+ * The namespace for `EditorWidgetFactory` class statics.
+ */
+export
+namespace EditorWidgetFactory {
+  /**
+   * The options used to create an editor widget factory.
+   */
+  export
+  interface IOptions {
+    /**
+     * The editor services used by the factory.
+     */
+    editorServices: IEditorServices;
+
+    /**
+     * The factory options associated with the factory.
+     */
+    factoryOptions: DocumentRegistry.IWidgetFactoryOptions;
+  }
 }

+ 153 - 0
test/src/editorwidget/widget.spec.ts

@@ -0,0 +1,153 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  ServiceManager, utils
+} from '@jupyterlab/services';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  CodeMirrorEditorFactory, CodeMirrorMimeTypeService
+} from '../../../lib/codemirror';
+
+import {
+  Context, DocumentRegistry, TextModelFactory
+} from '../../../lib/docregistry';
+
+import {
+  EditorWidget, EditorWidgetFactory
+} from '../../../lib/editorwidget';
+
+
+describe('editorwidget', () => {
+
+  let factory = new CodeMirrorEditorFactory();
+  let modelFactory = new TextModelFactory();
+  let mimeTypeService = new CodeMirrorMimeTypeService();
+  let context: DocumentRegistry.Context;
+  let manager: ServiceManager.IManager;
+
+  before((done) => {
+    manager = new ServiceManager();
+    manager.ready.then(done, done);
+  });
+
+  describe('EditorWidget', () => {
+
+    let widget: EditorWidget;
+
+    beforeEach(() => {
+      let path = utils.uuid() + '.py';
+      context = new Context({ manager, factory: modelFactory, path });
+      widget = new EditorWidget({
+        factory: (host: Widget) => factory.newDocumentEditor(host.node, {}),
+        mimeTypeService,
+        context
+      });
+    });
+
+    afterEach(() => {
+      widget.dispose();
+    });
+
+    describe('#constructor()', () => {
+
+      it('should create an editor widget', () => {
+        expect(widget).to.be.an(EditorWidget);
+      });
+
+      it('should update the editor text when the model changes', (done) => {
+        context.save().catch(done);
+        context.ready.then(() => {
+          widget.context.model.fromString('foo');
+          expect(widget.editor.model.value.text).to.be('foo');
+        }).then(done, done);
+      });
+
+      it('should set the mime type for the path', () => {
+        expect(widget.editor.model.mimeType).to.be('text/x-python');
+      });
+
+      it('should update the mime type when the path changes', (done) => {
+        context.pathChanged.connect((sender, args) => {
+          expect(widget.editor.model.mimeType).to.be('text/x-julia');
+          done();
+        });
+        context.save().then(() => {
+          return manager.contents.rename(context.path, utils.uuid() + '.jl');
+        }).catch(done);
+      });
+
+      it('should set the title for the path', () => {
+        expect(widget.title.label).to.be(context.path);
+      });
+
+      it('should add the dirty class when the model is dirty', (done) => {
+        context.save().catch(done);
+        context.ready.then(() => {
+          context.model.fromString('bar');
+          expect(widget.title.className).to.contain('jp-mod-dirty');
+        }).then(done, done);
+      });
+
+      it('should update the title when the path changes', (done) => {
+        let path = utils.uuid() + '.jl';
+        context.pathChanged.connect((sender, args) => {
+          expect(widget.title.label).to.be(path);
+          done();
+        });
+        context.save().then(() => {
+          return manager.contents.rename(context.path, path);
+        }).catch(done);
+      });
+
+    });
+
+    describe('#context', () => {
+
+      it('should be the context used by the widget', () => {
+        expect(widget.context).to.be(context);
+      });
+
+    });
+
+  });
+
+  describe('EditorWidgetFactory', () => {
+
+    let widgetFactory = new EditorWidgetFactory({
+      editorServices: {
+        factory,
+        mimeTypeService
+      },
+      factoryOptions: {
+        name: 'editor',
+        fileExtensions: ['*'],
+        defaultFor: ['*']
+      }
+    });
+
+    describe('#constructor()', () => {
+
+      it('should create an EditorWidgetFactory', () => {
+        expect(widgetFactory).to.be.an(EditorWidgetFactory);
+      });
+
+    });
+
+    describe('#createNewWidget()', () => {
+
+      it('should create an editor widget', () => {
+        expect(widgetFactory.createNew(context)).to.be.an(EditorWidget);
+      });
+
+    });
+
+  });
+
+});

+ 2 - 0
test/src/index.ts

@@ -35,6 +35,8 @@ import './docregistry/context.spec';
 import './docregistry/default.spec';
 import './docregistry/registry.spec';
 
+import './editorwidget/widget.spec';
+
 import './filebrowser/crumbs.spec';
 import './filebrowser/model.spec';