1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | 'use strict';
|
11 |
|
12 | const os = require('os');
|
13 | const fs = require('fs');
|
14 | const yaml = require('js-yaml');
|
15 | const log = require('npmlog');
|
16 | const path = require('path');
|
17 | const glob = require('glob');
|
18 | const url = require('url');
|
19 | const querystring = require('querystring');
|
20 | const Bluebird = require('bluebird');
|
21 |
|
22 | const browser_launcher = require('./browser_launcher');
|
23 | const LauncherFactory = require('./launcher-factory');
|
24 | const Chars = require('./utils/chars');
|
25 | const pad = require('./utils/strutils').pad;
|
26 | const isa = require('./utils/isa');
|
27 | const fileExists = require('./utils/fileutils').fileExists;
|
28 | const uniqBy = require('lodash.uniqby');
|
29 |
|
30 | const knownBrowsers = require('./utils/known-browsers');
|
31 | const globAsync = Bluebird.promisify(glob);
|
32 |
|
33 | class 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 |
|
72 |
|
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) {
|
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 |
|
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();
|
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];
|
362 | }
|
363 | if (isa(dontWant, String)) {
|
364 | dontWant = [dontWant];
|
365 | }
|
366 |
|
367 |
|
368 |
|
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 |
|
490 | Config.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 |
|
515 | module.exports = Config;
|