diff --git a/packages/app/src/studio/studio-app-types.ts b/packages/app/src/studio/studio-app-types.ts index b701c40146..2c42d65f49 100644 --- a/packages/app/src/studio/studio-app-types.ts +++ b/packages/app/src/studio/studio-app-types.ts @@ -2,10 +2,34 @@ // `studio` bundle. It is downloaded and copied to the app. // It should not be modified directly in the app. -import type { CloudStatus } from '@cy/store/user-project-status-store' - +// Recording state: 'recording' (active), 'paused' (user-controlled, resumable), +// or 'disabled' (system-controlled, cannot be activated until conditions change). export type RecordingState = 'recording' | 'paused' | 'disabled' +export type SynchronizationMetadata = { + timestamp: number + sequence: number + frameId: string +} + +export type CloudStatus = + | 'isLoggedOut' + | 'needsOrgConnect' + | 'needsProjectConnect' + | 'needsRecordedRun' + | 'allTasksCompleted' + +export interface LoginUserData { + id: string + fullName: string | null + email: string | null +} + +export interface OrganizationData { + id: string + cypressAiDisabled?: boolean | null +} + export interface UserProjectStatusStore { user: { isLoggedIn: boolean @@ -17,6 +41,12 @@ export interface UserProjectStatusStore { openLoginConnectModal: (options: { utmMedium: string }) => void cloudStatus: CloudStatus projectId: string + userData?: LoginUserData + organization?: OrganizationData +} + +export interface AutSnapshotStore { + isSnapshotPinned: boolean } export interface RequestProjectAccessMutationResult { @@ -40,7 +70,8 @@ const SPEC_DIRTY_DATA_MODULES = Object.freeze({ }, }) -export type SpecDirtyDataModule = (typeof SPEC_DIRTY_DATA_MODULES)[keyof typeof SPEC_DIRTY_DATA_MODULES] +export type SpecDirtyDataModule = + (typeof SPEC_DIRTY_DATA_MODULES)[keyof typeof SPEC_DIRTY_DATA_MODULES] export type SpecDirtyDataModuleKey = keyof typeof SPEC_DIRTY_DATA_MODULES @@ -59,15 +90,17 @@ export interface StudioPanelProps { useRunnerStatus?: RunnerStatusShape useTestContentRetriever?: TestContentRetrieverShape useCypress?: CypressShape + useSnapshotIframe?: SnapshotIframeShape autUrlSelector?: string studioAiAvailable?: boolean - userProjectStatusStore: UserProjectStatusStore - hasRequestedProjectAccess: boolean - requestProjectAccessMutation: RequestProjectAccessMutation - specDirtyDataStore: SpecDirtyDataStore + userProjectStatusStore?: UserProjectStatusStore + hasRequestedProjectAccess?: boolean + requestProjectAccessMutation?: RequestProjectAccessMutation + specDirtyDataStore?: SpecDirtyDataStore isSelectorPlaygroundOpen?: boolean // Callback to close the Selector Playground onCloseSelectorPlayground?: () => void + autSnapshotStore?: AutSnapshotStore } export type StudioPanelShape = (props: StudioPanelProps) => JSX.Element @@ -80,28 +113,82 @@ export interface StudioAppDefaultShape { export type CypressInternal = Cypress.Cypress & CyEventEmitter & { - state: (key: string) => any + state(key: string): V + state(key: string, value: any): V + // The main AUT iframe $autIframe: JQuery + // The container for the AUT panel, which contains the frames and snapshots controls + $autPanelContainer?: JQuery + // The container for the AUT panel iframes, which contains the AUT iframe and snapshot iframes + $autIframesContainer?: JQuery + // The individual snapshot iframes rendered by Cypress for studio snapshotrendering + $autSnapshotIframes?: JQuery[] mocha: { getRootSuite: () => Suite + getRunner: () => { + stats: { + suites: number + tests: number + } + } } areSourceMapsAvailable?: boolean stackUtils?: { - getSourceDetailsForFirstLine: (stack: string, projectRoot: string) => { + getSourceDetailsForFirstLine: ( + stack: string, + projectRoot: string + ) => { line: number column: number file: string } } + // External typings do not expose the `getSelectorPriority` function on the ElementSelector object + ElementSelector: { + defaults(options: Partial): void + getSelectorPriority?: () => Cypress.SelectorPriority[] + } +} + +export type LocalRecommendationId = string + +export interface PreSettledRecommendation { + // client-side generated id, same as the counterpart pending recommendation + id: LocalRecommendationId + // server-side generated id + generationId: string + // the assertions generated by the LLM (may be updated if user edits within the recommendation) + generatedAssertions: string + // the original assertions generated by the LLM (preserved for analytics) + originalGeneratedAssertions: string + // synchronization metadata for each snapshot used to generate this recommendation + snapshotSynchronizationMetadata: SynchronizationMetadata[] + // the reason(s) for the recommendation, from the LLM response + reason?: string[] + // the node ID of the target element for this recommendation (for highlighting) + nodeId?: number } +// a settled recommendation with the assertions generated by the LLM +export interface SettledRecommendation extends PreSettledRecommendation { + // line that the recommendation starts on + startingLineNumber: number +} + +export type SettledRecommendationsById = Record< + LocalRecommendationId, + SettledRecommendation +> + export interface TestBlock { content: string testBodyPosition: { contentStart: number contentEnd: number - indentation: number + indentation: string + indentationType?: IndentationType } + recommendations?: SettledRecommendationsById } export type RunnerStatus = 'running' | 'finished' @@ -122,24 +209,12 @@ export type RunnerStatusShape = (props: RunnerStatusProps) => { runnerStatus: RunnerStatus } -export interface StudioAIStreamProps { - canAccessStudioAI: boolean - runnerStatus: RunnerStatus - testCode?: string - isCreatingNewTest: boolean - Cypress: CypressInternal -} - -export interface StudioAIStream { - recommendation: string - isStreaming: boolean - generationId: string | null -} - -export type StudioAIStreamShape = (props: StudioAIStreamProps) => StudioAIStream +export type SnapshotIframeShape = (props: CypressProps) => void export interface TestContentRetrieverProps { Cypress: CypressInternal + showCypressGrepError: boolean + isCucumberSpec: boolean } export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => { @@ -157,3 +232,27 @@ export type Suite = { column: number } } + +export type IndentationType = 'space' | 'tab' + +// copied from the Cypress App +export interface AutSnapshot { + id?: number + name?: string + $el: any + snapshot?: AutSnapshot + coords: [number, number] + scrollBy: { + x: number + y: number + } + snapshots: AutSnapshot[] + highlightAttr: string + htmlAttrs: Record // Type is NamedNodeMap, not sure if we should include lib: ["DOM"] + viewportHeight: number + viewportWidth: number + url: string + body: { + get: () => unknown // TODO: find out what this is, some sort of JQuery API. + } +} diff --git a/packages/data-context/src/data/ProjectConfigIpc.ts b/packages/data-context/src/data/ProjectConfigIpc.ts index 8a85aa9be5..fe11ec8cb8 100644 --- a/packages/data-context/src/data/ProjectConfigIpc.ts +++ b/packages/data-context/src/data/ProjectConfigIpc.ts @@ -1,6 +1,6 @@ /* eslint-disable no-dupe-class-members */ import { CypressError, getError } from '@packages/errors' -import type { FullConfig, TestingType } from '@packages/types' +import type { DebugData, FullConfig, TestingType } from '@packages/types' import { ChildProcess, fork, ForkOptions, spawn } from 'child_process' import EventEmitter from 'events' import path from 'path' @@ -50,10 +50,6 @@ interface SerializedLoadConfigReply { requires: string[] } -export interface DebugData { - filePreprocessorHandlerText?: string -} - /** * The ProjectConfigIpc is an EventEmitter wrapping the childProcess, * adding a "send" method for sending events from the parent process into the childProcess, diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 78c617b4a9..572ea386e9 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -1,7 +1,7 @@ import { CypressError, getError } from '@packages/errors' -import { DebugData, PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' +import { PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' import assert from 'assert' -import type { AllModeOptions, FullConfig, TestingType } from '@packages/types' +import type { AllModeOptions, FullConfig, TestingType, DebugData } from '@packages/types' import debugLib from 'debug' import path from 'path' import _ from 'lodash' diff --git a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts index 743e141755..6dc1645e3f 100644 --- a/packages/server/lib/cloud/studio/StudioLifecycleManager.ts +++ b/packages/server/lib/cloud/studio/StudioLifecycleManager.ts @@ -25,6 +25,7 @@ import { INITIALIZATION_TELEMETRY_GROUP_NAMES } from './telemetry/constants/init import crypto from 'crypto' import { logError } from '@packages/stderr-filtering' import { isNonRetriableCertErrorCode } from '../network/non_retriable_cert_error_codes' +import type { DebugData } from '@packages/types' const debug = Debug('cypress:server:studio-lifecycle-manager') const routes = require('../routes') @@ -179,7 +180,7 @@ export class StudioLifecycleManager { }: { cloudDataSource: CloudDataSource cfg: Cfg - debugData: any + debugData?: DebugData getProjectOptions: Required['getProjectOptions'] }): Promise { let studioPath: string @@ -277,6 +278,7 @@ export class StudioLifecycleManager { }, manifest, getProjectOptions, + debugData, }) telemetryManager.mark(BUNDLE_LIFECYCLE_MARK_NAMES.STUDIO_MANAGER_SETUP_END) @@ -349,7 +351,7 @@ export class StudioLifecycleManager { }: { cloudDataSource: CloudDataSource cfg: Cfg - debugData: any + debugData?: DebugData getProjectOptions: Required['getProjectOptions'] }) { // Don't setup a watcher if the studio bundle is NOT local diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 8931d35223..2e5e5096a8 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -1,4 +1,4 @@ -import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions, StudioCDPClient } from '@packages/types' +import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, StudioConfig, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions, StudioCDPClient } from '@packages/types' import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' @@ -6,6 +6,7 @@ import path from 'path' import crypto, { BinaryLike } from 'crypto' import { StudioElectron } from './StudioElectron' import exception from '../exception' +import type { DebugData } from '@packages/types' interface StudioServer { default: StudioServerDefaultShape } @@ -16,6 +17,7 @@ interface SetupOptions { cloudApi: StudioCloudApi manifest: Record getProjectOptions: StudioServerOptions['getProjectOptions'] + debugData?: DebugData } const debug = Debug('cypress:server:studio') @@ -26,7 +28,7 @@ export class StudioManager implements StudioManagerShape { private _studioServer: StudioServerShape | undefined private _studioElectron: StudioElectron | undefined - async setup ({ script, studioPath, studioHash, cloudApi, manifest, getProjectOptions }: SetupOptions): Promise { + async setup ({ script, studioPath, studioHash, cloudApi, manifest, getProjectOptions, debugData }: SetupOptions): Promise { const { createStudioServer } = requireScript(script).default this._studioServer = await createStudioServer({ @@ -47,6 +49,7 @@ export class StudioManager implements StudioManagerShape { return actualHash === expectedHash }, getProjectOptions, + debugData, }) this.status = 'ENABLED' @@ -75,6 +78,26 @@ export class StudioManager implements StudioManagerShape { return !!(await this.invokeAsync('canAccessStudioAI', { isEssential: true }, browser)) } + async getStudioConfig (browser: Cypress.Browser): Promise { + const config = await this.invokeAsync('getStudioConfig', { isEssential: true }, browser) + + if (config === undefined) { + throw new Error('Studio is not available: server not initialized or an error occurred') + } + + return config + } + + getCachedStudioConfig (): StudioConfig { + const config = this.invokeSync('getCachedStudioConfig', { isEssential: true }) + + if (config === undefined) { + throw new Error('Studio is not available: server not initialized or an error occurred') + } + + return config + } + connectToBrowser (target: StudioCDPClient): void { if (this._studioServer) { return this.invokeSync('connectToBrowser', { isEssential: true }, target) @@ -187,11 +210,13 @@ export class StudioManager implements StudioManagerShape { } } -// Helper types for invokeSync / invokeAsync +// Helper types for invokeSync / invokeAsync (only method keys; exclude e.g. sessionId) +type StudioServerMethodKey = Exclude + type StudioServerSyncMethods = { - [K in keyof StudioServerShape]: ReturnType extends Promise ? never : K -}[keyof StudioServerShape] + [K in StudioServerMethodKey]: ReturnType extends Promise ? never : K +}[StudioServerMethodKey] type StudioServerAsyncMethods = { - [K in keyof StudioServerShape]: ReturnType extends Promise ? K : never -}[keyof StudioServerShape] + [K in StudioServerMethodKey]: ReturnType extends Promise ? K : never +}[StudioServerMethodKey] diff --git a/packages/server/test/support/fixtures/cloud/studio/test-studio.ts b/packages/server/test/support/fixtures/cloud/studio/test-studio.ts index c532cf8dff..46c0c9c67b 100644 --- a/packages/server/test/support/fixtures/cloud/studio/test-studio.ts +++ b/packages/server/test/support/fixtures/cloud/studio/test-studio.ts @@ -1,9 +1,14 @@ /// -import type { StudioServerShape, StudioServerDefaultShape, StudioEvent, StudioCDPClient } from '@packages/types' +import type { StudioServerShape, StudioServerDefaultShape, StudioEvent, StudioCDPClient, StudioConfig } from '@packages/types' import type { Router } from 'express' import type { Socket } from '@packages/socket' +const stubStudioConfig: StudioConfig = { + AI: { enabled: true }, + featureFlags: { studioNonNativeEvents: false, studioAI: true }, +} + class StudioServer implements StudioServerShape { initializeRoutes (router: Router): void { // This is a test implementation that does nothing @@ -13,6 +18,14 @@ class StudioServer implements StudioServerShape { return Promise.resolve(true) } + getStudioConfig (browser: Cypress.Browser): Promise { + return Promise.resolve(stubStudioConfig) + } + + getCachedStudioConfig (): StudioConfig { + return stubStudioConfig + } + initializeStudioAI (): Promise { return Promise.resolve() } diff --git a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts index 6394b357a5..607a3c938d 100644 --- a/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts +++ b/packages/server/test/unit/cloud/studio/StudioLifecycleManager_spec.ts @@ -21,6 +21,8 @@ const api = require('../../../../lib/cloud/api').default // Helper to wait for next tick in event loop const nextTick = () => new Promise((resolve) => process.nextTick(resolve)) +const debugData = { filePreprocessorHandlerText: 'handler text' } + describe('StudioLifecycleManager', () => { let studioLifecycleManager: StudioLifecycleManager let mockStudioManager: StudioManager @@ -197,7 +199,7 @@ describe('StudioLifecycleManager', () => { cloudDataSource: mockCloudDataSource, ctx: mockCtx, cfg: mockCfg, - debugData: {}, + debugData, }) const studioReadyPromise = new Promise((resolve) => { @@ -234,6 +236,7 @@ describe('StudioLifecycleManager', () => { asyncRetry, }, manifest: mockManifest, + debugData, }) expect(postStudioSessionStub).to.be.calledWith({ @@ -259,7 +262,7 @@ describe('StudioLifecycleManager', () => { proxyUrl: 'http://localhost:8888', }, mountVersion: 2, - debugData: {}, + debugData, mode: 'studio', }) @@ -332,6 +335,7 @@ describe('StudioLifecycleManager', () => { asyncRetry, }, manifest: {}, + debugData: {}, }) expect(postStudioSessionStub).to.be.calledWith({ @@ -350,7 +354,7 @@ describe('StudioLifecycleManager', () => { retryWithBackoff: api.retryWithBackoff, requestPromise: api.rp, }, - projectConfig: { + projectConfig: { devServerPublicPathRoute: '/__cypress/src', namespace: '__cypress', port: 8888, diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index 221cdc2c5c..aef9073e4e 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -65,6 +65,89 @@ describe('lib/cloud/studio', () => { sinon.restore() }) + describe('setup', () => { + it('passes debugData to createStudioServer when provided', async () => { + const createStudioServerStub = sinon.stub().resolves({ + initializeRoutes: sinon.stub(), + canAccessStudioAI: sinon.stub().resolves(true), + initializeStudioAI: sinon.stub().resolves(), + reportError: sinon.stub(), + destroy: sinon.stub().resolves(), + addSocketListeners: sinon.stub(), + captureStudioEvent: sinon.stub().resolves(), + updateSessionId: sinon.stub(), + connectToBrowser: sinon.stub(), + }) + + const StubbedStudioManager = (proxyquire('../lib/cloud/studio/studio', { + '../api/studio/report_studio_error': { reportStudioError: sinon.stub() }, + './StudioElectron': { StudioElectron: class {} }, + '../require_script': { + requireScript: () => ({ + default: { createStudioServer: createStudioServerStub }, + }), + }, + }) as typeof import('@packages/server/lib/cloud/studio/studio')).StudioManager + + const manager = new StubbedStudioManager() + const debugData = { filePreprocessorHandlerText: 'handler text' } + + await manager.setup({ + script: 'script', + studioPath: 'path', + studioHash: 'abcdefg', + getProjectOptions: sinon.stub().resolves({ projectSlug: '1234' }), + cloudApi: {} as any, + manifest: { 'server/index.js': 'abcdefg' }, + debugData, + }) + + expect(createStudioServerStub).to.have.been.calledOnce + expect(createStudioServerStub.firstCall.args[0].debugData).to.deep.equal(debugData) + }) + + it('passes undefined debugData to createStudioServer when not provided', async () => { + const createStudioServerStub = sinon.stub().resolves({ + initializeRoutes: sinon.stub(), + canAccessStudioAI: sinon.stub().resolves(true), + initializeStudioAI: sinon.stub().resolves(), + reportError: sinon.stub(), + destroy: sinon.stub().resolves(), + addSocketListeners: sinon.stub(), + captureStudioEvent: sinon.stub().resolves(), + updateSessionId: sinon.stub(), + connectToBrowser: sinon.stub(), + }) + + const StubbedStudioManager = (proxyquire('../lib/cloud/studio/studio', { + '../api/studio/report_studio_error': { reportStudioError: sinon.stub() }, + './StudioElectron': { StudioElectron: class {} }, + '../require_script': { + requireScript: () => ({ + default: { createStudioServer: createStudioServerStub }, + }), + }, + }) as typeof import('@packages/server/lib/cloud/studio/studio')).StudioManager + + const manager = new StubbedStudioManager() + + await manager.setup({ + script: 'script', + studioPath: 'path', + studioHash: 'abcdefg', + getProjectOptions: sinon.stub().resolves({ projectSlug: '1234' }), + cloudApi: {} as any, + manifest: { 'server/index.js': 'abcdefg' }, + }) + + expect(createStudioServerStub).to.have.been.calledOnce + const options = createStudioServerStub.firstCall.args[0] + + expect(options).to.have.property('debugData') + expect(options.debugData).to.be.undefined + }) + }) + describe('synchronous method invocation', () => { it('reports an error when a synchronous method fails', () => { const error = new Error('foo') @@ -217,6 +300,50 @@ describe('lib/cloud/studio', () => { }) }) + describe('getStudioConfig and getCachedStudioConfig', () => { + const browser = { + name: 'chrome', + family: 'chromium' as const, + channel: 'stable', + displayName: 'Chrome', + version: '120.0.0', + majorVersion: '120', + path: '/path/to/chrome', + isHeaded: true, + isHeadless: false, + } + + it('getStudioConfig returns config when server is initialized', async () => { + const config = await studioManager.getStudioConfig(browser as Cypress.Browser) + + expect(config).to.have.property('AI') + expect(config.AI).to.have.property('enabled') + expect(config).to.have.property('featureFlags') + }) + + it('getStudioConfig throws when server is not initialized', async () => { + const manager = new StudioManager() + + await expect(manager.getStudioConfig(browser as Cypress.Browser)) + .to.be.rejectedWith('Studio is not available: server not initialized or an error occurred') + }) + + it('getCachedStudioConfig returns config when server is initialized', () => { + const config = studioManager.getCachedStudioConfig() + + expect(config).to.have.property('AI') + expect(config.AI).to.have.property('enabled') + expect(config).to.have.property('featureFlags') + }) + + it('getCachedStudioConfig throws when server is not initialized', () => { + const manager = new StudioManager() + + expect(() => manager.getCachedStudioConfig()) + .to.throw('Studio is not available: server not initialized or an error occurred') + }) + }) + describe('addSocketListeners', () => { it('calls addSocketListeners on the studio server', () => { sinon.stub(studio, 'addSocketListeners') diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index d2cbbc5ff2..a3aa56d7ee 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -2,6 +2,7 @@ import type Database from 'better-sqlite3' import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping' import type { IncomingHttpHeaders } from 'http' import type { Readable } from 'stream' +import type { DebugData } from './studio/studio-server-types' import type { ProxyTimings } from './proxy' import type { FoundSpec } from './spec' @@ -105,9 +106,7 @@ export type ProtocolManagerOptions = { } projectConfig: ProjectConfig mountVersion?: number - debugData?: { - filePreprocessorHandlerText?: string - } + debugData?: DebugData mode?: 'record' | 'studio' } diff --git a/packages/types/src/studio/studio-server-types.ts b/packages/types/src/studio/studio-server-types.ts index e9c0a2badb..310a33b1ee 100644 --- a/packages/types/src/studio/studio-server-types.ts +++ b/packages/types/src/studio/studio-server-types.ts @@ -70,6 +70,19 @@ export type BrowserWindow = { show: () => void } +export interface BrowserSession { + browserWindow: BrowserWindow + done: () => Promise + isAborted: () => boolean +} + +export interface BrowserManager { + createSession(): Promise + initialize(): Promise + destroy(): Promise + computeVisibility: boolean +} + export type StudioElectronApi = { createBrowserWindow: () => BrowserWindow } @@ -100,6 +113,7 @@ export interface StudioServerOptions { manifest?: Record verifyHash: (contents: BinaryLike, expectedHash: string) => boolean studioElectron?: StudioElectronApi + debugData?: DebugData } export interface StudioAIInitializeOptions { @@ -113,6 +127,31 @@ export interface StudioAddSocketListenersOptions { onAfterSave: (options: { error?: Error }) => void } +export type AIDisabledReason = + | 'ai_disabled_locally' + | 'browser_not_supported' + | 'studio_ai_feature_flag_disabled' + | 'no_project_slug' + | 'project_not_found' + | 'no_user' + | 'org_ai_disabled' + | 'not_org_member' + | 'not_project_member' + | 'error' + +export interface StudioConfig { + AI: { + enabled: boolean + disabledReason?: AIDisabledReason + } + organizationUuid?: string + sessionId?: string + featureFlags: { + studioNonNativeEvents: boolean + studioAI: boolean + } +} + export type StudioCDPCommands = ProtocolMapping.Commands export type StudioCDPCommand = @@ -138,11 +177,13 @@ export interface StudioCDPClient { } export interface StudioServerShape { + sessionId?: string initializeRoutes(router: Router): void canAccessStudioAI(browser: Cypress.Browser): Promise + getStudioConfig(browser: Cypress.Browser): Promise + getCachedStudioConfig(): StudioConfig addSocketListeners(options: StudioAddSocketListenersOptions | Socket): void initializeStudioAI(options: StudioAIInitializeOptions): Promise - connectToBrowser(cdpClient: StudioCDPClient): void updateSessionId(sessionId: string): void reportError( error: unknown, @@ -151,6 +192,7 @@ export interface StudioServerShape { ): void destroy(): Promise captureStudioEvent(event: StudioEvent): Promise + connectToBrowser(cdpClient: StudioCDPClient): void } export interface StudioServerDefaultShape { @@ -159,3 +201,11 @@ export interface StudioServerDefaultShape { ) => Promise MOUNT_VERSION: number } + +export type SnapshotRendererVisibilityAlgorithm = + | 'default' + | 'experimental-fast' + +export type DebugData = { + filePreprocessorHandlerText?: string +}