Skip to content

Commit c8ebcff

Browse files
committed
Added support for ES modules
* 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`.
1 parent a91e8d4 commit c8ebcff

17 files changed

+246
-34
lines changed

lib/jasmine.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var path = require('path'),
22
util = require('util'),
33
glob = require('glob'),
4+
Loader = require('./loader'),
45
CompletionReporter = require('./reporters/completion_reporter'),
56
ConsoleSpecFilter = require('./filters/console_spec_filter');
67

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

1011
function Jasmine(options) {
1112
options = options || {};
13+
this.loader = options.loader || new Loader();
1214
var jasmineCore = options.jasmineCore || require('jasmine-core');
1315
this.jasmineCorePath = path.join(jasmineCore.files.path, 'jasmine.js');
1416
this.jasmine = jasmineCore.boot(jasmineCore);
@@ -84,16 +86,16 @@ Jasmine.prototype.addMatchers = function(matchers) {
8486
this.env.addMatchers(matchers);
8587
};
8688

87-
Jasmine.prototype.loadSpecs = function() {
88-
this.specFiles.forEach(function(file) {
89-
require(file);
90-
});
89+
Jasmine.prototype.loadSpecs = async function() {
90+
for (const file of this.specFiles) {
91+
await this.loader.load(file);
92+
}
9193
};
9294

93-
Jasmine.prototype.loadHelpers = function() {
94-
this.helperFiles.forEach(function(file) {
95-
require(file);
96-
});
95+
Jasmine.prototype.loadHelpers = async function() {
96+
for (const file of this.helperFiles) {
97+
await this.loader.load(file);
98+
}
9799
};
98100

99101
Jasmine.prototype.loadRequires = function() {
@@ -238,11 +240,11 @@ var checkExit = function(jasmineRunner) {
238240
};
239241
};
240242

241-
Jasmine.prototype.execute = function(files, filterString) {
243+
Jasmine.prototype.execute = async function(files, filterString) {
242244
this.completionReporter.exitHandler = this.checkExit;
243245

244246
this.loadRequires();
245-
this.loadHelpers();
247+
await this.loadHelpers();
246248
if (!this.defaultReporterConfigured) {
247249
this.configureDefaultReporter({ showColors: this.showingColors });
248250
}
@@ -262,8 +264,11 @@ Jasmine.prototype.execute = function(files, filterString) {
262264
this.addSpecFiles(files);
263265
}
264266

265-
this.loadSpecs();
267+
await this.loadSpecs();
266268

267269
this.addReporter(this.completionReporter);
268-
this.env.execute();
270+
271+
await new Promise(resolve => {
272+
this.env.execute(null, resolve);
273+
});
269274
};

lib/loader.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module.exports = Loader;
2+
3+
function Loader(options) {
4+
options = options || {};
5+
this.require_ = options.requireShim || requireShim;
6+
this.import_ = options.importShim || importShim;
7+
}
8+
9+
Loader.prototype.load = function(path) {
10+
if (path.endsWith('.mjs')) {
11+
return this.import_(path);
12+
} else {
13+
return new Promise(resolve => {
14+
this.require_(path);
15+
resolve();
16+
});
17+
}
18+
};
19+
20+
function requireShim(path) {
21+
require(path);
22+
}
23+
24+
function importShim(path) {
25+
return import(path);
26+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"eslintConfig": {
3535
"parserOptions": {
36-
"ecmaVersion": 6
36+
"ecmaVersion": 11
3737
},
3838
"rules": {
3939
"no-unused-vars": [

spec/esm_integration_spec.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const child_process = require('child_process');
2+
3+
4+
describe('ES module support', function() {
5+
it('supports ES modules', function(done) {
6+
const child = child_process.spawn(
7+
'node',
8+
['--experimental-modules', '../../../bin/jasmine.js', '--config=jasmine.json'],
9+
{
10+
cwd: 'spec/fixtures/esm',
11+
shell: false
12+
}
13+
);
14+
let output = '';
15+
child.stdout.on('data', function(data) {
16+
output += data;
17+
});
18+
child.stderr.on('data', function(data) {
19+
output += data;
20+
});
21+
child.on('close', function(exitCode) {
22+
expect(exitCode).toEqual(0);
23+
// Node < 14 outputs a warning when ES modules are used, e.g.:
24+
// (node:5258) ExperimentalWarning: The ESM module loader is experimental.
25+
// The position of this warning in the output varies. Sometimes it
26+
// occurs before the lines we're interested in but sometimes it's in
27+
// the middle of them.
28+
output = output.replace(/^.*ExperimentalWarning.*$\n/m, '');
29+
expect(output).toContain(
30+
'name_reporter\n' +
31+
'commonjs_helper\n' +
32+
'esm_helper\n' +
33+
'Started\n' +
34+
'Spec: A spec file ending in .js is required as a commonjs module\n' +
35+
'.Spec: A spec file ending in .mjs is imported as an es module\n'
36+
);
37+
done();
38+
});
39+
});
40+
});

spec/fixtures/esm/commonjs_helper.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
console.log('commonjs_helper');
2+
require('./commonjs_sentinel');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// An empty module that we can require, to see if require works.

spec/fixtures/esm/commonjs_spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('A spec file ending in .js', function() {
2+
it('is required as a commonjs module', function() {
3+
require('./commonjs_sentinel');
4+
});
5+
});

spec/fixtures/esm/esm_helper.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import './esm_sentinel.mjs';
2+
console.log('esm_helper');

spec/fixtures/esm/esm_sentinel.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// An empty module that will fail if loaded via require(), due to its extension

spec/fixtures/esm/esm_spec.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
describe('A spec file ending in .mjs', function() {
2+
it('is imported as an es module', function() {
3+
// Node probably threw already if we tried to require this file,
4+
// but check anyway just to be sure.
5+
expect(function() {
6+
require('./commonjs_sentinel');
7+
}).toThrowError(/require is not defined/);
8+
});
9+
});

spec/fixtures/esm/jasmine.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"spec_dir": ".",
3+
"spec_files": [
4+
"commonjs_spec.js",
5+
"esm_spec.mjs"
6+
],
7+
"helpers": [
8+
"name_reporter.js",
9+
"commonjs_helper.js",
10+
"esm_helper.mjs"
11+
],
12+
"stopSpecOnExpectationFailure": false,
13+
"random": false
14+
}

spec/fixtures/esm/name_reporter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
console.log('name_reporter');
2+
3+
beforeAll(function() {
4+
jasmine.getEnv().addReporter({
5+
specStarted: function (event) {
6+
console.log('Spec:', event.fullName);
7+
}
8+
});
9+
});

spec/fixtures/require_tester.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
global.require_tester_was_loaded = true;
2+
module.exports = {};

spec/jasmine_spec.js

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ describe('Jasmine', function() {
1010
clearReporters: jasmine.createSpy('clearReporters'),
1111
addMatchers: jasmine.createSpy('addMatchers'),
1212
provideFallbackReporter: jasmine.createSpy('provideFallbackReporter'),
13-
execute: jasmine.createSpy('execute'),
13+
execute: jasmine.createSpy('execute')
14+
.and.callFake(function(ignored, callback) {
15+
callback();
16+
}),
1417
configure: jasmine.createSpy('configure')
1518
}),
1619
Timer: jasmine.createSpy('Timer')
@@ -52,27 +55,52 @@ describe('Jasmine', function() {
5255
it('add spec files with glob pattern', function() {
5356
expect(this.testJasmine.specFiles).toEqual([]);
5457
this.testJasmine.addSpecFiles(['spec/*.js']);
55-
expect(this.testJasmine.specFiles.map(basename)).toEqual(['command_spec.js', 'jasmine_spec.js', 'load_config_spec.js']);
58+
expect(this.testJasmine.specFiles.map(basename)).toEqual([
59+
'command_spec.js',
60+
'esm_integration_spec.js',
61+
'jasmine_spec.js',
62+
'load_config_spec.js',
63+
'loader_spec.js',
64+
]);
5665
});
5766

5867
it('add spec files with excluded files', function() {
5968
expect(this.testJasmine.specFiles).toEqual([]);
6069
this.testJasmine.addSpecFiles(['spec/*.js', '!spec/command*']);
61-
expect(this.testJasmine.specFiles.map(basename)).toEqual(['jasmine_spec.js', 'load_config_spec.js']);
70+
expect(this.testJasmine.specFiles.map(basename)).toEqual([
71+
'esm_integration_spec.js',
72+
'jasmine_spec.js',
73+
'load_config_spec.js',
74+
'loader_spec.js',
75+
]);
6276
});
6377

6478
it('add spec files with glob pattern to existings files', function() {
6579
var aFile = path.join(this.testJasmine.projectBaseDir, this.testJasmine.specDir, 'spec/command_spec.js');
6680
this.testJasmine.specFiles = [aFile, 'b'];
6781
this.testJasmine.addSpecFiles(['spec/*.js']);
68-
expect(this.testJasmine.specFiles.map(basename)).toEqual(['command_spec.js', 'b', 'jasmine_spec.js', 'load_config_spec.js']);
82+
expect(this.testJasmine.specFiles.map(basename)).toEqual([
83+
'command_spec.js',
84+
'b',
85+
'esm_integration_spec.js',
86+
'jasmine_spec.js',
87+
'load_config_spec.js',
88+
'loader_spec.js',
89+
]);
6990
});
7091

7192
it('add helper files with glob pattern to existings files', function() {
7293
var aFile = path.join(this.testJasmine.projectBaseDir, this.testJasmine.specDir, 'spec/command_spec.js');
7394
this.testJasmine.helperFiles = [aFile, 'b'];
7495
this.testJasmine.addHelperFiles(['spec/*.js']);
75-
expect(this.testJasmine.helperFiles.map(basename)).toEqual(['command_spec.js', 'b', 'jasmine_spec.js', 'load_config_spec.js']);
96+
expect(this.testJasmine.helperFiles.map(basename)).toEqual([
97+
'command_spec.js',
98+
'b',
99+
'esm_integration_spec.js',
100+
'jasmine_spec.js',
101+
'load_config_spec.js',
102+
'loader_spec.js',
103+
]);
76104
});
77105
});
78106

@@ -331,63 +359,63 @@ describe('Jasmine', function() {
331359
expect(this.testJasmine.showingColors).toBe(false);
332360
});
333361

334-
describe('#execute', function() {
335-
it('uses the default console reporter if no reporters were added', function() {
362+
describe('#execute', function() {
363+
it('uses the default console reporter if no reporters were added', async function() {
336364
spyOn(this.testJasmine, 'configureDefaultReporter');
337365
spyOn(this.testJasmine, 'loadSpecs');
338366

339-
this.testJasmine.execute();
367+
await this.testJasmine.execute();
340368

341369
expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalledWith({showColors: true});
342370
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
343371
expect(this.testJasmine.env.execute).toHaveBeenCalled();
344372
});
345373

346-
it('configures the default console reporter with the right color settings', function() {
374+
it('configures the default console reporter with the right color settings', async function() {
347375
spyOn(this.testJasmine, 'configureDefaultReporter');
348376
spyOn(this.testJasmine, 'loadSpecs');
349377
this.testJasmine.showColors(false);
350378

351-
this.testJasmine.execute();
379+
await this.testJasmine.execute();
352380

353381
expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalledWith({showColors: false});
354382
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
355383
expect(this.testJasmine.env.execute).toHaveBeenCalled();
356384
});
357385

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

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

363391
spyOn(this.testJasmine, 'configureDefaultReporter');
364392

365-
this.testJasmine.execute();
393+
await this.testJasmine.execute();
366394

367395
expect(this.testJasmine.configureDefaultReporter).not.toHaveBeenCalled();
368396
expect(this.testJasmine.loadSpecs).toHaveBeenCalled();
369397
expect(this.testJasmine.env.execute).toHaveBeenCalled();
370398
});
371399

372-
it('loads helper files before checking if any reporters were added', function() {
400+
it('loads helper files before checking if any reporters were added', async function() {
373401
var loadHelpers = spyOn(this.testJasmine, 'loadHelpers');
374402
spyOn(this.testJasmine, 'configureDefaultReporter').and.callFake(function() {
375403
expect(loadHelpers).toHaveBeenCalled();
376404
});
377405
spyOn(this.testJasmine, 'loadSpecs');
378406

379-
this.testJasmine.execute();
407+
await this.testJasmine.execute();
380408

381409
expect(this.testJasmine.configureDefaultReporter).toHaveBeenCalled();
382410
});
383411

384-
it('can run only specified files', function() {
412+
it('can run only specified files', async function() {
385413
spyOn(this.testJasmine, 'configureDefaultReporter');
386414
spyOn(this.testJasmine, 'loadSpecs');
387415

388416
this.testJasmine.loadConfigFile();
389417

390-
this.testJasmine.execute(['spec/fixtures/**/*spec.js']);
418+
await this.testJasmine.execute(['spec/fixtures/sample_project/**/*spec.js']);
391419

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

399-
it('should add spec filter if filterString is provided', function() {
427+
it('should add spec filter if filterString is provided', async function() {
400428
this.testJasmine.loadConfigFile();
401429

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

406-
it('adds an exit code reporter', function() {
434+
it('adds an exit code reporter', async function() {
407435
var completionReporterSpy = jasmine.createSpyObj('reporter', ['onComplete']);
408436
this.testJasmine.completionReporter = completionReporterSpy;
409437
spyOn(this.testJasmine, 'addReporter');
410438

411-
this.testJasmine.execute();
439+
await this.testJasmine.execute();
412440

413441
expect(this.testJasmine.addReporter).toHaveBeenCalledWith(completionReporterSpy);
414442
expect(this.testJasmine.completionReporter.exitHandler).toBe(this.testJasmine.checkExit);

spec/load_config_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ describe('loadConfig', function() {
5151
expect(fakeJasmine.showColors).toHaveBeenCalled();
5252
expect(fakeJasmine.execute).toHaveBeenCalled();
5353
});
54-
});
54+
});

0 commit comments

Comments
 (0)