Browse Source

modernize docmanager tests

lint modernize script

wip modernize docmanager

wip modernize docmanager

pull out a commont tsconfigbase.test.json

pull out a commont tsconfigbase.test.json and put the options as a prompt

pull out a commont tsconfigbase.test.json and put the options as a prompt

pull out a commont tsconfigbase.test.json and put the options as a prompt

lint

comments

fix references

Update scripts and modernize script

wip docmanager test upgrades

wip docmanager tests

Finish docmanager tests

Add vscode launch file

Update interface
Steven Silvester 5 years ago
parent
commit
e3da74460d

+ 13 - 0
buildutils/src/ensure-package.ts

@@ -267,6 +267,19 @@ export async function ensurePackage(
     utils.writeJSONFile(tsConfigPath, tsConfigData);
   }
 
+  // Handle references in tsconfig.test.json if it exists
+  const tsConfigTestPath = path.join(pkgPath, 'tsconfig.test.json');
+  if (fs.existsSync(tsConfigTestPath)) {
+    const tsConfigTestData = utils.readJSONFile(tsConfigTestPath);
+    // Use the main references and add testutils.
+    tsConfigTestData.references = [];
+    Object.keys(references).forEach(name => {
+      tsConfigTestData.references.push({ path: references[name] });
+    });
+    tsConfigTestData.references.push({ path: '../../testutils' });
+    utils.writeJSONFile(tsConfigTestPath, tsConfigTestData);
+  }
+
   // Get a list of all the published files.
   // This will not catch .js or .d.ts files if they have not been built,
   // but we primarily use this to check for files that are published as-is,

+ 0 - 1
dev_mode/package.json

@@ -372,7 +372,6 @@
       "@jupyterlab/test-completer": "../tests/test-completer",
       "@jupyterlab/test-coreutils": "../tests/test-coreutils",
       "@jupyterlab/test-csvviewer": "../tests/test-csvviewer",
-      "@jupyterlab/test-docmanager": "../tests/test-docmanager",
       "@jupyterlab/test-docregistry": "../tests/test-docregistry",
       "@jupyterlab/test-filebrowser": "../tests/test-filebrowser",
       "@jupyterlab/test-fileeditor": "../tests/test-fileeditor",

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

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

+ 1 - 1
packages/console/jest.config.js

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

+ 1 - 0
packages/console/package.json

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

+ 1 - 1
packages/console/src/panel.ts

