Browse Source

wip notebook model tests

Steven Silvester 8 years ago
parent
commit
60051e2222
5 changed files with 207 additions and 567 deletions
  1. 1 1
      package.json
  2. 1 1
      src/notebook/common/undo.ts
  3. 15 81
      src/notebook/notebook/model.ts
  4. 1 0
      test/src/index.ts
  5. 189 484
      test/src/notebook/notebook/model.spec.ts

+ 1 - 1
package.json

@@ -78,7 +78,7 @@
     "test:firefox": "karma start --browsers=Firefox test/karma.conf.js",
     "test:ie": "karma start --browsers=IE test/karma.conf.js",
     "watch": "watch 'npm run build' src --wait 10",
-    "watch:test": "watch 'npm run build && npm test' src src/test --wait 10"
+    "watch:test": "watch 'npm run build && npm test' src test/src --wait 10"
   },
   "repository": {
     "type": "git",

+ 1 - 1
src/notebook/common/undo.ts

@@ -22,7 +22,7 @@ interface IJSONable {
  * An observable list that supports undo/redo.
  */
 export
-class OberservableUndoableList<T extends IJSONable> extends ObservableList<T> {
+class ObservableUndoableList<T extends IJSONable> extends ObservableList<T> {
   /**
    * Construct a new undoable observable list.
    */

+ 15 - 81
src/notebook/notebook/model.ts

@@ -20,6 +20,9 @@ import {
   ISignal, Signal, clearSignalData
 } from 'phosphor-signaling';
 
+import {
+  DocumentModel
+} from '../../docmanager/default';
 
 import {
   ICellModel, CodeCellModel, RawCellModel, MarkdownCellModel
@@ -34,7 +37,7 @@ import {
 } from '../common/metadata';
 
 import {
-  OberservableUndoableList
+  ObservableUndoableList
 } from '../common/undo';
 
 import {
@@ -63,7 +66,7 @@ interface INotebookModel extends IDocumentModel {
    * #### Notes
    * This is a read-only property.
    */
-  cells: OberservableUndoableList<ICellModel>;
+  cells: ObservableUndoableList<ICellModel>;
 
   /**
    * The major version number of the nbformat.
@@ -138,12 +141,13 @@ interface INotebookModel extends IDocumentModel {
  * An implementation of a notebook Model.
  */
 export
-class NotebookModel implements INotebookModel {
+class NotebookModel extends DocumentModel implements INotebookModel {
   /**
    * Construct a new notebook model.
    */
   constructor(languagePreference?: string) {
-    this._cells = new OberservableUndoableList<ICellModel>((data: nbformat.IBaseCell) => {
+    super(languagePreference);
+    this._cells = new ObservableUndoableList<ICellModel>((data: nbformat.IBaseCell) => {
       switch (data.cell_type) {
         case 'code':
           return this.createCodeCell(data);
@@ -155,24 +159,10 @@ class NotebookModel implements INotebookModel {
     });
     this._cells.changed.connect(this.onCellsChanged, this);
     if (languagePreference) {
-      this._metadata['language_info'] = `{"name":"${languagePreference}"}`;
+      this._metadata['language_info'] = { name: languagePreference };
     }
   }
 
-  /**
-   * A signal emitted when the document content changes.
-   */
-  get contentChanged(): ISignal<INotebookModel, void> {
-    return Private.contentChangedSignal.bind(this);
-  }
-
-  /**
-   * A signal emitted when a model state changes.
-   */
-  get stateChanged(): ISignal<IDocumentModel, IChangedArgs<any>> {
-    return Private.stateChangedSignal.bind(this);
-  }
-
   /**
    * A signal emitted when a metadata field changes.
    */
@@ -186,7 +176,7 @@ class NotebookModel implements INotebookModel {
    * #### Notes
    * This is a read-only property.
    */
-  get cells(): OberservableUndoableList<ICellModel> {
+  get cells(): ObservableUndoableList<ICellModel> {
     return this._cells;
   }
 
@@ -210,40 +200,6 @@ class NotebookModel implements INotebookModel {
     return this._nbformatMinor;
   }
 
-  /**
-   * The dirty state of the model.
-   *
-   * #### Notes
-   * This should be cleared when the document is loaded from
-   * or saved to disk.
-   */
-  get dirty(): boolean {
-    return this._dirty;
-  }
-  set dirty(newValue: boolean) {
-    if (newValue === this._dirty) {
-      return;
-    }
-    let oldValue = this._dirty;
-    this._dirty = newValue;
-    this.stateChanged.emit({ name: 'dirty', oldValue, newValue });
-  }
-
-  /**
-   * The read-only state of the model.
-   */
-  get readOnly(): boolean {
-    return this._readOnly;
-  }
-  set readOnly(newValue: boolean) {
-    if (newValue === this._readOnly) {
-      return;
-    }
-    let oldValue = this._readOnly;
-    this._readOnly = newValue;
-    this.stateChanged.emit({ name: 'readOnly', oldValue, newValue });
-  }
-
   /**
    * The default kernel name of the document.
    *
@@ -266,16 +222,6 @@ class NotebookModel implements INotebookModel {
     return info ? info.name : '';
   }
 
-  /**
-   * Get whether the model is disposed.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get isDisposed(): boolean {
-    return this._cells === null;
-  }
-
   /**
    * Dispose of the resources held by the model.
    */
@@ -298,6 +244,7 @@ class NotebookModel implements INotebookModel {
     }
     this._cursors = null;
     this._metadata = null;
+    super.dispose();
   }
 
   /**
@@ -506,6 +453,7 @@ class NotebookModel implements INotebookModel {
     default:
       return;
     }
+    this.contentChanged.emit(void 0);
     this.dirty = true;
   }
 
@@ -531,10 +479,8 @@ class NotebookModel implements INotebookModel {
     this.metadataChanged.emit({ name, oldValue, newValue });
   }
 
-  private _cells: OberservableUndoableList<ICellModel> = null;
+  private _cells: ObservableUndoableList<ICellModel> = null;
   private _metadata: { [key: string]: any } = Private.createMetadata();
-  private _dirty = false;
-  private _readOnly = false;
   private _cursors: { [key: string]: MetadataCursor } = Object.create(null);
   private _nbformat = nbformat.MAJOR_VERSION;
   private _nbformatMinor = nbformat.MINOR_VERSION;
@@ -545,18 +491,6 @@ class NotebookModel implements INotebookModel {
  * A private namespace for notebook model data.
  */
 namespace Private {
-  /**
-   * A signal emitted when the content of the model changes.
-   */
-  export
-  const contentChangedSignal = new Signal<INotebookModel, void>();
-
-  /**
-   * A signal emitted when a model state changes.
-   */
-  export
-  const stateChangedSignal = new Signal<IDocumentModel, IChangedArgs<any>>();
-
   /**
    * A signal emitted when a metadata field changes.
    */
@@ -569,8 +503,8 @@ namespace Private {
   export
   function createMetadata(): nbformat.INotebookMetadata {
     return {
-      kernelspec: { name: 'unknown', display_name: 'Unknown' },
-      language_info: { name: 'unknown' },
+      kernelspec: { name: '', display_name: '' },
+      language_info: { name: '' },
       orig_nbformat: 1
     };
   }

+ 1 - 0
test/src/index.ts

@@ -7,3 +7,4 @@ import './rendermime/rendermime.spec';
 import './renderers/latex.spec';
 
 import './notebook/notebook/nbformat.spec';
+import './notebook/notebook/model.spec';

+ 189 - 484
test/src/notebook/notebook/model.spec.ts

@@ -3,103 +3,87 @@
 
 import expect = require('expect.js');
 
-import {
-  INotebookSession
-} from 'jupyter-js-services';
-
 import {
   ObservableList, IListChangedArgs
 } from 'phosphor-observablelist';
 
 import {
-  IChangedArgs
-} from 'phosphor-properties';
-
-import {
-  EditorModel, IEditorModel
-} from '../../../../lib/notebook/editor/model';
+  ICellModel
+} from '../../../../lib/notebook/cells/model';
 
 import {
-  InputAreaModel
-} from '../../../../lib/notebook/input-area/model';
+  JSONObject, deepEqual
+} from '../../../../lib/notebook/common/json';
 
 import {
-  OutputAreaModel
-} from '../../../../lib/notebook/output-area/model';
+  ObservableUndoableList
+} from '../../../../lib/notebook/common/undo';
 
 import {
-  BaseCellModel, CodeCellModel, MarkdownCellModel, MetadataCursor,
-  RawCellModel, ICellModel
-} from '../../../../lib/notebook/cells/model';
+  nbformat
+} from '../../../../lib/notebook/notebook/nbformat';
 
 import {
   NotebookModel
 } from '../../../../lib/notebook/notebook/model';
 
-import {
-  MockSession
-} from 'jupyter-js-services/lib/mocksession';
 
-import {
-  MockKernel
-} from 'jupyter-js-services/lib/mockkernel';
+const DEFAULT_CONTENT: nbformat.INotebookContent = require('../../../../examples/notebook/test.ipynb') as nbformat.INotebookContent;
 
 
 /**
  * A notebook model which tests protected methods.
  */
-class MyNotebookModel extends NotebookModel {
+class LogNotebookModel extends NotebookModel {
   methods: string[] = [];
 
-  protected onEditorChanged(editor: IEditorModel, args: IChangedArgs<any>): void {
-    super.onEditorChanged(editor, args);
-    this.methods.push('onEditorChanged');
+  protected onCellChanged(cell: ICellModel, change: any): void {
+    super.onCellChanged(cell, change);
+    this.methods.push('onCellsChanged');
   }
 
   protected onCellsChanged(list: ObservableList<ICellModel>, change: IListChangedArgs<ICellModel>): void {
     super.onCellsChanged(list, change);
     this.methods.push('onCellsChanged');
   }
+
+  protected setCursorData(name: string, newValue: any): void {
+    super.setCursorData(name, newValue);
+    this.methods.push('setCursorData');
+  }
 }
 
 
-describe('jupyter-js-notebook', () => {
+describe('notebook/notebook', () => {
 
   describe('NotebookModel', () => {
 
     describe('#constructor()', () => {
 
-      it('should create an notebook model', () => {
+      it('should create a notebook model', () => {
         let model = new NotebookModel();
-        expect(model instanceof NotebookModel).to.be(true);
+        expect(model).to.be.a(NotebookModel);
       });
 
-    });
-
-    describe('#stateChanged', () => {
-
-      it('should be emitted when the state changes', () => {
-        let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('readOnly');
-          expect(change.oldValue).to.be(false);
-          expect(change.newValue).to.be(true);
-          called = true;
-        });
-        model.readOnly = true;
-        expect(called).to.be(true);
+      it('should accept an optional language preference', () => {
+        let model = new NotebookModel('python');
+        let cursor = model.getMetadata('language_info');
+        let lang = cursor.getValue() as nbformat.ILanguageInfoMetadata;
+        expect(lang.name).to.be('python');
       });
 
     });
 
     describe('#metadataChanged', () => {
 
-      it ('should be emitted when metadata changes', () => {
+      it('should be emitted when a metadata field changes', () => {
         let model = new NotebookModel();
         let called = false;
-        model.metadataChanged.connect((cell, name) => {
-          expect(name).to.be('foo');
+        model.metadataChanged.connect((sender, args) => {
+          expect(sender).to.be(model);
+          expect(args.name).to.be('foo');
+          expect(args.oldValue).to.be(void 0);
+          expect(args.newValue).to.be(1);
           called = true;
         });
         let foo = model.getMetadata('foo');
@@ -107,349 +91,212 @@ describe('jupyter-js-notebook', () => {
         expect(called).to.be(true);
       });
 
-    });
-
-    describe('#cells', () => {
-
-      it('should be an observable list', () => {
-        let model = new NotebookModel();
-        expect(model.cells instanceof ObservableList).to.be(true);
-      });
-
-      it('should be read-only', () => {
+      it('should not be emitted when the value does not change', () => {
         let model = new NotebookModel();
-        expect(() => { model.cells = null; }).to.throwError();
+        let called = false;
+        let foo = model.getMetadata('foo');
+        foo.setValue(1);
+        model.metadataChanged.connect(() => { called = true; });
+        foo.setValue(1);
+        expect(called).to.be(false);
       });
 
     });
 
-    describe('#defaultMimetype', () => {
+    describe('#cells', () => {
 
-      it('should default to `text/x-ipython`', () => {
+      it('should be an observable undoable list', () => {
         let model = new NotebookModel();
-        expect(model.defaultMimetype).to.be('text/x-ipython');
+        expect(model.cells).to.be.an(ObservableUndoableList);
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should default to an empty list', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('defaultMimetype');
-          expect(change.oldValue).to.be('text/x-ipython');
-          expect(change.newValue).to.be('text/python');
-          called = true;
-        });
-        model.defaultMimetype = 'text/python';
-        expect(called).to.be(true);
+        expect(model.cells.length).to.be(0);
       });
 
-    });
-
-    describe('#readOnly', () => {
-
-      it('should default to false', () => {
+      it('should be reset when loading from disk', () => {
         let model = new NotebookModel();
-        expect(model.readOnly).to.be(false);
+        let cell = model.createCodeCell();
+        model.cells.add(cell);
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.cells.indexOf(cell)).to.be(-1);
+        expect(model.cells.length).to.be(6);
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should be read-only', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('readOnly');
-          expect(change.oldValue).to.be(false);;
-          expect(change.newValue).to.be(true);
-          called = true;
-        });
-        model.readOnly = true;
-        expect(called).to.be(true);
+        expect(() => { model.cells = null; }).to.throwError();
       });
 
     });
 
-    describe('#session', () => {
+    describe('#nbformat', () => {
 
-      it('should default to null', () => {
+      it('should get the major version number of the nbformat', () => {
         let model = new NotebookModel();
-        expect(model.session).to.be(null);
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.nbformat).to.be(DEFAULT_CONTENT.nbformat);
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should be read-only', () => {
         let model = new NotebookModel();
-        let called = false;
-        let session = new MockSession('test.ipynb');
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('session');
-          expect(change.oldValue).to.be(null);
-          expect(change.newValue).to.be(session);
-          called = true;
-        });
-        model.session = session;
-        expect(called).to.be(true);
+        expect(() => { model.nbformat = 0; }).to.throwError();
       });
 
     });
 
-    describe('#kernelspec', () => {
+    describe('#nbformatMinor', () => {
 
-      it('should default to an unknown kernel', () => {
+      it('should get the minor version number of the nbformat', () => {
         let model = new NotebookModel();
-        expect(model.kernelspec.name).to.be('unknown');
-        expect(model.kernelspec.display_name).to.be('No Kernel!');
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.nbformatMinor).to.be(DEFAULT_CONTENT.nbformat_minor);
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should be read-only', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('kernelspec');
-          expect(change.oldValue.name).to.be('unknown');;
-          expect(change.newValue.name).to.be('python');
-          called = true;
-        });
-        model.kernelspec = { name: 'python', display_name: 'Python' };
-        expect(called).to.be(true);
+        expect(() => { model.nbformatMinor = 0; }).to.throwError();
       });
 
     });
 
-    describe('#languageInfo', () => {
+    describe('#defaultKernelName()', () => {
 
-      it('should default to an unknown language', () => {
+      it('should get the default kernel name of the document', () => {
         let model = new NotebookModel();
-        expect(model.languageInfo.name).to.be('unknown');
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.defaultKernelName).to.be('python3');
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should default to an empty string', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('languageInfo');
-          expect(change.oldValue.name).to.be('unknown');;
-          expect(change.newValue.name).to.be('python');
-          called = true;
-        });
-        model.languageInfo = { name: 'python' };
-        expect(called).to.be(true);
+        expect(model.defaultKernelName).to.be('');
       });
 
-    });
-
-    describe('#origNbformat', () => {
-
-      it('should default to null', () => {
-        let model = new NotebookModel();
-        expect(model.origNbformat).to.be(null);
-      });
-
-      it('should emit a stateChanged signal when changed', () => {
+      it('should be read-only', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('origNbformat');
-          expect(change.oldValue).to.be(null);
-          expect(change.newValue).to.be(4);
-          called = true;
-        });
-        model.origNbformat = 4;
-        expect(called).to.be(true);
+        expect(() => { model.defaultKernelName = ''; }).to.throwError();
       });
 
     });
 
-    describe('#activeCellIndex', () => {
+    describe('#defaultKernelLanguage', () => {
 
-      it('should default to null', () => {
+      it('should get the default kernel language of the document', () => {
         let model = new NotebookModel();
-        expect(model.activeCellIndex).to.be(null);
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.defaultKernelLanguage).to.be('python');
       });
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should default to an empty string', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('activeCellIndex');
-          expect(change.oldValue).to.be(null);
-          expect(change.newValue).to.be(0);
-          called = true;
-        });
-        model.cells.add(model.createMarkdownCell());
-        model.activeCellIndex = 0;
-        expect(called).to.be(true);
+        expect(model.defaultKernelLanguage).to.be('');
       });
 
-      it('should be clamped to the length of the cells list', () => {
-        let model = new NotebookModel();
-        model.cells.add(model.createMarkdownCell());
-        model.cells.add(model.createMarkdownCell());
-        model.activeCellIndex = -1;
-        expect(model.activeCellIndex).to.be(0);
-        model.activeCellIndex = 2;
-        expect(model.activeCellIndex).to.be(1);
+      it('should be set from the constructor arg', () => {
+        let model = new NotebookModel('foo');
+        expect(model.defaultKernelLanguage).to.be('foo');
       });
 
-
-      it('should unrender a markdown cell if in edit mode', () => {
-        let model = new NotebookModel();
-        let cell0 = model.createMarkdownCell();
-        let cell1 = model.createMarkdownCell();
-        model.cells.add(cell0);
-        model.cells.add(cell1);
-        model.mode = 'edit';
-        debugger;
-        expect(cell0.rendered).to.be(true);
-        expect(cell1.rendered).to.be(false);
-        model.activeCellIndex = 0;
-        expect(cell0.rendered).to.be(false);
-      });
-
-    });
-
-    describe('#mode', () => {
-
-      it('should default to command', () => {
-        let model = new NotebookModel();
-        expect(model.mode).to.be('command');
-      });
-
-      it('should emit a stateChanged signal when changed', () => {
+      it('should be read-only', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('mode');
-          expect(change.oldValue).to.be('command');
-          expect(change.newValue).to.be('edit');
-          called = true;
-        });
-        model.mode = 'edit';
-        expect(called).to.be(true);
+        expect(() => { model.defaultKernelLanguage = ''; }).to.throwError();
       });
 
     });
 
-    describe('#dirty', () => {
-
-      it('should default to false', () => {
-        let model = new NotebookModel();
-        expect(model.dirty).to.be(false);
-      });
+    describe('#dispose()', () => {
 
-      it('should emit a stateChanged signal when changed', () => {
+      it('should dispose of the resources held by the model', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect((nb, change) => {
-          expect(change.name).to.be('dirty');
-          expect(change.oldValue).to.be(false);
-          expect(change.newValue).to.be(true);
-          called = true;
-        });
-        model.dirty = true;
-        expect(called).to.be(true);
+        model.fromJSON(DEFAULT_CONTENT);
+        model.dispose();
+        expect(model.cells).to.be(null);
+        expect(model.isDisposed).to.be(true);
       });
 
-    });
-
-    describe('#isDisposed', () => {
-
-      it('should indicate whether the model is disposed', () => {
+      it('should be safe to call multiple times', () => {
         let model = new NotebookModel();
-        expect(model.isDisposed).to.be(false);
+        model.dispose();
         model.dispose();
         expect(model.isDisposed).to.be(true);
       });
 
     });
 
-    describe('#dispose()', () => {
+    describe('#toString()', () => {
 
-      it('should dispose of the resource held by the model', () => {
+      it('should serialize the model to a string', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.stateChanged.connect(() => { called = true; });
-        model.dispose();
-        model.dirty = true;
-        expect(called).to.be(false);
-        expect(model.cells).to.be(null);
-      });
-
-      it('should be safe to call multiple times', () => {
-        let model = new NotebookModel();
-        model.dispose();
-        model.dispose();
+        model.fromJSON(DEFAULT_CONTENT);
+        let text = model.toString();
+        let data = JSON.parse(text);
+        // TODO: use JSON types in services then deepEqual here.
+        expect(data.cells[0]).to.eql(DEFAULT_CONTENT.cells[0]);
       });
 
     });
 
-    describe('#metadataChanged', () => {
+    describe('#fromString()', () => {
 
-      it('should be emitted when metadata changes', () => {
+      it('should deserialize the model from a string', () => {
         let model = new NotebookModel();
-        let called = false;
-        model.metadataChanged.connect((cell, name) => {
-          expect(name).to.be('foo');
-          called = true;
-        });
-        let foo = model.getMetadata('foo');
-        foo.setValue(1);
-        expect(called).to.be(true);
+        model.fromString(JSON.stringify(DEFAULT_CONTENT));
+        expect(model.cells.length).to.be(6);
       });
 
-      it('should throw an error on blacklisted names', () => {
+      it('should set the dirty flag', () => {
         let model = new NotebookModel();
-        let invalid = ['kernelspec', 'languageInfo', 'origNbformat'];
-        for (let key of invalid) {
-          expect(() => { model.getMetadata(key); }).to.throwError();
-        }
+        model.dirty = false;
+        model.fromString(JSON.stringify(DEFAULT_CONTENT));
+        expect(model.dirty).to.be(true);
       });
 
     });
 
-    describe('#select()', () => {
+    describe('#toJSON()', () => {
 
-      it('should select a cell', () => {
+      it('should serialize the model to JSON', () => {
         let model = new NotebookModel();
-        let cell = model.createMarkdownCell();
-        model.cells.add(cell);
-        model.cells.add(model.createCodeCell())
-        expect(model.isSelected(cell)).to.be(false);
-        model.select(cell);
-        expect(model.isSelected(cell)).to.be(true);
+        model.fromJSON(DEFAULT_CONTENT);
+        let data = model.toJSON();
+        // TODO: use JSON types in services then deepEqual here.
+        expect(data.cells[0]).to.eql(DEFAULT_CONTENT.cells[0]);
       });
 
     });
 
-    describe('#deselect()', () => {
+    describe('#fromJSON()', () => {
 
-      it('should deselect a cell', () => {
+      it('should serialize the model from JSON', () => {
         let model = new NotebookModel();
-        let cell = model.createCodeCell();
-        model.cells.add(cell);
-        model.cells.add(model.createCodeCell());
-        model.select(cell);
-        expect(model.isSelected(cell)).to.be(true);
-        model.deselect(cell);
-        expect(model.isSelected(cell)).to.be(false);
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.cells.length).to.be(6);
+        expect(model.nbformat).to.be(DEFAULT_CONTENT.nbformat);
+        expect(model.nbformatMinor).to.be(DEFAULT_CONTENT.nbformat_minor);
       });
 
-      it('should have no effect on the active cell', () => {
+      it('should set the dirty flag', () => {
         let model = new NotebookModel();
-        let cell = model.createCodeCell();
-        model.cells.add(cell);
-        model.deselect(cell);
-        expect(model.isSelected(cell)).to.be(true);
+        model.dirty = false;
+        model.fromJSON(DEFAULT_CONTENT);
+        expect(model.dirty).to.be(true);
       });
 
     });
 
-    describe('#isSelected()', () => {
+    describe('#initialize()', () => {
 
-      it('should indicate whether a cell is selected', () => {
+      it('should initialize the model state', () => {
         let model = new NotebookModel();
         let cell = model.createCodeCell();
         model.cells.add(cell);
-        expect(model.isSelected(cell)).to.be(true);
-        model.cells.add(model.createMarkdownCell());
-        expect(model.isSelected(cell)).to.be(false);
+        expect(model.dirty).to.be(true);
+        expect(model.cells.canUndo).to.be(true);
+        model.initialize();
+        expect(model.dirty).to.be(false);
+        expect(model.cells.canUndo).to.be(false);
       });
 
     });
@@ -459,78 +306,23 @@ describe('jupyter-js-notebook', () => {
       it('should create a new code cell', () => {
         let model = new NotebookModel();
         let cell = model.createCodeCell();
-        expect(cell instanceof CodeCellModel).to.be(true);
+        expect(cell.type).to.be('code');
       });
 
-      it('should clone a code cell model', () => {
+      it('should clone an existing code cell', () => {
         let model = new NotebookModel();
         let cell = model.createCodeCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        cell.collapsed = true;
-        cell.scrolled = true;
-        cell.output.outputs.add({
-          output_type: 'error',
-          ename: 'foo',
-          evalue: '',
-          traceback: ['']
-        });
-        let newCell = model.createCodeCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
-        expect(newCell.collapsed).to.be(true);
-        expect(newCell.scrolled).to.be(true);
-        expect(newCell.output.outputs.length).to.be(1);
-      });
-
-      it('should clone from a markdown cell model', () => {
-        let model = new NotebookModel();
-        let cell = model.createMarkdownCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        let newCell = model.createCodeCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
-      });
-
-    });
-
-    describe('#createCodeCell()', () => {
-
-      it('should create a new markdown cell', () => {
-        let model = new NotebookModel();
-        let cell = model.createMarkdownCell();
-        expect(cell instanceof MarkdownCellModel).to.be(true);
-      });
-
-      it('should clone a markdown cell model', () => {
-        let model = new NotebookModel();
-        let cell = model.createMarkdownCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        cell.rendered = false;
-        let newCell = model.createMarkdownCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
-        expect(newCell.rendered).to.be(false);
+        cell.source = 'foo';
+        let newCell = model.createCodeCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
-      it('should clone from a code cell model', () => {
+      it('should clone an existing raw cell', () => {
         let model = new NotebookModel();
-        let cell = model.createCodeCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        let newCell = model.createMarkdownCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
+        let cell = model.createRawCell();
+        cell.source = 'foo';
+        let newCell = model.createCodeCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
     });
@@ -540,182 +332,95 @@ describe('jupyter-js-notebook', () => {
       it('should create a new raw cell', () => {
         let model = new NotebookModel();
         let cell = model.createRawCell();
-        expect(cell instanceof RawCellModel).to.be(true);
+        expect(cell.type).to.be('raw');
       });
 
-      it('should clone a raw cell model', () => {
+      it('should clone an existing raw cell', () => {
         let model = new NotebookModel();
         let cell = model.createRawCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        cell.format = 'foo';
-        let newCell = model.createRawCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
-        expect(newCell.format).to.be('foo');
+        cell.source = 'foo';
+        let newCell = model.createRawCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
-      it('should clone from a code cell model', () => {
+      it('should clone an existing code cell', () => {
         let model = new NotebookModel();
         let cell = model.createCodeCell();
-        cell.trusted = true;
-        cell.input.textEditor.text = 'foo';
-        cell.tags = ['foo', 'bar'];
-        let newCell = model.createRawCell(cell);
-        expect(newCell.trusted).to.be(true);
-        expect(newCell.input.textEditor.text).to.be('foo');
-        expect(newCell.tags).to.eql(['foo', 'bar']);
+        cell.source = 'foo';
+        let newCell = model.createRawCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
     });
 
-    describe('#runActiveCell()', () => {
+    describe('#createMarkdownCell()', () => {
 
-      it('should mark the active cell as trusted ', () => {
+      it('should create a new markdown cell', () => {
         let model = new NotebookModel();
-        let cell = model.createRawCell();
-        cell.trusted = false;
-        model.cells.add(cell);
-        model.activeCellIndex = 0;
-        model.runActiveCell();
-        expect(cell.trusted).to.be(true);
+        let cell = model.createMarkdownCell();
+        expect(cell.type).to.be('markdown');
       });
 
-      it('should have no effect on a readonly notebook', () => {
+      it('should clone an existing markdown cell', () => {
         let model = new NotebookModel();
-        let cell = model.createCodeCell();
-        cell.trusted = false;
-        model.cells.add(cell);
-        model.activeCellIndex = 0;
-        model.readOnly = true;
-        model.runActiveCell();
-        expect(cell.trusted).to.be(false);
+        let cell = model.createMarkdownCell();
+        cell.source = 'foo';
+        let newCell = model.createMarkdownCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
-      it('should have no effect if there is no active cell', () => {
+      it('should clone an existing raw cell', () => {
         let model = new NotebookModel();
-        let cell = model.createCodeCell();
-        cell.trusted = false;
-        model.runActiveCell();
-        expect(cell.trusted).to.be(false);
+        let cell = model.createRawCell();
+        cell.source = 'foo';
+        let newCell = model.createMarkdownCell(cell.toJSON());
+        expect(newCell.source).to.be('foo');
       });
 
-      it('should render a markdown cell', () => {
-        let model = new NotebookModel();
-        let cell = model.createMarkdownCell();
-        cell.rendered = false;
-        model.cells.add(cell);
-        model.runActiveCell();
-        expect(cell.rendered).to.be(true);
-      });
+    });
 
-      it('should clear the prompt on a code cell if there is no session', () => {
-        let model = new MyNotebookModel();
-        let cell = model.createCodeCell();
-        cell.input.textEditor.text = 'a = 1';
-        cell.input.prompt = '';
-        model.cells.add(cell);
-        model.runActiveCell();
-        expect(cell.input.prompt).to.be('');
+    describe('#getMetadata()', () => {
+
+      it('should get a metadata cursor for the notebook', () => {
+        let model = new NotebookModel();
+        let cursor = model.getMetadata('foo');
+        expect(cursor.getValue()).to.be(void 0);
       });
 
-      it('should execute on a code cell when there is a session', (done) => {
-        let model = new MyNotebookModel();
-        model.session = new MockSession('test.ipynb');
-        let cell = model.createCodeCell();
-        cell.input.textEditor.text = 'a = 1';
-        model.cells.add(cell);
-        let called = false;
-        model.session.statusChanged.connect(() => {
-          if (called) {
-            return;
-          }
-          called = true;
-          model.runActiveCell();
-          let kernel = model.session.kernel as MockKernel;
-          kernel.sendShellReply({
-            execution_count: 1,
-            data: {},
-            metadata: {}
-          });
-        });
-        cell.input.stateChanged.connect(() => {
-          if (cell.input.prompt === '1') {
-            done();
-          }
-        });
+      it('should get the value for all cursors', () => {
+        let model = new NotebookModel();
+        let cursor0 = model.getMetadata('foo');
+        let cursor1 = model.getMetadata('foo');
+        cursor0.setValue(1);
+        expect(cursor1.getValue()).to.be(1);
       });
 
     });
 
-    describe('#onEditorChanged()', () => {
+    describe('#listMetadata()', () => {
 
-      it('should set the dirty flag when a text editor text changes', () => {
-        let model = new MyNotebookModel();
-        let cell = model.createCodeCell();
-        model.cells.add(cell);
-        model.dirty = false;
-        cell.input.textEditor.text = 'foo';
-        expect(model.dirty).to.be(true);
-        expect(model.methods.indexOf('onEditorChanged')).to.not.be(-1);
+      it('should list the metadata namespace keys for the notebook', () => {
+        let model = new NotebookModel();
+        let keys = ['kernelspec', 'language_info', 'orig_nbformat'];
+        expect(model.listMetadata()).to.eql(keys);
+        let cursor = model.getMetadata('foo');
+        expect(model.listMetadata()).to.eql(keys);
+        cursor.setValue(1);
+        keys.push('foo');
+        expect(model.listMetadata()).to.eql(keys);
       });
 
     });
 
     describe('#onCellsChanged()', () => {
 
-      it('should set the dirty flag', () => {
-        let model = new MyNotebookModel();
-        let cell = model.createCodeCell();
-        expect(model.dirty).to.be(false);
-        model.cells.add(cell);
-        expect(model.methods.indexOf('onCellsChanged')).to.not.be(-1);
-        expect(model.dirty).to.be(true);
-      });
+    });
 
-      it('should set the activeCellIndex on an add', () => {
-        let model = new MyNotebookModel();
-        let cell = model.createCodeCell();
-        expect(model.activeCellIndex).to.be(null);
-        model.cells.add(cell);
-        expect(model.activeCellIndex).to.be(0);
-        expect(model.methods.indexOf('onCellsChanged')).to.not.be(-1);
-        expect(model.dirty).to.be(true);
-      });
+    describe('#onCellChanged()', () => {
 
-      it('should adjust the activeCellIndex on a remove', () => {
-        let model = new MyNotebookModel();
-        model.cells.add(model.createCodeCell());
-        let cell = model.createCodeCell();
-        model.cells.add(cell);
-        expect(model.activeCellIndex).to.be(1);
-        model.dirty = false;
-        model.cells.remove(cell);
-        expect(model.activeCellIndex).to.be(0);
-        expect(model.methods.indexOf('onCellsChanged')).to.not.be(-1);
-        expect(model.dirty).to.be(true);
-        expect(cell.isDisposed).to.be(true);
-      });
+    });
 
-      it('should dispose of all old cells on a replace', () => {
-        let model = new MyNotebookModel();
-        let cells: ICellModel[] = [];
-        for (let i = 0; i < 5; i++) {
-          let cell = model.createMarkdownCell()
-          cells.push(cell);
-          model.cells.add(cell);
-        }
-        model.dirty = false;
-        model.cells.clear();
-        expect(model.methods.indexOf('onCellsChanged')).to.not.be(-1);
-        expect(model.dirty).to.be(true);
-        for (let i = 0; i < 5; i++) {
-          let cell = cells[i];
-          expect(cell.isDisposed).to.be(true);
-        }
-      });
+    describe('#setCursorData()', () => {
 
     });