jasmine-browser-runner 2.0.0-beta.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/bin/jasmine-browser-runner +2 -2
- package/lib/command.js +1 -1
- package/lib/config.js +81 -0
- package/lib/server.js +62 -9
- package/lib/support/batchReporter.js +2 -2
- package/lib/support/loaders.js +2 -2
- package/lib/types.js +42 -0
- package/package.json +3 -2
- package/run.html.ejs +5 -0
package/README.md
CHANGED
|
@@ -68,7 +68,9 @@ Its value can be `"firefox"`, `"headlessFirefox"`, `"safari"`,
|
|
|
68
68
|
## ES module support
|
|
69
69
|
|
|
70
70
|
If a source, spec, or helper file's name ends in `.mjs`, it will be loaded as
|
|
71
|
-
an ES module rather than a regular script.
|
|
71
|
+
an ES module rather than a regular script. Note that ES modules can only be
|
|
72
|
+
loaded from other ES modules. So if your source files are ES modules, your
|
|
73
|
+
spec files need to be ES modules too.
|
|
72
74
|
|
|
73
75
|
To allow spec files to import source files via relative paths, set the `specDir`
|
|
74
76
|
config field to something that's high enough up to include both spec and source
|
|
@@ -78,6 +80,23 @@ running `npx jasmine-browser-runner init --esm`.
|
|
|
78
80
|
If you have specs or helper files that use top-level await, set the
|
|
79
81
|
`enableTopLevelAwait` config property is set to `true`.
|
|
80
82
|
|
|
83
|
+
[Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
|
|
84
|
+
are also supported:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
{
|
|
88
|
+
// ...
|
|
89
|
+
"importMap": {
|
|
90
|
+
"moduleRootDir": "node_modules",
|
|
91
|
+
"imports": {
|
|
92
|
+
"some-lib":"some-lib/dist/index.mjs",
|
|
93
|
+
"some-lib/": "some-lib/dist/",
|
|
94
|
+
"some-cdn-lib": "https://example.com/some-cdn-lib"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
81
100
|
## Use with Rails
|
|
82
101
|
|
|
83
102
|
You can use jasmine-browser-runner to test your Rails application's JavaScript,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const path = require('path'),
|
|
4
4
|
jasmineCore = require('../lib/jasmineCore'),
|
|
5
5
|
Command = require('../lib/command'),
|
|
6
6
|
jasmineBrowser = require('../index.js');
|
|
7
7
|
const UsageError = require('../lib/usage_error');
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const command = new Command({
|
|
10
10
|
baseDir: path.resolve(),
|
|
11
11
|
jasmineCore,
|
|
12
12
|
jasmineBrowser,
|
package/lib/command.js
CHANGED
|
@@ -250,7 +250,7 @@ function wrapDescription(indentLevel, prefixWidth, maxWidth, text) {
|
|
|
250
250
|
return text;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
const chunks = [];
|
|
254
254
|
while (text.length > columnWidth) {
|
|
255
255
|
const wrapChar = text.lastIndexOf(' ', columnWidth);
|
|
256
256
|
chunks.push(text.substring(0, wrapChar));
|
package/lib/config.js
CHANGED
|
@@ -50,6 +50,10 @@ function validateConfig(config) {
|
|
|
50
50
|
throw new Error(`Configuration's ${k} property is not an array`);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
if (config.importMap) {
|
|
55
|
+
validateImportMap(config.importMap);
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
function defaultConfig() {
|
|
@@ -67,6 +71,83 @@ function defaultEsmConfig() {
|
|
|
67
71
|
);
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
function validateImportMap(importMap) {
|
|
75
|
+
// basic validation, but not the entire spec.
|
|
76
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
|
|
77
|
+
|
|
78
|
+
if (typeof importMap !== 'object') {
|
|
79
|
+
throw new Error("Configuration's importMap property is not an object");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!importMap.imports && !importMap.scopes) {
|
|
83
|
+
throw new Error("Configuration's importMap contains no imports or scopes");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (importMap.imports) {
|
|
87
|
+
validateImports('importMap.imports', importMap.imports);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (importMap.scopes) {
|
|
91
|
+
for (const [kScope, vScope] of Object.entries(importMap.scopes)) {
|
|
92
|
+
if (kScope === '') {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Configuration's importMap.scopes cannot contain empty keys"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (typeof vScope !== 'object') {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Configuration's importMap.scopes property is not an object"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
validateImports('importMap.scopes', vScope);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const moduleRootDir = importMap.moduleRootDir;
|
|
107
|
+
|
|
108
|
+
if (moduleRootDir === '') {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'Configuration.importMap.moduleRootDir cannot be an empty string'
|
|
111
|
+
);
|
|
112
|
+
} else if (moduleRootDir && moduleRootDir.startsWith('../')) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Configuration.importMap.moduleRootDir cannot start with ../'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Both importMap.imports and importMap.scopes[k].value should be an object with
|
|
120
|
+
// module specifiers, hence this helper function for DRY.
|
|
121
|
+
// @param context `importMap.imports` | `importMap.scopes`
|
|
122
|
+
// @param module specifier map of string -> string
|
|
123
|
+
function validateImports(context, imports) {
|
|
124
|
+
if (typeof imports !== 'object') {
|
|
125
|
+
throw new Error(`Configuration's ${context} property is not an object`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Object.keys(imports).length === 0) {
|
|
129
|
+
throw new Error(`Configuration's ${context} map cannot be empty`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [kImport, vImport] of Object.entries(imports)) {
|
|
133
|
+
if (kImport === '') {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Configuration's ${context} map cannot contain empty string keys`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (vImport === '') {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Configuration's ${context} map cannot contain empty string values`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof vImport !== 'string') {
|
|
146
|
+
throw new Error(`Configuration's ${context} map value is not a string`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
70
151
|
module.exports = {
|
|
71
152
|
loadConfig,
|
|
72
153
|
validateConfig,
|
package/lib/server.js
CHANGED
|
@@ -40,7 +40,7 @@ class Server {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
allCss() {
|
|
43
|
-
|
|
43
|
+
const urls = this.getUrls(
|
|
44
44
|
this.options.srcDir,
|
|
45
45
|
this.options.cssFiles,
|
|
46
46
|
'/__src__'
|
|
@@ -71,7 +71,7 @@ class Server {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
userJs() {
|
|
74
|
-
|
|
74
|
+
const srcUrls = this.getUrls(
|
|
75
75
|
this.options.srcDir,
|
|
76
76
|
this.options.srcFiles,
|
|
77
77
|
'/__src__'
|
|
@@ -79,12 +79,12 @@ class Server {
|
|
|
79
79
|
// Exclude ES modules. These will be loaded by other ES modules.
|
|
80
80
|
return !url.endsWith('.mjs');
|
|
81
81
|
});
|
|
82
|
-
|
|
82
|
+
const helperUrls = this.getUrls(
|
|
83
83
|
this.options.specDir,
|
|
84
84
|
this.options.helpers,
|
|
85
85
|
'/__spec__'
|
|
86
86
|
);
|
|
87
|
-
|
|
87
|
+
const specUrls = this.getUrls(
|
|
88
88
|
this.options.specDir,
|
|
89
89
|
this.options.specFiles,
|
|
90
90
|
'/__spec__'
|
|
@@ -92,6 +92,33 @@ class Server {
|
|
|
92
92
|
return [].concat(srcUrls, helperUrls, specUrls);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Bound property for ejs template, used to inject a `<script
|
|
96
|
+
// type="importmap">` tag. This changes specifier target values for any
|
|
97
|
+
// relative module paths according to `this.options.importMap.moduleRootDir`.
|
|
98
|
+
// @returns ImportMap | undefined
|
|
99
|
+
importMap() {
|
|
100
|
+
const { importMap } = this.options;
|
|
101
|
+
if (!importMap) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const resultMap = {};
|
|
106
|
+
|
|
107
|
+
if (importMap.imports) {
|
|
108
|
+
resultMap.imports = reifyRawSpecifierMap(importMap.imports);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (importMap.scopes) {
|
|
112
|
+
resultMap.scopes = {};
|
|
113
|
+
|
|
114
|
+
for (const [scope, map] of Object.entries(importMap.scopes)) {
|
|
115
|
+
resultMap.scopes[scope] = reifyRawSpecifierMap(map);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return resultMap;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
/**
|
|
96
123
|
* Starts the server.
|
|
97
124
|
* @param {ServerStartOptions} options
|
|
@@ -99,7 +126,7 @@ class Server {
|
|
|
99
126
|
*/
|
|
100
127
|
start(serverOptions) {
|
|
101
128
|
serverOptions = serverOptions || {};
|
|
102
|
-
|
|
129
|
+
const app = express();
|
|
103
130
|
|
|
104
131
|
app.use('/__jasmine__', express.static(this.jasmineCore.files.path));
|
|
105
132
|
app.use('/__boot__', express.static(this.jasmineCore.files.bootDir));
|
|
@@ -114,14 +141,21 @@ class Server {
|
|
|
114
141
|
express.static(path.join(this.projectBaseDir, this.options.srcDir))
|
|
115
142
|
);
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
if (this.options.importMap) {
|
|
145
|
+
const dir = this.options.importMap.moduleRootDir
|
|
146
|
+
? path.join(this.projectBaseDir, this.options.importMap.moduleRootDir)
|
|
147
|
+
: this.projectBaseDir;
|
|
148
|
+
app.use('/__moduleRoot__', express.static(dir));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const indexTemplate = ejs.compile(
|
|
118
152
|
fs.readFileSync(path.resolve(__dirname, '../run.html.ejs')).toString()
|
|
119
153
|
);
|
|
120
|
-
|
|
154
|
+
const configTemplate = ejs.compile(
|
|
121
155
|
fs.readFileSync(path.resolve(__dirname, '../config.js.ejs')).toString()
|
|
122
156
|
);
|
|
123
157
|
|
|
124
|
-
|
|
158
|
+
const self = this;
|
|
125
159
|
app.get('/', function(req, res) {
|
|
126
160
|
try {
|
|
127
161
|
res.send(
|
|
@@ -129,6 +163,7 @@ class Server {
|
|
|
129
163
|
cssFiles: self.allCss(),
|
|
130
164
|
jasmineJsFiles: self.jasmineJs(),
|
|
131
165
|
userJsFiles: self.userJs(),
|
|
166
|
+
importMap: self.importMap(),
|
|
132
167
|
enableTopLevelAwait: self.options.enableTopLevelAwait || false,
|
|
133
168
|
})
|
|
134
169
|
);
|
|
@@ -151,7 +186,7 @@ class Server {
|
|
|
151
186
|
}
|
|
152
187
|
});
|
|
153
188
|
|
|
154
|
-
|
|
189
|
+
const port = findPort(serverOptions.port, this.options.port);
|
|
155
190
|
return new Promise(resolve => {
|
|
156
191
|
this._httpServer = app.listen(port, () => {
|
|
157
192
|
const runningPort = this._httpServer.address().port;
|
|
@@ -258,4 +293,22 @@ function unWindows(filePath) {
|
|
|
258
293
|
return filePath.replace(/\\/g, '/');
|
|
259
294
|
}
|
|
260
295
|
|
|
296
|
+
// Processes the incoming `rawSpecifierMap`, converting targets to use actual
|
|
297
|
+
// paths that the run.html.ejs file will contain. The `rawSpecifierMap` is not
|
|
298
|
+
// the entire importMap. It is a key/value map that may be the "imports" value
|
|
299
|
+
// or an individual map inside of "scopes"[someScope].
|
|
300
|
+
function reifyRawSpecifierMap(rawSpecifierMap) {
|
|
301
|
+
const concreteMap = {};
|
|
302
|
+
|
|
303
|
+
for (const [key, value] of Object.entries(rawSpecifierMap)) {
|
|
304
|
+
if (value.match(/^https?:\/\//)) {
|
|
305
|
+
concreteMap[key] = value; // pass through unchanged
|
|
306
|
+
} else {
|
|
307
|
+
concreteMap[key] = './' + unWindows(path.join('__moduleRoot__', value));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return concreteMap;
|
|
312
|
+
}
|
|
313
|
+
|
|
261
314
|
module.exports = Server;
|
package/lib/support/loaders.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/* eslint-env browser, jasmine */
|
|
2
2
|
|
|
3
3
|
window._jasmine_loadEsModule = function(src) {
|
|
4
|
-
|
|
4
|
+
const script = document.createElement('script');
|
|
5
5
|
script.type = 'module';
|
|
6
6
|
|
|
7
7
|
// Safari reports syntax errors in ES modules as a script element error
|
|
8
8
|
// event rather than a global error event. Rethrow so that Jasmine can
|
|
9
9
|
// pick it up and fail the suite.
|
|
10
10
|
script.addEventListener('error', function(event) {
|
|
11
|
-
|
|
11
|
+
const msg =
|
|
12
12
|
'An error occurred while loading ' +
|
|
13
13
|
src +
|
|
14
14
|
'. Check the browser console for details.';
|
package/lib/types.js
CHANGED
|
@@ -111,6 +111,14 @@
|
|
|
111
111
|
* @type boolean | undefined
|
|
112
112
|
* @default true
|
|
113
113
|
*/
|
|
114
|
+
/**
|
|
115
|
+
* Import maps entry to generate the `<script type="importmap">` section in the
|
|
116
|
+
* `<head>`, to enable ES Module testing in the browser.
|
|
117
|
+
*
|
|
118
|
+
* @name Configuration#importMap
|
|
119
|
+
* @type ImportMap | undefined
|
|
120
|
+
* @default undefined
|
|
121
|
+
*/
|
|
114
122
|
/**
|
|
115
123
|
* Whether to enable support for top-level await. This option is off by default
|
|
116
124
|
* because it comes with a performance penalty.
|
|
@@ -197,3 +205,37 @@
|
|
|
197
205
|
* @name ServerStartOptions#port
|
|
198
206
|
* @type number | undefined
|
|
199
207
|
*/
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Describes an import map.
|
|
211
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap}
|
|
212
|
+
* @see {@link https://github.com/WICG/import-maps}
|
|
213
|
+
* @interface ImportMap
|
|
214
|
+
*/
|
|
215
|
+
/**
|
|
216
|
+
* A single, unscoped module specifier map.
|
|
217
|
+
* @name ImportMap#imports
|
|
218
|
+
* @type {Object.<string, string>}
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Map of one or more scoped module specifier maps.
|
|
222
|
+
* @name ImportMap#scopes
|
|
223
|
+
* @type {Object.<string, Object.<string, string>>}
|
|
224
|
+
*/
|
|
225
|
+
/**
|
|
226
|
+
* Optional directory that specifies the root for relative paths in import map
|
|
227
|
+
* (if required).
|
|
228
|
+
*
|
|
229
|
+
* For example, if you only use import paths that resolve to absolute targets,
|
|
230
|
+
* e.g. 'my-pkg': 'https://mycdn.url/my-pkg', then you do not need this dir
|
|
231
|
+
* option. But if you need to reference a folder, e.g. `node_modules`, then this
|
|
232
|
+
* is required.
|
|
233
|
+
*
|
|
234
|
+
* moduleRootDir is evaluated relative to the project base directory, which is
|
|
235
|
+
* typically the current working directory from which jasmine-browser-runner is
|
|
236
|
+
* run.
|
|
237
|
+
*
|
|
238
|
+
* @name ImportMap#moduleRootDir
|
|
239
|
+
* @type string
|
|
240
|
+
* @example 'node_modules'
|
|
241
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jasmine-browser-runner",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"description": "Serve and run your Jasmine specs in a browser",
|
|
5
5
|
"bin": "bin/jasmine-browser-runner",
|
|
6
6
|
"exports": "./index.js",
|
|
@@ -115,7 +115,8 @@
|
|
|
115
115
|
"always"
|
|
116
116
|
],
|
|
117
117
|
"space-before-blocks": "error",
|
|
118
|
-
"no-console": "off"
|
|
118
|
+
"no-console": "off",
|
|
119
|
+
"no-var": "error"
|
|
119
120
|
},
|
|
120
121
|
"overrides": [
|
|
121
122
|
{
|
package/run.html.ejs
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
<% cssFiles.forEach(function(cssFile) { %>
|
|
9
9
|
<link rel="stylesheet" href="<%= cssFile %>" type="text/css" media="screen"/>
|
|
10
10
|
<% }) %>
|
|
11
|
+
<% if (importMap) { %>
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
<%- JSON.stringify(importMap, null, 2) %>
|
|
14
|
+
</script>
|
|
15
|
+
<% } %>
|
|
11
16
|
</head>
|
|
12
17
|
<body>
|
|
13
18
|
<% jasmineJsFiles.forEach(function(jsFile) { %>
|