Skip to content

Commit 9ca177c

Browse files
authored
feat(app): run e2e tests (#18448)
* update server * add launchMode param * update logic * update types * update gql * basic WIP for e2e * hot reload * dead code * try to conditionally launch existing runner * types * update server * params * append gql port * override to default ot e2e runner on launchpad and app e2e tests * do not set launchpad variable for e2e runs * use window to get graphql port and correctly run e2e conditionally using CIRCLECI env. var
1 parent d9347fc commit 9ca177c

File tree

15 files changed

+233
-129
lines changed

15 files changed

+233
-129
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('visit google', () => {
2+
it('goes to google.com', () => {
3+
cy.visit('https://google.com')
4+
})
5+
})

packages/app/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,18 @@ declare global {
4848
teardown: (state: Store) => Promise<void>
4949
teardownReporter: () => Promise<void>
5050
[key: string]: any
51+
52+
on (event: 'restart', ...args: unknown[]): void
5153
}
5254

55+
/**
56+
* This is the config served from the back-end.
57+
* We will manage config using GraphQL going forward,
58+
* but for now we are also caching it on `window`
59+
* to be able to move fast and iterate
60+
*/
61+
config: Record<string, any>
62+
5363
/**
5464
* To ensure we are only a single copy of React
5565
* We get a reference to the copy of React (and React DOM)

packages/app/src/pages/Specs.vue

-6
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,3 @@ const props = defineProps<{
3232
name: "Specs Page"
3333
}
3434
</route>
35-
36-
<style>
37-
iframe {
38-
width: 100%;
39-
}
40-
</style>

packages/app/src/runner/index.ts

+71-5
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ function teardownSpec (store: Store) {
6363
* Cypress on it.
6464
*
6565
*/
66-
function setupSpec (spec: BaseSpec) {
67-
// @ts-ignore - TODO: figure out how to manage window.config.
68-
const config = window.config
66+
function setupSpecCT (spec: BaseSpec) {
67+
// TODO: figure out how to manage window.config.
68+
const config = window.UnifiedRunner.config
6969

7070
// this is how the Cypress driver knows which spec to run.
7171
config.spec = spec
@@ -98,6 +98,64 @@ function setupSpec (spec: BaseSpec) {
9898
window.UnifiedRunner.eventManager.initialize($autIframe, config)
9999
}
100100

101+
/**
102+
* Create a Spec IFrame. Used for loading the spec to execute in E2E
103+
*/
104+
function createSpecIFrame (specSrc: string) {
105+
const el = document.createElement('iframe')
106+
107+
el.id = `Your Spec: '${specSrc}'`,
108+
el.className = 'spec-iframe'
109+
110+
return el
111+
}
112+
113+
/**
114+
* Set up an E2E spec by creating a fresh AUT for the spec to evaluate under,
115+
* a Spec IFrame to load the spec's source code, and
116+
* initialize Cypress on the AUT.
117+
*/
118+
function setupSpecE2E (spec: BaseSpec) {
119+
// TODO: manage config with GraphQL, don't put it on window.
120+
const config = window.UnifiedRunner.config
121+
122+
// this is how the Cypress driver knows which spec to run.
123+
config.spec = spec
124+
125+
// creates a new instance of the Cypress driver for this spec,
126+
// initializes a bunch of listeners
127+
// watches spec file for changes.
128+
window.UnifiedRunner.eventManager.setup(config)
129+
130+
const $runnerRoot = getRunnerElement()
131+
132+
// clear AUT, if there is one.
133+
empty($runnerRoot)
134+
135+
// create root for new AUT
136+
const $container = document.createElement('div')
137+
138+
$runnerRoot.append($container)
139+
140+
// create new AUT
141+
const autIframe = new window.UnifiedRunner.AutIframe('Test Project')
142+
143+
autIframe.showInitialBlankContents()
144+
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
145+
146+
// create Spec IFrame
147+
const specSrc = getSpecUrl(config.namespace, spec)
148+
const $specIframe = createSpecIFrame(specSrc)
149+
150+
// append to document, so the iframe will execute the spec
151+
$container.appendChild($specIframe)
152+
153+
$specIframe.src = specSrc
154+
155+
// initialize Cypress (driver) with the AUT!
156+
window.UnifiedRunner.eventManager.initialize($autIframe, config)
157+
}
158+
101159
/**
102160
* Inject the global `UnifiedRunner` via a <script src="..."> tag.
103161
* which includes the event manager and AutIframe constructor.
@@ -125,7 +183,7 @@ function initialize (ready: () => void) {
125183
*
126184
* 4. Force the Reporter to re-render with the new spec we are executed.
127185
*
128-
* 5. Setup the spec. This involves a few things, see the `setupSpec` function's
186+
* 5. Setup the spec. This involves a few things, see the `setupSpecCT` function's
129187
* description for more information.
130188
*/
131189
async function executeSpec (spec: BaseSpec) {
@@ -137,7 +195,15 @@ async function executeSpec (spec: BaseSpec) {
137195

138196
UnifiedReporterAPI.setupReporter()
139197

140-
return setupSpec(spec)
198+
if (window.UnifiedRunner.config.testingType === 'e2e') {
199+
return setupSpecE2E(spec)
200+
}
201+
202+
if (window.UnifiedRunner.config.testingType === 'component') {
203+
return setupSpecCT(spec)
204+
}
205+
206+
throw Error('Unknown or undefined testingType on window.UnifiedRunner.config.testingType')
141207
}
142208

143209
export const UnifiedRunnerAPI = {

packages/app/src/runner/injectBundle.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export async function injectBundle (ready: () => void) {
4242
// injectReporterStyle()
4343

4444
script.onload = () => {
45-
// @ts-ignore - just stick config on window until we figure out how we are
45+
// just stick config on window until we figure out how we are
4646
// going to manage it
47-
window.config = window.UnifiedRunner.decodeBase64(data.base64Config)
47+
window.UnifiedRunner.config = window.UnifiedRunner.decodeBase64(data.base64Config)
4848
ready()
4949
}
5050
}

packages/app/src/runs/Runner.vue

+18-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ const props = defineProps<{
3030
}>()
3131
3232
onMounted(() => {
33-
UnifiedRunnerAPI.initialize(execute)
33+
UnifiedRunnerAPI.initialize(() => {
34+
window.UnifiedRunner.eventManager.on('restart', () => {
35+
execute()
36+
})
37+
38+
execute()
39+
})
3440
})
3541
3642
onBeforeUnmount(() => {
@@ -72,3 +78,14 @@ const execute = () => {
7278
name: "Runner"
7379
}
7480
</route>
81+
82+
<style>
83+
#unified-runner > div {
84+
height: 100%;
85+
}
86+
87+
iframe {
88+
width: 100%;
89+
height: 100%;
90+
}
91+
</style>

packages/app/src/specs/SpecsList.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ fragment Specs_SpecsList on App {
6262
activeProject {
6363
id
6464
projectRoot
65-
specs(first: 25) {
65+
specs: specs(first: 25) {
6666
edges {
6767
...SpecNode_SpecsList
6868
}

packages/driver/src/cypress.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class $Cypress {
100100

101101
// set domainName but allow us to turn
102102
// off this feature in testing
103-
if (d) {
103+
if (d && config.testingType === 'e2e') {
104104
document.domain = d
105105
}
106106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
describe('visit google', () => {
2+
it('goes to google.com', () => {
3+
cy.visit('https://google.com')
4+
})
5+
})

packages/server/lib/controllers/runner.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _ from 'lodash'
2-
import type { Response } from 'express'
2+
import type { Request, Response } from 'express'
33
import send from 'send'
44
import os from 'os'
55
import { fs } from '../util/fs'
@@ -82,8 +82,8 @@ export const runner = {
8282
return serveRunner(runnerPkg, config, res)
8383
},
8484

85-
handle (testingType, req, res) {
86-
const pathToFile = getPathToDist(testingType === 'e2e' ? 'runner' : 'runner-ct', req.params[0])
85+
handle (testingType: Cypress.TestingType, req: Request, res: Response) {
86+
const pathToFile = getPathToDist(process.env.LAUNCHPAD || testingType === 'component' ? 'runner-ct' : 'runner', req.params[0])
8787

8888
return send(req, pathToFile)
8989
.pipe(res)

packages/server/lib/gui/makeGraphQLServer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function makeGraphQLServer (ctx: DataContext) {
4141

4242
const srv = graphqlServer = app.listen(() => {
4343
const port = (srv.address() as AddressInfo).port
44+
4445
const endpoint = `http://localhost:${port}/graphql`
4546

4647
if (process.env.NODE_ENV === 'development') {

packages/server/lib/open_project.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class OpenProject {
124124
// of potential domain changes, request buffers, etc
125125
this.openProject!.reset()
126126

127-
const url = getSpecUrl({
127+
let url = getSpecUrl({
128128
absoluteSpecPath: spec.absolute,
129129
specType: spec.specType,
130130
browserUrl: this.openProject.cfg.browserUrl,

packages/server/lib/routes-ct.ts

+1-105
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import Debug from 'debug'
2-
import httpProxy from 'http-proxy'
3-
import { makeServeConfig } from './runner-ct'
42
import { Request, Response, Router } from 'express'
3+
import type { InitializeRoutes } from './routes'
54
import send from 'send'
65
import { getPathToDist } from '@packages/resolve-dist'
7-
import type { InitializeRoutes } from './routes'
8-
import { fs } from './util/fs'
9-
import type { DataContextShell } from '@packages/data-context/src/DataContextShell'
106

117
const debug = Debug('cypress:server:routes-ct')
128

@@ -17,114 +13,18 @@ const serveChunk = (req: Request, res: Response, clientRoute: string) => {
1713
}
1814

1915
export const createRoutesCT = ({
20-
ctx,
2116
config,
2217
nodeProxy,
23-
getCurrentBrowser,
24-
specsStore,
2518
}: InitializeRoutes) => {
2619
const routesCt = Router()
2720

28-
if (process.env.CYPRESS_INTERNAL_VITE_APP_PORT) {
29-
const proxy = httpProxy.createProxyServer({
30-
target: `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`,
31-
})
32-
const proxyIndex = httpProxy.createProxyServer({
33-
target: `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`,
34-
selfHandleResponse: true,
35-
})
36-
37-
proxyIndex.on('proxyRes', function (proxyRes, _req, _res) {
38-
const body: any[] = []
39-
40-
proxyRes.on('data', function (chunk) {
41-
let chunkData = String(chunk)
42-
43-
if (chunkData.includes('<head>')) {
44-
chunkData = chunkData.replace('<body>', replaceBody(ctx))
45-
}
46-
47-
body.push(chunkData)
48-
})
49-
50-
proxyRes.on('end', function () {
51-
(_res as Response).send(body.join(''))
52-
})
53-
})
54-
55-
// TODO: can namespace this onto a "unified" route like __app-unified__
56-
// make sure to update the generated routes inside of vite.config.ts
57-
routesCt.get('/__vite__/*', (req, res) => {
58-
if (req.params[0] === '') {
59-
proxyIndex.web(req, res, {}, (e) => {})
60-
} else {
61-
proxy.web(req, res, {}, (e) => {})
62-
}
63-
})
64-
} else {
65-
routesCt.get('/__vite__/*', (req, res) => {
66-
const pathToFile = getPathToDist('app', req.params[0])
67-
68-
if (req.params[0] === '') {
69-
return fs.readFile(pathToFile, 'utf8')
70-
.then((file) => {
71-
res.send(file.replace('<body>', replaceBody(ctx)))
72-
})
73-
}
74-
75-
return send(req, pathToFile).pipe(res)
76-
})
77-
}
78-
79-
// TODO If prod, serve the build app files from app/dist
80-
81-
routesCt.get('/api', (req, res) => {
82-
const options = makeServeConfig({
83-
config,
84-
getCurrentBrowser,
85-
specsStore,
86-
})
87-
88-
res.json(options)
89-
})
90-
91-
routesCt.get('/__/api', (req, res) => {
92-
const options = makeServeConfig({
93-
config,
94-
getCurrentBrowser,
95-
specsStore,
96-
})
97-
98-
res.json(options)
99-
})
100-
10121
routesCt.get('/__cypress/static/*', (req, res) => {
10222
const pathToFile = getPathToDist('static', req.params[0])
10323

10424
return send(req, pathToFile)
10525
.pipe(res)
10626
})
10727

108-
routesCt.get('/__cypress/iframes/*', (req, res) => {
109-
// always proxy to the index.html file
110-
// attach header data for webservers
111-
// to properly intercept and serve assets from the correct src root
112-
// TODO: define a contract for dev-server plugins to configure this behavior
113-
req.headers.__cypress_spec_path = req.params[0]
114-
req.url = `${config.devServerPublicPathRoute}/index.html`
115-
116-
// user the node proxy here instead of the network proxy
117-
// to avoid the user accidentally intercepting and modifying
118-
// our internal index.html handler
119-
120-
nodeProxy.web(req, res, {}, (e) => {
121-
if (e) {
122-
// eslint-disable-next-line
123-
debug('Proxy request error. This is likely the socket hangup issue, we can basically ignore this because the stream will automatically continue once the asset will be available', e)
124-
}
125-
})
126-
})
127-
12828
// user app code + spec code
12929
// default mounted to /__cypress/src/*
13030
routesCt.get(`${config.devServerPublicPathRoute}*`, (req, res) => {
@@ -157,7 +57,3 @@ export const createRoutesCT = ({
15757

15858
return routesCt
15959
}
160-
161-
function replaceBody (ctx: DataContextShell) {
162-
return `<body><script>window.__CYPRESS_GRAPHQL_PORT__ = ${JSON.stringify(ctx.gqlServerPort)};</script>\n`
163-
}

0 commit comments

Comments
 (0)