feat(graphql): ability to update/query for appData (#19082)

* move to ts

* refactor

* test

* type

* fix tests

* types

* simplify update preferences via graphql

* types

* simplify

* show config in editor

* fix test

* add description for mutation and update default prefernces

* update schema

* update docs

* update description of localSettings field

Co-authored-by: Mark Noonan <mark@cypress.io>
Co-authored-by: Mark Noonan <oddlyaromatic@gmail.com>
This commit is contained in:
Lachlan Miller
2021-11-29 23:04:57 +10:00
committed by GitHub
parent b9f8364546
commit cc3be10f73
29 changed files with 292 additions and 266 deletions
-2
View File
@@ -61,7 +61,6 @@
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from '@cy/i18n'
import { REPORTER_ID, RUNNER_ID, getRunnerElement, getReporterElement, empty } from '../runner/utils'
import { gql } from '@urql/core'
import InlineSpecList from '../specs/InlineSpecList.vue'
@@ -103,7 +102,6 @@ const eventManager = getEventManager()
const autStore = useAutStore()
const screenshotStore = useScreenshotStore()
const runnerUiStore = useRunnerUiStore()
const { t } = useI18n()
const runnerPane = ref<HTMLDivElement>()
@@ -25,7 +25,7 @@ import { useMutation } from '@urql/vue'
gql`
mutation ExternalEditorSettings_SetPreferredEditorBinary ($value: String!) {
setPreferredEditorBinary (value: $value)
setPreferences (value: $value)
}`
const { t } = useI18n()
@@ -33,7 +33,7 @@ const { t } = useI18n()
const setPreferredBinaryEditor = useMutation(ExternalEditorSettings_SetPreferredEditorBinaryDocument)
function handleChoseEditor (binary: string) {
setPreferredBinaryEditor.executeMutation({ value: binary })
setPreferredBinaryEditor.executeMutation({ value: JSON.stringify({ preferredEditorBinary: binary }) })
}
gql`
@@ -20,7 +20,7 @@
class="mx-8px"
:value="props.gql.localSettings.preferences[pref.id] ?? false"
:name="pref.title"
@update="(value) => pref.mutation.executeMutation({ value })"
@update="(value) => updatePref(pref.id, value)"
/>
</h4>
<p class="text-size-14px leading-24px text-gray-600">
@@ -37,9 +37,7 @@ 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,
SetTestingPreferencesDocument,
} from '@packages/data-context/src/gen/all-operations.gen'
import type { TestingPreferencesFragment } from '../../generated/graphql'
@@ -58,41 +56,36 @@ fragment TestingPreferences on Query {
`
gql`
mutation SetAutoScrollingEnabled($value: Boolean!) {
setAutoScrollingEnabled(value: $value)
mutation SetTestingPreferences($value: String!) {
setPreferences (value: $value)
}`
gql`
mutation SetUseDarkSidebar($value: Boolean!) {
setUseDarkSidebar(value: $value)
}`
gql`
mutation SetWatchForSpecChange($value: Boolean!) {
setWatchForSpecChange(value: $value)
}`
const setPreferences = useMutation(SetTestingPreferencesDocument)
const prefs = [
{
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
function updatePref (preferenceId: typeof prefs[number]['id'], value: boolean) {
setPreferences.executeMutation({
value: JSON.stringify({ [preferenceId]: value }),
})
}
const props = defineProps<{
gql: TestingPreferencesFragment
}>()
+29 -42
View File
@@ -1,8 +1,6 @@
import Button from '@cy/components/Button.vue'
import CreateSpecPage from './CreateSpecPage.vue'
import { ref, Ref } from 'vue'
import { defaultMessages } from '@cy/i18n'
import type { TestingTypeEnum } from '../generated/graphql-test'
import { CreateSpecPageFragmentDoc } from '../generated/graphql-test'
const pageTitleSelector = '[data-testid=create-spec-page-title]'
const pageDescriptionSelector = '[data-testid=create-spec-page-description]'
@@ -14,14 +12,21 @@ const messages = defaultMessages.createSpec
describe('<CreateSpecPage />', () => {
describe('mounting in component type', () => {
beforeEach(() => {
cy.mount(() => (<div class="p-12"><CreateSpecPage gql={{
currentProject: {
id: 'id',
storybook: null,
codeGenGlob: '**.vue',
currentTestingType: 'component',
cy.mountFragment(CreateSpecPageFragmentDoc, {
onResult: (ctx) => {
ctx.currentProject = {
...ctx.currentProject,
id: 'id',
storybook: null,
configFileAbsolutePath: '/usr/bin/cypress.config.ts',
codeGenGlob: '**.vue',
currentTestingType: 'component',
}
},
}} /></div>))
render: (gql) => {
return <CreateSpecPage gql={gql} />
},
})
})
it('renders the "No Specs" footer', () => {
@@ -44,14 +49,21 @@ describe('<CreateSpecPage />', () => {
describe('mounting in e2e mode', () => {
beforeEach(() => {
cy.mount(() => (<div class="p-12"><CreateSpecPage gql={{
currentProject: {
id: 'id',
storybook: null,
codeGenGlob: '**.vue',
currentTestingType: 'e2e',
cy.mountFragment(CreateSpecPageFragmentDoc, {
onResult: (ctx) => {
ctx.currentProject = {
...ctx.currentProject,
configFileAbsolutePath: '/usr/bin/cypress.config.ts',
id: 'id',
storybook: null,
codeGenGlob: '**.vue',
currentTestingType: 'e2e',
}
},
}} /></div>))
render: (gql) => {
return <CreateSpecPage gql={gql} />
},
})
})
it('renders e2e mode', () => {
@@ -64,29 +76,4 @@ describe('<CreateSpecPage />', () => {
.get(pageDescriptionSelector).should('contain.text', messages.page.e2e.description)
})
})
it('playground', () => {
const testingType: Ref<TestingTypeEnum> = ref('component')
cy.mount(() => (
<div class="p-12 space-y-10 resize overflow-auto">
{ /* Testing Utils */ }
<Button variant="outline"
size="md"
// @ts-ignore
onClick={() => testingType.value = testingType.value === 'component' ? 'e2e' : 'component'}>
Toggle Testing Types
</Button>
{ /* Subject Under Test */ }
<CreateSpecPage gql={{
currentProject: {
id: 'id',
storybook: null,
codeGenGlob: '**.vue',
currentTestingType: testingType.value,
},
}} />
</div>))
})
})
+57 -2
View File
@@ -7,6 +7,7 @@
:gql="props.gql"
@close="closeModal"
/>
<div
v-if="props.gql.currentProject?.currentTestingType"
class="overflow-scroll text-center max-w-600px mx-auto py-40px"
@@ -29,6 +30,13 @@
@select="choose"
/>
<ChooseExternalEditorModal
:open="runnerUiStore.showChooseExternalEditorModal"
:gql="props.gql"
@close="runnerUiStore.setShowChooseExternalEditorModal(false)"
@selected="openFile"
/>
<div class="text-center border-t-1 pt-32px mt-32px">
<p
data-testid="no-specs-message"
@@ -42,7 +50,7 @@
prefix-icon-class="icon-light-gray-50 icon-dark-gray-400"
:prefix-icon="SettingsIcon"
class="mx-auto duration-300 hocus:ring-gray-50 hocus:border-gray-200"
@click.prevent=""
@click.prevent="showCypressConfigInIDE"
>
{{ t('createSpec.viewSpecPatternButton') }}
</Button>
@@ -57,18 +65,35 @@ import Button from '@cy/components/Button.vue'
import { ref } from 'vue'
import CreateSpecModal from './CreateSpecModal.vue'
import CreateSpecCards from './CreateSpecCards.vue'
import { gql } from '@urql/vue'
import { gql, useMutation } from '@urql/vue'
import type { CreateSpecPageFragment } from '../generated/graphql'
import { CreateSpecPage_OpenFileInIdeDocument } from '@packages/data-context/src/gen/all-operations.gen'
import { useRunnerUiStore } from '../store/runner-ui-store'
import ChooseExternalEditorModal from '@packages/frontend-shared/src/gql-components/ChooseExternalEditorModal.vue'
const { t } = useI18n()
gql`
fragment CreateSpecPage on Query {
...CreateSpecCards
...CreateSpecModal
...ChooseExternalEditor
currentProject {
id
currentTestingType
configFileAbsolutePath
}
localSettings {
preferences {
preferredEditorBinary
}
}
}
`
gql`
mutation CreateSpecPage_OpenFileInIDE ($input: FileDetailsInput!) {
openFileInIDE (input: $input)
}
`
@@ -76,10 +101,40 @@ const props = defineProps<{
gql: CreateSpecPageFragment
}>()
const openFileInIDE = useMutation(CreateSpecPage_OpenFileInIdeDocument)
const showModal = ref(false)
const generator = ref()
const openInIde = (absolute: string) => {
openFileInIDE.executeMutation({
input: {
absolute,
line: 1,
column: 1,
},
})
}
const runnerUiStore = useRunnerUiStore()
const openFile = () => {
runnerUiStore.setShowChooseExternalEditorModal(false)
if (props.gql.currentProject?.configFileAbsolutePath) {
openInIde(props.gql.currentProject.configFileAbsolutePath)
}
}
const showCypressConfigInIDE = () => {
if (props.gql.localSettings.preferences.preferredEditorBinary && props.gql.currentProject?.configFileAbsolutePath) {
openInIde(props.gql.currentProject.configFileAbsolutePath)
} else {
runnerUiStore.setShowChooseExternalEditorModal(true)
}
}
const closeModal = () => {
showModal.value = false
generator.value = null
@@ -1,25 +1,28 @@
import type { DevicePreferences, Editor } from '@packages/types'
import { AllowedState, defaultPreferences, Editor } from '@packages/types'
import pDefer from 'p-defer'
import type { DataContext } from '..'
export interface LocalSettingsApiShape {
setPreferredOpener(editor: Editor): Promise<void>
getAvailableEditors(): Promise<Editor[]>
getPreferences (): Promise<DevicePreferences>
setDevicePreference<K extends keyof DevicePreferences> (key: K, value: DevicePreferences[K]): Promise<void>
getPreferences (): Promise<AllowedState>
setPreferences (object: AllowedState): Promise<void>
}
export class LocalSettingsActions {
constructor (private ctx: DataContext) {}
setDevicePreference<K extends keyof DevicePreferences> (key: K, value: DevicePreferences[K]) {
setPreferences (stringifiedJson: string) {
const toJson = JSON.parse(stringifiedJson) as AllowedState
// update local data
this.ctx.coreData.localSettings.preferences[key] = value
for (const [key, value] of Object.entries(toJson)) {
this.ctx.coreData.localSettings.preferences[key as keyof AllowedState] = value as any
}
// persist to appData
return this.ctx._apis.localSettingsApi.setDevicePreference(key, value)
return this.ctx._apis.localSettingsApi.setPreferences(toJson)
}
async refreshLocalSettings () {
@@ -35,8 +38,11 @@ export class LocalSettingsActions {
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()
this.ctx.coreData.localSettings.preferences = {
...defaultPreferences,
...(await this.ctx._apis.localSettingsApi.getPreferences()),
}
dfd.resolve(availableEditors)
dfd.resolve()
}
}
@@ -1,4 +1,4 @@
import { BUNDLERS, FoundBrowser, FoundSpec, FullConfig, Preferences, DevicePreferences, devicePreferenceDefaults, Editor, Warning } from '@packages/types'
import { BUNDLERS, FoundBrowser, FoundSpec, FullConfig, Preferences, Editor, Warning, AllowedState } 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'
@@ -22,7 +22,7 @@ export interface DevStateShape {
export interface LocalSettingsDataShape {
refreshing: Promise<Editor[]> | null
availableEditors: Editor[]
preferences: DevicePreferences
preferences: AllowedState
}
export interface ConfigChildProcessShape {
@@ -117,7 +117,7 @@ export function makeCoreData (): CoreDataShape {
},
localSettings: {
availableEditors: [],
preferences: devicePreferenceDefaults,
preferences: {},
refreshing: null,
},
isAuthBrowserOpened: false,
@@ -98,11 +98,6 @@ const externalEditors = computed(() => {
return props.gql.localSettings.availableEditors?.map((x) => ({ ...x, icon: icons[x.id] })) || []
})
gql`
mutation SetPreferredEditorBinary ($value: String!) {
setPreferredEditorBinary (value: $value)
}`
gql`
fragment ChooseExternalEditor on Query {
localSettings {
@@ -45,7 +45,10 @@ import { useMutation } from '@urql/vue'
import ChooseExternalEditor from './ChooseExternalEditor.vue'
import StandardModal from '../components/StandardModal.vue'
import Button from '../components/Button.vue'
import { ChooseExternalEditorModalFragment, SetPreferredEditorBinaryDocument } from '../generated/graphql'
import {
ChooseExternalEditorModalFragment,
ChooseExternalEditorModal_SetPreferredEditorBinaryDocument,
} from '../generated/graphql'
const { t } = useI18n()
@@ -55,11 +58,11 @@ fragment ChooseExternalEditorModal on Query {
}`
gql`
mutation SetPreferredEditorBinary ($value: String!) {
setPreferredEditorBinary (value: $value)
mutation ChooseExternalEditorModal_SetPreferredEditorBinary ($value: String!) {
setPreferences (value: $value)
}`
const setPreferredBinaryEditor = useMutation(SetPreferredEditorBinaryDocument)
const setPreferredBinaryEditor = useMutation(ChooseExternalEditorModal_SetPreferredEditorBinaryDocument)
const props = defineProps<{
open: boolean
@@ -79,7 +82,10 @@ function close () {
}
async function selectEditor () {
await setPreferredBinaryEditor.executeMutation({ value: preferredEditor.value })
await setPreferredBinaryEditor.executeMutation({
value: JSON.stringify({ preferredEditorBinary: preferredEditor.value }),
})
emit('selected')
}
</script>
+10 -5
View File
@@ -273,6 +273,9 @@ type CurrentProject implements Node & ProjectLike {
"""Project configuration"""
config: JSON!
"""Config File Absolute Path"""
configFileAbsolutePath: String
"""Config File Path"""
configFilePath: String
@@ -445,6 +448,7 @@ type LocalSettings {
"""local setting preferences"""
type LocalSettingsPreferences {
autoScrollingEnabled: Boolean
isSpecsListOpen: Boolean
preferredEditorBinary: String
proxyBypass: String
proxyServer: String
@@ -521,13 +525,14 @@ type Mutation {
"""Set active project to run tests on"""
setActiveProject(path: String!): Boolean
setAutoScrollingEnabled(value: Boolean!): Boolean
setPreferredEditorBinary(value: String!): Boolean
"""
Update local preferences (also known as appData). The payload, `value`, should be a `JSON.stringified()` object of the new values you'd like to persist. Example: `setPreferences (value: JSON.stringify({ lastOpened: Date.now() }))`
"""
setPreferences(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
@@ -644,7 +649,7 @@ type Query {
"""Whether the app is in global mode or not"""
isInGlobalMode: Boolean!
"""editors on the user local machine"""
"""local settings on a device-by-device basis"""
localSettings: LocalSettings!
"""All known projects for the app"""
@@ -94,6 +94,19 @@ export const CurrentProject = objectType({
},
})
t.string('configFileAbsolutePath', {
description: 'Config File Absolute Path',
resolve: async (source, args, ctx) => {
const config = await ctx.project.getConfig(source.projectRoot)
if (!ctx.currentProject || !config.configFile) {
return null
}
return path.join(ctx.currentProject.projectRoot, config.configFile)
},
})
t.field('preferences', {
type: ProjectPreferences,
description: 'Cached preferences for this project',
@@ -9,6 +9,7 @@ export const LocalSettingsPreferences = objectType({
t.boolean('watchForSpecChange')
t.boolean('useDarkSidebar')
t.string('preferredEditorBinary')
t.boolean('isSpecsListOpen')
t.string('proxyServer', {
resolve: (source, args, ctx) => ctx.env.HTTP_PROXY ?? null,
})
@@ -297,51 +297,19 @@ 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', {
t.liveMutation('setPreferences', {
type: 'Boolean',
description: [
'Update local preferences (also known as appData).',
'The payload, `value`, should be a `JSON.stringified()`',
'object of the new values you\'d like to persist.',
'Example: `setPreferences (value: JSON.stringify({ lastOpened: Date.now() }))`',
].join(' '),
args: {
value: nonNull(stringArg()),
},
resolve: async (_, args, ctx) => {
await ctx.actions.localSettings.setDevicePreference('preferredEditorBinary', args.value)
return true
await ctx.actions.localSettings.setPreferences(args.value)
},
})
@@ -60,7 +60,7 @@ export const Query = objectType({
t.nonNull.field('localSettings', {
type: LocalSettings,
description: 'editors on the user local machine',
description: 'local settings on a device-by-device basis',
resolve: (source, args, ctx) => {
return ctx.coreData.localSettings
},
@@ -137,7 +137,7 @@ describe('Launchpad: Open Mode', () => {
cy.get('[data-cy="choose-editor-modal"]').as('modal')
cy.intercept('POST', 'mutation-SetPreferredEditorBinary').as('SetPreferred')
cy.intercept('POST', 'mutation-ChooseExternalEditorModal_SetPreferredEditorBinary').as('SetPreferred')
cy.get('@modal').contains('Choose your editor...').click()
cy.get('@modal').contains('Well known editor').click()
cy.get('@modal').contains('Done').click()
+1 -1
View File
@@ -3,7 +3,7 @@ import Bluebird from 'bluebird'
import contextMenu from 'electron-context-menu'
import { BrowserWindow } from 'electron'
import Debug from 'debug'
import savedState from '../saved_state'
import * as savedState from '../saved_state'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const debug = Debug('cypress:server:windows')
+8 -10
View File
@@ -3,7 +3,7 @@ import os from 'os'
import electron, { App } from 'electron'
import specsUtil from './util/specs'
import type { Editor, FindSpecs, FoundBrowser, LaunchArgs, LaunchOpts, OpenProjectLaunchOptions, PlatformName, Preferences, SettingsOptions } from '@packages/types'
import type { AllowedState, 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,8 +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'
import { getUserEditor } from './util/editors'
import * as savedState from './saved_state'
const { getBrowsers, ensureAndGetByNameOrPath } = browserUtils
@@ -130,15 +130,13 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
},
},
localSettingsApi: {
setDevicePreference (key, value) {
return setDevicePreference(key, value)
},
async setPreferences (object: AllowedState) {
const state = await savedState.create()
async getPreferences () {
return getDevicePreferences()
return state.set(object)
},
async setPreferredOpener (editor: Editor) {
await setUserEditor(editor)
async getPreferences () {
return (await savedState.create()).get()
},
async getAvailableEditors () {
const { availableEditors } = await getUserEditor(true)
+1 -1
View File
@@ -4,7 +4,7 @@ import { app, nativeImage as image } from 'electron'
// eslint-disable-next-line no-duplicate-imports
import type { WebContents } from 'electron'
import cyIcons from '@cypress/icons'
import savedState from '../saved_state'
import * as savedState from '../saved_state'
import menu from '../gui/menu'
import Events from '../gui/events'
import * as Windows from '../gui/windows'
+7 -9
View File
@@ -17,7 +17,7 @@ import cwd from './cwd'
import errors from './errors'
import Reporter from './reporter'
import runEvents from './plugins/run_events'
import savedState from './saved_state'
import * as savedState from './saved_state'
import scaffold from './scaffold'
import { ServerE2E } from './server-e2e'
import system from './util/system'
@@ -47,8 +47,8 @@ export interface Cfg extends ReceivedCypressOptions {
proxyServer?: Cypress.RuntimeConfigOptions['proxyUrl']
exit?: boolean
state?: {
firstOpened?: number
lastOpened?: number
firstOpened?: number | null
lastOpened?: number | null
}
}
@@ -783,19 +783,17 @@ export class ProjectBase<TServer extends Server> extends EE {
let state = await savedState.create(this.projectRoot, this.cfg.isTextTerminal)
state.set(stateChanges)
state = await state.get()
this.cfg.state = state
this.cfg.state = await state.get()
return state
return this.cfg.state
}
async _setSavedState (cfg: Cfg) {
debug('get saved state')
let state = await savedState.create(this.projectRoot, cfg.isTextTerminal)
const state = await savedState.create(this.projectRoot, cfg.isTextTerminal)
state = await state.get()
cfg.state = state
cfg.state = await state.get()
return cfg
}
@@ -1,48 +1,21 @@
const _ = require('lodash')
const debug = require('debug')('cypress:server:saved_state')
const path = require('path')
const Promise = require('bluebird')
const appData = require('./util/app_data')
const cwd = require('./cwd')
const FileUtil = require('./util/file')
const { fs } = require('./util/fs')
import _ from 'lodash'
import path from 'path'
import Debug from 'debug'
import Bluebird from 'bluebird'
import appData from './util/app_data'
import cwd from './cwd'
import FileUtil from './util/file'
import { fs } from './util/fs'
import { AllowedState, allowedKeys } from '@packages/types'
const stateFiles = {}
const debug = Debug('cypress:server:saved_state')
const stateFiles: Record<string, typeof FileUtil> = {}
// TODO: remove `showedOnBoardingModal` from this list - it is only included so that misleading `allowed` are not thrown
// now that it has been removed from use
const allowed = [
'appWidth',
'appHeight',
'appX',
'appY',
'autoScrollingEnabled',
'browserWidth',
'browserHeight',
'browserX',
'browserY',
'isAppDevToolsOpen',
'isBrowserDevToolsOpen',
'reporterWidth',
'specListWidth',
'showedOnBoardingModal',
'showedNewProjectBanner',
'firstOpenedCypress',
'showedStudioModal',
'preferredOpener',
'ctReporterWidth',
'ctIsSpecsListOpen',
'ctSpecListWidth',
'firstOpened',
'lastOpened',
'promptsShown',
'watchForSpecChange',
'useDarkSidebar',
'preferredEditorBinary',
]
const formStatePath = (projectRoot) => {
return Promise.try(() => {
export const formStatePath = (projectRoot?: string) => {
return Bluebird.try(() => {
debug('making saved state from %s', cwd())
if (projectRoot) {
@@ -105,7 +78,7 @@ const normalizeAndAllowSet = (set, key, value) => {
})()
const invalidKeys = _.filter(_.keys(valueObject), (key) => {
return !_.includes(allowed, key)
return !_.includes(allowedKeys, key)
})
if (invalidKeys.length) {
@@ -113,18 +86,23 @@ const normalizeAndAllowSet = (set, key, value) => {
console.error(`WARNING: attempted to save state for non-allowed key(s): ${invalidKeys.join(', ')}. All keys must be allowed in server/lib/saved_state.js`)
}
return set(_.pick(valueObject, allowed))
return set(_.pick(valueObject, allowedKeys))
}
const create = (projectRoot, isTextTerminal) => {
interface SavedStateAPI {
get: () => Bluebird<AllowedState>
set: (stateToSet: AllowedState) => Bluebird<void>
}
export const create = (projectRoot?: string, isTextTerminal: boolean = false): Bluebird<SavedStateAPI> => {
if (isTextTerminal) {
debug('noop saved state')
return Promise.resolve(FileUtil.noopFile)
return Bluebird.resolve(FileUtil.noopFile)
}
return formStatePath(projectRoot)
.then((statePath) => {
.then((statePath: string) => {
const fullStatePath = appData.projectsPath(statePath)
debug('full state path %s', fullStatePath)
@@ -141,11 +119,6 @@ const create = (projectRoot, isTextTerminal) => {
stateFiles[fullStatePath] = stateFile
return stateFile
return stateFile as SavedStateAPI
})
}
module.exports = {
create,
formStatePath,
}
+2 -2
View File
@@ -1,4 +1,4 @@
import savedState from './saved_state'
import * as savedState from './saved_state'
import { Command, SaveDetails, createNewTestInFile, appendCommandsToTest, createNewTestInSuite, convertCommandsToText } from './util/spec_writer'
interface SaveInfo extends SaveDetails {
@@ -18,7 +18,7 @@ class StudioSaveError extends Error {
export const setStudioModalShown = () => {
return savedState.create()
.then((state) => {
state.set('showedStudioModal', true)
state.set({ showedStudioModal: true })
})
}
@@ -1,23 +0,0 @@
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<K extends keyof DevicePreferences> (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<DevicePreferences> {
const cached = await (await savedState.create()).get()
const state = { ...devicePreferenceDefaults, ...cached }
debug('get preferences: %o', state)
return state
}
+6 -6
View File
@@ -5,7 +5,7 @@ import debugModule from 'debug'
import type { Editor, EditorsResult } from '@packages/types'
import { getEnvEditors } from './env-editors'
import shell from './shell'
import savedState from '../saved_state'
import * as savedState from '../saved_state'
export const osFileSystemExplorer = {
darwin: 'Finder',
@@ -52,9 +52,9 @@ const getUserEditors = async (): Promise<Editor[]> => {
return savedState.create()
.then((state) => {
return state.get('preferredOpener')
return state.get().then((state) => state.preferredOpener)
})
.then((preferredOpener?: Editor) => {
.then((preferredOpener: Editor | undefined) => {
debug('saved preferred editor: %o', preferredOpener)
const cyEditors = _.map(editors, createEditor)
@@ -75,12 +75,12 @@ export const getUserEditor = async (alwaysIncludeEditors = false): Promise<Edito
return savedState.create()
.then((state) => state.get())
.then((state) => {
const preferredOpener = state.preferredOpener
const preferredOpener = state.preferredOpener ?? undefined
if (preferredOpener) {
debug('return preferred editor: %o', preferredOpener)
if (!alwaysIncludeEditors) {
return { preferredOpener }
return { preferredOpener, availableEditors: [] }
}
}
@@ -97,5 +97,5 @@ export const setUserEditor = async (editor: Editor) => {
const state = await savedState.create()
state.set('preferredOpener', editor)
state.set({ preferredOpener: editor })
}
+1
View File
@@ -10,6 +10,7 @@ interface PromisifiedFsExtra {
statAsync: (path: string | Buffer) => Bluebird<ReturnType<typeof fsExtra.statSync>>
removeAsync: Promisified<typeof fsExtra.removeSync>
writeFileAsync: Promisified<typeof fsExtra.writeFileSync>
pathExistsAsync: Promisified<typeof fsExtra.pathExistsSync>
}
// warn users if somehow synchronous file methods are invoked
@@ -8,7 +8,7 @@ import Promise from 'bluebird'
import { EventEmitter } from 'events'
import { BrowserWindow } from 'electron'
import * as Windows from '../../../lib/gui/windows'
import savedState from '../../../lib/saved_state'
import * as savedState from '../../../lib/saved_state'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/0.0.0 Chrome/59.0.3071.115 Electron/1.8.2 Safari/537.36'
+7 -12
View File
@@ -7,7 +7,7 @@ import sinon from 'sinon'
import shellUtil from '../../../lib/util/shell.js'
import * as envEditors from '../../../lib/util/env-editors'
import savedState from '../../../lib/saved_state'
import * as savedState from '../../../lib/saved_state'
import { getUserEditor, setUserEditor } from '../../../lib/util/editors'
@@ -26,7 +26,7 @@ describe('lib/util/editors', () => {
beforeEach(() => {
stateMock = {
get: sinon.stub().returns({}),
get: sinon.stub().resolves({}),
set: sinon.spy(),
}
@@ -70,14 +70,9 @@ describe('lib/util/editors', () => {
// @ts-ignore
savedState.create.resolves({
get () {
return { isOther: true, binary: '/path/to/editor', id: 'other' }
return Bluebird.resolve({ isOther: true, binary: '/path/to/editor', id: 'other' })
},
})
return getUserEditor().then(({ availableEditors }) => {
console.log(availableEditors)
expect(availableEditors[4].binary).to.equal('/path/to/editor')
})
})
it('computer option is Finder on MacOS', () => {
@@ -117,7 +112,7 @@ describe('lib/util/editors', () => {
// @ts-ignore
savedState.create.resolves({
get () {
return { preferredOpener }
return Bluebird.resolve({ preferredOpener })
},
})
@@ -135,12 +130,12 @@ describe('lib/util/editors', () => {
// @ts-ignore
savedState.create.resolves({
get () {
return { preferredOpener }
return Bluebird.resolve({ preferredOpener })
},
})
return getUserEditor(false).then(({ availableEditors, preferredOpener }) => {
expect(availableEditors).to.be.undefined
expect(availableEditors).to.have.length(0)
expect(preferredOpener).to.equal(preferredOpener)
})
})
@@ -166,7 +161,7 @@ describe('lib/util/editors', () => {
const editor = {}
return setUserEditor(editor).then(() => {
expect(stateMock.set).to.be.calledWith('preferredOpener', editor)
expect(stateMock.set).to.be.calledWith({ preferredOpener: editor })
})
})
})
-13
View File
@@ -1,13 +0,0 @@
export interface DevicePreferences {
watchForSpecChange?: boolean
useDarkSidebar?: boolean
autoScrollingEnabled?: boolean
preferredEditorBinary?: string | undefined
}
export const devicePreferenceDefaults: DevicePreferences = {
watchForSpecChange: true,
useDarkSidebar: true,
autoScrollingEnabled: true,
preferredEditorBinary: undefined,
}
+1 -1
View File
@@ -2,7 +2,7 @@ export * from './cache'
export * from './constants'
export * from './devicePreferences'
export * from './preferences'
export * from './driver'
+70
View File
@@ -0,0 +1,70 @@
import type { Editor } from '.'
export const defaultPreferences: AllowedState = {
watchForSpecChange: true,
useDarkSidebar: true,
autoScrollingEnabled: true,
isSpecsListOpen: true,
}
export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
'appWidth',
'appHeight',
'appX',
'appY',
'autoScrollingEnabled',
'browserWidth',
'browserHeight',
'browserX',
'browserY',
'isAppDevToolsOpen',
'isBrowserDevToolsOpen',
'reporterWidth',
'specListWidth',
'showedNewProjectBanner',
'firstOpenedCypress',
'showedStudioModal',
'preferredOpener',
'ctReporterWidth',
'ctIsSpecsListOpen',
'isSpecsListOpen',
'ctSpecListWidth',
'firstOpened',
'lastOpened',
'promptsShown',
'watchForSpecChange',
'useDarkSidebar',
'preferredEditorBinary',
] as const
type Maybe<T> = T | null | undefined
export type AllowedState = Partial<{
appWidth: Maybe<number>
appHeight: Maybe<number>
appX: Maybe<number>
appY: Maybe<number>
isSpecsListOpen: Maybe<boolean>
autoScrollingEnabled: Maybe<boolean>
browserWidth: Maybe<number>
browserHeight: Maybe<number>
browserX: Maybe<number>
browserY: Maybe<number>
isAppDevToolsOpen: Maybe<boolean>
isBrowserDevToolsOpen: Maybe<boolean>
reporterWidth: Maybe<number>
specListWidth: Maybe<number>
showedNewProjectBanner: Maybe<boolean>
firstOpenedCypress: Maybe<number>
showedStudioModal: Maybe<boolean>
preferredOpener: Editor | undefined
ctReporterWidth: Maybe<number>
ctIsSpecsListOpen: Maybe<boolean>
ctSpecListWidth: Maybe<number>
firstOpened: Maybe<number>
lastOpened: Maybe<number>
promptsShown: Maybe<object>
watchForSpecChange: Maybe<boolean>
useDarkSidebar: Maybe<boolean>
preferredEditorBinary: Maybe<string>
}>