UNPKG

14.7 kBJavaScriptView Raw
1/*
2
3config.js
4=========
5
6This object returns all config info for the app. It handles reading the `testem.yml`
7or `testem.json` config file.
8
9*/
10'use strict';
11
12const os = require('os');
13const fs = require('fs');
14const yaml = require('js-yaml');
15const log = require('npmlog');
16const path = require('path');
17const glob = require('glob');
18const url = require('url');
19const querystring = require('querystring');
20const Bluebird = require('bluebird');
21
22const browser_launcher = require('./browser_launcher');
23const LauncherFactory = require('./launcher-factory');
24const Chars = require('./utils/chars');
25const pad = require('./utils/strutils').pad;
26const isa = require('./utils/isa');
27const fileExists = require('./utils/fileutils').fileExists;
28const uniqBy = require('lodash.uniqby');
29
30const knownBrowsers = require('./utils/known-browsers');
31const globAsync = Bluebird.promisify(glob);
32
33class Config {
34 constructor(appMode, progOptions, config) {
35 this.appMode = appMode;
36 this.progOptions = progOptions || {};
37 this.defaultOptions = {};
38 this.fileOptions = {};
39 this.config = config || {};
40 this.getters = {
41 test_page: 'getTestPage'
42 };
43
44 if (appMode === 'dev') {
45 this.progOptions.reporter = 'dev';
46 this.progOptions.parallel = -1;
47 }
48
49 if (this.progOptions.debug === true) {
50 this.progOptions.debug = 'testem.log';
51 }
52
53 if (appMode === 'ci') {
54 this.progOptions.disable_watching = true;
55 this.progOptions.single_run = true;
56 }
57 }
58
59 setDefaultOptions(defaultOptions) {
60 this.defaultOptions = defaultOptions;
61 }
62
63 read(callback) {
64 let configFile = this.progOptions.file;
65
66 if (configFile) {
67 this.readConfigFile(configFile, callback);
68 } else {
69 log.info('Seeking for config file...');
70
71 // Try all testem.json, testem.yml and testem.js
72 // testem.json gets precedence
73 let files = ['testem.json', '.testem.json', '.testem.yml', 'testem.yml', 'testem.js', '.testem.js'];
74 return Bluebird.filter(files.map(this.resolveConfigPath.bind(this)), fileExists).then(matched => {
75 let configFile = matched[0];
76 if (matched.length > 1) {
77 let baseNames = matched.map(fileName => path.basename(fileName));
78 console.warn('Found ' + matched.length + ' config files (' + baseNames + '), using ' + baseNames[0]);
79 }
80 if (configFile) {
81 this.readConfigFile(configFile, callback);
82 } else {
83 if (callback) {
84 callback.call(this);
85 }
86 }
87 });
88 }
89 }
90
91 resolvePath(filepath) {
92 if (filepath[0] === '/') {
93 return filepath;
94 }
95
96 return path.resolve(this.cwd(), filepath);
97 }
98
99 client() {
100 return {
101 decycle_depth: this.get('client_decycle_depth')
102 };
103 }
104
105 resolveConfigPath(filepath) {
106 if (this.progOptions.config_dir) {
107 return path.resolve(this.progOptions.config_dir, filepath);
108 } else if (this.defaultOptions && this.defaultOptions.config_dir) {
109 return path.resolve(this.defaultOptions.config_dir, filepath);
110 } else {
111 return this.resolvePath(filepath);
112 }
113 }
114
115 reverseResolvePath(filepath) {
116 return path.relative(this.cwd(), filepath);
117 }
118
119 cwd() {
120 return this.get('cwd') || process.cwd();
121 }
122
123 readConfigFile(configFile, callback) {
124 if (!configFile) { // allow empty configFile for programmatic setups
125 if (callback) {
126 callback.call(this);
127 }
128 } else if (configFile.match(/\.js$/)) {
129 this.readJS(configFile, callback);
130 } else if (configFile.match(/\.json$/)) {
131 this.readJSON(configFile, callback);
132 } else if (configFile.match(/\.yml$/)) {
133 this.readYAML(configFile, callback);
134 } else {
135 log.error('Unrecognized config file format for ' + configFile);
136 if (callback) {
137 callback.call(this);
138 }
139 }
140 }
141
142 readJS(configFile, callback) {
143 this.fileOptions = require(this.resolveConfigPath(configFile));
144 if (callback) {
145 callback.call(this);
146 }
147 }
148
149 readYAML(configFile, callback) {
150 fs.readFile(configFile, (err, data) => {
151 if (!err) {
152 let cfg = yaml.load(String(data));
153 this.fileOptions = cfg;
154 }
155 if (callback) {
156 callback.call(this);
157 }
158 });
159 }
160
161 readJSON(configFile, callback) {
162 fs.readFile(configFile, (err, data) => {
163 if (!err) {
164 let cfg = JSON.parse(data.toString());
165 this.fileOptions = cfg;
166 this.progOptions.file = configFile;
167 }
168 if (callback) {
169 callback.call(this);
170 }
171 });
172 }
173
174 mergeUrlAndQueryParams(urlString, queryParamsObj) {
175 if (!queryParamsObj) {
176 return urlString;
177 }
178
179 if (typeof queryParamsObj === 'string') {
180 if (queryParamsObj[0] === '?') {
181 queryParamsObj = queryParamsObj.substr(1);
182 }
183 queryParamsObj = querystring.parse(queryParamsObj);
184 }
185
186 let urlObj = url.parse(urlString);
187 let outputQueryParams = querystring.parse(urlObj.query) || {};
188 Object.keys(queryParamsObj).forEach(param => {
189 outputQueryParams[param] = queryParamsObj[param];
190 });
191 urlObj.query = outputQueryParams;
192 urlObj.search = querystring.stringify(outputQueryParams)
193 .replace(/=&/g, '&')
194 .replace(/=$/, '');
195 urlObj.path = urlObj.pathname + urlObj.search;
196 return url.format(urlObj);
197 }
198
199 getTestPage() {
200 let testPage = this.getConfigProperty('test_page');
201 let queryParams = this.getConfigProperty('query_params');
202
203 if (!Array.isArray(testPage)) {
204 testPage = [testPage];
205 }
206
207 return testPage.map(page => this.mergeUrlAndQueryParams(page, queryParams));
208 }
209
210 getConfigProperty(key) {
211 if (this.config && key in this.config) {
212 return this.config[key];
213 }
214 if (key in this.progOptions && typeof this.progOptions[key] !== 'undefined') {
215 return this.progOptions[key];
216 }
217 if (key in this.fileOptions && typeof this.fileOptions[key] !== 'undefined') {
218 return this.fileOptions[key];
219 }
220 if (this.defaultOptions && key in this.defaultOptions && typeof this.defaultOptions[key] !== 'undefined') {
221 return this.defaultOptions[key];
222 }
223 if (key in this.defaults) {
224 let defaultVal = this.defaults[key];
225 if (typeof defaultVal === 'function') {
226 return defaultVal.call(this);
227 } else {
228 return defaultVal;
229 }
230 }
231 }
232
233 get(key) {
234 let getterKey = this.getters[key];
235 let getter = getterKey && this[getterKey];
236 if (getter) {
237 return getter.call(this, key);
238 } else {
239 return this.getConfigProperty(key);
240 }
241 }
242
243 set(key, value) {
244 if (!this.config) {
245 this.config = {};
246 }
247 this.config[key] = value;
248 }
249
250 isCwdMode() {
251 return !this.get('src_files') && !this.get('test_page');
252 }
253
254 getAvailableLaunchers(cb) {
255 let browsers = knownBrowsers(process.platform, this);
256 browser_launcher.getAvailableBrowsers(this, browsers, (err, availableBrowsers) => {
257 if (err) {
258 return cb(err);
259 }
260
261 let availableLaunchers = {};
262 availableBrowsers.forEach(browser => {
263 let newLauncher = new LauncherFactory(browser.name, browser, this);
264 availableLaunchers[browser.name.toLowerCase()] = newLauncher;
265 });
266
267 // add custom launchers
268 let customLaunchers = this.get('launchers');
269 if (customLaunchers) {
270 for (let name in customLaunchers) {
271 let newLauncher = new LauncherFactory(name, customLaunchers[name], this);
272 availableLaunchers[name.toLowerCase()] = newLauncher;
273 }
274 }
275 cb(null, availableLaunchers);
276 });
277 }
278
279 getLaunchers(cb) {
280 this.getAvailableLaunchers((err, availableLaunchers) => {
281 if (err) {
282 return cb(err);
283 }
284
285 this.getWantedLaunchers(availableLaunchers, cb);
286 });
287 }
288
289 getWantedLauncherNames(available) {
290 let launchers = this.get('launch');
291 if (launchers) {
292 launchers = launchers.toLowerCase().split(',');
293 } else if (this.appMode === 'dev') {
294 launchers = this.get('launch_in_dev') || [];
295 } else {
296 launchers = this.get('launch_in_ci') || Object.keys(available);
297 }
298
299 let skip = this.get('skip');
300 if (skip) {
301 skip = skip.toLowerCase().split(',');
302 launchers = launchers.filter(name => skip.indexOf(name) === -1);
303 }
304 return launchers;
305 }
306
307 getWantedLaunchers(available, cb) {
308 let launchers = [];
309 let wanted = this.getWantedLauncherNames(available);
310 let err = null;
311
312 wanted.forEach(name => {
313 let launcher = available[name.toLowerCase()];
314 if (!launcher) {
315 if (this.appMode === 'dev' || this.get('ignore_missing_launchers')) {
316 log.warn('Launcher "' + name + '" is not recognized.');
317 } else {
318 err = new Error('Launcher ' + name + ' not found. Not installed?');
319 }
320 } else {
321 launchers.push(launcher);
322 }
323 });
324 cb(err, launchers);
325 }
326
327 printLauncherInfo() {
328 this.getAvailableLaunchers((err, launchers) => {
329 let launch_in_dev = (this.get('launch_in_dev') || [])
330 .map(s => s.toLowerCase());
331 let launch_in_ci = this.get('launch_in_ci');
332 if (launch_in_ci) {
333 launch_in_ci = launch_in_ci.map(s => s.toLowerCase());
334 }
335 launchers = Object.keys(launchers).map(k => launchers[k]);
336 console.log('Have ' + launchers.length + ' launchers available; auto-launch info displayed on the right.');
337 console.log(); // newline
338 console.log('Launcher Type CI Dev');
339 console.log('------------ ------------ -- ---');
340 console.log(launchers.map(launcher => {
341 let protocol = launcher.settings.protocol;
342 let kind = protocol === 'browser' ?
343 'browser' : (
344 protocol === 'tap' ?
345 'process(TAP)' : 'process');
346 let dev = launch_in_dev.indexOf(launcher.name.toLowerCase()) !== -1 ?
347 Chars.mark :
348 ' ';
349 let ci = !launch_in_ci || launch_in_ci.indexOf(launcher.name.toLowerCase()) !== -1 ?
350 Chars.mark :
351 ' ';
352 return (pad(launcher.name, 14, ' ', 1) +
353 pad(kind, 12, ' ', 1) +
354 ' ' + ci + ' ' + dev + ' ');
355 }).join('\n'));
356 });
357 }
358
359 getFileSet(want, dontWant, callback) {
360 if (isa(want, String)) {
361 want = [want]; // want is an Array
362 }
363 if (isa(dontWant, String)) {
364 dontWant = [dontWant]; // dontWant is an Array
365 }
366
367 // Filter glob < 6 negation patterns to still support them
368 // See https://github.com/isaacs/node-glob/tree/3f883c43#comments-and-negation
369 let positiveWants = [];
370 want.forEach(patternEntry => {
371 let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
372 if (pattern.indexOf('!') === 0) {
373 return dontWant.push(pattern.substring(1));
374 }
375
376 positiveWants.push(patternEntry);
377 });
378
379 dontWant = dontWant.map(p => p ? this.resolvePath(p) : p);
380 Bluebird.reduce(positiveWants, (allThatIWant, patternEntry) => {
381 let pattern = isa(patternEntry, String) ? patternEntry : patternEntry.src;
382 let attrs = patternEntry.attrs || [];
383 let patternUrl = url.parse(pattern);
384
385 if (patternUrl.protocol === 'file:') {
386 pattern = patternUrl.hostname + patternUrl.path;
387 } else if (patternUrl.protocol) {
388 return allThatIWant.concat({src: pattern, attrs: attrs});
389 }
390
391 return globAsync(this.resolvePath(pattern), { ignore: dontWant }).then(files => allThatIWant.concat(files.map(f => {
392 f = this.reverseResolvePath(f);
393 return {src: f, attrs: attrs};
394 })));
395 }, [])
396 .then(result => uniqBy(result, 'src'))
397 .asCallback(callback);
398 }
399
400 getSrcFiles(callback) {
401 let srcFiles = this.get('src_files') || '*.js';
402 let srcFilesIgnore = this.get('src_files_ignore') || '';
403 this.getFileSet(srcFiles, srcFilesIgnore, callback);
404 }
405
406 getFooterScripts(callback) {
407 var want = this.get('footer_scripts') || [];
408 var dontWant = this.get('src_files_ignore') || '';
409
410 this.getFileSet(want, dontWant, callback);
411 };
412
413
414 getServeFiles(callback) {
415 let want = this.get('serve_files') || this.get('src_files') || '*.js';
416 let dontWant = this.get('serve_files_ignore') || this.get('src_files_ignore') || '';
417 this.getFileSet(want, dontWant, callback);
418 }
419
420 getUserDataDir() {
421 if (this.get('user_data_dir')) {
422 return path.resolve(this.cwd(), this.get('user_data_dir'));
423 }
424
425 return os.tmpdir();
426 }
427
428 getHomeDir() {
429 return process.env.HOME || process.env.USERPROFILE;
430 }
431
432 getCSSFiles(callback) {
433 let want = this.get('css_files') || '';
434 this.getFileSet(want, '', callback);
435 }
436
437 getAllOptions() {
438 let options = [];
439 function getOptions(o) {
440 if (!o) {
441 return;
442 }
443 if (o.options) {
444 o.options.forEach(o => {
445 options.push(o.name());
446 });
447 }
448 getOptions(o.parent);
449 }
450 getOptions(this.progOptions);
451 return options;
452 }
453
454 getTemplateData(cb) {
455 let ret = {};
456 let options = this.getAllOptions();
457 let key;
458 for (key in this.progOptions) {
459 if (options.indexOf(key) !== -1) {
460 ret[key] = this.progOptions[key];
461 }
462 }
463 for (key in this.fileOptions) {
464 ret[key] = this.fileOptions[key];
465 }
466 for (key in this.config) {
467 ret[key] = this.config[key];
468 }
469 this.getServeFiles((err, files) => {
470 let replaceSlashes = f => ({
471 src: f.src.replace(/\\/g, '/'),
472 attrs: f.attrs
473 });
474
475 ret.serve_files = files.map(replaceSlashes);
476
477 this.getCSSFiles((err, files) => {
478 ret.css_files = files.map(replaceSlashes);
479 this.getFooterScripts((err, files) => {
480 ret.footer_scripts = files;
481 if (cb) {
482 cb(err, ret);
483 }
484 });
485 });
486 });
487 }
488}
489
490Config.prototype.defaults = {
491 host: 'localhost',
492 port: 7357,
493 url() {
494 let scheme = 'http';
495 if (this.get('key') || this.get('pfx')) {
496 scheme = 'https';
497 }
498 return scheme + '://' + this.get('host') + ':' + this.get('port') + '/';
499 },
500 parallel: 1,
501 reporter: 'tap',
502 bail_on_uncaught_error: true,
503 browser_start_timeout: 30,
504 browser_disconnect_timeout: 10,
505 browser_reconnect_limit: 3,
506 client_decycle_depth: 5,
507 socket_heartbeat_timeout() {
508 let browserDisconnectTimeout = this.get('browser_disconnect_timeout');
509 let defaultBrowserDisconnectTimeout = this.defaults.browser_disconnect_timeout;
510
511 return (browserDisconnectTimeout !== defaultBrowserDisconnectTimeout) ? browserDisconnectTimeout : 5;
512 }
513};
514
515module.exports = Config;