From 56626c62a8b6a81ca16bfe7eccdf93d8d6c8cf25 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 27 Jan 2022 18:04:15 +0000 Subject: [PATCH] feat: Focus browser from select browser screen and on dashboard login (#19842) Co-authored-by: Zachary Williams --- .../src/actions/BrowserActions.ts | 4 ++ .../src/sources/BrowserDataSource.ts | 27 +++++++++ .../cypress/e2e/e2ePluginSetup.ts | 7 +-- .../cypress/e2e/support/e2eSupport.ts | 8 ++- .../support/mock-graphql/longBrowsersList.ts | 26 +++++++++ .../support/mock-graphql/stubgql-Mutation.ts | 3 + packages/graphql/schemas/schema.graphql | 4 ++ .../schemaTypes/objectTypes/gql-Browser.ts | 3 + .../schemaTypes/objectTypes/gql-Mutation.ts | 10 ++++ .../cypress/e2e/choose-a-browser.cy.ts | 57 +++++++++++++++++++ packages/launchpad/src/setup/OpenBrowser.vue | 16 +++++- .../src/setup/OpenBrowserList.cy.tsx | 24 ++++++++ .../launchpad/src/setup/OpenBrowserList.vue | 17 +++--- .../server/lib/browsers/cdp_automation.ts | 2 + packages/server/lib/browsers/electron.js | 39 ++++++++----- packages/server/lib/browsers/index.js | 23 ++++++++ packages/server/lib/gui/auth.ts | 4 +- packages/server/lib/gui/windows.ts | 4 ++ packages/server/lib/makeDataContext.ts | 15 ++++- packages/server/lib/project-base.ts | 8 +++ packages/server/lib/server-base.ts | 4 ++ packages/server/lib/socket-base.ts | 10 ++++ .../test/unit/browsers/browsers_spec.js | 38 +++++++++++++ .../test/unit/browsers/cdp_automation_spec.ts | 8 +++ .../test/unit/browsers/electron_spec.js | 10 +++- packages/server/test/unit/gui/auth_spec.js | 8 ++- packages/server/test/unit/socket_spec.js | 10 ++++ 27 files changed, 351 insertions(+), 38 deletions(-) diff --git a/packages/data-context/src/actions/BrowserActions.ts b/packages/data-context/src/actions/BrowserActions.ts index 01dc37c40310..a7eb94eb5327 100644 --- a/packages/data-context/src/actions/BrowserActions.ts +++ b/packages/data-context/src/actions/BrowserActions.ts @@ -10,4 +10,8 @@ export class BrowserActions { closeBrowser () { return this.browserApi.close() } + + async focusActiveBrowserWindow () { + await this.browserApi.focusActiveBrowserWindow() + } } diff --git a/packages/data-context/src/sources/BrowserDataSource.ts b/packages/data-context/src/sources/BrowserDataSource.ts index 384710878863..5593c4725fb0 100644 --- a/packages/data-context/src/sources/BrowserDataSource.ts +++ b/packages/data-context/src/sources/BrowserDataSource.ts @@ -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 ensureAndGetByNameOrPath(nameOrPath: string): Promise getBrowsers(): Promise + focusActiveBrowserWindow(): Promise } 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 + } } diff --git a/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts b/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts index 63db90ae6d76..433080d02265 100644 --- a/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts +++ b/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts @@ -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 { + async __internal_openProject ({ argv, projectName }: InternalOpenProjectArgs): Promise { 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, diff --git a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts index 8a3c2b3d2169..223d17614802 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts @@ -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) => { diff --git a/packages/frontend-shared/cypress/support/mock-graphql/longBrowsersList.ts b/packages/frontend-shared/cypress/support/mock-graphql/longBrowsersList.ts index 036cf9a7e710..b00390cd1455 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/longBrowsersList.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/longBrowsersList.ts @@ -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 diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts index cf09734baa28..e6cdec4261b0 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts @@ -32,6 +32,9 @@ export const stubMutation: MaybeResolver = { return {} }, + focusActiveBrowserWindow (sourc, args, ctx) { + return true + }, hideBrowserWindow (source, args, ctx) { return true }, diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 3c0172326e3d..8d34dad29a24 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -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 diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Browser.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Browser.ts index 7edf4b2b1ae2..7e7b7acafaf6 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Browser.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Browser.ts @@ -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', diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index e57ff1ff11f0..5f7b620bc74a 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -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', diff --git a/packages/launchpad/cypress/e2e/choose-a-browser.cy.ts b/packages/launchpad/cypress/e2e/choose-a-browser.cy.ts index 8aa16494c8cd..51addabf9b4a 100644 --- a/packages/launchpad/cypress/e2e/choose-a-browser.cy.ts +++ b/packages/launchpad/cypress/e2e/choose-a-browser.cy.ts @@ -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', () => { diff --git a/packages/launchpad/src/setup/OpenBrowser.vue b/packages/launchpad/src/setup/OpenBrowser.vue index 03e288682b65..e7af716ebc6a 100644 --- a/packages/launchpad/src/setup/OpenBrowser.vue +++ b/packages/launchpad/src/setup/OpenBrowser.vue @@ -15,6 +15,7 @@ @navigated-back="backFn" @launch="launch" @close-browser="closeBrowserFn" + @focus-browser="setFocusToActiveBrowserWindow" /> @@ -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({}) +} + diff --git a/packages/launchpad/src/setup/OpenBrowserList.cy.tsx b/packages/launchpad/src/setup/OpenBrowserList.cy.tsx index 8f1dcfa90294..f2c43071391e 100644 --- a/packages/launchpad/src/setup/OpenBrowserList.cy.tsx +++ b/packages/launchpad/src/setup/OpenBrowserList.cy.tsx @@ -86,4 +86,28 @@ describe('', () => { cy.percySnapshot() }) + + it('hides focus button when unsupported', () => { + cy.mountFragment(OpenBrowserListFragmentDoc, { + onResult: (result) => { + result.currentBrowser = longBrowsersList.find((browser) => !browser.isFocusSupported) || null + }, + render: (gqlVal) => ( +
+ +
), + }) + + 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() + }) }) diff --git a/packages/launchpad/src/setup/OpenBrowserList.vue b/packages/launchpad/src/setup/OpenBrowserList.vue index c4ddd2eb93fa..7a59b0f99a71 100644 --- a/packages/launchpad/src/setup/OpenBrowserList.vue +++ b/packages/launchpad/src/setup/OpenBrowserList.vue @@ -5,7 +5,7 @@ >
-
+