UNPKG

10.3 kBJavaScriptView Raw
1var path = require('path')
2
3var logger = require('./logger')
4var log = logger.create('config')
5var helper = require('./helper')
6var constant = require('./constants')
7
8var COFFEE_SCRIPT_AVAILABLE = false
9var LIVE_SCRIPT_AVAILABLE = false
10
11// Coffee is required here to enable config files written in coffee-script.
12// It's not directly used in this file.
13try {
14 require('coffee-script').register()
15 COFFEE_SCRIPT_AVAILABLE = true
16} catch (e) {}
17
18// LiveScript is required here to enable config files written in LiveScript.
19// It's not directly used in this file.
20try {
21 require('LiveScript')
22 LIVE_SCRIPT_AVAILABLE = true
23} catch (e) {}
24
25var Pattern = function (pattern, served, included, watched, nocache) {
26 this.pattern = pattern
27 this.served = helper.isDefined(served) ? served : true
28 this.included = helper.isDefined(included) ? included : true
29 this.watched = helper.isDefined(watched) ? watched : true
30 this.nocache = helper.isDefined(nocache) ? nocache : false
31}
32
33var UrlPattern = function (url) {
34 Pattern.call(this, url, false, true, false, false)
35}
36
37var createPatternObject = function (pattern) {
38 if (pattern && helper.isString(pattern)) {
39 return helper.isUrlAbsolute(pattern) ? new UrlPattern(pattern) : new Pattern(pattern)
40 }
41
42 if (helper.isObject(pattern)) {
43 if (pattern.pattern && helper.isString(pattern.pattern)) {
44 return helper.isUrlAbsolute(pattern.pattern)
45 ? new UrlPattern(pattern.pattern)
46 : new Pattern(
47 pattern.pattern,
48 pattern.served,
49 pattern.included,
50 pattern.watched,
51 pattern.nocache)
52 }
53
54 log.warn('Invalid pattern %s!\n\tObject is missing "pattern" property.', pattern)
55 return new Pattern(null, false, false, false, false)
56 }
57
58 log.warn('Invalid pattern %s!\n\tExpected string or object with "pattern" property.', pattern)
59 return new Pattern(null, false, false, false, false)
60}
61
62var normalizeUrlRoot = function (urlRoot) {
63 var normalizedUrlRoot = urlRoot
64
65 if (normalizedUrlRoot.charAt(0) !== '/') {
66 normalizedUrlRoot = '/' + normalizedUrlRoot
67 }
68
69 if (normalizedUrlRoot.charAt(normalizedUrlRoot.length - 1) !== '/') {
70 normalizedUrlRoot = normalizedUrlRoot + '/'
71 }
72
73 if (normalizedUrlRoot !== urlRoot) {
74 log.warn('urlRoot normalized to "%s"', normalizedUrlRoot)
75 }
76
77 return normalizedUrlRoot
78}
79
80var normalizeConfig = function (config, configFilePath) {
81 var basePathResolve = function (relativePath) {
82 if (helper.isUrlAbsolute(relativePath)) {
83 return relativePath
84 }
85
86 if (!helper.isDefined(config.basePath) || !helper.isDefined(relativePath)) {
87 return ''
88 }
89 return path.resolve(config.basePath, relativePath)
90 }
91
92 var createPatternMapper = function (resolve) {
93 return function (objectPattern) {
94 objectPattern.pattern = resolve(objectPattern.pattern)
95
96 return objectPattern
97 }
98 }
99
100 if (helper.isString(configFilePath)) {
101 // resolve basePath
102 config.basePath = path.resolve(path.dirname(configFilePath), config.basePath)
103
104 // always ignore the config file itself
105 config.exclude.push(configFilePath)
106 } else {
107 config.basePath = path.resolve(config.basePath || '.')
108 }
109
110 config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
111 config.exclude = config.exclude.map(basePathResolve)
112
113 // normalize paths on windows
114 config.basePath = helper.normalizeWinPath(config.basePath)
115 config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
116 config.exclude = config.exclude.map(helper.normalizeWinPath)
117
118 // normalize urlRoot
119 config.urlRoot = normalizeUrlRoot(config.urlRoot)
120
121 // force protocol to end with ':'
122 config.protocol = (config.protocol || 'http').split(':')[0] + ':'
123 if (config.protocol.match(/https?:/) === null) {
124 log.warn('"%s" is not a supported protocol, defaulting to "http:"',
125 config.protocol)
126 config.protocol = 'http:'
127 }
128
129 if (config.proxies && config.proxies.hasOwnProperty(config.urlRoot)) {
130 log.warn('"%s" is proxied, you should probably change urlRoot to avoid conflicts',
131 config.urlRoot)
132 }
133
134 if (config.singleRun && config.autoWatch) {
135 log.debug('autoWatch set to false, because of singleRun')
136 config.autoWatch = false
137 }
138
139 if (!config.singleRun && config.browserDisconnectTolerance) {
140 log.debug('browserDisconnectTolerance set to 0, because of singleRun')
141 config.browserDisconnectTolerance = 0
142 }
143
144 if (helper.isString(config.reporters)) {
145 config.reporters = config.reporters.split(',')
146 }
147
148 if (config.client && config.client.args && !Array.isArray(config.client.args)) {
149 throw new Error('Invalid configuration: client.args must be an array of strings')
150 }
151
152 if (config.browsers && Array.isArray(config.browsers) === false) {
153 throw new TypeError('Invalid configuration: browsers option must be an array')
154 }
155
156 var defaultClient = config.defaultClient || {}
157 Object.keys(defaultClient).forEach(function (key) {
158 var option = config.client[key]
159 config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
160 })
161
162 // normalize preprocessors
163 var preprocessors = config.preprocessors || {}
164 var normalizedPreprocessors = config.preprocessors = Object.create(null)
165
166 Object.keys(preprocessors).forEach(function (pattern) {
167 var normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
168
169 normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
170 ? [preprocessors[pattern]] : preprocessors[pattern]
171 })
172
173 // define custom launchers/preprocessors/reporters - create an inlined plugin
174 var module = Object.create(null)
175 var hasSomeInlinedPlugin = false
176 var types = ['launcher', 'preprocessor', 'reporter']
177
178 types.forEach(function (type) {
179 var definitions = config['custom' + helper.ucFirst(type) + 's'] || {}
180
181 Object.keys(definitions).forEach(function (name) {
182 var definition = definitions[name]
183
184 if (!helper.isObject(definition)) {
185 return log.warn('Can not define %s %s. Definition has to be an object.', type, name)
186 }
187
188 if (!helper.isString(definition.base)) {
189 return log.warn('Can not define %s %s. Missing base %s.', type, name, type)
190 }
191
192 var token = type + ':' + definition.base
193 var locals = {
194 args: ['value', definition]
195 }
196
197 module[type + ':' + name] = ['factory', function (injector) {
198 var plugin = injector.createChild([locals], [token]).get(token)
199 if (type === 'launcher' && helper.isDefined(definition.displayName)) {
200 plugin.displayName = definition.displayName
201 }
202 return plugin
203 }]
204 hasSomeInlinedPlugin = true
205 })
206 })
207
208 if (hasSomeInlinedPlugin) {
209 config.plugins.push(module)
210 }
211
212 return config
213}
214
215var Config = function () {
216 var config = this
217
218 this.LOG_DISABLE = constant.LOG_DISABLE
219 this.LOG_ERROR = constant.LOG_ERROR
220 this.LOG_WARN = constant.LOG_WARN
221 this.LOG_INFO = constant.LOG_INFO
222 this.LOG_DEBUG = constant.LOG_DEBUG
223
224 this.set = function (newConfig) {
225 Object.keys(newConfig).forEach(function (key) {
226 config[key] = newConfig[key]
227 })
228 }
229
230 // DEFAULT CONFIG
231 this.frameworks = []
232 this.protocol = 'http:'
233 this.port = constant.DEFAULT_PORT
234 this.hostname = constant.DEFAULT_HOSTNAME
235 this.httpsServerConfig = {}
236 this.basePath = ''
237 this.files = []
238 this.exclude = []
239 this.logLevel = constant.LOG_INFO
240 this.colors = true
241 this.autoWatch = true
242 this.autoWatchBatchDelay = 250
243 this.restartOnFileChange = false
244 this.usePolling = process.platform === 'darwin' || process.platform === 'linux'
245 this.reporters = ['progress']
246 this.singleRun = false
247 this.browsers = []
248 this.captureTimeout = 60000
249 this.proxies = {}
250 this.proxyValidateSSL = true
251 this.preprocessors = {}
252 this.urlRoot = '/'
253 this.reportSlowerThan = 0
254 this.loggers = [constant.CONSOLE_APPENDER]
255 this.transports = ['polling', 'websocket']
256 this.forceJSONP = false
257 this.plugins = ['karma-*']
258 this.defaultClient = this.client = {
259 args: [],
260 useIframe: true,
261 captureConsole: true,
262 clearContext: true
263 }
264 this.browserDisconnectTimeout = 2000
265 this.browserDisconnectTolerance = 0
266 this.browserNoActivityTimeout = 10000
267 this.concurrency = Infinity
268 this.failOnEmptyTestSuite = true
269 this.retryLimit = 2
270}
271
272var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
273 ' config.set({\n' +
274 ' // your config\n' +
275 ' });\n' +
276 ' };\n'
277
278var parseConfig = function (configFilePath, cliOptions) {
279 var configModule
280 if (configFilePath) {
281 log.debug('Loading config %s', configFilePath)
282
283 try {
284 configModule = require(configFilePath)
285 } catch (e) {
286 if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(configFilePath) !== -1) {
287 log.error('File %s does not exist!', configFilePath)
288 } else {
289 log.error('Invalid config file!\n ' + e.stack)
290
291 var extension = path.extname(configFilePath)
292 if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
293 log.error('You need to install CoffeeScript.\n' +
294 ' npm install coffee-script --save-dev')
295 } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
296 log.error('You need to install LiveScript.\n' +
297 ' npm install LiveScript --save-dev')
298 }
299 }
300 return process.exit(1)
301 }
302 if (!helper.isFunction(configModule)) {
303 log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
304 return process.exit(1)
305 }
306 } else {
307 log.debug('No config file specified.')
308 // if no config file path is passed, we define a dummy config module.
309 configModule = function () {}
310 }
311
312 var config = new Config()
313 config.set(cliOptions)
314
315 try {
316 configModule(config)
317 } catch (e) {
318 log.error('Error in config file!\n', e)
319 return process.exit(1)
320 }
321
322 // merge the config from config file and cliOptions (precedence)
323 config.set(cliOptions)
324
325 // configure the logger as soon as we can
326 logger.setup(config.logLevel, config.colors, config.loggers)
327
328 return normalizeConfig(config, configFilePath)
329}
330
331// PUBLIC API
332exports.parseConfig = parseConfig
333exports.Pattern = Pattern
334exports.createPatternObject = createPatternObject
335exports.Config = Config