diff --git a/packages/app/cypress/e2e/integration/settings.spec.ts b/packages/app/cypress/e2e/integration/settings.spec.ts
index 1fcf1a658ba1..5513775238b1 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.openE2E('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/package.json b/packages/app/package.json
index 9094fda68ab1..135e7529c0dc 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -42,6 +42,7 @@
"cross-env": "6.0.3",
"cypress-real-events": "1.4.0",
"faker": "5.5.3",
+ "fuzzysort": "^1.1.4",
"graphql": "^15.5.1",
"graphql-tag": "^2.12.5",
"javascript-time-ago": "2.3.8",
@@ -84,9 +85,11 @@
"@vueuse/core",
"bluebird",
"cypress-file-upload",
+ "cypress-real-events",
"dedent",
"events",
"fake-uuid",
+ "fuzzysort",
"graphql",
"graphql-relay",
"graphql/jsutils/Path",
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 @@
{{ t('settingsPage.editor.description') }}
-
+
+
+
+
+
+
+
+
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 }} pref.mutation.executeMutation({ value })"
/>
@@ -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/app/src/specs/DirectoryItem.vue b/packages/app/src/specs/DirectoryItem.vue
index ae27a2e913db..c3a4c39dc8f2 100644
--- a/packages/app/src/specs/DirectoryItem.vue
+++ b/packages/app/src/specs/DirectoryItem.vue
@@ -5,29 +5,20 @@
:class="{'transform rotate-270': !expanded}"
/>
-
- {{ directory }}
-
- /
-
-
+
diff --git a/packages/app/src/specs/HighlightedText.vue b/packages/app/src/specs/HighlightedText.vue
new file mode 100644
index 000000000000..31f0a693f4c4
--- /dev/null
+++ b/packages/app/src/specs/HighlightedText.vue
@@ -0,0 +1,35 @@
+
+
+
+ {{ char }}
+
+ {{ char }}
+
+
+
+
+
+
+
diff --git a/packages/app/src/specs/InlineSpecList.spec.tsx b/packages/app/src/specs/InlineSpecList.spec.tsx
index 400bfd73016e..be741a3ec9a2 100644
--- a/packages/app/src/specs/InlineSpecList.spec.tsx
+++ b/packages/app/src/specs/InlineSpecList.spec.tsx
@@ -5,8 +5,14 @@ let specs: Array = []
describe('InlineSpecList', () => {
beforeEach(() => {
+ cy.fixture('found-specs').then((foundSpecs) => specs = foundSpecs)
cy.mountFragment(Specs_InlineSpecListFragmentDoc, {
onResult: (ctx) => {
+ if (!ctx.specs) {
+ return ctx
+ }
+
+ ctx.specs.edges = specs.map((spec) => ({ __typename: 'SpecEdge', node: { __typename: 'Spec', ...spec, id: spec.relative } }))
specs = ctx.specs?.edges || []
return ctx
@@ -22,6 +28,12 @@ describe('InlineSpecList', () => {
})
it('should render a list of spec', () => {
- cy.get('a').should('exist').and('have.length', specs.length)
+ cy.get('li').should('exist').and('have.length', 7)
+ })
+
+ it('should support fuzzy sort', () => {
+ cy.get('input').type('scomeA')
+
+ cy.get('li').should('have.length', 2).should('contain', 'src/components').and('contain', 'Spec-A.spec.tsx')
})
})
diff --git a/packages/app/src/specs/InlineSpecList.vue b/packages/app/src/specs/InlineSpecList.vue
index 308dd0d004a1..05678c0074ec 100644
--- a/packages/app/src/specs/InlineSpecList.vue
+++ b/packages/app/src/specs/InlineSpecList.vue
@@ -1,21 +1,11 @@
@@ -24,12 +14,12 @@
diff --git a/packages/app/src/specs/InlineSpecListHeader.spec.tsx b/packages/app/src/specs/InlineSpecListHeader.spec.tsx
index 3d7045e2d5ad..c530fadd2c19 100644
--- a/packages/app/src/specs/InlineSpecListHeader.spec.tsx
+++ b/packages/app/src/specs/InlineSpecListHeader.spec.tsx
@@ -1,25 +1,18 @@
import InlineSpecListHeader from './InlineSpecListHeader.vue'
-import type { SpecViewType } from './SpecsList.vue'
import { ref } from 'vue'
describe('InlineSpecListHeader', () => {
beforeEach(() => {
const search = ref('')
- const tab = ref('flat')
const onAddSpec = cy.spy().as('new-spec')
cy.wrap(search).as('search')
- cy.wrap(tab).as('tab')
const methods = {
search: search.value,
'onUpdate:search': (val: string) => {
search.value = val
},
- tab: tab.value,
- 'onUpdate:tab': (val: SpecViewType) => {
- tab.value = val
- },
onAddSpec,
}
@@ -37,12 +30,6 @@ describe('InlineSpecListHeader', () => {
.get('@search').its('value').should('eq', searchString)
})
- it('should toggle radio group', () => {
- cy.get('[data-cy="file-tree-radio-option"]')
- .click()
- .get('@tab').its('value').should('eq', 'tree')
- })
-
it('should emit add spec', () => {
cy.get('[data-cy="runner-spec-list-add-spec"]').click()
.get('@new-spec')
diff --git a/packages/app/src/specs/InlineSpecListHeader.vue b/packages/app/src/specs/InlineSpecListHeader.vue
index 468aa5536c32..6758431af75e 100644
--- a/packages/app/src/specs/InlineSpecListHeader.vue
+++ b/packages/app/src/specs/InlineSpecListHeader.vue
@@ -1,5 +1,5 @@
-
+
-
-
))
-
- cy.get('a')
- .first()
- .focus()
- .type('{downarrow}')
- .focused()
- .contains('Spec-B')
- .type('{uparrow}')
- .focused()
- .contains('Spec-A')
- .type('{uparrow}')
- .focused()
- .contains('Spec-C')
- .type('{downarrow}')
- .focused()
- .contains('Spec-A')
- })
-
- it('should show relative path on hover', () => {
- const spec = specs[0]
- const relativeFolder = spec.relative.replace(`/${spec.baseName}`, '')
-
- cy.mount(() =>
- (
-
-
))
-
- cy.findByText(relativeFolder).should('not.be.visible')
- cy.get('a').realHover().findByText(relativeFolder).should('be.visible')
- })
-})
diff --git a/packages/app/src/specs/InlineSpecListRow.vue b/packages/app/src/specs/InlineSpecListRow.vue
deleted file mode 100644
index 482517737bbe..000000000000
--- a/packages/app/src/specs/InlineSpecListRow.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
- {{ relativeFolder }}
-
-
-
-
-
-
-
diff --git a/packages/app/src/specs/InlineSpecListTree.spec.tsx b/packages/app/src/specs/InlineSpecListTree.spec.tsx
index b5a3efe28e73..91645f7b0a83 100644
--- a/packages/app/src/specs/InlineSpecListTree.spec.tsx
+++ b/packages/app/src/specs/InlineSpecListTree.spec.tsx
@@ -1,14 +1,12 @@
-import type { FoundSpec } from '@packages/types'
+import type { FuzzyFoundSpec } from '@packages/frontend-shared/src/utils/spec-utils'
import { ref } from 'vue'
import InlineSpecListTree from './InlineSpecListTree.vue'
describe('InlineSpecListTree', () => {
- let foundSpecs: FoundSpec[]
+ let foundSpecs: FuzzyFoundSpec[]
before(() => {
- cy.fixture('found-specs').then((specs) => {
- foundSpecs = specs
- })
+ cy.fixture('found-specs').then((specs) => foundSpecs = specs.map((spec) => ({ ...spec, indexes: [] })))
})
it('should handle keyboard navigation', () => {
@@ -38,7 +36,7 @@ describe('InlineSpecListTree', () => {
))
- cy.get('li').first().should('contain', 'src / components')
+ cy.get('li').first().should('contain', 'src/components')
cy.then(() => specProp.value = foundSpecs.slice(0, 4))
cy.get('li').should('have.length', 7).first().should('contain', 'src').and('not.contain', '/components')
})
diff --git a/packages/app/src/specs/InlineSpecListTree.vue b/packages/app/src/specs/InlineSpecListTree.vue
index bfbc7c91ace8..d9bd5e079de8 100644
--- a/packages/app/src/specs/InlineSpecListTree.vue
+++ b/packages/app/src/specs/InlineSpecListTree.vue
@@ -22,7 +22,7 @@
item
"
:style="{ paddingLeft: `${(row.depth - 2) * 10 + 16}px` }"
- :class="{'before:hover:(transitional-all duration-250 ease-in-out border-r-indigo-300) before:focus:(border-r-indigo-300)': row.isLeaf, 'before:border-r-indigo-300': isCurrentSpec(row.data)}"
+ :class="{'before:hover:(transitional-all duration-250 ease-in-out border-r-indigo-300) before:focus:(border-r-indigo-300)': row.isLeaf, 'before:border-r-indigo-300': isCurrentSpec(row)}"
@click="onRowClick(row, idx)"
@keypress.enter.space.prevent="onRowClick(row, idx)"
>
@@ -30,23 +30,24 @@
v-if="row.isLeaf"
:file-name="row.data?.fileName || row.name"
:extension="row.data?.specFileExtension || ''"
- :selected="isCurrentSpec(row.data)"
+ :selected="isCurrentSpec(row)"
+ :indexes="getIndexes(row)"
class="pl-22px"
/>
diff --git a/packages/app/src/specs/SelectSpecListView.vue b/packages/app/src/specs/SelectSpecListView.vue
deleted file mode 100644
index 539aab43751f..000000000000
--- a/packages/app/src/specs/SelectSpecListView.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-
- emit('update:tab', val)"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/app/src/specs/SpecFileItem.spec.tsx b/packages/app/src/specs/SpecFileItem.spec.tsx
index efa1f3788435..a08b17f2dda6 100644
--- a/packages/app/src/specs/SpecFileItem.spec.tsx
+++ b/packages/app/src/specs/SpecFileItem.spec.tsx
@@ -7,16 +7,4 @@ describe('SpecFileItem', () => {
))
})
-
- it('should highlight text when selected', () => {
- const fileName = 'Hello'
-
- cy.mount(() =>
- (
-
-
))
-
- cy.findByText(fileName).should('have.class', 'text-white')
- cy.get('svg').should('have.class', 'icon-dark-indigo-300').and('have.class', 'icon-light-indigo-600')
- })
})
diff --git a/packages/app/src/specs/SpecFileItem.vue b/packages/app/src/specs/SpecFileItem.vue
index 60e0502b85a3..69ff300cdb21 100644
--- a/packages/app/src/specs/SpecFileItem.vue
+++ b/packages/app/src/specs/SpecFileItem.vue
@@ -4,21 +4,28 @@
class="text-base group-hocus:(icon-dark-indigo-300 icon-light-indigo-600) group-hover:children:(transition-all ease-in-out duration-250)"
:class="selected ? 'icon-dark-indigo-300 icon-light-indigo-600' : 'icon-dark-gray-800 icon-light-gray-1000'"
/>
-
- {{ fileName }}
-
- {{ extension }}
+ />
+
diff --git a/packages/app/src/specs/SpecItem.vue b/packages/app/src/specs/SpecItem.vue
index cb1c8d9e5272..83f292c8b131 100644
--- a/packages/app/src/specs/SpecItem.vue
+++ b/packages/app/src/specs/SpecItem.vue
@@ -8,23 +8,31 @@
class="icon-light-gray-50 icon-dark-gray-200 document-icon"
/>
-
-
{{ spec.fileName }}
-
{{ spec.specFileExtension }}
+
+
+
diff --git a/packages/app/src/specs/SpecsList.spec.tsx b/packages/app/src/specs/SpecsList.spec.tsx
index ddd0bae76484..10a5fe94117c 100644
--- a/packages/app/src/specs/SpecsList.spec.tsx
+++ b/packages/app/src/specs/SpecsList.spec.tsx
@@ -1,20 +1,8 @@
import SpecsList from './SpecsList.vue'
import { Specs_SpecsListFragmentDoc, SpecNode_SpecsListFragment } from '../generated/graphql-test'
-import { defaultMessages } from '@cy/i18n'
const rowSelector = '[data-testid=specs-list-row]'
const inputSelector = 'input'
-const fullFile = (s) => `${s.node.fileName}${s.node.specFileExtension}`
-const hasSpecText = (_node: JQuery, spec: SpecNode_SpecsListFragment) => {
- const $node = _node as JQuery
-
- expect($node).to.contain(spec.node.fileName)
- expect($node).to.contain(spec.node.fileExtension)
- expect($node).to.contain(spec.node.gitInfo?.author)
- expect($node).to.contain(defaultMessages.file.git.modified)
-
- return $node
-}
let specs: Array = []
@@ -32,45 +20,21 @@ describe('', { keystrokeDelay: 0 }, () => {
})
})
- it('renders specs', () => {
- cy.get(rowSelector).first()
- .should(($node) => hasSpecText($node, specs[0]))
-
- cy.percySnapshot()
- })
+ it('should filter specs', () => {
+ const spec = specs[0].node
- it('filters the specs', () => {
- cy.get(rowSelector).first()
- // Establish a baseline for what the spec rendered is
- .should(($node) => hasSpecText($node, specs[0]))
- .should('be.visible')
-
- // Cause an empty spec state
- .get(inputSelector).type('garbage ๐', { delay: 0 })
+ cy.get(inputSelector).type('garbage ๐', { delay: 0 })
.get(rowSelector)
.should('not.exist')
- // Clear the input, make sure that the right spec is showing up
- .get(inputSelector)
- .type('{selectall}{backspace}', { delay: 0 })
-
- // Check the spec and its values
- .get(rowSelector).first()
- .should('be.visible')
- .should(($node) => hasSpecText($node, specs[0]))
- .get(inputSelector)
- .type(fullFile(specs[0]), { delay: 0 })
- .get(rowSelector).first()
- .should('contain.text', specs[0].node.fileName)
- .and('contain.text', specs[0].node.fileExtension)
- .click()
+ cy.get(inputSelector).clear().type(spec.relative)
+ cy.get(rowSelector).first().should('contain', spec.relative.replace(`/${spec.fileName}${spec.specFileExtension}`, ''))
+ cy.get(rowSelector).last().should('contain', `${spec.fileName}${spec.specFileExtension}`)
})
- it('changes to tree view', () => {
- cy.get('[data-cy="file-tree-radio-option"]').click()
-
+ it('should close directories', () => {
// close all directories
- ;['src', 'packages', 'frontend', '__test__', 'lib', 'tests'].forEach((dir) => {
+ ['src', 'packages', 'frontend', '__test__', 'lib', 'tests'].forEach((dir) => {
cy.get('[data-cy="row-directory-depth-0"]').contains(dir).click()
})
diff --git a/packages/app/src/specs/SpecsList.vue b/packages/app/src/specs/SpecsList.vue
index 65e76bda5b87..76bbc6fff8a5 100644
--- a/packages/app/src/specs/SpecsList.vue
+++ b/packages/app/src/specs/SpecsList.vue
@@ -12,80 +12,60 @@
@newSpec="showModal = true"
/>
-
-
-
- {{ t('specPage.componentSpecsHeader') }}
-
-
-
{{ t('specPage.gitStatusHeader') }}
-
-
+
+
+ {{ t('specPage.componentSpecsHeader') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {{ t('specPage.gitStatusHeader') }}
+
+
+
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -95,16 +75,16 @@ import SpecsListHeader from './SpecsListHeader.vue'
import SpecListGitInfo from './SpecListGitInfo.vue'
import SpecsListRowItem from './SpecsListRowItem.vue'
import { gql } from '@urql/vue'
-import { computed, ref } from 'vue'
+import { computed, ComputedRef, ref, watch } from 'vue'
import CreateSpecModal from './CreateSpecModal.vue'
import type { Specs_SpecsListFragment, SpecListRowFragment } from '../generated/graphql'
import { useI18n } from '@cy/i18n'
-import { buildSpecTree } from '@packages/frontend-shared/src/utils/buildSpecTree'
+import { buildSpecTree, FuzzyFoundSpec, getIndexes } from '@packages/frontend-shared/src/utils/spec-utils'
import { useCollapsibleTree } from '@packages/frontend-shared/src/composables/useCollapsibleTree'
import RowDirectory from './RowDirectory.vue'
import SpecItem from './SpecItem.vue'
-import type { FoundSpec } from '@packages/types/src'
-import SelectSpecListView from './SelectSpecListView.vue'
+import fuzzySort from 'fuzzysort'
+import { useVirtualList } from '@packages/frontend-shared/src/composables/useVirtualList'
const { t } = useI18n()
@@ -142,42 +122,29 @@ fragment Specs_SpecsList on Query {
}
`
-export type SpecViewType = 'flat' | 'tree'
-
const props = defineProps<{
gql: Specs_SpecsListFragment
}>()
const showModal = ref(false)
const search = ref('')
-const specViewType = ref('flat')
-const updateTab = (tab: SpecViewType) => {
- specViewType.value = tab
-}
+const specs = computed(() => {
+ const specs = props.gql.currentProject?.specs?.edges.map((x) => x.node) || []
-const flatSpecList = computed(() => {
- if (search.value) {
- return props.gql.currentProject?.specs?.edges.filter((x) => x.node.absolute.toLowerCase().includes(search.value.toLowerCase()))
+ if (!search.value) {
+ return specs.map((spec) => ({ ...spec, indexes: [] as number[] }))
}
- return props.gql.currentProject?.specs?.edges
+ return fuzzySort
+ .go(search.value, specs || [], { key: 'relative' })
+ .map(({ obj, indexes }) => ({ ...obj, indexes }))
})
-const specTree = computed(() => buildSpecTree(props.gql.currentProject?.specs?.edges.map((x) => x.node) || []))
-const collapsible = useCollapsibleTree(specTree.value, { dropRoot: true })
+const specTree = computed(() => buildSpecTree(specs.value))
+const collapsible = computed(() => useCollapsibleTree(specTree.value, { dropRoot: true }))
-const treeSpecList = computed(() => {
- if (search.value) {
- // todo(lachlan) this will not show the folders of the filtered specs
- // we should update the useCollapsibleTree to have some kind of search
- // functionality, ideally with fuzzysort, that correctly returns the matched
- // specs and the directories to show.
- return collapsible.tree.filter(((item) => {
- return !item.hidden.value && item.data?.absolute.toLowerCase().includes(search.value.toLowerCase())
- }))
- }
+const treeSpecList = computed(() => collapsible.value.tree.filter(((item) => !item.hidden.value)))
- return collapsible.tree.filter(((item) => !item.hidden.value))
-})
+const { containerProps, list, wrapperProps } = useVirtualList(treeSpecList, { itemHeight: 40, overscan: 10 })
diff --git a/packages/data-context/src/DataActions.ts b/packages/data-context/src/DataActions.ts
index 01e668f736f6..0a28fa7d9bac 100644
--- a/packages/data-context/src/DataActions.ts
+++ b/packages/data-context/src/DataActions.ts
@@ -1,7 +1,16 @@
import type { DataContext } from '.'
-import { AppActions, ApplicationDataActions, ProjectConfigDataActions, ElectronActions, FileActions, ProjectActions, WizardActions } from './actions'
+import {
+ AppActions,
+ ApplicationDataActions,
+ ProjectConfigDataActions,
+ ElectronActions,
+ FileActions,
+ ProjectActions,
+ WizardActions,
+} from './actions'
import { AuthActions } from './actions/AuthActions'
import { DevActions } from './actions/DevActions'
+import { LocalSettingsActions } from './actions/LocalSettingsActions'
import { cached } from './util'
export class DataActions {
@@ -32,6 +41,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 876594f6dad6..92dbd49a1875 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, ApplicationDataApiShape, DataEmitterActions, ProjectApiShape } from './actions'
+import { AppApiShape, ApplicationDataApiShape, 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'
@@ -52,6 +52,7 @@ export interface DataContextConfig {
*/
appApi: AppApiShape
appDataApi: ApplicationDataApiShape
+ localSettingsApi: LocalSettingsApiShape
authApi: AuthApiShape
projectApi: ProjectApiShape
electronApi: ElectronApiShape
@@ -81,6 +82,10 @@ export class DataContext {
return this._config.electronApi
}
+ get localSettingsApi () {
+ return this._config.localSettingsApi
+ }
+
get isGlobalMode () {
return !this.currentProject
}
@@ -92,7 +97,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(),
]
@@ -295,6 +301,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/FileActions.ts b/packages/data-context/src/actions/FileActions.ts
index 87fbde11bb6d..d24321d8601d 100644
--- a/packages/data-context/src/actions/FileActions.ts
+++ b/packages/data-context/src/actions/FileActions.ts
@@ -25,4 +25,14 @@ export class FileActions {
await this.ctx.fs.remove(path.join(this.ctx.currentProject?.projectRoot, relativePath))
}
+
+ async checkIfFileExists (relativePath: string) {
+ if (!this.ctx.currentProject) {
+ throw new Error(`Cannot check file in project exists without active project`)
+ }
+
+ const filePath = path.join(this.ctx.currentProject?.projectRoot, relativePath)
+
+ return await this.ctx.fs.stat(filePath)
+ }
}
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/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts
index fecca6f83245..adf1c444d02f 100644
--- a/packages/data-context/src/actions/ProjectActions.ts
+++ b/packages/data-context/src/actions/ProjectActions.ts
@@ -1,4 +1,4 @@
-import type { CodeGenType, MutationAddProjectArgs, MutationAppCreateConfigFileArgs, MutationSetProjectPreferencesArgs, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen'
+import type { CodeGenType, MutationAddProjectArgs, MutationSetProjectPreferencesArgs, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen'
import type { FindSpecs, FoundBrowser, FoundSpec, FullConfig, LaunchArgs, LaunchOpts, OpenProjectLaunchOptions, Preferences, SettingsOptions } from '@packages/types'
import path from 'path'
import type { ActiveProjectShape, ProjectShape } from '../data/coreDataShape'
@@ -62,22 +62,38 @@ export class ProjectActions {
await this.clearActiveProject()
// Set initial properties, so we can set the config object on the active project
- this.setCurrentProjectProperties({
+ await this.setCurrentProjectProperties({
projectRoot,
title,
ctPluginsInitialized: false,
e2ePluginsInitialized: false,
config: null,
configChildProcess: null,
- })
-
- this.setCurrentProjectProperties({
- isCTConfigured: await this.ctx.project.isTestingTypeConfigured(projectRoot, 'component'),
- isE2EConfigured: await this.ctx.project.isTestingTypeConfigured(projectRoot, 'e2e'),
+ isMissingConfigFile: false,
preferences: await this.ctx.project.getProjectPreferences(title),
})
- return this
+ try {
+ // read the config and cache it
+ await this.ctx.project.getConfig(projectRoot)
+
+ this.setCurrentProjectProperties({
+ isCTConfigured: await this.ctx.project.isTestingTypeConfigured(projectRoot, 'component'),
+ isE2EConfigured: await this.ctx.project.isTestingTypeConfigured(projectRoot, 'e2e'),
+ })
+
+ return this
+ } catch (error: any) {
+ if (error.type === 'NO_DEFAULT_CONFIG_FILE_FOUND') {
+ this.setCurrentProjectProperties({
+ isMissingConfigFile: true,
+ })
+
+ return this
+ }
+
+ throw error
+ }
}
// Temporary: remove after other refactor lands
@@ -277,14 +293,29 @@ export class ProjectActions {
//
}
- createConfigFile (args: MutationAppCreateConfigFileArgs) {
+ async createConfigFile (type?: 'component' | 'e2e' | null) {
const project = this.ctx.currentProject
if (!project) {
throw Error(`Cannot create config file without currentProject.`)
}
- this.ctx.fs.writeFileSync(path.resolve(project.projectRoot, args.configFilename), args.code)
+ let obj: { [k: string]: object } = {
+ e2e: {},
+ component: {},
+ }
+
+ if (type) {
+ obj = {
+ [type]: {},
+ }
+ }
+
+ await this.ctx.fs.writeFile(path.resolve(project.projectRoot, 'cypress.config.js'), `module.exports = ${JSON.stringify(obj, null, 2)}`)
+
+ this.setCurrentProjectProperties({
+ isMissingConfigFile: false,
+ })
}
async clearLatestProjectCache () {
diff --git a/packages/data-context/src/actions/index.ts b/packages/data-context/src/actions/index.ts
index a3fbbc923a43..8d294c968f7a 100644
--- a/packages/data-context/src/actions/index.ts
+++ b/packages/data-context/src/actions/index.ts
@@ -8,6 +8,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..d8f29d93a34f 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,8 +49,9 @@ export interface ActiveProjectShape extends ProjectShape {
specs?: FoundSpec[]
config: Promise | null
configChildProcess?: ConfigChildProcessShape | null
- preferences?: Preferences| null
+ preferences?: Preferences | null
browsers: FoundBrowser[] | null
+ isMissingConfigFile: boolean
}
export interface AppDataShape {
@@ -82,6 +89,7 @@ export interface BaseErrorDataShape {
export interface CoreDataShape {
baseError: BaseErrorDataShape | null
dev: DevStateShape
+ localSettings: LocalSettingsDataShape
app: AppDataShape
currentProject: ActiveProjectShape | null
wizard: WizardDataShape
@@ -107,6 +115,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/sources/HtmlDataSource.ts b/packages/data-context/src/sources/HtmlDataSource.ts
index bfa406bc8f38..2e842087d3e7 100644
--- a/packages/data-context/src/sources/HtmlDataSource.ts
+++ b/packages/data-context/src/sources/HtmlDataSource.ts
@@ -35,7 +35,28 @@ export class HtmlDataSource {
}
async fetchAppHtml () {
- return this.ctx.fs.readFile(getPathToDist('app', 'index.html'), 'utf8')
+ if (process.env.CYPRESS_INTERNAL_VITE_DEV) {
+ const response = await this.ctx.util.fetch(`http://localhost:${process.env.CYPRESS_INTERNAL_VITE_APP_PORT}/`, { method: 'GET' })
+ const html = await response.text()
+
+ return html
+ }
+
+ // Check if the file exists. If it doesn't, it probably means that Vite is re-building
+ // and we should retry a few times until the file exists
+ let retryCount = 0
+ let err
+
+ while (retryCount < 5) {
+ try {
+ return await this.ctx.fs.readFile(getPathToDist('app', 'index.html'), 'utf8')
+ } catch (e) {
+ err = e
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+ }
+
+ throw err
}
/**
diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts
index ba136a319ca1..27a1cb72fae7 100644
--- a/packages/data-context/src/sources/ProjectDataSource.ts
+++ b/packages/data-context/src/sources/ProjectDataSource.ts
@@ -88,21 +88,29 @@ export class ProjectDataSource {
}
async isTestingTypeConfigured (projectRoot: string, testingType: 'e2e' | 'component') {
- const config = await this.getConfig(projectRoot)
+ try {
+ const config = await this.getConfig(projectRoot)
- if (!config) {
- return true
- }
+ if (!config) {
+ return true
+ }
- if (testingType === 'e2e') {
- return Boolean(Object.keys(config.e2e ?? {}).length)
- }
+ if (testingType === 'e2e') {
+ return Boolean(Object.keys(config.e2e ?? {}).length)
+ }
- if (testingType === 'component') {
- return Boolean(Object.keys(config.component ?? {}).length)
- }
+ if (testingType === 'component') {
+ return Boolean(Object.keys(config.component ?? {}).length)
+ }
- return false
+ return false
+ } catch (error: any) {
+ if (error.type === 'NO_DEFAULT_CONFIG_FILE_FOUND') {
+ return false
+ }
+
+ throw error
+ }
}
async getProjectPreferences (projectTitle: string) {
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/e2e/support/e2eProjectDirs.ts b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts
index 85b396d29782..7094447d1848 100644
--- a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts
+++ b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts
@@ -72,6 +72,7 @@ export const e2eProjectDirs = [
'todos',
'ts-installed',
'ts-proj',
+ 'ts-proj-compiler',
'ts-proj-custom-names',
'ts-proj-esmoduleinterop-true',
'ts-proj-tsconfig-in-plugins',
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-Mutation.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts
index 6e4213bbf1af..e56a53254c78 100644
--- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts
+++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts
@@ -18,12 +18,6 @@ export const stubMutation: MaybeResolver = {
return true
},
- appCreateConfigFile: (source, args, ctx) => {
- ctx.wizard.chosenManualInstall = true
- ctx.wizard.canNavigateForward = true
-
- return true
- },
clearActiveProject (source, args, ctx) {
ctx.currentProject = null
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/composables/useCollapsibleTree.ts b/packages/frontend-shared/src/composables/useCollapsibleTree.ts
index 52e14783790d..f52a829e45fc 100644
--- a/packages/frontend-shared/src/composables/useCollapsibleTree.ts
+++ b/packages/frontend-shared/src/composables/useCollapsibleTree.ts
@@ -68,10 +68,45 @@ function buildTree> (rawNode: T, options: UseCollapsibleTre
return acc
}
+function sortTree> (tree: T) {
+ if (tree.children.length > 0) {
+ tree.children = tree.children.sort((a, b) => {
+ if (a.children.length === 0 && b.children.length === 0) {
+ return a.name > b.name ? 1 : -1
+ }
+
+ if (a.children.length === 0) {
+ return 1
+ }
+
+ if (b.children.length === 0) {
+ return -1
+ }
+
+ return a.name > b.name ? 1 : -1
+ })
+
+ tree.children.forEach(sortTree)
+ }
+}
+
export function useCollapsibleTree > (tree: T, options: UseCollapsibleTreeOptions = {}) {
options.expandInitially = options.expandInitially || true
+ sortTree(tree)
const collapsibleTree = buildTree(tree, options)
+ collapsibleTree.sort((a, b) => {
+ if (a.parent === b.parent) {
+ if (a.children.length && !b.children.length) {
+ return -1
+ }
+
+ return 0
+ }
+
+ return 0
+ })
+
return {
tree: options.dropRoot ? collapsibleTree.slice(1) : collapsibleTree,
}
diff --git a/packages/frontend-shared/src/composables/useVirtualList.ts b/packages/frontend-shared/src/composables/useVirtualList.ts
index 710ba8e6817c..e7049b8c8c1a 100644
--- a/packages/frontend-shared/src/composables/useVirtualList.ts
+++ b/packages/frontend-shared/src/composables/useVirtualList.ts
@@ -1,5 +1,5 @@
-import { watch, Ref, ref, computed } from 'vue'
-import { useElementSize } from '@vueuse/core'
+import { watch, Ref, ref, computed, shallowRef, CSSProperties } from 'vue'
+import { MaybeRef, useElementSize } from '@vueuse/core'
export interface UseVirtualListOptions {
/**
@@ -21,21 +21,16 @@ export type UseVirtualListItem = {
index: number
}
-export function useVirtualList (list: Ref, options: UseVirtualListOptions) {
+export function useVirtualList (list: MaybeRef, options: UseVirtualListOptions) {
const containerRef: Ref = ref()
const size = useElementSize(containerRef)
const currentList: Ref[]> = ref([])
- const _list = ref(list)
+ const source = shallowRef(list)
const state: Ref = ref({ start: 0, end: 10 })
const { itemHeight, overscan = 5 } = options
- if (!itemHeight) {
- // eslint-disable-next-line
- console.warn('please enter a valid itemHeight')
- }
-
const getViewCapacity = (containerHeight: number) => {
if (typeof itemHeight === 'number') {
return Math.ceil(containerHeight / itemHeight)
@@ -45,7 +40,7 @@ export function useVirtualList (list: Ref, options: UseVirtualLis
let sum = 0
let capacity = 0
- for (let i = start; i < _list.value.length; i++) {
+ for (let i = start; i < source.value.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
@@ -66,7 +61,7 @@ export function useVirtualList (list: Ref, options: UseVirtualLis
let sum = 0
let offset = 0
- for (let i = 0; i < _list.value.length; i++) {
+ for (let i = 0; i < source.value.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
@@ -91,33 +86,32 @@ export function useVirtualList (list: Ref, options: UseVirtualLis
state.value = {
start: from < 0 ? 0 : from,
- end: to > _list.value.length ? _list.value.length : to,
+ end: to > source.value.length
+ ? source.value.length
+ : to,
}
- currentList.value = _list.value.slice(state.value.start, state.value.end).map((ele, index) => {
+ currentList.value = source.value
+ .slice(state.value.start, state.value.end)
+ .map((ele, index) => {
return {
- data: ele as T,
+ data: ele,
index: index + state.value.start,
}
})
}
}
- watch(list, (newList) => {
- _list.value = newList as typeof _list.value
- calculateRange()
- })
-
- watch([size.width, size.height], () => {
+ watch([size.width, size.height, list], () => {
calculateRange()
})
const totalHeight = computed(() => {
if (typeof itemHeight === 'number') {
- return _list.value.length * itemHeight
+ return source.value.length * itemHeight
}
- return _list.value.reduce((sum, _, index) => sum + itemHeight(index), 0)
+ return source.value.reduce((sum, _, index) => sum + itemHeight(index), 0)
})
const getDistanceTop = (index: number) => {
@@ -127,7 +121,9 @@ export function useVirtualList (list: Ref, options: UseVirtualLis
return height
}
- const height = _list.value.slice(0, index).reduce((sum, _, i) => sum + itemHeight(i), 0)
+ const height = source.value
+ .slice(0, index)
+ .reduce((sum, _, i) => sum + itemHeight(i), 0)
return height
}
@@ -150,7 +146,7 @@ export function useVirtualList (list: Ref, options: UseVirtualLis
}
})
- const containerStyle: Partial = { overflowY: 'auto' }
+ const containerStyle: CSSProperties = { overflowY: 'auto' }
return {
list: currentList,
diff --git a/packages/frontend-shared/src/gql-components/topnav/TopNav.vue b/packages/frontend-shared/src/gql-components/topnav/TopNav.vue
index a229222e1039..dbdea7f0a137 100644
--- a/packages/frontend-shared/src/gql-components/topnav/TopNav.vue
+++ b/packages/frontend-shared/src/gql-components/topnav/TopNav.vue
@@ -211,9 +211,7 @@ import { allBrowsersIcons } from '@packages/frontend-shared/src/assets/browserLo
import { gql, useMutation } from '@urql/vue'
import { TopNavFragment, TopNav_LaunchOpenProjectDocument, TopNav_SetBrowserDocument } from '../../generated/graphql'
import { useI18n } from '@cy/i18n'
-import { computed, ref } from 'vue'
-// eslint-disable-next-line no-duplicate-imports
-import type { Ref } from 'vue'
+import { computed, ref, Ref } from 'vue'
const { t } = useI18n()
import { onClickOutside, onKeyStroke, useTimeAgo } from '@vueuse/core'
import DocsMenuContent from './DocsMenuContent.vue'
@@ -284,34 +282,43 @@ const props = defineProps<{
showBrowsers?: boolean
}>()
-const docsMenuVariant: Ref<'main' | 'orchestration' | 'ci'> = ref('main')
-
-const promptsEl: Ref = ref(null)
-
-// reset docs menu if click or keyboard navigation happens outside
-// so it doesn't reopen on the one of the prompts
-
-const versions = computed(() => {
+const versions = (() => {
if (!props.gql.versions) {
- return null
+ return ref(null)
}
- return {
- current: {
- released: useTimeAgo(new Date(props.gql.versions.current.released)).value,
- version: props.gql.versions.current.version,
- },
- latest: {
- released: useTimeAgo(new Date(props.gql.versions.latest.released)).value,
- version: props.gql.versions.latest.version,
- },
- }
-})
+ const currentReleased = useTimeAgo(new Date(props.gql.versions.current.released))
+ const latestReleased = useTimeAgo(new Date(props.gql.versions.latest.released))
+
+ return computed(() => {
+ if (!props.gql.versions) {
+ return null
+ }
+
+ return {
+ current: {
+ released: currentReleased.value,
+ version: props.gql.versions.current.version,
+ },
+ latest: {
+ released: latestReleased.value,
+ version: props.gql.versions.latest.version,
+ },
+ }
+ })
+})()
const runningOldVersion = computed(() => {
return props.gql.versions ? props.gql.versions.current.released < props.gql.versions.latest.released : false
})
+const docsMenuVariant: Ref<'main' | 'orchestration' | 'ci'> = ref('main')
+
+const promptsEl: Ref = ref(null)
+
+// reset docs menu if click or keyboard navigation happens outside
+// so it doesn't reopen on the one of the prompts
+
onClickOutside(promptsEl, () => {
setTimeout(() => {
// reset the content of the menu when
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/frontend-shared/src/utils/buildSpecTree.ts b/packages/frontend-shared/src/utils/spec-utils.ts
similarity index 64%
rename from packages/frontend-shared/src/utils/buildSpecTree.ts
rename to packages/frontend-shared/src/utils/spec-utils.ts
index 1c7d1aa7ca1b..e2d2bd1be681 100644
--- a/packages/frontend-shared/src/utils/buildSpecTree.ts
+++ b/packages/frontend-shared/src/utils/spec-utils.ts
@@ -1,6 +1,10 @@
import type { FoundSpec } from '@packages/types'
+import type { UseCollapsibleTreeNode } from '../composables/useCollapsibleTree'
+
+export type FuzzyFoundSpec = FoundSpec & { indexes: number[] }
export type SpecTreeNode = {
+ id: string
name: string
children: SpecTreeNode[]
isLeaf: boolean
@@ -8,9 +12,8 @@ export type SpecTreeNode = {
data?: T
}
-export function buildSpecTree (specs: FoundSpec[], root: SpecTreeNode = { name: '/', isLeaf: false, children: [] }) {
+export function buildSpecTree (specs: FoundSpec[], root: SpecTreeNode = { name: '', isLeaf: false, children: [], id: '' }) {
specs.forEach((spec) => buildSpecTreeRecursive(spec.relative, root, spec))
-
collapseEmptyChildren(root)
return root
@@ -18,9 +21,10 @@ export function buildSpecTree (specs: FoundSpec[], root: Sp
export function buildSpecTreeRecursive (path: string, tree: SpecTreeNode, data?: T) {
const [firstFile, ...rest] = path.split('/')
+ const id = tree.id ? [tree.id, firstFile].join('/') : firstFile
if (rest.length < 1) {
- tree.children.push({ name: firstFile, isLeaf: true, children: [], parent: tree, data })
+ tree.children.push({ name: firstFile, isLeaf: true, children: [], parent: tree, data, id })
return tree
}
@@ -33,7 +37,7 @@ export function buildSpecTreeRecursive (path: string, tree:
return tree
}
- const newTree = buildSpecTreeRecursive(rest.join('/'), { name: firstFile, isLeaf: false, children: [], parent: tree }, data)
+ const newTree = buildSpecTreeRecursive(rest.join('/'), { name: firstFile, isLeaf: false, children: [], parent: tree, id, data }, data)
tree.children.push(newTree)
@@ -52,8 +56,20 @@ function collapseEmptyChildren (node: SpecTreeNode) {
// so we check node.parent.parent
if (node.parent && node.parent.parent && (node.parent.children.length === 1)) {
node.parent.name = [node.parent.name, node.name].join('/')
+ node.parent.id = [node.parent.id, node.name].join('/')
node.parent.children = node.children
}
return
}
+
+export function getIndexes (row: UseCollapsibleTreeNode>) {
+ const indexes = row.data?.indexes || []
+
+ const maxIndex = row.id.length - 1
+ const minIndex = maxIndex - row.name.length + 1
+
+ const res = indexes?.filter((index) => index >= minIndex && index <= maxIndex)
+
+ return res.map((idx) => idx - minIndex)
+}
diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql
index c2d86e27a6bf..2af041789053 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(
@@ -430,9 +456,6 @@ type Mutation {
"""Create an Index HTML file for a new component testing project"""
appCreateComponentIndexHtml(template: String!): Boolean
-
- """Create a Cypress config file for a new project"""
- appCreateConfigFile(code: String!, configFilename: String!): Boolean
clearActiveProject: Boolean
"""
@@ -482,9 +505,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 +631,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..d9eccdb9d244 100644
--- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts
+++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts
@@ -84,12 +84,16 @@ export const mutation = mutationType({
})),
},
resolve: async (_, args, ctx) => {
+ if (ctx.coreData.currentProject?.isMissingConfigFile) {
+ await ctx.actions.project.createConfigFile(args.input.testingType)
+ }
+
if (args.input.testingType) {
- await ctx.actions.wizard.setTestingType(args.input.testingType)
+ ctx.actions.wizard.setTestingType(args.input.testingType)
}
if (args.input.direction) {
- await ctx.actions.wizard.navigate(args.input.direction)
+ ctx.actions.wizard.navigate(args.input.direction)
}
},
})
@@ -133,17 +137,6 @@ export const mutation = mutationType({
},
})
- t.liveMutation('appCreateConfigFile', {
- args: {
- code: nonNull('String'),
- configFilename: nonNull('String'),
- },
- description: 'Create a Cypress config file for a new project',
- resolve: async (_, args, ctx) => {
- await ctx.actions.project.createConfigFile(args)
- },
- })
-
t.liveMutation('appCreateComponentIndexHtml', {
args: {
template: nonNull('String'),
@@ -300,6 +293,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/launchpad/cypress/e2e/integration/config-files-error-handling.spec.ts b/packages/launchpad/cypress/e2e/integration/config-files-error-handling.spec.ts
index 7c69ab8ab855..f4d6d3c87be0 100644
--- a/packages/launchpad/cypress/e2e/integration/config-files-error-handling.spec.ts
+++ b/packages/launchpad/cypress/e2e/integration/config-files-error-handling.spec.ts
@@ -1,16 +1,14 @@
describe('Config files error handling', () => {
- beforeEach(() => {
+ it('it handles multiples config files', () => {
cy.openE2E('pristine-with-config-file')
cy.visitLaunchpad()
- })
- it('it handles multiple config files', () => {
+ cy.get('[data-cy-testingType=e2e]').click()
+
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('cypress.config.ts', 'export default {}')
})
- cy.get('[data-cy-testingType=e2e]').click()
-
cy.get('body').should('contain.text', 'Configuration Files')
cy.get('button').contains('Continue').click()
@@ -28,13 +26,16 @@ describe('Config files error handling', () => {
})
it('it handles legacy config file', () => {
+ cy.openE2E('pristine-with-config-file')
+ cy.visitLaunchpad()
+
+ cy.get('[data-cy-testingType=e2e]').click()
+
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('cypress.json', '{}')
await ctx.actions.file.removeFileInProject('cypress.config.js')
})
- cy.get('[data-cy-testingType=e2e]').click()
-
cy.get('body').should('contain.text', 'Configuration Files')
cy.get('button').contains('Continue').click()
@@ -62,12 +63,15 @@ describe('Config files error handling', () => {
})
it('it handles config files with legacy config file in same project', () => {
+ cy.openE2E('pristine-with-config-file')
+ cy.visitLaunchpad()
+
+ cy.get('[data-cy-testingType=e2e]').click()
+
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('cypress.json', '{}')
})
- cy.get('[data-cy-testingType=e2e]').click()
-
cy.get('body').should('contain.text', 'Configuration Files')
cy.get('button').contains('Continue').click()
@@ -84,4 +88,22 @@ describe('Config files error handling', () => {
cy.get('body')
.should('not.contain.text', 'Cypress Configuration Error')
})
+
+ it('creates config file if it do not exist', () => {
+ cy.openE2E('pristine')
+ cy.visitLaunchpad()
+
+ cy.get('[data-cy-testingType=e2e]').click()
+
+ cy.get('body').should('contain.text', 'Configuration Files')
+
+ cy.get('button').contains('Continue').click()
+
+ cy.get('body')
+ .should('contain.text', 'Initializing Config')
+
+ cy.withCtx(async (ctx) => {
+ await ctx.actions.file.checkIfFileExists('cypress.config.js')
+ })
+ })
})
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 9c38dcbb04b2..c0414bb9a1cb 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'
@@ -17,6 +17,8 @@ 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 app_data from './util/app_data'
+import { getDevicePreferences, setDevicePreference } from './util/device_preferences'
+import { getUserEditor, setUserEditor } from './util/editors'
const { getBrowsers, ensureAndGetByNameOrPath } = browserUtils
@@ -126,6 +128,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,
},
})