Skip to content
Merged
6 changes: 5 additions & 1 deletion cli/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,17 @@ const getApplicationDataFolder = (...paths) => {
const { env } = process

// allow overriding the app_data folder
const folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'
let folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'

const PRODUCT_NAME = pkg.productName || pkg.name
const OS_DATA_PATH = ospath.data()

const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME)

if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`
}

const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths)

return p
Expand Down
12 changes: 11 additions & 1 deletion packages/data-context/src/actions/DevActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,18 @@ export class DevActions {
}
}

// In a setTimeout so that we flush the triggering response to the client before sending
triggerRelaunch () {
return this.ctx.fs.writeFile(DevActions.CY_TRIGGER_UPDATE, JSON.stringify(new Date()))
setTimeout(async () => {
try {
await this.ctx.destroy()
} catch (e) {
this.ctx.logError(e)
} finally {
process.exitCode = 0
await this.ctx.fs.writeFile(DevActions.CY_TRIGGER_UPDATE, JSON.stringify(new Date()))
}
}, 10)
}

dismissRelaunch () {
Expand Down
5 changes: 1 addition & 4 deletions packages/data-context/src/sources/EnvDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import type { DataContext } from '../DataContext'

/**
* Centralizes all of the "env"
* TODO: see if/how we might want to use this?
*/
export class EnvDataSource {
constructor (private ctx: DataContext) {}

get CYPRESS_INTERNAL_VITE_APP_PORT () {
return process.env.CYPRESS_INTERNAL_VITE_APP_PORT
}
}
21 changes: 13 additions & 8 deletions packages/data-context/src/sources/HtmlDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,32 @@ import { getPathToDist } from '@packages/resolve-dist'
export class HtmlDataSource {
constructor (private ctx: DataContext) {}

async fetchAppInitialData () {
async fetchLaunchpadInitialData () {
const graphql = this.ctx.graphqlClient()

await Promise.all([
graphql.executeQuery('HeaderBar_HeaderBarQueryDocument', {}),
graphql.executeQuery('AppQueryDocument', {}),
graphql.executeQuery('MainLaunchpadQueryDocument', {}),
])

return graphql.getSSRData()
}

async fetchAppInitialData () {
const graphql = this.ctx.graphqlClient()

await Promise.all([
graphql.executeQuery('SettingsDocument', {}),
graphql.executeQuery('SpecsPageContainerDocument', {}),
graphql.executeQuery('HeaderBar_HeaderBarQueryDocument', {}),
graphql.executeQuery('SideBarNavigationDocument', {}),
])

return graphql.getSSRData()
}

async fetchAppHtml () {
if (this.ctx.env.CYPRESS_INTERNAL_VITE_APP_PORT) {
const response = await this.ctx.util.fetch(`http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`, { method: 'GET' })
const html = await response.text()

return html
}

return this.ctx.fs.readFile(getPathToDist('app', 'index.html'), 'utf8')
}

Expand Down
11 changes: 8 additions & 3 deletions packages/data-context/src/sources/VersionsDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface VersionData {
}

export class VersionsDataSource {
static result: undefined | Promise<string>

/**
* Returns most recent and current version of Cypress
* {
Expand All @@ -27,9 +29,12 @@ export class VersionsDataSource {
*/
async versions (): Promise<VersionData> {
const currentCypressVersion = require('@packages/root')
const result = await execa(`npm`, [`view`, `cypress`, `time`, `--json`])

const json = JSON.parse(result.stdout)
VersionsDataSource.result ??= execa(`npm`, [`view`, `cypress`, `time`, `--json`]).then((result) => result.stdout)

const result = await VersionsDataSource.result

const json = JSON.parse(result)

delete json['modified']
delete json['created']
Expand All @@ -50,7 +55,7 @@ export class VersionsDataSource {
latest: latestVersion,
current: {
version: currentCypressVersion.version,
released: currentCypressVersion.version === '0.0.0-development' ? new Date().toISOString() : json[currentCypressVersion.version],
released: json[currentCypressVersion.version] ?? new Date().toISOString(),
id: currentCypressVersion.version,
},
}
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ chai.use(sinonChai)

export async function e2ePluginSetup (projectRoot: string, on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
delete process.env.CYPRESS_INTERNAL_GRAPHQL_PORT
delete process.env.CYPRESS_INTERNAL_VITE_DEV
delete process.env.CYPRESS_INTERNAL_VITE_APP_PORT
delete process.env.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT
// require'd so we don't import the types from @packages/server which would
// pollute strict type checking
const { runInternalServer } = require('@packages/server/lib/modes/internal-server')
Expand Down
2 changes: 0 additions & 2 deletions packages/frontend-shared/cypress/fixtures/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@
{
"value": {
"INTERNAL_CLOUD_ENV": "staging",
"INTERNAL_VITE_APP_PORT": 3333,
"INTERNAL_VITE_LAUNCHPAD_PORT": 3001,
"KONFIG_ENV": "staging"
},
"field": "env",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ const promptsEl: Ref<HTMLElement | null> = ref(null)
// so it doesn't reopen on the one of the prompts

const versions = computed(() => {
if (!props.gql.versions) {
return null
}

return {
current: {
released: useTimeAgo(new Date(props.gql.versions.current.released)).value,
Expand All @@ -305,7 +309,7 @@ const versions = computed(() => {
})

const runningOldVersion = computed(() => {
return props.gql.versions.current.released < props.gql.versions.latest.released
return props.gql.versions ? props.gql.versions.current.released < props.gql.versions.latest.released : false
})

onClickOutside(promptsEl, () => {
Expand Down
57 changes: 34 additions & 23 deletions packages/frontend-shared/src/graphql/urqlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,37 @@ declare global {
}
}

export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
let gqlPort: string

function gqlPort () {
if (GQL_PORT_MATCH) {
gqlPort = GQL_PORT_MATCH[1]
} else if (window.__CYPRESS_GRAPHQL_PORT__) {
gqlPort = window.__CYPRESS_GRAPHQL_PORT__
} else {
throw new Error(`${window.location.href} cannot be visited without a gqlPort`)
return GQL_PORT_MATCH[1]
}

if (window.__CYPRESS_GRAPHQL_PORT__) {
return window.__CYPRESS_GRAPHQL_PORT__
}

const GRAPHQL_URL = `http://localhost:${gqlPort}/graphql`
throw new Error(`${window.location.href} cannot be visited without a gqlPort`)
}

