karma.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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. const url = require('url')
  14. const log = require('../logger').create('middleware:karma')
  15. const stripHost = require('./strip_host').stripHost
  16. const common = require('./common')
  17. const VERSION = require('../constants').VERSION
  18. const SCRIPT_TYPE = {
  19. js: 'text/javascript',
  20. module: 'module'
  21. }
  22. const FILE_TYPES = [
  23. 'css',
  24. 'html',
  25. 'js',
  26. 'module',
  27. 'dom'
  28. ]
  29. function filePathToUrlPath (filePath, basePath, urlRoot, proxyPath) {
  30. if (filePath.startsWith(basePath)) {
  31. return proxyPath + urlRoot.slice(1) + 'base' + filePath.slice(basePath.length)
  32. }
  33. return proxyPath + urlRoot.slice(1) + 'absolute' + filePath
  34. }
  35. function getQuery (urlStr) {
  36. // eslint-disable-next-line node/no-deprecated-api
  37. return url.parse(urlStr, true).query || {}
  38. }
  39. function getXUACompatibleMetaElement (url) {
  40. const query = getQuery(url)
  41. if (query['x-ua-compatible']) {
  42. return `<meta http-equiv="X-UA-Compatible" content="${query['x-ua-compatible']}"/>`
  43. }
  44. return ''
  45. }
  46. function getXUACompatibleUrl (url) {
  47. const query = getQuery(url)
  48. if (query['x-ua-compatible']) {
  49. return '?x-ua-compatible=' + encodeURIComponent(query['x-ua-compatible'])
  50. }
  51. return ''
  52. }
  53. function createKarmaMiddleware (
  54. filesPromise,
  55. serveStaticFile,
  56. serveFile,
  57. injector,
  58. basePath,
  59. urlRoot,
  60. upstreamProxy,
  61. browserSocketTimeout
  62. ) {
  63. const proxyPath = upstreamProxy ? upstreamProxy.path : '/'
  64. return function (request, response, next) {
  65. // These config values should be up to date on every request
  66. const client = injector.get('config.client')
  67. const customContextFile = injector.get('config.customContextFile')
  68. const customDebugFile = injector.get('config.customDebugFile')
  69. const customClientContextFile = injector.get('config.customClientContextFile')
  70. const includeCrossOriginAttribute = injector.get('config.crossOriginAttribute')
  71. const normalizedUrl = stripHost(request.url) || request.url
  72. // For backwards compatibility in middleware plugins, remove in v4.
  73. request.normalizedUrl = normalizedUrl
  74. let requestUrl = normalizedUrl.replace(/\?.*/, '')
  75. const requestedRangeHeader = request.headers.range
  76. // redirect /__karma__ to /__karma__ (trailing slash)
  77. if (requestUrl === urlRoot.slice(0, -1)) {
  78. response.setHeader('Location', proxyPath + urlRoot.slice(1))
  79. response.writeHead(301)
  80. return response.end('MOVED PERMANENTLY')
  81. }
  82. // ignore urls outside urlRoot
  83. if (!requestUrl.startsWith(urlRoot)) {
  84. return next()
  85. }
  86. // remove urlRoot prefix
  87. requestUrl = requestUrl.slice(urlRoot.length - 1)
  88. // serve client.html
  89. if (requestUrl === '/') {
  90. // redirect client_with_context.html
  91. if (!client.useIframe && client.runInParent) {
  92. requestUrl = '/client_with_context.html'
  93. } else { // serve client.html
  94. return serveStaticFile('/client.html', requestedRangeHeader, response, (data) =>
  95. data
  96. .replace('%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
  97. .replace('%X_UA_COMPATIBLE_URL%', getXUACompatibleUrl(request.url)))
  98. }
  99. }
  100. if (['/karma.js', '/context.js', '/debug.js'].includes(requestUrl)) {
  101. return serveStaticFile(requestUrl, requestedRangeHeader, response, (data) =>
  102. data
  103. .replace('%KARMA_URL_ROOT%', urlRoot)
  104. .replace('%KARMA_VERSION%', VERSION)
  105. .replace('%KARMA_PROXY_PATH%', proxyPath)
  106. .replace('%BROWSER_SOCKET_TIMEOUT%', browserSocketTimeout))
  107. }
  108. // serve the favicon
  109. if (requestUrl === '/favicon.ico') {
  110. return serveStaticFile(requestUrl, requestedRangeHeader, response)
  111. }
  112. // serve context.html - execution context within the iframe
  113. // or debug.html - execution context without channel to the server
  114. const isRequestingContextFile = requestUrl === '/context.html'
  115. const isRequestingDebugFile = requestUrl === '/debug.html'
  116. const isRequestingClientContextFile = requestUrl === '/client_with_context.html'
  117. if (isRequestingContextFile || isRequestingDebugFile || isRequestingClientContextFile) {
  118. return filesPromise.then((files) => {
  119. let fileServer
  120. let requestedFileUrl
  121. log.debug('custom files', customContextFile, customDebugFile, customClientContextFile)
  122. if (isRequestingContextFile && customContextFile) {
  123. log.debug(`Serving customContextFile ${customContextFile}`)
  124. fileServer = serveFile
  125. requestedFileUrl = customContextFile
  126. } else if (isRequestingDebugFile && customDebugFile) {
  127. log.debug(`Serving customDebugFile ${customDebugFile}`)
  128. fileServer = serveFile
  129. requestedFileUrl = customDebugFile
  130. } else if (isRequestingClientContextFile && customClientContextFile) {
  131. log.debug(`Serving customClientContextFile ${customClientContextFile}`)
  132. fileServer = serveFile
  133. requestedFileUrl = customClientContextFile
  134. } else {
  135. log.debug(`Serving static request ${requestUrl}`)
  136. fileServer = serveStaticFile
  137. requestedFileUrl = requestUrl
  138. }
  139. fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) {
  140. common.setNoCacheHeaders(response)
  141. const scriptTags = []
  142. for (const file of files.included) {
  143. let filePath = file.path
  144. const fileType = file.type || file.detectType()
  145. if (!FILE_TYPES.includes(fileType)) {
  146. if (file.type == null) {
  147. log.warn(
  148. 'Unable to determine file type from the file extension, defaulting to js.\n' +
  149. ` To silence the warning specify a valid type for ${file.originalPath} in the configuration file.\n` +
  150. ' See https://karma-runner.github.io/latest/config/files.html'
  151. )
  152. } else {
  153. log.warn(`Invalid file type (${file.type || 'empty string'}), defaulting to js.`)
  154. }
  155. }
  156. if (!file.isUrl) {
  157. filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath)
  158. if (requestUrl === '/context.html') {
  159. filePath += '?' + file.sha
  160. }
  161. }
  162. if (fileType === 'css') {
  163. scriptTags.push(`<link type="text/css" href="${filePath}" rel="stylesheet">`)
  164. } else if (fileType === 'dom') {
  165. scriptTags.push(file.content)
  166. } else if (fileType === 'html') {
  167. scriptTags.push(`<link href="${filePath}" rel="import">`)
  168. } else {
  169. const scriptType = (SCRIPT_TYPE[fileType] || 'text/javascript')
  170. const crossOriginAttribute = includeCrossOriginAttribute ? 'crossorigin="anonymous"' : ''
  171. if (fileType === 'module') {
  172. scriptTags.push(`<script onerror="throw 'Error loading ${filePath}'" type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
  173. } else {
  174. scriptTags.push(`<script type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
  175. }
  176. }
  177. }
  178. const scriptUrls = []
  179. // For client_with_context, html elements are not added directly through an iframe.
  180. // Instead, scriptTags is stored to window.__karma__.scriptUrls first. Later, the
  181. // client will read window.__karma__.scriptUrls and dynamically add them to the DOM
  182. // using DOMParser.
  183. if (requestUrl === '/client_with_context.html') {
  184. for (const script of scriptTags) {
  185. scriptUrls.push(
  186. // Escape characters with special roles (tags) in HTML. Open angle brackets are parsed as tags
  187. // immediately, even if it is within double quotations in browsers
  188. script.replace(/</g, '\\x3C').replace(/>/g, '\\x3E'))
  189. }
  190. }
  191. const mappings = data.includes('%MAPPINGS%') ? files.served.map((file) => {
  192. const filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath)
  193. .replace(/\\/g, '\\\\') // Windows paths contain backslashes and generate bad IDs if not escaped
  194. .replace(/'/g, '\\\'') // Escape single quotes - double quotes should not be allowed!
  195. return ` '${filePath}': '${file.sha}'`
  196. }) : []
  197. return data
  198. .replace('%SCRIPTS%', () => scriptTags.join('\n'))
  199. .replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n')
  200. .replace('%SCRIPT_URL_ARRAY%', () => 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n')
  201. .replace('%MAPPINGS%', () => 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n')
  202. .replace('%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
  203. })
  204. })
  205. } else if (requestUrl === '/context.json') {
  206. return filesPromise.then((files) => {
  207. common.setNoCacheHeaders(response)
  208. response.writeHead(200)
  209. response.end(JSON.stringify({
  210. files: files.included.map((file) => filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath))
  211. }))
  212. })
  213. }
  214. return next()
  215. }
  216. }
  217. createKarmaMiddleware.$inject = [
  218. 'filesPromise',
  219. 'serveStaticFile',
  220. 'serveFile',
  221. 'injector',
  222. 'config.basePath',
  223. 'config.urlRoot',
  224. 'config.upstreamProxy',
  225. 'config.browserSocketTimeout'
  226. ]
  227. // PUBLIC API
  228. exports.create = createKarmaMiddleware