@@ -210,7 +210,7 @@ export namespace ConsolePanel {
     /**
      * An existing session context to use.
      */
-    sessionContext?: SessionContext;
+    sessionContext?: ISessionContext;
 
     /**
      * The model factory for the console widget.

+ 28 - 23
packages/console/tsconfig.test.json

@@ -1,29 +1,34 @@
 {
-  "compilerOptions": {
-    "declaration": true,
-    "noImplicitAny": true,
-    "noEmitOnError": true,
-    "noUnusedLocals": true,
-    "module": "commonjs",
-    "moduleResolution": "node",
-    "target": "es2015",
-    "outDir": "lib",
-    "lib": [
-      "es2015",
-      "es2015.collection",
-      "dom",
-      "es2015.iterable",
-      "es2017.object"
-    ],
-    "types": [],
-    "jsx": "react",
-    "resolveJsonModule": true,
-    "esModuleInterop": true,
-    "strictNullChecks": true,
-    "skipLibCheck": true
-  },
+  "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../cells"
+    },
+    {
+      "path": "../codeeditor"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../nbformat"
+    },
+    {
+      "path": "../observables"
+    },
+    {
+      "path": "../rendermime"
+    },
+    {
+      "path": "../services"
+    },
+    {
+      "path": "../ui-components"
+    },
     {
       "path": "../../testutils"
     }

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

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

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

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

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

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

+ 9 - 1
packages/docmanager/package.json

@@ -29,10 +29,14 @@
   },
   "scripts": {
     "build": "tsc -b",
+    "build:test": "tsc --build tsconfig.test.json",
     "clean": "rimraf lib",
     "docs": "typedoc src",
     "prepublishOnly": "npm run build",
-    "watch": "tsc -b --watch"
+    "test": "jest",
+    "test:cov": "jest --collect-coverage",
+    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "watch": "npm run test -- --watch"
   },
   "dependencies": {
     "@jupyterlab/apputils": "^2.1.0",
@@ -50,7 +54,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"
   },

+ 39 - 38
tests/test-docmanager/src/manager.spec.ts → packages/docmanager/test/manager.spec.ts

@@ -1,13 +1,13 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { expect } from 'chai';
+import 'jest';
 
 import { ServiceManager } from '@jupyterlab/services';
 
 import { Widget } from '@lumino/widgets';
 
-import { DocumentManager } from '@jupyterlab/docmanager';
+import { DocumentManager } from '../src';
 
 import {
   DocumentRegistry,
@@ -19,6 +19,8 @@ import {
 
 import { dismissDialog } from '@jupyterlab/testutils';
 
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
 class WidgetFactory extends ABCWidgetFactory<IDocumentWidget> {
   protected createNewWidget(
     context: DocumentRegistry.Context
@@ -75,9 +77,8 @@ describe('@jupyterlab/docmanager', () => {
     fileTypes: []
   });
 
-  before(() => {
-    services = new ServiceManager({ standby: 'never' });
-    return services.ready;
+  beforeAll(() => {
+    services = new Mock.ServiceManagerMock();
   });
 
   beforeEach(() => {
@@ -105,37 +106,37 @@ describe('@jupyterlab/docmanager', () => {
   describe('DocumentWidgetManager', () => {
     describe('#constructor()', () => {
       it('should create a new document manager', () => {
-        expect(manager).to.be.an.instanceof(DocumentManager);
+        expect(manager).toBeInstanceOf(DocumentManager);
       });
     });
 
     describe('#isDisposed', () => {
       it('should test whether the manager is disposed', () => {
-        expect(manager.isDisposed).to.equal(false);
+        expect(manager.isDisposed).toBe(false);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
       });
     });
 
     describe('#dispose()', () => {
       it('should dispose of the resources used by the manager', () => {
-        expect(manager.isDisposed).to.equal(false);
+        expect(manager.isDisposed).toBe(false);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
       });
     });
 
     describe('#services', () => {
-      it('should get the service manager for the manager', () => {
-        expect(manager.services).to.be.an.instanceof(ServiceManager);
+      it('should get the service manager for the manager', async () => {
+        await manager.services.ready;
       });
     });
 
     describe('#registry', () => {
       it('should get the registry used by the manager', () => {
-        expect(manager.registry).to.be.an.instanceof(DocumentRegistry);
+        expect(manager.registry).toBeInstanceOf(DocumentRegistry);
       });
     });
 
@@ -146,7 +147,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.open(model.path)!;
-        expect(widget.hasClass('WidgetFactory')).to.equal(true);
+        expect(widget.hasClass('WidgetFactory')).toBe(true);
         await dismissDialog();
       });
 
@@ -165,7 +166,7 @@ describe('@jupyterlab/docmanager', () => {
         context = manager.contextForWidget(widget)!;
         await context.ready;
         await context.sessionContext.ready;
-        expect(context.sessionContext.session?.kernel).to.be.ok;
+        expect(context.sessionContext.session?.kernel).toBeTruthy();
         await context.sessionContext.shutdown();
       });
 
@@ -177,7 +178,7 @@ describe('@jupyterlab/docmanager', () => {
         widget = manager.open(model.path, 'default')!;
         context = manager.contextForWidget(widget)!;
         await dismissDialog();
-        expect(context.sessionContext.session?.kernel).to.be.not.ok;
+        expect(context.sessionContext.session?.kernel).toBeFalsy();
       });
 
       it('should return undefined if the factory is not found', async () => {
@@ -186,7 +187,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.open(model.path, 'foo');
-        expect(widget).to.be.undefined;
+        expect(widget).toBeUndefined();
       });
 
       it('should return undefined if the factory has no model factory', async () => {
@@ -201,7 +202,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.open(model.path, 'foo');
-        expect(widget).to.be.undefined;
+        expect(widget).toBeUndefined();
       });
     });
 
@@ -212,7 +213,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path)!;
-        expect(widget.hasClass('WidgetFactory')).to.equal(true);
+        expect(widget.hasClass('WidgetFactory')).toBe(true);
         await dismissDialog();
       });
 
@@ -231,7 +232,7 @@ describe('@jupyterlab/docmanager', () => {
         context = manager.contextForWidget(widget)!;
         await context.ready;
         await context.sessionContext.ready;
-        expect(context.sessionContext.session!.kernel!.id).to.equal(id);
+        expect(context.sessionContext.session!.kernel!.id).toBe(id);
         await context.sessionContext.shutdown();
       });
 
@@ -243,7 +244,7 @@ describe('@jupyterlab/docmanager', () => {
         widget = manager.createNew(model.path, 'default')!;
         context = manager.contextForWidget(widget)!;
         await dismissDialog();
-        expect(context.sessionContext.session?.kernel).to.be.not.ok;
+        expect(context.sessionContext.session?.kernel).toBeFalsy();
       });
 
       it('should return undefined if the factory is not found', async () => {
@@ -252,7 +253,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path, 'foo');
-        expect(widget).to.be.undefined;
+        expect(widget).toBeUndefined();
       });
 
       it('should return undefined if the factory has no model factory', async () => {
@@ -267,7 +268,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path, 'foo');
-        expect(widget).to.be.undefined;
+        expect(widget).toBeUndefined();
       });
     });
 
@@ -278,7 +279,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path);
-        expect(manager.findWidget(model.path, 'test')).to.equal(widget);
+        expect(manager.findWidget(model.path, 'test')).toBe(widget);
         await dismissDialog();
       });
 
@@ -288,12 +289,12 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path);
-        expect(manager.findWidget(model.path)).to.equal(widget);
+        expect(manager.findWidget(model.path)).toBe(widget);
         await dismissDialog();
       });
 
       it('should fail to find a widget', () => {
-        expect(manager.findWidget('foo')).to.be.undefined;
+        expect(manager.findWidget('foo')).toBeUndefined();
       });
 
       it('should fail to find a widget with non default factory and the default widget name', async () => {
@@ -307,7 +308,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path, 'test2');
-        expect(manager.findWidget(model.path)).to.be.undefined;
+        expect(manager.findWidget(model.path)).toBeUndefined();
       });
 
       it('should find a widget with non default factory given a file and a null widget name', async () => {
@@ -321,7 +322,7 @@ describe('@jupyterlab/docmanager', () => {
           ext: '.txt'
         });
         widget = manager.createNew(model.path, 'test2');
-        expect(manager.findWidget(model.path, null)).to.equal(widget);
+        expect(manager.findWidget(model.path, null)).toBe(widget);
         await dismissDialog();
       });
     });
@@ -334,13 +335,13 @@ describe('@jupyterlab/docmanager', () => {
         });
         widget = manager.createNew(model.path)!;
         context = manager.contextForWidget(widget)!;
-        expect(context.path).to.equal(model.path);
+        expect(context.path).toBe(model.path);
         await dismissDialog();
       });
 
       it('should fail to find the context for the widget', () => {
         widget = new Widget();
-        expect(manager.contextForWidget(widget)).to.be.undefined;
+        expect(manager.contextForWidget(widget)).toBeUndefined();
       });
     });
 
@@ -352,7 +353,7 @@ describe('@jupyterlab/docmanager', () => {
         });
         widget = manager.createNew(model.path)!;
         const clone = manager.cloneWidget(widget)!;
-        expect(manager.contextForWidget(widget)).to.equal(
+        expect(manager.contextForWidget(widget)).toBe(
           manager.contextForWidget(clone)
         );
         await dismissDialog();
@@ -360,7 +361,7 @@ describe('@jupyterlab/docmanager', () => {
 
       it('should return undefined if the source widget is not managed', () => {
         widget = new Widget();
-        expect(manager.cloneWidget(widget)).to.be.undefined;
+        expect(manager.cloneWidget(widget)).toBeUndefined();
       });
 
       it('should allow widget factories to have custom clone behavior', () => {
@@ -368,15 +369,15 @@ describe('@jupyterlab/docmanager', () => {
         const clonedWidget: CloneTestWidget = manager.cloneWidget(
           widget
         ) as CloneTestWidget;
-        expect(clonedWidget.counter).to.equal(1);
+        expect(clonedWidget.counter).toBe(1);
         const newWidget: CloneTestWidget = manager.createNew(
           'bar',
           'CloneTestWidget'
         ) as CloneTestWidget;
-        expect(newWidget.counter).to.equal(0);
+        expect(newWidget.counter).toBe(0);
         expect(
           (manager.cloneWidget(clonedWidget) as CloneTestWidget).counter
-        ).to.equal(2);
+        ).toBe(2);
       });
     });
 
@@ -400,7 +401,7 @@ describe('@jupyterlab/docmanager', () => {
         });
         await dismissDialog();
         await manager.closeFile(path);
-        expect(called).to.equal(2);
+        expect(called).toBe(2);
       });
 
       it('should be a no-op if there are no open files on that path', () => {
@@ -428,7 +429,7 @@ describe('@jupyterlab/docmanager', () => {
         });
         await dismissDialog();
         await manager.closeAll();
-        expect(called).to.equal(2);
+        expect(called).toBe(2);
       });
 
       it('should be a no-op if there are no open documents', async () => {

+ 28 - 27
tests/test-docmanager/src/savehandler.spec.ts → packages/docmanager/test/savehandler.spec.ts

@@ -1,7 +1,7 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { expect } from 'chai';
+import 'jest';
 
 import { ServiceManager } from '@jupyterlab/services';
 
@@ -11,7 +11,7 @@ import {
   TextModelFactory
 } from '@jupyterlab/docregistry';
 
-import { SaveHandler } from '@jupyterlab/docmanager';
+import { SaveHandler } from '../src';
 
 import { PromiseDelegate, UUID } from '@lumino/coreutils';
 
@@ -22,15 +22,16 @@ import {
   waitForDialog
 } from '@jupyterlab/testutils';
 
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
 describe('docregistry/savehandler', () => {
   let manager: ServiceManager.IManager;
   const factory = new TextModelFactory();
   let context: Context<DocumentRegistry.IModel>;
   let handler: SaveHandler;
 
-  before(() => {
-    manager = new ServiceManager({ standby: 'never' });
-    return manager.ready;
+  beforeAll(() => {
+    manager = new Mock.ServiceManagerMock();
   });
 
   beforeEach(() => {
@@ -51,63 +52,63 @@ describe('docregistry/savehandler', () => {
   describe('SaveHandler', () => {
     describe('#constructor()', () => {
       it('should create a new save handler', () => {
-        expect(handler).to.be.an.instanceof(SaveHandler);
+        expect(handler).toBeInstanceOf(SaveHandler);
       });
     });
 
     describe('#saveInterval()', () => {
       it('should be the save interval of the handler', () => {
-        expect(handler.saveInterval).to.equal(120);
+        expect(handler.saveInterval).toBe(120);
       });
 
       it('should be set-able', () => {
         handler.saveInterval = 200;
-        expect(handler.saveInterval).to.equal(200);
+        expect(handler.saveInterval).toBe(200);
       });
     });
 
     describe('#isActive', () => {
       it('should test whether the handler is active', () => {
-        expect(handler.isActive).to.equal(false);
+        expect(handler.isActive).toBe(false);
         handler.start();
-        expect(handler.isActive).to.equal(true);
+        expect(handler.isActive).toBe(true);
       });
     });
 
     describe('#isDisposed', () => {
       it('should test whether the handler is disposed', () => {
-        expect(handler.isDisposed).to.equal(false);
+        expect(handler.isDisposed).toBe(false);
         handler.dispose();
-        expect(handler.isDisposed).to.equal(true);
+        expect(handler.isDisposed).toBe(true);
       });
 
       it('should be true after the context is disposed', () => {
         context.dispose();
-        expect(handler.isDisposed).to.equal(true);
+        expect(handler.isDisposed).toBe(true);
       });
     });
 
     describe('#dispose()', () => {
       it('should dispose of the resources used by the handler', () => {
-        expect(handler.isDisposed).to.equal(false);
+        expect(handler.isDisposed).toBe(false);
         handler.dispose();
-        expect(handler.isDisposed).to.equal(true);
+        expect(handler.isDisposed).toBe(true);
         handler.dispose();
-        expect(handler.isDisposed).to.equal(true);
+        expect(handler.isDisposed).toBe(true);
       });
     });
 
     describe('#start()', () => {
       it('should start the save handler', () => {
         handler.start();
-        expect(handler.isActive).to.equal(true);
+        expect(handler.isActive).toBe(true);
       });
 
       it('should trigger a save', () => {
         const promise = signalToPromise(context.fileChanged);
         context.model.fromString('bar');
-        expect(handler.isActive).to.equal(false);
-        handler.saveInterval = 1;
+        expect(handler.isActive).toBe(false);
+        handler.saveInterval = 0.1;
         handler.start();
         return promise;
       });
@@ -126,8 +127,8 @@ describe('docregistry/savehandler', () => {
           }
         });
         context.model.fromString('foo');
-        expect(handler.isActive).to.equal(false);
-        handler.saveInterval = 1;
+        expect(handler.isActive).toBe(false);
+        handler.saveInterval = 0.1;
         handler.start();
         return promise;
       });
@@ -140,7 +141,7 @@ describe('docregistry/savehandler', () => {
         context.model.fromString('foo');
         await context.initialize(true);
 
-        // The server has a one second resolution for saves.
+        // The context allows up to 0.5 difference in timestamps before complaining.
         setTimeout(async () => {
           await manager.contents.save(context.path, {
             type: factory.contentType,
@@ -151,7 +152,7 @@ describe('docregistry/savehandler', () => {
           handler.start();
           context.model.fromString('baz');
           context.fileChanged.connect(() => {
-            expect(context.model.toString()).to.equal('baz');
+            expect(context.model.toString()).toBe('baz');
             delegate.resolve(undefined);
           });
         }, 1500);
@@ -181,11 +182,11 @@ describe('docregistry/savehandler', () => {
         await context.initialize(true);
         context.model.fromString('foo');
         context.fileChanged.connect(() => {
-          expect(context.model.toString()).to.equal('bar');
+          expect(context.model.toString()).toBe('bar');
           delegate.resolve(undefined);
         });
 
-        // The server has a one second resolution for saves.
+        // The context allows up to 0.5 difference in timestamps before complaining.
         setTimeout(async () => {
           await manager.contents.save(context.path, {
             type: factory.contentType,
@@ -207,9 +208,9 @@ describe('docregistry/savehandler', () => {
     describe('#stop()', () => {
       it('should stop the save timer', () => {
         handler.start();
-        expect(handler.isActive).to.equal(true);
+        expect(handler.isActive).toBe(true);
         handler.stop();
-        expect(handler.isActive).to.equal(false);
+        expect(handler.isActive).toBe(false);
       });
     });
   });

+ 48 - 40
tests/test-docmanager/src/widgetmanager.spec.ts → packages/docmanager/test/widgetmanager.spec.ts

@@ -1,11 +1,11 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { expect } from 'chai';
+import 'jest';
 
 import { ServiceManager } from '@jupyterlab/services';
 
-import { DocumentWidgetManager } from '@jupyterlab/docmanager';
+import { DocumentWidgetManager } from '../src';
 
 import {
   DocumentRegistry,
@@ -24,6 +24,8 @@ import { Widget } from '@lumino/widgets';
 
 import { acceptDialog, dismissDialog } from '@jupyterlab/testutils';
 
+import * as Mock from '@jupyterlab/testutils/lib/mock';
+
 class WidgetFactory extends ABCWidgetFactory<IDocumentWidget> {
   protected createNewWidget(
     context: DocumentRegistry.Context
@@ -69,8 +71,8 @@ describe('@jupyterlab/docmanager', () => {
     readOnly: true
   });
 
-  before(() => {
-    services = new ServiceManager({ standby: 'never' });
+  beforeAll(() => {
+    services = new Mock.ServiceManagerMock();
   });
 
   beforeEach(() => {
@@ -92,25 +94,25 @@ describe('@jupyterlab/docmanager', () => {
   describe('DocumentWidgetManager', () => {
     describe('#constructor()', () => {
       it('should create a new document widget manager', () => {
-        expect(manager).to.be.an.instanceof(DocumentWidgetManager);
+        expect(manager).toBeInstanceOf(DocumentWidgetManager);
       });
     });
 
     describe('#isDisposed', () => {
       it('should test whether the manager is disposed', () => {
-        expect(manager.isDisposed).to.equal(false);
+        expect(manager.isDisposed).toBe(false);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
       });
     });
 
     describe('#dispose()', () => {
       it('should dispose of the resources used by the manager', () => {
-        expect(manager.isDisposed).to.equal(false);
+        expect(manager.isDisposed).toBe(false);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
         manager.dispose();
-        expect(manager.isDisposed).to.equal(true);
+        expect(manager.isDisposed).toBe(true);
       });
     });
 
@@ -118,7 +120,7 @@ describe('@jupyterlab/docmanager', () => {
       it('should create a widget', () => {
         const widget = manager.createWidget(widgetFactory, context);
 
-        expect(widget).to.be.an.instanceof(Widget);
+        expect(widget).toBeInstanceOf(Widget);
       });
 
       it('should emit the widgetCreated signal', () => {
@@ -128,7 +130,7 @@ describe('@jupyterlab/docmanager', () => {
           called = true;
         });
         manager.createWidget(widgetFactory, context);
-        expect(called).to.equal(true);
+        expect(called).toBe(true);
       });
     });
 
@@ -139,7 +141,9 @@ describe('@jupyterlab/docmanager', () => {
 
         manager.adoptWidget(context, widget);
         MessageLoop.sendMessage(widget, new Message('foo'));
-        expect(manager.methods).to.contain('messageHook');
+        expect(manager.methods).toEqual(
+          expect.arrayContaining(['messageHook'])
+        );
       });
 
       it('should add the document class', () => {
@@ -147,7 +151,7 @@ describe('@jupyterlab/docmanager', () => {
         const widget = new DocumentWidget({ content, context });
 
         manager.adoptWidget(context, widget);
-        expect(widget.hasClass('jp-Document')).to.equal(true);
+        expect(widget.hasClass('jp-Document')).toBe(true);
       });
 
       it('should be retrievable', () => {
@@ -155,7 +159,7 @@ describe('@jupyterlab/docmanager', () => {
         const widget = new DocumentWidget({ content, context });
 
         manager.adoptWidget(context, widget);
-        expect(manager.contextForWidget(widget)).to.equal(context);
+        expect(manager.contextForWidget(widget)).toBe(context);
       });
     });
 
@@ -163,11 +167,11 @@ describe('@jupyterlab/docmanager', () => {
       it('should find a registered widget', () => {
         const widget = manager.createWidget(widgetFactory, context);
 
-        expect(manager.findWidget(context, 'test')).to.equal(widget);
+        expect(manager.findWidget(context, 'test')).toBe(widget);
       });
 
       it('should return undefined if not found', () => {
-        expect(manager.findWidget(context, 'test')).to.be.undefined;
+        expect(manager.findWidget(context, 'test')).toBeUndefined();
       });
     });
 
@@ -175,11 +179,11 @@ describe('@jupyterlab/docmanager', () => {
       it('should return the context for a widget', () => {
         const widget = manager.createWidget(widgetFactory, context);
 
-        expect(manager.contextForWidget(widget)).to.equal(context);
+        expect(manager.contextForWidget(widget)).toBe(context);
       });
 
       it('should return undefined if not tracked', () => {
-        expect(manager.contextForWidget(new Widget())).to.be.undefined;
+        expect(manager.contextForWidget(new Widget())).toBeUndefined();
       });
     });
 
@@ -188,13 +192,13 @@ describe('@jupyterlab/docmanager', () => {
         const widget = manager.createWidget(widgetFactory, context);
         const clone = manager.cloneWidget(widget)!;
 
-        expect(clone.hasClass('WidgetFactory')).to.equal(true);
-        expect(clone.hasClass('jp-Document')).to.equal(true);
-        expect(manager.contextForWidget(clone)).to.equal(context);
+        expect(clone.hasClass('WidgetFactory')).toBe(true);
+        expect(clone.hasClass('jp-Document')).toBe(true);
+        expect(manager.contextForWidget(clone)).toBe(context);
       });
 
       it('should return undefined if the source widget is not managed', () => {
-        expect(manager.cloneWidget(new Widget())).to.be.undefined;
+        expect(manager.cloneWidget(new Widget())).toBeUndefined();
       });
     });
 
@@ -204,8 +208,8 @@ describe('@jupyterlab/docmanager', () => {
         const clone = manager.cloneWidget(widget)!;
 
         await manager.closeWidgets(context);
-        expect(widget.isDisposed).to.equal(true);
-        expect(clone.isDisposed).to.equal(true);
+        expect(widget.isDisposed).toBe(true);
+        expect(clone.isDisposed).toBe(true);
       });
     });
 
@@ -216,21 +220,23 @@ describe('@jupyterlab/docmanager', () => {
 
         manager.adoptWidget(context, widget);
         MessageLoop.sendMessage(widget, new Message('foo'));
-        expect(manager.methods).to.contain('messageHook');
+        expect(manager.methods).toEqual(
+          expect.arrayContaining(['messageHook'])
+        );
       });
 
       it('should return false for close-request messages', () => {
         const widget = manager.createWidget(widgetFactory, context);
         const msg = new Message('close-request');
 
-        expect(manager.messageHook(widget, msg)).to.equal(false);
+        expect(manager.messageHook(widget, msg)).toBe(false);
       });
 
       it('should return true for other messages', () => {
         const widget = manager.createWidget(widgetFactory, context);
         const msg = new Message('foo');
 
-        expect(manager.messageHook(widget, msg)).to.equal(true);
+        expect(manager.messageHook(widget, msg)).toBe(true);
       });
     });
 
@@ -242,8 +248,10 @@ describe('@jupyterlab/docmanager', () => {
         const delegate = new PromiseDelegate();
 
         widget.title.changed.connect(async () => {
-          expect(manager.methods).to.contain('setCaption');
-          expect(widget.title.caption).to.contain('Last Checkpoint');
+          expect(manager.methods).toEqual(
+            expect.arrayContaining(['setCaption'])
+          );
+          expect(widget.title.caption).toContain('Last Checkpoint');
           await dismissDialog();
           delegate.resolve(undefined);
         });
@@ -257,11 +265,12 @@ describe('@jupyterlab/docmanager', () => {
         const delegate = new PromiseDelegate();
 
         widget.disposed.connect(async () => {
-          expect(manager.methods).to.contain('onClose');
+          expect(manager.methods).toEqual(expect.arrayContaining(['onClose']));
           await dismissDialog();
           delegate.resolve(undefined);
         });
         widget.close();
+        await delegate.promise;
       });
 
       it('should prompt the user before closing', async () => {
@@ -271,10 +280,9 @@ describe('@jupyterlab/docmanager', () => {
         const widget = manager.createWidget(widgetFactory, context);
         const closed = manager.onClose(widget);
 
-        await acceptDialog();
-        await closed;
+        await Promise.all([acceptDialog(), closed]);
 
-        expect(widget.isDisposed).to.equal(true);
+        expect(widget.isDisposed).toBe(true);
       });
 
       it('should not prompt if the factory is readonly', async () => {
@@ -282,7 +290,7 @@ describe('@jupyterlab/docmanager', () => {
 
         await manager.onClose(readonly);
 
-        expect(readonly.isDisposed).to.equal(true);
+        expect(readonly.isDisposed).toBe(true);
       });
 
       it('should not prompt if the other widget is writable', async () => {
@@ -294,8 +302,8 @@ describe('@jupyterlab/docmanager', () => {
 
         await manager.onClose(one);
 
-        expect(one.isDisposed).to.equal(true);
-        expect(two.isDisposed).to.equal(false);
+        expect(one.isDisposed).toBe(true);
+        expect(two.isDisposed).toBe(false);
         two.dispose();
       });
 
@@ -310,8 +318,8 @@ describe('@jupyterlab/docmanager', () => {
         await acceptDialog();
         await closed;
 
-        expect(writable.isDisposed).to.equal(true);
-        expect(readonly.isDisposed).to.equal(false);
+        expect(writable.isDisposed).toBe(true);
+        expect(readonly.isDisposed).toBe(false);
         readonly.dispose();
       });
 
@@ -321,7 +329,7 @@ describe('@jupyterlab/docmanager', () => {
         const promise = manager.onClose(widget);
         await dismissDialog();
         await promise;
-        expect(widget.isDisposed).to.equal(false);
+        expect(widget.isDisposed).toBe(false);
       });
     });
   });

+ 24 - 0
packages/docmanager/tsconfig.test.json

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

+ 35 - 46
packages/docregistry/src/context.ts

@@ -493,7 +493,7 @@ export class Context<T extends DocumentRegistry.IModel>
   /**
    * Save the document contents to disk.
    */
-  private _save(): Promise<void> {
+  private async _save(): Promise<void> {
     this._saveState.emit('started');
     const model = this._model;
     let content: PartialJSONValue;
@@ -511,55 +511,44 @@ export class Context<T extends DocumentRegistry.IModel>
       format: this._factory.fileFormat,
       content
     };
+    try {
+      let value: Contents.IModel;
+      await this._manager.ready;
+      if (!model.modelDB.isCollaborative) {
+        value = await this._maybeSave(options);
+      } else {
+        value = await this._manager.contents.save(this._path, options);
+      }
+      if (this.isDisposed) {
+        return;
+      }
 
-    return this._manager.ready
-      .then(() => {
-        if (!model.modelDB.isCollaborative) {
-          return this._maybeSave(options);
-        }
-        return this._manager.contents.save(this._path, options);
-      })
-      .then(value => {
-        if (this.isDisposed) {
-          return;
-        }
-
-        model.dirty = false;
-        this._updateContentsModel(value);
+      model.dirty = false;
+      this._updateContentsModel(value);
 
-        if (!this._isPopulated) {
-          return this._populate();
-        }
-      })
-      .catch(err => {
-        // If the save has been canceled by the user,
-        // throw the error so that whoever called save()
-        // can decide what to do.
-        if (err.message === 'Cancel') {
-          throw err;
-        }
+      if (!this._isPopulated) {
+        await this._populate();
+      }
 
-        // Otherwise show an error message and throw the error.
-        const localPath = this._manager.contents.localPath(this._path);
-        const name = PathExt.basename(localPath);
-        void this._handleError(err, `File Save Error for ${name}`);
+      // Emit completion.
+      this._saveState.emit('completed');
+    } catch (err) {
+      // If the save has been canceled by the user,
+      // throw the error so that whoever called save()
+      // can decide what to do.
+      if (err.message === 'Cancel') {
         throw err;
-      })
-      .then(
-        value => {
-          // Capture all success paths and emit completion.
-          this._saveState.emit('completed');
-          return value;
-        },
-        err => {
-          // Capture all error paths and emit failure.
-          this._saveState.emit('failed');
-          throw err;
-        }
-      )
-      .catch(() => {
-        /* no-op */
-      });
+      }
+
+      // Otherwise show an error message and throw the error.
+      const localPath = this._manager.contents.localPath(this._path);
+      const name = PathExt.basename(localPath);
+      void this._handleError(err, `File Save Error for ${name}`);
+
+      // Emit failure.
+      this._saveState.emit('failed');
+      throw err;
+    }
   }
 
   /**

+ 0 - 22
tests/convert-to-jest.js

@@ -38,15 +38,7 @@ glob.sync(path.join(testSrc, 'src', '**', '*.ts*')).forEach(function(filePath) {
   fs.writeFileSync(filePath, src, 'utf8');
 });
 
-// Create jest.config.js.
-const jestConfig = `
-const func = require('@jupyterlab/testutils/lib/jest-config');
-module.exports = func('${name}', __dirname);
-`;
-fs.writeFileSync(path.join(testSrc, 'jest.config.js'), jestConfig, 'utf8');
-
 // Open coreutils package.json
-const coreUtils = path.resolve(__dirname, 'test-coreutils');
 const coreUtilsData = require('./test-coreutils/package.json');
 
 // Open target package.json
@@ -76,17 +68,3 @@ utils.writeJSONFile(path.join(testSrc, 'tsconfig.json'), tsData);
 ['karma-cov.conf.js', 'karma.conf.js', 'run-test.py'].forEach(fname => {
   utils.run(`git rm -f ./test-${name}/${fname} || true`);
 });
-
-// Copy common files from coreutils
-['run.py', 'babel.config.js'].forEach(fname => {
-  fs.copySync(path.join(coreUtils, fname), path.join(testSrc, fname));
-});
-
-// Add new files to git
-utils.run(`git add ./test-${name}/run.py ./test-${name}/jest.config.js`);
-
-// Update deps and build test
-utils.run('jlpm && jlpm build', { cwd: testSrc });
-
-// Test
-utils.run('jlpm test', { cwd: testSrc });

+ 108 - 0
tests/modernize.js

@@ -0,0 +1,108 @@
+/* eslint-disable no-console */
+const fs = require('fs-extra');
+const glob = require('glob');
+const path = require('path');
+const utils = require('@jupyterlab/buildutils');
+
+let target = process.argv[2];
+if (!target) {
+  console.error('Specify a target dir');
+  process.exit(1);
+}
+if (target.indexOf('test-') !== 0) {
+  target = 'test-' + target;
+}
+
+// Make sure folder exists
+let testSrc = path.join(__dirname, target);
+
+console.debug(testSrc); // eslint-disable-line
+if (!fs.existsSync(testSrc)) {
+  console.debug('bailing'); // eslint-disable-line
+  process.exit(1);
+}
+
+const pkgPath = path.resolve(path.join(__dirname, '../packages'));
+const name = target.replace('test-', '');
+
+// Convert to jest if needed
+if (fs.existsSync(path.join(testSrc, 'karma.conf.js'))) {
+  utils.run(`node convert-to-jest.js ${name}`);
+}
+
+// Copy files from console
+['tsconfig.test.json', 'babel.config.js', 'jest.config.js'].forEach(fname => {
+  const srcPath = path.join(pkgPath, 'console', fname);
+  fs.copySync(srcPath, path.join(testSrc, fname));
+});
+
+// Update target package.json
+const sourceData = utils.readJSONFile(
+  path.join(pkgPath, 'console', 'package.json')
+);
+const targetData = utils.readJSONFile(path.join(pkgPath, name, 'package.json'));
+// Add dev dependencies
+['@jupyterlab/testutils', '@types/jest', 'jest', 'ts-jest'].forEach(dep => {
+  targetData['devDependencies'][dep] = sourceData['devDependencies'][dep];
+});
+// Update scripts
+['build:test', 'test', 'test:cov', 'test:debug', 'watch'].forEach(script => {
+  targetData['scripts'][script] = sourceData['scripts'][script];
+});
+utils.writeJSONFile(path.join(pkgPath, name, 'package.json'), targetData);
+
+// Update tsconfigs.json - Remove skipLibCheck (added because jest and mocha types confict)
+const tsData = utils.readJSONFile(path.join(testSrc, 'tsconfig.json'));
+delete tsData['compilerOptions']['skipLibCheck'];
+utils.writeJSONFile(path.join(testSrc, 'tsconfig.json'), tsData);
+
+// Update the test files to use imports from `../src`
+glob.sync(path.join(testSrc, 'src', '**', '*.ts*')).forEach(function(filePath) {
+  console.debug(filePath);
+  let src = fs.readFileSync(filePath, 'utf8');
+  src = src.split(`'@jupyterlab/${name}/src';`).join(`'../src';`);
+  fs.writeFileSync(filePath, src, 'utf8');
+});
+
+// Commit changes (needed for jlpm jest-codemods)
+utils.run(
+  `git add test-${name} && git commit -m "wip modernize ${name} tests"`
+);
+
+// Run jest-codemods to convert from chai to jest.
+console.debug('------------------------------------');
+console.debug('Select the following options');
+console.debug('TypeScript & TSX');
+console.debug('Chai : Should / Expect BDD Syntax');
+console.debug("Yes, and I'm not afraid of false positive transformations");
+console.debug('Yes, use the globals provided by Jest(recommended)');
+console.debug('.');
+console.debug('------------------------------------');
+utils.run('jlpm jest-codemods', { cwd: testSrc });
+
+// Move the test files to `/packages/{name}/test`
+utils.run(`git mv ${testSrc}/src ${pkgPath}/${name}/test`);
+['tsconfig.test.json', 'babel.config.js', 'jest.config.js'].forEach(fname => {
+  utils.run(`mv ${testSrc}/${fname} ${pkgPath}/${name}`);
+});
+
+// Add a vscode launch file and force it to commit.
+utils.run(`mkdir -p ${pkgPath}/${name}/.vscode`);
+utils.run(
+  `cp ${pkgPath}/console/.vscode/launch.json ${pkgPath}/${name}/.vscode`
+);
+utils.run(`git add -f ${pkgPath}/${name}/.vscode/launch.json`);
+
+// Run integrity and build the new tests
+const rootDir = path.resolve('..');
+utils.run(`jlpm integrity && cd packages/${name} && jlpm run build:test`, {
+  cwd: rootDir
+});
+
+// Remove local folder
+utils.run(`git rm -rf ${testSrc}`);
+
+// Commit the changes
+utils.run(
+  `git add ${pkgPath}/${name} && git commit --no-verify -m "wip modernize ${name} tests`
+);

+ 1 - 0
tests/package.json

@@ -50,6 +50,7 @@
     "fork-ts-checker-webpack-plugin": "^3.1.1",
     "fs-extra": "^8.1.0",
     "istanbul-instrumenter-loader": "~3.0.1",
+    "jest-codemods": "^0.22.1",
     "json-loader": "^0.5.7",
     "karma": "^4.4.1",
     "karma-chrome-launcher": "~3.1.0",

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

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

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

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

+ 0 - 38
tests/test-docmanager/package.json

@@ -1,38 +0,0 @@
-{
-  "name": "@jupyterlab/test-docmanager",
-  "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/docmanager": "^2.1.0",
-    "@jupyterlab/docregistry": "^2.1.0",
-    "@jupyterlab/services": "^5.1.0",
-    "@jupyterlab/testutils": "^2.1.0",
-    "@lumino/coreutils": "^1.4.2",
-    "@lumino/messaging": "^1.3.3",
-    "@lumino/widgets": "^1.11.1",
-    "chai": "^4.2.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-docmanager/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 - 25
tests/test-docmanager/tsconfig.json

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

+ 2 - 0
testutils/package.json

@@ -35,11 +35,13 @@
     "@jupyterlab/cells": "^2.1.0",
     "@jupyterlab/codeeditor": "^2.1.0",
     "@jupyterlab/codemirror": "^2.1.0",
+    "@jupyterlab/coreutils": "^4.1.0",
     "@jupyterlab/docregistry": "^2.1.0",
     "@jupyterlab/nbformat": "^2.1.0",
     "@jupyterlab/notebook": "^2.1.0",
     "@jupyterlab/rendermime": "^2.1.0",
     "@jupyterlab/services": "^5.1.0",
+    "@lumino/algorithm": "^1.2.3",
     "@lumino/coreutils": "^1.4.2",
     "@lumino/properties": "^1.1.6",
     "@lumino/signaling": "^1.3.5",

+ 1 - 1
testutils/src/jest-config-new.ts

@@ -1,6 +1,6 @@
 import path = require('path');
 
-module.exports = function(name: string, baseDir: string) {
+module.exports = function(baseDir: string) {
   return {
     preset: 'ts-jest/presets/js-with-babel',
     moduleNameMapper: {

+ 1 - 0
testutils/src/jest-shim.ts

@@ -3,6 +3,7 @@
 const fetchMod = ((window as any).fetch = require('node-fetch')); // tslint:disable-line
 (window as any).Request = fetchMod.Request;
 (window as any).Headers = fetchMod.Headers;
+(window as any).Response = fetchMod.Response;
 
 (global as any).Image = (window as any).Image;
 (global as any).Range = function Range() {

+ 197 - 38
testutils/src/mock.ts

@@ -10,15 +10,21 @@ import {
   KernelMessage,
   KernelSpec,
   Session,
-  ServiceManager
+  ServiceManager,
+  Contents,
+  ServerConnection
 } from '@jupyterlab/services';
 
+import { ArrayIterator } from '@lumino/algorithm';
+
 import { AttachedProperty } from '@lumino/properties';
 
 import { UUID } from '@lumino/coreutils';
 
 import { Signal } from '@lumino/signaling';
 
+import { PathExt } from '@jupyterlab/coreutils';
+
 export const KERNELSPECS: KernelSpec.ISpecModel[] = [
   {
     argv: [
@@ -118,7 +124,7 @@ export function emitIopubMessage(
  */
 export function createSimpleSessionContext(
   model: Private.RecursivePartial<Session.IModel> = {}
-): SessionContext {
+): ISessionContext {
   const kernel = new KernelMock({ model: model?.kernel || {} });
   const session = new SessionConnectionMock({ model }, kernel);
   return new SessionContextMock({}, session);
@@ -157,7 +163,7 @@ export const KernelMock = jest.fn<
   };
   let executionCount = 0;
   const spec = Private.kernelSpecForKernelName(model!.name!)!;
-  const thisObject = {
+  const thisObject: Kernel.IKernelConnection = {
     ...jest.requireActual('@jupyterlab/services'),
     ...options,
     ...model,
@@ -192,7 +198,7 @@ export const KernelMock = jest.fn<
       });
       return Promise.resolve(historyReply);
     }),
-    requestExecute: jest.fn(code => {
+    requestExecute: jest.fn(options => {
       const msgId = UUID.uuid4();
       executionCount++;
       Private.lastMessageProperty.set(thisObject, msgId);
@@ -203,7 +209,7 @@ export const KernelMock = jest.fn<
         username: thisObject.username,
         msgId,
         content: {
-          code,
+          code: options.code,
           execution_count: executionCount
         }
       });
@@ -247,7 +253,7 @@ export const SessionConnectionMock = jest.fn<
     ...options.model,
     kernel: kernel!.model
   };
-  const thisObject = {
+  const thisObject: Session.ISessionConnection = {
     ...jest.requireActual('@jupyterlab/services'),
     id: UUID.uuid4(),
     ...options,
@@ -259,14 +265,23 @@ export const SessionConnectionMock = jest.fn<
       return Private.changeKernel(kernel!, partialModel!);
     }),
     selectKernel: jest.fn(),
-    shutdown: jest.fn(() => {
-      return Promise.resolve();
-    })
+    shutdown: jest.fn(() => Promise.resolve(void 0))
   };
+  const disposedSignal = new Signal<Session.ISessionConnection, undefined>(
+    thisObject
+  );
+  const propertyChangedSignal = new Signal<
+    Session.ISessionConnection,
+    'path' | 'name' | 'type'
+  >(thisObject);
   const statusChangedSignal = new Signal<
     Session.ISessionConnection,
     Kernel.Status
   >(thisObject);
+  const connectionStatusChangedSignal = new Signal<
+    Session.ISessionConnection,
+    Kernel.ConnectionStatus
+  >(thisObject);
   const kernelChangedSignal = new Signal<
     Session.ISessionConnection,
     Session.ISessionConnection.IKernelChangedArgs
@@ -276,6 +291,11 @@ export const SessionConnectionMock = jest.fn<
     KernelMessage.IIOPubMessage
   >(thisObject);
 
+  const unhandledMessageSignal = new Signal<
+    Session.ISessionConnection,
+    KernelMessage.IMessage
+  >(thisObject);
+
   kernel!.iopubMessage.connect((_, args) => {
     iopubMessageSignal.emit(args);
   }, thisObject);
@@ -284,9 +304,13 @@ export const SessionConnectionMock = jest.fn<
     statusChangedSignal.emit(args);
   }, thisObject);
 
+  (thisObject as any).disposed = disposedSignal;
+  (thisObject as any).connectionStatusChanged = connectionStatusChangedSignal;
+  (thisObject as any).propertyChanged = propertyChangedSignal;
   (thisObject as any).statusChanged = statusChangedSignal;
   (thisObject as any).kernelChanged = kernelChangedSignal;
   (thisObject as any).iopubMessage = iopubMessageSignal;
+  (thisObject as any).unhandledMessage = unhandledMessageSignal;
   return thisObject;
 });
 
@@ -296,7 +320,7 @@ export const SessionConnectionMock = jest.fn<
  * @param session The session connection object to use
  */
 export const SessionContextMock = jest.fn<
-  SessionContext,
+  ISessionContext,
   [Partial<SessionContext.IOptions>, Session.ISessionConnection | null]
 >((options, connection) => {
   const session =
@@ -311,7 +335,7 @@ export const SessionContextMock = jest.fn<
       },
       null
     );
-  const thisObject = {
+  const thisObject: ISessionContext = {
     ...jest.requireActual('@jupyterlab/apputils'),
     ...options,
     path: session.path,
@@ -320,23 +344,19 @@ export const SessionContextMock = jest.fn<
     kernel: session.kernel,
     session,
     dispose: jest.fn(),
-    initialize: jest.fn(() => {
-      return Promise.resolve();
-    }),
-    ready: jest.fn(() => {
-      return Promise.resolve();
-    }),
+    initialize: jest.fn(() => Promise.resolve(void 0)),
+    ready: Promise.resolve(void 0),
     changeKernel: jest.fn(partialModel => {
       return Private.changeKernel(
-        session.kernel || Private.RUNNING_KERNELS_MOCKS[0],
+        session.kernel || Private.RUNNING_KERNELS[0],
         partialModel!
       );
     }),
-    shutdown: jest.fn(() => {
-      return Promise.resolve();
-    })
+    shutdown: jest.fn(() => Promise.resolve(void 0))
   };
 
+  const disposedSignal = new Signal<ISessionContext, undefined>(thisObject);
+
   const propertyChangedSignal = new Signal<
     ISessionContext,
     'path' | 'name' | 'type'
@@ -371,30 +391,169 @@ export const SessionContextMock = jest.fn<
   (thisObject as any).kernelChanged = kernelChangedSignal;
   (thisObject as any).iopubMessage = iopubMessageSignal;
   (thisObject as any).propertyChanged = propertyChangedSignal;
+  (thisObject as any).disposed = disposedSignal;
   (thisObject as any).session = session;
 
   return thisObject;
 });
 
+/**
+ * A mock contents manager.
+ */
+export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
+  const files: { [key: string]: Contents.IModel } = {};
+  const checkpoints: { [key: string]: Contents.ICheckpointModel } = {};
+
+  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;
+      fileChangedSignal.emit({
+        type: 'new',
+        oldValue: null,
+        newValue: model
+      });
+      return Promise.resolve(model);
+    }),
+    createCheckpoint: jest.fn(path => {
+      const lastModified = new Date().toISOString();
+      checkpoints[path] = { id: UUID.uuid4(), last_modified: lastModified };
+      return Promise.resolve();
+    }),
+    listCheckpoints: jest.fn(path => {
+      if (checkpoints[path]) {
+        return Promise.resolve([checkpoints[path]]);
+      }
+      return Promise.resolve([]);
+    }),
+    getModelDBFactory: jest.fn(() => {
+      return null;
+    }),
+    normalize: jest.fn(path => {
+      return path;
+    }),
+    localPath: jest.fn(path => {
+      return path;
+    }),
+    get: jest.fn((path, _) => {
+      if (!files[path]) {
+        const resp = new Response(void 0, { status: 404 });
+        return Promise.reject(new ServerConnection.ResponseError(resp));
+      }
+      return Promise.resolve(files[path]);
+    }),
+    save: jest.fn((path, options) => {
+      const timeStamp = new Date().toISOString();
+      if (files[path]) {
+        files[path] = { ...files[path], ...options, last_modified: timeStamp };
+      } else {
+        files[path] = {
+          path,
+          name: PathExt.basename(path),
+          content: '',
+          writable: true,
+          created: timeStamp,
+          type: 'file',
+          format: 'text',
+          mimetype: 'plain/text',
+          ...options,
+          last_modified: timeStamp
+        };
+      }
+      fileChangedSignal.emit({
+        type: 'save',
+        oldValue: null,
+        newValue: files[path]
+      });
+      return Promise.resolve(files[path]);
+    })
+  };
+  const fileChangedSignal = new Signal<
+    Contents.IManager,
+    Contents.IChangedArgs
+  >(thisObject);
+  (thisObject as any).fileChanged = fileChangedSignal;
+  return thisObject;
+});
+
+/**
+ * A mock sessions manager.
+ */
+export const SessionManagerMock = jest.fn<Session.IManager, []>(() => {
+  const 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);
+      return session;
+    }),
+    connectTo: jest.fn(options => {
+      return new SessionConnectionMock(options, null);
+    }),
+    refreshRunning: jest.fn(() => Promise.resolve(void 0)),
+    running: jest.fn(() => new ArrayIterator(sessions))
+  };
+  return thisObject;
+});
+
+/**
+ * A mock kernel specs manager
+ */
+export const KernelSpecManagerMock = jest.fn<KernelSpec.IManager, []>(() => {
+  const thisObject: KernelSpec.IManager = {
+    ...jest.requireActual('@jupyterlab/services'),
+    specs: { default: KERNELSPECS[0].name, kernelspecs: KERNELSPECS },
+    refreshSpecs: jest.fn(() => Promise.resolve(void 0))
+  };
+  return thisObject;
+});
+
 /**
  * A mock service manager.
  */
