jasmine-browser-runner 2.0.0-beta.0 → 2.0.0

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 CHANGED
@@ -1,6 +1,3 @@
1
- [![Build Status](https://circleci.com/gh/jasmine/jasmine-browser.svg?style=shield)](https://circleci.com/gh/jasmine/jasmine-browser)
2
-
3
-
4
1
  jasmine-browser-runner runs your Jasmine specs in a browser. It's suitable for
5
2
  interactive use with normal browsers as well as running specs in CI builds
6
3
  using either headless Chrome or Saucelabs.
@@ -9,44 +6,34 @@ using either headless Chrome or Saucelabs.
9
6
 
10
7
  ```bash
11
8
  npm install --save-dev jasmine-browser-runner jasmine-core
9
+ npx jasmine-browser-runner init
12
10
  ```
13
11
 
14
12
  or
15
13
 
16
14
  ```bash
17
15
  yarn add -D jasmine-browser-runner jasmine-core
16
+ npx jasmine-browser-runner init
18
17
  ```
19
18
 
20
- Add a `spec/support/jasmine-browser.json`. For example:
19
+ If you intend to use ES modules, add `--esm` to the `jasmine-browser-runner init`
20
+ command.
21
21
 
22
- ```json
23
- {
24
- "srcDir": "src",
25
- "srcFiles": [
26
- "**/*.?(m)js"
27
- ],
28
- "specDir": "spec",
29
- "specFiles": [
30
- "**/*[Ss]pec.?(m)js"
31
- ],
32
- "helpers": [
33
- "helpers/asyncAwait.js"
34
- ],
35
- "env": {
36
- "random": true
37
- }
38
- }
39
- ```
22
+ Then, customize `spec/support/jasmine-browser.json` to suit your needs. You can
23
+ change the spec files, helpers, and source files that are loaded, specify the
24
+ [Jasmine env's configuration](https://jasmine.github.io/api/edge/Configuration.html),
25
+ and more.
40
26
 
41
27
  You can also use the `--config` option to specify a different file. This file can be a JSON file or a javascript file that exports a object that looks like the JSON above.
42
28
 
43
- Start the server:
29
+ To start the server so that you can run the specs interactively (particularly
30
+ useful for debugging):
44
31
 
45
32
  ```
46
33
  npx jasmine-browser-runner serve
47
34
  ```
48
35
 
49
- Run the tests in a browser (defaults to Firefox)
36
+ To run the specs in a browser (defaults to Firefox):
50
37
 
51
38
  ```
52
39
  npx jasmine-browser-runner runSpecs
@@ -68,7 +55,11 @@ Its value can be `"firefox"`, `"headlessFirefox"`, `"safari"`,
68
55
  ## ES module support
69
56
 
70
57
  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.
58
+ an ES module rather than a regular script. Note that ES modules can only be
59
+ loaded from other ES modules. So if your source files are ES modules, your
60
+ spec files need to be ES modules too. Want to use a different extension than
61
+ `.esm`? Just set the `esmFilenameExtension` config property, e.g.
62
+ `"esmFilenameExtension": ".js"`.
72
63
 
73
64
  To allow spec files to import source files via relative paths, set the `specDir`
74
65
  config field to something that's high enough up to include both spec and source
@@ -78,6 +69,23 @@ running `npx jasmine-browser-runner init --esm`.
78
69
  If you have specs or helper files that use top-level await, set the
79
70
  `enableTopLevelAwait` config property is set to `true`.
80
71
 
72
+ [Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
73
+ are also supported:
74
+
75
+ ```javascript
76
+ {
77
+ // ...
78
+ "importMap": {
79
+ "moduleRootDir": "node_modules",
80
+ "imports": {
81
+ "some-lib":"some-lib/dist/index.mjs",
82
+ "some-lib/": "some-lib/dist/",
83
+ "some-cdn-lib": "https://example.com/some-cdn-lib"
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
81
89
  ## Use with Rails
82
90
 
83
91
  You can use jasmine-browser-runner to test your Rails application's JavaScript,
@@ -204,7 +212,7 @@ Firefox, and Microsoft Edge) as well as Node.
204
212
 
205
213
  | Environment | Supported versions |
206
214
  |-------------------|------------------------|
207
- | Node | 12.17+, 14, 16, 18, 20 |
215
+ | Node | 18, 20 |
208
216
  | Safari | 15-16 |
209
217
  | Chrome | Evergreen |
210
218
  | Firefox | Evergreen, 102 |
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- var path = require('path'),
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
- var command = new Command({
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
- var chunks = [];
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,
@@ -8,6 +8,8 @@
8
8
  "helpers": [
9
9
  "spec/helpers/**/*.?(m)js"
10
10
  ],
11
+ "esmFilenameExtension": ".mjs",
12
+ "enableTopLevelAwait": false,
11
13
  "env": {
12
14
  "stopSpecOnExpectationFailure": false,
13
15
  "stopOnSpecFailure": false,
package/lib/server.js CHANGED
@@ -14,7 +14,7 @@ class Server {
14
14
  * @param {ServerCtorOptions} options
15
15
  */
16
16
  constructor(options) {
17
- this.options = options;
17
+ this.options = { ...options };
18
18
  this.useHtmlReporter =
19
19
  options.useHtmlReporter === undefined ? true : options.useHtmlReporter;
20
20
  this.projectBaseDir = options.projectBaseDir || path.resolve();
@@ -29,6 +29,13 @@ class Server {
29
29
  return unWindows(path.join('/__jasmine__', fileName));
30
30
  })
31
31
  .concat(this.bootFiles());
32
+
33
+ if (!this.options.esmFilenameExtension) {
34
+ this.options.esmFilenameExtension = '.mjs';
35
+ } else if (this.options.esmFilenameExtension[0] !== '.') {
36
+ this.options.esmFilenameExtension =
37
+ '.' + this.options.esmFilenameExtension;
38
+ }
32
39
  }
33
40
 
34
41
  bootFiles() {
@@ -40,7 +47,7 @@ class Server {
40
47
  }
41
48
 
42
49
  allCss() {
43
- var urls = this.getUrls(
50
+ const urls = this.getUrls(
44
51
  this.options.srcDir,
45
52
  this.options.cssFiles,
46
53
  '/__src__'
@@ -71,20 +78,20 @@ class Server {
71
78
  }
72
79
 
73
80
  userJs() {
74
- var srcUrls = this.getUrls(
81
+ const srcUrls = this.getUrls(
75
82
  this.options.srcDir,
76
83
  this.options.srcFiles,
77
84
  '/__src__'
78
- ).filter(function(url) {
85
+ ).filter(url => {
79
86
  // Exclude ES modules. These will be loaded by other ES modules.
80
- return !url.endsWith('.mjs');
87
+ return !url.endsWith(this.options.esmFilenameExtension);
81
88
  });
82
- var helperUrls = this.getUrls(
89
+ const helperUrls = this.getUrls(
83
90
  this.options.specDir,
84
91
  this.options.helpers,
85
92
  '/__spec__'
86
93
  );
87
- var specUrls = this.getUrls(
94
+ const specUrls = this.getUrls(
88
95
  this.options.specDir,
89
96
  this.options.specFiles,
90
97
  '/__spec__'
@@ -92,6 +99,33 @@ class Server {
92
99
  return [].concat(srcUrls, helperUrls, specUrls);
93
100
  }
94
101
 
102
+ // Bound property for ejs template, used to inject a `<script
103
+ // type="importmap">` tag. This changes specifier target values for any
104
+ // relative module paths according to `this.options.importMap.moduleRootDir`.
105
+ // @returns ImportMap | undefined
106
+ importMap() {
107
+ const { importMap } = this.options;
108
+ if (!importMap) {
109
+ return undefined;
110
+ }
111
+
112
+ const resultMap = {};
113
+
114
+ if (importMap.imports) {
115
+ resultMap.imports = reifyRawSpecifierMap(importMap.imports);
116
+ }
117
+
118
+ if (importMap.scopes) {
119
+ resultMap.scopes = {};
120
+
121
+ for (const [scope, map] of Object.entries(importMap.scopes)) {
122
+ resultMap.scopes[scope] = reifyRawSpecifierMap(map);
123
+ }
124
+ }
125
+
126
+ return resultMap;
127
+ }
128
+
95
129
  /**
96
130
  * Starts the server.
97
131
  * @param {ServerStartOptions} options
@@ -99,7 +133,7 @@ class Server {
99
133
  */
100
134
  start(serverOptions) {
101
135
  serverOptions = serverOptions || {};
102
- var app = express();
136
+ const app = express();
103
137
 
104
138
  app.use('/__jasmine__', express.static(this.jasmineCore.files.path));
105
139
  app.use('/__boot__', express.static(this.jasmineCore.files.bootDir));
@@ -114,14 +148,21 @@ class Server {
114
148
  express.static(path.join(this.projectBaseDir, this.options.srcDir))
115
149
  );
116
150
 
117
- var indexTemplate = ejs.compile(
151
+ if (this.options.importMap) {
152
+ const dir = this.options.importMap.moduleRootDir
153
+ ? path.join(this.projectBaseDir, this.options.importMap.moduleRootDir)
154
+ : this.projectBaseDir;
155
+ app.use('/__moduleRoot__', express.static(dir));
156
+ }
157
+
158
+ const indexTemplate = ejs.compile(
118
159
  fs.readFileSync(path.resolve(__dirname, '../run.html.ejs')).toString()
119
160
  );
120
- var configTemplate = ejs.compile(
161
+ const configTemplate = ejs.compile(
121
162
  fs.readFileSync(path.resolve(__dirname, '../config.js.ejs')).toString()
122
163
  );
123
164
 
124
- var self = this;
165
+ const self = this;
125
166
  app.get('/', function(req, res) {
126
167
  try {
127
168
  res.send(
@@ -129,6 +170,8 @@ class Server {
129
170
  cssFiles: self.allCss(),
130
171
  jasmineJsFiles: self.jasmineJs(),
131
172
  userJsFiles: self.userJs(),
173
+ esmFilenameExtension: self.options.esmFilenameExtension,
174
+ importMap: self.importMap(),
132
175
  enableTopLevelAwait: self.options.enableTopLevelAwait || false,
133
176
  })
134
177
  );
@@ -151,7 +194,7 @@ class Server {
151
194
  }
152
195
  });
153
196
 
154
- var port = findPort(serverOptions.port, this.options.port);
197
+ const port = findPort(serverOptions.port, this.options.port);
155
198
  return new Promise(resolve => {
156
199
  this._httpServer = app.listen(port, () => {
157
200
  const runningPort = this._httpServer.address().port;
@@ -258,4 +301,22 @@ function unWindows(filePath) {
258
301
  return filePath.replace(/\\/g, '/');
259
302
  }
260
303
 
304
+ // Processes the incoming `rawSpecifierMap`, converting targets to use actual
305
+ // paths that the run.html.ejs file will contain. The `rawSpecifierMap` is not
306
+ // the entire importMap. It is a key/value map that may be the "imports" value
307
+ // or an individual map inside of "scopes"[someScope].
308
+ function reifyRawSpecifierMap(rawSpecifierMap) {
309
+ const concreteMap = {};
310
+
311
+ for (const [key, value] of Object.entries(rawSpecifierMap)) {
312
+ if (value.match(/^https?:\/\//)) {
313
+ concreteMap[key] = value; // pass through unchanged
314
+ } else {
315
+ concreteMap[key] = './' + unWindows(path.join('__moduleRoot__', value));
316
+ }
317
+ }
318
+
319
+ return concreteMap;
320
+ }
321
+
261
322
  module.exports = Server;
@@ -1,9 +1,9 @@
1
1
  /* eslint-env browser, jasmine */
2
2
  function BatchReporter() {
3
- var events = [];
3
+ let events = [];
4
4
 
5
5
  this.getBatch = function() {
6
- var result = events;
6
+ const result = events;
7
7
  events = [];
8
8
  return result;
9
9
  };
@@ -1,14 +1,14 @@
1
1
  /* eslint-env browser, jasmine */
2
2
 
3
3
  window._jasmine_loadEsModule = function(src) {
4
- var script = document.createElement('script');
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
- var msg =
11
+ const msg =
12
12
  'An error occurred while loading ' +
13
13
  src +
14
14
  '. Check the browser console for details.';
@@ -19,7 +19,10 @@ window._jasmine_loadEsModule = function(src) {
19
19
  document.head.appendChild(script);
20
20
  };
21
21
 
22
- window._jasmine_loadWithTopLevelAwaitSupport = async function(scriptUrls) {
22
+ window._jasmine_loadWithTopLevelAwaitSupport = async function(
23
+ scriptUrls,
24
+ esmFilenameExtension
25
+ ) {
23
26
  const scriptsLoaded = (async function() {
24
27
  // Load scripts sequentially to ensure that users can get a stable order
25
28
  // by disabling randomization or setting a seed. This can be considerably
@@ -28,7 +31,7 @@ window._jasmine_loadWithTopLevelAwaitSupport = async function(scriptUrls) {
28
31
  // describes will execute in a consistent order in the presence of top-level
29
32
  // await.
30
33
  for (const url of scriptUrls) {
31
- const isEsm = url.endsWith('.mjs');
34
+ const isEsm = url.endsWith(esmFilenameExtension);
32
35
 
33
36
  if (isEsm) {
34
37
  try {
package/lib/types.js CHANGED
@@ -70,6 +70,12 @@
70
70
  * @name ServerCtorOptions#srcFiles
71
71
  * @type string[] | undefined
72
72
  */
73
+ /**
74
+ * The file extension used by ES modules
75
+ * @name ServerCtorOptions#esmFilenameExtension
76
+ * @type string | undefined
77
+ * @default ".mjs"
78
+ */
73
79
 
74
80
  /**
75
81
  * Specifies the properties of the configuration file, as well as
@@ -111,6 +117,14 @@
111
117
  * @type boolean | undefined
112
118
  * @default true
113
119
  */
120
+ /**
121
+ * Import maps entry to generate the `<script type="importmap">` section in the
122
+ * `<head>`, to enable ES Module testing in the browser.
123
+ *
124
+ * @name Configuration#importMap
125
+ * @type ImportMap | undefined
126
+ * @default undefined
127
+ */
114
128
  /**
115
129
  * Whether to enable support for top-level await. This option is off by default
116
130
  * because it comes with a performance penalty.
@@ -197,3 +211,37 @@
197
211
  * @name ServerStartOptions#port
198
212
  * @type number | undefined
199
213
  */
214
+
215
+ /**
216
+ * Describes an import map.
217
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap}
218
+ * @see {@link https://github.com/WICG/import-maps}
219
+ * @interface ImportMap
220
+ */
221
+ /**
222
+ * A single, unscoped module specifier map.
223
+ * @name ImportMap#imports
224
+ * @type {Object.<string, string>}
225
+ */
226
+ /**
227
+ * Map of one or more scoped module specifier maps.
228
+ * @name ImportMap#scopes
229
+ * @type {Object.<string, Object.<string, string>>}
230
+ */
231
+ /**
232
+ * Optional directory that specifies the root for relative paths in import map
233
+ * (if required).
234
+ *
235
+ * For example, if you only use import paths that resolve to absolute targets,
236
+ * e.g. 'my-pkg': 'https://mycdn.url/my-pkg', then you do not need this dir
237
+ * option. But if you need to reference a folder, e.g. `node_modules`, then this
238
+ * is required.
239
+ *
240
+ * moduleRootDir is evaluated relative to the project base directory, which is
241
+ * typically the current working directory from which jasmine-browser-runner is
242
+ * run.
243
+ *
244
+ * @name ImportMap#moduleRootDir
245
+ * @type string
246
+ * @example 'node_modules'
247
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jasmine-browser-runner",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0",
4
4
  "description": "Serve and run your Jasmine specs in a browser",
5
5
  "bin": "bin/jasmine-browser-runner",
6
6
  "exports": "./index.js",
@@ -43,14 +43,14 @@
43
43
  "selenium-webdriver": "^4.8.2"
44
44
  },
45
45
  "peerDependencies": {
46
- "jasmine-core": "^5.0.0-beta.0"
46
+ "jasmine-core": "^5.0.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "ejs-lint": "^2.0.0",
50
50
  "eslint": "^8.38.0",
51
51
  "eslint-plugin-jasmine": "^4.1.3",
52
- "jasmine": "^5.0.0-beta.0",
53
- "jasmine-core": "^5.0.0-beta.0",
52
+ "jasmine": "^5.0.0",
53
+ "jasmine-core": "^5.0.0",
54
54
  "prettier": "^1.17.1",
55
55
  "shelljs": "^0.8.3",
56
56
  "temp": "^0.9.4"
@@ -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) { %>
@@ -19,11 +24,11 @@
19
24
  <% userJsFiles.forEach(function(jsFile) { %>
20
25
  '<%= jsFile %>',
21
26
  <% }) %>
22
- ]);
27
+ ], '<%=esmFilenameExtension%>');
23
28
  </script>
24
29
  <% } else { %>
25
30
  <% userJsFiles.forEach(function(jsFile) { %>
26
- <% if (jsFile.endsWith('.mjs')) { %>
31
+ <% if (jsFile.endsWith(esmFilenameExtension)) { %>
27
32
  <script type="module">_jasmine_loadEsModule('<%= jsFile %>')</script>
28
33
  <% } else { %>
29
34
  <script src="<%= jsFile %>" type="text/javascript"></script>