Bladeren bron

Merge pull request #8366 from blink1073/testutils-tests

Add tests for mocks and jupyter server
Steven Silvester 4 jaren geleden
bovenliggende
commit
23d7624d30
46 gewijzigde bestanden met toevoegingen van 929 en 82 verwijderingen
  1. 1 1
      .github/workflows/linuxjs-tests.yml
  2. 14 11
      buildutils/src/ensure-package.ts
  3. 3 0
      buildutils/template/tsconfig.test.json
  4. 0 1
      package.json
  5. 3 0
      packages/application/tsconfig.test.json
  6. 3 0
      packages/apputils/tsconfig.test.json
  7. 3 0
      packages/cells/tsconfig.test.json
  8. 3 0
      packages/codeeditor/tsconfig.test.json
  9. 3 0
      packages/codemirror/tsconfig.test.json
  10. 3 0
      packages/completer/tsconfig.test.json
  11. 3 0
      packages/console/tsconfig.test.json
  12. 3 0
      packages/coreutils/tsconfig.test.json
  13. 3 0
      packages/csvviewer/tsconfig.test.json
  14. 3 0
      packages/docmanager/tsconfig.test.json
  15. 23 15
      packages/docregistry/test/default.spec.ts
  16. 2 4
      packages/docregistry/test/mimedocument.spec.ts
  17. 3 0
      packages/docregistry/tsconfig.test.json
  18. 3 0
      packages/filebrowser/tsconfig.test.json
  19. 3 0
      packages/fileeditor/tsconfig.test.json
  20. 2 2
      packages/imageviewer/test/widget.spec.ts
  21. 3 0
      packages/imageviewer/tsconfig.test.json
  22. 3 0
      packages/inspector/tsconfig.test.json
  23. 3 0
      packages/logconsole/tsconfig.test.json
  24. 3 0
      packages/mainmenu/tsconfig.test.json
  25. 3 0
      packages/nbformat/tsconfig.test.json
  26. 3 0
      packages/notebook/tsconfig.test.json
  27. 3 0
      packages/observables/tsconfig.test.json
  28. 3 0
      packages/outputarea/tsconfig.test.json
  29. 1 7
      packages/rendermime/test/registry.spec.ts
  30. 3 0
      packages/rendermime/tsconfig.test.json
  31. 3 0
      packages/services/tsconfig.test.json
  32. 3 0
      packages/settingregistry/tsconfig.test.json
  33. 3 0
      packages/statedb/tsconfig.test.json
  34. 3 0
      packages/statusbar/tsconfig.test.json
  35. 3 0
      packages/terminal/tsconfig.test.json
  36. 3 0
      packages/ui-components/tsconfig.test.json
  37. 6 17
      scripts/ci_script.sh
  38. 1 0
      testutils/babel.config.js
  39. 2 0
      testutils/jest.config.js
  40. 6 1
      testutils/package.json
  41. 43 16
      testutils/src/mock.ts
  42. 7 6
      testutils/src/start_jupyter_server.ts
  43. 644 0
      testutils/test/mock.spec.ts
  44. 19 0
      testutils/test/start_jupyter_server.spec.ts
  45. 69 0
      testutils/tsconfig.test.json
  46. 2 1
      tsconfig.eslint.json

+ 1 - 1
.github/workflows/linuxjs-tests.yml

@@ -7,7 +7,7 @@ jobs:
     name: JS
     strategy:
       matrix:
-        group: [js-services, js-application, js-apputils, js-cells, js-codeeditor, js-codemirror, js-completer, js-console, js-coreutils, js-csvviewer, js-docmanager, js-docregistry, js-filebrowser, js-fileeditor, js-imageviewer, js-inspector, js-logconsole, js-mainmenu, js-nbformat, js-notebook, js-observables, js-outputarea, js-rendermime,  js-settingregistry, js-statedb, js-statusbar, js-terminal, js-ui-components]
+        group: [js-services, js-application, js-apputils, js-cells, js-codeeditor, js-codemirror, js-completer, js-console, js-coreutils, js-csvviewer, js-docmanager, js-docregistry, js-filebrowser, js-fileeditor, js-imageviewer, js-inspector, js-logconsole, js-mainmenu, js-nbformat, js-notebook, js-observables, js-outputarea, js-rendermime,  js-settingregistry, js-statedb, js-statusbar, js-terminal, js-ui-components, js-testutils]
       fail-fast: false
     runs-on: ubuntu-latest
     steps:

+ 14 - 11
buildutils/src/ensure-package.ts

