Skip to content

Commit f62a65f

Browse files
authored
internal: (studio) add socket listeners (#31470)
1 parent a40c148 commit f62a65f

File tree

9 files changed

+96
-3
lines changed

9 files changed

+96
-3
lines changed

packages/app/src/studio/studio-app-types.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export interface StudioPanelProps {
22
canAccessStudioAI: boolean
3+
useStudioEventManager?: StudioEventManagerShape
4+
useStudioAIStream?: StudioAIStreamShape
35
}
46

57
export type StudioPanelShape = (props: StudioPanelProps) => JSX.Element
@@ -9,3 +11,27 @@ export interface StudioAppDefaultShape {
911
// transferred to the Cypress app
1012
StudioPanel: StudioPanelShape
1113
}
14+
15+
export type CypressInternal = Cypress.Cypress &
16+
CyEventEmitter & {
17+
state: (key: string) => any
18+
}
19+
20+
export interface StudioEventManagerProps {
21+
Cypress: CypressInternal
22+
}
23+
24+
export type RunnerStatus = 'running' | 'finished'
25+
26+
export type StudioEventManagerShape = (props: StudioEventManagerProps) => {
27+
runnerStatus: RunnerStatus
28+
testBlock: string | null
29+
}
30+
31+
export interface StudioAIStreamProps {
32+
canAccessStudioAI: boolean
33+
AIOutputRef: { current: HTMLTextAreaElement | null }
34+
runnerStatus: RunnerStatus
35+
}
36+
37+
export type StudioAIStreamShape = (props: StudioAIStreamProps) => void

packages/server/lib/cloud/studio.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { StudioErrorReport, StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi } from '@packages/types'
22
import type { Router } from 'express'
3+
import type { Socket } from 'socket.io'
34
import fetch from 'cross-fetch'
45
import pkg from '@packages/root'
56
import os from 'os'
@@ -61,6 +62,12 @@ export class StudioManager implements StudioManagerShape {
6162
}
6263
}
6364

65+
addSocketListeners (socket: Socket): void {
66+
if (this._studioServer) {
67+
this.invokeSync('addSocketListeners', { isEssential: true }, socket)
68+
}
69+
}
70+
6471
async canAccessStudioAI (browser: Cypress.Browser): Promise<boolean> {
6572
return (await this.invokeAsync('canAccessStudioAI', { isEssential: true }, browser)) ?? false
6673
}

packages/server/lib/socket-base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ export class SocketBase {
407407
return socket.emit('dev-server:on-spec-updated')
408408
})
409409

410+
getCtx().coreData.studio?.addSocketListeners(socket)
411+
410412
socket.on('studio:init', async (cb) => {
411413
try {
412414
const { canAccessStudioAI } = await options.onStudioInit()

packages/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
"request-promise": "4.2.6",
202202
"sinon": "17.0.1",
203203
"snap-shot-it": "7.9.10",
204+
"socket.io": "4.0.1",
204205
"ssestream": "1.0.1",
205206
"supertest": "4.0.2",
206207
"supertest-session": "4.0.0",

packages/server/test/support/fixtures/cloud/studio/test-studio.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1-
import type { StudioServerShape, StudioServerDefaultShape, StudioBrowser } from '@packages/types'
1+
/// <reference types="cypress" />
2+
3+
import type { StudioServerShape, StudioServerDefaultShape } from '@packages/types'
24
import type Database from 'better-sqlite3'
35
import type { Router } from 'express'
6+
import type { Socket } from '@packages/socket'
47

58
class StudioServer implements StudioServerShape {
69
initializeRoutes (router: Router): void {
7-
10+
// This is a test implementation that does nothing
811
}
912

10-
canAccessStudioAI (browser: StudioBrowser): Promise<boolean> {
13+
canAccessStudioAI (browser: Cypress.Browser): Promise<boolean> {
1114
return Promise.resolve(true)
1215
}
1316

17+
addSocketListeners (socket: Socket): void {
18+
// This is a test implementation that does nothing
19+
}
20+
1421
setProtocolDb (db: Database.Database): void {
22+
// This is a test implementation that does nothing
1523
}
1624
}
1725

packages/server/test/unit/cloud/studio_spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,31 @@ describe('lib/cloud/studio', () => {
172172
})
173173
})
174174

175+
describe('addSocketListeners', () => {
176+
it('calls addSocketListeners on the studio server', () => {
177+
sinon.stub(studio, 'addSocketListeners')
178+
const mockSocket = { id: 'test-socket' } as any
179+
180+
studioManager.addSocketListeners(mockSocket)
181+
182+
expect(studio.addSocketListeners).to.be.calledWith(mockSocket)
183+
})
184+
185+
it('does not call addSocketListeners when studio server is not defined', () => {
186+
// Set _studioServer to undefined
187+
(studioManager as any)._studioServer = undefined
188+
189+
// Create a spy on invokeSync to verify it's not called
190+
const invokeSyncSpy = sinon.spy(studioManager, 'invokeSync')
191+
192+
const mockSocket = { id: 'test-socket' } as any
193+
194+
studioManager.addSocketListeners(mockSocket)
195+
196+
expect(invokeSyncSpy).to.not.be.called
197+
})
198+
})
199+
175200
describe('setProtocolDb', () => {
176201
it('sets the protocol database on the studio server', () => {
177202
const mockDb = { test: 'db' }

packages/server/test/unit/socket_spec.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ describe('lib/socket', () => {
7474
screenshotsFolder: this.cfg.screenshotsFolder,
7575
})
7676

77+
// Create a mock studio object with addSocketListeners method
78+
const mockStudio = {
79+
addSocketListeners: sinon.stub(),
80+
}
81+
82+
// Set the studio in the context
83+
ctx.coreData.studio = mockStudio
84+
7785
this.server.startWebsockets(this.automation, this.cfg, this.options)
7886
this.socket = this.server._socket
7987

@@ -580,6 +588,14 @@ describe('lib/socket', () => {
580588
})
581589
})
582590

591+
context('studio.addSocketListeners', () => {
592+
it('calls addSocketListeners on studio when socket connects', function () {
593+
// The socket connection was already established in the beforeEach so
594+
// we can just verify that addSocketListeners was called
595+
expect(ctx.coreData.studio.addSocketListeners).to.be.called
596+
})
597+
})
598+
583599
context('#isRunnerSocketConnected', function () {
584600
it('returns false when runner is not connected', function () {
585601
expect(this.socket.isRunnerSocketConnected()).to.eq(false)

packages/types/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"better-sqlite3": "11.5.0",
2020
"devtools-protocol": "0.0.1413303",
2121
"express": "4.21.0",
22+
"socket.io": "4.0.1",
2223
"typescript": "~5.4.5"
2324
},
2425
"files": [

packages/types/src/studio/studio-server-types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { Router } from 'express'
44
import type { AxiosInstance } from 'axios'
55
import type Database from 'better-sqlite3'
6+
import type { Socket } from 'socket.io'
67

78
interface RetryOptions {
89
maxAttempts: number
@@ -34,6 +35,12 @@ export interface StudioServerShape {
3435
initializeRoutes(router: Router): void
3536
canAccessStudioAI(browser: Cypress.Browser): Promise<boolean>
3637
setProtocolDb(database: Database.Database): void
38+
addSocketListeners(socket: Socket): void
39+
}
40+
41+
export interface StudioRouteOptions extends StudioServerOptions {
42+
router: Router
43+
getProtocolDb: () => Database.Database | undefined
3744
}
3845

3946
export interface StudioServerDefaultShape {

0 commit comments

Comments
 (0)