diff --git a/packages/app/cypress/e2e/integration/settings.spec.ts b/packages/app/cypress/e2e/integration/settings.spec.ts index b9ad02baaa90..4a92cc3e5b85 100644 --- a/packages/app/cypress/e2e/integration/settings.spec.ts +++ b/packages/app/cypress/e2e/integration/settings.spec.ts @@ -1,4 +1,4 @@ -describe('Settings', { viewportWidth: 1200 }, () => { +describe('Settings', { viewportWidth: 600 }, () => { beforeEach(() => { cy.setupE2E('component-tests') @@ -38,4 +38,18 @@ describe('Settings', { viewportWidth: 1200 }, () => { cy.findByText('Reconfigure Project').click() cy.wait('@ReconfigureProject') }) + + it('selects well known editor', () => { + cy.visitApp() + cy.get('[href="#/settings"]').click() + cy.contains('Device Settings').click() + cy.findByPlaceholderText('Custom path...').clear().type('/usr/local/bin/vim') + + cy.intercept('POST', 'mutation-SetPreferredEditorBinary', (req) => { + expect(req.body.variables).to.eql({ 'value': '/usr/local/bin/vim' }) + }).as('SetPreferred') + + cy.get('[data-cy="use-custom-editor"]').click() + cy.wait('@SetPreferred') + }) }) diff --git a/packages/app/src/settings/SettingsContainer.vue b/packages/app/src/settings/SettingsContainer.vue index 15ad88923d98..03bcbffce90a 100644 --- a/packages/app/src/settings/SettingsContainer.vue +++ b/packages/app/src/settings/SettingsContainer.vue @@ -9,7 +9,9 @@ :icon="IconLaptop" max-height="800px" > - + + + ', () => { - it('renders', () => { - cy.mount(() => ) - }) -}) diff --git a/packages/app/src/settings/device/DeviceSettings.vue b/packages/app/src/settings/device/DeviceSettings.vue deleted file mode 100644 index f95dd2ede27e..000000000000 --- a/packages/app/src/settings/device/DeviceSettings.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/packages/app/src/settings/device/ExternalEditorSettings.spec.tsx b/packages/app/src/settings/device/ExternalEditorSettings.spec.tsx index e46ec06c7291..828c9bfc449e 100644 --- a/packages/app/src/settings/device/ExternalEditorSettings.spec.tsx +++ b/packages/app/src/settings/device/ExternalEditorSettings.spec.tsx @@ -1,34 +1,60 @@ import ExternalEditorSettings from './ExternalEditorSettings.vue' import { defaultMessages } from '@cy/i18n' +import { ExternalEditorSettingsFragmentDoc } from '../../generated/graphql-test' const editorText = defaultMessages.settingsPage.editor describe('', () => { - beforeEach(() => { - cy.mount(() => ) - }) - it('renders the placeholder by default', () => { + cy.mountFragment(ExternalEditorSettingsFragmentDoc, { + render: (gqlVal) => { + return + }, + }) + cy.findByText(editorText.noEditorSelectedPlaceholder).should('be.visible') }) it('renders the title and description', () => { + cy.mountFragment(ExternalEditorSettingsFragmentDoc, { + render: (gqlVal) => { + return + }, + }) + cy.findByText(editorText.description).should('be.visible') cy.findByText(editorText.title).should('be.visible') }) it('can select an editor', () => { + cy.mountFragment(ExternalEditorSettingsFragmentDoc, { + render: (gqlVal) => { + return + }, + }) + const optionsSelector = '[role=option]' const inputSelector = '[aria-haspopup=true]' cy.get(inputSelector).click() .get(optionsSelector).should('be.visible') .get(optionsSelector).then(($options) => { - const text = $options.first().text() - cy.wrap($options.first()).click() - .get(optionsSelector).should('not.exist') - .get(inputSelector).should('have.text', text) }) }) + + it('can input a custom binary', () => { + cy.mountFragment(ExternalEditorSettingsFragmentDoc, { + render: (gqlVal) => { + return + }, + }) + + cy.findByPlaceholderText('Custom path...').type('/usr/bin') + cy.get('[data-cy="use-custom-editor"]').as('custom') + cy.get('@custom').click() + + cy.get('@custom').should('be.focused') + cy.get('[data-cy="use-well-known-editor"]').should('not.be.focused') + }) }) diff --git a/packages/app/src/settings/device/ExternalEditorSettings.vue b/packages/app/src/settings/device/ExternalEditorSettings.vue index 5f479fac986f..540faff1d84c 100644 --- a/packages/app/src/settings/device/ExternalEditorSettings.vue +++ b/packages/app/src/settings/device/ExternalEditorSettings.vue @@ -6,42 +6,81 @@ - + +
+ + + +
+ +
+ + +
+ + + +
+
diff --git a/packages/app/src/settings/device/ProxySettings.spec.tsx b/packages/app/src/settings/device/ProxySettings.spec.tsx index 8a9f8dd442f1..04e221614b5f 100644 --- a/packages/app/src/settings/device/ProxySettings.spec.tsx +++ b/packages/app/src/settings/device/ProxySettings.spec.tsx @@ -1,12 +1,21 @@ +import { ProxySettingsFragmentDoc } from '../../generated/graphql-test' import ProxySettings from './ProxySettings.vue' describe('', () => { it('renders', () => { cy.viewport(400, 400) - .mount(() => ).get('body') - .findByText('Proxy Server').get('body') - .findByText('Proxy Bypass List') - .get('[data-testid=bypass-list]').should('be.visible') - .get('[data-testid=proxy-server]').should('be.visible') + .mountFragment(ProxySettingsFragmentDoc, { + onResult: (ctx) => { + ctx.localSettings.preferences.proxyServer = 'proxy-server' + ctx.localSettings.preferences.proxyBypass = 'proxy-bypass' + }, + render: (gql) => , + }) + + cy.findByText('Proxy Bypass List') + .get('[data-testid=bypass-list]').contains('proxy-bypass') + + cy.findByText('Proxy Server') + .get('[data-testid=proxy-server]').contains('proxy-server') }) }) diff --git a/packages/app/src/settings/device/ProxySettings.vue b/packages/app/src/settings/device/ProxySettings.vue index 019dfa232510..245de538fb4c 100644 --- a/packages/app/src/settings/device/ProxySettings.vue +++ b/packages/app/src/settings/device/ProxySettings.vue @@ -12,14 +12,14 @@ {{ proxyServer }} + >{{ props.gql.localSettings.preferences.proxyServer || '-' }}
{{ t('settingsPage.proxy.bypassList') }} {{ bypassList }} + >{{ props.gql.localSettings.preferences.proxyBypass || '-' }}
@@ -28,10 +28,23 @@ diff --git a/packages/app/src/settings/device/TestingPreferences.spec.tsx b/packages/app/src/settings/device/TestingPreferences.spec.tsx index 2d48d3430899..009a5c568984 100644 --- a/packages/app/src/settings/device/TestingPreferences.spec.tsx +++ b/packages/app/src/settings/device/TestingPreferences.spec.tsx @@ -1,7 +1,10 @@ +import { TestingPreferencesFragmentDoc } from '../../generated/graphql-test' import TestingPreferences from './TestingPreferences.vue' describe('', () => { it('renders', () => { - cy.mount(() =>
) + cy.mountFragment(TestingPreferencesFragmentDoc, { + render: (gql) => , + }) }) }) diff --git a/packages/app/src/settings/device/TestingPreferences.vue b/packages/app/src/settings/device/TestingPreferences.vue index 5e6fcabcefb2..0a13c3d3fc3d 100644 --- a/packages/app/src/settings/device/TestingPreferences.vue +++ b/packages/app/src/settings/device/TestingPreferences.vue @@ -11,14 +11,16 @@ >

- {{ pref.title }}

@@ -33,15 +35,65 @@ import SettingsSection from '../SettingsSection.vue' import { useI18n } from '@cy/i18n' import Switch from '@packages/frontend-shared/src/components/Switch.vue' +import { gql, useMutation } from '@urql/vue' +import { + SetAutoScrollingEnabledDocument, + SetUseDarkSidebarDocument, + SetWatchForSpecChangeDocument, +} from '@packages/data-context/src/gen/all-operations.gen' +import type { TestingPreferencesFragment } from '../../generated/graphql' const { t } = useI18n() +gql` +fragment TestingPreferences on Query { + localSettings { + preferences { + autoScrollingEnabled + useDarkSidebar + watchForSpecChange + } + } +} +` + +gql` +mutation SetAutoScrollingEnabled($value: Boolean!) { + setAutoScrollingEnabled(value: $value) +}` + +gql` +mutation SetUseDarkSidebar($value: Boolean!) { + setUseDarkSidebar(value: $value) +}` + +gql` +mutation SetWatchForSpecChange($value: Boolean!) { + setWatchForSpecChange(value: $value) +}` + const prefs = [ { - title: 'Auto-scrolling', - enabled: true, - description: 'Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + id: 'autoScrollingEnabled', + title: t('settingsPage.testingPreferences.autoScrollingEnabled.title'), + mutation: useMutation(SetAutoScrollingEnabledDocument), + description: t('settingsPage.testingPreferences.autoScrollingEnabled.description'), + }, + { + id: 'useDarkSidebar', + title: t('settingsPage.testingPreferences.useDarkSidebar.title'), + mutation: useMutation(SetUseDarkSidebarDocument), + description: t('settingsPage.testingPreferences.useDarkSidebar.description'), + }, + { + id: 'watchForSpecChange', + title: t('settingsPage.testingPreferences.watchForSpecChange.title'), + mutation: useMutation(SetWatchForSpecChangeDocument), + description: t('settingsPage.testingPreferences.watchForSpecChange.description'), }, -] +] as const +const props = defineProps<{ + gql: TestingPreferencesFragment +}>() diff --git a/packages/data-context/src/DataActions.ts b/packages/data-context/src/DataActions.ts index d0386fdade2f..4b6769aeb35d 100644 --- a/packages/data-context/src/DataActions.ts +++ b/packages/data-context/src/DataActions.ts @@ -1,5 +1,13 @@ import type { DataContext } from '.' -import { AppActions, ProjectConfigDataActions, ElectronActions, FileActions, ProjectActions, WizardActions } from './actions' +import { + LocalSettingsActions, + AppActions, + ProjectConfigDataActions, + ElectronActions, + FileActions, + ProjectActions, + WizardActions, +} from './actions' import { AuthActions } from './actions/AuthActions' import { DevActions } from './actions/DevActions' import { cached } from './util' @@ -27,6 +35,11 @@ export class DataActions { return new AuthActions(this.ctx) } + @cached + get localSettings () { + return new LocalSettingsActions(this.ctx) + } + @cached get wizard () { return new WizardActions(this.ctx) diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts index 0457b29387b5..cce292d10c4b 100644 --- a/packages/data-context/src/DataContext.ts +++ b/packages/data-context/src/DataContext.ts @@ -2,7 +2,7 @@ import type { LaunchArgs, OpenProjectLaunchOptions, PlatformName } from '@packag import fsExtra from 'fs-extra' import path from 'path' -import { AppApiShape, DataEmitterActions, ProjectApiShape } from './actions' +import { AppApiShape, DataEmitterActions, LocalSettingsApiShape, ProjectApiShape } from './actions' import type { NexusGenAbstractTypeMembers } from '@packages/graphql/src/gen/nxs.gen' import type { AuthApiShape } from './actions/AuthActions' import type { ElectronApiShape } from './actions/ElectronActions' @@ -51,6 +51,7 @@ export interface DataContextConfig { * Injected from the server */ appApi: AppApiShape + localSettingsApi: LocalSettingsApiShape authApi: AuthApiShape projectApi: ProjectApiShape electronApi: ElectronApiShape @@ -80,6 +81,10 @@ export class DataContext { return this._config.electronApi } + get localSettingsApi () { + return this._config.localSettingsApi + } + get isGlobalMode () { return !this.currentProject } @@ -91,7 +96,8 @@ export class DataContext { this.actions.app.refreshBrowsers(), // load the cached user & validate the token on start this.actions.auth.getUser(), - + // and grab the user device settings + this.actions.localSettings.refreshLocalSettings(), this.actions.app.refreshNodePathAndVersion(), ] @@ -293,6 +299,7 @@ export class DataContext { authApi: this._config.authApi, projectApi: this._config.projectApi, electronApi: this._config.electronApi, + localSettingsApi: this._config.localSettingsApi, busApi: this._rootBus, } } diff --git a/packages/data-context/src/actions/LocalSettingsActions.ts b/packages/data-context/src/actions/LocalSettingsActions.ts new file mode 100644 index 000000000000..d09592fc0c7f --- /dev/null +++ b/packages/data-context/src/actions/LocalSettingsActions.ts @@ -0,0 +1,42 @@ +import type { DevicePreferences, Editor } from '@packages/types' +import pDefer from 'p-defer' + +import type { DataContext } from '..' + +export interface LocalSettingsApiShape { + setPreferredOpener(editor: Editor): Promise + getAvailableEditors(): Promise + + getPreferences (): Promise + setDevicePreference (key: K, value: DevicePreferences[K]): Promise +} + +export class LocalSettingsActions { + constructor (private ctx: DataContext) {} + + setDevicePreference (key: K, value: DevicePreferences[K]) { + // update local data + this.ctx.coreData.localSettings.preferences[key] = value + + // persist to appData + return this.ctx._apis.localSettingsApi.setDevicePreference(key, value) + } + + async refreshLocalSettings () { + if (this.ctx.coreData.localSettings?.refreshing) { + return + } + + const dfd = pDefer() + + this.ctx.coreData.localSettings.refreshing = dfd.promise + + // TODO(tim): global unhandled error concept + const availableEditors = await this.ctx._apis.localSettingsApi.getAvailableEditors() + + this.ctx.coreData.localSettings.availableEditors = availableEditors + this.ctx.coreData.localSettings.preferences = await this.ctx._apis.localSettingsApi.getPreferences() + + dfd.resolve(availableEditors) + } +} diff --git a/packages/data-context/src/actions/index.ts b/packages/data-context/src/actions/index.ts index 4fa5984c63cb..07648d70af37 100644 --- a/packages/data-context/src/actions/index.ts +++ b/packages/data-context/src/actions/index.ts @@ -7,6 +7,7 @@ export * from './DataEmitterActions' export * from './DevActions' export * from './ElectronActions' export * from './FileActions' +export * from './LocalSettingsActions' export * from './ProjectActions' export * from './ProjectConfigDataActions' export * from './WizardActions' diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index e1d00e4ef86a..f84c507039c3 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -1,4 +1,4 @@ -import { BUNDLERS, FoundBrowser, FoundSpec, FullConfig, Preferences, NodePathAndVersion } from '@packages/types' +import { BUNDLERS, FoundBrowser, FoundSpec, FullConfig, Preferences, NodePathAndVersion, DevicePreferences, devicePreferenceDefaults, Editor } from '@packages/types' import type { NexusGenEnums, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen' import type { BrowserWindow } from 'electron' import type { ChildProcess } from 'child_process' @@ -19,6 +19,12 @@ export interface DevStateShape { refreshState: null | string } +export interface LocalSettingsDataShape { + refreshing: Promise | null + availableEditors: Editor[] + preferences: DevicePreferences +} + export interface ConfigChildProcessShape { /** * Child process executing the config & sourcing plugin events @@ -43,7 +49,7 @@ export interface ActiveProjectShape extends ProjectShape { specs?: FoundSpec[] config: Promise | null configChildProcess?: ConfigChildProcessShape | null - preferences?: Preferences| null + preferences?: Preferences | null browsers: FoundBrowser[] | null } @@ -82,6 +88,7 @@ export interface BaseErrorDataShape { export interface CoreDataShape { baseError: BaseErrorDataShape | null dev: DevStateShape + localSettings: LocalSettingsDataShape app: AppDataShape currentProject: ActiveProjectShape | null wizard: WizardDataShape @@ -107,6 +114,11 @@ export function makeCoreData (): CoreDataShape { refreshingNodePathAndVersion: null, nodePathAndVersion: null, }, + localSettings: { + availableEditors: [], + preferences: devicePreferenceDefaults, + refreshing: null, + }, isAuthBrowserOpened: false, currentProject: null, wizard: { diff --git a/packages/data-context/src/sources/EnvDataSource.ts b/packages/data-context/src/sources/EnvDataSource.ts index 55f7c310b49c..00e2f2528a8e 100644 --- a/packages/data-context/src/sources/EnvDataSource.ts +++ b/packages/data-context/src/sources/EnvDataSource.ts @@ -6,4 +6,12 @@ import type { DataContext } from '../DataContext' */ export class EnvDataSource { constructor (private ctx: DataContext) {} + + get HTTP_PROXY () { + return process.env.HTTPS_PROXY || process.env.HTTP_PROXY + } + + get NO_PROXY () { + return process.env.NO_PROXY + } } diff --git a/packages/data-context/src/util/urqlCacheKeys.ts b/packages/data-context/src/util/urqlCacheKeys.ts index f4f322116f70..4382c8fe3861 100644 --- a/packages/data-context/src/util/urqlCacheKeys.ts +++ b/packages/data-context/src/util/urqlCacheKeys.ts @@ -17,5 +17,7 @@ export const urqlCacheKeys: Partial = { BaseError: () => null, ProjectPreferences: (data) => data.__typename, VersionData: () => null, + LocalSettings: (data) => data.__typename, + LocalSettingsPreferences: () => null, }, } diff --git a/packages/desktop-gui/cypress/integration/settings_spec.js b/packages/desktop-gui/cypress/integration/settings_spec.js index 716b4b151af1..b138152bbb4c 100644 --- a/packages/desktop-gui/cypress/integration/settings_spec.js +++ b/packages/desktop-gui/cypress/integration/settings_spec.js @@ -782,11 +782,11 @@ describe('Settings', () => { describe('file preference panel', () => { const availableEditors = [ - { id: 'atom', name: 'Atom', isOther: false, openerId: 'atom' }, - { id: 'vim', name: 'Vim', isOther: false, openerId: 'vim' }, - { id: 'sublime', name: 'Sublime Text', isOther: false, openerId: 'sublime' }, - { id: 'vscode', name: 'Visual Studio Code', isOther: false, openerId: 'vscode' }, - { id: 'other', name: 'Other', isOther: true, openerId: '' }, + { id: 'atom', name: 'Atom', isOther: false, binary: 'atom' }, + { id: 'vim', name: 'Vim', isOther: false, binary: 'vim' }, + { id: 'sublime', name: 'Sublime Text', isOther: false, binary: 'sublime' }, + { id: 'vscode', name: 'Visual Studio Code', isOther: false, binary: 'vscode' }, + { id: 'other', name: 'Other', isOther: true, binary: '' }, ] beforeEach(function () { diff --git a/packages/desktop-gui/cypress/integration/specs_list_spec.js b/packages/desktop-gui/cypress/integration/specs_list_spec.js index 212157271ba9..92fbb4dead23 100644 --- a/packages/desktop-gui/cypress/integration/specs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/specs_list_spec.js @@ -904,12 +904,12 @@ describe('Specs List', function () { describe('opens files', function () { beforeEach(function () { this.availableEditors = [ - { id: 'computer', name: 'On Computer', isOther: false, openerId: 'computer' }, - { id: 'atom', name: 'Atom', isOther: false, openerId: 'atom' }, - { id: 'vim', name: 'Vim', isOther: false, openerId: 'vim' }, - { id: 'sublime', name: 'Sublime Text', isOther: false, openerId: 'sublime' }, - { id: 'vscode', name: 'Visual Studio Code', isOther: false, openerId: 'vscode' }, - { id: 'other', name: 'Other', isOther: true, openerId: '' }, + { id: 'computer', name: 'On Computer', isOther: false, binary: 'computer' }, + { id: 'atom', name: 'Atom', isOther: false, binary: 'atom' }, + { id: 'vim', name: 'Vim', isOther: false, binary: 'vim' }, + { id: 'sublime', name: 'Sublime Text', isOther: false, binary: 'sublime' }, + { id: 'vscode', name: 'Visual Studio Code', isOther: false, binary: 'vscode' }, + { id: 'other', name: 'Other', isOther: true, binary: '' }, ] cy.get('@spec').realHover() diff --git a/packages/desktop-gui/src/settings/file-preference.jsx b/packages/desktop-gui/src/settings/file-preference.jsx index 066502a1d49c..27a0b535036d 100644 --- a/packages/desktop-gui/src/settings/file-preference.jsx +++ b/packages/desktop-gui/src/settings/file-preference.jsx @@ -39,7 +39,7 @@ const FilePreference = observer(() => { setOtherPath: action((otherPath) => { const otherOption = _.find(state.editors, { isOther: true }) - otherOption.openerId = otherPath + otherOption.binary = otherPath save(otherOption) }), })) diff --git a/packages/desktop-gui/src/settings/file-preference_spec.jsx b/packages/desktop-gui/src/settings/file-preference_spec.jsx index eddeb9eeab74..c787cff42198 100644 --- a/packages/desktop-gui/src/settings/file-preference_spec.jsx +++ b/packages/desktop-gui/src/settings/file-preference_spec.jsx @@ -8,11 +8,11 @@ import '../main.scss' /* global cy, Cypress */ describe('FilePreference', () => { const availableEditors = [ - { id: 'atom', name: 'Atom', isOther: false, openerId: 'atom' }, - { id: 'vim', name: 'Vim', isOther: false, openerId: 'vim' }, - { id: 'sublime', name: 'Sublime Text', isOther: false, openerId: 'sublime' }, - { id: 'vscode', name: 'Visual Studio Code', isOther: false, openerId: 'vscode' }, - { id: 'other', name: 'Other', isOther: true, openerId: '' }, + { id: 'atom', name: 'Atom', isOther: false, binary: 'atom' }, + { id: 'vim', name: 'Vim', isOther: false, binary: 'vim' }, + { id: 'sublime', name: 'Sublime Text', isOther: false, binary: 'sublime' }, + { id: 'vscode', name: 'Visual Studio Code', isOther: false, binary: 'vscode' }, + { id: 'other', name: 'Other', isOther: true, binary: '' }, ] it('shows editor choice', () => { diff --git a/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts b/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts index 72cc184c011a..3127f5d1e5c2 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts @@ -1,4 +1,4 @@ -import type { CloudUser } from '../generated/test-cloud-graphql-types.gen' +import type { AuthenticatedUserShape } from '@packages/data-context/src/data' import type { WizardStep, CurrentProject, @@ -8,6 +8,7 @@ import type { TestingTypeEnum, GlobalProject, VersionData, + LocalSettings, } from '../generated/test-graphql-types.gen' import { resetTestNodeIdx } from './clientTestUtils' import { stubBrowsers } from './stubgql-Browser' @@ -24,6 +25,7 @@ export interface ClientTestContext { } versions: VersionData isAuthBrowserOpened: boolean + localSettings: LocalSettings wizard: { step: WizardStep canNavigateForward: boolean @@ -37,7 +39,7 @@ export interface ClientTestContext { chosenBrowser: null browserErrorMessage: null } - user: Partial | null + user: AuthenticatedUserShape | null cloudTypes: typeof cloudTypes __mockPartial: any } @@ -89,6 +91,29 @@ export function makeClientTestContext (): ClientTestContext { }, user: null, cloudTypes, + localSettings: { + __typename: 'LocalSettings', + preferences: { + __typename: 'LocalSettingsPreferences', + autoScrollingEnabled: true, + useDarkSidebar: true, + watchForSpecChange: true, + }, + availableEditors: [ + { + __typename: 'Editor', + id: 'code', + name: 'VS Code', + binary: 'code', + }, + { + __typename: 'Editor', + id: 'vim', + name: 'Vim', + binary: 'vim', + }, + ], + }, __mockPartial: {}, } } diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts index ceccc21f24cb..e22b62c92770 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts @@ -6,6 +6,9 @@ export const stubQuery: MaybeResolver = { dev () { return {} }, + localSettings (source, args, ctx) { + return ctx.localSettings + }, wizard (source, args, ctx) { return ctx.wizard }, diff --git a/packages/frontend-shared/src/components/Switch.vue b/packages/frontend-shared/src/components/Switch.vue index bc1cf6b0c62c..f2cb274e7ca9 100644 --- a/packages/frontend-shared/src/components/Switch.vue +++ b/packages/frontend-shared/src/components/Switch.vue @@ -48,5 +48,7 @@ const sizeClasses = { }, } -defineEmits(['update']) +defineEmits<{ + (e: 'update', value: boolean): void +}>() diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 1be8b889b909..36b50c719058 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -312,7 +312,19 @@ }, "testingPreferences": { "title": "Testing Preferences", - "description": "Configure your testing environment with these flags" + "description": "Configure your testing environment with these flags", + "autoScrollingEnabled": { + "title": "Auto Scrolling Enabled", + "description": "Scroll behavior when running tests." + }, + "watchForSpecChange": { + "title": "Watch for Spec Change", + "description": "Re-run specs when a file changes." + }, + "useDarkSidebar": { + "title": "Dark sidebar", + "description": "Select the color theme of the app sidebar." + } }, "footer": { "text": "You can reconfigure the settings for this project if you’re experiencing issues with your Cypress configuration.", diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index c2d86e27a6bf..cc13b042dab9 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -344,6 +344,16 @@ type DevState { needsRelaunch: Boolean } +"""Represents an editor on the local machine""" +type Editor { + """Binary that opens the editor""" + binary: String! + id: String! + + """name of editor""" + name: String! +} + """Represents a spec on the file system""" type FileParts implements Node { """ @@ -420,6 +430,22 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// """ scalar JSON +"""local settings on a device-by-device basis""" +type LocalSettings { + availableEditors: [Editor!]! + preferences: LocalSettingsPreferences! +} + +"""local setting preferences""" +type LocalSettingsPreferences { + autoScrollingEnabled: Boolean + preferredEditorBinary: String + proxyBypass: String + proxyServer: String + useDarkSidebar: Boolean + watchForSpecChange: Boolean +} + type Mutation { """Add project to projects array and cache it""" addProject( @@ -482,9 +508,13 @@ type Mutation { """Set active project to run tests on""" setActiveProject(path: String!): Boolean + setAutoScrollingEnabled(value: Boolean!): Boolean + setPreferredEditorBinary(value: String!): Boolean """Save the projects preferences to cache""" setProjectPreferences(browserPath: String!, testingType: TestingTypeEnum!): Query! + setUseDarkSidebar(value: Boolean!): Boolean + setWatchForSpecChange(value: Boolean!): Boolean """show the launchpad at the browser picker step""" showElectronOnAppExit: Boolean @@ -604,6 +634,9 @@ type Query { """Whether the app is in global mode or not""" isInGlobalMode: Boolean! + """editors on the user local machine""" + localSettings: LocalSettings! + """All known projects for the app""" projects: [ProjectLike!]! diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Editor.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Editor.ts new file mode 100644 index 000000000000..ed144ff6f798 --- /dev/null +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Editor.ts @@ -0,0 +1,17 @@ +import { objectType } from 'nexus' + +export const Editor = objectType({ + name: 'Editor', + description: 'Represents an editor on the local machine', + definition (t) { + t.nonNull.string('id') + + t.nonNull.string('name', { + description: 'name of editor', + }) + + t.nonNull.string('binary', { + description: 'Binary that opens the editor', + }) + }, +}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-LocalSettings.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-LocalSettings.ts new file mode 100644 index 000000000000..d3fc392a5c35 --- /dev/null +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-LocalSettings.ts @@ -0,0 +1,34 @@ +import { objectType } from 'nexus' +import { Editor } from './gql-Editor' + +export const LocalSettingsPreferences = objectType({ + name: 'LocalSettingsPreferences', + description: 'local setting preferences', + definition (t) { + t.boolean('autoScrollingEnabled') + t.boolean('watchForSpecChange') + t.boolean('useDarkSidebar') + t.string('preferredEditorBinary') + t.string('proxyServer', { + resolve: (source, args, ctx) => ctx.env.HTTP_PROXY ?? null, + }) + + t.string('proxyBypass', { + resolve: (source, args, ctx) => ctx.env.NO_PROXY ?? null, + }) + }, +}) + +export const LocalSettings = objectType({ + name: 'LocalSettings', + description: 'local settings on a device-by-device basis', + definition (t) { + t.nonNull.list.nonNull.field('availableEditors', { + type: Editor, + }) + + t.nonNull.field('preferences', { + type: LocalSettingsPreferences, + }) + }, +}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 2c23e79e9cf2..2e0996537c45 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -300,6 +300,54 @@ export const mutation = mutationType({ }, }) + t.liveMutation('setAutoScrollingEnabled', { + type: 'Boolean', + args: { + value: nonNull(booleanArg()), + }, + resolve: async (_, args, ctx) => { + await ctx.actions.localSettings.setDevicePreference('autoScrollingEnabled', args.value) + + return true + }, + }) + + t.liveMutation('setUseDarkSidebar', { + type: 'Boolean', + args: { + value: nonNull(booleanArg()), + }, + resolve: async (_, args, ctx) => { + await ctx.actions.localSettings.setDevicePreference('useDarkSidebar', args.value) + + return true + }, + }) + + t.liveMutation('setWatchForSpecChange', { + type: 'Boolean', + args: { + value: nonNull(booleanArg()), + }, + resolve: async (_, args, ctx) => { + await ctx.actions.localSettings.setDevicePreference('watchForSpecChange', args.value) + + return true + }, + }) + + t.liveMutation('setPreferredEditorBinary', { + type: 'Boolean', + args: { + value: nonNull(stringArg()), + }, + resolve: async (_, args, ctx) => { + await ctx.actions.localSettings.setDevicePreference('preferredEditorBinary', args.value) + + return true + }, + }) + t.liveMutation('showElectronOnAppExit', { description: 'show the launchpad at the browser picker step', resolve: (_, args, ctx) => { diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts index 8b7f43ca8598..e95ebb5bd722 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts @@ -3,6 +3,7 @@ import { BaseError } from '.' import { ProjectLike } from '..' import { CurrentProject } from './gql-CurrentProject' import { DevState } from './gql-DevState' +import { LocalSettings } from './gql-LocalSettings' import { VersionData } from './gql-VersionData' import { Wizard } from './gql-Wizard' @@ -63,5 +64,13 @@ export const Query = objectType({ description: 'Whether the browser has been opened for auth or not', resolve: (source, args, ctx) => ctx.coreData.isAuthBrowserOpened, }) + + t.nonNull.field('localSettings', { + type: LocalSettings, + description: 'editors on the user local machine', + resolve: (source, args, ctx) => { + return ctx.coreData.localSettings + }, + }) }, }) diff --git a/packages/graphql/src/schemaTypes/objectTypes/index.ts b/packages/graphql/src/schemaTypes/objectTypes/index.ts index 5c37816a0aa4..e6ed820b7b63 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/index.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/index.ts @@ -6,10 +6,12 @@ export * from './gql-Browser' export * from './gql-CodeGenResult' export * from './gql-CurrentProject' export * from './gql-DevState' +export * from './gql-Editor' export * from './gql-FileParts' export * from './gql-GeneratedSpec' export * from './gql-GitInfo' export * from './gql-GlobalProject' +export * from './gql-LocalSettings' export * from './gql-Mutation' export * from './gql-ProjectPreferences' export * from './gql-Query' diff --git a/packages/reporter/cypress/support/utils.ts b/packages/reporter/cypress/support/utils.ts index 758f21600fe3..20a12d51fc47 100644 --- a/packages/reporter/cypress/support/utils.ts +++ b/packages/reporter/cypress/support/utils.ts @@ -50,12 +50,12 @@ export const itHandlesFileOpening = ({ getRunner, selector, file, stackTrace = f describe('when user has not already set opener and opens file', () => { const availableEditors = [ - { id: 'computer', name: 'On Computer', isOther: false, openerId: 'computer' }, - { id: 'atom', name: 'Atom', isOther: false, openerId: 'atom' }, - { id: 'vim', name: 'Vim', isOther: false, openerId: 'vim' }, - { id: 'sublime', name: 'Sublime Text', isOther: false, openerId: 'sublime' }, - { id: 'vscode', name: 'Visual Studio Code', isOther: false, openerId: 'vscode' }, - { id: 'other', name: 'Other', isOther: true, openerId: '' }, + { id: 'computer', name: 'On Computer', isOther: false, binary: 'computer' }, + { id: 'atom', name: 'Atom', isOther: false, binary: 'atom' }, + { id: 'vim', name: 'Vim', isOther: false, binary: 'vim' }, + { id: 'sublime', name: 'Sublime Text', isOther: false, binary: 'sublime' }, + { id: 'vscode', name: 'Visual Studio Code', isOther: false, binary: 'vscode' }, + { id: 'other', name: 'Other', isOther: true, binary: '' }, ] beforeEach(() => { diff --git a/packages/runner/cypress/support/verify-failures.js b/packages/runner/cypress/support/verify-failures.js index d0975113d1e3..6f0c73c054c1 100644 --- a/packages/runner/cypress/support/verify-failures.js +++ b/packages/runner/cypress/support/verify-failures.js @@ -37,7 +37,7 @@ const verifyFailure = (options) => { preferredOpener: { id: 'foo-editor', name: 'Foo', - openerId: 'foo-editor', + binary: 'foo-editor', isOther: false, }, }) diff --git a/packages/server/lib/gui/events.ts b/packages/server/lib/gui/events.ts index 341f4fb42da2..70723bdf0292 100644 --- a/packages/server/lib/gui/events.ts +++ b/packages/server/lib/gui/events.ts @@ -365,7 +365,24 @@ const handleEvent = function (options, bus, event, id, type, arg) { case 'get:user:editor': return editors.getUserEditor(true) - .then(send) + .then((data) => { + // todo(lachlan): remove post 10.0 + // just here to support an assumption in desktop-gui + // that there will be a "placeholder" empty editor + // where binary is null. + // moving forward, `binary` is non nullable (doesn't make sense). + data = { + ...data, + availableEditors: data.availableEditors.concat({ + id: 'other', + name: 'Other', + binary: null, + isOther: true, + }), + } + + return send(data) + }) .catch(sendErr) case 'set:user:editor': diff --git a/packages/server/lib/makeDataContext.ts b/packages/server/lib/makeDataContext.ts index 0ef8a34c7ce0..4d6816d7c281 100644 --- a/packages/server/lib/makeDataContext.ts +++ b/packages/server/lib/makeDataContext.ts @@ -3,7 +3,7 @@ import os from 'os' import type { App } from 'electron' import specsUtil from './util/specs' -import type { FindSpecs, FoundBrowser, LaunchArgs, LaunchOpts, OpenProjectLaunchOptions, PlatformName, Preferences, SettingsOptions } from '@packages/types' +import type { Editor, FindSpecs, FoundBrowser, LaunchArgs, LaunchOpts, OpenProjectLaunchOptions, PlatformName, Preferences, SettingsOptions } from '@packages/types' import browserUtils from './browsers/utils' import auth from './gui/auth' import user from './user' @@ -16,6 +16,8 @@ import findSystemNode from './util/find_system_node' import { graphqlSchema } from '@packages/graphql/src/schema' import type { InternalDataContextOptions } from '@packages/data-context/src/DataContext' import { openExternal } from '@packages/server/lib/gui/links' +import { getDevicePreferences, setDevicePreference } from './util/device_preferences' +import { getUserEditor, setUserEditor } from './util/editors' const { getBrowsers, ensureAndGetByNameOrPath } = browserUtils @@ -124,6 +126,23 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext { openExternal(url) }, }, + localSettingsApi: { + setDevicePreference (key, value) { + return setDevicePreference(key, value) + }, + + async getPreferences () { + return getDevicePreferences() + }, + async setPreferredOpener (editor: Editor) { + await setUserEditor(editor) + }, + async getAvailableEditors () { + const { availableEditors } = await getUserEditor(true) + + return availableEditors + }, + }, }) return ctx diff --git a/packages/server/lib/saved_state.js b/packages/server/lib/saved_state.js index bf619f2d1785..ad249721aade 100644 --- a/packages/server/lib/saved_state.js +++ b/packages/server/lib/saved_state.js @@ -35,6 +35,10 @@ ctSpecListWidth firstOpened lastOpened promptsShown +watchForSpecChange +useDarkSidebar +preferredEditorBinary + `.trim().split(/\s+/) const formStatePath = (projectRoot) => { diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index 824ee4a15957..9ecc38b646f1 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -197,7 +197,7 @@ export abstract class ServerBase { target: config.baseUrl && testingType === 'component' ? config.baseUrl : undefined, }) - this._socket = new SocketCtor(config) as TSocket + this._socket = new SocketCtor(config, this.ctx) as TSocket clientCertificates.loadClientCertificateConfig(config) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index d4312b9be239..198048eb3439 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -11,13 +11,14 @@ import fixture from './fixture' import task from './task' import { ensureProp } from './util/class-helpers' import { getUserEditor, setUserEditor } from './util/editors' -import { openFile } from './util/file-opener' +import { openFile, OpenFileDetails } from './util/file-opener' import open from './util/open' import type { DestroyableHttpServer } from './util/server_destroy' import * as session from './session' // eslint-disable-next-line no-duplicate-imports import type { Socket } from '@packages/socket' import path from 'path' +import type { DataContext } from '@packages/data-context' type StartListeningCallbacks = { onSocketConnection: (socket: any) => void @@ -77,7 +78,7 @@ export class SocketBase { protected _io?: socketIo.SocketIOServer protected testsDir: string | null - constructor (config: Record) { + constructor (config: Record, private ctx: DataContext) { this.ended = false this.testsDir = null } @@ -485,7 +486,23 @@ export class SocketBase { setUserEditor(editor) }) - socket.on('open:file', (fileDetails) => { + socket.on('open:file', async (fileDetails: OpenFileDetails) => { + // todo(lachlan): post 10.0 we should not pass the + // editor (in the `fileDetails.where` key) from the + // front-end, but rather rely on the server context + // to grab the prefered editor, like I'm doing here, + // so we do not need to + // maintain two sources of truth for the preferred editor + // adding this conditional to maintain backwards compat with + // existing runner and reporter API. + if (process.env.LAUNCHPAD) { + fileDetails.where = { + binary: this.ctx.coreData.localSettings.preferences.preferredEditorBinary || 'computer', + } + } + + debug('opening file %o', fileDetails) + openFile(fileDetails) }) diff --git a/packages/server/lib/socket-ct.ts b/packages/server/lib/socket-ct.ts index 1ce0d17188d5..35eb543abe8e 100644 --- a/packages/server/lib/socket-ct.ts +++ b/packages/server/lib/socket-ct.ts @@ -3,12 +3,13 @@ import type * as socketIo from '@packages/socket' import devServer from '@packages/server/lib/plugins/dev-server' import { SocketBase } from '@packages/server/lib/socket-base' import type { DestroyableHttpServer } from '@packages/server/lib/util/server_destroy' +import type { DataContext } from '@packages/data-context' const debug = Debug('cypress:server:socket-ct') export class SocketCt extends SocketBase { - constructor (config: Record) { - super(config) + constructor (config: Record, ctx: DataContext) { + super(config, ctx) devServer.emitter.on('dev-server:compile:error', (error: string | undefined) => { this.toRunner('dev-server:hmr:error', error) diff --git a/packages/server/lib/socket-e2e.ts b/packages/server/lib/socket-e2e.ts index 2d633831a4c5..505e549e3c7f 100644 --- a/packages/server/lib/socket-e2e.ts +++ b/packages/server/lib/socket-e2e.ts @@ -5,6 +5,7 @@ import { SocketBase } from './socket-base' import { fs } from './util/fs' import type { DestroyableHttpServer } from './util/server_destroy' import * as studio from './studio' +import type { DataContext } from '@packages/data-context' const debug = Debug('cypress:server:socket-e2e') @@ -15,8 +16,8 @@ const isSpecialSpec = (name) => { export class SocketE2E extends SocketBase { private testFilePath: string | null - constructor (config: Record) { - super(config) + constructor (config: Record, ctx: DataContext) { + super(config, ctx) this.testFilePath = null diff --git a/packages/server/lib/util/device_preferences.ts b/packages/server/lib/util/device_preferences.ts new file mode 100644 index 000000000000..b5834072d11c --- /dev/null +++ b/packages/server/lib/util/device_preferences.ts @@ -0,0 +1,23 @@ +import debugModule from 'debug' +import savedState from '../saved_state' +import { DevicePreferences, devicePreferenceDefaults } from '@packages/types/src/devicePreferences' + +const debug = debugModule('cypress:server:preferences') + +export async function setDevicePreference (key: K, value: DevicePreferences[K]) { + debug('set preference: %s: %s', key, value) + + const state = await savedState.create() + + state.set(key, value) +} + +export async function getDevicePreferences (): Promise { + const cached = await (await savedState.create()).get() + + const state = { ...devicePreferenceDefaults, ...cached } + + debug('get preferences: %o', state) + + return state +} diff --git a/packages/server/lib/util/editors.ts b/packages/server/lib/util/editors.ts index f262bfb506e7..1771d408d551 100644 --- a/packages/server/lib/util/editors.ts +++ b/packages/server/lib/util/editors.ts @@ -2,64 +2,46 @@ import _ from 'lodash' import Bluebird from 'bluebird' import debugModule from 'debug' -import { getEnvEditors, Editor } from './env-editors' +import type { Editor, EditorsResult } from '@packages/types' +import { getEnvEditors } from './env-editors' import shell from './shell' import savedState from '../saved_state' -const debug = debugModule('cypress:server:editors') - -interface CyEditor { - id: string - name: string - openerId: string - isOther: boolean -} +export const osFileSystemExplorer = { + darwin: 'Finder', + win32: 'File Explorer', + linux: 'File System', +} as const -interface EditorsResult { - preferredOpener?: CyEditor - availableEditors?: CyEditor[] -} +const debug = debugModule('cypress:server:editors') -const createEditor = (editor: Editor): CyEditor => { +const createEditor = (editor: Editor): Editor => { return { id: editor.id, name: editor.name, - openerId: editor.binary, - isOther: false, + binary: editor.binary, } } -const getOtherEditor = (preferredOpener?: CyEditor) => { +const getOtherEditor = (preferredOpener?: Editor): Editor | undefined => { // if preferred editor is the 'other' option, use it since it has the - // path (openerId) saved with it - if (preferredOpener && preferredOpener.isOther) { + // path (binary) saved with it + if (preferredOpener && preferredOpener.id === 'other') { return preferredOpener } - return { - id: 'other', - name: 'Other', - openerId: '', - isOther: true, - } + return } -const computerOpener = (): CyEditor => { - const names = { - darwin: 'Finder', - win32: 'File Explorer', - linux: 'File System', - } - +const computerOpener = (): Editor => { return { id: 'computer', - name: names[process.platform] || names.linux, - openerId: 'computer', - isOther: false, + name: osFileSystemExplorer[process.platform] || osFileSystemExplorer.linux, + binary: 'computer', } } -const getUserEditors = (): Bluebird => { +const getUserEditors = async (): Promise => { return Bluebird.filter(getEnvEditors(), (editor) => { debug('check if user has editor %s with binary %s', editor.name, editor.binary) @@ -72,18 +54,22 @@ const getUserEditors = (): Bluebird => { .then((state) => { return state.get('preferredOpener') }) - .then((preferredOpener?: CyEditor) => { + .then((preferredOpener?: Editor) => { debug('saved preferred editor: %o', preferredOpener) const cyEditors = _.map(editors, createEditor) + const preferred = getOtherEditor(preferredOpener) + + if (!preferred) { + return [computerOpener()].concat(cyEditors) + } - // @ts-ignore - return [computerOpener()].concat(cyEditors).concat([getOtherEditor(preferredOpener)]) + return [computerOpener()].concat(cyEditors, preferred) }) }) } -export const getUserEditor = (alwaysIncludeEditors = false): Bluebird => { +export const getUserEditor = async (alwaysIncludeEditors = false): Promise => { debug('get user editor') return savedState.create() @@ -106,11 +92,10 @@ export const getUserEditor = (alwaysIncludeEditors = false): Bluebird { +export const setUserEditor = async (editor: Editor) => { debug('set user editor: %o', editor) - return savedState.create() - .then((state) => { - state.set('preferredOpener', editor) - }) + const state = await savedState.create() + + state.set('preferredOpener', editor) } diff --git a/packages/server/lib/util/env-editors.ts b/packages/server/lib/util/env-editors.ts index ab29b264f002..805366d3f32d 100644 --- a/packages/server/lib/util/env-editors.ts +++ b/packages/server/lib/util/env-editors.ts @@ -1,4 +1,6 @@ -const linuxEditors = [ +import type { Editor } from '@packages/types' + +export const linuxEditors = [ { id: 'atom', binary: 'atom', @@ -43,10 +45,14 @@ const linuxEditors = [ id: 'webstorm', binary: 'webstorm', name: 'WebStorm', + }, { + id: 'webstorm64', + binary: 'webstorm64.exe', + name: 'WebStorm 64-bit', }, -] +] as const -const osxEditors = [ +export const macOSEditors = [ { id: 'atom', binary: 'atom', @@ -120,9 +126,9 @@ const osxEditors = [ binary: 'vim', name: 'Vim', }, -] +] as const -const windowsEditors = [ +export const windowsEditors = [ { id: 'brackets', binary: 'Brackets.exe', @@ -187,23 +193,13 @@ const windowsEditors = [ id: 'webstorm', binary: 'webstorm.exe', name: 'WebStorm', - }, { - id: 'webstorm64', - binary: 'webstorm64.exe', - name: 'WebStorm (64-bit)', }, -] - -export interface Editor { - id: string - binary: string - name: string -} +] as const -export const getEnvEditors = (): Editor[] => { +export const getEnvEditors = (): readonly Editor[] => { switch (process.platform) { case 'darwin': - return osxEditors + return macOSEditors case 'win32': return windowsEditors default: diff --git a/packages/server/lib/util/file-opener.ts b/packages/server/lib/util/file-opener.ts index 7e4fdc183cc2..92f2061eb1fc 100644 --- a/packages/server/lib/util/file-opener.ts +++ b/packages/server/lib/util/file-opener.ts @@ -3,12 +3,21 @@ import launchEditor from 'launch-editor' const debug = debugModule('cypress:server:file-opener') -export const openFile = (fileDetails) => { +export interface OpenFileDetails { + file: string + where: { + binary: string + } + line: number + column: number +} + +export const openFile = (fileDetails: OpenFileDetails) => { debug('open file: %o', fileDetails) - const openerId = fileDetails.where.openerId + const binary = fileDetails.where.binary - if (openerId === 'computer') { + if (binary === 'computer') { try { require('electron').shell.showItemInFolder(fileDetails.file) } catch (err) { @@ -20,7 +29,7 @@ export const openFile = (fileDetails) => { const { file, line, column } = fileDetails - launchEditor(`${file}:${line}:${column}`, `"${openerId}"`, (__, errMsg) => { + launchEditor(`${file}:${line}:${column}`, `"${binary}"`, (__, errMsg) => { debug('error opening file: %s', errMsg) }) } diff --git a/packages/server/test/unit/util/editors_spec.ts b/packages/server/test/unit/util/editors_spec.ts index 7e831aa64914..3e962390757a 100644 --- a/packages/server/test/unit/util/editors_spec.ts +++ b/packages/server/test/unit/util/editors_spec.ts @@ -1,4 +1,3 @@ -import _ from 'lodash' import Bluebird from 'bluebird' import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' @@ -67,37 +66,17 @@ describe('lib/util/editors', () => { sinon.restore() }) - it('returns a list of editors on the user\'s system with an "On Computer" option prepended and an "Other" option appended', () => { - return getUserEditor().then(({ availableEditors }) => { - const names = _.map(availableEditors, 'name') - - expect(names).to.eql(['Finder', 'Sublime Text', 'Visual Studio Code', 'Vim', 'Other']) - expect(availableEditors[0]).to.eql({ - id: 'computer', - name: 'Finder', - isOther: false, - openerId: 'computer', - }) - - expect(availableEditors[4]).to.eql({ - id: 'other', - name: 'Other', - isOther: true, - openerId: '', - }) - }) - }) - it('includes user-set path for "Other" option if available', () => { // @ts-ignore savedState.create.resolves({ get () { - return { isOther: true, openerId: '/path/to/editor' } + return { isOther: true, binary: '/path/to/editor', id: 'other' } }, }) return getUserEditor().then(({ availableEditors }) => { - expect(availableEditors[4].openerId).to.equal('/path/to/editor') + console.log(availableEditors) + expect(availableEditors[4].binary).to.equal('/path/to/editor') }) }) @@ -143,7 +122,7 @@ describe('lib/util/editors', () => { }) return getUserEditor(true).then(({ availableEditors, preferredOpener }) => { - expect(availableEditors).to.have.length(5) + expect(availableEditors).to.have.length(4) expect(preferredOpener).to.equal(preferredOpener) }) }) @@ -168,14 +147,14 @@ describe('lib/util/editors', () => { it('returns available editors if preferred opener has not been saved', () => { return getUserEditor(false).then(({ availableEditors, preferredOpener }) => { - expect(availableEditors).to.have.length(5) + expect(availableEditors).to.have.length(4) expect(preferredOpener).to.be.undefined }) }) it('is default', () => { return getUserEditor().then(({ availableEditors, preferredOpener }) => { - expect(availableEditors).to.have.length(5) + expect(availableEditors).to.have.length(4) expect(preferredOpener).to.be.undefined }) }) diff --git a/packages/types/src/devicePreferences.ts b/packages/types/src/devicePreferences.ts new file mode 100644 index 000000000000..53342f53dcaf --- /dev/null +++ b/packages/types/src/devicePreferences.ts @@ -0,0 +1,13 @@ +export interface DevicePreferences { + watchForSpecChange?: boolean + useDarkSidebar?: boolean + autoScrollingEnabled?: boolean + preferredEditorBinary?: string | undefined +} + +export const devicePreferenceDefaults: DevicePreferences = { + watchForSpecChange: true, + useDarkSidebar: true, + autoScrollingEnabled: true, + preferredEditorBinary: undefined, +} diff --git a/packages/types/src/editors.ts b/packages/types/src/editors.ts new file mode 100644 index 000000000000..718f8d4ddee7 --- /dev/null +++ b/packages/types/src/editors.ts @@ -0,0 +1,10 @@ +export interface Editor { + id: string + binary: string + name: string +} + +export interface EditorsResult { + preferredOpener?: Editor + availableEditors: Editor[] +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 370c059a5f47..82cb43d5ac5c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,10 +2,14 @@ export * from './cache' export * from './constants' +export * from './devicePreferences' + export * from './driver' export * from './spec' +export * from './editors' + export type { AllPackages, AllPackageTypes, diff --git a/packages/ui-components/cypress/integration/editor-picker_spec.jsx b/packages/ui-components/cypress/integration/editor-picker_spec.jsx index 78f944babd20..dfea08ece06a 100644 --- a/packages/ui-components/cypress/integration/editor-picker_spec.jsx +++ b/packages/ui-components/cypress/integration/editor-picker_spec.jsx @@ -15,13 +15,13 @@ describe('', () => { beforeEach(() => { defaultProps = { - chosen: { id: 'vscode', name: 'VS Code', openerId: 'vscode', isOther: false }, + chosen: { id: 'vscode', name: 'VS Code', binary: 'vscode', isOther: false }, editors: [ - { id: 'computer', name: 'On Computer', openerId: 'computer', isOther: false, description: 'Opens on computer etc etc' }, - { id: 'atom', name: 'Atom', openerId: 'atom', isOther: false }, - { id: 'sublime', name: 'Sublime Text', openerId: 'sublime', isOther: false }, - { id: 'vscode', name: 'VS Code', openerId: 'vscode', isOther: false }, - { id: 'other', name: 'Other', openerId: '', isOther: true, description: 'Enter the full path etc etc' }, + { id: 'computer', name: 'On Computer', binary: 'computer', isOther: false, description: 'Opens on computer etc etc' }, + { id: 'atom', name: 'Atom', binary: 'atom', isOther: false }, + { id: 'sublime', name: 'Sublime Text', binary: 'sublime', isOther: false }, + { id: 'vscode', name: 'VS Code', binary: 'vscode', isOther: false }, + { id: 'other', name: 'Other', binary: '', isOther: true, description: 'Enter the full path etc etc' }, ], onSelect: () => {}, } @@ -89,7 +89,7 @@ describe('', () => { }) it('populates path if specified', () => { - defaultProps.editors[4].openerId = '/path/to/my/editor' + defaultProps.editors[4].binary = '/path/to/my/editor' cy.render(render, ) cy.contains('Other').find('input[type="text"]').should('have.value', '/path/to/my/editor') @@ -106,7 +106,7 @@ describe('', () => { setOtherPath: action((otherPath) => { const otherOption = _.find(state.editors, { isOther: true }) - otherOption.openerId = otherPath + otherOption.binary = otherPath }), })) @@ -133,7 +133,7 @@ describe('', () => { cy.contains('Other').find('input[type="text"]').type(` ${path} `, { delay: 0 }) .should(() => { - expect(onSelect.lastCall.args[0].openerId).to.equal(path) + expect(onSelect.lastCall.args[0].binary).to.equal(path) }) }) @@ -148,7 +148,7 @@ describe('', () => { cy.contains('Other').find('input[type="text"]').type(letter, { delay: 0 }) .should(() => { expect(onSelect.lastCall.args[0].id).to.equal('other') - expect(onSelect.lastCall.args[0].openerId).to.equal(pathSoFar) + expect(onSelect.lastCall.args[0].binary).to.equal(pathSoFar) }) }) }) diff --git a/packages/ui-components/cypress/integration/file-opener_spec.jsx b/packages/ui-components/cypress/integration/file-opener_spec.jsx index 302c78611ca2..43bad4fe1b68 100644 --- a/packages/ui-components/cypress/integration/file-opener_spec.jsx +++ b/packages/ui-components/cypress/integration/file-opener_spec.jsx @@ -16,16 +16,16 @@ const fileDetails = { const preferredOpener = { id: 'vscode', name: 'VS Code', - openerId: 'vscode', + binary: 'vscode', isOther: false, } const availableEditors = [ - { id: 'computer', name: 'On Computer', openerId: 'computer', isOther: false, description: 'Opens on computer etc etc' }, - { id: 'atom', name: 'Atom', openerId: 'atom', isOther: false }, - { id: 'sublime', name: 'Sublime Text', openerId: 'sublime', isOther: false }, - { id: 'vscode', name: 'VS Code', openerId: 'vscode', isOther: false }, - { id: 'other', name: 'Other', openerId: '', isOther: true, description: 'Enter the full path etc etc' }, + { id: 'computer', name: 'On Computer', binary: 'computer', isOther: false, description: 'Opens on computer etc etc' }, + { id: 'atom', name: 'Atom', binary: 'atom', isOther: false }, + { id: 'sublime', name: 'Sublime Text', binary: 'sublime', isOther: false }, + { id: 'vscode', name: 'VS Code', binary: 'vscode', isOther: false }, + { id: 'other', name: 'Other', binary: '', isOther: true, description: 'Enter the full path etc etc' }, ] describe('', () => { diff --git a/packages/ui-components/src/file-opener/editor-picker-modal.tsx b/packages/ui-components/src/file-opener/editor-picker-modal.tsx index 7707519c95ba..4bdcc64e5a0c 100644 --- a/packages/ui-components/src/file-opener/editor-picker-modal.tsx +++ b/packages/ui-components/src/file-opener/editor-picker-modal.tsx @@ -25,7 +25,7 @@ const validate = (chosenEditor: Editor) => { let isValid = !!chosenEditor && !!chosenEditor.id let validationMessage = 'Please select a preference' - if (isValid && chosenEditor.isOther && !chosenEditor.openerId) { + if (isValid && chosenEditor.isOther && !chosenEditor.binary) { isValid = false validationMessage = 'Please enter the path for the "Other" editor' } @@ -42,7 +42,7 @@ const EditorPickerModal = observer(({ chosenEditor, editors, isOpen, onClose, on const otherOption = _.find(external.editors, { isOther: true }) if (otherOption) { - otherOption.openerId = otherPath + otherOption.binary = otherPath } }), }), { editors }) diff --git a/packages/ui-components/src/file-opener/editor-picker.tsx b/packages/ui-components/src/file-opener/editor-picker.tsx index 541e70450920..0dd1b78e3ba1 100644 --- a/packages/ui-components/src/file-opener/editor-picker.tsx +++ b/packages/ui-components/src/file-opener/editor-picker.tsx @@ -30,7 +30,7 @@ const EditorPicker = observer(({ chosen = {}, editors, onSelect, onUpdateOtherPa diff --git a/packages/ui-components/src/file-opener/file-model.ts b/packages/ui-components/src/file-opener/file-model.ts index 9e6d1b6734cf..7041f66784d9 100644 --- a/packages/ui-components/src/file-opener/file-model.ts +++ b/packages/ui-components/src/file-opener/file-model.ts @@ -12,7 +12,7 @@ export interface FileDetails { export interface Editor { id: string name: string - openerId: string + binary: string isOther: boolean description?: string } diff --git a/system-tests/projects/e2e/cypress/support/util.js b/system-tests/projects/e2e/cypress/support/util.js index da5ebf6b0ccd..6ec50668203c 100644 --- a/system-tests/projects/e2e/cypress/support/util.js +++ b/system-tests/projects/e2e/cypress/support/util.js @@ -35,7 +35,7 @@ export const verify = (ctx, options) => { preferredOpener: { id: 'foo-editor', name: 'Foo', - openerId: 'foo-editor', + binary: 'foo-editor', isOther: false, }, })