Skip to content

Commit c46b3c3

Browse files
internal: Add support for Studio first use instructions (#32197)
* add support for studioFirstUseInstructionsDismissed * add support for setting global or project saved state * add get:app:state socket event * handle getSavedState errors * check that config is initialized
1 parent e59949f commit c46b3c3

File tree

5 files changed

+131
-13
lines changed

5 files changed

+131
-13
lines changed

packages/server/lib/project-base.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,8 @@ export class ProjectBase extends EE {
403403
onReloadBrowser: options.onReloadBrowser,
404404
onFocusTests: options.onFocusTests,
405405
onSpecChanged: options.onSpecChanged,
406-
onSavedStateChanged: (state: any) => this.saveState(state),
406+
getSavedState: this.getSavedState.bind(this),
407+
onSavedStateChanged: this.saveState.bind(this),
407408
closeExtraTargets: this.closeExtraTargets,
408409

409410
onStudioInit: async () => {
@@ -714,17 +715,27 @@ export class ProjectBase extends EE {
714715

715716
// Saved state
716717

718+
async getSavedState (options: { type: 'global' | 'project' } = { type: 'project' }) {
719+
if (!this.cfg) {
720+
throw new Error('Missing project config trying to get saved state')
721+
}
722+
723+
const state = await savedState.create(options.type === 'project' ? this.projectRoot : undefined, this.cfg.isTextTerminal)
724+
725+
return state.get()
726+
}
727+
717728
// forces saving of project's state by first merging with argument
718-
async saveState (stateChanges = {}) {
729+
async saveState (stateChanges = {}, options: { type: 'global' | 'project' } = { type: 'project' }) {
719730
if (!this.cfg) {
720-
throw new Error('Missing project config')
731+
throw new Error('Missing project config trying to save state')
721732
}
722733

723-
if (!this.projectRoot) {
734+
if (options.type === 'project' && !this.projectRoot) {
724735
throw new Error('Missing project root')
725736
}
726737

727-
let state = await savedState.create(this.projectRoot, this.cfg.isTextTerminal)
738+
let state = await savedState.create(options.type === 'project' ? this.projectRoot : undefined, this.cfg.isTextTerminal)
728739

729740
state.set(stateChanges)
730741
this.cfg.state = await state.get()

packages/server/lib/socket-base.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export class SocketBase {
157157
onChromiumRun () {},
158158
onReloadBrowser () {},
159159
closeExtraTargets () {},
160+
getSavedState () {},
160161
onSavedStateChanged () {},
161162
onTestFileChange () {},
162163
onCaptureVideoFrames () {},
@@ -581,8 +582,21 @@ export class SocketBase {
581582
return cb(s || {}, cachedTestState)
582583
})
583584

585+
socket.on('get:app:state', async (opts, cb) => {
586+
try {
587+
const state = await options.getSavedState(opts)
588+
589+
cb({ data: state })
590+
} catch (error) {
591+
cb({ error: errors.cloneErr(error) })
592+
}
593+
})
594+
584595
socket.on('save:app:state', (state, cb) => {
585-
options.onSavedStateChanged(state)
596+
const opts = state.__options
597+
const stateWithoutOptions = _.omit(state, '__options')
598+
599+
options.onSavedStateChanged(stateWithoutOptions, opts)
586600

587601
// we only use the 'ack' here in tests
588602
if (cb) {

packages/server/test/unit/project_spec.js

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,51 @@ describe('lib/project-base', () => {
8080
expect(p.projectRoot).to.eq(path.resolve(path.join('..', 'foo', 'bar')))
8181
})
8282

83+
context('#getSavedState', () => {
84+
beforeEach(async function () {
85+
const globalState = await savedState.create()
86+
87+
await globalState.remove()
88+
await globalState.set({ reporterWidth: 400 })
89+
90+
const projectState = await savedState.create(this.project.projectRoot)
91+
92+
await projectState.remove()
93+
await projectState.set({ reporterWidth: 500 })
94+
})
95+
96+
it('returns global state when type is global', async function () {
97+
const state = await this.project.getSavedState({ type: 'global' })
98+
99+
expect(state).to.deep.eq({ reporterWidth: 400 })
100+
})
101+
102+
it('returns project state when type is project', async function () {
103+
const state = await this.project.getSavedState({ type: 'project' })
104+
105+
expect(state).to.deep.eq({ reporterWidth: 500 })
106+
})
107+
108+
it('returns project state when type is undefined', async function () {
109+
const state = await this.project.getSavedState()
110+
111+
expect(state).to.deep.eq({ reporterWidth: 500 })
112+
})
113+
})
114+
83115
context('#saveState', function () {
84-
beforeEach(function () {
116+
beforeEach(async function () {
85117
const supportFile = path.join('the', 'save', 'state', 'test')
86118

87119
this.project.cfg = { supportFile }
88120

89-
return savedState.create(this.project.projectRoot)
90-
.then((state) => state.remove())
121+
const globalState = await savedState.create()
122+
123+
await globalState.remove()
124+
125+
const projectState = await savedState.create(this.project.projectRoot)
126+
127+
await projectState.remove()
91128
})
92129

93130
afterEach(function () {
@@ -119,6 +156,33 @@ describe('lib/project-base', () => {
119156
.then(() => this.project.saveState({ appWidth: 'modified' }))
120157
.then((state) => expect(state).to.deep.eq({ appWidth: 'modified' }))
121158
})
159+
160+
it('saves global state when type is global', async function () {
161+
await this.project.saveState({ reporterWidth: 1 }, { type: 'global' })
162+
163+
const state = await savedState.create()
164+
.then((state) => state.get())
165+
166+
expect(state).to.deep.eq({ reporterWidth: 1 })
167+
})
168+
169+
it('saves project state when type is project', async function () {
170+
await this.project.saveState({ reporterWidth: 2 }, { type: 'project' })
171+
172+
const state = await savedState.create(this.project.projectRoot)
173+
.then((state) => state.get())
174+
175+
expect(state).to.deep.eq({ reporterWidth: 2 })
176+
})
177+
178+
it('saves project state when type is undefined', async function () {
179+
await this.project.saveState({ reporterWidth: 3 })
180+
181+
const state = await savedState.create(this.project.projectRoot)
182+
.then((state) => state.get())
183+
184+
expect(state).to.deep.eq({ reporterWidth: 3 })
185+
})
122186
})
123187

124188
context('#initializeConfig', () => {

packages/server/test/unit/socket_spec.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('lib/socket', () => {
6464
})
6565
.then(() => {
6666
this.options = {
67+
getSavedState: sinon.stub(),
6768
onSavedStateChanged: sinon.spy(),
6869
onStudioInit: sinon.stub(),
6970
onStudioDestroy: sinon.stub(),
@@ -250,12 +251,38 @@ describe('lib/socket', () => {
250251
})
251252
})
252253

254+
context('on(get:app:state)', () => {
255+
it('calls getSavedState with options and returns the state', function (done) {
256+
this.options.getSavedState.resolves({ reporterWidth: 500 })
257+
258+
this.client.emit('get:app:state', { type: 'global' }, (resp) => {
259+
expect(this.options.getSavedState).to.be.calledWith({ type: 'global' })
260+
expect(resp.data).to.deep.eq({ reporterWidth: 500 })
261+
262+
done()
263+
})
264+
})
265+
266+
it('handles errors thrown by getSavedState', function (done) {
267+
const err = new Error('boom')
268+
269+
this.options.getSavedState.rejects(err)
270+
271+
this.client.emit('get:app:state', { type: 'global' }, (resp) => {
272+
expect(this.options.getSavedState).to.be.calledWith({ type: 'global' })
273+
expect(resp.error).to.deep.eq(errors.cloneErr(err))
274+
275+
done()
276+
})
277+
})
278+
})
279+
253280
context('on(save:app:state)', () => {
254-
it('calls onSavedStateChanged with the state', function (done) {
255-
return this.client.emit('save:app:state', { reporterWidth: 500 }, () => {
256-
expect(this.options.onSavedStateChanged).to.be.calledWith({ reporterWidth: 500 })
281+
it('calls onSavedStateChanged with the state and options', function (done) {
282+
this.client.emit('save:app:state', { reporterWidth: 500, __options: { type: 'global' } }, () => {
283+
expect(this.options.onSavedStateChanged).to.be.calledWith({ reporterWidth: 500 }, { type: 'global' })
257284

258-
return done()
285+
done()
259286
})
260287
})
261288
})

packages/types/src/preferences.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
5151
'notifyWhenRunStarts',
5252
'notifyWhenRunStartsFailing',
5353
'notifyWhenRunCompletes',
54+
'studioFirstUseInstructionsDismissed',
5455
] as const
5556

5657
type Maybe<T> = T | null | undefined
@@ -93,4 +94,5 @@ export type AllowedState = Partial<{
9394
notifyWhenRunStarts: Maybe<boolean>
9495
notifyWhenRunStartsFailing: Maybe<boolean>
9596
notifyWhenRunCompletes: Maybe<NotifyWhenRunCompletes[]>
97+
studioFirstUseInstructionsDismissed: Maybe<boolean>
9698
}>

0 commit comments

Comments
 (0)