Files
cypress/packages/server/lib/modes/interactive.ts

198 lines
5.6 KiB
TypeScript

import _ from 'lodash'
import os from 'os'
import { app, nativeImage as image } from 'electron'
import * as cyIcons from '@packages/icons'
import * as savedState from '../saved_state'
import menu from '../gui/menu'
import * as Windows from '../gui/windows'
import { makeGraphQLServer } from '@packages/graphql/src/makeGraphQLServer'
import { globalPubSub, getCtx, clearCtx } from '@packages/data-context'
// eslint-disable-next-line no-duplicate-imports
import type { WebContents } from 'electron'
import type { LaunchArgs, Preferences } from '@packages/types'
import debugLib from 'debug'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const debug = debugLib('cypress:server:interactive')
const isDev = () => {
return Boolean(process.env['CYPRESS_INTERNAL_ENV'] === 'development')
}
export = {
isMac () {
return os.platform() === 'darwin'
},
getWindowArgs (url: string, state: Preferences) {
// Electron Window's arguments
// These options are passed to Electron's BrowserWindow
const minWidth = Math.round(/* 13" MacBook Air */ 1792 / 3) // Thirds
const preferredWidth = 1200
const minHeight = 400
const preferredHeight = 800
const chooseDimensions = ({ preferred, previous, minimum }) => {
// If the user doesn't have a previous size that's valid or big
// enough, use the preferred size instead.
if (!previous || previous < minimum) {
return preferred
}
return previous
}
const common = {
url,
// The backgroundColor should match the value we will show in the
// launchpad frontend.
// When we use a dist'd launchpad (production), this color won't be
// as visible. However, in our local dev setup (launchpad running via
// a dev server), the backgroundColor will flash if it is a
// different color.
backgroundColor: 'white',
// Dimensions of the Electron window on initial launch.
// Because we are migrating users that may have
// a width smaller than the min dimensions, we will
// force the dimensions to be within the minimum bounds.
//
// Doing this before launch (instead of relying on minW + minH)
// prevents the window from jumping.
width: chooseDimensions({
preferred: preferredWidth,
minimum: minWidth,
previous: state.appWidth,
}),
height: chooseDimensions({
preferred: preferredHeight,
minimum: minHeight,
previous: state.appHeight,
}),
minWidth,
minHeight,
x: state.appX,
y: state.appY,
type: 'INDEX',
devTools: state.isAppDevToolsOpen,
trackState: {
width: 'appWidth',
height: 'appHeight',
x: 'appX',
y: 'appY',
devTools: 'isAppDevToolsOpen',
},
onBlur (this: {webContents: WebContents}) {
if (this.webContents.isDevToolsOpened()) {
return
}
return Windows.hideAllUnlessAnotherWindowIsFocused()
},
onFocus () {
// hide internal dev tools if in production and previously focused
// window was the electron browser
menu.set({ withInternalDevTools: isDev() })
return Windows.showAll()
},
onClose () {
app.quit()
},
}
return _.extend(common, this.platformArgs())
},
platformArgs () {
const args = {
darwin: {
show: true,
frame: true,
transparent: false,
},
linux: {
show: true,
frame: true,
transparent: false,
icon: image.createFromPath(cyIcons.getPathToIcon('icon_64x64.png')),
},
}
return args[os.platform()]
},
async ready (options: LaunchArgs, launchpadPort: number) {
const { projectRoot } = options
const ctx = getCtx()
menu.set({
withInternalDevTools: isDev(),
onLogOutClicked () {
return globalPubSub.emit('menu:item:clicked', 'log:out')
},
getGraphQLPort: () => {
return ctx?.gqlServerPort
},
})
const State = await savedState.create(projectRoot, false)
const state = await State.get()
const url = getPathToDesktopIndex(launchpadPort)
const win = await Windows.open(projectRoot, this.getWindowArgs(url, state))
ctx?.actions.electron.setBrowserWindow(win)
return win
},
async run (options: LaunchArgs, _loading: Promise<void>) {
const [, port] = await Promise.all([
app.whenReady(),
makeGraphQLServer(),
])
// Before the electron app quits, we interrupt and ensure the current
// DataContext is completely destroyed prior to quitting the process.
// Parts of the DataContext teardown are asynchronous, particularly the
// closing of open file watchers, and not awaiting these can cause
// the electron process to throw.
// https://github.com/cypress-io/cypress/issues/22026
app.once('will-quit', (event: Event) => {
// We must call synchronously call preventDefault on the will-quit event
// to halt the current quit lifecycle
event.preventDefault()
debug('clearing DataContext prior to quit')
// We use setImmediate to guarantee that app.quit will be called asynchronously;
// synchronously calling app.quit in the will-quit handler prevent the subsequent
// close from occurring
setImmediate(async () => {
try {
await clearCtx()
} catch (e) {
// Silently handle clearCtx errors, we still need to quit the app
debug(`DataContext cleared with error: ${e?.message}`)
}
debug('DataContext cleared, quitting app')
app.quit()
})
})
return this.ready(options, port)
},
}