mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-22 07:00:22 -05:00
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:
@@ -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
|
||||
}>()
|
||||
|
||||
@@ -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>))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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,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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export * from './cache'
|
||||
|
||||
export * from './constants'
|
||||
|
||||
export * from './devicePreferences'
|
||||
export * from './preferences'
|
||||
|
||||
export * from './driver'
|
||||
|
||||
|
||||
@@ -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>
|
||||
}>
|
||||
Reference in New Issue
Block a user