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']) {
|
---|
48 | return `\n<meta http-equiv="X-UA-Compatible" content="${query['x-ua-compatible']}"/>`
|
---|
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
|
---|
110 | .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
|
---|
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')
|
---|
229 | .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
|
---|
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
|
---|