浏览代码

Merge pull request #8357 from blink1073/test-cleanup-2

Test Modernization Follow up  2
Steven Silvester 5 年之前
父节点
当前提交
e9ea2d5a76

+ 1 - 1
.eslintignore

@@ -7,7 +7,7 @@ node_modules
 **/typings
 **/schemas
 **/themes
-tests/**/coverage
+coverage
 *.map.js
 *.bundle.js
 

+ 10 - 4
.eslintrc.js

@@ -2,7 +2,9 @@ module.exports = {
   env: {
     browser: true,
     es6: true,
-    commonjs: true
+    commonjs: true,
+    node: true,
+    'jest/globals': true
   },
   root: true,
   extends: [
@@ -10,13 +12,14 @@ module.exports = {
     'plugin:@typescript-eslint/eslint-recommended',
     'plugin:@typescript-eslint/recommended',
     'prettier/@typescript-eslint',
-    'plugin:react/recommended'
+    'plugin:react/recommended',
+    'plugin:jest/recommended'
   ],
   parser: '@typescript-eslint/parser',
   parserOptions: {
     project: 'tsconfig.eslint.json'
   },
-  plugins: ['@typescript-eslint'],
+  plugins: ['@typescript-eslint', 'jest'],
   rules: {
     '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
     '@typescript-eslint/interface-name-prefix': [
@@ -41,7 +44,10 @@ module.exports = {
     'no-undef': 'warn',
     'no-case-declarations': 'warn',
     'no-useless-escape': 'off',
-    'prefer-const': 'off'
+    'prefer-const': 'off',
+    'jest/no-jest-import': 'off',
+    'jest/no-export': 'warn',
+    'jest/no-try-expect': 'warn'
   },
   settings: {
     react: {

+ 7 - 2
jupyterlab/tests/test_app.py

@@ -25,6 +25,7 @@ from jupyter_core.application import base_aliases, base_flags
 
 from jupyterlab_server.process_app import ProcessApp
 import jupyterlab_server
+from jupyterlab.utils import deprecated
 
 
 HERE = osp.realpath(osp.dirname(__file__))
@@ -210,7 +211,7 @@ jest_flags['watchAll'] = (
 
 
 class JestApp(ProcessTestApp):
-    """A notebook app that runs a jest test."""
+    """DEPRECATED: A notebook app that runs a jest test."""
 
     coverage = Bool(False, help='Whether to run coverage').tag(config=True)
 
@@ -230,6 +231,7 @@ class JestApp(ProcessTestApp):
 
     open_browser = False
 
+    @deprecated(removed_version=4)
     def get_command(self):
         """Get the command to run"""
         terminalsAvailable = self.web_app.settings['terminals_available']
@@ -288,12 +290,13 @@ class JestApp(ProcessTestApp):
 
 
 class KarmaTestApp(ProcessTestApp):
-    """A notebook app that runs the jupyterlab karma tests.
+    """DEPRECATED: A notebook app that runs the jupyterlab karma tests.
     """
     karma_pattern = Unicode('src/*.spec.ts*')
     karma_base_dir = Unicode('')
     karma_coverage_dir = Unicode('')
 
+    @deprecated(removed_version=4)
     def get_command(self):
         """Get the command to run."""
         terminalsAvailable = self.web_app.settings['terminals_available']
@@ -353,6 +356,7 @@ class KarmaTestApp(ProcessTestApp):
         return cmd, dict(env=env, cwd=cwd)
 
 
+@deprecated(removed_version=4)
 def run_jest(jest_dir):
     """Run a jest test in the given base directory.
     """
@@ -362,6 +366,7 @@ def run_jest(jest_dir):
     app.start()
 
 
+@deprecated(removed_version=4)
 def run_karma(base_dir, coverage_dir=''):
     """Run a karma test in the given base directory.
     """

+ 65 - 0
jupyterlab/utils.py

@@ -0,0 +1,65 @@
+import functools
+import warnings
+
+
+class jupyterlab_deprecation(Warning):
+    """Create our own deprecation class, since Python >= 2.7
+    silences deprecations by default.
+    """
+    pass
+
+
+class deprecated(object):
+    """Decorator to mark deprecated functions with warning.
+    Adapted from `scikit-image/skimage/_shared/utils.py`.
+
+    Parameters
+    ----------
+    alt_func : str
+        If given, tell user what function to use instead.
+    behavior : {'warn', 'raise'}
+        Behavior during call to deprecated function: 'warn' = warn user that
+        function is deprecated; 'raise' = raise error.
+    removed_version : str
+        The package version in which the deprecated function will be removed.
+    """
+
+    def __init__(self, alt_func=None, behavior='warn', removed_version=None):
+        self.alt_func = alt_func
+        self.behavior = behavior
+        self.removed_version = removed_version
+
+    def __call__(self, func):
+
+        alt_msg = ''
+        if self.alt_func is not None:
+            alt_msg = ' Use ``%s`` instead.' % self.alt_func
+        rmv_msg = ''
+        if self.removed_version is not None:
+            rmv_msg = (' and will be removed in version %s' %
+                       self.removed_version)
+
+        msg = ('Function ``%s`` is deprecated' % func.__name__ +
+               rmv_msg + '.' + alt_msg)
+
+        @functools.wraps(func)
+        def wrapped(*args, **kwargs):
+            if self.behavior == 'warn':
+                func_code = func.__code__
+                warnings.simplefilter('always', jupyterlab_deprecation)
+                warnings.warn_explicit(msg,
+                                       category=jupyterlab_deprecation,
+                                       filename=func_code.co_filename,
+                                       lineno=func_code.co_firstlineno + 1)
+            elif self.behavior == 'raise':
+                raise jupyterlab_deprecation(msg)
+            return func(*args, **kwargs)
+
+        # modify doc string to display deprecation warning
+        doc = '**Deprecated function**.' + alt_msg
+        if wrapped.__doc__ is None:
+            wrapped.__doc__ = doc
+        else:
+            wrapped.__doc__ = doc + '\n\n    ' + wrapped.__doc__
+
+        return wrapped

+ 1 - 0
package.json

@@ -94,6 +94,7 @@
     "@typescript-eslint/parser": "^2.27.0",
     "eslint": "^6.8.0",
     "eslint-config-prettier": "^6.7.0",
+    "eslint-plugin-jest": "^23.8.2",
     "eslint-plugin-prettier": "^3.1.1",
     "eslint-plugin-react": "^7.19.0",
     "husky": "^3.1.0",

+ 3 - 3
packages/application/src/frontend.ts

@@ -116,13 +116,13 @@ export abstract class JupyterFrontEnd<
    * event, testing each HTMLElement ancestor for a user-supplied funcion. This can
    * be used to find an HTMLElement on which to operate, given a context menu click.
    *
-   * @param test - a function that takes an `HTMLElement` and returns a
+   * @param fn - a function that takes an `HTMLElement` and returns a
    *   boolean for whether it is the element the requester is seeking.
    *
    * @returns an HTMLElement or undefined, if none is found.
    */
   contextMenuHitTest(
-    test: (node: HTMLElement) => boolean
+    fn: (node: HTMLElement) => boolean
   ): HTMLElement | undefined {
     if (
       !this._contextMenuEvent ||
@@ -132,7 +132,7 @@ export abstract class JupyterFrontEnd<
     }
     let node: Node | null = this._contextMenuEvent.target;
     do {
-      if (node instanceof HTMLElement && test(node)) {
+      if (node instanceof HTMLElement && fn(node)) {
         return node;
       }
       node = node.parentNode;

+ 1 - 1
packages/cells/test/widget.spec.ts

@@ -794,7 +794,7 @@ describe('cells/widget', () => {
         await expect(future1).rejects.toThrow('Canceled');
         expect(widget.promptNode.textContent).toEqual('[*]:');
         const msg = await future2;
-        expect(msg).not.toBeUndefined;
+        expect(msg).not.toBeUndefined();
 
         // The `if` is a Typescript type guard so that msg.content works below.
         if (msg) {

+ 1 - 1
packages/codemirror/test/factory.spec.ts

@@ -43,7 +43,7 @@ describe('CodeMirrorEditorFactory', () => {
       expect(factory).toBeInstanceOf(CodeMirrorEditorFactory);
     });
 
-    it('should create a CodeMirrorEditorFactory', () => {
+    it('should create a CodeMirrorEditorFactory with options', () => {
       const factory = new ExposeCodeMirrorEditorFactory(options);
       expect(factory).toBeInstanceOf(CodeMirrorEditorFactory);
       expect(factory.inlineCodeMirrorConfig.extraKeys).toEqual(

+ 1 - 1
packages/coreutils/test/pageconfig.spec.ts

@@ -34,7 +34,7 @@ describe('@jupyterlab/coreutils', () => {
         expect(PageConfig.setOption('bar', 'foo')).toBe('');
       });
 
-      it('should get a known option', () => {
+      it('should get a different known option', () => {
         expect(PageConfig.getOption('bar')).toBe('foo');
       });
     });

+ 7 - 14
packages/coreutils/test/url.spec.ts

@@ -20,25 +20,18 @@ describe('@jupyterlab/coreutils', () => {
       it('should handle query and hash', () => {
         const url = "http://example.com/path?that's#all, folks";
         const obj = URLExt.parse(url);
-        try {
-          expect(obj.href).toBe(
-            'http://example.com/path?that%27s#all,%20folks'
-          );
-        } catch (e) {
-          // Chrome
-          expect(obj.href).toBe('http://example.com/path?that%27s#all, folks');
-        }
+        // Chrome has a different href
+        expect([
+          'http://example.com/path?that%27s#all,%20folks',
+          'http://example.com/path?that%27s#all, folks'
+        ]).toContain(obj.href);
         expect(obj.protocol).toBe('http:');
         expect(obj.host).toBe('example.com');
         expect(obj.hostname).toBe('example.com');
         expect(obj.search).toBe('?that%27s');
         expect(obj.pathname).toBe('/path');
-        try {
-          expect(obj.hash).toBe('#all,%20folks');
-        } catch (e) {
-          // Chrome
-          expect(obj.hash).toBe('#all, folks');
-        }
+        // Chrome has a different hash
+        expect(['#all,%20folks', '#all, folks']).toContain(obj.hash);
       });
     });
 

+ 3 - 6
packages/docregistry/test/context.spec.ts

@@ -149,12 +149,9 @@ describe('docregistry/context', () => {
           called += 1;
         });
 
-        try {
-          await context.initialize(true);
-        } catch (err) {
-          expect(err.message).toContain('Invalid response: 403 Forbidden');
-        }
-
+        await expect(context.initialize(true)).rejects.toThrowError(
+          'Invalid response: 403 Forbidden'
+        );
         expect(called).toBe(2);
         expect(checked).toBe('failed');
 

+ 1 - 1
packages/logconsole/test/logger.spec.ts

@@ -342,7 +342,7 @@ describe('Logger', () => {
       s.dispose();
     });
 
-    it('emits an "append" content changed signal', () => {
+    it('emits an "append" content changed signal and log outputs', () => {
       const s = new SignalLogger(logger.contentChanged);
       logger.log({
         type: 'output',

+ 22 - 25
packages/notebook/test/default-toolbar.spec.ts

@@ -19,31 +19,18 @@ import {
 } from '../src';
 
 import {
-  initNotebookContext,
   signalToPromise,
   sleep,
   framePromise,
-  acceptDialog,
-  flakyIt as it
+  acceptDialog
 } from '@jupyterlab/testutils';
 
-import { JupyterServer } from '@jupyterlab/testutils/lib/start_jupyter_server';
-
 import * as utils from './utils';
+import { PromiseDelegate } from '@lumino/coreutils';
+import { KernelMessage } from '@jupyterlab/services';
 
 const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
 
-const server = new JupyterServer();
-
-beforeAll(async () => {
-  jest.setTimeout(20000);
-  await server.start();
-});
-
-afterAll(async () => {
-  await server.shutdown();
-});
-
 describe('@jupyterlab/notebook', () => {
   describe('ToolbarItems', () => {
     describe('noKernel', () => {
@@ -51,7 +38,7 @@ describe('@jupyterlab/notebook', () => {
       let panel: NotebookPanel;
 
       beforeEach(async () => {
-        context = await initNotebookContext();
+        context = await utils.createMockContext();
         panel = utils.createNotebookPanel(context);
         context.model.fromJSON(utils.DEFAULT_CONTENT);
       });
@@ -247,7 +234,7 @@ describe('@jupyterlab/notebook', () => {
       let panel: NotebookPanel;
 
       beforeEach(async function() {
-        context = await initNotebookContext({ startKernel: true });
+        context = await utils.createMockContext(true);
         panel = utils.createNotebookPanel(context);
         context.model.fromJSON(utils.DEFAULT_CONTENT);
       });
@@ -272,10 +259,16 @@ describe('@jupyterlab/notebook', () => {
           widget.select(mdCell);
 
           Widget.attach(button, document.body);
-          await framePromise();
-          simulate(button.node.firstChild as HTMLElement, 'mousedown');
-          await framePromise();
           await context.sessionContext.session!.kernel!.info;
+
+          const delegate = new PromiseDelegate();
+          panel.sessionContext.iopubMessage.connect((_, msg) => {
+            if (KernelMessage.isExecuteInputMsg(msg)) {
+              delegate.resolve(void 0);
+            }
+          });
+          simulate(button.node.firstChild as HTMLElement, 'mousedown');
+          await delegate.promise;
           button.dispose();
         });
 
@@ -299,12 +292,16 @@ describe('@jupyterlab/notebook', () => {
           mdCell.rendered = false;
 
           Widget.attach(button, document.body);
-          await context.sessionContext.ready;
-          await framePromise();
+          await panel.sessionContext.ready;
+          const delegate = new PromiseDelegate();
+          panel.sessionContext.iopubMessage.connect((_, msg) => {
+            if (KernelMessage.isExecuteInputMsg(msg)) {
+              delegate.resolve(void 0);
+            }
+          });
           simulate(button.node.firstChild as HTMLElement, 'mousedown');
           await acceptDialog();
-          await framePromise();
-          await context.sessionContext.session!.kernel!.info;
+          await delegate.promise;
           button.dispose();
         });
 

+ 4 - 4
packages/notebook/test/model.spec.ts

@@ -310,8 +310,8 @@ describe('@jupyterlab/notebook', () => {
       it('should have default values', () => {
         const model = new NotebookModel();
         const metadata = model.metadata;
-        expect(metadata.has('kernelspec'));
-        expect(metadata.has('language_info'));
+        expect(metadata.has('kernelspec')).toBeTruthy();
+        expect(metadata.has('language_info')).toBeTruthy();
         expect(metadata.size).toBe(2);
       });
 
@@ -392,12 +392,12 @@ describe('@jupyterlab/notebook', () => {
           expect(cell.type).toBe('code');
         });
 
-        it('should create a new code cell', () => {
+        it('should create a new markdown cell', () => {
           const cell = factory.createCell('markdown', {});
           expect(cell.type).toBe('markdown');
         });
 
-        it('should create a new code cell', () => {
+        it('should create a new raw cell', () => {
           const cell = factory.createCell('raw', {});
           expect(cell.type).toBe('raw');
         });

+ 34 - 2
packages/notebook/test/utils.ts

@@ -3,9 +3,16 @@
 
 import { Context } from '@jupyterlab/docregistry';
 
-import { INotebookModel, NotebookPanel, Notebook, NotebookModel } from '../src';
+import {
+  INotebookModel,
+  NotebookPanel,
+  Notebook,
+  NotebookModel,
+  NotebookModelFactory
+} from '../src';
 
-import { NBTestUtils } from '@jupyterlab/testutils';
+import { NBTestUtils, Mock } from '@jupyterlab/testutils';
+import { UUID } from '@lumino/coreutils';
 
 /**
  * Local versions of the NBTestUtils that import from `src` instead of `lib`.
@@ -70,3 +77,28 @@ export const clipboard = NBTestUtils.clipboard;
 export function defaultRenderMime() {
   return NBTestUtils.defaultRenderMime();
 }
+
+/**
+ * Create a context for a file.
+ */
+export async function createMockContext(
+  startKernel = false
+): Promise<Context<INotebookModel>> {
+  const path = UUID.uuid4() + '.txt';
+  const manager = new Mock.ServiceManagerMock();
+  const factory = new NotebookModelFactory({});
+
+  const context = new Context({
+    manager,
+    factory,
+    path,
+    kernelPreference: {
+      shouldStart: startKernel,
+      canStart: startKernel,
+      autoStartDefault: startKernel
+    }
+  });
+  await context.initialize(true);
+  await context.sessionContext.initialize();
+  return context;
+}

+ 1 - 1
packages/notebook/test/widget.spec.ts

@@ -1195,7 +1195,7 @@ describe('@jupyter/notebook', () => {
           expect(widget.activeCell).toBe(child);
         });
 
-        it.only('should extend selection if invoked with shift', () => {
+        it('should extend selection if invoked with shift', () => {
           widget.activeCellIndex = 3;
 
           // shift click below

+ 1 - 1
packages/rendermime/test/factories.spec.ts

@@ -230,7 +230,7 @@ describe('rendermime/factories', () => {
         expect(w.node.innerHTML).toBe(source);
       });
 
-      it.only('should add header anchors', async () => {
+      it('should add header anchors', async () => {
         const f = markdownRendererFactory;
         const mimeType = 'text/markdown';
         const model = createModel(mimeType, sampleData);

+ 1 - 1
packages/services/test/kernel/ikernel.spec.ts

@@ -987,7 +987,7 @@ describe('Kernel.IKernel', () => {
       const metadata = { cellId: 'test' };
       const future = defaultKernel.requestExecute(options, false, metadata);
       await future.done;
-      expect((future.msg.metadata = metadata));
+      expect(future.msg.metadata).toEqual(metadata);
     });
   });
 

+ 0 - 1
packages/services/test/kernelspec/manager.spec.ts

@@ -43,7 +43,6 @@ describe('kernel/manager', () => {
 
   beforeEach(() => {
     manager = new KernelSpecManager({ standby: 'never' });
-    expect(manager.specs).toBeNull();
     return manager.ready;
   });
 

+ 3 - 7
packages/services/test/session/isession.spec.ts

@@ -430,13 +430,9 @@ describe('session', () => {
       it('should handle a specific error status', async () => {
         handleRequest(defaultSession, 410, {});
         const promise = defaultSession.shutdown();
-        try {
-          await promise;
-          throw Error('should not get here');
-        } catch (err) {
-          const text = 'The kernel was deleted but the session was not';
-          expect(err.message).toEqual(text);
-        }
+        await expect(promise).rejects.toThrow(
+          'The kernel was deleted but the session was not'
+        );
       });
 
       it('should fail for an error response status', async () => {

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

@@ -1,70 +0,0 @@
-const path = require('path');
-const glob = require('glob');
-const fs = require('fs-extra');
-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
-const testSrc = path.join(__dirname, target);
-
-console.log(testSrc); // eslint-disable-line
-if (!fs.existsSync(testSrc)) {
-  console.log('bailing'); // eslint-disable-line
-  process.exit(1);
-}
-
-const name = target.replace('test-', '');
-
-// Update the test files
-glob.sync(path.join(testSrc, 'src', '**', '*.ts*')).forEach(function(filePath) {
-  console.log(filePath); // eslint-disable-line
-  // Convert test files to use jest
-  let src = fs.readFileSync(filePath, 'utf8');
-  src = src.split('before(').join('beforeAll(');
-  src = src.split('context(').join('describe(');
-  src = src.split('after(').join('afterAll(');
-
-  // Use imports from /src
-  src = src.split(`'@jupyterlab/${name}';`).join(`'@jupyterlab/${name}/src';`);
-
-  fs.writeFileSync(filePath, src, 'utf8');
-});
-
-// Open coreutils package.json
-const coreUtilsData = require('./test-coreutils/package.json');
-
-// Open target package.json
-const targetData = utils.readJSONFile(path.join(testSrc, 'package.json'));
-
-// Assign scripts from coreutils
-targetData.scripts = coreUtilsData.scripts;
-
-// Assign dependencies from coreutils
-['jest', 'ts-jest', '@jupyterlab/testutils'].forEach(name => {
-  targetData.dependencies[name] = coreUtilsData.dependencies[name];
-});
-
-// Assign devDependencies from coreutils
-targetData.devDependencies = coreUtilsData.devDependencies;
-
-// Write out the package.json file.
-utils.writeJSONFile(path.join(testSrc, 'package.json'), targetData);
-
-// Update tsconfig to use jest types.
-const tsData = utils.readJSONFile(path.join(testSrc, 'tsconfig.json'));
-const index = tsData.compilerOptions.types.indexOf('mocha');
-tsData.compilerOptions.types[index] = 'jest';
-utils.writeJSONFile(path.join(testSrc, 'tsconfig.json'), tsData);
-
-// Git remove old tests infra
-['karma-cov.conf.js', 'karma.conf.js', 'run-test.py'].forEach(fname => {
-  utils.run(`git rm -f ./test-${name}/${fname} || true`);
-});

+ 0 - 23
tests/karma-cov.conf.js

@@ -1,23 +0,0 @@
-const path = require('path');
-const baseConf = require('./karma.conf');
-
-module.exports = function(config) {
-  baseConf(config);
-  config.reporters = ['mocha', 'coverage-istanbul'];
-  config.webpack.module.rules.push(
-    // instrument only testing sources with Istanbul
-    {
-      test: /\.js$/,
-      use: {
-        loader: 'istanbul-instrumenter-loader',
-        options: { esModules: true }
-      },
-      include: process.env.KARMA_COVER_FOLDER
-    }
-  );
-  config.coverageIstanbulReporter = {
-    reports: ['html', 'text-summary'],
-    dir: path.join('.', 'coverage'),
-    fixWebpackSourcePaths: true
-  };
-};

+ 0 - 42
tests/karma.conf.js

@@ -1,42 +0,0 @@
-const path = require('path');
-const webpack = require('./webpack.config');
-
-process.env.CHROME_BIN = require('puppeteer').executablePath();
-
-module.exports = function(config) {
-  config.set({
-    basePath: '.',
-    frameworks: ['mocha'],
-    reporters: ['mocha'],
-    client: {
-      captureConsole: true,
-      mocha: {
-        timeout: 120000, // 120 seconds - upped from 2 seconds
-        retries: 3 // Allow for slow server on CI.
-      }
-    },
-    files: [
-      { pattern: path.resolve('./build/injector.js'), watched: false },
-      { pattern: process.env.KARMA_FILE_PATTERN, watched: false }
-    ],
-    preprocessors: {
-      'build/injector.js': ['webpack'],
-      'src/*.spec.{ts,tsx}': ['webpack', 'sourcemap']
-    },
-    mime: {
-      'text/x-typescript': ['ts', 'tsx']
-    },
-    webpack: webpack,
-    webpackMiddleware: {
-      noInfo: true,
-      stats: 'errors-only'
-    },
-    browserNoActivityTimeout: 61000, // 61 seconds - upped from 10 seconds
-    browserDisconnectTimeout: 61000, // 61 seconds - upped from 2 seconds
-    browserDisconnectTolerance: 2,
-    port: 9876,
-    colors: true,
-    singleRun: true,
-    logLevel: config.LOG_INFO
-  });
-};

+ 0 - 112
tests/modernize.js

@@ -1,112 +0,0 @@
-/* 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',
-  'test:debug:watch',
-  '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` and import "jest"
-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';`);
-  let lines = src.split('\n');
-  let i = 0;
-  while (!lines[i++].startsWith('//')) {
-    // no-op
-  }
-  lines.splice(i, 0, ["\nimport 'jest';"]);
-  fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
-});
-
-// 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 --force', { cwd: testSrc });
-
-// Move the test config files to `/packages/{name}`
-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}`);
-});
-utils.run(`git add ${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`);
-
-// Remove local folder
-utils.run(`git rm -rf ${testSrc} && rm -rf ${testSrc}`);
-
-// Run integrity
-const rootDir = path.resolve('..');
-utils.run(`jlpm integrity`, {
-  cwd: rootDir
-});

+ 0 - 77
tests/package.json

@@ -1,77 +0,0 @@
-{
-  "name": "@jupyterlab/test-root",
-  "version": "2.1.0",
-  "private": true,
-  "dependencies": {
-    "@babel/preset-env": "^7.7.6",
-    "@jupyterlab/apputils": "^2.1.0",
-    "@jupyterlab/cells": "^2.1.0",
-    "@jupyterlab/codeeditor": "^2.1.0",
-    "@jupyterlab/codemirror": "^2.1.0",
-    "@jupyterlab/completer": "^2.1.0",
-    "@jupyterlab/console": "^2.1.0",
-    "@jupyterlab/coreutils": "^4.1.0",
-    "@jupyterlab/csvviewer": "^2.1.0",
-    "@jupyterlab/docmanager": "^2.1.0",
-    "@jupyterlab/docregistry": "^2.1.0",
-    "@jupyterlab/filebrowser": "^2.1.0",
-    "@jupyterlab/fileeditor": "^2.1.0",
-    "@jupyterlab/imageviewer": "^2.1.0",
-    "@jupyterlab/inspector": "^2.1.0",
-    "@jupyterlab/mainmenu": "^2.1.0",
-    "@jupyterlab/mathjax2-extension": "^2.1.0",
-    "@jupyterlab/notebook": "^2.1.0",
-    "@jupyterlab/observables": "^3.1.0",
-    "@jupyterlab/outputarea": "^2.1.0",
-    "@jupyterlab/rendermime": "^2.1.0",
-    "@jupyterlab/services": "^5.1.0",
-    "@jupyterlab/terminal": "^2.1.0",
-    "@jupyterlab/testutils": "^2.1.0",
-    "@lumino/algorithm": "^1.2.3",
-    "@lumino/commands": "^1.10.1",
-    "@lumino/coreutils": "^1.4.2",
-    "@lumino/disposable": "^1.3.5",
-    "@lumino/domutils": "^1.1.7",
-    "@lumino/messaging": "^1.3.3",
-    "@lumino/signaling": "^1.3.5",
-    "@lumino/virtualdom": "^1.6.1",
-    "@lumino/widgets": "^1.11.1",
-    "chai": "^4.2.0",
-    "expect.js": "~0.3.1",
-    "json-to-html": "~0.1.2",
-    "karma-babel-preprocessor": "^8.0.1",
-    "react": "~16.9.0",
-    "simulate-event": "~1.4.0"
-  },
-  "devDependencies": {
-    "css-loader": "~3.2.0",
-    "es6-promise": "~4.2.8",
-    "file-loader": "~5.0.2",
-    "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",
-    "karma-coverage": "^2.0.1",
-    "karma-coverage-istanbul-reporter": "^2.1.1",
-    "karma-firefox-launcher": "^1.2.0",
-    "karma-ie-launcher": "^1.0.0",
-    "karma-mocha": "^1.3.0",
-    "karma-mocha-reporter": "^2.2.5",
-    "karma-sourcemap-loader": "~0.3.7",
-    "karma-webpack": "^4.0.2",
-    "mocha": "^7.1.1",
-    "puppeteer": "~2.0.0",
-    "raw-loader": "~4.0.0",
-    "rimraf": "~3.0.0",
-    "style-loader": "~1.0.1",
-    "svg-url-loader": "~3.0.3",
-    "thread-loader": "^2.1.3",
-    "ts-loader": "^6.2.1",
-    "typescript": "~3.7.3",
-    "url-loader": "~3.0.0",
-    "webpack": "^4.41.2"
-  }
-}

+ 0 - 71
tests/webpack.config.js

@@ -1,71 +0,0 @@
-// Use sourcemaps if in watch or debug mode;
-const devtool =
-  process.argv.indexOf('--watch') !== -1 ||
-  process.argv.indexOf('--debug') !== -1
-    ? 'source-map-inline'
-    : 'eval';
-
-module.exports = {
-  resolve: {
-    extensions: ['.ts', '.tsx', '.js']
-  },
-  bail: true,
-  devtool: devtool,
-  mode: 'development',
-  node: {
-    fs: 'empty'
-  },
-  module: {
-    rules: [
-      {
-        test: /\.tsx?$/,
-        use: [{ loader: 'ts-loader', options: { context: process.cwd() } }]
-      },
-      {
-        test: /\.js$/,
-        use: ['source-map-loader'],
-        enforce: 'pre',
-        // eslint-disable-next-line no-undef
-        exclude: /node_modules/
-      },
-      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
-      { test: /\.csv$/, use: 'raw-loader' },
-      { test: /\.ipynb$/, use: 'json-loader' },
-      { test: /\.html$/, use: 'file-loader' },
-      { test: /\.md$/, use: 'raw-loader' },
-      { test: /\.(jpg|png|gif)$/, use: 'file-loader' },
-      { test: /\.js.map$/, use: 'file-loader' },
-      {
-        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
-        use: 'url-loader?limit=10000&mimetype=application/font-woff'
-      },
-      {
-        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
-        use: 'url-loader?limit=10000&mimetype=application/font-woff'
-      },
-      {
-        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
-        use: 'url-loader?limit=10000&mimetype=application/octet-stream'
-      },
-      { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
-      {
-        // In .css files, svg is loaded as a data URI.
-        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
-        issuer: { test: /\.css$/ },
-        use: {
-          loader: 'svg-url-loader',
-          options: { encoding: 'none', limit: 10000 }
-        }
-      },
-      {
-        // In .ts and .tsx files (both of which compile to .js), svg files
-        // must be loaded as a raw string instead of data URIs.
-        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
-        issuer: { test: /\.js$/ },
-        use: {
-          loader: 'raw-loader'
-        }
-      }
-    ]
-  }
-};

+ 7 - 1
testutils/src/common.ts

@@ -264,9 +264,15 @@ export async function initNotebookContext(
   } = {}
 ): Promise<Context<INotebookModel>> {
   const factory = Private.notebookFactory;
-
   const manager = options.manager || Private.getManager();
   const path = options.path || UUID.uuid4() + '.ipynb';
+  console.debug(
+    'Initializing notebook context for',
+    path,
+    'kernel:',
+    options.startKernel
+  );
+
   const startKernel =
     options.startKernel === undefined ? false : options.startKernel;
   await manager.ready;

+ 5 - 6
testutils/src/flakyIt.ts

@@ -29,12 +29,7 @@ async function runTest(fn: any): Promise<void> {
  * @param retries The number of retries
  * @param wait The time to wait in milliseconds between retries
  */
-export async function flakyIt(
-  name: string,
-  fn: any,
-  retries = 3,
-  wait = 1000
-): Promise<void> {
+export function flakyIt(name: string, fn: any, retries = 3, wait = 1000): void {
   test(name, async () => {
     let latestError;
     for (let tries = 0; tries < retries; tries++) {
@@ -49,3 +44,7 @@ export async function flakyIt(
     throw latestError;
   });
 }
+
+flakyIt.only = it.only;
+flakyIt.skip = it.skip;
+flakyIt.todo = it.todo;

+ 4 - 0
testutils/src/index.ts

@@ -26,3 +26,7 @@ export {
 } from './common';
 
 export { flakyIt } from './flakyIt';
+
+import * as Mock from './mock';
+
+export { Mock };

+ 23 - 14
testutils/src/jest-shim.ts

@@ -10,6 +10,7 @@ const fetchMod = ((window as any).fetch = require('node-fetch')); // tslint:disa
   /* no-op */
 };
 
+// HACK: Polyfill that allows CodeMirror to render in a JSDOM env.
 const createContextualFragment = (html: string) => {
   const div = document.createElement('div');
   div.innerHTML = html;
@@ -19,11 +20,6 @@ const createContextualFragment = (html: string) => {
 (global as any).Range.prototype.createContextualFragment = (html: string) =>
   createContextualFragment(html);
 
-window.focus = () => {
-  /* no-op */
-};
-
-// HACK: Polyfill that allows codemirror to render in a JSDOM env.
 (window as any).document.createRange = function createRange() {
   return {
     setEnd: () => {
@@ -37,10 +33,32 @@ window.focus = () => {
     createContextualFragment
   };
 };
+// end CodeMirror HACK
+
+window.focus = () => {
+  /* JSDom throws "Not Implemented" */
+};
 
 (window as any).document.elementFromPoint = (left: number, top: number) =>
   document.body;
 
+if (!window.hasOwnProperty('getSelection')) {
+  // Minimal getSelection() that supports a fake selection
+  (window as any).getSelection = function getSelection() {
+    return {
+      _selection: '',
+      selectAllChildren: () => {
+        this._selection = 'foo';
+      },
+      toString: () => {
+        const val = this._selection;
+        this._selection = '';
+        return val;
+      }
+    };
+  };
+}
+
 process.on('unhandledRejection', (error, promise) => {
   console.error('Unhandled promise rejection somewhere in tests');
   if (error) {
@@ -52,12 +70,3 @@ process.on('unhandledRejection', (error, promise) => {
   }
   promise.catch(err => console.error('promise rejected', err));
 });
-
-(window as any).getSelection = function getSelection() {
-  return {
-    selectAllChildren: () => {
-      // no-op
-    },
-    toString: () => ''
-  };
-};

+ 37 - 6
testutils/src/mock.ts

@@ -186,8 +186,8 @@ export const KernelMock = jest.fn<
       });
       return newKernel;
     }),
-    info: jest.fn(Promise.resolve),
-    shutdown: jest.fn(Promise.resolve),
+    info: jest.fn(() => Promise.resolve(void 0)),
+    shutdown: jest.fn(() => Promise.resolve(void 0)),
     requestHistory: jest.fn(() => {
       const historyReply = KernelMessage.createMessage({
         channel: 'shell',
@@ -201,6 +201,7 @@ export const KernelMock = jest.fn<
       });
       return Promise.resolve(historyReply);
     }),
+    restart: jest.fn(() => Promise.resolve(void 0)),
     requestExecute: jest.fn(options => {
       const msgId = UUID.uuid4();
       executionCount++;
@@ -217,7 +218,21 @@ export const KernelMock = jest.fn<
         }
       });
       iopubMessageSignal.emit(msg);
-      return new MockShellFuture();
+      const reply = KernelMessage.createMessage<KernelMessage.IExecuteReplyMsg>(
+        {
+          channel: 'shell',
+          msgType: 'execute_reply',
+          session: thisObject.clientId,
+          username: thisObject.username,
+          msgId,
+          content: {
+            user_expressions: {},
+            execution_count: executionCount,
+            status: 'ok'
+          }
+        }
+      );
+      return new MockShellFuture(reply);
     })
   };
   // Add signals.
@@ -268,7 +283,19 @@ export const SessionConnectionMock = jest.fn<
       return Private.changeKernel(kernel!, partialModel!);
     }),
     selectKernel: jest.fn(),
-    shutdown: jest.fn(() => Promise.resolve(void 0))
+    shutdown: jest.fn(() => Promise.resolve(void 0)),
+    setPath: jest.fn(path => {
+      (thisObject as any).path = path;
+      propertyChangedSignal.emit('path');
+    }),
+    setName: jest.fn(name => {
+      (thisObject as any).name = name;
+      propertyChangedSignal.emit('name');
+    }),
+    setType: jest.fn(type => {
+      (thisObject as any).type = type;
+      propertyChangedSignal.emit('type');
+    })
   };
   const disposedSignal = new Signal<Session.ISessionConnection, undefined>(
     thisObject
@@ -634,10 +661,14 @@ export const ServiceManagerMock = jest.fn<ServiceManager.IManager, []>(() => {
 /**
  * A mock kernel shell future.
  */
-export const MockShellFuture = jest.fn<Kernel.IShellFuture, []>(() => {
+export const MockShellFuture = jest.fn<
+  Kernel.IShellFuture,
+  [KernelMessage.IShellMessage]
+>((result: KernelMessage.IShellMessage) => {
   const thisObject: Kernel.IShellFuture = {
     ...jest.requireActual('@jupyterlab/services'),
-    done: Promise.resolve(void 0)
+    dispose: jest.fn(),
+    done: Promise.resolve(result)
   };
   return thisObject;
 });

+ 34 - 11
testutils/src/start_jupyter_server.ts

@@ -4,8 +4,9 @@ import { spawn, ChildProcess } from 'child_process';
 import * as fs from 'fs';
 import * as path from 'path';
 
-import { PageConfig } from '@jupyterlab/coreutils';
+import { PageConfig, URLExt } from '@jupyterlab/coreutils';
 import { PromiseDelegate, UUID } from '@lumino/coreutils';
+import { sleep } from './common';
 
 /**
  * A Jupyter Server that runs as a child process.
@@ -42,7 +43,7 @@ export class JupyterServer {
     if (Private.child !== null) {
       throw Error('Previous server was not disposed');
     }
-    const startDelegate = new PromiseDelegate();
+    const startDelegate = new PromiseDelegate<void>();
 
     const env = {
       JUPYTER_CONFIG_DIR: Private.handleConfig(),
@@ -58,18 +59,19 @@ export class JupyterServer {
     let started = false;
 
     // Handle server output.
-    function handleOutput(output: string) {
+    const handleOutput = (output: string) => {
       console.debug(output);
 
       if (started) {
         return;
       }
-      if (Private.handleStartup(output)) {
+      const baseUrl = Private.handleStartup(output);
+      if (baseUrl) {
         console.debug('Jupyter Server started');
-        startDelegate.resolve(void 0);
         started = true;
+        void Private.connect(baseUrl, startDelegate);
       }
-    }
+    };
 
     child.stdout.on('data', data => {
       handleOutput(String(data));
@@ -105,7 +107,7 @@ export class JupyterServer {
       if (Private.child) {
         Private.child.kill(9);
       }
-    }, 1000);
+    }, 3000);
 
     return stopDelegate.promise;
   }
@@ -252,10 +254,10 @@ namespace Private {
    *
    * @param output the process output
    *
-   * @returns Whether the process has started.
+   * @returns The baseUrl of the server or `null`.
    */
-  export function handleStartup(output: string): boolean {
-    let baseUrl = '';
+  export function handleStartup(output: string): string | null {
+    let baseUrl: string | null = null;
     output.split('\n').forEach(line => {
       const baseUrlMatch = line.match(/(http:\/\/localhost:\d+\/[^?]*)/);
       if (baseUrlMatch) {
@@ -263,6 +265,27 @@ namespace Private {
         PageConfig.setOption('baseUrl', baseUrl);
       }
     });
-    return baseUrl.length > 0;
+    return baseUrl;
+  }
+
+  /**
+   * Connect to the Jupyter server.
+   */
+  export async function connect(
+    baseUrl: string,
+    startDelegate: PromiseDelegate<void>
+  ): Promise<void> {
+    // eslint-disable-next-line
+    while (true) {
+      try {
+        await fetch(URLExt.join(baseUrl, 'api'));
+        startDelegate.resolve(void 0);
+        return;
+      } catch (e) {
+        // spin until we can connect to the server.
+        console.warn(e);
+        await sleep(1000);
+      }
+    }
   }
 }

文件差异内容过多而无法显示
+ 5 - 534
yarn.lock


部分文件因为文件数量过多而无法显示