testem 3.20.2 → 3.21.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
@@ -33,7 +33,7 @@ Features
33
33
 
34
34
  Installation
35
35
  ------------
36
- Testem needs a supported **[Node.js](https://nodejs.org/)** runtime. The required range is defined in [`package.json`](package.json) under `engines` (currently **^20.19.0**, **^22.12.0**, or **>=24.0.0**).
36
+ Testem needs a supported **[Node.js](https://nodejs.org/)** runtime. The required range is defined in [`package.json`](package.json) under `engines` (currently **^20.19.0**, **^22.12.0**, **^24.0.0**, or **>= 26.0.0**).
37
37
 
38
38
  **Recommended:** install Testem **both** as a **dev dependency** (so your project pins a version) **and** **globally** (so the `testem` command is always available on your `PATH`):
39
39
 
@@ -278,7 +278,7 @@ This calls for the `testem.json` configuration file (you can also alternatively
278
278
 
279
279
  ```json
280
280
  {
281
- "framework": "jasmine",
281
+ "framework": "jasmine2",
282
282
  "src_files": [
283
283
  "hello.js",
284
284
  "hello_spec.js"
@@ -286,6 +286,8 @@ This calls for the `testem.json` configuration file (you can also alternatively
286
286
  }
287
287
  ```
288
288
 
289
+ The default `framework` is still `"jasmine"` (Jasmine 1.x). That default is **deprecated** and will be removed in the next version of Testem. New projects should use `"jasmine2"` with `jasmine-core` (see [Browser framework dependencies](#browser-framework-dependencies)).
290
+
289
291
  The `src_files` can also be unix glob patterns.
290
292
 
291
293
  ```json
@@ -312,6 +314,48 @@ You can also ignore certain files using `src_files_ignore`.
312
314
 
313
315
  Read [more details](docs/config_file.md) about the config options.
314
316
 
317
+ Browser framework dependencies
318
+ ------------------------------
319
+
320
+ Built-in `mocha`, `mocha+chai`, `qunit`, and `jasmine2` runners **prefer** files from `/node_modules/` in the project `cwd`. If those packages are not installed, Testem still loads the previous CDN URLs (Mocha 2.3.4, Chai 3.4.1, QUnit 1.20.0, Jasmine 2.4.1). Installing the packages is optional on this version and required in the next major version.
321
+
322
+ | `framework` | Install for modern versions |
323
+ |---|---|
324
+ | `jasmine2` | `jasmine-core` |
325
+ | `qunit` | `qunit` |
326
+ | `mocha` | `mocha` |
327
+ | `mocha+chai` | `mocha` and `chai` |
328
+
329
+ Run `npm install` in the project directory. In a monorepo, or when `cwd` is not the install root, map the path with `routes`:
330
+
331
+ ```json
332
+ {
333
+ "routes": {
334
+ "/node_modules": "../node_modules"
335
+ }
336
+ }
337
+ ```
338
+
339
+ The `mocha+chai` runner loads local Chai when Mocha is also local. Chai 4 uses the UMD build `chai/chai.js` as a classic script. Chai 5+ (`"type": "module"`) is imported as an ES module, then your spec files load as classic scripts (`var expect = chai.expect` still works).
340
+
341
+ `jasmine2` uses `jasmine-core` 5 `boot0.js`/`boot1.js` when those files are present, and `boot.js` for jasmine-core 3/4. Incomplete installs fall back to the CDN pin.
342
+
343
+ ### Jasmine 1.x deprecation
344
+
345
+ `framework: "jasmine"` (the default) still runs Jasmine 1.3.1 from CDN. Testem logs a deprecation warning on the server, and the Jasmine 1 adapter logs a warning in the browser (including custom `test_page`s that still load Jasmine 1).
346
+
347
+ **The next version of Testem will not support Jasmine 1.** Migrate with:
348
+
349
+ ```json
350
+ {
351
+ "framework": "jasmine2"
352
+ }
353
+ ```
354
+
355
+ ```bash
356
+ npm install --save-dev jasmine-core
357
+ ```
358
+
315
359
  Custom Test Pages
316
360
  -----------------
317
361
 
@@ -330,7 +374,7 @@ Next, the test page you use needs to have the adapter code installed on them, as
330
374
 
331
375
  ### Include Snippet
332
376
 
333
- Include this snippet directly after your `jasmine.js`, `qunit.js` or `mocha.js` scripts to enable *Testem* with your test page.
377
+ Include this snippet directly after your `jasmine.js`, `qunit.js` or `mocha.js` scripts to enable *Testem* with your test page. Prefer loading those frameworks from `/node_modules/...` (see [examples/qunit_lazy](examples/qunit_lazy) and [examples/vite](examples/vite)); CDN URLs still work.
334
378
 
335
379
  ```html
336
380
  <script src="/testem.js"></script>
@@ -340,9 +384,11 @@ Or if you are using require.js or another loader, just make sure you load `/test
340
384
 
341
385
  '/testem.js' here is dynamically generated to be used client-side and it should not be confused with server-side 'testem.js'.
342
386
 
343
- ### Dynamic Substitution
387
+ ### Dynamic Substitution (deprecated)
388
+
389
+ Naming `test_page` with a `.mustache` extension to interpolate `serve_files` / `css_files` is **deprecated** and will be removed in Testem 4. Prefer a static HTML page and include `<script src="/testem.js"></script>` as shown above.
344
390
 
345
- To enable dynamic substitutions within the JavaScript files in your custom test page, you must
391
+ On Testem 3.x the old form still works:
346
392
 
347
393
  1. name your test page using `.mustache` as the extension
348
394
  2. use `{{#serve_files}}` to loop over the set of JavaScript files to be served, and then reference its `src` property to access their path (or `{{#css_files}}` for stylesheets)
@@ -357,6 +403,28 @@ Example:
357
403
  <link rel="stylesheet" href="{{src}}">
358
404
  {{/css_files}}
359
405
 
406
+ To migrate, rename the page to `.html`, list the same scripts and styles as tags, and point `test_page` at that file. `serve_files` can stay in config for watching or compilation; it no longer needs to be injected.
407
+
408
+ Before:
409
+
410
+ ```html
411
+ {{#serve_files}}
412
+ <script src="{{src}}"></script>
413
+ {{/serve_files}}
414
+ {{#css_files}}
415
+ <link rel="stylesheet" href="{{src}}">
416
+ {{/css_files}}
417
+ ```
418
+
419
+ After, if config has `"serve_files": ["test-bundle.js"]` and `"css_files": ["app.css"]`:
420
+
421
+ ```html
422
+ <script src="test-bundle.js"></script>
423
+ <link rel="stylesheet" href="app.css">
424
+ ```
425
+
426
+ Keep `/testem.js` and your test framework scripts in the same order as a normal custom page. If you interpolated other config keys (`{{port}}`, custom options), bake those values into the HTML or generate the page in `before_tests`.
427
+
360
428
  ### Multiple Test Pages
361
429
 
362
430
  You can also specify multiple test pages to run by passing an array to the `test_page` option.
@@ -576,7 +644,7 @@ Simply add a `proxies` section to the `testem.json` configuration file.
576
644
  ```
577
645
 
578
646
  This functionality is implemented as a *transparent proxy*, hence a request to
579
- `http://localhost:7357/api/posts.json` will be proxied to `http://localhost:4200/api/posts.json` without removing the `/api` prefix. Setting the `secure` option to `false` as in the above `/xmlapi` configuration block will ignore TLS certificate validation and allow tests to successfully reach that URL even if testem was launched over HTTP. Other available options can be found here: https://github.com/http-party/node-http-proxy#options
647
+ `http://localhost:7357/api/posts.json` will be proxied to `http://localhost:4200/api/posts.json` without removing the `/api` prefix. Setting the `secure` option to `false` as in the above `/xmlapi` configuration block will ignore TLS certificate validation and allow tests to successfully reach that URL even if testem was launched over HTTP. Other available options can be found here: https://github.com/unjs/httpxy#options
580
648
 
581
649
  To limit the functionality to only certain content types, use "onlyContentTypes".
582
650
 
@@ -585,7 +653,9 @@ Preprocessors (Babel, TypeScript, LESS, Sass, Browserify, CoffeeScript, etc)
585
653
 
586
654
  If you need to run a preprocessor (or indeed any shell command before the start of the tests) use the `before_tests` option, such as
587
655
 
588
- "before_tests": "coffee -c *.coffee"
656
+ "before_tests": "coffee -c hello.coffee tests.coffee"
657
+
658
+ On Windows, list files explicitly in string hooks like this—cmd.exe does not expand `*` for external programs (see [Available hooks](docs/config_file.md#available-hooks) and the [coffeescript example](examples/coffeescript)). Testem's own `src_files` / `watch_files` globs are expanded by Testem, not by the hook shell.
589
659
 
590
660
  or, with Babel (see the [Babel example](https://github.com/testem/testem/tree/master/examples/babel)):
591
661
 
package/lib/api.js CHANGED
@@ -25,7 +25,7 @@ const Server = require('./server');
25
25
  launch_in_dev: [Array] list of launchers to use for dev runs
26
26
  launch_in_ci: [Array] list of launchers to use for CI runs
27
27
  timeout: [Number] timeout for a browser
28
- framework: [String] test framework to use
28
+ framework: [String] test framework to use (jasmine [deprecated Jasmine 1.x], jasmine2, qunit, mocha, mocha+chai, custom, tap)
29
29
  url: [String] url server runs at (http://{host}:{port}/)
30
30
  src_files: [Array] list of files or file patterns to use
31
31
  src_files_ignore: [Array] list of files or file patterns to exclude from usage
@@ -1,7 +1,7 @@
1
1
 
2
2
 
3
+ const { inspect } = require('util');
3
4
  const indent = require('../utils/strutils').indent;
4
- const printf = require('printf');
5
5
  const displayutils = require('../utils/displayutils');
6
6
 
7
7
  module.exports = class DotReporter {
@@ -78,20 +78,20 @@ module.exports = class DotReporter {
78
78
  return;
79
79
  }
80
80
 
81
- printf(this.out, '%*d) [%s] %s\n', idx + 1, 3, data.launcher, result.name);
81
+ this.out.write(`${String(idx + 1).padStart(3)}) [${data.launcher}] ${result.name}\n`);
82
82
 
83
83
  if (error.message) {
84
- printf(this.out, ' %s\n', error.message);
84
+ this.out.write(` ${error.message}\n`);
85
85
  }
86
86
 
87
87
  if ('expected' in error && 'actual' in error) {
88
- printf(this.out, '\n' +
89
- ' expected: %s%O\n' +
90
- ' actual: %O\n', (error.negative ? 'NOT ' : ''), error.expected, error.actual);
88
+ this.out.write('\n' +
89
+ ` expected: ${error.negative ? 'NOT ' : ''}${inspect(error.expected)}\n` +
90
+ ` actual: ${inspect(error.actual)}\n`);
91
91
  }
92
92
 
93
93
  if (error.stack) {
94
- printf(this.out, '\n%s', indent(error.stack, 5));
94
+ this.out.write(`\n${indent(error.stack, 5)}`);
95
95
  }
96
96
 
97
97
  this.out.write('\n\n');
@@ -0,0 +1,18 @@
1
+ const { scriptTags, styleTags } = require('./html');
2
+
3
+ module.exports = function renderCustomRunner({
4
+ scripts, styles, footer_scripts
5
+ }) {
6
+ return `<!doctype html>
7
+ <html>
8
+ <head>
9
+ <title>Test'em</title>
10
+ <script src="/testem.js"></script>
11
+ ${scriptTags(scripts)}
12
+ ${styleTags(styles)}
13
+ </head>
14
+ <body>
15
+ </body>
16
+ ${scriptTags(footer_scripts)}
17
+ </html>`;
18
+ };
@@ -0,0 +1,12 @@
1
+ const { escapeHtml } = require('./html');
2
+
3
+ module.exports = function renderDirectoryListing({ files }) {
4
+ const items = (files || []).map(name => {
5
+ const safe = escapeHtml(name);
6
+ return `<li><a href="${safe}">${safe}</a></li>`;
7
+ }).join('');
8
+ return `<h1>Files In This Directory</h1>
9
+ <ul>
10
+ ${items}
11
+ </ul>`;
12
+ };
@@ -0,0 +1,45 @@
1
+ function escapeHtml(value) {
2
+ return String(value)
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;');
7
+ }
8
+
9
+ function attrSuffix(attrs) {
10
+ return (attrs || []).map(attr => ` ${attr}`).join('');
11
+ }
12
+
13
+ function scriptTags(files) {
14
+ return (files || [])
15
+ .map(file => `<script src="${file.src}"${attrSuffix(file.attrs)}></script>`)
16
+ .join('');
17
+ }
18
+
19
+ function asHrefList(hrefs) {
20
+ if (hrefs === undefined || hrefs === null || hrefs === '') {
21
+ return [];
22
+ }
23
+ return Array.isArray(hrefs) ? hrefs : [hrefs];
24
+ }
25
+
26
+ function styleTags(hrefs) {
27
+ return asHrefList(hrefs)
28
+ .map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`)
29
+ .join('');
30
+ }
31
+
32
+ function loadScriptCalls(files) {
33
+ return (files || []).map(file => {
34
+ const extra = (file.attrs || []).map(attr => `, '${attr}'`).join('');
35
+ return `await loadScript('${file.src}'${extra});`;
36
+ }).join('\n');
37
+ }
38
+
39
+ module.exports = {
40
+ escapeHtml,
41
+ attrSuffix,
42
+ scriptTags,
43
+ styleTags,
44
+ loadScriptCalls
45
+ };
@@ -0,0 +1,19 @@
1
+ const pages = {
2
+ jasmine: require('./jasmine'),
3
+ jasmine2: require('./jasmine2'),
4
+ qunit: require('./qunit'),
5
+ mocha: require('./mocha'),
6
+ 'mocha+chai': require('./mocha_chai'),
7
+ custom: require('./custom'),
8
+ tap: require('./tap')
9
+ };
10
+
11
+ function renderRunner(framework, data) {
12
+ const render = pages[framework];
13
+ return render ? render(data) : null;
14
+ }
15
+
16
+ module.exports = {
17
+ renderRunner,
18
+ renderDirectoryListing: require('./directory')
19
+ };
@@ -1,4 +1,9 @@
1
- <!doctype html>
1
+ const { scriptTags, styleTags } = require('./html');
2
+
3
+ module.exports = function renderJasmineRunner({
4
+ scripts, styles, footer_scripts
5
+ }) {
6
+ return `<!doctype html>
2
7
  <html>
3
8
  <head>
4
9
  <title>Test'em</title>
@@ -16,14 +21,15 @@
16
21
  })();
17
22
  </script>
18
23
 
19
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
24
+ ${scriptTags(scripts)}
20
25
 
21
26
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/1.3.1/jasmine.css">
22
27
 
23
- {{#styles}}<link rel="stylesheet" href="{{.}}">{{/styles}}
28
+ ${styleTags(styles)}
24
29
  </head>
25
30
  <body>
26
31
  <div id="jasmine_content"></div>
27
32
  </body>
28
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
29
- </html>
33
+ ${scriptTags(footer_scripts)}
34
+ </html>`;
35
+ };
@@ -0,0 +1,38 @@
1
+ const { scriptTags, styleTags } = require('./html');
2
+
3
+ module.exports = function renderJasmine2Runner({
4
+ jasmineJs,
5
+ jasmineHtml,
6
+ jasmineCss,
7
+ jasmineBoot,
8
+ jasmineCoreV5,
9
+ scripts,
10
+ styles,
11
+ footer_scripts
12
+ }) {
13
+ const boot = jasmineCoreV5
14
+ ? `<script src="/node_modules/jasmine-core/lib/jasmine-core/boot0.js"></script>
15
+ <script src="/node_modules/jasmine-core/lib/jasmine-core/boot1.js"></script>`
16
+ : `<script src="${jasmineBoot || ''}"></script>`;
17
+
18
+ return `<!doctype html>
19
+ <html>
20
+ <head>
21
+ <title>Test'em</title>
22
+ <script src="${jasmineJs}"></script>
23
+ <script src="${jasmineHtml}"></script>
24
+ ${boot}
25
+ <script src="/testem.js"></script>
26
+
27
+ ${scriptTags(scripts)}
28
+
29
+ <link rel="stylesheet" href="${jasmineCss}">
30
+
31
+ ${styleTags(styles)}
32
+ </head>
33
+ <body>
34
+ <div id="jasmine_content"></div>
35
+ </body>
36
+ ${scriptTags(footer_scripts)}
37
+ </html>`;
38
+ };
@@ -0,0 +1,25 @@
1
+ const { scriptTags, styleTags } = require('./html');
2
+
3
+ module.exports = function renderMochaRunner({
4
+ mochaCss, mochaJs, scripts, styles, footer_scripts
5
+ }) {
6
+ return `<!doctype html>
7
+ <html>
8
+ <head>
9
+ <title>Test'em</title>
10
+ <link rel="stylesheet" href="${mochaCss}">
11
+ <script src="${mochaJs}"></script>
12
+ <script src="/testem.js"></script>
13
+ <script>mocha.setup('bdd')</script>
14
+ ${scriptTags(scripts)}
15
+ ${styleTags(styles)}
16
+ </head>
17
+ <body>
18
+ <div id="mocha"></div>
19
+ ${scriptTags(footer_scripts)}
20
+ <script>
21
+ mocha.run()
22
+ </script>
23
+ </body>
24
+ </html>`;
25
+ };
@@ -0,0 +1,83 @@
1
+ const { scriptTags, styleTags, loadScriptCalls } = require('./html');
2
+
3
+ const LOAD_SCRIPT_HELPER = `function applyScriptAttr(el, attr) {
4
+ attr = String(attr).trim();
5
+ var eq = attr.indexOf('=');
6
+ if (eq === -1) {
7
+ el.setAttribute(attr, '');
8
+ return;
9
+ }
10
+ var name = attr.slice(0, eq);
11
+ var value = attr.slice(eq + 1).replace(/^["']|["']$/g, '');
12
+ el.setAttribute(name, value);
13
+ }
14
+ function loadScript(src) {
15
+ var attrs = Array.prototype.slice.call(arguments, 1);
16
+ return new Promise(function(resolve, reject) {
17
+ var s = document.createElement('script');
18
+ s.src = src;
19
+ attrs.forEach(function(attr) { applyScriptAttr(s, attr); });
20
+ s.onload = resolve;
21
+ s.onerror = reject;
22
+ document.body.appendChild(s);
23
+ });
24
+ }`;
25
+
26
+ function renderUmd({
27
+ mochaCss, mochaJs, chaiJs, scripts, styles, footer_scripts
28
+ }) {
29
+ return `<!doctype html>
30
+ <html>
31
+ <head>
32
+ <title>Test'em</title>
33
+ <link rel="stylesheet" href="${mochaCss}">
34
+ <script src="${mochaJs}"></script>
35
+ <script src="${chaiJs}"></script>
36
+ <script src="/testem.js"></script>
37
+ <script>mocha.setup('bdd')</script>
38
+ ${scriptTags(scripts)}
39
+ ${styleTags(styles)}
40
+ </head>
41
+ <body>
42
+ <div id="mocha"></div>
43
+ ${scriptTags(footer_scripts)}
44
+ <script>
45
+ mocha.run()
46
+ </script>
47
+ </body>
48
+ </html>`;
49
+ }
50
+
51
+ function renderEsm({
52
+ mochaCss, mochaJs, chaiJs, scripts, styles, footer_scripts
53
+ }) {
54
+ const awaits = [loadScriptCalls(scripts), loadScriptCalls(footer_scripts)]
55
+ .filter(Boolean)
56
+ .join('\n');
57
+
58
+ return `<!doctype html>
59
+ <html>
60
+ <head>
61
+ <title>Test'em</title>
62
+ <link rel="stylesheet" href="${mochaCss}">
63
+ <script src="${mochaJs}"></script>
64
+ <script src="/testem.js"></script>
65
+ <script>mocha.setup('bdd')</script>
66
+ ${styleTags(styles)}
67
+ </head>
68
+ <body>
69
+ <div id="mocha"></div>
70
+ <script type="module">
71
+ import * as chai from '${chaiJs}';
72
+ globalThis.chai = chai;
73
+ ${LOAD_SCRIPT_HELPER}
74
+ ${awaits}
75
+ globalThis.mocha.run();
76
+ </script>
77
+ </body>
78
+ </html>`;
79
+ }
80
+
81
+ module.exports = function renderMochaChaiRunner(data) {
82
+ return data.chaiLocal ? renderEsm(data) : renderUmd(data);
83
+ };
@@ -0,0 +1,22 @@
1
+ const { scriptTags, styleTags } = require('./html');
2
+
3
+ module.exports = function renderQunitRunner({
4
+ qunitJs, qunitCss, scripts, styles, footer_scripts
5
+ }) {
6
+ return `<!doctype html>
7
+ <html>
8
+ <head>
9
+ <title>Test'em</title>
10
+ <script src="${qunitJs}"></script>
11
+ <script src="/testem.js"></script>
12
+ ${scriptTags(scripts)}
13
+ <link rel="stylesheet" href="${qunitCss}"/>
14
+ ${styleTags(styles)}
15
+ </head>
16
+ <body>
17
+ <div id="qunit"></div>
18
+ <div id="qunit-fixture"></div>
19
+ </body>
20
+ ${scriptTags(footer_scripts)}
21
+ </html>`;
22
+ };
@@ -1,4 +1,7 @@
1
- <!doctype html>
1
+ const { scriptTags } = require('./html');
2
+
3
+ module.exports = function renderTapRunner({ scripts, footer_scripts }) {
4
+ return `<!doctype html>
2
5
  <html>
3
6
  <head>
4
7
  <title>Test'em</title>
@@ -11,11 +14,12 @@
11
14
  var log = document.getElementById('log')
12
15
  Testem.handleConsoleMessage = function(msg){
13
16
  Testem.emit('tap', msg)
14
- log.appendChild(document.createTextNode(msg + '\n'))
17
+ log.appendChild(document.createTextNode(msg + '\\n'))
15
18
  return false
16
19
  }
17
20
  </script>
18
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
19
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
21
+ ${scriptTags(scripts)}
22
+ ${scriptTags(footer_scripts)}
20
23
  </body>
21
- </html>
24
+ </html>`;
25
+ };
@@ -19,8 +19,22 @@ var EventEmitter = require('events').EventEmitter;
19
19
  var Mustache = require('consolidate').mustache;
20
20
  var http = require('http');
21
21
  var https = require('https');
22
- var httpProxy = require('http-proxy');
23
22
  const { fromCallback, each } = require('../utils/promises');
23
+ const { resolveRunnerFrameworkAssets } = require('../utils/runner_framework_assets');
24
+ const { warnJasmine1Framework } = require('../utils/jasmine1_deprecation');
25
+ const { warnMustacheTestPage } = require('../utils/mustache_test_page_deprecation');
26
+ const { convertToPosix } = require('../utils/posix');
27
+ const { renderRunner, renderDirectoryListing } = require('../runner_pages');
28
+
29
+ function browserScriptEntry(file) {
30
+ if (!file || !file.src) {
31
+ return file;
32
+ }
33
+ return {
34
+ src: convertToPosix(file.src),
35
+ attrs: file.attrs
36
+ };
37
+ }
24
38
 
25
39
  class Server extends EventEmitter {
26
40
  constructor(config) {
@@ -35,7 +49,12 @@ class Server extends EventEmitter {
35
49
  this.nextSocketId = 0;
36
50
  }
37
51
 
38
- start() {
52
+ async start() {
53
+ if (!this.createProxyServer) {
54
+ const { createProxyServer } = await import('httpxy');
55
+ this.createProxyServer = createProxyServer;
56
+ }
57
+
39
58
  this.createExpress();
40
59
 
41
60
  // Start the server!
@@ -227,13 +246,22 @@ class Server extends EventEmitter {
227
246
  return false;
228
247
  }
229
248
 
249
+ sendProxyError(res, err) {
250
+ const message = (err && (err.message || err.code)) || 'Proxy error';
251
+ log.error(message);
252
+ // WebSocket upgrades pass a raw socket with no Express res.
253
+ if (res && res.status && !res.headersSent) {
254
+ res.status(500).json({ error: message });
255
+ }
256
+ }
257
+
230
258
  configureProxy(app) {
231
259
  var proxies = this.config.get('proxies');
232
260
  if (proxies) {
233
- this.proxy = new httpProxy.createProxyServer({ changeOrigin: true });
261
+ this.proxy = this.createProxyServer({ changeOrigin: true });
234
262
 
235
263
  this.proxy.on('error', (err, req, res) => {
236
- res && res.status && res.status(500).json(err);
264
+ this.sendProxyError(res, err);
237
265
  });
238
266
 
239
267
  Object.keys(proxies).forEach((url) => {
@@ -241,7 +269,9 @@ class Server extends EventEmitter {
241
269
  // set up proxy for WebSocket
242
270
  this.server.on('upgrade', (req, socket, head) => {
243
271
  if (req.url.startsWith(url)) {
244
- this.proxy.ws(req, socket, head, proxies[url]);
272
+ var wsOpts = Object.assign({}, proxies[url]);
273
+ delete wsOpts.onlyContentTypes;
274
+ this.proxy.ws(req, socket, wsOpts, head).catch(() => {});
245
275
  }
246
276
  });
247
277
  return;
@@ -252,15 +282,21 @@ class Server extends EventEmitter {
252
282
  let wIdx = 0;
253
283
  const proxyPath =
254
284
  url.replace(/\*/g, () => `:_proxyW${wIdx++}`).replace(/\/$/, '') + '{/*_proxyRest}';
255
- app.all(proxyPath, (req, res, next) => {
285
+ app.all(proxyPath, async (req, res, next) => {
256
286
  var opts = proxies[url];
257
287
  if (this.shouldProxy(req, opts)) {
258
- if (opts.host) {
259
- opts.target = `http://${opts.host}:${opts.port}`;
260
- delete opts.host;
261
- delete opts.port;
288
+ var proxyOpts = Object.assign({}, opts);
289
+ if (proxyOpts.host) {
290
+ proxyOpts.target = `http://${proxyOpts.host}:${proxyOpts.port}`;
291
+ delete proxyOpts.host;
292
+ delete proxyOpts.port;
293
+ }
294
+ delete proxyOpts.onlyContentTypes;
295
+ try {
296
+ await this.proxy.web(req, res, proxyOpts);
297
+ } catch (err) {
298
+ this.sendProxyError(res, err);
262
299
  }
263
- this.proxy.web(req, res, opts);
264
300
  } else {
265
301
  next();
266
302
  }
@@ -273,20 +309,23 @@ class Server extends EventEmitter {
273
309
  var config = this.config;
274
310
  var framework = config.get('framework') || 'jasmine';
275
311
  var css_files = config.get('css_files');
276
- var templateFile = {
277
- jasmine: 'jasminerunner',
278
- jasmine2: 'jasmine2runner',
279
- qunit: 'qunitrunner',
280
- mocha: 'mocharunner',
281
- 'mocha+chai': 'mochachairunner',
282
- custom: 'customrunner',
283
- tap: 'taprunner'
284
- }[framework] + '.mustache';
285
- res.render(`${__dirname}/../../views/${templateFile}`, {
286
- scripts: files,
312
+ if (framework === 'jasmine') {
313
+ warnJasmine1Framework();
314
+ }
315
+ var frameworkAssets = resolveRunnerFrameworkAssets(
316
+ config.cwd(),
317
+ config.get('routes') || config.get('route')
318
+ );
319
+ var html = renderRunner(framework, Object.assign({
320
+ scripts: (files || []).map(browserScriptEntry),
287
321
  styles: css_files,
288
- footer_scripts: footerScripts
289
- });
322
+ footer_scripts: (footerScripts || []).map(browserScriptEntry)
323
+ }, frameworkAssets));
324
+ if (html === null) {
325
+ res.status(500).send('Failed to lookup view "' + framework + '"');
326
+ return;
327
+ }
328
+ res.type('html').send(html);
290
329
  }
291
330
 
292
331
  renderDefaultTestPage(req, res) {
@@ -394,6 +433,7 @@ class Server extends EventEmitter {
394
433
  res.write('403 Forbidden');
395
434
  res.end();
396
435
  } else if (ext === '.mustache') {
436
+ warnMustacheTestPage();
397
437
  config.getTemplateData((err, data) => {
398
438
  res.render(filePath, data);
399
439
  this.emit('file-requested', filePath);
@@ -410,8 +450,7 @@ class Server extends EventEmitter {
410
450
  this.emit('file-requested', filePath);
411
451
  if (stat.isDirectory()) {
412
452
  return fs.promises.readdir(filePath).then(files => {
413
- var dirListingPage = `${__dirname}/../../views/directorylisting.mustache`;
414
- res.render(dirListingPage, { files: files });
453
+ res.type('html').send(renderDirectoryListing({ files: files }));
415
454
  });
416
455
  }
417
456
  res.sendFile(filePath, sendOptions);
@@ -0,0 +1,27 @@
1
+ const log = require('../log');
2
+
3
+ const JASMINE1_FRAMEWORK_WARNING =
4
+ 'Testem: framework "jasmine" (Jasmine 1.x) is deprecated and will be removed in the next version of Testem. Use "framework": "jasmine2" with the jasmine-core package, or load Jasmine 2+ on a custom test_page.';
5
+
6
+ let warned = false;
7
+
8
+ function warnJasmine1Framework() {
9
+ if (warned) {
10
+ return;
11
+ }
12
+ warned = true;
13
+ log.warn(JASMINE1_FRAMEWORK_WARNING);
14
+ process.emitWarning(JASMINE1_FRAMEWORK_WARNING, {
15
+ code: 'TESTEM_JASMINE1_DEPRECATED'
16
+ });
17
+ }
18
+
19
+ function resetJasmine1FrameworkWarning() {
20
+ warned = false;
21
+ }
22
+
23
+ module.exports = {
24
+ JASMINE1_FRAMEWORK_WARNING,
25
+ warnJasmine1Framework,
26
+ resetJasmine1FrameworkWarning
27
+ };
@@ -0,0 +1,27 @@
1
+ const log = require('../log');
2
+
3
+ const MUSTACHE_TEST_PAGE_WARNING =
4
+ 'Testem: naming test_page with a .mustache extension is deprecated and will be removed in Testem 4. Use a static HTML page and include <script src="/testem.js"></script>.';
5
+
6
+ let warned = false;
7
+
8
+ function warnMustacheTestPage() {
9
+ if (warned) {
10
+ return;
11
+ }
12
+ warned = true;
13
+ log.warn(MUSTACHE_TEST_PAGE_WARNING);
14
+ process.emitWarning(MUSTACHE_TEST_PAGE_WARNING, {
15
+ code: 'TESTEM_MUSTACHE_TEST_PAGE_DEPRECATED'
16
+ });
17
+ }
18
+
19
+ function resetMustacheTestPageWarning() {
20
+ warned = false;
21
+ }
22
+
23
+ module.exports = {
24
+ MUSTACHE_TEST_PAGE_WARNING,
25
+ warnMustacheTestPage,
26
+ resetMustacheTestPageWarning
27
+ };
@@ -2,7 +2,6 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const mkdirp = require('mkdirp');
6
5
  const PassThrough = require('stream').PassThrough;
7
6
  module.exports = class ReportFile {
8
7
  constructor(reportFile) {
@@ -10,25 +9,20 @@ module.exports = class ReportFile {
10
9
 
11
10
  this.outputStream = new PassThrough();
12
11
 
13
- mkdirp.sync(path.dirname(path.resolve(reportFile)));
12
+ fs.mkdirSync(path.dirname(path.resolve(reportFile)), { recursive: true });
14
13
 
15
14
  this.outputStream = fs.createWriteStream(reportFile, { flags: 'w+' });
16
15
 
17
- let alreadyEnded = false;
18
- function finish(data) {
19
- if (!alreadyEnded) {
20
- alreadyEnded = true;
21
- this.outputStream.end(data);
22
- }
23
- }
24
-
25
- this.outputStream.on('end', finish);
26
- this.outputStream.on('error', finish);
27
-
28
16
  this.closePromise = new Promise((resolve, reject) => {
29
17
  this.outputStream.on('finish', resolve);
30
18
  this.outputStream.on('error', reject);
31
19
  });
20
+
21
+ // The stream can fail long before anything calls close() - opening an
22
+ // unwritable report file rejects on the next tick. Attach a no-op handler
23
+ // so that rejection is not reported as unhandled; close() still returns
24
+ // closePromise, so the error is surfaced to the caller.
25
+ this.closePromise.catch(() => {});
32
26
  }
33
27
 
34
28
  close() {
@@ -0,0 +1,154 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const CDN = {
5
+ mochaJs: '//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.js',
6
+ mochaCss: '//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.css',
7
+ chaiJs: '//cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.js',
8
+ jasmineJs: '//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js',
9
+ jasmineHtml: '//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js',
10
+ jasmineBoot: '//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js',
11
+ jasmineCss: '//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css',
12
+ qunitJs: '//code.jquery.com/qunit/qunit-1.20.0.js',
13
+ qunitCss: '//code.jquery.com/qunit/qunit-1.20.0.css'
14
+ };
15
+
16
+ const JASMINE_JS = 'jasmine-core/lib/jasmine-core/jasmine.js';
17
+ const JASMINE_HTML = 'jasmine-core/lib/jasmine-core/jasmine-html.js';
18
+ const JASMINE_CSS = 'jasmine-core/lib/jasmine-core/jasmine.css';
19
+ const JASMINE_BOOT0 = 'jasmine-core/lib/jasmine-core/boot0.js';
20
+ const JASMINE_BOOT1 = 'jasmine-core/lib/jasmine-core/boot1.js';
21
+ const JASMINE_BOOT = 'jasmine-core/lib/jasmine-core/boot.js';
22
+
23
+ function nodeModulesUrl(relPath) {
24
+ return '/node_modules/' + relPath.replace(/\\/g, '/');
25
+ }
26
+
27
+ function nodeModulesDirs(cwd, routes) {
28
+ const dirs = [path.resolve(cwd, 'node_modules')];
29
+ const routesObj = routes || {};
30
+ ['/node_modules', '/node_modules/'].forEach(prefix => {
31
+ if (!(prefix in routesObj)) {
32
+ return;
33
+ }
34
+ [].concat(routesObj[prefix]).forEach(target => {
35
+ dirs.push(path.resolve(cwd, target));
36
+ });
37
+ });
38
+ return dirs;
39
+ }
40
+
41
+ function existsInNodeModules(dirs, relPath) {
42
+ return dirs.some(dir => fs.existsSync(path.join(dir, relPath)));
43
+ }
44
+
45
+ function findInNodeModules(dirs, relPath) {
46
+ for (let i = 0; i < dirs.length; i++) {
47
+ const full = path.join(dirs[i], relPath);
48
+ if (fs.existsSync(full)) {
49
+ return full;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function resolveAsset(dirs, relPath, fallbackUrl) {
56
+ if (existsInNodeModules(dirs, relPath)) {
57
+ return nodeModulesUrl(relPath);
58
+ }
59
+ return fallbackUrl;
60
+ }
61
+
62
+ function isChaiEsm(dirs) {
63
+ const pkgPath = findInNodeModules(dirs, 'chai/package.json');
64
+ if (!pkgPath) {
65
+ return false;
66
+ }
67
+ try {
68
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
69
+ return pkg.type === 'module';
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function resolveChai(dirs, mochaLocal) {
76
+ if (!mochaLocal) {
77
+ return { chaiLocal: false, chaiJs: CDN.chaiJs };
78
+ }
79
+ if (existsInNodeModules(dirs, 'chai/chai.js')) {
80
+ return { chaiLocal: false, chaiJs: nodeModulesUrl('chai/chai.js') };
81
+ }
82
+ if (existsInNodeModules(dirs, 'chai/index.js') && isChaiEsm(dirs)) {
83
+ return { chaiLocal: true, chaiJs: nodeModulesUrl('chai/index.js') };
84
+ }
85
+ return { chaiLocal: false, chaiJs: CDN.chaiJs };
86
+ }
87
+
88
+ function resolveJasmine(dirs) {
89
+ const hasCore = existsInNodeModules(dirs, JASMINE_JS) &&
90
+ existsInNodeModules(dirs, JASMINE_HTML) &&
91
+ existsInNodeModules(dirs, JASMINE_CSS);
92
+ const jasmineCoreV5 = hasCore &&
93
+ existsInNodeModules(dirs, JASMINE_BOOT0) &&
94
+ existsInNodeModules(dirs, JASMINE_BOOT1);
95
+ const jasmineLegacy = hasCore &&
96
+ existsInNodeModules(dirs, JASMINE_BOOT) &&
97
+ !jasmineCoreV5;
98
+
99
+ if (jasmineCoreV5) {
100
+ return {
101
+ jasmineCoreV5: true,
102
+ jasmineJs: nodeModulesUrl(JASMINE_JS),
103
+ jasmineHtml: nodeModulesUrl(JASMINE_HTML),
104
+ jasmineCss: nodeModulesUrl(JASMINE_CSS),
105
+ jasmineBoot: null
106
+ };
107
+ }
108
+ if (jasmineLegacy) {
109
+ return {
110
+ jasmineCoreV5: false,
111
+ jasmineJs: nodeModulesUrl(JASMINE_JS),
112
+ jasmineHtml: nodeModulesUrl(JASMINE_HTML),
113
+ jasmineCss: nodeModulesUrl(JASMINE_CSS),
114
+ jasmineBoot: nodeModulesUrl(JASMINE_BOOT)
115
+ };
116
+ }
117
+ return {
118
+ jasmineCoreV5: false,
119
+ jasmineJs: CDN.jasmineJs,
120
+ jasmineHtml: CDN.jasmineHtml,
121
+ jasmineCss: CDN.jasmineCss,
122
+ jasmineBoot: CDN.jasmineBoot
123
+ };
124
+ }
125
+
126
+ function resolveRunnerFrameworkAssets(cwd, routes) {
127
+ cwd = cwd || process.cwd();
128
+ const dirs = nodeModulesDirs(cwd, routes);
129
+
130
+ const mochaJs = resolveAsset(dirs, 'mocha/mocha.js', CDN.mochaJs);
131
+ const mochaCss = resolveAsset(dirs, 'mocha/mocha.css', CDN.mochaCss);
132
+ const mochaLocal = mochaJs.indexOf('/node_modules/') === 0;
133
+ const chai = resolveChai(dirs, mochaLocal);
134
+ const jasmine = resolveJasmine(dirs);
135
+
136
+ return {
137
+ mochaJs,
138
+ mochaCss,
139
+ chaiLocal: chai.chaiLocal,
140
+ chaiJs: chai.chaiJs,
141
+ jasmineCoreV5: jasmine.jasmineCoreV5,
142
+ jasmineJs: jasmine.jasmineJs,
143
+ jasmineHtml: jasmine.jasmineHtml,
144
+ jasmineBoot: jasmine.jasmineBoot,
145
+ jasmineCss: jasmine.jasmineCss,
146
+ qunitJs: resolveAsset(dirs, 'qunit/qunit/qunit.js', CDN.qunitJs),
147
+ qunitCss: resolveAsset(dirs, 'qunit/qunit/qunit.css', CDN.qunitCss)
148
+ };
149
+ }
150
+
151
+ module.exports = {
152
+ CDN,
153
+ resolveRunnerFrameworkAssets
154
+ };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "unittest",
8
8
  "browser"
9
9
  ],
10
- "version": "3.20.2",
10
+ "version": "3.21.0",
11
11
  "author": "Toby Ho <airportyh@gmail.com>",
12
12
  "repository": {
13
13
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/testem/testem/issues"
18
18
  },
19
19
  "engines": {
20
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
20
+ "node": "^20.19.0 || ^22.12.0 || ^24.0.0 || >= 26.0.0"
21
21
  },
22
22
  "license": "MIT",
23
23
  "dependencies": {
@@ -31,20 +31,18 @@
31
31
  "execa": "^9.6.1",
32
32
  "express": "^5.2.1",
33
33
  "glob": "^13.0.6",
34
- "http-proxy": "^1.18.1",
35
- "js-yaml": "^4.1.1",
34
+ "httpxy": "^0.5.5",
35
+ "js-yaml": "^5.3.0",
36
36
  "lodash": "^4.18.1",
37
37
  "minimatch": "^10.2.5",
38
- "mkdirp": "^3.0.1",
39
38
  "mustache": "^4.2.0",
40
- "toasted-notifier": "^10.1.0",
41
- "printf": "^0.6.1",
42
39
  "proc-log": "^6.1.0",
43
40
  "rimraf": "^6.1.3",
44
41
  "socket.io": "^4.8.3",
45
42
  "spawn-args": "^0.2.0",
46
43
  "styled_string": "0.0.1",
47
- "tap-parser": "^18.3.0"
44
+ "tap-parser": "^18.3.0",
45
+ "toasted-notifier": "^10.1.0"
48
46
  },
49
47
  "files": [
50
48
  "lib",
@@ -52,8 +50,7 @@
52
50
  "README.md",
53
51
  "testem.js",
54
52
  "assets",
55
- "package.json",
56
- "views"
53
+ "package.json"
57
54
  ],
58
55
  "scripts": {
59
56
  "test": "mocha tests/*_tests.js tests/**/*_tests.js",
@@ -68,30 +65,23 @@
68
65
  "@eslint/eslintrc": "^3.3.5",
69
66
  "@eslint/js": "^10.0.1",
70
67
  "chai": "^6.2.2",
71
- "chai-files": "^1.4.0",
72
- "chai-shallow-deep-equal": "^1.4.6",
73
68
  "cheerio": "^1.2.0",
74
69
  "dirty-chai": "^3.0.0",
75
- "esbuild": "^0.27.4",
76
- "esbuild-plugins-node-modules-polyfill": "^1.8.1",
70
+ "esbuild": "^0.28.2",
71
+ "esbuild-plugins-node-modules-polyfill": "^1.8.3",
77
72
  "eslint": "^10.2.0",
78
73
  "eslint-config-prettier": "^10.1.8",
79
74
  "eslint-plugin-chai-expect": "^4.0.0",
80
- "eslint-plugin-mocha": "^11.2.0",
75
+ "eslint-plugin-mocha": "^12.0.2",
81
76
  "fresh-tape": "^5.9.1",
82
- "mocha": "^11.7.5",
77
+ "jasmine-core": "^5.1.0",
78
+ "mocha": "^12.0.0",
79
+ "qunit": "^2.26.0",
83
80
  "saucie": "^4.1.0",
84
81
  "sinon": "^21.0.3",
85
82
  "sinon-chai": "^4.0.1",
86
83
  "socket.io-client": "^4.8.3"
87
84
  },
88
- "overrides": {
89
- "mocha": {
90
- "diff": "^8.0.3",
91
- "minimatch": "^9.0.7",
92
- "serialize-javascript": "^7.0.5"
93
- }
94
- },
95
85
  "bin": {
96
86
  "testem": "./testem.js"
97
87
  },
@@ -7,11 +7,17 @@ Testem's adapter for Jasmine. It works by adding a custom reporter.
7
7
 
8
8
  */
9
9
 
10
- /* globals emit, jasmine */
10
+ /* globals emit, jasmine, console */
11
11
  /* exported jasmineAdapter */
12
12
  'use strict';
13
13
 
14
14
  function jasmineAdapter() {
15
+ if (!jasmineAdapter._deprecationWarned) {
16
+ jasmineAdapter._deprecationWarned = true;
17
+ if (typeof console !== 'undefined' && console.warn) {
18
+ console.warn('Testem: the Jasmine 1.x adapter is deprecated and will be removed in the next version of Testem. Upgrade to Jasmine 2+ (jasmine-core) so Testem can use the jasmine2 adapter.');
19
+ }
20
+ }
15
21
 
16
22
  var results = {
17
23
  failed: 0,
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Test'em</title>
5
- <script src="/testem.js"></script>
6
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
7
- {{#styles}}<link rel="stylesheet" href="{{.}}">{{/styles}}
8
- </head>
9
- <body>
10
- </body>
11
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
12
- </html>
@@ -1,4 +0,0 @@
1
- <h1>Files In This Directory</h1>
2
- <ul>
3
- {{#files}}<li><a href="{{.}}">{{.}}</a></li>{{/files}}
4
- </ul>
@@ -1,20 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Test'em</title>
5
- <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
6
- <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
7
- <script src="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
8
- <script src="/testem.js"></script>
9
-
10
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
11
-
12
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css">
13
-
14
- {{#styles}}<link rel="stylesheet" href="{{.}}">{{/styles}}
15
- </head>
16
- <body>
17
- <div id="jasmine_content"></div>
18
- </body>
19
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
20
- </html>
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Test'em</title>
5
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.css">
6
- <script src="//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.js"></script>
7
- <script src="//cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.js"></script>
8
- <script src="/testem.js"></script>
9
- <script>mocha.setup('bdd')</script>
10
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
11
- </head>
12
- <body>
13
- <div id="mocha"></div>
14
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
15
- <script>
16
- mocha.run()
17
- </script>
18
- </body>
19
- </html>
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Test'em</title>
5
- <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.css">
6
- <script src="//cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.js"></script>
7
- <script src="/testem.js"></script>
8
- <script>mocha.setup('bdd')</script>
9
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
10
- {{#styles}}<link rel="stylesheet" href="{{.}}">{{/styles}}
11
- </head>
12
- <body>
13
- <div id="mocha"></div>
14
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
15
- <script>
16
- mocha.run()
17
- </script>
18
- </body>
19
- </html>
@@ -1,16 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>Test'em</title>
5
- <script src="//code.jquery.com/qunit/qunit-1.20.0.js"></script>
6
- <script src="/testem.js"></script>
7
- {{#scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/scripts}}
8
- <link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.20.0.css"/>
9
- {{#styles}}<link rel="stylesheet" href="{{.}}">{{/styles}}
10
- </head>
11
- <body>
12
- <div id="qunit"></div>
13
- <div id="qunit-fixture"></div>
14
- </body>
15
- {{#footer_scripts}}<script src="{{{src}}}"{{#attrs}} {{&.}}{{/attrs}}></script>{{/footer_scripts}}
16
- </html>