mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-03 05:20:38 -05:00
feat: Focus browser from select browser screen and on dashboard login (#19842)
Co-authored-by: Zachary Williams <zachjw34@gmail.com>
This commit is contained in:
@@ -10,4 +10,8 @@ export class BrowserActions {
|
||||
closeBrowser () {
|
||||
return this.browserApi.close()
|
||||
}
|
||||
|
||||
async focusActiveBrowserWindow () {
|
||||
await this.browserApi.focusActiveBrowserWindow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import type { FoundBrowser } from '@packages/types'
|
||||
import os from 'os'
|
||||
import { execSync } from 'child_process'
|
||||
import type { DataContext } from '..'
|
||||
|
||||
let isPowerShellAvailable = false
|
||||
|
||||
try {
|
||||
execSync(`[void] ''`, { shell: 'powershell' })
|
||||
isPowerShellAvailable = true
|
||||
} catch {
|
||||
// Powershell is unavailable
|
||||
}
|
||||
|
||||
const platform = os.platform()
|
||||
|
||||
export interface BrowserApiShape {
|
||||
close(): Promise<any>
|
||||
ensureAndGetByNameOrPath(nameOrPath: string): Promise<FoundBrowser | undefined>
|
||||
getBrowsers(): Promise<FoundBrowser[]>
|
||||
focusActiveBrowserWindow(): Promise<any>
|
||||
}
|
||||
|
||||
export class BrowserDataSource {
|
||||
@@ -48,4 +62,17 @@ export class BrowserDataSource {
|
||||
|
||||
return this.idForBrowser(this.ctx.coreData.chosenBrowser) === this.idForBrowser(obj)
|
||||
}
|
||||
|
||||
isFocusSupported (obj: FoundBrowser) {
|
||||
if (platform === 'darwin' || obj.family !== 'firefox') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only allow focusing if PowerShell is available on Windows, since that's what we use to do it
|
||||
if (obj.family === 'firefox' && platform === 'win32') {
|
||||
return isPowerShellAvailable
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import pDefer from 'p-defer'
|
||||
interface InternalOpenProjectArgs {
|
||||
argv: string[]
|
||||
projectName: string
|
||||
browser: string
|
||||
}
|
||||
|
||||
interface InternalAddProjectOpts {
|
||||
@@ -228,15 +227,11 @@ async function makeE2ETasks () {
|
||||
e2eServerPort: ctx.appServerPort,
|
||||
}
|
||||
},
|
||||
async __internal_openProject ({ argv, projectName, browser }: InternalOpenProjectArgs): Promise<ResetOptionsResult> {
|
||||
async __internal_openProject ({ argv, projectName }: InternalOpenProjectArgs): Promise<ResetOptionsResult> {
|
||||
if (!scaffoldedProjects.has(projectName)) {
|
||||
throw new Error(`${projectName} has not been scaffolded. Be sure to call cy.scaffoldProject('${projectName}') in the test, a before, or beforeEach hook`)
|
||||
}
|
||||
|
||||
if (browser !== 'chrome') {
|
||||
throw new Error(`Cypress in cypress does not support running in the ${browser} browser`)
|
||||
}
|
||||
|
||||
const openArgv = [...argv, '--project', Fixtures.projectPath(projectName), '--port', '4455']
|
||||
|
||||
// Runs the launchArgs through the whole pipeline for the CLI open process,
|
||||
|
||||
@@ -186,7 +186,7 @@ function openProject (projectName: ProjectFixture, argv: string[] = []) {
|
||||
}
|
||||
|
||||
return logInternal({ name: 'openProject', message: argv.join(' ') }, () => {
|
||||
return taskInternal('__internal_openProject', { projectName, argv, browser: Cypress.browser.name })
|
||||
return taskInternal('__internal_openProject', { projectName, argv })
|
||||
}).then((obj) => {
|
||||
Cypress.env('e2e_serverPort', obj.e2eServerPort)
|
||||
|
||||
@@ -195,6 +195,12 @@ function openProject (projectName: ProjectFixture, argv: string[] = []) {
|
||||
}
|
||||
|
||||
function startAppServer (mode: 'component' | 'e2e' = 'e2e') {
|
||||
const browser = Cypress.browser.name
|
||||
|
||||
if (browser !== 'chrome') {
|
||||
throw new Error(`Cypress in cypress does not support running in the ${browser} browser`)
|
||||
}
|
||||
|
||||
return logInternal('startAppServer', (log) => {
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const longBrowsersList = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'electron',
|
||||
displayName: 'Electron',
|
||||
family: 'chromium',
|
||||
@@ -8,8 +9,10 @@ export const longBrowsersList = [
|
||||
path: '',
|
||||
majorVersion: '73',
|
||||
info: 'Info about electron browser',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'chrome',
|
||||
displayName: 'Chrome',
|
||||
family: 'chromium',
|
||||
@@ -17,8 +20,10 @@ export const longBrowsersList = [
|
||||
version: '78.0.3904.108',
|
||||
path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
majorVersion: '78',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'chrome',
|
||||
displayName: 'Chrome',
|
||||
family: 'chromium',
|
||||
@@ -26,8 +31,10 @@ export const longBrowsersList = [
|
||||
version: '88.0.3904.00',
|
||||
path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
majorVersion: '88',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'chrome',
|
||||
displayName: 'Canary',
|
||||
family: 'chromium',
|
||||
@@ -35,8 +42,10 @@ export const longBrowsersList = [
|
||||
version: '80.0.3977.4',
|
||||
path: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
majorVersion: '80',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'chromium',
|
||||
displayName: 'Chromium',
|
||||
family: 'chromium',
|
||||
@@ -44,8 +53,10 @@ export const longBrowsersList = [
|
||||
version: '74.0.3729.0',
|
||||
path: '/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
majorVersion: '74',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'chromium',
|
||||
displayName: 'Chromium',
|
||||
family: 'chromium',
|
||||
@@ -53,8 +64,10 @@ export const longBrowsersList = [
|
||||
version: '85.0.3729.0',
|
||||
path: '/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
majorVersion: '85',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'edge',
|
||||
displayName: 'Edge Beta',
|
||||
family: 'chromium',
|
||||
@@ -62,8 +75,10 @@ export const longBrowsersList = [
|
||||
version: '79.0.309.71',
|
||||
path: '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta',
|
||||
majorVersion: '79',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'edge',
|
||||
displayName: 'Edge Canary',
|
||||
family: 'chromium',
|
||||
@@ -71,8 +86,10 @@ export const longBrowsersList = [
|
||||
version: '79.0.309.71',
|
||||
path: '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary',
|
||||
majorVersion: '79',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'edge',
|
||||
displayName: 'Edge Dev',
|
||||
family: 'chromium',
|
||||
@@ -80,8 +97,10 @@ export const longBrowsersList = [
|
||||
version: '80.0.309.71',
|
||||
path: '/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev',
|
||||
majorVersion: '79',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: 'firefox',
|
||||
displayName: 'Firefox',
|
||||
family: 'firefox',
|
||||
@@ -90,8 +109,10 @@ export const longBrowsersList = [
|
||||
path: '/Applications/Firefox/Contents/MacOS/Firefox',
|
||||
majorVersion: '69',
|
||||
unsupportedVersion: true,
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: 'firefox',
|
||||
displayName: 'Firefox',
|
||||
family: 'firefox',
|
||||
@@ -100,8 +121,10 @@ export const longBrowsersList = [
|
||||
path: '/Applications/Firefox/Contents/MacOS/Firefox',
|
||||
majorVersion: '75',
|
||||
unsupportedVersion: true,
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
name: 'firefox',
|
||||
displayName: 'Firefox Developer Edition',
|
||||
channel: 'dev',
|
||||
@@ -109,8 +132,10 @@ export const longBrowsersList = [
|
||||
version: '69.0.2',
|
||||
path: '/Applications/Firefox Developer/Contents/MacOS/Firefox Developer',
|
||||
majorVersion: '69',
|
||||
isFocusSupported: true,
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
name: 'firefox',
|
||||
displayName: 'Firefox Nightly',
|
||||
channel: 'beta',
|
||||
@@ -118,5 +143,6 @@ export const longBrowsersList = [
|
||||
version: '69.0.3',
|
||||
path: '/Applications/Firefox Nightly/Contents/MacOS/Firefox Nightly',
|
||||
majorVersion: '69',
|
||||
isFocusSupported: false,
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -32,6 +32,9 @@ export const stubMutation: MaybeResolver<Mutation> = {
|
||||
|
||||
return {}
|
||||
},
|
||||
focusActiveBrowserWindow (sourc, args, ctx) {
|
||||
return true
|
||||
},
|
||||
hideBrowserWindow (source, args, ctx) {
|
||||
return true
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ type Browser implements Node {
|
||||
|
||||
"""Relay style Node ID field for the Browser field"""
|
||||
id: ID!
|
||||
isFocusSupported: Boolean!
|
||||
isSelected: Boolean!
|
||||
majorVersion: String
|
||||
name: String!
|
||||
@@ -692,6 +693,9 @@ type Mutation {
|
||||
"""user has finished migration component specs - move to next step"""
|
||||
finishedRenamingComponentSpecs: Query
|
||||
|
||||
"""Sets focus to the active browser window"""
|
||||
focusActiveBrowserWindow: Boolean!
|
||||
|
||||
"""Generate spec from source"""
|
||||
generateSpecFromSource(codeGenCandidate: String!, type: CodeGenType!): ScaffoldedFile
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ export const Browser = objectType({
|
||||
t.nonNull.string('name')
|
||||
t.nonNull.string('path')
|
||||
t.nonNull.string('version')
|
||||
t.nonNull.boolean('isFocusSupported', {
|
||||
resolve: (source, args, ctx) => ctx.browser.isFocusSupported(source),
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/types',
|
||||
|
||||
@@ -347,6 +347,16 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.field('focusActiveBrowserWindow', {
|
||||
type: 'Boolean',
|
||||
description: 'Sets focus to the active browser window',
|
||||
resolve: async (_, args, ctx) => {
|
||||
await ctx.actions.browser.focusActiveBrowserWindow()
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.field('reconfigureProject', {
|
||||
type: 'Boolean',
|
||||
description: 'show the launchpad windows',
|
||||
|
||||
@@ -182,6 +182,63 @@ describe('Choose a Browser Page', () => {
|
||||
cy.contains('button', 'Close').click()
|
||||
cy.wait('@closeBrowser')
|
||||
})
|
||||
|
||||
it('performs mutation to focus open browser when focus button is pressed', () => {
|
||||
cy.openProject('launchpad', ['--e2e'])
|
||||
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
|
||||
cy.contains('button', 'Start E2E Testing in Chrome').as('launchButton')
|
||||
|
||||
// Stub out response to prevent browser launch but not break internals
|
||||
cy.intercept('mutation-OpenBrowser_LaunchProject', {
|
||||
body: {
|
||||
data: {
|
||||
launchOpenProject: true,
|
||||
setProjectPreferences: {
|
||||
currentProject: {
|
||||
id: 'test-id',
|
||||
title: 'launchpad',
|
||||
__typename: 'CurrentProject',
|
||||
},
|
||||
__typename: 'Query',
|
||||
},
|
||||
},
|
||||
},
|
||||
delay: 500,
|
||||
}).as('launchProject')
|
||||
|
||||
cy.get('@launchButton').click()
|
||||
cy.contains('button', 'Opening E2E Testing in Chrome').should('be.visible')
|
||||
|
||||
cy.wait('@launchProject').then(({ request }) => {
|
||||
expect(request?.body.variables.testingType).to.eq('e2e')
|
||||
})
|
||||
|
||||
cy.intercept('query-OpenBrowser', (req) => {
|
||||
req.on('before:response', (res) => {
|
||||
res.body.data.currentProject.isBrowserOpen = true
|
||||
})
|
||||
})
|
||||
|
||||
cy.contains('button', 'Focus').as('focusButton')
|
||||
|
||||
cy.intercept('mutation-OpenBrowser_FocusActiveBrowserWindow').as('focusBrowser')
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
sinon.spy(ctx.actions.browser, 'focusActiveBrowserWindow')
|
||||
})
|
||||
|
||||
cy.get('@focusButton').click()
|
||||
|
||||
cy.wait('@focusBrowser').then(() => {
|
||||
cy.withCtx((ctx) => {
|
||||
expect(ctx.actions.browser.focusActiveBrowserWindow).to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('No System Browsers Detected', () => {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@navigated-back="backFn"
|
||||
@launch="launch"
|
||||
@close-browser="closeBrowserFn"
|
||||
@focus-browser="setFocusToActiveBrowserWindow"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
@@ -23,7 +24,7 @@
|
||||
import { useMutation, gql, useQuery } from '@urql/vue'
|
||||
import OpenBrowserList from './OpenBrowserList.vue'
|
||||
import WarningList from '../warning/WarningList.vue'
|
||||
import { OpenBrowserDocument, OpenBrowser_CloseBrowserDocument, OpenBrowser_ClearTestingTypeDocument, OpenBrowser_LaunchProjectDocument } from '../generated/graphql'
|
||||
import { OpenBrowserDocument, OpenBrowser_CloseBrowserDocument, OpenBrowser_ClearTestingTypeDocument, OpenBrowser_LaunchProjectDocument, OpenBrowser_FocusActiveBrowserWindowDocument } from '../generated/graphql'
|
||||
import LaunchpadHeader from './LaunchpadHeader.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -81,6 +82,12 @@ mutation OpenBrowser_CloseBrowser {
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation OpenBrowser_FocusActiveBrowserWindow {
|
||||
focusActiveBrowserWindow
|
||||
}
|
||||
`
|
||||
|
||||
const launchOpenProject = useMutation(OpenBrowser_LaunchProjectDocument)
|
||||
const clearCurrentTestingType = useMutation(OpenBrowser_ClearTestingTypeDocument)
|
||||
const closeBrowser = useMutation(OpenBrowser_CloseBrowserDocument)
|
||||
@@ -114,4 +121,11 @@ const isBrowserOpening = computed(() => !!launchOpenProject.fetching.value || la
|
||||
const headingDescription = computed(() => {
|
||||
return t('setupWizard.chooseBrowser.description', { testingType: query.data.value?.currentProject?.currentTestingType === 'component' ? 'component' : 'E2E' })
|
||||
})
|
||||
|
||||
const focusActiveBrowserWindow = useMutation(OpenBrowser_FocusActiveBrowserWindowDocument)
|
||||
|
||||
const setFocusToActiveBrowserWindow = () => {
|
||||
focusActiveBrowserWindow.executeMutation({})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -86,4 +86,28 @@ describe('<OpenBrowserList />', () => {
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('hides focus button when unsupported', () => {
|
||||
cy.mountFragment(OpenBrowserListFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.currentBrowser = longBrowsersList.find((browser) => !browser.isFocusSupported) || null
|
||||
},
|
||||
render: (gqlVal) => (
|
||||
<div class="border-current border-1 resize overflow-auto">
|
||||
<OpenBrowserList
|
||||
gql={gqlVal}
|
||||
isBrowserOpen={true}
|
||||
isBrowserOpening={false}
|
||||
onClose-browser={cy.stub().as('closeBrowser')}/>
|
||||
</div>),
|
||||
})
|
||||
|
||||
cy.get('[data-cy-browser]').each((browser) => cy.wrap(browser).should('have.attr', 'aria-disabled', 'true'))
|
||||
cy.contains('button', defaultMessages.openBrowser.running.replace('{browser}', 'Electron')).should('be.disabled')
|
||||
cy.contains('button', defaultMessages.openBrowser.focus).should('not.exist')
|
||||
cy.contains('button', defaultMessages.openBrowser.close).click()
|
||||
cy.get('@closeBrowser').should('have.been.called')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<RadioGroup
|
||||
v-model="selectedBrowserId"
|
||||
class="flex flex-wrap justify-center py-40px gap-24px"
|
||||
class="flex flex-wrap py-40px gap-24px justify-center"
|
||||
data-cy="open-browser-list"
|
||||
>
|
||||
<RadioGroupOption
|
||||
@@ -18,7 +18,7 @@
|
||||
>
|
||||
<RadioGroupLabel
|
||||
:for="browser.id"
|
||||
class="relative block pt-6 pb-4 text-center rounded radio-label border-1 min-h-144px w-160px"
|
||||
class="rounded border-1 text-center min-h-144px pt-6 pb-4 w-160px relative block radio-label"
|
||||
:class="{
|
||||
'border-jade-300 ring-2 ring-jade-100 focus:border-jade-400 focus:border-1 focus:outline-none': checked,
|
||||
'border-gray-200 before:hocus:cursor-pointer': !checked && !(isBrowserOpening || isBrowserOpen) ,
|
||||
@@ -31,7 +31,7 @@
|
||||
<img
|
||||
:src="allBrowsersIcons[browser.displayName]"
|
||||
alt=""
|
||||
class="inline h-40px w-40px"
|
||||
class="h-40px w-40px inline"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -50,7 +50,7 @@
|
||||
v-if="props.gql.currentTestingType"
|
||||
class="mb-14"
|
||||
>
|
||||
<div class="flex items-center justify-center mb-4 gap-16px">
|
||||
<div class="flex mb-4 gap-16px items-center justify-center">
|
||||
<template v-if="!isBrowserOpen">
|
||||
<Button
|
||||
v-if="!isBrowserOpening"
|
||||
@@ -91,12 +91,14 @@
|
||||
{{ browserText.running }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="props.gql.currentBrowser?.isFocusSupported"
|
||||
size="lg"
|
||||
type="button"
|
||||
variant="outline"
|
||||
:prefix-icon="ExportIcon"
|
||||
prefix-icon-class="icon-dark-gray-500"
|
||||
class="font-medium"
|
||||
@click="emit('focus-browser')"
|
||||
>
|
||||
{{ browserText.focus }}
|
||||
</Button>
|
||||
@@ -119,7 +121,7 @@
|
||||
variant="text"
|
||||
:prefix-icon="ArrowLeftIcon"
|
||||
prefix-icon-class="icon-dark-gray-500"
|
||||
class="mx-auto font-medium text-gray-600 hover:text-indigo-500"
|
||||
class="font-medium mx-auto text-gray-600 hover:text-indigo-500"
|
||||
@click="emit('navigated-back')"
|
||||
>
|
||||
{{ browserText.switchTestingType }}
|
||||
@@ -161,6 +163,7 @@ fragment OpenBrowserList on CurrentProject {
|
||||
id
|
||||
displayName
|
||||
path
|
||||
isFocusSupported
|
||||
}
|
||||
browsers {
|
||||
id
|
||||
@@ -188,9 +191,7 @@ const emit = defineEmits<{
|
||||
(e: 'navigated-back'): void
|
||||
(e: 'launch'): void
|
||||
(e: 'close-browser'): void
|
||||
// TODO: Add browser focus
|
||||
// see: https://cypress-io.atlassian.net/browse/UNIFY-953
|
||||
// (e: 'focus-browser'): void
|
||||
(e: 'focus-browser'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -322,6 +322,8 @@ export class CdpAutomation {
|
||||
.then(({ data }) => {
|
||||
return `data:image/png;base64,${data}`
|
||||
})
|
||||
case 'focus:browser:window':
|
||||
return this.sendDebuggerCommandFn('Page.bringToFront')
|
||||
default:
|
||||
throw new Error(`No automation handler registered for: '${message}'`)
|
||||
}
|
||||
|
||||
@@ -54,24 +54,33 @@ const _getAutomation = function (win, options, parent) {
|
||||
|
||||
const automation = new CdpAutomation(sendCommand, on, parent)
|
||||
|
||||
if (!options.onScreencastFrame) {
|
||||
// after upgrading to Electron 8, CDP screenshots can hang if a screencast is not also running
|
||||
// workaround: start and stop screencasts between screenshots
|
||||
// @see https://github.com/cypress-io/cypress/pull/6555#issuecomment-596747134
|
||||
automation.onRequest = _.wrap(automation.onRequest, async (fn, message, data) => {
|
||||
if (message !== 'take:screenshot') {
|
||||
automation.onRequest = _.wrap(automation.onRequest, async (fn, message, data) => {
|
||||
switch (message) {
|
||||
case 'take:screenshot': {
|
||||
// after upgrading to Electron 8, CDP screenshots can hang if a screencast is not also running
|
||||
// workaround: start and stop screencasts between screenshots
|
||||
// @see https://github.com/cypress-io/cypress/pull/6555#issuecomment-596747134
|
||||
if (!options.onScreencastFrame) {
|
||||
await sendCommand('Page.startScreencast', screencastOpts)
|
||||
const ret = await fn(message, data)
|
||||
|
||||
await sendCommand('Page.stopScreencast')
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
return fn(message, data)
|
||||
}
|
||||
case 'focus:browser:window': {
|
||||
win.show()
|
||||
|
||||
await sendCommand('Page.startScreencast', screencastOpts)
|
||||
|
||||
const ret = await fn(message, data)
|
||||
|
||||
await sendCommand('Page.stopScreencast')
|
||||
|
||||
return ret
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
default: {
|
||||
return fn(message, data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return automation
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ const Promise = require('bluebird')
|
||||
const debug = require('debug')('cypress:server:browsers')
|
||||
const utils = require('./utils')
|
||||
const check = require('check-more-types')
|
||||
const { exec } = require('child_process')
|
||||
const util = require('util')
|
||||
const os = require('os')
|
||||
|
||||
// returns true if the passed string is a known browser family name
|
||||
const isBrowserFamily = check.oneOf(['chromium', 'firefox'])
|
||||
@@ -40,6 +43,25 @@ const kill = function (unbind, isProcessExit) {
|
||||
})
|
||||
}
|
||||
|
||||
const setFocus = async function () {
|
||||
const platform = os.platform()
|
||||
const execAsync = util.promisify(exec)
|
||||
|
||||
try {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return execAsync(`open -a "$(ps -p ${instance.pid} -o comm=)"`)
|
||||
case 'win32': {
|
||||
return execAsync(`(New-Object -ComObject WScript.Shell).AppActivate(((Get-WmiObject -Class win32_process -Filter "ParentProcessID = '${instance.pid}'") | Select -ExpandProperty ProcessId))`, { shell: 'powershell.exe' })
|
||||
}
|
||||
default:
|
||||
debug(`Unexpected os platform ${platform}. Set focus is only functional on Windows and MacOS`)
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Failure to set focus. ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getBrowserLauncher = function (browser) {
|
||||
debug('getBrowserLauncher %o', { browser })
|
||||
if (!isBrowserFamily(browser.family)) {
|
||||
@@ -166,4 +188,5 @@ module.exports = {
|
||||
})
|
||||
})
|
||||
},
|
||||
setFocus,
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ const _internal = {
|
||||
/**
|
||||
* @returns a promise that is resolved with a user when auth is complete or rejected when it fails
|
||||
*/
|
||||
const start = (onMessage, utmCode) => {
|
||||
const start = (onMessage, utmCode, onLogin) => {
|
||||
function sendMessage (type, name, arg1) {
|
||||
onMessage({
|
||||
type,
|
||||
@@ -222,7 +222,7 @@ const start = (onMessage, utmCode) => {
|
||||
})
|
||||
.finally(() => {
|
||||
_internal.stopServer()
|
||||
require('./windows').focusMainWindow()
|
||||
onLogin()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,10 @@ export function hideAllUnlessAnotherWindowIsFocused () {
|
||||
return _.invoke(windows, 'hide')
|
||||
}
|
||||
|
||||
export function isMainWindowFocused () {
|
||||
return getByType('INDEX').isFocused()
|
||||
}
|
||||
|
||||
export function focusMainWindow () {
|
||||
return getByType('INDEX').show()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
|
||||
return await ensureAndGetByNameOrPath(nameOrPath, false, browsers)
|
||||
},
|
||||
async focusActiveBrowserWindow () {
|
||||
return openProject.projectBase?.sendFocusBrowserMessage()
|
||||
},
|
||||
},
|
||||
errorApi: {
|
||||
error: errors.get,
|
||||
@@ -77,7 +80,17 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
return user.get()
|
||||
},
|
||||
logIn (onMessage) {
|
||||
return auth.start(onMessage, 'launchpad')
|
||||
const windows = require('./gui/windows')
|
||||
const originalIsMainWindowFocused = windows.isMainWindowFocused()
|
||||
const onLogin = async () => {
|
||||
if (originalIsMainWindowFocused || !ctx.browser.isFocusSupported(ctx.coreData.chosenBrowser)) {
|
||||
windows.focusMainWindow()
|
||||
} else {
|
||||
await ctx.actions.browser.focusActiveBrowserWindow()
|
||||
}
|
||||
}
|
||||
|
||||
return auth.start(onMessage, 'launchpad', onLogin)
|
||||
},
|
||||
logOut () {
|
||||
return user.logOut()
|
||||
|
||||
@@ -474,6 +474,14 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
this.server.changeToUrl(url)
|
||||
}
|
||||
|
||||
async sendFocusBrowserMessage () {
|
||||
if (this.browser.family === 'firefox') {
|
||||
await browsers.setFocus()
|
||||
} else {
|
||||
await this.server.sendFocusBrowserMessage()
|
||||
}
|
||||
}
|
||||
|
||||
shouldCorrelatePreRequests = () => {
|
||||
if (!this.browser) {
|
||||
return false
|
||||
|
||||
@@ -577,6 +577,10 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
return this._socket && this._socket.changeToUrl(url)
|
||||
}
|
||||
|
||||
async sendFocusBrowserMessage () {
|
||||
this._socket && await this._socket.sendFocusBrowserMessage()
|
||||
}
|
||||
|
||||
onRequest (fn) {
|
||||
this._middleware = fn
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ const retry = (fn: (res: any) => void) => {
|
||||
}
|
||||
|
||||
export class SocketBase {
|
||||
private _sendFocusBrowserMessage
|
||||
|
||||
protected ended: boolean
|
||||
protected _io?: socketIo.SocketIOServer
|
||||
|
||||
@@ -273,6 +275,10 @@ export class SocketBase {
|
||||
})
|
||||
})
|
||||
|
||||
this._sendFocusBrowserMessage = async () => {
|
||||
await automationRequest('focus:browser:window', {})
|
||||
}
|
||||
|
||||
socket.on('reporter:connected', () => {
|
||||
if (socket.inReporterRoom) {
|
||||
return
|
||||
@@ -538,6 +544,10 @@ export class SocketBase {
|
||||
return this.toRunner('change:to:url', url)
|
||||
}
|
||||
|
||||
async sendFocusBrowserMessage () {
|
||||
await this._sendFocusBrowserMessage()
|
||||
}
|
||||
|
||||
close () {
|
||||
return this._io?.close()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
require('../../spec_helper')
|
||||
|
||||
const os = require('os')
|
||||
const browsers = require(`../../../lib/browsers`)
|
||||
const utils = require(`../../../lib/browsers/utils`)
|
||||
const snapshot = require('snap-shot-it')
|
||||
const { EventEmitter } = require('events')
|
||||
const { sinon } = require('../../spec_helper')
|
||||
const { exec } = require('child_process')
|
||||
const util = require('util')
|
||||
|
||||
const normalizeBrowsers = (message) => {
|
||||
return message.replace(/(found on your system are:)(?:\n- .*)*/, '$1\n- chrome\n- firefox\n- electron')
|
||||
@@ -160,6 +164,40 @@ describe('lib/browsers/index', () => {
|
||||
expect(utils.getMajorVersion(vers)).to.eq(vers)
|
||||
})
|
||||
})
|
||||
|
||||
context('setFocus', () => {
|
||||
it('calls open when running MacOS', () => {
|
||||
const mockExec = sinon.stub()
|
||||
|
||||
sinon.stub(os, 'platform').returns('darwin')
|
||||
sinon.stub(util, 'promisify').returns(mockExec)
|
||||
|
||||
browsers._setInstance({
|
||||
pid: 3333,
|
||||
})
|
||||
|
||||
browsers.setFocus()
|
||||
|
||||
expect(util.promisify).to.be.calledWith(exec)
|
||||
expect(mockExec).to.be.calledWith(`open -a "$(ps -p 3333 -o comm=)"`)
|
||||
})
|
||||
|
||||
it('calls WScript AppActivate to activate the window when running Windows', () => {
|
||||
const mockExec = sinon.stub()
|
||||
|
||||
sinon.stub(os, 'platform').returns('win32')
|
||||
sinon.stub(util, 'promisify').returns(mockExec)
|
||||
|
||||
browsers._setInstance({
|
||||
pid: 3333,
|
||||
})
|
||||
|
||||
browsers.setFocus()
|
||||
|
||||
expect(util.promisify).to.be.calledWith(exec)
|
||||
expect(mockExec).to.be.calledWith(`(New-Object -ComObject WScript.Shell).AppActivate(((Get-WmiObject -Class win32_process -Filter "ParentProcessID = '3333'") | Select -ExpandProperty ProcessId))`, { shell: 'powershell.exe' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Ooo, browser clean up tests are disabled?!!
|
||||
|
||||
@@ -218,5 +218,13 @@ context('lib/browsers/cdp_automation', () => {
|
||||
.to.be.rejectedWith('The browser responded with an error when Cypress attempted to take a screenshot.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('focus:browser:window', function () {
|
||||
it('sends Page.bringToFront when focus is requested', function () {
|
||||
this.sendDebuggerCommand.withArgs('Page.bringToFront').resolves()
|
||||
|
||||
return this.onRequest('focus:browser:window').then((resp) => expect(resp).to.be.undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -289,6 +289,7 @@ describe('lib/browsers/electron', () => {
|
||||
this.newWin = {
|
||||
maximize: sinon.stub(),
|
||||
setSize: sinon.stub(),
|
||||
show: sinon.stub(),
|
||||
webContents: this.win.webContents,
|
||||
}
|
||||
|
||||
@@ -339,14 +340,19 @@ describe('lib/browsers/electron', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('registers onRequest automation middleware', function () {
|
||||
it('registers onRequest automation middleware and calls show when requesting to be focused', function () {
|
||||
sinon.spy(this.automation, 'use')
|
||||
|
||||
return electron._render(this.url, this.automation, this.preferences, this.options)
|
||||
electron._render(this.url, this.automation, this.preferences, this.options)
|
||||
.then(() => {
|
||||
expect(Windows.create).to.be.calledWith(this.options.projectRoot, this.options)
|
||||
|
||||
expect(this.automation.use).to.be.called
|
||||
expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function')
|
||||
|
||||
this.automation.use.lastCall.args[0].onRequest('focus:browser:window')
|
||||
|
||||
expect(this.newWin.show).to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -132,7 +132,9 @@ describe('lib/gui/auth', function () {
|
||||
sinon.stub(auth._internal, 'stopServer')
|
||||
sinon.stub(windows, 'focusMainWindow').callsFake(() => {})
|
||||
|
||||
await auth.start(() => {}, 'code')
|
||||
await auth.start(() => {}, 'code', () => {
|
||||
windows.focusMainWindow()
|
||||
})
|
||||
|
||||
expect(auth._internal.stopServer).to.be.calledOnce
|
||||
expect(windows.focusMainWindow).to.be.calledOnce
|
||||
@@ -144,7 +146,9 @@ describe('lib/gui/auth', function () {
|
||||
sinon.stub(windows, 'focusMainWindow').callsFake(() => {})
|
||||
|
||||
try {
|
||||
await auth.start(() => {}, 'code')
|
||||
await auth.start(() => {}, 'code', () => {
|
||||
windows.focusMainWindow()
|
||||
})
|
||||
} catch (e) {
|
||||
expect(e.message).to.eql('test error')
|
||||
}
|
||||
|
||||
@@ -601,6 +601,16 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('#sendFocusBrowserMessage', function () {
|
||||
it('sends an automation request of focus:browser:window', function () {
|
||||
sinon.stub(this.automation, 'request')
|
||||
|
||||
this.socket.sendFocusBrowserMessage()
|
||||
|
||||
expect(this.automation.request).to.be.calledWith('focus:browser:window', {})
|
||||
})
|
||||
})
|
||||
|
||||
context('#close', () => {
|
||||
it('calls close on #io', function () {
|
||||
this.socket.close()
|
||||
|
||||
Reference in New Issue
Block a user