export async function preloadLaunchpadData () {
try {
const resp = await fetch(`http://localhost:${gqlPort()}/__cypress/launchpad-preload`)

window.__CYPRESS_INITIAL_DATA__ = await resp.json()
} catch {
//
}
}

export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
const port = gqlPort()

const GRAPHQL_URL = `http://localhost:${port}/graphql`

// 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, serverPort: SERVER_PORT_MATCH?.[1] })
const io = getPubSubSource({ target, gqlPort: port, serverPort: SERVER_PORT_MATCH?.[1] })

const exchanges: Exchange[] = [
dedupExchange,
Expand All @@ -67,32 +81,29 @@ export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
${error.stack ?? ''}
`

toast.error(message, {
timeout: false,
})
if (process.env.NODE_ENV !== 'production') {
toast.error(message, {
timeout: false,
})
}

// eslint-disable-next-line
console.error(error)
},
}),
// https://formidable.com/open-source/urql/docs/graphcache/errors/
makeCacheExchange(),
ssrExchange({
isClient: true,
initialState: window.__CYPRESS_INITIAL_DATA__ ?? {},
}),
namedRouteExchange,
// TODO(tim): add this when we want to use the socket as the GraphQL
// transport layer for all operations
// target === 'launchpad' ? fetchExchange : socketExchange(io),
fetchExchange,
]

// If we're in the launched app, we want to use the SSR exchange
if (target === 'app') {
exchanges.push(ssrExchange({
isClient: true,
initialState: window.__CYPRESS_INITIAL_DATA__ ?? {},
}))
}

exchanges.push(fetchExchange)

if (import.meta.env.DEV) {
exchanges.unshift(devtoolsExchange)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const namedRouteExchange: Exchange = ({ client, forward }) => {
return forward(pipe(
ops$,
map((o) => {
if (o.context.requestPolicy === 'cache-first' || o.context.requestPolicy === 'cache-only') {
return o
}

if (!o.context.url.endsWith('/graphql')) {
throw new Error(`Infinite loop detected? Ping @tgriesser to help debug`)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ type Query {
projects: [ProjectLike!]!

"""Previous versions of cypress and their release date"""
versions: VersionData!
versions: VersionData

"""Metadata about the wizard, null if we arent showing the wizard"""
wizard: Wizard!
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Query = objectType({
resolve: (root, args, ctx) => ctx.coreData.dev,
})

t.nonNull.field('versions', {
t.field('versions', {
type: VersionData,
description: 'Previous versions of cypress and their release date',
resolve: (root, args, ctx) => {
Expand Down
26 changes: 14 additions & 12 deletions packages/launchpad/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,20 @@ let isShowingRelaunch = ref(false)
const toast = useToast()

watch(query.data, () => {
if (query.data.value?.dev.needsRelaunch && !isShowingRelaunch.value) {
isShowingRelaunch.value = true
toast.info('Server updated, click to relaunch', {
timeout: false,
onClick: () => {
relaunchMutation.executeMutation({ action: 'trigger' })
},
onClose: () => {
isShowingRelaunch.value = false
relaunchMutation.executeMutation({ action: 'dismiss' })
},
})
if (process.env.NODE_ENV !== 'production') {
if (query.data.value?.dev.needsRelaunch && !isShowingRelaunch.value) {
isShowingRelaunch.value = true
toast.info('Server updated, click to relaunch', {
timeout: false,
onClick: () => {
relaunchMutation.executeMutation({ action: 'trigger' })
},
onClose: () => {
isShowingRelaunch.value = false
relaunchMutation.executeMutation({ action: 'dismiss' })
},
})
}
}
})

Expand Down
3 changes: 2 additions & 1 deletion packages/launchpad/src/error/BaseError.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ const props = defineProps<{
const latestOperation = window.localStorage.getItem('latestGQLOperation')

const retry = async () => {
const { launchpadClient } = await import('../main')
const { getLaunchpadClient } = await import('../main')
const launchpadClient = getLaunchpadClient()

const op = latestOperation ? JSON.parse(latestOperation) : null

Expand Down
28 changes: 21 additions & 7 deletions packages/launchpad/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,42 @@
import { createApp } from 'vue'
import './main.scss'
import 'virtual:windi.css'
import urql from '@urql/vue'
import urql, { Client } from '@urql/vue'
import App from './App.vue'
import Toast, { POSITION } from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import { makeUrqlClient } from '@packages/frontend-shared/src/graphql/urqlClient'
import { makeUrqlClient, preloadLaunchpadData } from '@packages/frontend-shared/src/graphql/urqlClient'
import { createI18n } from '@cy/i18n'
import { initHighlighter } from '@cy/components/ShikiHighlight.vue'

const app = createApp(App)

export const launchpadClient = makeUrqlClient('launchpad')

app.use(Toast, {
position: POSITION.BOTTOM_RIGHT,
})

app.use(urql, launchpadClient)
app.use(createI18n())

let launchpadClient: Client

// TODO: (tim) remove this when we refactor to remove the retry plugin logic
export function getLaunchpadClient () {
if (!launchpadClient) {
throw new Error(`Cannot access launchpadClient before app has been init`)
}

return launchpadClient
}

// Make sure highlighter is initialized before
// we show any code to avoid jank at rendering
// @ts-ignore
initHighlighter().then(() => {
Promise.all([
// @ts-ignore
initHighlighter(),
preloadLaunchpadData(),
]).then(() => {
launchpadClient = makeUrqlClient('launchpad')
app.use(urql, launchpadClient)

app.mount('#app')
})
4 changes: 2 additions & 2 deletions packages/resolve-dist/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export const getPathToIndex = (pkg: RunnerPkg) => {
}

export const getPathToDesktopIndex = (pkg: 'desktop-gui' | 'launchpad', graphqlPort?: number) => {
// For now, if we see that there's a CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT
// For now, if we see that there's a CYPRESS_INTERNAL_VITE_DEV
// we assume we're running Cypress targeting that (dev server)
if (pkg === 'launchpad' && process.env.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT) {
if (pkg === 'launchpad' && process.env.CYPRESS_INTERNAL_VITE_DEV) {
return `http://localhost:${process.env.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT}?gqlPort=${graphqlPort}`
}

Expand Down
Loading