Explorar el Código

Merge pull request #1373 from blink1073/loader-tests

Add tests for loader and application
Afshin Darian hace 8 años
padre
commit
49e4f0beca

+ 7 - 4
jupyterlab/src/loader.ts

@@ -3,14 +3,13 @@
 
 import {
   ModuleLoader
-} from '../../lib/application';
+} from '../../lib/application/loader';
 
 
 /**
  * A module loader instance.
  */
-export
-const loader = new ModuleLoader();
+const _loader = new ModuleLoader();
 
 
 /**
@@ -23,5 +22,9 @@ const loader = new ModuleLoader();
  */
 export
 function define(path: string, callback: ModuleLoader.DefineCallback): void {
-  loader.define.call(loader, path, callback);
+  _loader.define.call(_loader, path, callback);
 }
+
+
+export
+const loader = _loader;

+ 31 - 56
src/application/loader.ts

@@ -10,15 +10,15 @@ import {
 } from 'phosphor/lib/ui/widget';
 
 import {
-  maxSatisfying
+  maxSatisfying, satisfies
 } from 'semver';
 
 
 /**
  * A module loader using semver for dynamic resolution of requires.
  *
- * It is meant to be used in conjunction with the JuptyerLabPlugin
- * for WebPack.
+ * It is meant to be used in conjunction with the JupyterLabPlugin
+ * for WebPack from `@jupyterlab/extension-builder`.
  */
 export
 class ModuleLoader {
@@ -29,8 +29,7 @@ class ModuleLoader {
     // Provide the `require.ensure` function used for code
     // splitting in the WebPack bundles.
     // https://webpack.github.io/docs/code-splitting.html
-    (this.require as any).ensure = this.ensureBundle.bind(this);
-    this._boundRequire = this.require.bind(this);
+    this._boundRequire = this.require.bind(this) as any;
     this._boundRequire.ensure = this.ensureBundle.bind(this);
   }
 
@@ -43,7 +42,7 @@ class ModuleLoader {
    * @param callback - The callback function for invoking the module.
    *
    * #### Notes
-   * The callback is called with the module,
+   * This is a no-op if the path is already registered.
    */
   define(path: string, callback: ModuleLoader.DefineCallback): void {
     if (!(path in this._registered)) {
@@ -60,18 +59,25 @@ class ModuleLoader {
    * @returns The exports of the requested module, if registered.  The module
    *   selected is the registered module that maximally satisfies the semver
    *   range of the request.
+   *
+   * #### Notes
+   * Will throw an error if the required path cannot be satisfied.
    */
   require(path: string): any {
     // Check if module is in cache.
     let id = this._findMatch(path);
+    if (!id) {
+      throw new Error(`No matching module found for: "${path}"`);
+    }
     let installed = this._modules;
     if (installed[id]) {
       return installed[id].exports;
     }
 
     // Create a new module (and put it into the cache).
-    let mod: Private.IModule = installed[id] = {
+    let mod: ModuleLoader.IModule = installed[id] = {
       exports: {},
+      require: this._boundRequire,
       id,
       loaded: false
     };
@@ -88,18 +94,20 @@ class ModuleLoader {
   }
 
   /**
-   * Ensure a bundle is loaded on a page.
+   * Ensure a bundle is loaded on the page.
    *
    * @param path - The public path of the bundle (e.g. "lab/jupyter.bundle.js").
    *
    * @param callback - The callback invoked when the bundle has loaded.
+   *
+   * @returns A promise that resolves when the bundle is loaded.
    */
   ensureBundle(path: string, callback?: ModuleLoader.EnsureCallback): Promise<void> {
     let bundle = this._getBundle(path);
 
     if (bundle.loaded) {
       if (callback) {
-        callback.call(null, this.require);
+        callback.call(null, this._boundRequire);
       }
       return Promise.resolve(void 0);
     }
@@ -179,7 +187,8 @@ class ModuleLoader {
       }
       if (sources.length === targets.length && sources.every((source, i) => {
         return (source.package === targets[i].package
-          && source.module === targets[i].module);
+          && source.module === targets[i].module
+          && satisfies(targets[i].version, source.version));
       })) {
         matches.push(mod);
         versions.push(targets.map(t => t.version));
@@ -221,7 +230,7 @@ class ModuleLoader {
     let promise = new Promise<void>((resolve, reject) => {
       script.onload = () => {
         while (bundle.callbacks.length) {
-          bundle.callbacks.shift().call(null, this.require.bind(this));
+          bundle.callbacks.shift().call(null, this._boundRequire);
         }
         bundle.loaded = true;
         resolve(void 0);
@@ -267,10 +276,10 @@ class ModuleLoader {
 
   private _registered: { [key: string]: ModuleLoader.DefineCallback } = Object.create(null);
   private _parsed: { [key: string]: Private.IPathInfo } = Object.create(null);
-  private _modules: { [key: string]: Private.IModule } = Object.create(null);
+  private _modules: { [key: string]: ModuleLoader.IModule } = Object.create(null);
   private _bundles: { [key: string]: Private.IBundle } = Object.create(null);
   private _matches: { [key: string]: string } = Object.create(null);
-  private _boundRequire: any;
+  private _boundRequire: ModuleLoader.IRequire;
 }
 
 
@@ -280,36 +289,23 @@ class ModuleLoader {
 export
 namespace ModuleLoader {
   /**
-   * The interface for a node require function.
-   */
-  export
-  interface NodeRequireFunction {
-    (id: string): any;
-  }
-
-  /**
-   * The interface for the node require function.
+   * The interface for the require function.
    */
   export
-  interface NodeRequire extends NodeRequireFunction {
-    resolve(id: string): string;
-    cache: any;
-    extensions: any;
-    main: NodeModule | undefined;
+  interface IRequire {
+    (path: string): any;
+    ensure(path: string, callback?: EnsureCallback): Promise<void>;
   }
 
   /**
-   * The interface fore a node require module.
+   * The interface for a module.
    */
   export
-  interface NodeModule {
+  interface IModule {
     exports: any;
-    require: NodeRequireFunction;
+    require: IRequire;
     id: string;
-    filename: string;
     loaded: boolean;
-    parent: NodeModule | null;
-    children: NodeModule[];
   }
 
   /**
@@ -317,13 +313,13 @@ namespace ModuleLoader {
    * and a require function.
    */
   export
-  type DefineCallback = (module: any, exports: any, require: NodeRequire) => void;
+  type DefineCallback = (module: IModule, exports: any, require: IRequire) => void;
 
   /**
    * A callback for an ensure function that takes a require function.
    */
   export
-  type EnsureCallback = (require: NodeRequire) => void;
+  type EnsureCallback = (require: IRequire) => void;
 }
 
 
@@ -331,27 +327,6 @@ namespace ModuleLoader {
  * A namespace for private module data.
  */
 namespace Private {
-  /**
-   * A module record.
-   */
-  export
-  interface IModule {
-    /**
-     * The exports of the module.
-     */
-    exports: any;
-
-    /**
-     * The id of the module.
-     */
-    id: string;
-
-    /**
-     * Whether the module has been loaded.
-     */
-    loaded: boolean;
-  }
-
   /**
    * A bundle record.
    */

+ 99 - 0
test/src/application/index.spec.ts

@@ -0,0 +1,99 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  JupyterLab, ApplicationShell, ModuleLoader
+} from '../../../lib/application';
+
+
+class LabTest extends JupyterLab {
+
+  createShell(): ApplicationShell {
+    return super.createShell();
+  }
+}
+
+
+describe('JupyterLab', () => {
+
+  let lab: JupyterLab;
+  let loader = new ModuleLoader();
+
+  beforeEach(() => {
+    lab = new JupyterLab();
+  });
+
+  describe('#constructor()', () => {
+
+    it('should create a JupyterLab object', () => {
+      expect(lab).to.be.a(JupyterLab);
+    });
+
+    it('should accept options', () => {
+      lab = new JupyterLab({
+        version: 'foo',
+        gitDescription: 'foo',
+        loader
+      });
+    });
+
+  });
+
+  describe('#started', () => {
+
+    it('should resolve when the application is started', (done) => {
+      lab.started.then(done, done);
+      lab.start();
+    });
+
+  });
+
+  describe('#info', () => {
+
+    it('should be the info about the application', () => {
+      expect(lab.info.version).to.be('unknown');
+      expect(lab.info.gitDescription).to.be('unknown');
+      lab = new JupyterLab({
+        version: 'foo',
+        gitDescription: 'foo'
+      });
+      expect(lab.info.version).to.be('foo');
+      expect(lab.info.gitDescription).to.be('foo');
+    });
+
+  });
+
+  describe('#loader', () => {
+
+    it('should be the loader used by the application', () => {
+      expect(lab.loader).to.be(null);
+      lab = new JupyterLab({ loader });
+      expect(lab.loader).to.be(loader);
+    });
+
+  });
+
+  describe('#start()', () => {
+
+    it('should start the application', (done) => {
+      lab.start().then(done, done);
+    });
+
+    it('should accept options', (done) => {
+      lab.start({ hostID: 'foo' }).then(done, done);
+    });
+
+  });
+
+  describe('#createShell()', () => {
+
+    it('should create the application shell', () => {
+      let test = new LabTest();
+      expect(test.createShell()).to.be.an(ApplicationShell);
+    });
+
+  });
+
+});

+ 73 - 0
test/src/application/loader.spec.ts

@@ -16,6 +16,79 @@ describe('ModuleLoader', () => {
     loader = new ModuleLoader();
   });
 
+  describe('#constructor()', () => {
+
+    it('should create a ModuleLoader object', () => {
+      expect(loader).to.be.a(ModuleLoader);
+    });
+
+  });
+
+  describe('#define()', () => {
+
+    it('should define a module that can be synchronously required', () => {
+      let called = false;
+      let callback = (module: any, exports: any, require: any) => {
+        called = true;
+      };
+      loader.define('foo@1.0.1/index.js', callback);
+      loader.require('foo@^1.0.1/index.js');
+      expect(called).to.be(true);
+    });
+
+    it('should be a no-op if the path is already registered', () => {
+      let called0 = false;
+      let called1 = false;
+      let callback0 = (module: any, exports: any, require: any) => {
+        called0 = true;
+      };
+      let callback1 = (module: any, exports: any, require: any) => {
+        called1 = true;
+      };
+      loader.define('foo@1.0.1/index.js', callback0);
+      loader.define('foo@1.0.1/index.js', callback1);
+      loader.require('foo@^1.0.1/index.js');
+      expect(called0).to.be(true);
+      expect(called1).to.be(false);
+    });
+
+  });
+
+  describe('#require()', () => {
+
+    it('should synchronously return a module that has already been loaded', () => {
+      let callback = (module: any, exports: any, require: any) => {
+        module.exports = 'hello';
+      };
+      loader.define('foo@1.0.1/index.js', callback);
+      let value = loader.require('foo@^1.0/index.js');
+      expect(value).to.be('hello');
+    });
+
+    it('should return the maximally satisfying module', () => {
+      let callback0 = (module: any, exports: any, require: any) => {
+        module.exports = '1.0.0';
+      };
+      loader.define('foo@1.0.0/index.js', callback0);
+      let callback1 = (module: any, exports: any, require: any) => {
+        module.exports = '1.0.1';
+      };
+      loader.define('foo@1.0.1/index.js', callback1);
+      let value = loader.require('foo@^1.0/index.js');
+      expect(value).to.be('1.0.1');
+    });
+
+    it('should throw an error if the required module is not found', () => {
+      expect(() => { loader.require('foo@^1.0/index.js'); }).to.throwError();
+      let callback = (module: any, exports: any, require: any) => {
+        module.exports = 'hello';
+      };
+      loader.define('foo@1.0.1/index.js', callback);
+      expect(() => { loader.require('foo@^1.0.2/index.js'); }).to.throwError();
+    });
+
+  });
+
   describe('#extractPlugins()', () => {
 
     it('should pass for a valid plugin array', () => {