123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- /**
- * 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
- }
- }
- if (fileType === 'css') {
- scriptTags.push(`<link type="text/css" href="${filePath}" rel="stylesheet">`)
- } else if (fileType === 'dom') {
- scriptTags.push(file.content)
- } else if (fileType === 'html') {
- scriptTags.push(`<link href="${filePath}" rel="import">`)
- } else {
- const scriptType = (SCRIPT_TYPE[fileType] || 'text/javascript')
- const crossOriginAttribute = includeCrossOriginAttribute ? 'crossorigin="anonymous"' : ''
- if (fileType === 'module') {
- scriptTags.push(`<script onerror="throw 'Error loading ${filePath}'" type="${scriptType}" src="${filePath}" ${crossOriginAttribute}></script>`)
- } else {
- scriptTags.push(`<script type="${scriptType}" src="${filePath}" ${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
|