-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
cypress.ts
183 lines (147 loc) · 6.2 KB
/
cypress.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import debugFn from 'debug'
import type { ModuleNode, Plugin, ViteDevServer } from 'vite'
import type { Vite } from '../getVite'
import { parse, HTMLElement } from 'node-html-parser'
import fs from 'fs'
import type { ViteDevServerConfig } from '../devServer'
import path from 'path'
const debug = debugFn('cypress:vite-dev-server:plugins:cypress')
const INIT_FILEPATH = path.resolve(__dirname, '../../client/initCypressTests.js')
const HMR_DEPENDENCY_LOOKUP_MAX_ITERATION = 50
interface Spec {
absolute: string
relative: string
}
function getSpecsPathsSet (specs: Spec[]) {
return new Set<string>(
specs.map((spec) => spec.absolute),
)
}
export const Cypress = (
options: ViteDevServerConfig,
vite: Vite,
): Plugin => {
let base = '/'
const projectRoot = options.cypressConfig.projectRoot
const supportFilePath = options.cypressConfig.supportFile ? path.resolve(projectRoot, options.cypressConfig.supportFile) : false
const devServerEvents = options.devServerEvents
const specs = options.specs
const indexHtmlFile = options.cypressConfig.indexHtmlFile
let specsPathsSet = getSpecsPathsSet(specs)
// TODO: use async fs methods here
// eslint-disable-next-line no-restricted-syntax
let loader = fs.readFileSync(INIT_FILEPATH, 'utf8')
devServerEvents.on('dev-server:specs:changed', (specs: Spec[]) => {
specsPathsSet = getSpecsPathsSet(specs)
})
return {
name: 'cypress:main',
enforce: 'pre',
configResolved (config) {
base = config.base
},
async transformIndexHtml (html) {
// it's possibe other plugins have modified the HTML
// before we get to. For example vitejs/plugin-react will
// add a preamble. We do our best to look at the HTML we
// receive and inject it.
// For now we just grab any `<script>` tags and inject them to <head>.
// We will add more handling as we learn the use cases.
const root = parse(html)
const scriptTagsToInject = root.childNodes.filter((node) => {
return node instanceof HTMLElement && node.rawTagName === 'script'
})
const indexHtmlPath = path.resolve(projectRoot, indexHtmlFile)
debug('resolved the indexHtmlPath as', indexHtmlPath, 'from', indexHtmlFile)
let indexHtmlContent = await fs.promises.readFile(indexHtmlPath, { encoding: 'utf8' })
// Inject the script tags
indexHtmlContent = indexHtmlContent.replace(
'<head>',
`<head>
${scriptTagsToInject.join('')}
`,
)
// find </body> last index
const endOfBody = indexHtmlContent.lastIndexOf('</body>')
// insert the script in the end of the body
const newHtml = `
${indexHtmlContent.substring(0, endOfBody)}
<script>${loader}</script>
${indexHtmlContent.substring(endOfBody)}
`
return newHtml
},
configureServer: async (server: ViteDevServer) => {
server.middlewares.use(`${base}index.html`, async (req, res) => {
let transformedIndexHtml = await server.transformIndexHtml(base, '')
const viteImport = `<script type="module" src="${options.cypressConfig.devServerPublicPathRoute}/@vite/client"></script>`
// If we're doing cy-in-cy, we need to be able to access the Cypress instance from the parent frame.
if (process.env.CYPRESS_INTERNAL_VITE_OPEN_MODE_TESTING) {
transformedIndexHtml = transformedIndexHtml.replace(viteImport, `<script>document.domain = 'localhost';</script>${viteImport}`)
}
return res.end(transformedIndexHtml)
})
},
handleHotUpdate: ({ server, file }) => {
debug('handleHotUpdate - file', file)
// If the user provided IndexHtml is changed, do a full-reload
if (vite.normalizePath(file) === path.resolve(projectRoot, indexHtmlFile)) {
server.ws.send({
type: 'full-reload',
})
return
}
// get the graph node for the file that just got updated
let moduleImporters = server.moduleGraph.fileToModulesMap.get(file)
let iterationNumber = 0
const exploredFiles = new Set<string>()
// until we reached a point where the current module is imported by no other
while (moduleImporters?.size) {
if (iterationNumber > HMR_DEPENDENCY_LOOKUP_MAX_ITERATION) {
debug(`max hmr iteration reached: ${HMR_DEPENDENCY_LOOKUP_MAX_ITERATION}; Rerun will not happen on this file change.`)
return
}
// as soon as we find one of the specs, we trigger the re-run of tests
for (const mod of moduleImporters.values()) {
debug('handleHotUpdate - mod.file', mod.file)
if (mod.file === supportFilePath) {
debug('handleHotUpdate - support compile success')
devServerEvents.emit('dev-server:compile:success')
// if we update support we know we have to re-run it all
// no need to check further
return
}
if (mod.file && specsPathsSet.has(mod.file)) {
debug('handleHotUpdate - spec compile success', mod.file)
devServerEvents.emit('dev-server:compile:success', { specFile: mod.file })
// if we find one spec, does not mean we are done yet,
// there could be other spec files to re-run
// see https://github.com/cypress-io/cypress/issues/17691
}
}
// get all the modules that import the current one
moduleImporters = getImporters(moduleImporters, exploredFiles)
iterationNumber += 1
}
return
},
}
}
/**
* Gets all the modules that import the set of modules passed in parameters
* @param modules the set of module whose dependents to return
* @param alreadyExploredFiles set of files that have already been looked at and should be avoided in case of circular dependency
* @returns a set of ModuleMode that import directly the current modules
*/
function getImporters (modules: Set<ModuleNode>, alreadyExploredFiles: Set<string>): Set<ModuleNode> {
const allImporters = new Set<ModuleNode>()
modules.forEach((m) => {
if (m.file && !alreadyExploredFiles.has(m.file)) {
alreadyExploredFiles.add(m.file)
m.importers.forEach((imp) => {
allImporters.add(imp)
})
}
})
return allImporters
}