diff --git a/packages/app/src/studio/studio-app-types.ts b/packages/app/src/studio/studio-app-types.ts index 0ae6a0be93..9f050b260d 100644 --- a/packages/app/src/studio/studio-app-types.ts +++ b/packages/app/src/studio/studio-app-types.ts @@ -1,8 +1,10 @@ export interface StudioPanelProps { canAccessStudioAI: boolean - onStudioPanelClose: () => void - useStudioEventManager?: StudioEventManagerShape + onStudioPanelClose?: () => void + useRunnerStatus?: RunnerStatusShape + useTestContentRetriever?: TestContentRetrieverShape useStudioAIStream?: StudioAIStreamShape + useCypress?: CypressShape } export type StudioPanelShape = (props: StudioPanelProps) => JSX.Element @@ -18,21 +20,49 @@ CyEventEmitter & { state: (key: string) => any } -export interface StudioEventManagerProps { - Cypress: CypressInternal +export interface TestBlock { + content: string + testBodyPosition: { + contentStart: number + contentEnd: number + indentation: number + } } export type RunnerStatus = 'running' | 'finished' -export type StudioEventManagerShape = (props: StudioEventManagerProps) => { +export interface RunnerStatusProps { + Cypress: CypressInternal +} + +export interface CypressProps { + Cypress: CypressInternal +} + +export type CypressShape = (props: CypressProps) => { + currentCypress: CypressInternal +} + +export type RunnerStatusShape = (props: RunnerStatusProps) => { runnerStatus: RunnerStatus - testBlock: string | null } export interface StudioAIStreamProps { canAccessStudioAI: boolean AIOutputRef: { current: HTMLTextAreaElement | null } runnerStatus: RunnerStatus + testCode?: string + isCreatingNewTest: boolean } export type StudioAIStreamShape = (props: StudioAIStreamProps) => void + +export interface TestContentRetrieverProps { + Cypress: CypressInternal +} + +export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => { + isLoading: boolean + testBlock: TestBlock | null + isCreatingNewTest: boolean +} diff --git a/packages/server/lib/StudioLifecycleManager.ts b/packages/server/lib/StudioLifecycleManager.ts index fa36c0ca99..db6117e787 100644 --- a/packages/server/lib/StudioLifecycleManager.ts +++ b/packages/server/lib/StudioLifecycleManager.ts @@ -11,6 +11,8 @@ import { reportStudioError } from './cloud/api/studio/report_studio_error' import { CloudRequest } from './cloud/api/cloud_request' import { isRetryableError } from './cloud/network/is_retryable_error' import { asyncRetry } from './util/async_retry' +import { postStudioSession } from './cloud/api/studio/post_studio_session' + const debug = Debug('cypress:server:studio-lifecycle-manager') const routes = require('./cloud/routes') @@ -42,41 +44,11 @@ export class StudioLifecycleManager { }): void { debug('Initializing studio manager') - const studioManagerPromise = getAndInitializeStudioManager({ + const studioManagerPromise = this.createStudioManager({ projectId, cloudDataSource, - }).then(async (studioManager) => { - if (studioManager.status === 'ENABLED') { - debug('Cloud studio is enabled - setting up protocol') - const protocolManager = new ProtocolManager() - const protocolUrl = routes.apiRoutes.captureProtocolCurrent() - const script = await api.getCaptureProtocolScript(protocolUrl) - - await protocolManager.prepareProtocol(script, { - runId: 'studio', - projectId: cfg.projectId, - testingType: cfg.testingType, - cloudApi: { - url: routes.apiUrl, - retryWithBackoff: api.retryWithBackoff, - requestPromise: api.rp, - }, - projectConfig: _.pick(cfg, ['devServerPublicPathRoute', 'port', 'proxyUrl', 'namespace']), - mountVersion: api.runnerCapabilities.protocolMountVersion, - debugData, - mode: 'studio', - }) - - studioManager.protocolManager = protocolManager - } else { - debug('Cloud studio is not enabled - skipping protocol setup') - } - - debug('Studio is ready') - this.studioManager = studioManager - this.callRegisteredListeners() - - return studioManager + cfg, + debugData, }).catch(async (error) => { debug('Error during studio manager setup: %o', error) @@ -125,6 +97,59 @@ export class StudioLifecycleManager { return await this.studioManagerPromise } + private async createStudioManager ({ + projectId, + cloudDataSource, + cfg, + debugData, + }: { + projectId?: string + cloudDataSource: CloudDataSource + cfg: Cfg + debugData: any + }): Promise { + const studioSession = await postStudioSession({ + projectId, + }) + + const studioManager = await getAndInitializeStudioManager({ + studioUrl: studioSession.studioUrl, + projectId, + cloudDataSource, + }) + + if (studioManager.status === 'ENABLED') { + debug('Cloud studio is enabled - setting up protocol') + const protocolManager = new ProtocolManager() + const script = await api.getCaptureProtocolScript(studioSession.protocolUrl) + + await protocolManager.prepareProtocol(script, { + runId: 'studio', + projectId: cfg.projectId, + testingType: cfg.testingType, + cloudApi: { + url: routes.apiUrl, + retryWithBackoff: api.retryWithBackoff, + requestPromise: api.rp, + }, + projectConfig: _.pick(cfg, ['devServerPublicPathRoute', 'port', 'proxyUrl', 'namespace']), + mountVersion: api.runnerCapabilities.protocolMountVersion, + debugData, + mode: 'studio', + }) + + studioManager.protocolManager = protocolManager + } else { + debug('Cloud studio is not enabled - skipping protocol setup') + } + + debug('Studio is ready') + this.studioManager = studioManager + this.callRegisteredListeners() + + return studioManager + } + private callRegisteredListeners () { if (!this.studioManager) { throw new Error('Studio manager has not been initialized') diff --git a/packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts b/packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts index b6ab51618a..fa88464124 100644 --- a/packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts +++ b/packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts @@ -14,8 +14,12 @@ import { PUBLIC_KEY_VERSION } from '../../constants' import { CloudRequest } from '../cloud_request' import type { CloudDataSource } from '@packages/data-context/src/sources' +interface Options { + studioUrl: string + projectId?: string +} + const pkg = require('@packages/root') -const routes = require('../../routes') const _delay = linearDelay(500) @@ -24,11 +28,11 @@ export const studioPath = path.join(os.tmpdir(), 'cypress', 'studio') const bundlePath = path.join(studioPath, 'bundle.tar') const serverFilePath = path.join(studioPath, 'server', 'index.js') -const downloadStudioBundleToTempDirectory = async (projectId?: string): Promise => { +const downloadStudioBundleToTempDirectory = async ({ studioUrl, projectId }: Options): Promise => { let responseSignature: string | null = null await (asyncRetry(async () => { - const response = await fetch(routes.apiRoutes.studio() as string, { + const response = await fetch(studioUrl, { // @ts-expect-error - this is supported agent, method: 'GET', @@ -90,7 +94,7 @@ const getTarHash = (): Promise => { }) } -export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?: string } = {}): Promise<{ studioHash: string | undefined }> => { +export const retrieveAndExtractStudioBundle = async ({ studioUrl, projectId }: Options): Promise<{ studioHash: string | undefined }> => { // First remove studioPath to ensure we have a clean slate await fs.promises.rm(studioPath, { recursive: true, force: true }) await ensureDir(studioPath) @@ -106,7 +110,7 @@ export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId? return { studioHash: undefined } } - await downloadStudioBundleToTempDirectory(projectId) + await downloadStudioBundleToTempDirectory({ studioUrl, projectId }) const studioHash = await getTarHash() @@ -118,7 +122,7 @@ export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId? return { studioHash } } -export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource }: { projectId?: string, cloudDataSource: CloudDataSource }): Promise => { +export const getAndInitializeStudioManager = async ({ studioUrl, projectId, cloudDataSource }: { studioUrl: string, projectId?: string, cloudDataSource: CloudDataSource }): Promise => { let script: string const cloudEnv = (process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production' @@ -128,7 +132,7 @@ export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource let studioHash: string | undefined try { - ({ studioHash } = await retrieveAndExtractStudioBundle({ projectId })) + ({ studioHash } = await retrieveAndExtractStudioBundle({ studioUrl, projectId })) script = await readFile(serverFilePath, 'utf8') diff --git a/packages/server/lib/cloud/api/studio/post_studio_session.ts b/packages/server/lib/cloud/api/studio/post_studio_session.ts new file mode 100644 index 0000000000..0bc754a683 --- /dev/null +++ b/packages/server/lib/cloud/api/studio/post_studio_session.ts @@ -0,0 +1,42 @@ +import { asyncRetry, linearDelay } from '../../../util/async_retry' +import { isRetryableError } from '../../network/is_retryable_error' +import fetch from 'cross-fetch' +import os from 'os' + +const pkg = require('@packages/root') +const routes = require('../../routes') as typeof import('../../routes') + +interface GetStudioSessionOptions { + projectId?: string +} + +const _delay = linearDelay(500) + +export const postStudioSession = async ({ projectId }: GetStudioSessionOptions) => { + return await (asyncRetry(async () => { + const response = await fetch(routes.apiRoutes.studioSession(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-os-name': os.platform(), + 'x-cypress-version': pkg.version, + }, + body: JSON.stringify({ projectSlug: projectId, studioMountVersion: 1, protocolMountVersion: 2 }), + }) + + if (!response.ok) { + throw new Error('Failed to create studio session') + } + + const data = await response.json() + + return { + studioUrl: data.studioUrl, + protocolUrl: data.protocolUrl, + } + }, { + maxAttempts: 3, + retryDelay: _delay, + shouldRetry: isRetryableError, + }))() +} diff --git a/packages/server/lib/cloud/routes.ts b/packages/server/lib/cloud/routes.ts index 5d0d96a99b..e675cde7b4 100644 --- a/packages/server/lib/cloud/routes.ts +++ b/packages/server/lib/cloud/routes.ts @@ -16,8 +16,7 @@ const CLOUD_ENDPOINTS = { instanceStdout: 'instances/:id/stdout', instanceArtifacts: 'instances/:id/artifacts', captureProtocolErrors: 'capture-protocol/errors', - captureProtocolCurrent: 'capture-protocol/script/current.js', - studio: 'studio/bundle/current.tgz', + studioSession: 'studio/session', studioErrors: 'studio/errors', exceptions: 'exceptions', telemetry: 'telemetry', diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 9b4c8c05ae..392c3b4a9f 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -493,11 +493,12 @@ export class ProjectBase extends EE { const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio() - if (studio?.protocolManager) { + await studio?.destroy() + + if (this.protocolManager) { await browsers.closeProtocolConnection({ browser: this.browser, foundBrowsers: this.options.browsers }) this.protocolManager?.close() this.protocolManager = undefined - await studio.destroy() } }, diff --git a/packages/server/test/unit/StudioLifecycleManager_spec.ts b/packages/server/test/unit/StudioLifecycleManager_spec.ts index 3eec4c9048..3b4de67cc9 100644 --- a/packages/server/test/unit/StudioLifecycleManager_spec.ts +++ b/packages/server/test/unit/StudioLifecycleManager_spec.ts @@ -9,6 +9,7 @@ import * as getAndInitializeStudioManagerModule from '../../lib/cloud/api/studio import * as reportStudioErrorPath from '../../lib/cloud/api/studio/report_studio_error' import ProtocolManager from '../../lib/cloud/protocol' const api = require('../../lib/cloud/api').default +import * as postStudioSessionModule from '../../lib/cloud/api/studio/post_studio_session' // Helper to wait for next tick in event loop const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) @@ -19,6 +20,7 @@ describe('StudioLifecycleManager', () => { let mockCtx: DataContext let mockCloudDataSource: CloudDataSource let mockCfg: Cfg + let postStudioSessionStub: sinon.SinonStub let getAndInitializeStudioManagerStub: sinon.SinonStub let getCaptureProtocolScriptStub: sinon.SinonStub let prepareProtocolStub: sinon.SinonStub @@ -53,6 +55,12 @@ describe('StudioLifecycleManager', () => { namespace: '__cypress', } as unknown as Cfg + postStudioSessionStub = sinon.stub(postStudioSessionModule, 'postStudioSession') + postStudioSessionStub.resolves({ + studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz', + protocolUrl: 'https://cloud.cypress.io/capture-protocol/script/def.js', + }) + getAndInitializeStudioManagerStub = sinon.stub(getAndInitializeStudioManagerModule, 'getAndInitializeStudioManager') getAndInitializeStudioManagerStub.resolves(mockStudioManager) @@ -107,7 +115,11 @@ describe('StudioLifecycleManager', () => { await studioReadyPromise - expect(getCaptureProtocolScriptStub).to.be.calledWith('http://localhost:1234/capture-protocol/script/current.js') + expect(postStudioSessionStub).to.be.calledWith({ + projectId: 'abc123', + }) + + expect(getCaptureProtocolScriptStub).to.be.calledWith('https://cloud.cypress.io/capture-protocol/script/def.js') expect(prepareProtocolStub).to.be.calledWith('console.log("hello")', { runId: 'studio', projectId: 'abc123', diff --git a/packages/server/test/unit/cloud/api/studio/get_and_initialize_studio_manager_spec.ts b/packages/server/test/unit/cloud/api/studio/get_and_initialize_studio_manager_spec.ts index 7c4ea4c8a1..ca6a4bd89d 100644 --- a/packages/server/test/unit/cloud/api/studio/get_and_initialize_studio_manager_spec.ts +++ b/packages/server/test/unit/cloud/api/studio/get_and_initialize_studio_manager_spec.ts @@ -127,7 +127,7 @@ describe('getAndInitializeStudioManager', () => { process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1' delete process.env.CYPRESS_LOCAL_STUDIO_PATH - await getAndInitializeStudioManager({ projectId: '12345', cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud }) expect(studioManagerSetupStub).to.be.calledWith(sinon.match({ shouldEnableStudio: true, @@ -138,7 +138,7 @@ describe('getAndInitializeStudioManager', () => { delete process.env.CYPRESS_ENABLE_CLOUD_STUDIO process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio' - await getAndInitializeStudioManager({ projectId: '12345', cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud }) expect(studioManagerSetupStub).to.be.calledWith(sinon.match({ shouldEnableStudio: true, @@ -149,7 +149,7 @@ describe('getAndInitializeStudioManager', () => { delete process.env.CYPRESS_ENABLE_CLOUD_STUDIO delete process.env.CYPRESS_LOCAL_STUDIO_PATH - await getAndInitializeStudioManager({ projectId: '12345', cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud }) expect(studioManagerSetupStub).to.be.calledWith(sinon.match({ shouldEnableStudio: false, @@ -160,7 +160,7 @@ describe('getAndInitializeStudioManager', () => { process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1' process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio' - await getAndInitializeStudioManager({ projectId: '12345', cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud }) expect(studioManagerSetupStub).to.be.calledWith(sinon.match({ shouldEnableStudio: true, @@ -188,6 +188,7 @@ describe('getAndInitializeStudioManager', () => { }) await getAndInitializeStudioManager({ + studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud, }) @@ -264,12 +265,12 @@ describe('getAndInitializeStudioManager', () => { const projectId = '12345' - await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud }) expect(rmStub).to.be.calledWith('/tmp/cypress/studio') expect(ensureStub).to.be.calledWith('/tmp/cypress/studio') - expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', { + expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', { agent: sinon.match.any, method: 'GET', headers: { @@ -331,12 +332,12 @@ describe('getAndInitializeStudioManager', () => { const projectId = '12345' - await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud }) expect(rmStub).to.be.calledWith('/tmp/cypress/studio') expect(ensureStub).to.be.calledWith('/tmp/cypress/studio') - expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', { + expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', { agent: sinon.match.any, method: 'GET', headers: { @@ -388,13 +389,13 @@ describe('getAndInitializeStudioManager', () => { const projectId = '12345' - await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud }) expect(rmStub).to.be.calledWith('/tmp/cypress/studio') expect(ensureStub).to.be.calledWith('/tmp/cypress/studio') expect(crossFetchStub).to.be.calledThrice - expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', { + expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', { agent: sinon.match.any, method: 'GET', headers: { @@ -452,13 +453,13 @@ describe('getAndInitializeStudioManager', () => { const projectId = '12345' - await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud }) expect(rmStub).to.be.calledWith('/tmp/cypress/studio') expect(ensureStub).to.be.calledWith('/tmp/cypress/studio') expect(writeResult).to.eq('console.log("studio script")') - expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', { + expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', { agent: sinon.match.any, method: 'GET', headers: { @@ -508,7 +509,7 @@ describe('getAndInitializeStudioManager', () => { const projectId = '12345' - await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud }) + await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud }) expect(rmStub).to.be.calledWith('/tmp/cypress/studio') expect(ensureStub).to.be.calledWith('/tmp/cypress/studio') diff --git a/packages/server/test/unit/cloud/api/studio/post_studio_session_spec.ts b/packages/server/test/unit/cloud/api/studio/post_studio_session_spec.ts new file mode 100644 index 0000000000..52929ee79c --- /dev/null +++ b/packages/server/test/unit/cloud/api/studio/post_studio_session_spec.ts @@ -0,0 +1,62 @@ +import { SystemError } from '../../../../../lib/cloud/network/system_error' +import { proxyquire } from '../../../../spec_helper' + +describe('postStudioSession', () => { + let postStudioSession: typeof import('@packages/server/lib/cloud/api/studio/post_studio_session').postStudioSession + let crossFetchStub: sinon.SinonStub = sinon.stub() + + beforeEach(() => { + crossFetchStub.reset() + postStudioSession = (proxyquire('@packages/server/lib/cloud/api/studio/post_studio_session', { + 'cross-fetch': crossFetchStub, + }) as typeof import('@packages/server/lib/cloud/api/studio/post_studio_session')).postStudioSession + }) + + it('should post a studio session', async () => { + crossFetchStub.resolves({ + ok: true, + json: () => { + return Promise.resolve({ + studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', + protocolUrl: 'http://localhost:1234/capture-protocol/script/def.js', + }) + }, + }) + + const result = await postStudioSession({ + projectId: '12345', + }) + + expect(result).to.deep.equal({ + studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', + protocolUrl: 'http://localhost:1234/capture-protocol/script/def.js', + }) + }) + + it('should throw immediately if the response is not ok', async () => { + crossFetchStub.resolves({ + ok: false, + json: () => { + return Promise.resolve({ + error: 'Failed to create studio session', + }) + }, + }) + + await expect(postStudioSession({ + projectId: '12345', + })).to.be.rejectedWith('Failed to create studio session') + + expect(crossFetchStub).to.have.been.calledOnce + }) + + it('should throw an error if we receive a retryable error more than twice', async () => { + crossFetchStub.rejects(new SystemError(new Error('Failed to create studio session'), 'http://localhost:1234/studio/session')) + + await expect(postStudioSession({ + projectId: '12345', + })).to.be.rejected + + expect(crossFetchStub).to.have.been.calledThrice + }) +}) diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index ace798f9ec..490c4297e7 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -609,7 +609,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s const studio = { isProtocolEnabled: true } - sinon.stub(studioLifecycleManager, 'isStudioReady').returns(true) + studioLifecycleManager.isStudioReady = sinon.stub().returns(true) sinon.stub(studioLifecycleManager, 'getStudio').resolves(studio) let protocolManagerValue = this.project._protocolManager @@ -631,7 +631,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s beforeEach(function () { this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' }) this.project.watchers = {} - this.project._server = { close () {}, startWebsockets: sinon.stub() } + this.project._server = { close () {}, startWebsockets: sinon.stub(), setProtocolManager: sinon.stub() } sinon.stub(ProjectBase.prototype, 'open').resolves() }) @@ -902,15 +902,18 @@ This option will not have an effect in Some-other-name. Tests that rely on web s this.project.ctx.coreData = this.project.ctx.coreData || {} // Create a studio manager with minimal properties - const studioManager = new StudioManager() + const protocolManager = { close: sinon.stub().resolves() } + const studioManager = { + destroy: sinon.stub().resolves(), + protocolManager, + } - // Create a StudioLifecycleManager and set it on coreData - const studioLifecycleManager = new StudioLifecycleManager() + this.project.ctx.coreData.studioLifecycleManager = { + getStudio: sinon.stub().resolves(studioManager), + isStudioReady: sinon.stub().resolves(true), + } - this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager - - // Set up the studio manager promise directly - studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager) + this.project['_protocolManager'] = protocolManager // Create a browser object this.project.browser = { @@ -922,19 +925,22 @@ This option will not have an effect in Some-other-name. Tests that rely on web s sinon.stub(browsers, 'closeProtocolConnection').resolves() - // Track the callbacks passed to startWebsockets - let callbacks - // Modify the startWebsockets stub to track the callbacks - this.project.server.startWebsockets.callsFake((automation, config, cbObject) => { - callbacks = cbObject + const callbackPromise = new Promise((resolve) => { + this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => { + await callbacks.onStudioDestroy() + resolve() + }) }) this.project.startWebsockets({}, {}) - // Verify the callback exists and is a function - expect(callbacks).to.have.property('onStudioDestroy') - expect(typeof callbacks.onStudioDestroy).to.equal('function') + await callbackPromise + + expect(studioManager.destroy).to.have.been.calledOnce + expect(browsers.closeProtocolConnection).to.have.been.calledOnce + expect(protocolManager.close).to.have.been.calledOnce + expect(this.project['_protocolManager']).to.be.undefined }) }) diff --git a/scripts/gulp/tasks/gulpCloudDeliveredTypes.ts b/scripts/gulp/tasks/gulpCloudDeliveredTypes.ts index cddde7947c..f00c1b1a46 100644 --- a/scripts/gulp/tasks/gulpCloudDeliveredTypes.ts +++ b/scripts/gulp/tasks/gulpCloudDeliveredTypes.ts @@ -3,9 +3,12 @@ process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV ?? 'producti import path from 'path' import fs from 'fs-extra' import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager' +import { postStudioSession } from '@packages/server/lib/cloud/api/studio/post_studio_session' export const downloadStudioTypes = async (): Promise => { - await retrieveAndExtractStudioBundle({ projectId: 'ypt4pf' }) + const studioSession = await postStudioSession({ projectId: 'ypt4pf' }) + + await retrieveAndExtractStudioBundle({ studioUrl: studioSession.studioUrl, projectId: 'ypt4pf' }) await fs.copyFile( path.join(studioPath, 'app', 'types.ts'),