Skip to content

Commit

Permalink
Added support for ES modules
Browse files Browse the repository at this point in the history
* Fixes #150.
* Specs and helpers with names ending in .mjs are loaded as ES modules
  (`import("foo.mjs")`).
* All other specs and helpers are loaded as CommonJS modules, as before
  (`require("foo.js")`).
* If using ES modules with Node 10 or 11, run
  `node --experimental-modules /path/to/jasmine` instead of `jasmine`.
  • Loading branch information
sgravrock committed Oct 7, 2020
1 parent a91e8d4 commit c8ebcff
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 34 deletions.
29 changes: 17 additions & 12 deletions lib/jasmine.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var path = require('path'),
util = require('util'),
glob = require('glob'),
Loader = require('./loader'),
CompletionReporter = require('./reporters/completion_reporter'),
ConsoleSpecFilter = require('./filters/console_spec_filter');

Expand All @@ -9,6 +10,7 @@ module.exports.ConsoleReporter = require('./reporters/console_reporter');

function Jasmine(options) {
options = options || {};
this.loader = options.loader || new Loader();
var jasmineCore = options.jasmineCore || require('jasmine-core');
this.jasmineCorePath = path.join(jasmineCore.files.path, 'jasmine.js');
this.jasmine = jasmineCore.boot(jasmineCore);
Expand Down Expand Up @@ -84,16 +86,16 @@ Jasmine.prototype.addMatchers = function(matchers) {
this.env.addMatchers(matchers);
};

Jasmine.prototype.loadSpecs = function() {
this.specFiles.forEach(function(file) {
require(file);
});
Jasmine.prototype.loadSpecs = async function() {
for (const file of this.specFiles) {
await this.loader.load(file);
}
};

Jasmine.prototype.loadHelpers = function() {
this.helperFiles.forEach(function(file) {
require(file);
});
Jasmine.prototype.loadHelpers = async function() {
for (const file of this.helperFiles) {
await this.loader.load(file);
}
};

Jasmine.prototype.loadRequires = function() {
Expand Down Expand Up @@ -238,11 +240,11 @@ var checkExit = function(jasmineRunner) {
};
};

Jasmine.prototype.execute = function(files, filterString) {
Jasmine.prototype.execute = async function(files, filterString) {
this.completionReporter.exitHandler = this.checkExit;

this.loadRequires();
this.loadHelpers();
await this.loadHelpers();
if (!this.defaultReporterConfigured) {
this.configureDefaultReporter({ showColors: this.showingColors });
}
Expand All @@ -262,8 +264,11 @@ Jasmine.prototype.execute = function(files, filterString) {
this.addSpecFiles(files);
}

this.loadSpecs();
await this.loadSpecs();

this.addReporter(this.completionReporter);
this.env.execute();

await new Promise(resolve => {
this.env.execute(null, resolve);
});
};
26 changes: 26 additions & 0 deletions lib/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = Loader;

function Loader(options) {
options = options || {};
this.require_ = options.requireShim || requireShim;
this.import_ = options.importShim || importShim;
}

Loader.prototype.load = function(path) {
if (path.endsWith('.mjs')) {
return this.import_(path);
} else {
return new Promise(resolve => {
this.require_(path);
resolve();
});
}
};

function requireShim(path) {
require(path);
}

function importShim(path) {
return import(path);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 6
"ecmaVersion": 11
},
"rules": {
"no-unused-vars": [
Expand Down
40 changes: 40 additions & 0 deletions spec/esm_integration_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const child_process = require('child_process');


describe('ES module support', function() {
it('supports ES modules', function(done) {
const child = child_process.spawn(
'node',
['--experimental-modules', '../../../bin/jasmine.js', '--config=jasmine.json'],
{
cwd: 'spec/fixtures/esm',
shell: false
}
);
let output = '';
child.stdout.on('data', function(data) {
output += data;
});
child.stderr.on('data', function(data) {
output += data;
});
child.on('close', function(exitCode) {
expect(exitCode).toEqual(0);
// Node < 14 outputs a warning when ES modules are used, e.g.:
// (node:5258) ExperimentalWarning: The ESM module loader is experimental.
// The position of this warning in the output varies. Sometimes it
// occurs before the lines we're interested in but sometimes it's in
// the middle of them.
output = output.replace(/^.*ExperimentalWarning.*$\n/m, '');
expect(output).toContain(
'name_reporter\n' +
'commonjs_helper\n' +
'esm_helper\n' +
'Started\n' +
'Spec: A spec file ending in .js is required as a commonjs module\n' +
'.Spec: A spec file ending in .mjs is imported as an es module\n'
);
done();
});
});
});
2 changes: 2 additions & 0 deletions spec/fixtures/esm/commonjs_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
console.log('commonjs_helper');
require('./commonjs_sentinel');
1 change: 1 addition & 0 deletions spec/fixtures/esm/commonjs_sentinel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// An empty module that we can require, to see if require works.
5 changes: 5 additions & 0 deletions spec/fixtures/esm/commonjs_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('A spec file ending in .js', function() {
it('is required as a commonjs module', function() {
require('./commonjs_sentinel');
});
});
2 changes: 2 additions & 0 deletions spec/fixtures/esm/esm_helper.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './esm_sentinel.mjs';
console.log('esm_helper');
1 change: 1 addition & 0 deletions spec/fixtures/esm/esm_sentinel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// An empty module that will fail if loaded via require(), due to its extension
9 changes: 9 additions & 0 deletions spec/fixtures/esm/esm_spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
describe('A spec file ending in .mjs', function() {
it('is imported as an es module', function() {
// Node probably threw already if we tried to require this file,
// but check anyway just to be sure.
expect(function() {
require('./commonjs_sentinel');
}).toThrowError(/require is not defined/);
});
});
14 changes: 14 additions & 0 deletions spec/fixtures/esm/jasmine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"spec_dir": ".",
"spec_files": [
"commonjs_spec.js",
"esm_spec.mjs"
],
"helpers": [
"name_reporter.js",
"commonjs_helper.js",
"esm_helper.mjs"
],
"stopSpecOnExpectationFailure": false,
"random": false
}
9 changes: 9 additions & 0 deletions spec/fixtures/esm/name_reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
console.log('name_reporter');

beforeAll(function() {
jasmine.getEnv().addReporter({
specStarted: function (event) {
console.log('Spec:', event.fullName);
}
});
});
2 changes: 2 additions & 0 deletions spec/fixtures/require_tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global.require_tester_was_loaded = true;
module.exports = {};
68 changes: 48 additions & 20 deletions spec/jasmine_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ describe('Jasmine', function() {
clearReporters: jasmine.createSpy('clearReporters'),
addMatchers: jasmine.createSpy('addMatchers'),
provideFallbackReporter: jasmine.createSpy('provideFallbackReporter'),
execute: jasmine.createSpy('execute'),
execute: jasmine.createSpy('execute')
.and.callFake(function(ignored, callback) {
callback();
}),
configure: jasmine.createSpy('configure')
}),
Timer: jasmine.createSpy('Timer')
Expand Down Expand Up @@ -52,27 +55,52 @@ describe('Jasmine', function() {
it('add spec files with glob pattern', function() {
expect(this.testJasmine.specFiles).toEqual([]);
this.testJasmine.addSpecFiles(['spec/*.js']);
expect(this.testJasmine.specFiles.map(basename)).toEqual(['command_spec.js', 'jasmine_spec.js', 'load_config_spec.js']);
expect(this.testJasmine.specFiles.map(basename)).toEqual([
'command_spec.js',
'esm_integration_spec.js',
'jasmine_spec.js',
'load_config_spec.js',
'loader_spec.js',
]);
});

it('add spec files with excluded files', function() {
expect(this.testJasmine.specFiles).toEqual([]);
this.testJasmine.addSpecFiles(['spec/*.js', '!spec/command*']);
expect(this.testJasmine.specFiles.map(basename)).toEqual(['jasmine_spec.js', 'load_config_spec.js']);
expect(this.testJasmine.specFiles.map(basename)).toEqual([
'esm_integration_spec.js',
'jasmine_spec.js',
'load_config_spec.js',
'loader_spec.js',
]);
});

it('add spec files with glob pattern to existings files', function() {
var aFile = path.join(this.testJasmine.projectBaseDir, this.testJasmine.specDir, 'spec/command_spec.js');
this.testJasmine.specFiles = [aFile, 'b'];
this.testJasmine.addSpecFiles(['spec/*.js']);
expect(this.testJasmine.specFiles.map(basename)).toEqual(['command_spec.js', 'b', 'jasmine_spec.js', 'load_config_spec.js']);
expect(this.testJasmine.specFiles.map(basename)).toEqual([
'command_spec.js',
'b',
'esm_integration_spec.js',
'jasmine_spec.js',
'load_config_spec.js',
'loader_spec.js',
]);
});

it('add helper files with glob pattern to existings files', function() {
var aFile = path.join(this.testJasmine.projectBaseDir, this.testJasmine.specDir, 'spec/command_spec.js');
this.testJasmine.helperFiles = [aFile, 'b'];
this.testJasmine.addHelperFiles(['spec/*.js']);
expect(this.testJasmine.helperFiles.map(basename)).toEqual(['command_spec.js', 'b', 'jasmine_spec.js', 'load_config_spec.js']);
expect(this.testJasmine.helperFiles.map(basename)).toEqual([
'command_spec.js',
'b',
'esm_integration_spec.js',
'jasmine_spec.js',
'load_config_spec.js',
'loader_spec.js',
]);
});
});

Expand Down Expand Up @@ -331,63 +359,63 @@ describe('Jasmine', function() {
expect(this.testJasmine.showingColors).toBe(false);
});

describe('#execute', function() {
it('uses the default console reporter if no reporters were added', function() {
describe('#execute', function() {
it('uses the default console reporter if no reporters were added', async function() {
spyOn(this.testJasmine, 'configureDefaultReporter');
spyOn(this.testJasmine, 'loadSpecs');

this.testJasmine.execute();
await this.testJasmine.execute();

expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalledWith({showColors: true});
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
expect(this.testJasmine.env.execute).toHaveBeenCalled();
});

it('configures the default console reporter with the right color settings', function() {
it('configures the default console reporter with the right color settings', async function() {
spyOn(this.testJasmine, 'configureDefaultReporter');
spyOn(this.testJasmine, 'loadSpecs');
this.testJasmine.showColors(false);

this.testJasmine.execute();
await this.testJasmine.execute();

expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalledWith({showColors: false});
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
expect(this.testJasmine.env.execute).toHaveBeenCalled();
});

it('does not configure the default reporter if this was already done', function() {
it('does not configure the default reporter if this was already done', async function() {
spyOn(this.testJasmine, 'loadSpecs');

this.testJasmine.configureDefaultReporter({showColors: false});

spyOn(this.testJasmine, 'configureDefaultReporter');

this.testJasmine.execute();
await this.testJasmine.execute();

expect(this.testJasmine.configureDefaultReporter).not.toHaveBeenCalled();
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
expect(this.testJasmine.env.execute).toHaveBeenCalled();
});

it('loads helper files before checking if any reporters were added', function() {
it('loads helper files before checking if any reporters were added', async function() {
var loadHelpers = spyOn(this.testJasmine, 'loadHelpers');
spyOn(this.testJasmine, 'configureDefaultReporter').and.callFake(function() {
expect(loadHelpers).toHaveBeenCalled();
});
spyOn(this.testJasmine, 'loadSpecs');

this.testJasmine.execute();
await this.testJasmine.execute();

expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalled();
});

it('can run only specified files', function() {
it('can run only specified files', async function() {
spyOn(this.testJasmine, 'configureDefaultReporter');
spyOn(this.testJasmine, 'loadSpecs');

this.testJasmine.loadConfigFile();

this.testJasmine.execute(['spec/fixtures/**/*spec.js']);
await this.testJasmine.execute(['spec/fixtures/sample_project/**/*spec.js']);

var relativePaths = this.testJasmine.specFiles.map(function(filePath) {
return slash(path.relative(__dirname, filePath));
Expand All @@ -396,19 +424,19 @@ describe('Jasmine', function() {
expect(relativePaths).toEqual(['fixtures/sample_project/spec/fixture_spec.js', 'fixtures/sample_project/spec/other_fixture_spec.js']);
});

it('should add spec filter if filterString is provided', function() {
it('should add spec filter if filterString is provided', async function() {
this.testJasmine.loadConfigFile();

this.testJasmine.execute(['spec/fixtures/**/*spec.js'], 'interesting spec');
await this.testJasmine.execute(['spec/fixtures/example/*spec.js'], 'interesting spec');
expect(this.testJasmine.env.configure).toHaveBeenCalledWith({specFilter: jasmine.any(Function)});
});

it('adds an exit code reporter', function() {
it('adds an exit code reporter', async function() {
var completionReporterSpy = jasmine.createSpyObj('reporter', ['onComplete']);
this.testJasmine.completionReporter = completionReporterSpy;
spyOn(this.testJasmine, 'addReporter');

this.testJasmine.execute();
await this.testJasmine.execute();

expect(this.testJasmine.addReporter).toHaveBeenCalledWith(completionReporterSpy);
expect(this.testJasmine.completionReporter.exitHandler).toBe(this.testJasmine.checkExit);
Expand Down
2 changes: 1 addition & 1 deletion spec/load_config_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ describe('loadConfig', function() {
expect(fakeJasmine.showColors).toHaveBeenCalled();
expect(fakeJasmine.execute).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit c8ebcff

Please sign in to comment.