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