-export const ServiceManagerMock = jest.fn<ServiceManager, []>(() => ({
-  ...jest.requireActual('@jupyterlab/services'),
-  ready: jest.fn(() => {
-    return Promise.resolve();
-  })
-}));
+export const ServiceManagerMock = jest.fn<ServiceManager.IManager, []>(() => {
+  const thisObject: ServiceManager.IManager = {
+    ...jest.requireActual('@jupyterlab/services'),
+    ready: Promise.resolve(void 0),
+    contents: new ContentsManagerMock(),
+    sessions: new SessionManagerMock(),
+    kernelspecs: new KernelSpecManagerMock()
+  };
+  return thisObject;
+});
 
 /**
  * A mock kernel shell future.
  */
-export const MockShellFuture = jest.fn<Kernel.IShellFuture, []>(() => ({
-  ...jest.requireActual('@jupyterlab/services'),
-  done: jest.fn(() => {
-    return Promise.resolve();
-  })
-}));
+export const MockShellFuture = jest.fn<Kernel.IShellFuture, []>(() => {
+  const thisObject: Kernel.IShellFuture = {
+    ...jest.requireActual('@jupyterlab/services'),
+    done: Promise.resolve(void 0)
+  };
+  return thisObject;
+});
 
 /**
  * A namespace for module private data.
@@ -438,9 +597,9 @@ namespace Private {
         return model.id === partialModel.id;
       });
       if (kernelIdx !== -1) {
-        (kernel.model as any) = RUNNING_KERNELS_MOCKS[kernelIdx].model;
+        (kernel.model as any) = RUNNING_KERNELS[kernelIdx].model;
         (kernel.id as any) = partialModel.id;
-        return Promise.resolve(RUNNING_KERNELS_MOCKS[kernelIdx]);
+        return Promise.resolve(RUNNING_KERNELS[kernelIdx]);
       } else {
         throw new Error(
           `Unable to change kernel to one with id: ${partialModel.id}`
@@ -451,9 +610,9 @@ namespace Private {
         return model.name === partialModel.name;
       });
       if (kernelIdx !== -1) {
-        (kernel.model as any) = RUNNING_KERNELS_MOCKS[kernelIdx].model;
+        (kernel.model as any) = RUNNING_KERNELS[kernelIdx].model;
         (kernel.id as any) = partialModel.id;
-        return Promise.resolve(RUNNING_KERNELS_MOCKS[kernelIdx]);
+        return Promise.resolve(RUNNING_KERNELS[kernelIdx]);
       } else {
         throw new Error(
           `Unable to change kernel to one with name: ${partialModel.name}`
@@ -465,7 +624,7 @@ namespace Private {
   }
 
   // This list of running kernels simply mirrors the KERNEL_MODELS and KERNELSPECS lists
-  export const RUNNING_KERNELS_MOCKS: Kernel.IKernelConnection[] = KERNEL_MODELS.map(
+  export const RUNNING_KERNELS: Kernel.IKernelConnection[] = KERNEL_MODELS.map(
     (model, _) => {
       return new KernelMock({ model });
     }

+ 3 - 0
testutils/tsconfig.json

@@ -19,6 +19,9 @@
     {
       "path": "../packages/codemirror"
     },
+    {
+      "path": "../packages/coreutils"
+    },
     {
       "path": "../packages/docregistry"
     },

+ 25 - 0
tsconfigbase.test.json

@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "declaration": true,
+    "noImplicitAny": true,
+    "noEmitOnError": true,
+    "noUnusedLocals": true,
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "target": "es2015",
+    "outDir": "lib",
+    "lib": [
+      "es2015",
+      "es2015.collection",
+      "dom",
+      "es2015.iterable",
+      "es2017.object"
+    ],
+    "types": [],
+    "jsx": "react",
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "strictNullChecks": true,
+    "skipLibCheck": true
+  }
+}

File diff suppressed because it is too large
+ 533 - 4
yarn.lock


Some files were not shown because too many files changed in this diff