UNPKG

karma

Version:

Spectacular Test Runner for JavaScript.

260 lines (229 loc) 10.2 kB
/** * Karma middleware is responsible for serving: * - client.html (the entrypoint for capturing a browser) * - debug.html * - context.html (the execution context, loaded within an iframe) * - karma.js * * The main part is generating context.html, as it contains: * - generating mappings * - including <script> and <link> tags * - setting propert caching headers */ const url = require('url') const log = require('../logger').create('middleware:karma') const stripHost = require('./strip_host').stripHost const common = require('./common') const VERSION = require('../constants').VERSION const SCRIPT_TYPE = { js: 'text/javascript', module: 'module' } const FILE_TYPES = [ 'css', 'html', 'js', 'module', 'dom' ] function filePathToUrlPath (filePath, basePath, urlRoot, proxyPath) { if (filePath.startsWith(basePath)) { return proxyPath + urlRoot.slice(1) + 'base' + filePath.slice(basePath.length) } return proxyPath + urlRoot.slice(1) + 'absolute' + filePath } function getQuery (urlStr) { // eslint-disable-next-line node/no-deprecated-api return url.parse(urlStr, true).query || {} } function getXUACompatibleMetaElement (url) { const query = getQuery(url) if (query['x-ua-compatible']) { return `<meta http-equiv="X-UA-Compatible" content="${query['x-ua-compatible']}"/>` } return '' } function getXUACompatibleUrl (url) { const query = getQuery(url) if (query['x-ua-compatible']) { return '?x-ua-compatible=' + encodeURIComponent(query['x-ua-compatible']) } return '' } function createKarmaMiddleware ( filesPromise, serveStaticFile, serveFile, injector, basePath, urlRoot, upstreamProxy, browserSocketTimeout ) { const proxyPath = upstreamProxy ? upstreamProxy.path : '/' return function (request, response, next) { // These config values should be up to date on every request const client = injector.get('config.client') const customContextFile = injector.get('config.customContextFile') const customDebugFile = injector.get('config.customDebugFile') const customClientContextFile = injector.get('config.customClientContextFile') const includeCrossOriginAttribute = injector.get('config.crossOriginAttribute') const normalizedUrl = stripHost(request.url) || request.url // For backwards compatibility in middleware plugins, remove in v4. request.normalizedUrl = normalizedUrl let requestUrl = normalizedUrl.replace(/\?.*/, '') const requestedRangeHeader = request.headers.range // redirect /__karma__ to /__karma__ (trailing slash) if (requestUrl === urlRoot.slice(0, -1)) { response.setHeader('Location', proxyPath + urlRoot.slice(1)) response.writeHead(301) return response.end('MOVED PERMANENTLY') } // ignore urls outside urlRoot if (!requestUrl.startsWith(urlRoot)) { return next() } // remove urlRoot prefix requestUrl = requestUrl.slice(urlRoot.length - 1) // serve client.html if (requestUrl === '/') { // redirect client_with_context.html if (!client.useIframe && client.runInParent) { requestUrl = '/client_with_context.html' } else { // serve client.html return serveStaticFile('/client.html', requestedRangeHeader, response, (data) => data .replace('%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url)) .replace('%X_UA_COMPATIBLE_URL%', getXUACompatibleUrl(request.url))) } } if (['/karma.js', '/context.js', '/debug.js'].includes(requestUrl)) { return serveStaticFile(requestUrl, requestedRangeHeader, response, (data) => data .replace('%KARMA_URL_ROOT%', urlRoot) .replace('%KARMA_VERSION%', VERSION) .replace('%KARMA_PROXY_PATH%', proxyPath) .replace('%BROWSER_SOCKET_TIMEOUT%', browserSocketTimeout)) } // serve the favicon if (requestUrl === '/favicon.ico') { return serveStaticFile(requestUrl, requestedRangeHeader, response) } // serve context.html - execution context within the iframe // or debug.html - execution context without channel to the server const isRequestingContextFile = requestUrl === '/context.html' const isRequestingDebugFile = requestUrl === '/debug.html' const isRequestingClientContextFile = requestUrl === '/client_with_context.html' if (isRequestingContextFile || isRequestingDebugFile || isRequestingClientContextFile) { return filesPromise.then((files) => { let fileServer let requestedFileUrl log.debug('custom files', customContextFile, customDebugFile, customClientContextFile) if (isRequestingContextFile && customContextFile) { log.debug(`Serving customContextFile ${customContextFile}`) fileServer = serveFile requestedFileUrl = customContextFile } else if (isRequestingDebugFile && customDebugFile) { log.debug(`Serving customDebugFile ${customDebugFile}`) fileServer = serveFile requestedFileUrl = customDebugFile } else if (isRequestingClientContextFile && customClientContextFile) { log.debug(`Serving customClientContextFile ${customClientContextFile}`) fileServer = serveFile requestedFileUrl = customClientContextFile } else { log.debug(`Serving static request ${requestUrl}`) fileServer = serveStaticFile requestedFileUrl = requestUrl } fileServer(requestedFileUrl, requestedRangeHeader, response, function (data) { common.setNoCacheHeaders(response) const scriptTags = [] for (const file of files.included) { let filePath = file.path const fileType = file.type || file.detectType() if (!FILE_TYPES.includes(fileType)) { if (file.type == null) { log.warn( 'Unable to determine file type from the file extension, defaulting to js.\n' + ` To silence the warning specify a valid type for ${file.originalPath} in the configuration file.\n` + ' See https://karma-runner.github.io/latest/config/files.html' ) } else { log.warn(`Invalid file type (${file.type || 'empty string'}), defaulting to js.`) } } if (!file.isUrl) { filePath = filePathToUrlPath(filePath, basePath, urlRoot, proxyPath) if (requestUrl === '/context.html') { filePath += '?' + file.sha } } const integrityAttribute = file.integrity ? ` integrity="${file.integrity}"` : '' const crossOriginAttribute = includeCrossOriginAttribute ? ' crossorigin="anonymous"' : '' if (fileType === 'css') { scriptTags.push(`<link type="text/css" href="${filePath}" rel="stylesheet"${integrityAttribute}${crossOriginAttribute}>`) } else if (fileType === 'dom') { scriptTags.push(file.content) } else if (fileType === 'html') { scriptTags.push(`<link href="${filePath}" rel="import"${integrityAttribute}${crossOriginAttribute}>`) } else { const scriptType = (SCRIPT_TYPE[fileType] || 'text/javascript') if (fileType === 'module') { scriptTags.push(`<script onerror="throw 'Error loading ${filePath}'" type="${scriptType}" src="${filePath}"${integrityAttribute}${crossOriginAttribute}></script>`) } else { scriptTags.push(`<script type="${scriptType}" src="${filePath}"${integrityAttribute}${crossOriginAttribute}></script>`) } } } const scriptUrls = [] // For client_with_context, html elements are not added directly through an iframe. // Instead, scriptTags is stored to window.__karma__.scriptUrls first. Later, the // client will read window.__karma__.scriptUrls and dynamically add them to the DOM // using DOMParser. if (requestUrl === '/client_with_context.html') { for (const script of scriptTags) { scriptUrls.push( // Escape characters with special roles (tags) in HTML. Open angle brackets are parsed as tags // immediately, even if it is within double quotations in browsers script.replace(/</g, '\\x3C').replace(/>/g, '\\x3E')) } } const mappings = data.includes('%MAPPINGS%') ? files.served.map((file) => { const filePath = filePathToUrlPath(file.path, basePath, urlRoot, proxyPath) .replace(/\\/g, '\\\\') // Windows paths contain backslashes and generate bad IDs if not escaped .replace(/'/g, '\\\'') // Escape single quotes - double quotes should not be allowed! return ` '${filePath}': '${file.sha}'` }) : [] return data .replace('%SCRIPTS%', () => scriptTags.join('\n')) .replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n') .replace('%SCRIPT_URL_ARRAY%', () => 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n') .replace('%MAPPINGS%', () => 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n') .replace('%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url)) }) }) } else if (requestUrl === '/context.json') { return filesPromise.then((files) => { common.setNoCacheHeaders(response) response.writeHead(200) response.end(JSON.stringify({ files: files.included.map((file) => filePathToUrlPath(file.path + '?' + file.sha, basePath, urlRoot, proxyPath)) })) }) } return next() } } createKarmaMiddleware.$inject = [ 'filesPromise', 'serveStaticFile', 'serveFile', 'injector', 'config.basePath', 'config.urlRoot', 'config.upstreamProxy', 'config.browserSocketTimeout' ] // PUBLIC API exports.create = createKarmaMiddleware