karma.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /**
  2. * Karma middleware is responsible for serving:
  3. * - client.html (the entrypoint for capturing a browser)
  4. * - debug.html
  5. * - context.html (the execution context, loaded within an iframe)
  6. * - karma.js
  7. *
  8. * The main part is generating context.html, as it contains:
  9. * - generating mappings
  10. * - including <script> and <link> tags
  11. * - setting propert caching headers
  12. */
  13. var path = require('path')
  14. var util = require('util')
  15. var url = require('url')
  16. var _ = require('lodash')
  17. var log = require('../logger').create('middleware:karma')
  18. var urlparse = function (urlStr) {
  19. var urlObj = url.parse(urlStr, true)
  20. urlObj.query = urlObj.query || {}
  21. return urlObj
  22. }
  23. var common = require('./common')
  24. var VERSION = require('../constants').VERSION
  25. var SCRIPT_TAG = '<script type="%s" src="%s" %s></script>'
  26. var CROSSORIGIN_ATTRIBUTE = 'crossorigin="anonymous"'
  27. var LINK_TAG_CSS = '<link type="text/css" href="%s" rel="stylesheet">'
  28. var LINK_TAG_HTML = '<link href="%s" rel="import">'
  29. var SCRIPT_TYPE = {
  30. 'js': 'text/javascript',
  31. 'dart': 'application/dart',
  32. 'module': 'module'
  33. }
  34. var FILE_TYPES = [
  35. 'css',
  36. 'html',
  37. 'js',
  38. 'dart',
  39. 'module'
  40. ]
  41. var filePathToUrlPath = function (filePath, basePath, urlRoot, proxyPath) {
  42. if (filePath.indexOf(basePath) === 0) {
  43. return proxyPath + urlRoot.substr(1) + 'base' + filePath.substr(basePath.length)
  44. }
  45. return proxyPath + urlRoot.substr(1) + 'absolute' + filePath
  46. }
  47. var getXUACompatibleMetaElement = function (url) {
  48. var tag = ''
  49. var urlObj = urlparse(url)
  50. if (urlObj.query['x-ua-compatible']) {
  51. tag = '\n<meta http-equiv="X-UA-Compatible" content="' +
  52. urlObj.query['x-ua-compatible'] + '"/>'
  53. }
  54. return tag
  55. }
  56. var getXUACompatibleUrl = function (url) {
  57. var value = ''
  58. var urlObj = urlparse(url)
  59. if (urlObj.query['x-ua-compatible']) {
  60. value = '?x-ua-compatible=' + encodeURIComponent(urlObj.query['x-ua-compatible'])
  61. }
  62. return value
  63. }
  64. var createKarmaMiddleware = function (
  65. filesPromise,
  66. serveStaticFile,
  67. serveFile,
  68. injector,
  69. basePath,
  70. urlRoot,
  71. upstreamProxy
  72. ) {
  73. var proxyPath = upstreamProxy ? upstreamProxy.path : '/'
  74. return function (request, response, next) {
  75. // These config values should be up to date on every request
  76. var client = injector.get('config.client')
  77. var customContextFile = injector.get('config.customContextFile')
  78. var customDebugFile = injector.get('config.customDebugFile')
  79. var customClientContextFile = injector.get('config.customClientContextFile')
  80. var includeCrossOriginAttribute = injector.get('config.crossOriginAttribute')
  81. var requestUrl = request.normalizedUrl.replace(/\?.*/, '')
  82. var requestedRangeHeader = request.headers['range']
  83. // redirect /__karma__ to /__karma__ (trailing slash)
  84. if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) {
  85. response.setHeader('Location', proxyPath + urlRoot.substr(1))
  86. response.writeHead(301)
  87. return response.end('MOVED PERMANENTLY')
  88. }
  89. // ignore urls outside urlRoot
  90. if (requestUrl.indexOf(urlRoot) !== 0) {
  91. return next()
  92. }
  93. // remove urlRoot prefix
  94. requestUrl = requestUrl.substr(urlRoot.length - 1)
  95. // serve client.html
  96. if (requestUrl === '/') {
  97. // redirect client_with_context.html
  98. if (!client.useIframe && client.runInParent) {
  99. requestUrl = '/client_with_context.html'
  100. } else { // serve client.html
  101. return serveStaticFile('/client.html', requestedRangeHeader, response, function (data) {
  102. return data
  103. .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
  104. .replace('%X_UA_COMPATIBLE_URL%', getXUACompatibleUrl(request.url))
  105. })
  106. }
  107. }
  108. // serve karma.js, context.js, and debug.js
  109. var jsFiles = ['/karma.js', '/context.js', '/debug.js']
  110. var isRequestingJsFile = jsFiles.indexOf(requestUrl) !== -1
  111. if (isRequestingJsFile) {
  112. return serveStaticFile(requestUrl, requestedRangeHeader, response, function (data) {
  113. return data.replace('%KARMA_URL_ROOT%', urlRoot)
  114. .replace('%KARMA_VERSION%', VERSION)
  115. .replace('%KARMA_PROXY_PATH%', proxyPath)
  116. })
  117. }
  118. // serve the favicon
  119. if (requestUrl === '/favicon.ico') {
  120. return serveStaticFile(requestUrl, requestedRangeHeader, response)
  121. }
  122. // serve context.html - execution context within the iframe
  123. // or debug.html - execution context without channel to the server
  124. var isRequestingContextFile = requestUrl === '/context.html'
  125. var isRequestingDebugFile = requestUrl === '/debug.html'
  126. var isRequestingClientContextFile = requestUrl === '/client_with_context.html'
  127. if (isRequestingContextFile || isRequestingDebugFile || isRequestingClientContextFile) {
  128. return filesPromise.then(function (files) {
  129. var fileServer
  130. var requestedFileUrl
  131. log.debug('custom files', customContextFile, customDebugFile, customClientContextFile)
  132. if (isRequestingContextFile && customContextFile) {
  133. log.debug('Serving customContextFile %s', customContextFile)
  134. fileServer = serveFile
  135. requestedFileUrl = customContextFile
  136. } else if (isRequestingDebugFile && customDebugFile) {
  137. log.debug('Serving customDebugFile %s', customDebugFile)
  138. fileServer = serveFile
  139. requestedFileUrl = customDebugFile
  140. } else if (isRequestingClientContextFile && customClientContextFile) {
  141. log.debug('Serving customClientContextFile %s', customClientContextFile)
  142. fileServer = serveFile
  143. requestedFileUrl = customClientContextFile
  144. } else {
  145. log.debug('Serving static request %s', requestUrl)
  146. fileServer = serveStaticFile
  147. requestedFileUrl = requestUrl
  148. }
  149. fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) {
  150. common.setNoCacheHeaders(response)
  151. var scriptTags = []
  152. var scriptUrls = []
  153. for (var i = 0; i < files.included.length; i++) {
  154. var file = files.included[i]
  155. var filePath = file.path
  156. var fileExt = path.extname(filePath)
  157. var fileType = file.type
  158. if (!files.included.hasOwnProperty(i)) {
  159. continue
  160. }
  161. if (!_.isUndefined(fileType) && FILE_TYPES.indexOf(fileType) === -1) {
  162. log.warn('Invalid file type, defaulting to js.', fileType)
  163. }
  164. if (!file.isUrl) {
  165. filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath)
  166. if (requestUrl === '/context.html') {
  167. filePath += '?' + file.sha
  168. }
  169. }
  170. scriptUrls.push(filePath)
  171. if (fileType === 'css' || (!fileType && fileExt === '.css')) {
  172. scriptTags.push(util.format(LINK_TAG_CSS, filePath))
  173. continue
  174. }
  175. if (fileType === 'html' || (!fileType && fileExt === '.html')) {
  176. scriptTags.push(util.format(LINK_TAG_HTML, filePath))
  177. continue
  178. }
  179. // The script tag to be placed
  180. var scriptFileType = (fileType || fileExt.substring(1))
  181. var scriptType = (SCRIPT_TYPE[scriptFileType] || 'text/javascript')
  182. var crossOriginAttribute = includeCrossOriginAttribute ? CROSSORIGIN_ATTRIBUTE : ''
  183. scriptTags.push(util.format(SCRIPT_TAG, scriptType, filePath, crossOriginAttribute))
  184. }
  185. // TODO(vojta): don't compute if it's not in the template
  186. var mappings = files.served.map(function (file) {
  187. // Windows paths contain backslashes and generate bad IDs if not escaped
  188. var filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath).replace(/\\/g, '\\\\')
  189. // Escape single quotes that might be in the filename -
  190. // double quotes should not be allowed!
  191. filePath = filePath.replace(/'/g, '\\\'')
  192. return util.format(" '%s': '%s'", filePath, file.sha)
  193. })
  194. var clientConfig = 'window.__karma__.config = ' + JSON.stringify(client) + ';\n'
  195. var scriptUrlsJS = 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n'
  196. mappings = 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n'
  197. return data
  198. .replace('%SCRIPTS%', scriptTags.join('\n'))
  199. .replace('%CLIENT_CONFIG%', clientConfig)
  200. .replace('%SCRIPT_URL_ARRAY%', scriptUrlsJS)
  201. .replace('%MAPPINGS%', mappings)
  202. .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
  203. })
  204. }, function (errorFiles) {
  205. serveStaticFile(requestUrl, response, function (data) {
  206. common.setNoCacheHeaders(response)
  207. return data.replace('%SCRIPTS%', '').replace('%CLIENT_CONFIG%', '').replace('%MAPPINGS%',
  208. 'window.__karma__.error("TEST RUN WAS CANCELLED because ' +
  209. (errorFiles.length > 1 ? 'these files contain' : 'this file contains') +
  210. ' some errors:\\n ' + errorFiles.join('\\n ') + '");')
  211. })
  212. })
  213. } else if (requestUrl === '/context.json') {
  214. return filesPromise.then(function (files) {
  215. common.setNoCacheHeaders(response)
  216. response.writeHead(200)
  217. response.end(JSON.stringify({
  218. files: files.included.map(function (file) {
  219. return filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath)
  220. })
  221. }))
  222. })
  223. }
  224. return next()
  225. }
  226. }
  227. createKarmaMiddleware.$inject = [
  228. 'filesPromise',
  229. 'serveStaticFile',
  230. 'serveFile',
  231. 'injector',
  232. 'config.basePath',
  233. 'config.urlRoot',
  234. 'config.upstreamProxy'
  235. ]
  236. // PUBLIC API
  237. exports.create = createKarmaMiddleware