testem 3.20.2 → 3.21.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.
@@ -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!
@@ -206,34 +225,40 @@ class Server extends EventEmitter {
206
225
  }
207
226
 
208
227
  shouldProxy(req, opts) {
209
- var accepts;
228
+ //Only apply filtering logic if 'onlyContentTypes' key is present
229
+ if (!('onlyContentTypes' in opts)) {
230
+ return true;
231
+ }
232
+
233
+ var onlyContentTypes = [].concat(opts.onlyContentTypes);
210
234
  var acceptCheck = [
211
235
  'html',
212
236
  'css',
213
237
  'javascript'
214
- ];
238
+ ].concat(onlyContentTypes, 'text');
215
239
 
216
- //Only apply filtering logic if 'onlyContentTypes' key is present
217
- if (!('onlyContentTypes' in opts)) {
218
- return true;
219
- }
240
+ // req.accepts returns the single best matching type as a string, or false
241
+ // when the request accepts none of them.
242
+ var accepts = req.accepts(acceptCheck);
243
+ return accepts !== false && onlyContentTypes.indexOf(accepts) !== -1;
244
+ }
220
245
 
221
- acceptCheck = acceptCheck.concat(opts.onlyContentTypes);
222
- acceptCheck.push('text');
223
- accepts = req.accepts(acceptCheck);
224
- if (accepts.indexOf(opts.onlyContentTypes) !== -1) {
225
- return true;
246
+ sendProxyError(res, err) {
247
+ const message = (err && (err.message || err.code)) || 'Proxy error';
248
+ log.error(message);
249
+ // WebSocket upgrades pass a raw socket with no Express res.
250
+ if (res && res.status && !res.headersSent) {
251
+ res.status(500).json({ error: message });
226
252
  }
227
- return false;
228
253
  }
229
254
 
230
255
  configureProxy(app) {
231
256
  var proxies = this.config.get('proxies');
232
257
  if (proxies) {
233
- this.proxy = new httpProxy.createProxyServer({ changeOrigin: true });
258
+ this.proxy = this.createProxyServer({ changeOrigin: true });
234
259
 
235
260
  this.proxy.on('error', (err, req, res) => {
236
- res && res.status && res.status(500).json(err);
261
+ this.sendProxyError(res, err);
237
262
  });
238
263
 
239
264
  Object.keys(proxies).forEach((url) => {
@@ -241,7 +266,9 @@ class Server extends EventEmitter {
241
266
  // set up proxy for WebSocket
242
267
  this.server.on('upgrade', (req, socket, head) => {
243
268
  if (req.url.startsWith(url)) {
244
- this.proxy.ws(req, socket, head, proxies[url]);
269
+ var wsOpts = Object.assign({}, proxies[url]);
270
+ delete wsOpts.onlyContentTypes;
271
+ this.proxy.ws(req, socket, wsOpts, head).catch(() => {});
245
272
  }
246
273
  });
247
274
  return;
@@ -252,15 +279,21 @@ class Server extends EventEmitter {
252
279
  let wIdx = 0;
253
280
  const proxyPath =
254
281
  url.replace(/\*/g, () => `:_proxyW${wIdx++}`).replace(/\/$/, '') + '{/*_proxyRest}';
255
- app.all(proxyPath, (req, res, next) => {
282
+ app.all(proxyPath, async (req, res, next) => {
256
283
  var opts = proxies[url];
257
284
  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;
285
+ var proxyOpts = Object.assign({}, opts);
286
+ if (proxyOpts.host) {
287
+ proxyOpts.target = `http://${proxyOpts.host}:${proxyOpts.port}`;
288
+ delete proxyOpts.host;
289
+ delete proxyOpts.port;
290
+ }
291
+ delete proxyOpts.onlyContentTypes;
292
+ try {
293
+ await this.proxy.web(req, res, proxyOpts);
294
+ } catch (err) {
295
+ this.sendProxyError(res, err);
262
296
  }
263
- this.proxy.web(req, res, opts);
264
297
  } else {
265
298
  next();
266
299
  }
@@ -273,20 +306,23 @@ class Server extends EventEmitter {
273
306
  var config = this.config;
274
307
  var framework = config.get('framework') || 'jasmine';
275
308
  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,
309
+ if (framework === 'jasmine') {
310
+ warnJasmine1Framework();
311
+ }
312
+ var frameworkAssets = resolveRunnerFrameworkAssets(
313
+ config.cwd(),
314
+ config.get('routes') || config.get('route')
315
+ );
316
+ var html = renderRunner(framework, Object.assign({
317
+ scripts: (files || []).map(browserScriptEntry),
287
318
  styles: css_files,
288
- footer_scripts: footerScripts
289
- });
319
+ footer_scripts: (footerScripts || []).map(browserScriptEntry)
320
+ }, frameworkAssets));
321
+ if (html === null) {
322
+ res.status(500).send('Failed to lookup view "' + framework + '"');
323
+ return;
324
+ }
325
+ res.type('html').send(html);
290
326
  }
291
327
 
292
328
  renderDefaultTestPage(req, res) {
@@ -394,6 +430,7 @@ class Server extends EventEmitter {
394
430
  res.write('403 Forbidden');
395
431
  res.end();
396
432
  } else if (ext === '.mustache') {
433
+ warnMustacheTestPage();
397
434
  config.getTemplateData((err, data) => {
398
435
  res.render(filePath, data);
399
436
  this.emit('file-requested', filePath);
@@ -410,8 +447,7 @@ class Server extends EventEmitter {
410
447
  this.emit('file-requested', filePath);
411
448
  if (stat.isDirectory()) {
412
449
  return fs.promises.readdir(filePath).then(files => {
413
- var dirListingPage = `${__dirname}/../../views/directorylisting.mustache`;
414
- res.render(dirListingPage, { files: files });
450
+ res.type('html').send(renderDirectoryListing({ files: files }));
415
451
  });
416
452
  }
417
453
  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
+ };
@@ -123,13 +123,18 @@ function mapLimit(arr, concurrency, fn) {
123
123
  return Promise.all(workers).then(() => results);
124
124
  }
125
125
 
126
- async function retry(fn, { max_tries = 3 } = {}) {
126
+ async function retry(fn, { max_tries = 3, interval = 0, backoff = 1 } = {}) {
127
127
  let lastErr;
128
+ let wait = interval;
128
129
  for (let i = 0; i < max_tries; i++) {
129
130
  try {
130
131
  return await fn();
131
132
  } catch (e) {
132
133
  lastErr = e;
134
+ if (i < max_tries - 1 && wait > 0) {
135
+ await delay(wait);
136
+ wait *= backoff;
137
+ }
133
138
  }
134
139
  }
135
140
  throw lastErr;
@@ -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
+ };