mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-10 00:49:38 -05:00
internal: (studio) pass the getProjectOptions function (#32573)
This commit is contained in:
@@ -13,9 +13,15 @@ export const transformError = (err: AxiosError | Error & { error?: any, statusCo
|
||||
{ data: err.error, status: err.statusCode }
|
||||
|
||||
if (isObject(data)) {
|
||||
const body = JSON.stringify(data, null, 2)
|
||||
let body: string | null = null
|
||||
|
||||
err.message = [status, body].join('\n\n')
|
||||
try {
|
||||
body = JSON.stringify(data, null, 2)
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
err.message = body ? [status, body].join('\n\n') : `${status}`
|
||||
}
|
||||
|
||||
err.isApiError = true
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CloudRequest } from '../api/cloud_request'
|
||||
import { isRetryableError } from '../network/is_retryable_error'
|
||||
import { asyncRetry } from '../../util/async_retry'
|
||||
import { postStudioSession } from '../api/studio/post_studio_session'
|
||||
import type { StudioStatus } from '@packages/types'
|
||||
import type { StudioServerOptions, StudioStatus } from '@packages/types'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { ensureStudioBundle } from './ensure_studio_bundle'
|
||||
@@ -39,7 +39,6 @@ export class StudioLifecycleManager {
|
||||
private currentStudioHash?: string
|
||||
|
||||
private initializationParams?: {
|
||||
projectId?: string
|
||||
cloudDataSource: CloudDataSource
|
||||
cfg: Cfg
|
||||
debugData: any
|
||||
@@ -55,20 +54,17 @@ export class StudioLifecycleManager {
|
||||
/**
|
||||
* Initialize the studio manager and possibly set up protocol.
|
||||
* Also registers this instance in the data context.
|
||||
* @param projectId The project ID
|
||||
* @param cloudDataSource The cloud data source
|
||||
* @param cfg The project configuration
|
||||
* @param debugData Debug data for the configuration
|
||||
* @param ctx Data context to register this instance with
|
||||
*/
|
||||
initializeStudioManager ({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
ctx,
|
||||
}: {
|
||||
projectId?: string
|
||||
cloudDataSource: CloudDataSource
|
||||
cfg: Cfg
|
||||
debugData: any
|
||||
@@ -77,7 +73,7 @@ export class StudioLifecycleManager {
|
||||
debug('Initializing studio manager')
|
||||
|
||||
// Store initialization parameters for retry
|
||||
this.initializationParams = { projectId, cloudDataSource, cfg, debugData, ctx }
|
||||
this.initializationParams = { cloudDataSource, cfg, debugData, ctx }
|
||||
|
||||
// Register this instance in the data context
|
||||
ctx.update((data) => {
|
||||
@@ -88,37 +84,53 @@ export class StudioLifecycleManager {
|
||||
|
||||
this.updateStatus('INITIALIZING')
|
||||
|
||||
const getProjectOptions = async () => {
|
||||
const [user, config] = await Promise.all([
|
||||
ctx.actions.auth.authApi.getUser(),
|
||||
ctx.project.getConfig(),
|
||||
])
|
||||
|
||||
return {
|
||||
user,
|
||||
projectSlug: config.projectId || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const studioManagerPromise = this.createStudioManager({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
getProjectOptions,
|
||||
}).catch(async (error) => {
|
||||
debug('Error during studio manager setup: %o', error)
|
||||
|
||||
const { cloudUrl, cloudHeaders } = await getCloudMetadata(cloudDataSource)
|
||||
try {
|
||||
const { cloudUrl, cloudHeaders } = await getCloudMetadata(cloudDataSource)
|
||||
|
||||
reportStudioError({
|
||||
cloudApi: {
|
||||
cloudUrl,
|
||||
cloudHeaders,
|
||||
CloudRequest,
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
studioHash: projectId,
|
||||
projectSlug: cfg.projectId,
|
||||
error,
|
||||
studioMethod: 'initializeStudioManager',
|
||||
studioMethodArgs: [],
|
||||
})
|
||||
reportStudioError({
|
||||
cloudApi: {
|
||||
cloudUrl,
|
||||
cloudHeaders,
|
||||
CloudRequest,
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
studioHash: this.currentStudioHash,
|
||||
projectSlug: (await getProjectOptions()).projectSlug,
|
||||
error,
|
||||
studioMethod: 'initializeStudioManager',
|
||||
studioMethodArgs: [],
|
||||
})
|
||||
|
||||
this.updateStatus('IN_ERROR')
|
||||
this.updateStatus('IN_ERROR')
|
||||
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_END)
|
||||
reportTelemetry(BUNDLE_LIFECYCLE_TELEMETRY_GROUP_NAMES.COMPLETE_BUNDLE_LIFECYCLE, {
|
||||
success: false,
|
||||
})
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.BUNDLE_LIFECYCLE_END)
|
||||
reportTelemetry(BUNDLE_LIFECYCLE_TELEMETRY_GROUP_NAMES.COMPLETE_BUNDLE_LIFECYCLE, {
|
||||
success: false,
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error reporting studio error: %o', error)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
@@ -126,10 +138,10 @@ export class StudioLifecycleManager {
|
||||
this.studioManagerPromise = studioManagerPromise
|
||||
|
||||
this.setupWatcher({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
getProjectOptions,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,22 +170,24 @@ export class StudioLifecycleManager {
|
||||
}
|
||||
|
||||
private async createStudioManager ({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
getProjectOptions,
|
||||
}: {
|
||||
projectId?: string
|
||||
cloudDataSource: CloudDataSource
|
||||
cfg: Cfg
|
||||
debugData: any
|
||||
getProjectOptions: StudioServerOptions['getProjectOptions']
|
||||
}): Promise<StudioManager> {
|
||||
let studioPath: string
|
||||
let studioHash: string
|
||||
let manifest: Record<string, string>
|
||||
|
||||
const currentProjectOptions = await getProjectOptions()
|
||||
|
||||
initializeTelemetryReporter({
|
||||
projectSlug: projectId,
|
||||
projectSlug: currentProjectOptions.projectSlug,
|
||||
cloudDataSource,
|
||||
})
|
||||
|
||||
@@ -181,7 +195,7 @@ export class StudioLifecycleManager {
|
||||
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_START)
|
||||
const studioSession = await postStudioSession({
|
||||
projectId,
|
||||
projectId: currentProjectOptions.projectSlug,
|
||||
})
|
||||
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.POST_STUDIO_SESSION_END)
|
||||
@@ -192,22 +206,27 @@ export class StudioLifecycleManager {
|
||||
studioHash = studioSession.studioUrl.split('/').pop()?.split('.')[0] ?? ''
|
||||
studioPath = path.join(os.tmpdir(), 'cypress', 'studio', studioHash)
|
||||
|
||||
debug('Setting current studio hash: %s', studioHash)
|
||||
// Store the current studio hash so that we can clear the cache entry when retrying
|
||||
this.currentStudioHash = studioHash
|
||||
|
||||
let hashLoadingPromise = StudioLifecycleManager.hashLoadingMap.get(studioHash)
|
||||
|
||||
if (!hashLoadingPromise) {
|
||||
debug('Ensuring studio bundle for hash: %s', studioHash)
|
||||
|
||||
hashLoadingPromise = ensureStudioBundle({
|
||||
studioUrl: studioSession.studioUrl,
|
||||
studioPath,
|
||||
projectId,
|
||||
projectId: currentProjectOptions.projectSlug,
|
||||
})
|
||||
|
||||
StudioLifecycleManager.hashLoadingMap.set(studioHash, hashLoadingPromise)
|
||||
}
|
||||
|
||||
manifest = await hashLoadingPromise
|
||||
|
||||
debug('Manifest: %o', manifest)
|
||||
} else {
|
||||
studioPath = process.env.CYPRESS_LOCAL_STUDIO_PATH
|
||||
studioHash = 'local'
|
||||
@@ -226,10 +245,14 @@ export class StudioLifecycleManager {
|
||||
const actualHash = crypto.createHash('sha256').update(script).digest('hex')
|
||||
|
||||
if (!expectedHash) {
|
||||
debug('Expected hash %s for studio server script not found in manifest: %o', expectedHash, manifest)
|
||||
|
||||
throw new Error('Expected hash for studio server script not found in manifest')
|
||||
}
|
||||
|
||||
if (actualHash !== expectedHash) {
|
||||
debug('Invalid hash for studio server script: %s !== %s', actualHash, expectedHash)
|
||||
|
||||
throw new Error('Invalid hash for studio server script')
|
||||
}
|
||||
}
|
||||
@@ -244,7 +267,6 @@ export class StudioLifecycleManager {
|
||||
script,
|
||||
studioPath,
|
||||
studioHash,
|
||||
projectSlug: projectId,
|
||||
cloudApi: {
|
||||
cloudUrl,
|
||||
cloudHeaders,
|
||||
@@ -252,8 +274,8 @@ export class StudioLifecycleManager {
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
shouldEnableStudio: this.cloudStudioRequested,
|
||||
manifest,
|
||||
getProjectOptions,
|
||||
})
|
||||
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_END)
|
||||
@@ -270,7 +292,7 @@ export class StudioLifecycleManager {
|
||||
telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_PROTOCOL_PREPARE_START)
|
||||
await protocolManager.prepareProtocol(script, {
|
||||
runId: 'studio',
|
||||
projectId: cfg.projectId,
|
||||
projectId: currentProjectOptions.projectSlug,
|
||||
testingType: cfg.testingType,
|
||||
cloudApi: {
|
||||
url: routes.apiUrl,
|
||||
@@ -320,15 +342,15 @@ export class StudioLifecycleManager {
|
||||
}
|
||||
|
||||
private setupWatcher ({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
getProjectOptions,
|
||||
}: {
|
||||
projectId?: string
|
||||
cloudDataSource: CloudDataSource
|
||||
cfg: Cfg
|
||||
debugData: any
|
||||
getProjectOptions: StudioServerOptions['getProjectOptions']
|
||||
}) {
|
||||
// Don't setup a watcher if the studio bundle is NOT local
|
||||
if (!process.env.CYPRESS_LOCAL_STUDIO_PATH) {
|
||||
@@ -348,10 +370,10 @@ export class StudioLifecycleManager {
|
||||
await this.studioManager?.destroy()
|
||||
this.studioManager = undefined
|
||||
this.studioManagerPromise = this.createStudioManager({
|
||||
projectId,
|
||||
cloudDataSource,
|
||||
cfg,
|
||||
debugData,
|
||||
getProjectOptions,
|
||||
}).then((studioManager) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Studio manager reloaded')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions } from '@packages/types'
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import Debug from 'debug'
|
||||
import { requireScript } from '../require_script'
|
||||
@@ -13,10 +13,9 @@ interface SetupOptions {
|
||||
script: string
|
||||
studioPath: string
|
||||
studioHash?: string
|
||||
projectSlug?: string
|
||||
cloudApi: StudioCloudApi
|
||||
shouldEnableStudio: boolean
|
||||
manifest: Record<string, string>
|
||||
getProjectOptions: StudioServerOptions['getProjectOptions']
|
||||
}
|
||||
|
||||
const debug = Debug('cypress:server:studio')
|
||||
@@ -27,30 +26,12 @@ export class StudioManager implements StudioManagerShape {
|
||||
private _studioServer: StudioServerShape | undefined
|
||||
private _studioElectron: StudioElectron | undefined
|
||||
|
||||
static createInErrorManager ({ cloudApi, studioHash, projectSlug, error, studioMethod, studioMethodArgs }: ReportStudioErrorOptions): StudioManager {
|
||||
const manager = new StudioManager()
|
||||
|
||||
manager.status = 'IN_ERROR'
|
||||
|
||||
reportStudioError({
|
||||
cloudApi,
|
||||
studioHash,
|
||||
projectSlug,
|
||||
error,
|
||||
studioMethod,
|
||||
studioMethodArgs,
|
||||
})
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
async setup ({ script, studioPath, studioHash, projectSlug, cloudApi, shouldEnableStudio, manifest }: SetupOptions): Promise<void> {
|
||||
async setup ({ script, studioPath, studioHash, cloudApi, manifest, getProjectOptions }: SetupOptions): Promise<void> {
|
||||
const { createStudioServer } = requireScript<StudioServer>(script).default
|
||||
|
||||
this._studioServer = await createStudioServer({
|
||||
studioHash,
|
||||
studioPath,
|
||||
projectSlug,
|
||||
cloudApi,
|
||||
betterSqlite3Path: path.dirname(require.resolve('better-sqlite3/package.json')),
|
||||
manifest,
|
||||
@@ -65,9 +46,10 @@ export class StudioManager implements StudioManagerShape {
|
||||
|
||||
return actualHash === expectedHash
|
||||
},
|
||||
getProjectOptions,
|
||||
})
|
||||
|
||||
this.status = shouldEnableStudio ? 'ENABLED' : 'INITIALIZED'
|
||||
this.status = 'ENABLED'
|
||||
}
|
||||
|
||||
initializeRoutes (router: Router): void {
|
||||
@@ -79,10 +61,8 @@ export class StudioManager implements StudioManagerShape {
|
||||
async captureStudioEvent (event: StudioEvent): Promise<void> {
|
||||
if (this._studioServer) {
|
||||
// this request is not essential - we don't want studio to error out if a telemetry request fails
|
||||
return (await this.invokeAsync('captureStudioEvent', { isEssential: false }, event))
|
||||
await this.invokeAsync('captureStudioEvent', { isEssential: false }, event)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
addSocketListeners (options: StudioAddSocketListenersOptions): void {
|
||||
|
||||
@@ -16,7 +16,18 @@ import { SocketCt } from './socket-ct'
|
||||
import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
import system from './util/system'
|
||||
import { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands, StudioMetricsTypes } from '@packages/types'
|
||||
import type {
|
||||
BannersState,
|
||||
FoundBrowser,
|
||||
FoundSpec,
|
||||
OpenProjectLaunchOptions,
|
||||
ProtocolManagerShape,
|
||||
ReceivedCypressOptions,
|
||||
ResolvedConfigurationOptions,
|
||||
TestingType,
|
||||
VideoRecording,
|
||||
AutomationCommands,
|
||||
} from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
import { createHmac } from 'crypto'
|
||||
import { ServerBase } from './server-base'
|
||||
@@ -161,7 +172,6 @@ export class ProjectBase extends EE {
|
||||
const studioLifecycleManager = new StudioLifecycleManager()
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: cfg.projectId,
|
||||
cloudDataSource: this.ctx.cloud,
|
||||
cfg,
|
||||
debugData: this.configDebugData,
|
||||
@@ -475,26 +485,6 @@ export class ProjectBase extends EE {
|
||||
|
||||
const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio()
|
||||
|
||||
// only capture studio started event if the user is accessing legacy studio
|
||||
if (!this.ctx.coreData.studioLifecycleManager?.cloudStudioRequested) {
|
||||
try {
|
||||
studio?.captureStudioEvent({
|
||||
type: StudioMetricsTypes.STUDIO_STARTED,
|
||||
machineId: await this.ctx.coreData.machineId ?? '',
|
||||
projectId: this.cfg.projectId,
|
||||
browser: this.browser ? {
|
||||
name: this.browser.name,
|
||||
family: this.browser.family,
|
||||
channel: this.browser.channel,
|
||||
version: this.browser.version,
|
||||
} : undefined,
|
||||
cypressVersion: pkg.version,
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error capturing studio event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.spec && studio?.protocolManager) {
|
||||
telemetryManager.mark(INITIALIZATION_MARK_NAMES.CAN_ACCESS_STUDIO_AI_START)
|
||||
const canAccessStudioAI = await studio?.canAccessStudioAI(this.browser) ?? false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { StudioServerShape, StudioServerDefaultShape } from '@packages/types'
|
||||
import type { StudioServerShape, StudioServerDefaultShape, StudioEvent } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
@@ -32,6 +32,10 @@ class StudioServer implements StudioServerShape {
|
||||
captureStudioEvent (event: StudioEvent): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
updateSessionId (sessionId: string): void {
|
||||
// This is a test implementation that does nothing
|
||||
}
|
||||
}
|
||||
|
||||
const studioServerDefault: StudioServerDefaultShape = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sinon, proxyquire } from '../../../spec_helper'
|
||||
import { proxyquire } from '../../../spec_helper'
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { StudioManager } from '../../../../lib/cloud/studio/studio'
|
||||
import { StudioLifecycleManager } from '../../../../lib/cloud/studio/StudioLifecycleManager'
|
||||
@@ -130,12 +131,26 @@ describe('StudioLifecycleManager', () => {
|
||||
emitter: {
|
||||
studioStatusChange: studioStatusChangeEmitterStub,
|
||||
},
|
||||
actions: {
|
||||
auth: {
|
||||
authApi: {
|
||||
getUser: sinon.stub().resolves({
|
||||
authToken: 'test-token',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
getConfig: sinon.stub().resolves({
|
||||
projectId: 'abc123',
|
||||
}),
|
||||
},
|
||||
} as unknown as DataContext
|
||||
|
||||
mockCloudDataSource = {
|
||||
getCloudUrl: sinon.stub().returns('https://cloud.cypress.io'),
|
||||
additionalHeaders: sinon.stub().resolves({ 'Authorization': 'Bearer test-token' }),
|
||||
} as CloudDataSource
|
||||
} as unknown as CloudDataSource
|
||||
|
||||
mockCfg = {
|
||||
projectId: 'abc123',
|
||||
@@ -179,7 +194,6 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -204,14 +218,14 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(ensureStudioBundleStub).to.be.calledWith({
|
||||
studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'),
|
||||
studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz',
|
||||
projectId: 'test-project-id',
|
||||
projectId: 'abc123',
|
||||
})
|
||||
|
||||
expect(studioManagerSetupStub).to.be.calledWith({
|
||||
script: 'console.log("studio script")',
|
||||
studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'),
|
||||
studioHash: 'abc',
|
||||
projectSlug: 'test-project-id',
|
||||
getProjectOptions: sinon.match.func,
|
||||
cloudApi: {
|
||||
cloudUrl: 'https://cloud.cypress.io',
|
||||
cloudHeaders: { 'Authorization': 'Bearer test-token' },
|
||||
@@ -219,12 +233,11 @@ describe('StudioLifecycleManager', () => {
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
shouldEnableStudio: true,
|
||||
manifest: mockManifest,
|
||||
})
|
||||
|
||||
expect(postStudioSessionStub).to.be.calledWith({
|
||||
projectId: 'test-project-id',
|
||||
projectId: 'abc123',
|
||||
})
|
||||
|
||||
expect(readFileStub).to.be.calledWith(path.join(os.tmpdir(), 'cypress', 'studio', 'abc', 'server', 'index.js'), 'utf8')
|
||||
@@ -233,7 +246,7 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(prepareProtocolStub).not.to.be.called
|
||||
|
||||
expect(initializeTelemetryReporterStub).to.be.calledWith({
|
||||
projectSlug: 'test-project-id',
|
||||
projectSlug: 'abc123',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
})
|
||||
|
||||
@@ -263,7 +276,6 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -288,14 +300,14 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(ensureStudioBundleStub).to.be.calledWith({
|
||||
studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'),
|
||||
studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz',
|
||||
projectId: 'test-project-id',
|
||||
projectId: 'abc123',
|
||||
})
|
||||
|
||||
expect(studioManagerSetupStub).to.be.calledWith({
|
||||
script: 'console.log("studio script")',
|
||||
studioPath: path.join(os.tmpdir(), 'cypress', 'studio', 'abc'),
|
||||
studioHash: 'abc',
|
||||
projectSlug: 'test-project-id',
|
||||
getProjectOptions: sinon.match.func,
|
||||
cloudApi: {
|
||||
cloudUrl: 'https://cloud.cypress.io',
|
||||
cloudHeaders: { 'Authorization': 'Bearer test-token' },
|
||||
@@ -303,12 +315,11 @@ describe('StudioLifecycleManager', () => {
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
shouldEnableStudio: true,
|
||||
manifest: mockManifest,
|
||||
})
|
||||
|
||||
expect(postStudioSessionStub).to.be.calledWith({
|
||||
projectId: 'test-project-id',
|
||||
projectId: 'abc123',
|
||||
})
|
||||
|
||||
expect(readFileStub).to.be.calledWith(path.join(os.tmpdir(), 'cypress', 'studio', 'abc', 'server', 'index.js'), 'utf8')
|
||||
@@ -335,7 +346,7 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
expect(initializeTelemetryReporterStub).to.be.calledWith({
|
||||
projectSlug: 'test-project-id',
|
||||
projectSlug: 'abc123',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
})
|
||||
|
||||
@@ -367,7 +378,6 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -395,7 +405,7 @@ describe('StudioLifecycleManager', () => {
|
||||
script: 'console.log("studio script")',
|
||||
studioPath: '/path/to/studio',
|
||||
studioHash: 'local',
|
||||
projectSlug: 'test-project-id',
|
||||
getProjectOptions: sinon.match.func,
|
||||
cloudApi: {
|
||||
cloudUrl: 'https://cloud.cypress.io',
|
||||
cloudHeaders: { 'Authorization': 'Bearer test-token' },
|
||||
@@ -403,12 +413,11 @@ describe('StudioLifecycleManager', () => {
|
||||
isRetryableError,
|
||||
asyncRetry,
|
||||
},
|
||||
shouldEnableStudio: true,
|
||||
manifest: {},
|
||||
})
|
||||
|
||||
expect(postStudioSessionStub).to.be.calledWith({
|
||||
projectId: 'test-project-id',
|
||||
projectId: 'abc123',
|
||||
})
|
||||
|
||||
expect(readFileStub).to.be.calledWith(path.join('/path', 'to', 'studio', 'server', 'index.js'), 'utf8')
|
||||
@@ -485,7 +494,6 @@ describe('StudioLifecycleManager', () => {
|
||||
ensureStudioBundleStub.resolves(mockManifest)
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -502,7 +510,7 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(reportStudioErrorStub).to.be.calledOnce
|
||||
expect(reportStudioErrorStub).to.be.calledWithMatch({
|
||||
cloudApi: sinon.match.object,
|
||||
studioHash: 'test-project-id',
|
||||
studioHash: 'abc',
|
||||
projectSlug: 'abc123',
|
||||
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Expected hash for studio server script not found in manifest')),
|
||||
studioMethod: 'initializeStudioManager',
|
||||
@@ -532,7 +540,6 @@ describe('StudioLifecycleManager', () => {
|
||||
ensureStudioBundleStub.resolves(mockManifest)
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -549,7 +556,7 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(reportStudioErrorStub).to.be.calledOnce
|
||||
expect(reportStudioErrorStub).to.be.calledWithMatch({
|
||||
cloudApi: sinon.match.object,
|
||||
studioHash: 'test-project-id',
|
||||
studioHash: 'abc',
|
||||
projectSlug: 'abc123',
|
||||
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Invalid hash for studio server script')),
|
||||
studioMethod: 'initializeStudioManager',
|
||||
@@ -579,7 +586,6 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
await studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -598,7 +604,7 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(reportStudioErrorStub).to.be.calledOnce
|
||||
expect(reportStudioErrorStub).to.be.calledWithMatch({
|
||||
cloudApi: sinon.match.object,
|
||||
studioHash: 'test-project-id',
|
||||
studioHash: 'abc',
|
||||
projectSlug: 'abc123',
|
||||
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Test error')),
|
||||
studioMethod: 'initializeStudioManager',
|
||||
@@ -618,7 +624,7 @@ describe('StudioLifecycleManager', () => {
|
||||
}
|
||||
|
||||
expect(initializeTelemetryReporterStub).to.be.calledWith({
|
||||
projectSlug: 'test-project-id',
|
||||
projectSlug: 'abc123',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
})
|
||||
|
||||
@@ -776,7 +782,6 @@ describe('StudioLifecycleManager', () => {
|
||||
])
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -848,7 +853,6 @@ describe('StudioLifecycleManager', () => {
|
||||
const statusChangesSpy = sinon.spy(studioLifecycleManager as any, 'updateStatus')
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
cfg: mockCfg,
|
||||
debugData: {},
|
||||
@@ -875,7 +879,6 @@ describe('StudioLifecycleManager', () => {
|
||||
const statusChangesSpy = sinon.spy(studioLifecycleManager as any, 'updateStatus')
|
||||
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
cfg: mockCfg,
|
||||
debugData: {},
|
||||
@@ -924,7 +927,6 @@ describe('StudioLifecycleManager', () => {
|
||||
|
||||
// First initialize with some state
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -988,28 +990,39 @@ describe('StudioLifecycleManager', () => {
|
||||
})
|
||||
|
||||
it('clears the current studio hash from cached bundle promises on retry', async () => {
|
||||
const mockManifest = {
|
||||
'server/index.js': 'e1ed3dc8ba9eb8ece23914004b99ad97bba37e80a25d8b47c009e1e4948a6159',
|
||||
}
|
||||
|
||||
ensureStudioBundleStub.resolves(mockManifest)
|
||||
|
||||
// Add some cached promises to the static map
|
||||
const dummyPromise = Promise.resolve()
|
||||
const dummyPromise = Promise.resolve({
|
||||
'server/index.js': 'e1ed3dc8ba9eb8ece23914004b99ad97bba37e80a25d8b47c009e1e4948a6159',
|
||||
})
|
||||
|
||||
// @ts-expect-error - accessing private static property
|
||||
StudioLifecycleManager.hashLoadingMap.set('test-hash-1', dummyPromise)
|
||||
// @ts-expect-error - accessing private static property
|
||||
StudioLifecycleManager.hashLoadingMap.set('abc', dummyPromise) // This should be the current hash (from studioUrl)
|
||||
|
||||
// @ts-expect-error - accessing private static property
|
||||
expect(StudioLifecycleManager.hashLoadingMap.size).to.equal(2)
|
||||
|
||||
// Initialize with ctx so retry will work
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
debugData: {},
|
||||
})
|
||||
|
||||
// @ts-expect-error - accessing private property
|
||||
studioLifecycleManager.currentStudioHash = 'abc'
|
||||
// @ts-expect-error - accessing private static property
|
||||
expect(StudioLifecycleManager.hashLoadingMap.size).to.equal(2)
|
||||
|
||||
// Wait for initialization to complete
|
||||
await new Promise((resolve) => {
|
||||
studioLifecycleManager.registerStudioReadyListener(() => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
studioLifecycleManager.retry()
|
||||
|
||||
@@ -1020,6 +1033,13 @@ describe('StudioLifecycleManager', () => {
|
||||
expect(StudioLifecycleManager.hashLoadingMap.has('abc')).to.be.false
|
||||
// @ts-expect-error - accessing private static property
|
||||
expect(StudioLifecycleManager.hashLoadingMap.size).to.equal(1)
|
||||
|
||||
// Wait for retry to complete
|
||||
await new Promise((resolve) => {
|
||||
studioLifecycleManager.registerStudioReadyListener(() => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('clears the local hash when using local studio path', async () => {
|
||||
@@ -1038,7 +1058,6 @@ describe('StudioLifecycleManager', () => {
|
||||
|
||||
// Initialize with ctx so retry will work
|
||||
studioLifecycleManager.initializeStudioManager({
|
||||
projectId: 'test-project-id',
|
||||
cloudDataSource: mockCloudDataSource,
|
||||
ctx: mockCtx,
|
||||
cfg: mockCfg,
|
||||
@@ -1054,6 +1073,13 @@ describe('StudioLifecycleManager', () => {
|
||||
|
||||
studioLifecycleManager.retry()
|
||||
|
||||
// Wait for retry to complete
|
||||
await new Promise((resolve) => {
|
||||
studioLifecycleManager.registerStudioReadyListener(() => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Verify only the 'local' hash was cleared
|
||||
// @ts-expect-error - accessing private static property
|
||||
expect(StudioLifecycleManager.hashLoadingMap.has('test-hash-1')).to.be.true
|
||||
|
||||
@@ -46,9 +46,10 @@ describe('lib/cloud/studio', () => {
|
||||
script: stubStudio,
|
||||
studioPath: 'path',
|
||||
studioHash: 'abcdefg',
|
||||
projectSlug: '1234',
|
||||
getProjectOptions: sinon.stub().resolves({
|
||||
projectSlug: '1234',
|
||||
}),
|
||||
cloudApi: {} as any,
|
||||
shouldEnableStudio: true,
|
||||
manifest: {
|
||||
'server/index.js': 'abcdefg',
|
||||
},
|
||||
@@ -102,29 +103,6 @@ describe('lib/cloud/studio', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('createInErrorManager', () => {
|
||||
it('creates a studio manager in error state', () => {
|
||||
const error = new Error('foo')
|
||||
const manager = StudioManager.createInErrorManager({
|
||||
error,
|
||||
cloudApi: {} as any,
|
||||
studioHash: 'abcdefg',
|
||||
projectSlug: '1234',
|
||||
studioMethod: 'initializeRoutes',
|
||||
})
|
||||
|
||||
expect(manager.status).to.eq('IN_ERROR')
|
||||
expect(reportStudioError).to.be.calledWithMatch({
|
||||
error,
|
||||
cloudApi: {} as any,
|
||||
studioHash: 'abcdefg',
|
||||
projectSlug: '1234',
|
||||
studioMethod: 'initializeRoutes',
|
||||
studioMethodArgs: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('initializeRoutes', () => {
|
||||
it('initializes routes', () => {
|
||||
sinon.stub(studio, 'initializeRoutes')
|
||||
@@ -233,6 +211,83 @@ describe('lib/cloud/studio', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureStudioEvent', () => {
|
||||
it('captures a studio event', async () => {
|
||||
sinon.stub(studio, 'captureStudioEvent').resolves()
|
||||
|
||||
await studioManager.captureStudioEvent({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
})
|
||||
|
||||
expect(studio.captureStudioEvent).to.be.calledWith({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not call captureStudioEvent when studio server is not defined', () => {
|
||||
// Set _studioServer to undefined
|
||||
(studioManager as any)._studioServer = undefined
|
||||
|
||||
// Create a spy on invokeSync to verify it's not called
|
||||
const invokeSyncSpy = sinon.spy(studioManager, 'invokeSync')
|
||||
|
||||
studioManager.captureStudioEvent({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
})
|
||||
|
||||
expect(invokeSyncSpy).to.not.be.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSessionId', () => {
|
||||
it('updates the session ID', () => {
|
||||
sinon.stub(studio, 'updateSessionId')
|
||||
const mockSessionId = 'test-session-id'
|
||||
|
||||
studioManager.updateSessionId(mockSessionId)
|
||||
|
||||
expect(studio.updateSessionId).to.be.calledWith(mockSessionId)
|
||||
})
|
||||
|
||||
it('does not call updateSessionId when studio server is not defined', () => {
|
||||
// Set _studioServer to undefined
|
||||
(studioManager as any)._studioServer = undefined
|
||||
|
||||
// Create a spy on invokeSync to verify it's not called
|
||||
const invokeSyncSpy = sinon.spy(studioManager, 'invokeSync')
|
||||
|
||||
studioManager.updateSessionId('test-session-id')
|
||||
|
||||
expect(invokeSyncSpy).to.not.be.called
|
||||
})
|
||||
|
||||
it('does not call updateSessionId when _studioServer.updateSessionId is not a function', () => {
|
||||
// Set _studioServer.updateSessionId to undefined
|
||||
(studioManager as any)._studioServer.updateSessionId = undefined
|
||||
|
||||
// Create a spy on invokeSync to verify it's not called
|
||||
const invokeSyncSpy = sinon.spy(studioManager, 'invokeSync')
|
||||
|
||||
studioManager.updateSessionId('test-session-id')
|
||||
|
||||
expect(invokeSyncSpy).to.not.be.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('reportError', () => {
|
||||
it('reports an error', () => {
|
||||
sinon.stub(studio, 'reportError')
|
||||
const error = new Error('foo')
|
||||
|
||||
studioManager.reportError(error, 'reportError', 'test-args')
|
||||
|
||||
expect(studio.reportError).to.be.calledWith(error, 'reportError', 'test-args')
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroy', () => {
|
||||
it('destroys the studio server', async () => {
|
||||
sinon.stub(studio, 'destroy').resolves()
|
||||
|
||||
@@ -71,10 +71,26 @@ export type StudioElectronApi = {
|
||||
createBrowserWindow: () => BrowserWindow
|
||||
}
|
||||
|
||||
export interface StudioAuthenticatedUserShape {
|
||||
id?: string // Cloud user id
|
||||
name?: string
|
||||
email?: string
|
||||
authToken?: string
|
||||
}
|
||||
|
||||
export interface StudioProjectOptions {
|
||||
user?: StudioAuthenticatedUserShape
|
||||
projectSlug?: string
|
||||
}
|
||||
|
||||
export interface StudioServerOptions {
|
||||
studioHash?: string
|
||||
studioPath: string
|
||||
/**
|
||||
* @deprecated use getProjectOptions instead
|
||||
*/
|
||||
projectSlug?: string
|
||||
getProjectOptions: () => Promise<StudioProjectOptions>
|
||||
cloudApi: StudioCloudApi
|
||||
betterSqlite3Path: string
|
||||
sessionId?: string
|
||||
|
||||
Reference in New Issue
Block a user