@@ -271,6 +271,10 @@ export async function ensurePackage(
   const tsConfigTestPath = path.join(pkgPath, 'tsconfig.test.json');
   if (fs.existsSync(tsConfigTestPath)) {
     const testReferences: { [key: string]: string } = { ...references };
+
+    // Add a reference to self to build the local package as well.
+    testReferences['.'] = '.';
+
     Object.keys(devDeps).forEach(name => {
       if (!(name in locals)) {
         return;
@@ -282,17 +286,16 @@ export async function ensurePackage(
       const ref = path.relative(pkgPath, locals[name]);
       testReferences[name] = ref.split(path.sep).join('/');
     });
-    if (Object.keys(testReferences).length > 0) {
-      const tsConfigTestData = utils.readJSONFile(tsConfigTestPath);
-      tsConfigTestData.references = [];
-      Object.keys(testReferences).forEach(name => {
-        tsConfigTestData.references.push({ path: testReferences[name] });
-      });
-      Object.keys(references).forEach(name => {
-        tsConfigTestData.references.push({ path: testReferences[name] });
-      });
-      utils.writeJSONFile(tsConfigTestPath, tsConfigTestData);
-    }
+
+    const tsConfigTestData = utils.readJSONFile(tsConfigTestPath);
+    tsConfigTestData.references = [];
+    Object.keys(testReferences).forEach(name => {
+      tsConfigTestData.references.push({ path: testReferences[name] });
+    });
+    Object.keys(references).forEach(name => {
+      tsConfigTestData.references.push({ path: testReferences[name] });
+    });
+    utils.writeJSONFile(tsConfigTestPath, tsConfigTestData);
   }
 
   // Get a list of all the published files.

+ 3 - 0
buildutils/template/tsconfig.test.json

@@ -2,6 +2,9 @@
   "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     }

+ 0 - 1
package.json

@@ -31,7 +31,6 @@
     "build:packages:scope": "lerna run build",
     "build:src": "lerna run build --scope \"@jupyterlab/!(test-|example-|application-top)*\" --concurrency 1",
     "build:storybook": "lerna run build:storybook --concurrency 1",
-    "build:test": "lerna run build --scope \"@jupyterlab/test-*\" --concurrency 1",
     "build:test:scope": "lerna run build --concurrency 1",
     "build:testutils": "cd testutils && jlpm run build",
     "build:utils": "cd buildutils && jlpm run build",

+ 3 - 0
packages/application/tsconfig.test.json

@@ -26,6 +26,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/apputils/tsconfig.test.json

@@ -17,6 +17,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/cells/tsconfig.test.json

@@ -38,6 +38,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/codeeditor/tsconfig.test.json

@@ -14,6 +14,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/codemirror/tsconfig.test.json

@@ -20,6 +20,9 @@
     {
       "path": "../statusbar"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/completer/tsconfig.test.json

@@ -20,6 +20,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/console/tsconfig.test.json

@@ -29,6 +29,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/coreutils/tsconfig.test.json

@@ -2,6 +2,9 @@
   "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     }

+ 3 - 0
packages/csvviewer/tsconfig.test.json

@@ -11,6 +11,9 @@
     {
       "path": "../docregistry"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

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

@@ -17,6 +17,9 @@
     {
       "path": "../statusbar"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 23 - 15
packages/docregistry/test/default.spec.ts

@@ -22,7 +22,7 @@ import {
 
 import { ServiceManager } from '@jupyterlab/services';
 
-import { createFileContext, sleep } from '@jupyterlab/testutils';
+import { sleep } from '@jupyterlab/testutils';
 
 import * as Mock from '@jupyterlab/testutils/lib/mock';
 
@@ -179,7 +179,7 @@ describe('docregistry/default', () => {
         expect(factory.canStartKernel).toBe(true);
       });
 
-      it('should have toolbar items', () => {
+      it('should have toolbar items', async () => {
         const factory = new WidgetFactory({
           name: 'test',
           fileTypes: ['text'],
@@ -194,7 +194,7 @@ describe('docregistry/default', () => {
             }
           ]
         });
-        const context = createFileContext();
+        const context = await Mock.createFileContext();
         const widget = factory.createNew(context);
         const widget2 = factory.createNew(context);
         expect(toArray(widget.toolbar.names())).toEqual(['foo', 'bar']);
@@ -229,16 +229,16 @@ describe('docregistry/default', () => {
     });
 
     describe('#createNew()', () => {
-      it('should create a new widget given a document model and a context', () => {
+      it('should create a new widget given a document model and a context', async () => {
         const factory = createFactory();
-        const context = createFileContext();
+        const context = await Mock.createFileContext();
         const widget = factory.createNew(context);
         expect(widget).toBeInstanceOf(Widget);
       });
 
-      it('should take an optional source widget for cloning', () => {
+      it('should take an optional source widget for cloning', async () => {
         const factory = createFactory();
-        const context = createFileContext();
+        const context = await Mock.createFileContext();
         const widget = factory.createNew(context);
         const clonedWidget: IDocumentWidget = factory.createNew(
           context,
@@ -552,8 +552,8 @@ describe('docregistry/default', () => {
     let content: Widget;
     let widget: DocumentWidget;
 
-    const setup = () => {
-      context = createFileContext(undefined, manager);
+    const setup = async () => {
+      context = await Mock.createFileContext(false, manager);
       content = new Widget();
       widget = new DocumentWidget({ context, content });
     };
@@ -572,14 +572,11 @@ describe('docregistry/default', () => {
 
       it('should update the title when the path changes', async () => {
         const path = UUID.uuid4() + '.jl';
-        await context.initialize(true);
         await manager.contents.rename(context.path, path);
         expect(widget.title.label).toBe(path);
       });
 
       it('should add the dirty class when the model is dirty', async () => {
-        await context.initialize(true);
-        await context.ready;
         context.model.fromString('bar');
         expect(widget.title.className).toContain('jp-mod-dirty');
       });
@@ -593,22 +590,33 @@ describe('docregistry/default', () => {
       beforeEach(setup);
 
       it('should resolve after the reveal and context ready promises', async () => {
+        const thisContext = new Context({
+          manager,
+          factory: new TextModelFactory(),
+          path: UUID.uuid4()
+        });
         const x = Object.create(null);
         const reveal = sleep(300, x);
-        const contextReady = Promise.all([context.ready, x]);
-        const widget = new DocumentWidget({ context, content, reveal });
+        const contextReady = Promise.all([thisContext.ready, x]);
+        const widget = new DocumentWidget({
+          context: thisContext,
+          content,
+          reveal
+        });
         expect(widget.isRevealed).toBe(false);
 
         // Our promise should resolve before the widget reveal promise.
         expect(await Promise.race([widget.revealed, reveal])).toBe(x);
         // The context ready promise should also resolve first.
-        void context.initialize(true);
+        void thisContext.initialize(true);
         expect(await Promise.race([widget.revealed, contextReady])).toEqual([
           undefined,
           x
         ]);
         // The widget.revealed promise should finally resolve.
         expect(await widget.revealed).toBeUndefined();
+
+        thisContext.dispose();
       });
     });
   });

+ 2 - 4
packages/docregistry/test/mimedocument.spec.ts

@@ -53,8 +53,8 @@ const fooFactory: IRenderMime.IRendererFactory = {
 describe('docregistry/mimedocument', () => {
   let dContext: Context<DocumentRegistry.IModel>;
 
-  beforeEach(() => {
-    dContext = Mock.createFileContext();
+  beforeEach(async () => {
+    dContext = await Mock.createFileContext();
   });
 
   afterEach(() => {
@@ -100,7 +100,6 @@ describe('docregistry/mimedocument', () => {
           renderTimeout: 1000,
           dataType: 'string'
         });
-        void dContext.initialize(true);
         await widget.ready;
         const layout = widget.layout as BoxLayout;
         expect(layout.widgets.length).toBe(1);
@@ -110,7 +109,6 @@ describe('docregistry/mimedocument', () => {
     describe('contents changed', () => {
       it('should change the document contents', async () => {
         RENDERMIME.addFactory(fooFactory);
-        await dContext.initialize(true);
         const emission = testEmission(dContext.model.contentChanged, {
           test: () => {
             expect(dContext.model.toString()).toBe('bar');

+ 3 - 0
packages/docregistry/tsconfig.test.json

@@ -29,6 +29,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/filebrowser/tsconfig.test.json

@@ -26,6 +26,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/fileeditor/tsconfig.test.json

@@ -17,6 +17,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 2 - 2
packages/imageviewer/test/widget.spec.ts

@@ -161,14 +161,14 @@ describe('ImageViewer', () => {
 
 describe('ImageViewerFactory', () => {
   describe('#createNewWidget', () => {
-    it('should create an image document widget', () => {
+    it('should create an image document widget', async () => {
       const factory = new ImageViewerFactory({
         name: 'Image',
         modelName: 'base64',
         fileTypes: ['png'],
         defaultFor: ['png']
       });
-      const context = createFileContext(
+      const context = await createFileContext(
         IMAGE.path,
         new Mock.ServiceManagerMock()
       );

+ 3 - 0
packages/imageviewer/tsconfig.test.json

@@ -11,6 +11,9 @@
     {
       "path": "../docregistry"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/inspector/tsconfig.test.json

@@ -20,6 +20,9 @@
     {
       "path": "../statedb"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/logconsole/tsconfig.test.json

@@ -20,6 +20,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/mainmenu/tsconfig.test.json

@@ -11,6 +11,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/nbformat/tsconfig.test.json

@@ -2,6 +2,9 @@
   "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     }

+ 3 - 0
packages/notebook/tsconfig.test.json

@@ -35,6 +35,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/observables/tsconfig.test.json

@@ -2,6 +2,9 @@
   "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     }

+ 3 - 0
packages/outputarea/tsconfig.test.json

@@ -20,6 +20,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 1 - 7
packages/rendermime/test/registry.spec.ts

@@ -69,13 +69,7 @@ describe('rendermime/registry', () => {
   let RESOLVER: IRenderMime.IResolver;
 
   beforeAll(async () => {
-    const fileContext = Mock.createFileContext(true);
-    await fileContext.initialize(true);
-
-    // The context initialization kicks off a sessionContext initialization,
-    // but does not wait for it. We need to wait for it so our url resolver
-    // has access to the session.
-    await fileContext.sessionContext.initialize();
+    const fileContext = await Mock.createFileContext(true);
     RESOLVER = fileContext.urlResolver;
   });
 

+ 3 - 0
packages/rendermime/tsconfig.test.json

@@ -23,6 +23,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../mathjax2"
     },

+ 3 - 0
packages/services/tsconfig.test.json

@@ -17,6 +17,9 @@
     {
       "path": "../statedb"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/settingregistry/tsconfig.test.json

@@ -5,6 +5,9 @@
     {
       "path": "../statedb"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/statedb/tsconfig.test.json

@@ -2,6 +2,9 @@
   "extends": "../../tsconfigbase.test",
   "include": ["src/*", "test/*"],
   "references": [
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     }

+ 3 - 0
packages/statusbar/tsconfig.test.json

@@ -17,6 +17,9 @@
     {
       "path": "../ui-components"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/terminal/tsconfig.test.json

@@ -8,6 +8,9 @@
     {
       "path": "../services"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 3 - 0
packages/ui-components/tsconfig.test.json

@@ -5,6 +5,9 @@
     {
       "path": "../coreutils"
     },
+    {
+      "path": "."
+    },
     {
       "path": "../../testutils"
     },

+ 6 - 17
scripts/ci_script.sh

@@ -19,33 +19,22 @@ fi
 
 if [[ $GROUP == js* ]]; then
 
-    if [[ $GROUP == js-* ]]; then
+    if [[ $GROUP == "js-testutils" ]]; then
+        pushd testutils
+    else
         # extract the group name
         export PKG="${GROUP#*-}"
         pushd packages/${PKG}
-        jlpm run build; true
-        jlpm run build:test; true
-        CMD="jlpm run test:cov"
-    else
-        jlpm build:packages
-        jlpm build:test
-        CMD="jlpm test:cov --loglevel success"
     fi
 
+    jlpm run build:test; true
+
     export FORCE_COLOR=1
+    CMD="jlpm run test:cov"
     $CMD || $CMD || $CMD
     jlpm run clean
 fi
 
-if [[ $GROUP == jsscope ]]; then
-
-    jlpm build:packages
-    jlpm build:test
-    FORCE_COLOR=1 jlpm test:scope --loglevel success --scope $JSTESTGROUP
-
-    jlpm run clean
-fi
-
 
 if [[ $GROUP == docs ]]; then
     # Verify tutorial docs build

+ 1 - 0
testutils/babel.config.js

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

+ 2 - 0
testutils/jest.config.js

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

+ 6 - 1
testutils/package.json

@@ -26,9 +26,14 @@
   },
   "scripts": {
     "build": "tsc -b",
+    "build:test": "tsc --build tsconfig.test.json",
     "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
     "prepublishOnly": "npm run build",
-    "watch": "tsc --watch"
+    "test": "jest",
+    "test:cov": "jest --collect-coverage",
+    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
+    "watch": "tsc -b --watch"
   },
   "dependencies": {
     "@jupyterlab/apputils": "^2.1.0",

+ 43 - 16
testutils/src/mock.ts

@@ -28,6 +28,9 @@ import { Signal } from '@lumino/signaling';
 
 import { PathExt } from '@jupyterlab/coreutils';
 
+// The default kernel name
+export const DEFAULT_NAME = 'python3';
+
 export const KERNELSPECS: KernelSpec.ISpecModel[] = [
   {
     argv: [
@@ -40,7 +43,7 @@ export const KERNELSPECS: KernelSpec.ISpecModel[] = [
     display_name: 'Python 3',
     language: 'python',
     metadata: {},
-    name: 'python3',
+    name: DEFAULT_NAME,
     resources: {}
   },
   {
@@ -61,7 +64,7 @@ export const KERNELSPECS: KernelSpec.ISpecModel[] = [
 
 export const KERNEL_MODELS: Kernel.IModel[] = [
   {
-    name: 'python3',
+    name: DEFAULT_NAME,
     id: UUID.uuid4()
   },
   {
@@ -69,7 +72,7 @@ export const KERNEL_MODELS: Kernel.IModel[] = [
     id: UUID.uuid4()
   },
   {
-    name: 'python3',
+    name: DEFAULT_NAME,
     id: UUID.uuid4()
   }
 ];
@@ -156,7 +159,7 @@ export const KernelMock = jest.fn<
     (model! as any).id = 'foo';
   }
   if (!model.name) {
-    (model! as any).name = KERNEL_MODELS[0].name;
+    (model! as any).name = DEFAULT_NAME;
   }
   options = {
     clientId: UUID.uuid4(),
@@ -171,9 +174,7 @@ export const KernelMock = jest.fn<
     ...options,
     ...model,
     status: 'idle',
-    spec: () => {
-      return Promise.resolve(spec);
-    },
+    spec: Promise.resolve(spec),
     dispose: jest.fn(),
     clone: jest.fn(() => {
       const newKernel = Private.cloneKernel(options);
@@ -186,7 +187,7 @@ export const KernelMock = jest.fn<
       });
       return newKernel;
     }),
-    info: jest.fn(() => Promise.resolve(void 0)),
+    info: Promise.resolve(Private.getInfo(model!.name!)),
     shutdown: jest.fn(() => Promise.resolve(void 0)),
     requestHistory: jest.fn(() => {
       const historyReply = KernelMessage.createMessage({
@@ -262,7 +263,7 @@ export const SessionConnectionMock = jest.fn<
     Kernel.IKernelConnection | null
   ]
 >((options, kernel) => {
-  const name = kernel?.name || options.model?.name || KERNEL_MODELS[0].name;
+  const name = kernel?.name || options.model?.kernel?.name || DEFAULT_NAME;
   kernel = kernel || new KernelMock({ model: { name } });
   const model = {
     path: 'foo',
@@ -282,7 +283,6 @@ export const SessionConnectionMock = jest.fn<
     changeKernel: jest.fn(partialModel => {
       return Private.changeKernel(kernel!, partialModel!);
     }),
-    selectKernel: jest.fn(),
     shutdown: jest.fn(() => Promise.resolve(void 0)),
     setPath: jest.fn(path => {
       (thisObject as any).path = path;
@@ -441,7 +441,6 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
 
   const thisObject: Contents.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
-    ready: Promise.resolve(void 0),
     newUntitled: jest.fn(options => {
       const model = Private.createFile(options || {});
       files.set(model.path, model);
@@ -603,6 +602,7 @@ export const SessionManagerMock = jest.fn<Session.IManager, []>(() => {
   const thisObject: Session.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
+    isReady: true,
     startNew: jest.fn(options => {
       const session = new SessionConnectionMock({ model: options }, null);
       sessions.push(session.model);
@@ -638,6 +638,8 @@ export const KernelSpecManagerMock = jest.fn<KernelSpec.IManager, []>(() => {
   const thisObject: KernelSpec.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     specs: { default: KERNELSPECS[0].name, kernelspecs: KERNELSPECS },
+    isReady: true,
+    ready: Promise.resolve(void 0),
     refreshSpecs: jest.fn(() => Promise.resolve(void 0))
   };
   return thisObject;
@@ -650,6 +652,7 @@ export const ServiceManagerMock = jest.fn<ServiceManager.IManager, []>(() => {
   const thisObject: ServiceManager.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
+    isReady: true,
     contents: new ContentsManagerMock(),
     sessions: new SessionManagerMock(),
     kernelspecs: new KernelSpecManagerMock(),
@@ -676,13 +679,16 @@ export const MockShellFuture = jest.fn<
 /**
  * Create a context for a file.
  */
-export function createFileContext(startKernel = false): Context {
+export async function createFileContext(
+  startKernel = false,
+  manager?: ServiceManager.IManager
+): Promise<Context> {
   const path = UUID.uuid4() + '.txt';
-  const manager = new ServiceManagerMock();
+  manager = manager || new ServiceManagerMock();
   const factory = new TextModelFactory();
 
-  return new Context({
-    manager,
+  const context = new Context({
+    manager: manager || new ServiceManagerMock(),
     factory,
     path,
     kernelPreference: {
@@ -691,6 +697,9 @@ export function createFileContext(startKernel = false): Context {
       autoStartDefault: startKernel
     }
   });
+  await context.initialize(true);
+  await context.sessionContext.initialize();
+  return context;
 }
 
 /**
@@ -765,7 +774,7 @@ namespace Private {
   export function cloneKernel(
     options: RecursivePartial<Kernel.IKernelConnection.IOptions>
   ): Kernel.IKernelConnection {
-    return new KernelMock(options);
+    return new KernelMock({ ...options, clientId: UUID.uuid4() });
   }
 
   // Get the kernel spec for kernel name
@@ -775,6 +784,24 @@ namespace Private {
     });
   }
 
+  // Get the kernel info for kernel name
+  export function getInfo(
+    name: string
+  ): KernelMessage.IInfoReplyMsg['content'] {
+    return {
+      protocol_version: '1',
+      implementation: 'foo',
+      implementation_version: '1',
+      language_info: {
+        version: '1',
+        name
+      },
+      banner: 'hello, world!',
+      help_links: [],
+      status: 'ok'
+    };
+  }
+
   export function changeKernel(
     kernel: Kernel.IKernelConnection,
     partialModel: Partial<Kernel.IModel>

+ 7 - 6
testutils/src/start_jupyter_server.ts

@@ -35,15 +35,15 @@ export class JupyterServer {
   /**
    * Start the server.
    *
-   * @returns A promise that resolves when the server has started
+   * @returns A promise that resolves with the url of the server
    *
    * @throws Error if another server is still running.
    */
-  async start(): Promise<void> {
+  async start(): Promise<string> {
     if (Private.child !== null) {
       throw Error('Previous server was not disposed');
     }
-    const startDelegate = new PromiseDelegate<void>();
+    const startDelegate = new PromiseDelegate<string>();
 
     const env = {
       JUPYTER_CONFIG_DIR: Private.handleConfig(),
@@ -81,7 +81,8 @@ export class JupyterServer {
       handleOutput(String(data));
     });
 
-    await startDelegate.promise;
+    const url = await startDelegate.promise;
+    return url;
   }
 
   /**
@@ -273,13 +274,13 @@ namespace Private {
    */
   export async function connect(
     baseUrl: string,
-    startDelegate: PromiseDelegate<void>
+    startDelegate: PromiseDelegate<string>
   ): Promise<void> {
     // eslint-disable-next-line
     while (true) {
       try {
         await fetch(URLExt.join(baseUrl, 'api'));
-        startDelegate.resolve(void 0);
+        startDelegate.resolve(baseUrl);
         return;
       } catch (e) {
         // spin until we can connect to the server.

+ 644 - 0
testutils/test/mock.spec.ts

@@ -0,0 +1,644 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import 'jest';
+
+import * as Mock from '../src/mock';
+import { KernelMessage } from '@jupyterlab/services';
+import { toArray } from '@lumino/algorithm';
+
+describe('mock', () => {
+  describe('createSimpleSessionContext()', () => {
+    it('should create a session context', () => {
+      const context = Mock.createSimpleSessionContext();
+      expect(context.session!.kernel!.name).toEqual(Mock.DEFAULT_NAME);
+    });
+
+    it('should accept a session model', () => {
+      const context = Mock.createSimpleSessionContext({
+        name: 'hi',
+        path: 'foo',
+        type: 'bar',
+        kernel: { name: 'fizz' }
+      });
+      expect(context.name).toEqual('hi');
+      expect(context.path).toEqual('foo');
+      expect(context.type).toEqual('bar');
+      expect(context.session!.kernel!.name).toEqual('fizz');
+    });
+  });
+
+  describe('updateKernelStatus()', () => {
+    it('should update the kernel status', () => {
+      const context = Mock.createSimpleSessionContext();
+      let called = false;
+      context.statusChanged.connect((_, status) => {
+        if (status === 'dead') {
+          called = true;
+        }
+      });
+      Mock.updateKernelStatus(context, 'dead');
+      expect(context.session!.kernel!.status).toEqual('dead');
+      expect(called).toEqual(true);
+    });
+  });
+
+  describe('emitIopubMessage', () => {
+    it('should emit an iopub message', () => {
+      const context = Mock.createSimpleSessionContext();
+      const source = KernelMessage.createMessage({
+        channel: 'iopub',
+        msgType: 'execute_input',
+        session: 'foo',
+        username: 'bar',
+        msgId: 'fizz',
+        content: {
+          code: 'hello, world!',
+          execution_count: 0
+        }
+      });
+      let called = false;
+      context.iopubMessage.connect((_, msg) => {
+        expect(msg).toBe(source);
+        called = true;
+      });
+      Mock.emitIopubMessage(context, source);
+      expect(called).toBe(true);
+    });
+  });
+
+  describe('cloneKernel()', () => {
+    it('should clone a kernel', () => {
+      const kernel0 = new Mock.KernelMock({});
+      const kernel1 = Mock.cloneKernel(kernel0);
+      expect(kernel0.id).toBe(kernel1.id);
+      expect(kernel0.clientId).not.toBe(kernel1.clientId);
+    });
+  });
+
+  describe('KernelMock', () => {
+    describe('.constructor()', () => {
+      it('should create a mock kernel', () => {
+        const kernel = new Mock.KernelMock({});
+        expect(kernel.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should take options', () => {
+        const kernel = new Mock.KernelMock({ model: { name: 'foo' } });
+        expect(kernel.name).toBe('foo');
+      });
+    });
+
+    describe('.spec()', () => {
+      it('should resolve with a kernel spec', async () => {
+        const kernel = new Mock.KernelMock({});
+        const spec = await kernel.spec;
+        expect(spec!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const kernel = new Mock.KernelMock({});
+        kernel.dispose();
+      });
+    });
+
+    describe('.clone()', () => {
+      it('should clone the kernel', () => {
+        const kernel0 = new Mock.KernelMock({});
+        const kernel1 = kernel0.clone();
+        expect(kernel0.id).toBe(kernel1.id);
+        expect(kernel0.clientId).not.toBe(kernel1.clientId);
+      });
+    });
+
+    describe('.info', () => {
+      it('should resolve with info', async () => {
+        const kernel = new Mock.KernelMock({});
+        const info = await kernel.info;
+        expect(info.language_info.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const kernel = new Mock.KernelMock({});
+        await kernel.shutdown();
+      });
+    });
+
+    describe('.requestHistory()', () => {
+      it('should get the history info', async () => {
+        const kernel = new Mock.KernelMock({});
+        const reply = await kernel.requestHistory({} as any);
+        expect(reply.content.status).toBe('ok');
+      });
+    });
+
+    describe('.restart()', () => {
+      it('should be a no-op', async () => {
+        const kernel = new Mock.KernelMock({});
+        await kernel.restart();
+      });
+    });
+
+    describe('.requestExecute()', () => {
+      it('should request execution', async () => {
+        const kernel = new Mock.KernelMock({});
+        let called = false;
+        kernel.iopubMessage.connect((_, msg) => {
+          if (msg.header.msg_type === 'execute_input') {
+            called = true;
+          }
+        });
+        const future = kernel.requestExecute({ code: 'foo ' });
+        await future.done;
+        expect(called).toBe(true);
+      });
+    });
+  });
+
+  describe('SessionConnectionMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new SessionConnectionMock', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should take options', () => {
+        const kernel = new Mock.KernelMock({});
+        const session = new Mock.SessionConnectionMock(
+          { model: { name: 'foo' } },
+          kernel
+        );
+        expect(session.kernel).toBe(kernel);
+        expect(session.name).toBe('foo');
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        session.dispose();
+      });
+    });
+
+    describe('.changeKernel()', () => {
+      it('should change the kernel', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        const oldId = session.kernel!.id;
+        const kernel = await session.changeKernel({ name: Mock.DEFAULT_NAME });
+        expect(kernel!.id).not.toBe(oldId);
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        await session.shutdown();
+      });
+    });
+
+    describe('.setPath()', () => {
+      it('should set the path', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'path') {
+            called = true;
+          }
+        });
+        await session.setPath('foo');
+        expect(session.path).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.setType()', () => {
+      it('should set the type', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'type') {
+            called = true;
+          }
+        });
+        await session.setType('foo');
+        expect(session.type).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.setName()', () => {
+      it('should set the name', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'name') {
+            called = true;
+          }
+        });
+        await session.setName('foo');
+        expect(session.name).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+  });
+
+  describe('SessionContextMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new context', () => {
+        const context = new Mock.SessionContextMock({}, null);
+        expect(context.session!.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should accept options', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        const context = new Mock.SessionContextMock({ path: 'foo' }, session);
+        expect(context.session).toBe(session);
+        expect(context.path).toBe('foo');
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const context = new Mock.SessionContextMock({}, null);
+        context.dispose();
+      });
+    });
+
+    describe('.initialize()', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.initialize();
+      });
+    });
+
+    describe('.ready', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.ready;
+      });
+    });
+
+    describe('.changeKernel()', () => {
+      it('should change the kernel', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        const oldId = context.session!.kernel!.id;
+        const kernel = await context.changeKernel({ name: Mock.DEFAULT_NAME });
+        expect(kernel!.id).not.toBe(oldId);
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.shutdown();
+      });
+    });
+  });
+
+  describe('ContentsManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.localPath('foo')).toBe('foo');
+      });
+    });
+
+    describe('.newUntitled', () => {
+      it('should create a new text file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'new') {
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        expect(contents.type).toBe('file');
+        expect(called).toBe(true);
+      });
+
+      it('should create a new notebook', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const contents = await manager.newUntitled({ type: 'notebook' });
+        expect(contents.type).toBe('notebook');
+      });
+    });
+
+    describe('.createCheckpoint()', () => {
+      it('should create a checkpoint', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        expect(checkpoint.id).toBeTruthy();
+      });
+    });
+
+    describe('.listCheckpoints()', () => {
+      it('should list the checkpoints', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        const checkpoints = await manager.listCheckpoints(content.path);
+        expect(checkpoints[0].id).toBe(checkpoint.id);
+      });
+    });
+
+    describe('.deleteCheckpoint', () => {
+      it('should delete a checkpoints', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        await manager.deleteCheckpoint(content.path, checkpoint.id);
+        const checkpoints = await manager.listCheckpoints(content.path);
+        expect(checkpoints.length).toBe(0);
+      });
+    });
+
+    describe('.restoreCheckpoint()', () => {
+      it('should restore the contents', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        await manager.save(content.path, { content: 'foo' });
+        const checkpoint = await manager.createCheckpoint(content.path);
+        await manager.save(content.path, { content: 'bar' });
+        await manager.restoreCheckpoint(content.path, checkpoint.id);
+        const newContent = await manager.get(content.path);
+        expect(newContent.content).toBe('foo');
+      });
+    });
+
+    describe('.getModelDBFactory()', () => {
+      it('should return null', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.getModelDBFactory('foo')).toBe(null);
+      });
+    });
+
+    describe('.normalize()', () => {
+      it('should normalize a path', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.normalize('foo/bar/../baz')).toBe('foo/baz');
+      });
+    });
+
+    describe('.localPath', () => {
+      it('should get the local path of a file', () => {
+        const manager = new Mock.ContentsManagerMock();
+        const defaultDrive = manager.driveName('foo');
+        expect(manager.localPath(`${defaultDrive}foo/bar`)).toBe('foo/bar');
+      });
+    });
+
+    describe('.get()', () => {
+      it('should get the file contents', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        await manager.save(content.path, { content: 'foo' });
+        const newContent = await manager.get(content.path);
+        expect(newContent.content).toBe('foo');
+      });
+    });
+
+    describe('.driveName()', () => {
+      it('should get the drive name of the path', () => {
+        const manager = new Mock.ContentsManagerMock();
+        const defaultDrive = manager.driveName('foo');
+        expect(manager.driveName(`${defaultDrive}/bar`)).toBe(defaultDrive);
+      });
+    });
+
+    describe('.rename()', () => {
+      it('should rename the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'rename') {
+            expect(args.newValue!.path).toBe('foo');
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.rename(contents.path, 'foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.delete()', () => {
+      it('should delete the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'delete') {
+            expect(args.newValue).toBe(null);
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.delete(contents.path);
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.save()', () => {
+      it('should save the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'save') {
+            expect(args.newValue!.content).toBe('bar');
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.save(contents.path, { content: 'bar' });
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const manager = new Mock.ContentsManagerMock();
+        manager.dispose();
+      });
+    });
+  });
+
+  describe('SessionManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new session manager', () => {
+        const manager = new Mock.SessionManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.startNew()', () => {
+      it('should start a new session', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.connectTo()', () => {
+      it('should connect to a session', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.connectTo({
+          model: {
+            id: 'fizz',
+            path: 'foo',
+            type: 'bar',
+            name: 'baz',
+            kernel: { name: Mock.DEFAULT_NAME, id: 'fuzz' }
+          }
+        });
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.stopIfNeeded()', () => {
+      it('should remove a running kernel', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(toArray(manager.running()).length).toBe(1);
+        await manager.stopIfNeeded(session.path);
+        expect(toArray(manager.running()).length).toBe(0);
+      });
+    });
+
+    describe('.refreshRunning()', () => {
+      it('should be a no-op', async () => {
+        const manager = new Mock.SessionManagerMock();
+        await manager.refreshRunning();
+      });
+    });
+
+    describe('.running()', () => {
+      it('should be an iterable of running sessions', async () => {
+        const manager = new Mock.SessionManagerMock();
+        await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(toArray(manager.running()).length).toBe(1);
+      });
+    });
+  });
+
+  describe('KernelSpecManagerMock', () => {
+    describe('.constructor', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.specs', () => {
+      it('should be the kernel specs', () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        expect(manager.specs!.default).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.refreshSpecs()', () => {
+      it('should be a no-op', async () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        await manager.refreshSpecs();
+      });
+    });
+  });
+
+  describe('ServiceManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.ready', () => {
+      it('should resolve', async () => {
+        const manager = new Mock.ServiceManagerMock();
+        await manager.ready;
+      });
+    });
+
+    describe('.contents', () => {
+      it('should be a contents manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.contents.normalize).toBeTruthy();
+      });
+    });
+
+    describe('.sessions', () => {
+      it('should be a sessions manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.sessions.isReady).toBe(true);
+      });
+    });
+
+    describe('.kernelspecs', () => {
+      it('should be a kernelspecs manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.kernelspecs.isReady).toBe(true);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const manager = new Mock.ServiceManagerMock();
+        manager.dispose();
+      });
+    });
+  });
+
+  describe('MockShellFuture', () => {
+    it('should create a new mock', async () => {
+      const msg = KernelMessage.createMessage({
+        channel: 'shell',
+        msgType: 'execute_reply',
+        session: 'foo',
+        username: 'bar',
+        msgId: 'fizz',
+        content: {
+          user_expressions: {},
+          execution_count: 0,
+          status: 'ok'
+        }
+      });
+      const future = new Mock.MockShellFuture(msg);
+      const reply = await future.done;
+      expect(reply).toBe(msg);
+      future.dispose();
+    });
+  });
+
+  describe('createFileContext()', () => {
+    it('should create a context without a kernel', async () => {
+      const context = await Mock.createFileContext();
+      expect(context.sessionContext.session).toBe(null);
+    });
+
+    it('should create a context with a kernel', async () => {
+      const context = await Mock.createFileContext(true);
+      expect(context.sessionContext.session!.kernel!.name).toBe(
+        Mock.DEFAULT_NAME
+      );
+    });
+  });
+});

+ 19 - 0
testutils/test/start_jupyter_server.spec.ts

@@ -0,0 +1,19 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import 'jest';
+
+const fetch = require('node-fetch');
+
+import { JupyterServer } from '../src';
+import { URLExt } from '@jupyterlab/coreutils';
+
+describe('JupyterServer', () => {
+  it('should start the server', async () => {
+    jest.setTimeout(20000);
+    const server = new JupyterServer();
+    const url = await server.start();
+    await fetch(URLExt.join(url, 'api'));
+    await server.shutdown();
+  });
+});

+ 69 - 0
testutils/tsconfig.test.json

@@ -0,0 +1,69 @@
+{
+  "extends": "../tsconfigbase.test",
+  "include": ["src/*", "test/*"],
+  "references": [
+    {
+      "path": "../packages/apputils"
+    },
+    {
+      "path": "../packages/cells"
+    },
+    {
+      "path": "../packages/codeeditor"
+    },
+    {
+      "path": "../packages/codemirror"
+    },
+    {
+      "path": "../packages/coreutils"
+    },
+    {
+      "path": "../packages/docregistry"
+    },
+    {
+      "path": "../packages/nbformat"
+    },
+    {
+      "path": "../packages/notebook"
+    },
+    {
+      "path": "../packages/rendermime"
+    },
+    {
+      "path": "../packages/services"
+    },
+    {
+      "path": "."
+    },
+    {
+      "path": "../packages/apputils"
+    },
+    {
+      "path": "../packages/cells"
+    },
+    {
+      "path": "../packages/codeeditor"
+    },
+    {
+      "path": "../packages/codemirror"
+    },
+    {
+      "path": "../packages/coreutils"
+    },
+    {
+      "path": "../packages/docregistry"
+    },
+    {
+      "path": "../packages/nbformat"
+    },
+    {
+      "path": "../packages/notebook"
+    },
+    {
+      "path": "../packages/rendermime"
+    },
+    {
+      "path": "../packages/services"
+    }
+  ]
+}

+ 2 - 1
tsconfig.eslint.json

@@ -16,6 +16,7 @@
     "packages/**/stories/*",
     "scripts/*",
     "jupyterlab/*",
-    "jupyterlab/staging/*"
+    "jupyterlab/staging/*",
+    "testutils/**/*"
   ]
 }