fix: cache last-used browser by project, name, and channel (#20760)

Co-authored-by: Barthélémy Ledoux <bart@cypress.io>
This commit is contained in:
Zach Bloomquist
2022-04-06 15:55:30 -04:00
committed by GitHub
parent 6a19bbf617
commit 4fd151014d
27 changed files with 182 additions and 136 deletions
@@ -80,7 +80,7 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
cy.contains('li', 'Electron').click()
cy.withCtx((ctx) => {
expect(ctx.coreData.chosenBrowser?.displayName).eq('Electron')
expect(ctx.coreData.activeBrowser?.displayName).eq('Electron')
expect(ctx.actions.project.launchProject).to.have.been.called
})
})
@@ -127,7 +127,7 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
it('shows current browser and possible browsers', () => {
cy.mountFragment(SpecRunnerHeaderFragmentDoc, {
onResult: (ctx) => {
ctx.currentBrowser = ctx.browsers?.find((x) => x.displayName === 'Chrome') ?? null
ctx.activeBrowser = ctx.browsers?.find((x) => x.displayName === 'Chrome') ?? null
},
render: (gqlVal) => {
return renderWithGql(gqlVal)
@@ -153,7 +153,7 @@ fragment SpecRunnerHeader on CurrentProject {
id
currentTestingType
currentBrowser {
activeBrowser {
id
displayName
majorVersion
@@ -163,14 +163,6 @@ fragment SpecRunnerHeader on CurrentProject {
}
`
gql`
fragment SpecRunnerHeader_Browser on Browser {
id
name
displayName
}
`
const { t } = useI18n()
const autStore = useAutStore()
@@ -203,7 +195,7 @@ const selectorPlaygroundStore = useSelectorPlaygroundStore()
const togglePlayground = () => _togglePlayground(autIframe)
// Have to spread gql props since binding it to v-model causes error when testing
const selectedBrowser = ref({ ...props.gql.currentBrowser })
const selectedBrowser = ref({ ...props.gql.activeBrowser })
const activeSpecPath = specStore.activeSpec?.absolute
@@ -65,7 +65,7 @@ gql`
fragment AutomationMissing on CurrentProject {
id
...VerticalBrowserListItems
currentBrowser {
activeBrowser {
id
displayName
majorVersion
@@ -76,6 +76,6 @@ fragment AutomationMissing on CurrentProject {
const props = withDefaults(defineProps<{ gql: AutomationMissingFragment | null }>(), { gql: null })
// Have to spread gql props since binding it to v-model causes error when testing
const selectedBrowser = ref({ ...props.gql?.currentBrowser })
const selectedBrowser = ref({ ...props.gql?.activeBrowser })
</script>
@@ -26,7 +26,6 @@ exports['@packages/data-context initializeData initializes 1'] = {
},
"app": {
"currentTestingType": null,
"refreshingBrowsers": {},
"browsers": [
{
"path": "/dev/chrome"
@@ -57,7 +56,7 @@ exports['@packages/data-context initializeData initializes 1'] = {
"history": [
"welcome"
],
"chosenBrowser": {
"activeBrowser": {
"path": "/dev/chrome"
}
},
+13 -22
View File
@@ -17,19 +17,24 @@ export interface ApplicationDataApiShape {
export class AppActions {
constructor (private ctx: DataContext) {}
setActiveBrowser (browser: FoundBrowser) {
this.ctx.coreData.chosenBrowser = browser
async setActiveBrowser (browser: FoundBrowser) {
this.ctx.coreData.activeBrowser = browser
await this.ctx._apis.projectApi.insertProjectPreferencesToCache(this.ctx.lifecycleManager.projectTitle, {
lastBrowser: {
name: browser.name,
channel: browser.channel,
},
})
}
setActiveBrowserById (id: string) {
async setActiveBrowserById (id: string) {
const browserId = this.ctx.fromId(id, 'Browser')
// Ensure that this is a valid ID to set
const browser = this.ctx.lifecycleManager.browsers?.find((b) => this.idForBrowser(b as FoundBrowser) === browserId)
if (browser) {
this.setActiveBrowser(browser)
}
if (!browser) throw new Error('no browser in setActiveBrowserById')
await this.setActiveBrowser(browser)
}
async removeAppDataDir () {
@@ -43,18 +48,4 @@ export class AppActions {
private idForBrowser (obj: FoundBrowser) {
return this.ctx.browser.idForBrowser(obj)
}
/**
* Check whether we have a current chosen browser, and it matches up to one of the
* ones we have selected
*/
private hasValidChosenBrowser (browsers: FoundBrowser[]) {
const chosenBrowser = this.ctx.coreData.chosenBrowser
if (!chosenBrowser) {
return false
}
return browsers.some((b) => this.idForBrowser(b) === this.idForBrowser(chosenBrowser))
}
}
@@ -209,9 +209,16 @@ export class ProjectActions {
testingType = testingType || this.ctx.coreData.currentTestingType
if (!testingType) {
return null
}
// It's strange to have no testingType here, but `launchProject` is called when switching testing types,
// so it needs to short-circuit and return here.
// TODO: Untangle this. https://cypress-io.atlassian.net/browse/UNIFY-1528
if (!testingType) return
this.ctx.coreData.currentTestingType = testingType
const browser = this.ctx.coreData.activeBrowser
if (!browser) throw new Error('Missing browser in launchProject')
let activeSpec: FoundSpec | undefined
@@ -219,17 +226,6 @@ export class ProjectActions {
activeSpec = this.ctx.project.getCurrentSpecByAbsolute(specPath)
}
// Ensure that we have loaded browsers to choose from
if (this.ctx.appData.refreshingBrowsers) {
await this.ctx.appData.refreshingBrowsers
}
const browser = this.ctx.coreData.chosenBrowser ?? this.ctx.appData.browsers?.[0]
if (!browser) {
return null
}
// launchProject expects a spec when opening browser for url navigation.
// We give it an empty spec if none is passed so as to land on home page
const emptySpec: Cypress.Spec = {
@@ -239,8 +235,6 @@ export class ProjectActions {
specType: testingType === 'e2e' ? 'integration' : 'component',
}
this.ctx.coreData.currentTestingType = testingType
await this.api.launchProject(browser, activeSpec ?? emptySpec, options)
return
@@ -258,10 +252,6 @@ export class ProjectActions {
return this.api.removeProjectFromCache(projectRoot)
}
syncProjects () {
//
}
async createConfigFile (type?: 'component' | 'e2e' | null) {
const project = this.ctx.currentProject
@@ -252,9 +252,7 @@ export class ProjectLifecycleManager {
}
}
if (this.ctx.coreData.cliBrowser) {
await this.setActiveBrowser(this.ctx.coreData.cliBrowser)
}
await this.setInitialActiveBrowser()
if (this._currentTestingType && finalConfig.specPattern) {
await this.ctx.actions.project.setSpecsFoundBySpecPattern({
@@ -272,6 +270,43 @@ export class ProjectLifecycleManager {
})
}
/**
* Sets the initial `activeBrowser` depending on these criteria, in order of preference:
* 1. The value of `--browser` passed via CLI.
* 2. The last browser selected in `open` mode (by name and channel) for this project.
* 3. The first browser found.
*/
async setInitialActiveBrowser () {
if (this.ctx.coreData.cliBrowser) {
await this.setActiveBrowserByNameOrPath(this.ctx.coreData.cliBrowser)
// only finish if `cliBrowser` was successfully set - we must have an activeBrowser once this function resolves
if (this.ctx.coreData.activeBrowser) return
}
// lastBrowser is cached per-project.
const prefs = await this.ctx.project.getProjectPreferences(path.basename(this.projectRoot))
const browsers = await this.ctx.browser.machineBrowsers()
if (!browsers[0]) throw new Error('No browsers available in setInitialActiveBrowser, cannot set initial active browser')
this.ctx.coreData.activeBrowser = (prefs?.lastBrowser && browsers.find((b) => {
return b.name === prefs.lastBrowser!.name && b.channel === prefs.lastBrowser!.channel
})) || browsers[0]
}
private async setActiveBrowserByNameOrPath (nameOrPath: string) {
try {
const browser = await this.ctx._apis.browserApi.ensureAndGetByNameOrPath(nameOrPath)
this.ctx.coreData.activeBrowser = browser
} catch (e) {
const error = e as CypressError
this.ctx.onWarning(error)
}
}
async refreshLifecycle () {
assert(this._projectRoot, 'Cannot reload config without a project root')
assert(this._configManager, 'Cannot reload config without a config manager')
@@ -312,21 +347,6 @@ export class ProjectLifecycleManager {
return this._configManager.initializeConfig()
}
private async setActiveBrowser (cliBrowser: string) {
// When we're starting up, if we've chosen a browser to run with, check if it exists
this.ctx.coreData.cliBrowser = null
try {
const browser = await this.ctx._apis.browserApi.ensureAndGetByNameOrPath(cliBrowser)
this.ctx.coreData.chosenBrowser = browser ?? null
} catch (e) {
const error = e as CypressError
this.ctx.onWarning(error)
}
}
/**
* When we set the current project, we need to cleanup the
* previous project that might have existed. We use this as the
@@ -56,8 +56,6 @@ export interface AppDataShape {
isInGlobalMode: boolean
browsers: ReadonlyArray<FoundBrowser> | null
projects: ProjectShape[]
refreshingBrowsers: Promise<FoundBrowser[]> | null
refreshingNodePath: Promise<string> | null
nodePath: Maybe<string>
browserStatus: BrowserStatus
relaunchBrowser: boolean
@@ -111,7 +109,7 @@ export interface ForceReconfigureProjectDataShape {
export interface CoreDataShape {
cliBrowser: string | null
cliTestingType: string | null
chosenBrowser: FoundBrowser | null
activeBrowser: FoundBrowser | null
machineBrowsers: Promise<FoundBrowser[]> | null
servers: {
appServer?: Maybe<Server>
@@ -158,10 +156,8 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
},
app: {
isInGlobalMode: Boolean(modeOptions.global),
refreshingBrowsers: null,
browsers: null,
projects: [],
refreshingNodePath: null,
nodePath: modeOptions.userNodePath,
browserStatus: 'closed',
relaunchBrowser: false,
@@ -201,7 +197,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
},
},
warnings: [],
chosenBrowser: null,
activeBrowser: null,
user: null,
electron: {
app: null,
@@ -16,7 +16,7 @@ const platform = os.platform()
export interface BrowserApiShape {
close(): Promise<any>
ensureAndGetByNameOrPath(nameOrPath: string): Promise<FoundBrowser | undefined>
ensureAndGetByNameOrPath(nameOrPath: string): Promise<FoundBrowser>
getBrowsers(): Promise<FoundBrowser[]>
focusActiveBrowserWindow(): Promise<any>
}
@@ -32,10 +32,8 @@ export class BrowserDataSource {
if (!this.ctx.coreData.machineBrowsers) {
const p = this.ctx._apis.browserApi.getBrowsers()
this.ctx.coreData.machineBrowsers = p.then((browsers) => {
if (browsers[0]) {
this.ctx.coreData.chosenBrowser = browsers[0]
}
this.ctx.coreData.machineBrowsers = p.then(async (browsers) => {
if (!browsers[0]) throw new Error('no browsers found in machineBrowsers')
return browsers
}).catch((e) => {
@@ -56,11 +54,11 @@ export class BrowserDataSource {
}
isSelected (obj: FoundBrowser) {
if (!this.ctx.coreData.chosenBrowser) {
if (!this.ctx.coreData.activeBrowser) {
return false
}
return this.idForBrowser(this.ctx.coreData.chosenBrowser) === this.idForBrowser(obj)
return this.idForBrowser(this.ctx.coreData.activeBrowser) === this.idForBrowser(obj)
}
isFocusSupported (obj: FoundBrowser) {
@@ -112,7 +112,7 @@ export class HtmlDataSource {
window.__CYPRESS_MODE__ = ${JSON.stringify(this.ctx.isRunMode ? 'run' : 'open')};
window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)};
window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}'
window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.chosenBrowser)}
window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)}
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET
? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';`
: ''
@@ -0,0 +1,63 @@
import { expect } from 'chai'
import type { DataContext } from '../../../src'
import { createTestDataContext } from '../helper'
import sinon from 'sinon'
describe('ProjectLifecycleManager', () => {
let ctx: DataContext
beforeEach(() => {
ctx = createTestDataContext('open')
})
context('#setInitialActiveBrowser', () => {
const browsers = [
{ name: 'electron', family: 'chromium', channel: 'stable', displayName: 'Electron' },
{ name: 'chrome', family: 'chromium', channel: 'stable', displayName: 'Chrome' },
{ name: 'chrome', family: 'chromium', channel: 'beta', displayName: 'Chrome Beta' },
]
beforeEach(() => {
ctx.coreData.activeBrowser = undefined
ctx.coreData.cliBrowser = undefined
ctx._apis.browserApi.getBrowsers = sinon.stub().resolves(browsers)
ctx.project.getProjectPreferences = sinon.stub().resolves(null)
// @ts-expect-error
ctx.lifecycleManager._projectRoot = 'foo'
})
it('falls back to browsers[0] if preferences and cliBrowser do not exist', async () => {
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
})
it('uses cli --browser option if one is set', async () => {
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('electron').resolves(browsers[0])
ctx.coreData.cliBrowser = 'electron'
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.cliBrowser).to.eq('electron')
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
})
it('uses lastBrowser if available', async () => {
ctx.project.getProjectPreferences = sinon.stub().resolves({ lastBrowser: { name: 'chrome', channel: 'beta' } })
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'chrome', displayName: 'Chrome Beta' })
})
it('falls back to browsers[0] if lastBrowser does not exist', async () => {
ctx.project.getProjectPreferences = sinon.stub().resolves({ lastBrowser: { name: 'chrome', channel: 'dev' } })
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
})
})
})
@@ -271,6 +271,9 @@ function startAppServer (mode: 'component' | 'e2e' = 'e2e') {
await isInitialized.promise
if (!ctx.lifecycleManager.browsers?.length) throw new Error('No browsers available in startAppServer')
await ctx.actions.app.setActiveBrowser(ctx.lifecycleManager.browsers[0])
await ctx.actions.project.launchProject(o.mode, { url: o.url })
return ctx.appServerPort
@@ -18,7 +18,7 @@ export interface ClientTestContext {
currentProject: CurrentProject | null
projects: GlobalProject[]
app: {
currentBrowser: Browser | null
activeBrowser: Browser | null
browsers: Browser[] | null
}
versions: VersionData
@@ -29,7 +29,6 @@ export interface ClientTestContext {
chosenFramework: WizardFrontendFramework | null
chosenManualInstall: boolean
allBundlers: WizardBundler[]
chosenBrowser: null
warnings: []
}
migration: {}
@@ -52,7 +51,7 @@ export function makeClientTestContext (): ClientTestContext {
projects: [stubGlobalProject, createTestGlobalProject('another-test-project')],
app: {
browsers: stubBrowsers,
currentBrowser: stubBrowsers[0],
activeBrowser: stubBrowsers[0],
},
versions: {
__typename: 'VersionData',
@@ -77,7 +76,6 @@ export function makeClientTestContext (): ClientTestContext {
chosenFramework: null,
chosenManualInstall: false,
allBundlers,
chosenBrowser: null,
warnings: [],
},
user: null,
@@ -47,7 +47,7 @@ export const createTestCurrentProject = (title: string, currentProject: Partial<
__typename: 'CodeGenGlobs',
component: '**/*.vue',
},
currentBrowser: stubBrowsers[0],
activeBrowser: stubBrowsers[0],
browsers: stubBrowsers,
isDefaultSpecPattern: true,
browserStatus: 'closed',
@@ -102,19 +102,19 @@
</ExternalLink>
<TopNavList
v-if="props.gql?.currentProject?.currentBrowser && showBrowsers"
v-if="props.gql?.currentProject?.activeBrowser && showBrowsers"
>
<template #heading="{ open }">
<img
class="w-16px filter group-hocus:grayscale-0"
data-cy="top-nav-active-browser-icon"
:class="open ? 'grayscale-0' : 'grayscale'"
:src="allBrowsersIcons[props.gql?.currentProject?.currentBrowser?.displayName] || allBrowsersIcons.generic"
:src="allBrowsersIcons[props.gql?.currentProject?.activeBrowser?.displayName] || allBrowsersIcons.generic"
>
<span
data-cy="top-nav-active-browser"
class="font-medium whitespace-nowrap"
>{{ props.gql.currentProject?.currentBrowser?.displayName }} {{ props.gql.currentProject?.currentBrowser?.majorVersion }}</span>
>{{ props.gql.currentProject?.activeBrowser?.displayName }} {{ props.gql.currentProject?.activeBrowser?.majorVersion }}</span>
</template>
<VerticalBrowserListItems
:gql="props.gql.currentProject"
@@ -230,7 +230,7 @@ fragment TopNav on Query {
id
title
packageManager
currentBrowser {
activeBrowser {
id
displayName
majorVersion
+3 -6
View File
@@ -355,6 +355,9 @@ enum CodeLanguageEnum {
The currently opened Cypress project, represented by a cypress.config.{ts|js} file
"""
type CurrentProject implements Node & ProjectLike {
"""The currently selected browser for the project"""
activeBrowser: Browser
"""The current branch of the project"""
branch: String
@@ -380,9 +383,6 @@ type CurrentProject implements Node & ProjectLike {
"""Config File Absolute Path"""
configFileAbsolutePath: String
"""The currently selected browser for the application"""
currentBrowser: Browser
"""The mode the interactive runner was launched in"""
currentTestingType: TestingTypeEnum
@@ -1129,9 +1129,6 @@ interface ProjectLike {
"""Preferences specific to a project"""
type ProjectPreferences {
"""The preferred browser to launch"""
browserPath: String
"""The preferred testing type to start in"""
testingType: String
}
@@ -42,11 +42,11 @@ export const CurrentProject = objectType({
resolve: (_, args, ctx) => ctx.coreData.currentTestingType,
})
t.field('currentBrowser', {
t.field('activeBrowser', {
type: Browser,
description: 'The currently selected browser for the application',
description: 'The currently selected browser for the project',
resolve: (source, args, ctx) => {
return ctx.coreData.chosenBrowser
return ctx.coreData.activeBrowser
},
})
@@ -229,8 +229,8 @@ export const mutation = mutationType({
description: 'ID of the browser that we want to set',
})),
},
resolve (_, args, ctx) {
ctx.actions.app.setActiveBrowserById(args.id)
async resolve (_, args, ctx) {
await ctx.actions.app.setActiveBrowserById(args.id)
return ctx.lifecycleManager
},
@@ -7,9 +7,5 @@ export const ProjectPreferences = objectType({
t.string('testingType', {
description: 'The preferred testing type to start in',
})
t.string('browserPath', {
description: 'The preferred browser to launch',
})
},
})
@@ -254,7 +254,7 @@ describe('Choose a Browser Page', () => {
})
})
it('subscribes to changes to browserStatus/currentBrowser through the browserStatusUpdated subscription', () => {
it('subscribes to changes to browserStatus/activeBrowser through the browserStatusUpdated subscription', () => {
cy.openProject('launchpad', ['--e2e'])
cy.visitLaunchpad()
@@ -111,7 +111,7 @@ describe('<OpenBrowserList />', () => {
cy.mountFragment(OpenBrowserListFragmentDoc, {
onResult: (res) => {
res.browserStatus = 'open'
res.currentBrowser!.isFocusSupported = false
res.activeBrowser!.isFocusSupported = false
},
render: (gqlVal) => {
return (
@@ -129,10 +129,15 @@ describe('<OpenBrowserList />', () => {
cy.percySnapshot()
})
it('hides action buttons when currentBrowser is null', () => {
it('throws when activeBrowser is null', (done) => {
cy.once('uncaught:exception', (err) => {
expect(err.message).to.include('Missing activeBrowser in selectedBrowserId')
done()
})
cy.mountFragment(OpenBrowserListFragmentDoc, {
onResult: (res) => {
res.currentBrowser = null
res.activeBrowser = null
},
render: (gqlVal) => {
return (
@@ -143,11 +148,5 @@ describe('<OpenBrowserList />', () => {
</div>)
},
})
cy.get('button[data-cy="launch-button"]').should('not.exist')
cy.contains('button', defaultMessages.openBrowser.focus).should('not.exist')
cy.contains('button', defaultMessages.openBrowser.close).should('not.exist')
cy.percySnapshot()
})
})
@@ -110,7 +110,7 @@
{{ browserText.running }}
</Button>
<Button
v-if="props.gql.currentBrowser?.isFocusSupported"
v-if="props.gql.activeBrowser?.isFocusSupported"
size="lg"
type="button"
variant="outline"
@@ -181,7 +181,7 @@ mutation OpenBrowserList_SetBrowser($id: ID!) {
gql`
fragment OpenBrowserList on CurrentProject {
id
currentBrowser {
activeBrowser {
id
isFocusSupported
}
@@ -202,7 +202,7 @@ subscription OpenBrowserList_browserStatusChange {
browserStatusChange {
id
browserStatus
currentBrowser {
activeBrowser {
id
isFocusSupported
}
@@ -237,9 +237,10 @@ const setBrowser = useMutation(OpenBrowserList_SetBrowserDocument)
const selectedBrowserId = computed({
get: () => {
// NOTE: The currentBrowser is set to the first detected browser
// found during project initialization. It should always be defined.
return props.gql.currentBrowser?.id
// NOTE: The activeBrowser is set during project initialization. It should always be defined.
if (!props.gql.activeBrowser) throw new Error('Missing activeBrowser in selectedBrowserId')
return props.gql.activeBrowser.id
},
set (browserId) {
if (browserId) {
+2 -2
View File
@@ -245,8 +245,8 @@ const parseBrowserOption = (opt) => {
}
}
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers: FoundBrowser[]): Bluebird<FoundBrowser | undefined>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers: FoundBrowser[]): Bluebird<FoundBrowser[] | undefined>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers: FoundBrowser[]): Bluebird<FoundBrowser>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers: FoundBrowser[]): Bluebird<FoundBrowser[]>
function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, browsers: FoundBrowser[] = []) {
const findBrowsers = browsers.length ? Bluebird.resolve(browsers) : getBrowsers()
+1 -1
View File
@@ -75,7 +75,7 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
const windows = require('./gui/windows')
const originalIsMainWindowFocused = windows.isMainWindowFocused()
const onLoginFlowComplete = async () => {
if (originalIsMainWindowFocused || !ctx.browser.isFocusSupported(ctx.coreData.chosenBrowser)) {
if (originalIsMainWindowFocused || !ctx.browser.isFocusSupported(ctx.coreData.activeBrowser)) {
windows.focusMainWindow()
} else {
await ctx.actions.browser.focusActiveBrowserWindow()
+3 -3
View File
@@ -4,9 +4,9 @@ export interface Cache {
USER: CachedUser
}
export interface Preferences {
testingType: 'e2e' | 'component' | null
}
import type { AllowedState } from './preferences'
export type Preferences = AllowedState
export interface CachedUser {
authToken: string
+3
View File
@@ -33,6 +33,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
'promptsShown',
'preferredEditorBinary',
'isSideNavigationOpen',
'lastBrowser',
] as const
type Maybe<T> = T | null | undefined
@@ -64,4 +65,6 @@ export type AllowedState = Partial<{
promptsShown: Maybe<object>
preferredEditorBinary: Maybe<string>
isSideNavigationOpen: Maybe<boolean>
testingType: 'e2e' | 'component'
lastBrowser: { name: string, channel: string }
}>