mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-02 21:10:47 -05:00
feat: Set up cypress in cypress (#19602)
Co-authored-by: Brian Mann <brian.mann86@gmail.com>
This commit is contained in:
@@ -37,6 +37,7 @@ export const makeCypressPlugin = (
|
||||
supportFilePath: string | false,
|
||||
devServerEvents: NodeJS.EventEmitter,
|
||||
specs: Spec[],
|
||||
namespace: string,
|
||||
indexHtml?: string,
|
||||
): Plugin => {
|
||||
let base = '/'
|
||||
@@ -60,7 +61,7 @@ export const makeCypressPlugin = (
|
||||
return {
|
||||
define: {
|
||||
'import.meta.env.__cypress_supportPath': JSON.stringify(normalizedSupportFilePath),
|
||||
'import.meta.env.__cypress_originAutUrl': JSON.stringify(`__cypress/iframes/${convertPathToPosix(projectRoot)}/`),
|
||||
'import.meta.env.__cypress_originAutUrl': JSON.stringify(`${namespace}/iframes/${convertPathToPosix(projectRoot)}/`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,16 +26,16 @@ export interface StartDevServerOptions {
|
||||
}
|
||||
|
||||
const resolveServerConfig = async ({ viteConfig, options, indexHtml }: StartDevServerOptions): Promise<InlineConfig> => {
|
||||
const { projectRoot, supportFile } = options.config
|
||||
const { projectRoot, supportFile, namespace } = options.config
|
||||
|
||||
const requiredOptions: InlineConfig = {
|
||||
base: '/__cypress/src/',
|
||||
base: `/${namespace}/src/`,
|
||||
root: projectRoot,
|
||||
}
|
||||
|
||||
const finalConfig: InlineConfig = { ...viteConfig, ...requiredOptions }
|
||||
|
||||
finalConfig.plugins = [...(finalConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents, options.specs, indexHtml)]
|
||||
finalConfig.plugins = [...(finalConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents, options.specs, options.config.namespace, indexHtml)]
|
||||
|
||||
// This alias is necessary to avoid a "prefixIdentifiers" issue from slots mounting
|
||||
// only cjs compiler-core accepts using prefixIdentifiers in slots which vue test utils use.
|
||||
|
||||
@@ -6,6 +6,12 @@ const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.
|
||||
|
||||
export default defineConfig({
|
||||
projectId: CYPRESS_INTERNAL_CLOUD_ENV === 'staging' ? 'ypt4pf' : 'sehy69',
|
||||
// @ts-ignore We are setting these namespaces in order to properly test Cypress in Cypress
|
||||
clientRoute: '/__app/',
|
||||
namespace: '__cypress-app',
|
||||
socketIoRoute: '/__app-socket.io',
|
||||
socketIoCookie: '__app-socket.io',
|
||||
devServerPublicPathRoute: '/__cypress-app/src',
|
||||
viewportWidth: 800,
|
||||
viewportHeight: 850,
|
||||
retries: {
|
||||
@@ -38,6 +44,8 @@ export default defineConfig({
|
||||
pluginsFile: 'cypress/e2e/plugins/index.ts',
|
||||
supportFile: 'cypress/e2e/support/e2eSupport.ts',
|
||||
async setupNodeEvents (on, config) {
|
||||
// Delete this as we only want to honor it on parent Cypress when doing E2E Cypress in Cypress testing
|
||||
delete process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS
|
||||
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
|
||||
// process.env.DEBUG = '*'
|
||||
const { e2ePluginSetup } = require('@packages/frontend-shared/cypress/e2e/e2ePluginSetup')
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
describe('Cypress In Cypress', () => {
|
||||
it('test component', () => {
|
||||
cy.scaffoldProject('cypress-in-cypress')
|
||||
cy.openProject('cypress-in-cypress')
|
||||
cy.startAppServer('component')
|
||||
cy.visitApp()
|
||||
cy.contains('TestComponent.spec').click()
|
||||
cy.location().should((location) => {
|
||||
expect(location.hash).to.contain('TestComponent.spec')
|
||||
})
|
||||
|
||||
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
describe('Cypress In Cypress', () => {
|
||||
it('test e2e', () => {
|
||||
cy.scaffoldProject('cypress-in-cypress')
|
||||
cy.openProject('cypress-in-cypress')
|
||||
cy.startAppServer()
|
||||
cy.visitApp()
|
||||
cy.contains('dom-content.spec').click()
|
||||
cy.location().should((location) => {
|
||||
expect(location.hash).to.contain('dom-content.spec')
|
||||
})
|
||||
|
||||
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,35 @@ describe('App: Index', () => {
|
||||
cy.visitApp()
|
||||
})
|
||||
|
||||
// TODO(ryan m and tim): Skipping until https://github.com/cypress-io/cypress/pull/19619 is merged
|
||||
const tempSkip = new Date() > new Date('2022-01-21') ? context : context.skip
|
||||
|
||||
tempSkip('scaffold example specs', () => {
|
||||
const assertSpecs = (createdSpecs: FoundSpec[]) => cy.wrap(createdSpecs).each((spec: FoundSpec) => cy.contains(spec.baseName).scrollIntoView().should('be.visible'))
|
||||
|
||||
it('should generate example specs', () => {
|
||||
let createdSpecs: FoundSpec[]
|
||||
|
||||
cy.visitApp()
|
||||
|
||||
cy.intercept('POST', 'mutation-ScaffoldGeneratorStepOne_scaffoldIntegration').as('scaffoldIntegration')
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.header).click()
|
||||
cy.wait('@scaffoldIntegration').then((interception: Interception) => {
|
||||
createdSpecs = interception.response?.body.data.scaffoldIntegration.map((res) => res.file)
|
||||
|
||||
expect(createdSpecs).lengthOf.above(0)
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.specsAddedHeader).should('be.visible')
|
||||
assertSpecs(createdSpecs)
|
||||
})
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.specsAddedButton).click()
|
||||
|
||||
cy.visitApp().then(() => assertSpecs(createdSpecs))
|
||||
})
|
||||
})
|
||||
|
||||
context('with no specs', () => {
|
||||
it('shows "Create spec" title', () => {
|
||||
// TODO: we need more e2e tests around this, but it requires changes to how we set up config in our
|
||||
@@ -57,29 +86,4 @@ describe('App: Index', () => {
|
||||
cy.findByTestId('spec-item').should('contain', 'new-file')
|
||||
})
|
||||
})
|
||||
|
||||
context('scaffold example specs', () => {
|
||||
const assertSpecs = (createdSpecs: FoundSpec[]) => cy.wrap(createdSpecs).each((spec: FoundSpec) => cy.contains(spec.baseName).scrollIntoView().should('be.visible'))
|
||||
|
||||
it('should generate example specs', () => {
|
||||
let createdSpecs: FoundSpec[]
|
||||
|
||||
cy.visitApp()
|
||||
|
||||
cy.intercept('mutation-ScaffoldGeneratorStepOne_scaffoldIntegration').as('scaffoldIntegration')
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.header).click()
|
||||
cy.wait('@scaffoldIntegration').then((interception: Interception) => {
|
||||
createdSpecs = interception.response?.body.data.scaffoldIntegration.map((res) => res.file)
|
||||
|
||||
expect(createdSpecs).lengthOf.above(0)
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.specsAddedHeader).should('be.visible')
|
||||
assertSpecs(createdSpecs)
|
||||
})
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.specsAddedButton).click()
|
||||
|
||||
cy.visitApp().then(() => assertSpecs(createdSpecs))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
context('Runs - Unauthorized Project Requested', () => {
|
||||
beforeEach(() => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {\'projectId\': \'abcdef\'}')
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {\'projectId\': \'abcdef\' }')
|
||||
})
|
||||
|
||||
cy.loginUser()
|
||||
@@ -215,7 +215,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
context('Runs - No Runs', () => {
|
||||
it('when no runs and not connected, shows connect to dashboard button', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: null}')
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: null }')
|
||||
})
|
||||
|
||||
cy.loginUser()
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('App: Settings', () => {
|
||||
})
|
||||
|
||||
it('can reconfigure a project', () => {
|
||||
cy.visitApp('#settings')
|
||||
cy.visitApp('settings')
|
||||
|
||||
cy.intercept('mutation-SettingsContainer_ReconfigureProject', { 'data': { 'reconfigureProject': true } }).as('ReconfigureProject')
|
||||
cy.findByText('Reconfigure Project').click()
|
||||
@@ -49,7 +49,7 @@ describe('App: Settings', () => {
|
||||
ctx.coreData.localSettings.preferences.preferredEditorBinary = undefined
|
||||
})
|
||||
|
||||
cy.visitApp('#settings')
|
||||
cy.visitApp('settings')
|
||||
cy.contains('Device Settings').click()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<link href="/__cypress/assets/favicon.png" rel="icon">
|
||||
<title>Cypress</title>
|
||||
<link href="/__cypress/runner/favicon.ico" rel="icon">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
"clean": "rimraf dist && rimraf ./node_modules/.vite && echo 'cleaned'",
|
||||
"clean-deps": "rimraf node_modules",
|
||||
"test": "echo 'ok'",
|
||||
"cypress:launch": "cross-env TZ=America/New_York gulp open --project ${PWD}",
|
||||
"cypress:open": "cross-env TZ=America/New_York gulp open --project ${PWD}",
|
||||
"cypress:run:ct": "cross-env TZ=America/New_York node ../../scripts/cypress run --component --project ${PWD}",
|
||||
"cypress:run:e2e": "cross-env TZ=America/New_York node ../../scripts/cypress run --project ${PWD}",
|
||||
"cypress:run-cypress-in-cypress": "cross-env HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS=http://localhost:4455 CYPRESS_REMOTE_DEBUGGING_PORT=6666 TZ=America/New_York",
|
||||
"cypress:launch": "yarn cypress:run-cypress-in-cypress gulp open --project ${PWD}",
|
||||
"cypress:open": "yarn cypress:run-cypress-in-cypress gulp open --project ${PWD}",
|
||||
"cypress:run:ct": "yarn cypress:run-cypress-in-cypress node ../../scripts/cypress run --component --project ${PWD}",
|
||||
"cypress:run:e2e": "yarn cypress:run-cypress-in-cypress node ../../scripts/cypress run --project ${PWD}",
|
||||
"debug": "gulp debug --project ${PWD}",
|
||||
"dev": "gulp dev --project ${PWD}",
|
||||
"start": "echo \"run 'yarn dev' from the root\" && exit 1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'virtual:windi.css'
|
||||
import urql from '@urql/vue'
|
||||
import App from './App.vue'
|
||||
import { makeUrqlClient } from '@packages/frontend-shared/src/graphql/urqlClient'
|
||||
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
|
||||
import { createI18n } from '@cy/i18n'
|
||||
import { createRouter } from './router/router'
|
||||
import { createPinia } from './store'
|
||||
@@ -19,7 +20,9 @@ window.__vite__ = true
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const ws = createWebsocket()
|
||||
const config = JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config)) as Cypress.Config
|
||||
|
||||
const ws = createWebsocket(config.socketIoRoute)
|
||||
|
||||
window.ws = ws
|
||||
|
||||
@@ -27,7 +30,7 @@ app.use(Toast, {
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
})
|
||||
|
||||
app.use(urql, makeUrqlClient('app'))
|
||||
app.use(urql, makeUrqlClient({ target: 'app', socketIoRoute: config.socketIoRoute }))
|
||||
app.use(createRouter())
|
||||
app.use(createI18n())
|
||||
app.use(createPinia())
|
||||
|
||||
@@ -16,8 +16,6 @@ type $Cypress = any
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
const automationElementId = '__cypress-string' as const
|
||||
|
||||
const driverToReporterEvents = 'paused session:add'.split(' ')
|
||||
const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
|
||||
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
|
||||
@@ -58,7 +56,7 @@ export class EventManager {
|
||||
return Cypress
|
||||
}
|
||||
|
||||
addGlobalListeners (state: BaseStore, connectionInfo: { automationElement: typeof automationElementId, randomString: string }) {
|
||||
addGlobalListeners (state: BaseStore, connectionInfo: { element: string, string: string }) {
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
|
||||
@@ -24,18 +24,17 @@ import { IframeModel } from './iframe-model'
|
||||
import { AutIframe } from './aut-iframe'
|
||||
import { EventManager } from './event-manager'
|
||||
import { client } from '@packages/socket/lib/browser'
|
||||
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
|
||||
|
||||
let _eventManager: EventManager | undefined
|
||||
|
||||
export function createWebsocket () {
|
||||
const PORT_MATCH = /serverPort=(\d+)/.exec(window.location.search)
|
||||
|
||||
export function createWebsocket (socketIoRoute) {
|
||||
const socketConfig = {
|
||||
path: '/__socket.io',
|
||||
path: socketIoRoute,
|
||||
transports: ['websocket'],
|
||||
}
|
||||
|
||||
const ws = PORT_MATCH ? client(`http://localhost:${PORT_MATCH[1]}`, socketConfig) : client(socketConfig)
|
||||
const ws = client(socketConfig)
|
||||
|
||||
ws.on('connect', () => {
|
||||
ws.emit('runner:connected')
|
||||
@@ -116,12 +115,12 @@ function createIframeModel () {
|
||||
* for communication between driver, runner, reporter via event bus,
|
||||
* and server (via web socket).
|
||||
*/
|
||||
function setupRunner () {
|
||||
function setupRunner (namespace) {
|
||||
const mobxRunnerStore = getMobxRunnerStore()
|
||||
|
||||
getEventManager().addGlobalListeners(mobxRunnerStore, {
|
||||
automationElement: '__cypress-string',
|
||||
randomString,
|
||||
element: `${namespace}-string`,
|
||||
string: randomString,
|
||||
})
|
||||
|
||||
getEventManager().start(window.UnifiedRunner.config)
|
||||
@@ -285,16 +284,14 @@ function runSpecE2E (spec: BaseSpec) {
|
||||
async function initialize () {
|
||||
isTorndown = false
|
||||
|
||||
await injectBundle()
|
||||
const config = JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config))
|
||||
|
||||
await injectBundle(config.namespace)
|
||||
|
||||
if (isTorndown) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await window.fetch('/api')
|
||||
const data = await response.json()
|
||||
|
||||
const config = window.UnifiedRunner.decodeBase64(data.base64Config) as any
|
||||
const autStore = useAutStore()
|
||||
|
||||
// TODO(lachlan): use GraphQL to get the viewport dimensions
|
||||
@@ -316,7 +313,7 @@ async function initialize () {
|
||||
store.updateDimensions(config.viewportWidth, config.viewportHeight)
|
||||
})
|
||||
|
||||
window.UnifiedRunner.MobX.runInAction(() => setupRunner())
|
||||
window.UnifiedRunner.MobX.runInAction(() => setupRunner(config.namespace))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export async function injectBundle () {
|
||||
const src = '/__cypress/runner/cypress_runner.js'
|
||||
export async function injectBundle (namespace: string) {
|
||||
const src = `/${namespace}/runner/cypress_runner.js`
|
||||
|
||||
const alreadyInjected = document.querySelector(`script[src="${src}"]`)
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function injectBundle () {
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.href = '/__cypress/runner/cypress_runner.css'
|
||||
link.href = `/${namespace}/runner/cypress_runner.css`
|
||||
|
||||
document.head.appendChild(script)
|
||||
document.head.appendChild(link)
|
||||
|
||||
@@ -24,8 +24,6 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
|
||||
"e2e": {
|
||||
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"
|
||||
},
|
||||
"defaultCommandTimeout": 4000,
|
||||
"downloadsFolder": "cypress/downloads",
|
||||
"env": {},
|
||||
"execTimeout": 60000,
|
||||
"exit": true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CodeGenType, MutationSetProjectPreferencesArgs, NexusGenObjects, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, AddProject } from '@packages/types'
|
||||
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject } from '@packages/types'
|
||||
import execa from 'execa'
|
||||
import path from 'path'
|
||||
import assert from 'assert'
|
||||
@@ -31,6 +31,7 @@ export interface ProjectApiShape {
|
||||
clearProjectPreferences(projectTitle: string): Promise<unknown>
|
||||
clearAllProjectPreferences(): Promise<unknown>
|
||||
closeActiveProject(shouldCloseBrowser?: boolean): Promise<unknown>
|
||||
getConfig(): ReceivedCypressOptions | undefined
|
||||
getCurrentProjectSavedState(): {} | undefined
|
||||
setPromptShown(slug: string): void
|
||||
getDevServer (): {
|
||||
|
||||
@@ -59,24 +59,33 @@ export class HtmlDataSource {
|
||||
throw err
|
||||
}
|
||||
|
||||
async makeServeConfig () {
|
||||
return {
|
||||
projectName: this.ctx.lifecycleManager.projectTitle,
|
||||
base64Config: Buffer.from(JSON.stringify(this.ctx._apis.projectApi.getConfig())).toString('base64'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The app html includes the SSR'ed data to bootstrap the page for the app
|
||||
*/
|
||||
async appHtml () {
|
||||
const [appHtml, appInitialData] = await Promise.all([
|
||||
const [appHtml, appInitialData, serveConfig] = await Promise.all([
|
||||
this.fetchAppHtml(),
|
||||
this.fetchAppInitialData(),
|
||||
this.makeServeConfig(),
|
||||
])
|
||||
|
||||
return this.replaceBody(appHtml, appInitialData)
|
||||
return this.replaceBody(appHtml, appInitialData, serveConfig)
|
||||
}
|
||||
|
||||
private replaceBody (html: string, initialData: object) {
|
||||
private replaceBody (html: string, initialData: object, serveConfig: object) {
|
||||
return html.replace('<body>', `
|
||||
<body>
|
||||
<script>
|
||||
window.__CYPRESS_GRAPHQL_PORT__ = ${JSON.stringify(this.ctx.gqlServerPort)};
|
||||
window.__CYPRESS_INITIAL_DATA__ = ${JSON.stringify(initialData)};
|
||||
window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)}
|
||||
</script>
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const setPostMessageLocalStorage = async (specWindow, originOptions) => {
|
||||
if (!origins.length) return []
|
||||
|
||||
_.each(origins, (u) => {
|
||||
const $iframe = $(`<iframe src="${`${u}/__cypress/automation/setLocalStorage?${u}`}"></iframe>`)
|
||||
const $iframe = $(`<iframe src="${`${u}/${Cypress.config('namespace')}/automation/setLocalStorage?${u}`}"></iframe>`)
|
||||
|
||||
$iframe.appendTo($iframeContainer)
|
||||
iframes.push($iframe)
|
||||
|
||||
@@ -232,7 +232,7 @@ async function makeE2ETasks () {
|
||||
throw new Error(`${projectName} has not been scaffolded. Be sure to call cy.scaffoldProject('${projectName}') in the test, a before, or beforeEach hook`)
|
||||
}
|
||||
|
||||
const openArgv = [...argv, '--project', Fixtures.projectPath(projectName)]
|
||||
const openArgv = [...argv, '--project', Fixtures.projectPath(projectName), '--port', '4455']
|
||||
|
||||
// Runs the launchArgs through the whole pipeline for the CLI open process,
|
||||
// which probably needs a bit of refactoring / consolidating
|
||||
|
||||
@@ -13,6 +13,7 @@ export const e2eProjectDirs = [
|
||||
'config-with-short-timeout',
|
||||
'config-with-ts',
|
||||
'cookies',
|
||||
'cypress-in-cypress',
|
||||
'default-layout',
|
||||
'downloads',
|
||||
'e2e',
|
||||
|
||||
@@ -195,59 +195,61 @@ function openProject (projectName: ProjectFixture, argv: string[] = []) {
|
||||
|
||||
function startAppServer (mode: 'component' | 'e2e' = 'e2e') {
|
||||
return logInternal('startAppServer', (log) => {
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
ctx.actions.project.setCurrentTestingType(o.mode)
|
||||
const isInitialized = o.pDefer()
|
||||
const initializeActive = ctx.actions.project.initializeActiveProject
|
||||
const onErrorStub = o.sinon.stub(ctx, 'onError')
|
||||
// @ts-expect-error - errors b/c it's a private method
|
||||
const onLoadErrorStub = o.sinon.stub(ctx.lifecycleManager, 'onLoadError')
|
||||
const initializeActiveProjectStub = o.sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
ctx.actions.project.setCurrentTestingType(o.mode)
|
||||
const isInitialized = o.pDefer()
|
||||
const initializeActive = ctx.actions.project.initializeActiveProject
|
||||
const onErrorStub = o.sinon.stub(ctx, 'onError')
|
||||
// @ts-expect-error - errors b/c it's a private method
|
||||
const onLoadErrorStub = o.sinon.stub(ctx.lifecycleManager, 'onLoadError')
|
||||
const initializeActiveProjectStub = o.sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
|
||||
function restoreStubs () {
|
||||
onErrorStub.restore()
|
||||
onLoadErrorStub.restore()
|
||||
initializeActiveProjectStub.restore()
|
||||
}
|
||||
function restoreStubs () {
|
||||
onErrorStub.restore()
|
||||
onLoadErrorStub.restore()
|
||||
initializeActiveProjectStub.restore()
|
||||
}
|
||||
|
||||
function onStartAppError (e: Error) {
|
||||
isInitialized.reject(e)
|
||||
restoreStubs()
|
||||
}
|
||||
|
||||
onErrorStub.callsFake(onStartAppError)
|
||||
onLoadErrorStub.callsFake(onStartAppError)
|
||||
|
||||
initializeActiveProjectStub.callsFake(async function (this: any, ...args) {
|
||||
try {
|
||||
const result = await initializeActive.apply(this, args)
|
||||
|
||||
isInitialized.resolve(result)
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
function onStartAppError (e: Error) {
|
||||
isInitialized.reject(e)
|
||||
} finally {
|
||||
restoreStubs()
|
||||
}
|
||||
|
||||
return
|
||||
onErrorStub.callsFake(onStartAppError)
|
||||
onLoadErrorStub.callsFake(onStartAppError)
|
||||
|
||||
initializeActiveProjectStub.callsFake(async function (this: any, ...args) {
|
||||
try {
|
||||
const result = await initializeActive.apply(this, args)
|
||||
|
||||
isInitialized.resolve(result)
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
isInitialized.reject(e)
|
||||
} finally {
|
||||
restoreStubs()
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
await isInitialized.promise
|
||||
|
||||
await ctx.actions.project.launchProject(o.mode, { url: o.url })
|
||||
|
||||
return ctx.appServerPort
|
||||
}, { log: false, mode, url: win.top ? win.top.location.href : undefined }).then((serverPort) => {
|
||||
log.set({ message: `port: ${serverPort}` })
|
||||
Cypress.env('e2e_serverPort', serverPort)
|
||||
})
|
||||
|
||||
await isInitialized.promise
|
||||
|
||||
await ctx.actions.project.launchProject(o.mode, {})
|
||||
|
||||
return ctx.appServerPort
|
||||
}, { log: false, mode }).then((serverPort) => {
|
||||
log.set({ message: `port: ${serverPort}` })
|
||||
Cypress.env('e2e_serverPort', serverPort)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function visitApp (href?: string) {
|
||||
const { e2e_serverPort, e2e_gqlPort } = Cypress.env()
|
||||
const { e2e_serverPort } = Cypress.env()
|
||||
|
||||
if (!e2e_serverPort) {
|
||||
throw new Error(`
|
||||
@@ -258,15 +260,11 @@ function visitApp (href?: string) {
|
||||
}
|
||||
|
||||
return cy.withCtx(async (ctx) => {
|
||||
return JSON.stringify(await ctx.html.fetchAppInitialData())
|
||||
}, { log: false }).then((ssrData) => {
|
||||
return cy.visit(`dist-app/index.html?serverPort=${e2e_serverPort}${href || ''}`, {
|
||||
onBeforeLoad (win) {
|
||||
// Simulates the inject SSR data when we're loading the page normally in the app
|
||||
win.__CYPRESS_INITIAL_DATA__ = JSON.parse(ssrData)
|
||||
win.__CYPRESS_GRAPHQL_PORT__ = e2e_gqlPort
|
||||
},
|
||||
})
|
||||
const config = await ctx.lifecycleManager.getFullInitialConfig()
|
||||
|
||||
return config.clientRoute
|
||||
}).then((clientRoute) => {
|
||||
return cy.visit(`http://localhost:${e2e_serverPort}${clientRoute || '/__/'}#${href || ''}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ declare global {
|
||||
interface Window {
|
||||
__CYPRESS_INITIAL_DATA__: SSRData
|
||||
__CYPRESS_GRAPHQL_PORT__?: string
|
||||
__CYPRESS_CONFIG__: {
|
||||
base64Config: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ function gqlPort () {
|
||||
|
||||
export async function preloadLaunchpadData () {
|
||||
try {
|
||||
const resp = await fetch(`http://localhost:${gqlPort()}/__cypress/launchpad-preload`)
|
||||
const resp = await fetch(`http://localhost:${gqlPort()}/__launchpad/preload`)
|
||||
|
||||
window.__CYPRESS_INITIAL_DATA__ = await resp.json()
|
||||
} catch {
|
||||
@@ -57,7 +60,18 @@ export async function preloadLaunchpadData () {
|
||||
}
|
||||
}
|
||||
|
||||
export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
|
||||
interface LaunchpadUrqlClientConfig {
|
||||
target: 'launchpad'
|
||||
}
|
||||
|
||||
interface AppUrqlClientConfig {
|
||||
target: 'app'
|
||||
socketIoRoute: string
|
||||
}
|
||||
|
||||
export type UrqlClientConfig = LaunchpadUrqlClientConfig | AppUrqlClientConfig
|
||||
|
||||
export function makeUrqlClient (config: UrqlClientConfig): Client {
|
||||
const port = gqlPort()
|
||||
|
||||
const GRAPHQL_URL = `http://localhost:${port}/graphql`
|
||||
@@ -65,7 +79,7 @@ export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
|
||||
// If we're in the launchpad, we connect to the known GraphQL Socket port,
|
||||
// otherwise we connect to the /__socket.io of the current domain, unless we've explicitly
|
||||
//
|
||||
const io = getPubSubSource({ target, gqlPort: port, serverPort: SERVER_PORT_MATCH?.[1] })
|
||||
const io = getPubSubSource({ gqlPort: port, serverPort: SERVER_PORT_MATCH?.[1], ...config })
|
||||
|
||||
let hasError = false
|
||||
|
||||
@@ -120,12 +134,21 @@ export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
|
||||
})
|
||||
}
|
||||
|
||||
interface PubSubConfig {
|
||||
target: 'launchpad' | 'app'
|
||||
interface LaunchpadPubSubConfig {
|
||||
target: 'launchpad'
|
||||
gqlPort: string
|
||||
serverPort?: string
|
||||
}
|
||||
|
||||
interface AppPubSubConfig {
|
||||
target: 'app'
|
||||
gqlPort: string
|
||||
serverPort?: string
|
||||
socketIoRoute: string
|
||||
}
|
||||
|
||||
type PubSubConfig = LaunchpadPubSubConfig | AppPubSubConfig
|
||||
|
||||
function getPubSubSource (config: PubSubConfig) {
|
||||
if (config.target === 'launchpad') {
|
||||
return client(`http://localhost:${config.gqlPort}`, {
|
||||
@@ -137,13 +160,13 @@ function getPubSubSource (config: PubSubConfig) {
|
||||
// Only happens during testing
|
||||
if (config.serverPort) {
|
||||
return client(`http://localhost:${config.serverPort}`, {
|
||||
path: '/__socket.io',
|
||||
path: config.socketIoRoute,
|
||||
transports: ['websocket'],
|
||||
})
|
||||
}
|
||||
|
||||
return client({
|
||||
path: '/__socket.io',
|
||||
path: config.socketIoRoute,
|
||||
transports: ['websocket'],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Correctly decodes Unicode string in encoded in base64
|
||||
* Copied from driver/src/cypress/utils.ts
|
||||
*
|
||||
* @see https://github.com/cypress-io/cypress/issues/5435
|
||||
* @see https://github.com/cypress-io/cypress/issues/7507
|
||||
* @see https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
|
||||
*
|
||||
* @example
|
||||
```
|
||||
Buffer.from(JSON.stringify({state: '🙂'})).toString('base64')
|
||||
// 'eyJzdGF0ZSI6IvCfmYIifQ=='
|
||||
// "window.atob" does NOT work
|
||||
// atob('eyJzdGF0ZSI6IvCfmYIifQ==')
|
||||
// "{"state":"ð"}"
|
||||
// but this function works
|
||||
b64DecodeUnicode('eyJzdGF0ZSI6IvCfmYIifQ==')
|
||||
'{"state":"🙂"}'
|
||||
```
|
||||
*/
|
||||
|
||||
export function decodeBase64Unicode (str: string) {
|
||||
return decodeURIComponent(atob(str).split('').map((char) => {
|
||||
return `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
|
||||
}).join(''))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from driver/src/cypress/utils.ts
|
||||
*
|
||||
* Correctly encodes Unicode string to base64
|
||||
* @see https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
|
||||
*/
|
||||
export function encodeBase64Unicode (str: string) {
|
||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
||||
// @ts-ignore
|
||||
return String.fromCharCode(`0x${p1}`)
|
||||
}))
|
||||
}
|
||||
@@ -27,11 +27,7 @@ export async function makeGraphQLServer () {
|
||||
|
||||
app.use(cors())
|
||||
|
||||
app.get('/__cypress/shiki-themes', (req, res) => {
|
||||
res.json([{}, {}])
|
||||
})
|
||||
|
||||
app.get('/__cypress/launchpad-preload', (req, res) => {
|
||||
app.get('/__launchpad/preload', (req, res) => {
|
||||
const ctx = getCtx()
|
||||
|
||||
ctx.html.fetchLaunchpadInitialData().then((data) => {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
//
|
||||
|
||||
import { graphqlHTTP, GraphQLParams } from 'express-graphql'
|
||||
import { graphqlSchema } from './schema'
|
||||
import type { DataContext } from '@packages/data-context'
|
||||
import type express from 'express'
|
||||
import { parse } from 'graphql'
|
||||
|
||||
const SHOW_GRAPHIQL = process.env.CYPRESS_INTERNAL_ENV !== 'production'
|
||||
|
||||
export function addGraphQLHTTP (app: ReturnType<typeof express>, context: DataContext) {
|
||||
app.use('/graphql/:operationName?', graphqlHTTP((req, res, params) => {
|
||||
const ctx = SHOW_GRAPHIQL ? maybeProxyContext(params, context) : context
|
||||
|
||||
return {
|
||||
schema: graphqlSchema,
|
||||
graphiql: SHOW_GRAPHIQL,
|
||||
context: ctx,
|
||||
}
|
||||
}))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds runtime validations during development to ensure patterns of access are enforced
|
||||
* on the DataContext
|
||||
*/
|
||||
function maybeProxyContext (params: GraphQLParams | undefined, context: DataContext): DataContext {
|
||||
if (params?.query) {
|
||||
const parsed = parse(params.query)
|
||||
const def = parsed.definitions[0]
|
||||
|
||||
if (def?.kind === 'OperationDefinition') {
|
||||
return def.operation === 'query' ? proxyContext(context, def.name?.value ?? '(anonymous)') : context
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function proxyContext (ctx: DataContext, operationName: string) {
|
||||
return new Proxy(ctx, {
|
||||
get (target, p, receiver) {
|
||||
// Allows us to get the context value, deref'ed so it's not guarded
|
||||
if (p === 'deref') {
|
||||
return Reflect.get(ctx, 'deref', ctx)
|
||||
}
|
||||
|
||||
if (p === 'actions' || p === 'emitter') {
|
||||
throw new Error(
|
||||
`Cannot access ctx.${p} within a query, only within mutations / outside of a GraphQL request\n` +
|
||||
`Seen in operation: ${operationName}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Reflect.get(target, p, receiver)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Promise.all([
|
||||
initHighlighter(),
|
||||
preloadLaunchpadData(),
|
||||
]).then(() => {
|
||||
launchpadClient = makeUrqlClient('launchpad')
|
||||
launchpadClient = makeUrqlClient({ target: 'launchpad' })
|
||||
app.use(urql, launchpadClient)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -5,7 +5,7 @@ describe('<ManualInstall />', () => {
|
||||
it('playground', { viewportWidth: 800, viewportHeight: 600 }, () => {
|
||||
cy.mountFragment(ManualInstallFragmentDoc, {
|
||||
render: (gqlVal) => (
|
||||
<div class="m-10 border-1 rounded border-gray-400">
|
||||
<div class="rounded border-1 border-gray-400 m-10">
|
||||
<ManualInstall gql={gqlVal} />
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -203,6 +203,20 @@ export class CombinedAgent {
|
||||
}
|
||||
}
|
||||
|
||||
const getProxyOrTargetOverrideForUrl = (href) => {
|
||||
// HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS is used for Cypress in Cypress E2E testing and will
|
||||
// force the parent Cypress server to treat the child Cypress server like a proxy without
|
||||
// having HTTP_PROXY set and will force traffic ONLY bound to that origin to behave
|
||||
// like a proxy
|
||||
const targetHost = process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS
|
||||
|
||||
if (targetHost && href.includes(targetHost)) {
|
||||
return targetHost
|
||||
}
|
||||
|
||||
return getProxyForUrl(href)
|
||||
}
|
||||
|
||||
class HttpAgent extends http.Agent {
|
||||
httpsAgent: https.Agent
|
||||
|
||||
@@ -214,8 +228,8 @@ class HttpAgent extends http.Agent {
|
||||
}
|
||||
|
||||
addRequest (req: http.ClientRequest, options: http.RequestOptions) {
|
||||
if (process.env.HTTP_PROXY) {
|
||||
const proxy = getProxyForUrl(options.href)
|
||||
if (process.env.HTTP_PROXY || process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS) {
|
||||
const proxy = getProxyOrTargetOverrideForUrl(options.href)
|
||||
|
||||
if (proxy) {
|
||||
options.proxy = proxy
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('lib/agent', function () {
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
process.env.NO_PROXY = process.env.HTTP_PROXY = process.env.HTTPS_PROXY = ''
|
||||
process.env.NO_PROXY = process.env.HTTP_PROXY = process.env.HTTPS_PROXY = process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS = ''
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
@@ -413,6 +413,20 @@ describe('lib/agent', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('HTTP pages can be loaded with the Upstream target URL', function (done) {
|
||||
process.env.HTTP_PROXY = process.env.HTTPS_PROXY = ''
|
||||
process.env.NO_PROXY = ''
|
||||
process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS = `http://localhost:${HTTP_PORT}`
|
||||
|
||||
this.request({
|
||||
url: `http://localhost:${HTTP_PORT}/get`,
|
||||
}).on('response', (response) => {
|
||||
expect(response.req.path).to.equal('http://localhost:31080/get')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class Runnable extends Component<RunnableProps> {
|
||||
'runnable-retried': model.hasRetried,
|
||||
'runnable-studio': appState.studioActive,
|
||||
})}
|
||||
data-model-state={model.state}
|
||||
>
|
||||
{model.type === 'test' ? <Test model={model as TestModel} /> : <Suite model={model as SuiteModel} />}
|
||||
</li>
|
||||
|
||||
@@ -32,7 +32,9 @@ const Runner: any = {
|
||||
},
|
||||
|
||||
start (el, base64Config) {
|
||||
const ws = createWebsocket()
|
||||
const config = JSON.parse(driverUtils.decodeBase64Unicode(base64Config))
|
||||
|
||||
const ws = createWebsocket(config.socketIoRoute)
|
||||
|
||||
// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set
|
||||
window.runnerWs = ws
|
||||
@@ -47,8 +49,6 @@ const Runner: any = {
|
||||
)
|
||||
|
||||
MobX.action('started', () => {
|
||||
const config = JSON.parse(driverUtils.decodeBase64Unicode(base64Config))
|
||||
|
||||
const NO_COMMAND_LOG = config.env && config.env.NO_COMMAND_LOG
|
||||
const configState = config.state || {}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{projectName}}</title>
|
||||
|
||||
<link href="/__cypress/runner/favicon.ico" rel="icon">
|
||||
<link href="/{{namespace}}/runner/favicon.ico" rel="icon">
|
||||
|
||||
<link rel="stylesheet" href="/__cypress/runner/cypress_runner.css">
|
||||
<link rel="stylesheet" href="/{{namespace}}/runner/cypress_runner.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="/__cypress/runner/cypress_runner.js"></script>
|
||||
<script type="text/javascript" src="/{{namespace}}/runner/cypress_runner.js"></script>
|
||||
<script type="text/javascript">
|
||||
// set a global so we know the 'top' window
|
||||
window.__Cypress__ = true
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
export const automationElementId = '__cypress-string' as const
|
||||
|
||||
interface AutomationElementProps {
|
||||
namespace: string
|
||||
randomString: string
|
||||
}
|
||||
|
||||
export const AutomationElement: React.FC<AutomationElementProps> = ({
|
||||
namespace,
|
||||
randomString,
|
||||
}) => {
|
||||
return (
|
||||
<div id={automationElementId} style={{ display: 'none' }}>
|
||||
<div id={`${namespace}-string`} style={{ display: 'none' }}>
|
||||
{randomString}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -6,15 +6,17 @@ import App from '@packages/runner/src/app/app'
|
||||
import { AutomationDisconnected } from '../automation-disconnected'
|
||||
import { automation } from '../automation'
|
||||
import { NoAutomation } from '../no-automation'
|
||||
import { automationElementId } from '../automation-element'
|
||||
import NoSpec from '@packages/runner/src/errors/no-spec'
|
||||
|
||||
import { Container } from '.'
|
||||
|
||||
const automationElementId = '__cypress-string'
|
||||
|
||||
const createProps = () => ({
|
||||
runner: 'e2e',
|
||||
hasSpecFile: sinon.stub(),
|
||||
config: {
|
||||
namespace: '__cypress',
|
||||
browsers: [],
|
||||
integrationFolder: '',
|
||||
numTestsKeptInMemory: 1,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react'
|
||||
import { AutomationDisconnected } from '../automation-disconnected'
|
||||
import { automation } from '../automation'
|
||||
import { NoAutomation } from '../no-automation'
|
||||
import { automationElementId, AutomationElement } from '../automation-element'
|
||||
import { AutomationElement } from '../automation-element'
|
||||
|
||||
@observer
|
||||
export class Container extends Component {
|
||||
@@ -18,7 +18,7 @@ export class Container extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.eventManager.addGlobalListeners(this.props.state, {
|
||||
element: automationElementId,
|
||||
element: `${this.props.config.namespace}-string`,
|
||||
string: this.randomString,
|
||||
})
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class Container extends Component {
|
||||
|
||||
_automationElement () {
|
||||
return (
|
||||
<AutomationElement randomString={this.randomString} />
|
||||
<AutomationElement namespace={this.props.config.namespace} randomString={this.randomString} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ function createCypress (defaultOptions = {}) {
|
||||
|
||||
return cy.visit('/fixtures/isolated-runner.html#/tests/cypress/fixtures/empty_spec.js')
|
||||
.then({ timeout: 60000 }, (win) => {
|
||||
win.Runner._initialize('/__socket.io')
|
||||
win.runnerWs.destroy()
|
||||
|
||||
allStubs = cy.stub().snapshot(enableStubSnapshots).log(false)
|
||||
|
||||
@@ -12,37 +12,46 @@ import { createWebsocket } from '@packages/app/src/runner'
|
||||
import util from './lib/util'
|
||||
import { UnifiedRunner } from '@packages/runner-ct/unified-runner'
|
||||
|
||||
const driverUtils = $Cypress.utils
|
||||
|
||||
window.UnifiedRunner = UnifiedRunner
|
||||
|
||||
MobX.configure({ enforceActions: 'always' })
|
||||
|
||||
const ws = createWebsocket()
|
||||
|
||||
// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set
|
||||
window.runnerWs = ws
|
||||
window.ws = ws
|
||||
|
||||
const eventManager = new EventManager(
|
||||
$Cypress,
|
||||
MobX,
|
||||
selectorPlaygroundModel,
|
||||
StudioRecorder,
|
||||
ws,
|
||||
)
|
||||
|
||||
// NOTE: this is for testing Cypress-in-Cypress, window.Cypress is undefined here
|
||||
// unless Cypress has been loaded into the AUT frame
|
||||
if (window.Cypress) {
|
||||
window.eventManager = eventManager
|
||||
}
|
||||
let ws
|
||||
let eventManager
|
||||
|
||||
const Runner = {
|
||||
start (el, base64Config) {
|
||||
MobX.action('started', () => {
|
||||
const config = JSON.parse(driverUtils.decodeBase64Unicode(base64Config))
|
||||
_initialize (socketIoRoute) {
|
||||
if (!ws) {
|
||||
ws = createWebsocket(socketIoRoute)
|
||||
}
|
||||
|
||||
// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set
|
||||
window.runnerWs = ws
|
||||
window.ws = ws
|
||||
|
||||
if (!eventManager) {
|
||||
eventManager = new EventManager(
|
||||
$Cypress,
|
||||
MobX,
|
||||
selectorPlaygroundModel,
|
||||
StudioRecorder,
|
||||
ws,
|
||||
)
|
||||
}
|
||||
|
||||
// NOTE: this is for testing Cypress-in-Cypress, window.Cypress is undefined here
|
||||
// unless Cypress has been loaded into the AUT frame
|
||||
if (window.Cypress) {
|
||||
window.eventManager = eventManager
|
||||
}
|
||||
},
|
||||
|
||||
start (el, base64Config) {
|
||||
const config = UnifiedRunner.decodeBase64(base64Config)
|
||||
|
||||
this._initialize(config.socketIoRoute)
|
||||
|
||||
MobX.action('started', () => {
|
||||
const NO_COMMAND_LOG = config.env && config.env.NO_COMMAND_LOG
|
||||
const useInlineSpecList = false
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{projectName}}</title>
|
||||
|
||||
<link href="/__cypress/runner/favicon.ico" rel="icon">
|
||||
<link href="/{{namespace}}/runner/favicon.ico" rel="icon">
|
||||
|
||||
<link rel="stylesheet" href="/__cypress/runner/cypress_runner.css">
|
||||
<link rel="stylesheet" href="/{{namespace}}/runner/cypress_runner.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="/__cypress/runner/cypress_runner.js"></script>
|
||||
<script type="text/javascript" src="/{{namespace}}/runner/cypress_runner.js"></script>
|
||||
<script type="text/javascript">
|
||||
// set a global so we know the 'top' window
|
||||
window.__Cypress__ = true
|
||||
|
||||
@@ -247,13 +247,13 @@ const _disableRestorePagesPrompt = function (userDir) {
|
||||
|
||||
// After the browser has been opened, we can connect to
|
||||
// its remote interface via a websocket.
|
||||
const _connectToChromeRemoteInterface = function (port, onError, browserDisplayName) {
|
||||
const _connectToChromeRemoteInterface = function (port, onError, browserDisplayName, url?) {
|
||||
// @ts-ignore
|
||||
la(check.userPort(port), 'expected port number to connect CRI to', port)
|
||||
|
||||
debug('connecting to Chrome remote interface at random port %d', port)
|
||||
|
||||
return protocol.getWsTargetFor(port, browserDisplayName)
|
||||
return protocol.getWsTargetFor(port, browserDisplayName, url)
|
||||
.then((wsUrl) => {
|
||||
debug('received wsUrl %s for port %d', wsUrl, port)
|
||||
|
||||
@@ -443,6 +443,13 @@ export = {
|
||||
return args
|
||||
},
|
||||
|
||||
async connectToExisting (browser: Browser, options: CypressConfiguration = {}, automation) {
|
||||
const port = await protocol.getRemoteDebuggingPort()
|
||||
const criClient = await this._connectToChromeRemoteInterface(port, options, browser.displayName, options.url)
|
||||
|
||||
this._setAutomation(criClient, automation)
|
||||
},
|
||||
|
||||
async open (browser: Browser, url, options: CypressConfiguration = {}, automation) {
|
||||
const { isTextTerminal } = options
|
||||
|
||||
|
||||
@@ -380,6 +380,10 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
async connectToExisting () {
|
||||
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron')
|
||||
},
|
||||
|
||||
open (browser, url, options = {}, automation) {
|
||||
const { projectRoot, isTextTerminal } = options
|
||||
|
||||
|
||||
@@ -357,6 +357,10 @@ export function _createDetachedInstance (browserInstance: BrowserInstance): Brow
|
||||
return detachedInstance
|
||||
}
|
||||
|
||||
export function connectToExisting () {
|
||||
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for browser')
|
||||
}
|
||||
|
||||
export async function open (browser: Browser, url, options: any = {}, automation): Promise<BrowserInstance> {
|
||||
// see revision comment here https://wiki.mozilla.org/index.php?title=WebDriver/RemoteProtocol&oldid=1234946
|
||||
const hasCdp = browser.majorVersion >= 86
|
||||
|
||||
@@ -92,21 +92,33 @@ module.exports = {
|
||||
return utils.getBrowsers()
|
||||
},
|
||||
|
||||
connectToExisting (browser, options = {}, automation) {
|
||||
const browserLauncher = getBrowserLauncher(browser)
|
||||
|
||||
if (!browserLauncher) {
|
||||
utils.throwBrowserNotFound(browser.name, options.browsers)
|
||||
}
|
||||
|
||||
return browserLauncher.connectToExisting(browser, options, automation)
|
||||
},
|
||||
|
||||
open (browser, options = {}, automation) {
|
||||
return kill(true)
|
||||
.then(() => {
|
||||
let browserLauncher; let url
|
||||
|
||||
_.defaults(options, {
|
||||
onBrowserOpen () {},
|
||||
onBrowserClose () {},
|
||||
})
|
||||
|
||||
if (!(browserLauncher = getBrowserLauncher(browser))) {
|
||||
const browserLauncher = getBrowserLauncher(browser)
|
||||
|
||||
if (!browserLauncher) {
|
||||
utils.throwBrowserNotFound(browser.name, options.browsers)
|
||||
}
|
||||
|
||||
if (!(url = options.url)) {
|
||||
const { url } = options
|
||||
|
||||
if (!url) {
|
||||
throw new Error('options.url must be provided when opening a browser. You passed:', options)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,15 @@ export function _connectAsync (opts) {
|
||||
*
|
||||
* @returns {string} web socket debugger url
|
||||
*/
|
||||
const findStartPage = (targets) => {
|
||||
debug('CRI List %o', { numTargets: targets.length, targets })
|
||||
const findStartPage = (targets, url = 'about:blank') => {
|
||||
debug('CRI List %o', { numTargets: targets.length, targets, url })
|
||||
// activate the first available id
|
||||
// find the first target page that's a real tab
|
||||
// and not the dev tools or background page.
|
||||
// since we open a blank page first, it has a special url
|
||||
const newTabTargetFields = {
|
||||
type: 'page',
|
||||
url: 'about:blank',
|
||||
url,
|
||||
}
|
||||
|
||||
const target = _.find(targets, newTabTargetFields)
|
||||
@@ -65,17 +65,17 @@ const findStartPage = (targets) => {
|
||||
return target.webSocketDebuggerUrl
|
||||
}
|
||||
|
||||
const findStartPageTarget = (connectOpts) => {
|
||||
const findStartPageTarget = (connectOpts, url) => {
|
||||
debug('CRI.List %o', connectOpts)
|
||||
|
||||
// what happens if the next call throws an error?
|
||||
// it seems to leave the browser instance open
|
||||
// need to clone connectOpts, CRI modifies it
|
||||
return CRI.List(_.clone(connectOpts)).then(findStartPage)
|
||||
return CRI.List(_.clone(connectOpts)).then((targets) => findStartPage(targets, url))
|
||||
}
|
||||
|
||||
export async function getRemoteDebuggingPort () {
|
||||
const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT)
|
||||
const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) || utils.getPort()
|
||||
|
||||
return port || utils.getPort()
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export async function getRemoteDebuggingPort () {
|
||||
* @param {number} port Port number to connect to
|
||||
* @param {string} browserName Browser name, for warning/error messages
|
||||
*/
|
||||
export const getWsTargetFor = (port: number, browserName: string) => {
|
||||
export const getWsTargetFor = (port: number, browserName: string, url?: string | null) => {
|
||||
debug('Getting WS connection to CRI on port %d', port)
|
||||
la(is.port(port), 'expected port number', port)
|
||||
|
||||
@@ -108,7 +108,7 @@ export const getWsTargetFor = (port: number, browserName: string) => {
|
||||
const retry = () => {
|
||||
debug('attempting to find CRI target... %o', { retryIndex })
|
||||
|
||||
return findStartPageTarget(connectOpts)
|
||||
return findStartPageTarget(connectOpts, url)
|
||||
.catch((err) => {
|
||||
retryIndex++
|
||||
const delay = _getDelayMsForRetry(retryIndex, browserName)
|
||||
|
||||
@@ -7,8 +7,6 @@ const debug = require('debug')('cypress:server:controllers')
|
||||
const { escapeFilenameInUrl } = require('../util/escape_filename')
|
||||
const { getCtx } = require('@packages/data-context')
|
||||
|
||||
const SPEC_URL_PREFIX = '/__cypress/tests?p'
|
||||
|
||||
module.exports = {
|
||||
handleIframe (req, res, config, getRemoteState, extraOptions) {
|
||||
const test = req.params[0]
|
||||
@@ -50,7 +48,7 @@ module.exports = {
|
||||
|
||||
debug('converted %s to %s', spec, convertedSpec)
|
||||
|
||||
return this.prepareForBrowser(convertedSpec, config.projectRoot)
|
||||
return this.prepareForBrowser(convertedSpec, config.projectRoot, config.namespace)
|
||||
}
|
||||
|
||||
const specFilter = _.get(extraOptions, 'specFilter')
|
||||
@@ -108,7 +106,9 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
prepareForBrowser (filePath, projectRoot) {
|
||||
prepareForBrowser (filePath, projectRoot, namespace) {
|
||||
const SPEC_URL_PREFIX = `/${namespace}/tests?p`
|
||||
|
||||
filePath = filePath.replace(SPEC_URL_PREFIX, '__CYPRESS_SPEC_URL_PREFIX__')
|
||||
filePath = escapeFilenameInUrl(filePath).replace('__CYPRESS_SPEC_URL_PREFIX__', SPEC_URL_PREFIX)
|
||||
const relativeFilePath = path.relative(projectRoot, filePath)
|
||||
@@ -116,12 +116,12 @@ module.exports = {
|
||||
return {
|
||||
absolute: filePath,
|
||||
relative: relativeFilePath,
|
||||
relativeUrl: this.getTestUrl(relativeFilePath),
|
||||
relativeUrl: this.getTestUrl(relativeFilePath, namespace),
|
||||
}
|
||||
},
|
||||
|
||||
getTestUrl (file) {
|
||||
const url = `${SPEC_URL_PREFIX}=${file}`
|
||||
getTestUrl (file, namespace) {
|
||||
const url = `/${namespace}/tests?p=${file}`
|
||||
|
||||
debug('test url for file %o', { file, url })
|
||||
|
||||
@@ -137,7 +137,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
getSupportFile (config) {
|
||||
const { projectRoot, supportFile } = config
|
||||
const { projectRoot, supportFile, namespace } = config
|
||||
|
||||
let files = []
|
||||
|
||||
@@ -167,7 +167,7 @@ module.exports = {
|
||||
return glob(p, { nodir: true })
|
||||
}).then(_.flatten)
|
||||
.map((filePath) => {
|
||||
return this.prepareForBrowser(filePath, projectRoot)
|
||||
return this.prepareForBrowser(filePath, projectRoot, namespace)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const serveRunner = (runnerPkg: RunnerPkg, config: Cfg, res: Response) =>
|
||||
return res.render(runnerPath, {
|
||||
base64Config,
|
||||
projectName: config.projectName,
|
||||
namespace: config.namespace,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,9 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
closeActiveProject () {
|
||||
return openProject.closeActiveProject()
|
||||
},
|
||||
getConfig () {
|
||||
return openProject.getConfig()
|
||||
},
|
||||
getCurrentProjectSavedState () {
|
||||
// TODO: See if this is the best way we should be getting this config,
|
||||
// shouldn't we have this already in the DataContext?
|
||||
|
||||
@@ -106,7 +106,10 @@ export class OpenProject {
|
||||
// set the current browser object on options
|
||||
// so we can pass it down
|
||||
options.browser = browser
|
||||
options.url = url
|
||||
|
||||
if (!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
options.url = url
|
||||
}
|
||||
|
||||
this.projectBase.setCurrentSpecAndBrowser(spec, browser)
|
||||
|
||||
@@ -177,7 +180,7 @@ export class OpenProject {
|
||||
.then(() => {
|
||||
// TODO: Stub this so we can detect it being called
|
||||
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
return
|
||||
return browsers.connectToExisting(browser, options, automation)
|
||||
}
|
||||
|
||||
return browsers.open(browser, options, automation)
|
||||
|
||||
@@ -25,21 +25,14 @@ import { ensureProp } from './util/class-helpers'
|
||||
import { fs } from './util/fs'
|
||||
import preprocessor from './plugins/preprocessor'
|
||||
import { checkSupportFile } from './project_utils'
|
||||
|
||||
import type { FoundBrowser, OpenProjectLaunchOptions, FoundSpec, TestingType, ReceivedCypressOptions } from '@packages/types'
|
||||
import devServer from './plugins/dev-server'
|
||||
import type { FoundBrowser, OpenProjectLaunchOptions, FoundSpec } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
|
||||
// Cannot just use RuntimeConfigOptions as is because some types are not complete.
|
||||
// Instead, this is an interface of values that have been manually validated to exist
|
||||
// and are required when creating a project.
|
||||
type ReceivedCypressOptions =
|
||||
Pick<Cypress.RuntimeConfigOptions, 'hosts' | 'projectName' | 'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'report' | 'socketIoCookie' | 'configFile' | 'isTextTerminal' | 'isNewProject' | 'proxyUrl' | 'browsers' | 'browserUrl' | 'socketIoRoute' | 'arch' | 'platform' | 'spec' | 'specs' | 'browser' | 'version' | 'remote'>
|
||||
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'pluginsFile' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'testFiles' | 'ignoreSpecPattern' | 'specPattern'> // TODO: Figure out how to type this better.
|
||||
|
||||
export interface Cfg extends ReceivedCypressOptions {
|
||||
projectRoot: string
|
||||
proxyServer?: Cypress.RuntimeConfigOptions['proxyUrl']
|
||||
testingType: TestingType
|
||||
exit?: boolean
|
||||
state?: {
|
||||
firstOpened?: number | null
|
||||
@@ -502,7 +495,10 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
|
||||
async initializeConfig (): Promise<Cfg> {
|
||||
this.ctx.lifecycleManager.setCurrentTestingType(this.testingType)
|
||||
let theCfg: Cfg = await this.ctx.lifecycleManager.getFullInitialConfig() as Cfg // ?? types are definitely wrong here I think
|
||||
let theCfg: Cfg = {
|
||||
...(await this.ctx.lifecycleManager.getFullInitialConfig()),
|
||||
testingType: this.testingType,
|
||||
} as Cfg // ?? types are definitely wrong here I think
|
||||
|
||||
theCfg = this.testingType === 'e2e'
|
||||
? theCfg
|
||||
@@ -531,7 +527,13 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
|
||||
debug('project has config %o', this._cfg)
|
||||
|
||||
return this._cfg
|
||||
return {
|
||||
...this._cfg,
|
||||
remote: this._server?._getRemoteState() ?? {} as Cypress.RemoteState,
|
||||
browser: this.browser,
|
||||
testingType: this.ctx.coreData.currentTestingType ?? 'e2e',
|
||||
specs: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Saved state
|
||||
|
||||
@@ -18,7 +18,7 @@ export const createRoutesCT = ({
|
||||
}: InitializeRoutes) => {
|
||||
const routesCt = Router()
|
||||
|
||||
routesCt.get('/__cypress/static/*', (req, res) => {
|
||||
routesCt.get(`/${config.namespace}/static/*`, (req, res) => {
|
||||
const pathToFile = getPathToDist('static', req.params[0])
|
||||
|
||||
return send(req, pathToFile)
|
||||
|
||||
@@ -20,22 +20,22 @@ export const createRoutesE2E = ({
|
||||
|
||||
// routing for the actual specs which are processed automatically
|
||||
// this could be just a regular .js file or a .coffee file
|
||||
routesE2E.get('/__cypress/tests', (req, res, next) => {
|
||||
routesE2E.get(`/${config.namespace}/tests`, (req, res, next) => {
|
||||
// slice out the cache buster
|
||||
const test = decodeURIComponent(CacheBuster.strip(req.query.p))
|
||||
|
||||
specController.handle(test, req, res, config, next, onError)
|
||||
})
|
||||
|
||||
routesE2E.get('/__cypress/socket.io.js', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/socket.io.js`, (req, res) => {
|
||||
client.handle(req, res)
|
||||
})
|
||||
|
||||
routesE2E.get('/__cypress/reporter/*', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/reporter/*`, (req, res) => {
|
||||
reporter.handle(req, res)
|
||||
})
|
||||
|
||||
routesE2E.get('/__cypress/automation/getLocalStorage', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/automation/getLocalStorage`, (req, res) => {
|
||||
// gathers and sends localStorage and sessionStorage via postMessage to the Cypress frame
|
||||
// detect existence of local/session storage with JSON.stringify(...).length since localStorage.length may not be accurate
|
||||
res.send(`<html><body><script>(${(function () {
|
||||
@@ -62,7 +62,7 @@ export const createRoutesE2E = ({
|
||||
})
|
||||
|
||||
/* eslint-disable no-undef */
|
||||
routesE2E.get('/__cypress/automation/setLocalStorage', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/automation/setLocalStorage`, (req, res) => {
|
||||
const origin = req.originalUrl.slice(req.originalUrl.indexOf('?') + 1)
|
||||
|
||||
networkProxy.http.getRenderedHTMLOrigins()[origin] = true
|
||||
@@ -103,7 +103,7 @@ export const createRoutesE2E = ({
|
||||
})
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
routesE2E.get('/__cypress/source-maps/:id.map', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/source-maps/:id.map`, (req, res) => {
|
||||
networkProxy.handleSourceMapRequest(req, res)
|
||||
})
|
||||
|
||||
@@ -115,7 +115,7 @@ export const createRoutesE2E = ({
|
||||
})
|
||||
|
||||
// special fallback - serve dist'd (bundled/static) files from the project path folder
|
||||
routesE2E.get('/__cypress/bundled/*', (req, res) => {
|
||||
routesE2E.get(`/${config.namespace}/bundled/*`, (req, res) => {
|
||||
const file = AppData.getBundledFilePath(config.projectRoot, path.join('src', req.params[0]))
|
||||
|
||||
debug(`Serving dist'd bundle at file path: %o`, { path: file, url: req.url })
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import httpProxy from 'http-proxy'
|
||||
import _ from 'lodash'
|
||||
import Debug from 'debug'
|
||||
import { ErrorRequestHandler, Router } from 'express'
|
||||
import send from 'send'
|
||||
@@ -9,7 +8,7 @@ import type { Browser } from './browsers/types'
|
||||
import type { NetworkProxy } from '@packages/proxy'
|
||||
import type { Cfg } from './project-base'
|
||||
import xhrs from './controllers/xhrs'
|
||||
import { runner, ServeOptions } from './controllers/runner'
|
||||
import { runner } from './controllers/runner'
|
||||
import { iframesController } from './controllers/iframes'
|
||||
import type { FoundSpec } from '@packages/types'
|
||||
import { getCtx } from '@packages/data-context'
|
||||
@@ -38,45 +37,8 @@ export const createCommonRoutes = ({
|
||||
nodeProxy,
|
||||
exit,
|
||||
}: InitializeRoutes) => {
|
||||
const makeServeConfig = (options: Partial<ServeOptions>) => {
|
||||
const config = {
|
||||
...options.config,
|
||||
testingType,
|
||||
browser: options.getCurrentBrowser?.(),
|
||||
} as Cfg
|
||||
|
||||
if (testingType === 'e2e') {
|
||||
config.remote = getRemoteState()
|
||||
}
|
||||
|
||||
// TODO: move the component file watchers in here
|
||||
// and update them in memory when they change and serve
|
||||
// them straight to the HTML on load
|
||||
|
||||
debug('serving runner index.html with config %o',
|
||||
_.pick(config, 'version', 'platform', 'arch', 'projectName'))
|
||||
|
||||
// base64 before embedding so user-supplied contents can't break out of <script>
|
||||
// https://github.com/cypress-io/cypress/issues/4952
|
||||
|
||||
const base64Config = Buffer.from(JSON.stringify(config)).toString('base64')
|
||||
|
||||
return {
|
||||
base64Config,
|
||||
projectName: config.projectName,
|
||||
}
|
||||
}
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(['/api', '/__/api'], (req, res) => {
|
||||
const options = makeServeConfig({
|
||||
config,
|
||||
getCurrentBrowser,
|
||||
})
|
||||
|
||||
res.json(options)
|
||||
})
|
||||
const { clientRoute, namespace } = config
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_VITE_DEV) {
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
@@ -94,15 +56,15 @@ export const createCommonRoutes = ({
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/__cypress/runner/*', (req, res) => {
|
||||
router.get(`/${namespace}/runner/*`, (req, res) => {
|
||||
runner.handle(testingType, req, res)
|
||||
})
|
||||
|
||||
router.all('/__cypress/xhrs/*', (req, res, next) => {
|
||||
router.all(`/${namespace}/xhrs/*`, (req, res, next) => {
|
||||
xhrs.handle(req, res, config, next)
|
||||
})
|
||||
|
||||
router.get('/__cypress/iframes/*', (req, res) => {
|
||||
router.get(`/${namespace}/iframes/*`, (req, res) => {
|
||||
if (testingType === 'e2e') {
|
||||
iframesController.e2e({ config, getSpec, getRemoteState }, req, res)
|
||||
}
|
||||
@@ -112,8 +74,6 @@ export const createCommonRoutes = ({
|
||||
}
|
||||
})
|
||||
|
||||
const clientRoute = config.clientRoute
|
||||
|
||||
if (!clientRoute) {
|
||||
throw Error(`clientRoute is required. Received ${clientRoute}`)
|
||||
}
|
||||
@@ -121,7 +81,7 @@ export const createCommonRoutes = ({
|
||||
router.get(clientRoute, (req, res) => {
|
||||
debug('Serving Cypress front-end by requested URL:', req.url)
|
||||
|
||||
if (process.env.LAUNCHPAD) {
|
||||
if (process.env.LAUNCHPAD || process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
getCtx().html.appHtml()
|
||||
.then((html) => res.send(html))
|
||||
.catch((e) => res.status(500).send({ stack: e.stack }))
|
||||
|
||||
@@ -40,6 +40,7 @@ export const makeServeConfig = (options) => {
|
||||
return {
|
||||
base64Config,
|
||||
projectName: config.projectName,
|
||||
namespace: config.namespace,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,6 @@ import { createRoutesE2E } from './routes-e2e'
|
||||
import { createRoutesCT } from './routes-ct'
|
||||
import type { FoundSpec } from '@packages/types'
|
||||
|
||||
const ALLOWED_PROXY_BYPASS_URLS = [
|
||||
'/',
|
||||
'/__cypress/runner/cypress_runner.css',
|
||||
'/__cypress/runner/cypress_runner.js', // TODO: fix this
|
||||
'/__cypress/runner/favicon.ico',
|
||||
]
|
||||
const DEFAULT_DOMAIN_NAME = 'localhost'
|
||||
const fullyQualifiedRe = /^https?:\/\//
|
||||
|
||||
@@ -49,7 +43,14 @@ const _isNonProxiedRequest = (req) => {
|
||||
return req.proxiedUrl.startsWith('/')
|
||||
}
|
||||
|
||||
const _forceProxyMiddleware = function (clientRoute) {
|
||||
const _forceProxyMiddleware = function (clientRoute, namespace = '__cypress') {
|
||||
const ALLOWED_PROXY_BYPASS_URLS = [
|
||||
'/',
|
||||
`/${namespace}/runner/cypress_runner.css`,
|
||||
`/${namespace}/runner/cypress_runner.js`, // TODO: fix this
|
||||
`/${namespace}/runner/favicon.ico`,
|
||||
]
|
||||
|
||||
// normalize clientRoute to help with comparison
|
||||
const trimmedClientRoute = _.trimEnd(clientRoute, '/')
|
||||
|
||||
@@ -233,7 +234,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
}
|
||||
|
||||
createExpressApp (config) {
|
||||
const { morgan, clientRoute } = config
|
||||
const { morgan, clientRoute, namespace } = config
|
||||
const app = express()
|
||||
|
||||
// set the cypress config from the cypress.config.{ts|js} file
|
||||
@@ -258,7 +259,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
return next()
|
||||
})
|
||||
|
||||
app.use(_forceProxyMiddleware(clientRoute))
|
||||
app.use(_forceProxyMiddleware(clientRoute, namespace))
|
||||
|
||||
app.use(require('cookie-parser')())
|
||||
app.use(compression({ filter: notSSE }))
|
||||
@@ -518,6 +519,9 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
port,
|
||||
protocol,
|
||||
},
|
||||
headers: {
|
||||
'x-cypress-forwarded-from-cypress': true,
|
||||
},
|
||||
agent,
|
||||
}, onProxyErr)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,11 @@ export class SocketAllowed {
|
||||
*/
|
||||
isRequestAllowed (req: Request) {
|
||||
const { remotePort, remoteAddress } = req.socket
|
||||
const isAllowed = this.allowedLocalPorts.includes(remotePort!)
|
||||
&& ['127.0.0.1', '::1'].includes(remoteAddress!)
|
||||
const remotePortInAllowList = this.allowedLocalPorts.includes(remotePort!)
|
||||
|
||||
// When testing cypress in cypress, we pass along the x-cypress-forwarded-from-cypress header to signify this is a safe request
|
||||
const trustedSourceUsingCypressInCypress = !!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF && !!req.headers['x-cypress-forwarded-from-cypress']
|
||||
const isAllowed = (remotePortInAllowList || trustedSourceUsingCypressInCypress) && ['127.0.0.1', '::1'].includes(remoteAddress!)
|
||||
|
||||
debug('is incoming request allowed? %o', { isAllowed, reqUrl: req.url, remotePort, remoteAddress })
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ export interface FullConfig extends Partial<Cypress.RuntimeConfigOptions & Cypre
|
||||
resolved: ResolvedConfigurationOptions
|
||||
}
|
||||
|
||||
// Cannot just use RuntimeConfigOptions as is because some types are not complete.
|
||||
// Instead, this is an interface of values that have been manually validated to exist
|
||||
// and are required when creating a project.
|
||||
export type ReceivedCypressOptions =
|
||||
Pick<Cypress.RuntimeConfigOptions, 'hosts' | 'projectName' | 'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'report' | 'socketIoCookie' | 'configFile' | 'isTextTerminal' | 'isNewProject' | 'proxyUrl' | 'browsers' | 'browserUrl' | 'socketIoRoute' | 'arch' | 'platform' | 'spec' | 'specs' | 'browser' | 'version' | 'remote'>
|
||||
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'pluginsFile' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'testFiles' | 'ignoreSpecPattern' | 'specPattern'> // TODO: Figure out how to type this better.
|
||||
|
||||
export interface SampleConfigFile{
|
||||
status: 'changes' | 'valid' | 'skipped' | 'error'
|
||||
filePath: string
|
||||
|
||||
@@ -266,9 +266,13 @@ export async function scaffoldProjectNodeModules (project: string, updateYarnLoc
|
||||
|
||||
export async function scaffoldCommonNodeModules () {
|
||||
await Promise.all([
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react',
|
||||
'babel-loader',
|
||||
// Used for import { defineConfig } from 'cypress'
|
||||
'cypress',
|
||||
'@cypress/code-coverage',
|
||||
'@cypress/react',
|
||||
'@cypress/webpack-dev-server',
|
||||
'@packages/socket',
|
||||
'@packages/ts',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"globals": {
|
||||
"defineProps": "readonly",
|
||||
"defineEmits": "readonly",
|
||||
"defineExpose": "readonly",
|
||||
"withDefaults": "readonly"
|
||||
},
|
||||
"plugins": [
|
||||
"cypress",
|
||||
"@cypress/dev"
|
||||
],
|
||||
"extends": [
|
||||
"../../../packages/frontend-shared/.eslintrc.json"
|
||||
],
|
||||
"env": {
|
||||
"cypress/globals": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-dupe-class-members": "off",
|
||||
"@typescript-eslint/no-dupe-class-members": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"lib/*"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"**/*.json"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": "off",
|
||||
"comma-dangle": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.tsx",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"react/jsx-no-bind": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/no-unknown-property": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
projectId: 'abc123',
|
||||
experimentalInteractiveRunEvents: true,
|
||||
component: {
|
||||
specPattern: 'src/**/*.{spec,cy}.{js,ts,tsx,jsx}',
|
||||
supportFile: false,
|
||||
devServer: require('@cypress/react/plugins/load-webpack'),
|
||||
devServerConfig: {
|
||||
webpackFilename: 'webpack.config.js',
|
||||
},
|
||||
},
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.spec.{js,ts}',
|
||||
supportFile: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { registerMountFn } from '@packages/frontend-shared/cypress/support/common'
|
||||
|
||||
registerMountFn()
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>DOM Content</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<ul class="list">
|
||||
<li>
|
||||
Item 1
|
||||
</li>
|
||||
<li>
|
||||
Item 2
|
||||
</li>
|
||||
<li>
|
||||
Item 3
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
@@ -0,0 +1,5 @@
|
||||
describe('Dom Content', () => {
|
||||
it('renders the test content', () => {
|
||||
cy.visit('cypress/e2e/dom-content.html')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { mount } from '@cypress/react'
|
||||
|
||||
describe('TestComponent', () => {
|
||||
it('renders the test component', () => {
|
||||
mount(<div>Component Test</div>)
|
||||
|
||||
cy.contains('Component Test').should('be.visible')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -9527,9 +9527,9 @@
|
||||
webpack "^5"
|
||||
|
||||
"@types/webpack@^4", "@types/webpack@^4.4.31", "@types/webpack@^4.41.12", "@types/webpack@^4.41.21", "@types/webpack@^4.41.26", "@types/webpack@^4.41.8":
|
||||
version "4.41.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.30.tgz#fd3db6d0d41e145a8eeeafcd3c4a7ccde9068ddc"
|
||||
integrity sha512-GUHyY+pfuQ6haAfzu4S14F+R5iGRwN6b2FRNJY7U0NilmFAqbsOfK6j1HwuLBAqwRIT+pVdNDJGJ6e8rpp0KHA==
|
||||
version "4.41.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.32.tgz#a7bab03b72904070162b2f169415492209e94212"
|
||||
integrity sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/tapable" "^1"
|
||||
@@ -18857,10 +18857,10 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0, enhanced-resolve@^4.3.0, enhan
|
||||
memory-fs "^0.5.0"
|
||||
tapable "^1.0.0"
|
||||
|
||||
enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.0:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
|
||||
integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
|
||||
enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0"
|
||||
integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
@@ -19074,10 +19074,10 @@ es-get-iterator@^1.0.2, es-get-iterator@^1.1.1:
|
||||
is-string "^1.0.5"
|
||||
isarray "^2.0.5"
|
||||
|
||||
es-module-lexer@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
|
||||
integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==
|
||||
es-module-lexer@^0.9.0:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
|
||||
integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
|
||||
|
||||
es-to-primitive@^1.2.1:
|
||||
version "1.2.1"
|
||||
@@ -42134,10 +42134,10 @@ watchpack@^1.6.0, watchpack@^1.7.4:
|
||||
chokidar "^3.4.1"
|
||||
watchpack-chokidar2 "^2.0.1"
|
||||
|
||||
watchpack@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
|
||||
integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
|
||||
watchpack@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
||||
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
|
||||
dependencies:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
@@ -42556,10 +42556,10 @@ webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-
|
||||
source-list-map "^2.0.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
webpack-sources@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
|
||||
integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
|
||||
webpack-sources@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260"
|
||||
integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==
|
||||
|
||||
webpack-subresource-integrity@1.5.2:
|
||||
version "1.5.2"
|
||||
@@ -42697,9 +42697,9 @@ webpack@4.44.2:
|
||||
webpack-sources "^1.4.1"
|
||||
|
||||
webpack@^5, webpack@^5.38.1:
|
||||
version "5.52.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.52.0.tgz#88d997c2c3ebb62abcaa453d2a26e0fd917c71a3"
|
||||
integrity sha512-yRZOat8jWGwBwHpco3uKQhVU7HYaNunZiJ4AkAVQkPCUGoZk/tiIXiwG+8HIy/F+qsiZvSOa+GLQOj3q5RKRYg==
|
||||
version "5.65.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.65.0.tgz#ed2891d9145ba1f0d318e4ea4f89c3fa18e6f9be"
|
||||
integrity sha512-Q5or2o6EKs7+oKmJo7LaqZaMOlDWQse9Tm5l1WAfU/ujLGN5Pb0SqGeVkN/4bpPmEqEP5RnVhiqsOtWtUVwGRw==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.0"
|
||||
"@types/estree" "^0.0.50"
|
||||
@@ -42710,8 +42710,8 @@ webpack@^5, webpack@^5.38.1:
|
||||
acorn-import-assertions "^1.7.6"
|
||||
browserslist "^4.14.5"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.8.0"
|
||||
es-module-lexer "^0.7.1"
|
||||
enhanced-resolve "^5.8.3"
|
||||
es-module-lexer "^0.9.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
glob-to-regexp "^0.4.1"
|
||||
@@ -42723,8 +42723,8 @@ webpack@^5, webpack@^5.38.1:
|
||||
schema-utils "^3.1.0"
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.1.3"
|
||||
watchpack "^2.2.0"
|
||||
webpack-sources "^3.2.0"
|
||||
watchpack "^2.3.1"
|
||||
webpack-sources "^3.2.2"
|
||||
|
||||
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
|
||||
version "0.7.4"
|
||||
|
||||
Reference in New Issue
Block a user