diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts
index 4339f79b5d7..84b0701d77b 100644
--- a/cli/types/cypress.d.ts
+++ b/cli/types/cypress.d.ts
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
///
///
///
@@ -687,22 +686,22 @@ declare namespace Cypress {
Keyboard: {
defaults(options: Partial): void
Keys: {
- DOWN: 'ArrowDown',
- LEFT: 'ArrowLeft',
- RIGHT: 'ArrowRight',
- UP: 'ArrowUp',
- END: 'End',
- HOME: 'Home',
- PAGEDOWN: 'PageDown',
- PAGEUP: 'PageUp',
- ENTER: 'Enter',
- TAB: 'Tab',
- BACKSPACE: 'Backspace',
- SPACE: 'Space',
- DELETE: 'Delete',
- INSERT: 'Insert',
- ESC: 'Escape',
- },
+ DOWN: 'ArrowDown'
+ LEFT: 'ArrowLeft'
+ RIGHT: 'ArrowRight'
+ UP: 'ArrowUp'
+ END: 'End'
+ HOME: 'Home'
+ PAGEDOWN: 'PageDown'
+ PAGEUP: 'PageUp'
+ ENTER: 'Enter'
+ TAB: 'Tab'
+ BACKSPACE: 'Backspace'
+ SPACE: 'Space'
+ DELETE: 'Delete'
+ INSERT: 'Insert'
+ ESC: 'Escape'
+ }
}
/**
@@ -758,7 +757,7 @@ declare namespace Cypress {
* Trigger action
* @private
*/
- action: (action: string, ...args: any[]) => T
+ action: (action: string, ...args: any[]) => T
/**
* Load files
@@ -1857,7 +1856,7 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/prompt
*/
- prompt(steps: string[], options?: PromptOptions): Chainable
+ prompt(steps: string[], options?: PromptOptions): Chainable
/**
* Read a file and yield its contents.
*
@@ -2906,8 +2905,8 @@ declare namespace Cypress {
}
type RetryStrategyWithModeSpecs = RetryStrategy & {
- openMode: boolean; // defaults to false
- runMode: boolean; // defaults to true
+ openMode: boolean // defaults to false
+ runMode: boolean // defaults to true
}
type RetryStrategy =
@@ -2915,18 +2914,18 @@ declare namespace Cypress {
| RetryStrategyDetectFlakeButAlwaysFailType
interface RetryStrategyDetectFlakeAndPassOnThresholdType {
- experimentalStrategy: "detect-flake-and-pass-on-threshold"
+ experimentalStrategy: 'detect-flake-and-pass-on-threshold'
experimentalOptions?: {
- maxRetries: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0
- passesRequired: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0 and <= maxRetries
+ maxRetries: number // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0
+ passesRequired: number // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0 and <= maxRetries
}
}
interface RetryStrategyDetectFlakeButAlwaysFailType {
- experimentalStrategy: "detect-flake-but-always-fail"
+ experimentalStrategy: 'detect-flake-but-always-fail'
experimentalOptions?: {
- maxRetries: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0
- stopIfAnyPassed: boolean; // defaults to false if experimentalOptions is not provided
+ maxRetries: number // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0
+ stopIfAnyPassed: boolean // defaults to false if experimentalOptions is not provided
}
}
interface ResolvedConfigOptions {
@@ -3148,7 +3147,7 @@ declare namespace Cypress {
* @see https://on.cypress.io/experiments#Experimental-CSP-Allow-List
* @default false
*/
- experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[],
+ experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[]
/**
* Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode.
* @default false
@@ -3281,7 +3280,7 @@ declare namespace Cypress {
*/
experimentalOriginDependencies?: boolean
/**
- * Enables support for the prompt command.
+ * Enables support for `cy.prompt`, an AI-powered command that turns natural language steps into executable Cypress test code.
* @default false
*/
experimentalPromptCommand?: boolean
diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts
index 7055f6c8c5f..4bddeb5e828 100644
--- a/packages/app/src/runner/event-manager.ts
+++ b/packages/app/src/runner/event-manager.ts
@@ -919,6 +919,7 @@ export class EventManager {
crossOriginLogs = {}
this.studioStore.setActive(false)
this.promptStore.resetState()
+ await new Promise((resolve) => this.ws.emit('prompt:reset', resolve))
}
resetReporter () {
diff --git a/packages/app/src/runner/index.ts b/packages/app/src/runner/index.ts
index a5f810b3e64..9ce7f903692 100644
--- a/packages/app/src/runner/index.ts
+++ b/packages/app/src/runner/index.ts
@@ -190,7 +190,7 @@ function teardownSpec (isRerun: boolean = false) {
export async function teardown () {
UnifiedReporterAPI.setInitializedReporter(false)
_eventManager?.stop()
- _eventManager?.teardown(getMobxRunnerStore())
+ await _eventManager?.teardown(getMobxRunnerStore())
await _eventManager?.resetReporter()
_eventManager = undefined
}
diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts
index f666159240a..b214a860141 100644
--- a/packages/config/src/options.ts
+++ b/packages/config/src/options.ts
@@ -231,25 +231,25 @@ const driverConfigOptions: Array = [
overrideLevel: 'any',
requireRestartOnChange: 'browser',
}, {
- name: 'experimentalSourceRewriting',
+ name: 'experimentalPromptCommand',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
- name: 'experimentalSingleTabRunMode',
+ name: 'experimentalSourceRewriting',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
- name: 'experimentalStudio',
+ name: 'experimentalSingleTabRunMode',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
- name: 'experimentalPromptCommand',
+ name: 'experimentalStudio',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
diff --git a/packages/config/test/__snapshots__/index.spec.ts.snap b/packages/config/test/__snapshots__/index.spec.ts.snap
index 2e4a587627e..2c54f643686 100644
--- a/packages/config/test/__snapshots__/index.spec.ts.snap
+++ b/packages/config/test/__snapshots__/index.spec.ts.snap
@@ -223,7 +223,7 @@ exports[`config/src/index > .getPublicConfigKeys > returns list of public config
"experimentalModifyObstructiveThirdPartyCode",
"injectDocumentDomain",
"experimentalOriginDependencies",
- "experimentalPromptCommand": false,
+ "experimentalPromptCommand",
"experimentalSourceRewriting",
"experimentalSingleTabRunMode",
"experimentalStudio",
diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts
index a6f2ac7b4f2..4c952f53a99 100644
--- a/packages/config/test/project/utils.spec.ts
+++ b/packages/config/test/project/utils.spec.ts
@@ -1008,76 +1008,6 @@ describe('config/src/project/utils', () => {
const getFilesByGlob = vi.fn().mockReturnValue(['path/to/file'])
-<<<<<<< HEAD
- return mergeDefaults(obj, options, {}, getFilesByGlob)
- .then((cfg) => {
- expect(cfg.resolved).to.deep.eq({
- animationDistanceThreshold: { value: 5, from: 'default' },
- arch: { value: os.arch(), from: 'default' },
- baseUrl: { value: null, from: 'default' },
- blockHosts: { value: null, from: 'default' },
- browsers: { value: [], from: 'default' },
- chromeWebSecurity: { value: true, from: 'default' },
- clientCertificates: { value: [], from: 'default' },
- defaultBrowser: { value: null, from: 'default' },
- defaultCommandTimeout: { value: 4000, from: 'default' },
- downloadsFolder: { value: 'cypress/downloads', from: 'default' },
- env: {},
- excludeSpecPattern: { value: '*.hot-update.js', from: 'default' },
- execTimeout: { value: 60000, from: 'default' },
- experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
- experimentalCspAllowList: { value: false, from: 'default' },
- experimentalInteractiveRunEvents: { value: false, from: 'default' },
- experimentalMemoryManagement: { value: false, from: 'default' },
- experimentalOriginDependencies: { value: false, from: 'default' },
- experimentalRunAllSpecs: { value: false, from: 'default' },
- experimentalSingleTabRunMode: { value: false, from: 'default' },
- experimentalStudio: { value: false, from: 'default' },
- experimentalPromptCommand: { value: false, from: 'default' },
- experimentalSourceRewriting: { value: false, from: 'default' },
- experimentalWebKitSupport: { value: false, from: 'default' },
- fileServerFolder: { value: '', from: 'default' },
- fixturesFolder: { value: 'cypress/fixtures', from: 'default' },
- hosts: { value: null, from: 'default' },
- includeShadowDom: { value: false, from: 'default' },
- injectDocumentDomain: { value: false, from: 'default' },
- justInTimeCompile: { value: true, from: 'default' },
- isInteractive: { value: true, from: 'default' },
- keystrokeDelay: { value: 0, from: 'default' },
- modifyObstructiveCode: { value: true, from: 'default' },
- numTestsKeptInMemory: { value: 50, from: 'default' },
- pageLoadTimeout: { value: 60000, from: 'default' },
- platform: { value: os.platform(), from: 'default' },
- port: { value: 1234, from: 'cli' },
- projectId: { value: null, from: 'default' },
- redirectionLimit: { value: 20, from: 'default' },
- reporter: { value: 'json', from: 'cli' },
- resolvedNodePath: { value: null, from: 'default' },
- resolvedNodeVersion: { value: null, from: 'default' },
- reporterOptions: { value: null, from: 'default' },
- requestTimeout: { value: 5000, from: 'default' },
- responseTimeout: { value: 30000, from: 'default' },
- retries: { value: { runMode: 0, openMode: 0, experimentalStrategy: undefined, experimentalOptions: undefined }, from: 'default' },
- screenshotOnRunFailure: { value: true, from: 'default' },
- screenshotsFolder: { value: 'cypress/screenshots', from: 'default' },
- specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' },
- slowTestThreshold: { value: 10000, from: 'default' },
- supportFile: { value: false, from: 'config' },
- supportFolder: { value: false, from: 'default' },
- taskTimeout: { value: 60000, from: 'default' },
- testIsolation: { value: true, from: 'default' },
- trashAssetsBeforeRuns: { value: true, from: 'default' },
- userAgent: { value: null, from: 'default' },
- video: { value: false, from: 'default' },
- videoCompression: { value: false, from: 'default' },
- videosFolder: { value: 'cypress/videos', from: 'default' },
- viewportHeight: { value: 660, from: 'default' },
- viewportWidth: { value: 1000, from: 'default' },
- waitForAnimations: { value: true, from: 'default' },
- scrollBehavior: { value: 'top', from: 'default' },
- watchForFileChanges: { value: true, from: 'default' },
- })
-=======
const cfg = await mergeDefaults(obj, options, {}, getFilesByGlob)
expect(cfg.resolved).toEqual({
@@ -1099,6 +1029,7 @@ describe('config/src/project/utils', () => {
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },
+ experimentalPromptCommand: { value: false, from: 'default' },
experimentalRunAllSpecs: { value: false, from: 'default' },
experimentalSingleTabRunMode: { value: false, from: 'default' },
experimentalStudio: { value: false, from: 'default' },
@@ -1144,7 +1075,6 @@ describe('config/src/project/utils', () => {
waitForAnimations: { value: true, from: 'default' },
scrollBehavior: { value: 'top', from: 'default' },
watchForFileChanges: { value: true, from: 'default' },
->>>>>>> origin/develop
})
})
@@ -1192,62 +1122,6 @@ describe('config/src/project/utils', () => {
value: 'foo',
from: 'config',
},
-<<<<<<< HEAD
- excludeSpecPattern: { value: '*.hot-update.js', from: 'default' },
- execTimeout: { value: 60000, from: 'default' },
- experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
- experimentalCspAllowList: { value: false, from: 'default' },
- experimentalInteractiveRunEvents: { value: false, from: 'default' },
- experimentalMemoryManagement: { value: false, from: 'default' },
- experimentalOriginDependencies: { value: false, from: 'default' },
- experimentalRunAllSpecs: { value: false, from: 'default' },
- experimentalSingleTabRunMode: { value: false, from: 'default' },
- experimentalStudio: { value: false, from: 'default' },
- experimentalPromptCommand: { value: false, from: 'default' },
- experimentalSourceRewriting: { value: false, from: 'default' },
- experimentalWebKitSupport: { value: false, from: 'default' },
- fileServerFolder: { value: '', from: 'default' },
- fixturesFolder: { value: 'cypress/fixtures', from: 'default' },
- hosts: { value: null, from: 'default' },
- includeShadowDom: { value: false, from: 'default' },
- injectDocumentDomain: { value: false, from: 'default' },
- justInTimeCompile: { value: true, from: 'default' },
- isInteractive: { value: true, from: 'default' },
- keystrokeDelay: { value: 0, from: 'default' },
- modifyObstructiveCode: { value: true, from: 'default' },
- numTestsKeptInMemory: { value: 50, from: 'default' },
- pageLoadTimeout: { value: 60000, from: 'default' },
- platform: { value: os.platform(), from: 'default' },
- port: { value: 2020, from: 'config' },
- projectId: { value: 'projectId123', from: 'env' },
- redirectionLimit: { value: 20, from: 'default' },
- reporter: { value: 'spec', from: 'default' },
- resolvedNodePath: { value: null, from: 'default' },
- resolvedNodeVersion: { value: null, from: 'default' },
- reporterOptions: { value: null, from: 'default' },
- requestTimeout: { value: 5000, from: 'default' },
- responseTimeout: { value: 30000, from: 'default' },
- retries: { value: { runMode: 0, openMode: 0, experimentalStrategy: undefined, experimentalOptions: undefined }, from: 'default' },
- screenshotOnRunFailure: { value: true, from: 'default' },
- screenshotsFolder: { value: 'cypress/screenshots', from: 'default' },
- slowTestThreshold: { value: 10000, from: 'default' },
- specPattern: { value: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', from: 'default' },
- supportFile: { value: false, from: 'config' },
- supportFolder: { value: false, from: 'default' },
- taskTimeout: { value: 60000, from: 'default' },
- testIsolation: { value: true, from: 'default' },
- trashAssetsBeforeRuns: { value: true, from: 'default' },
- userAgent: { value: null, from: 'default' },
- video: { value: false, from: 'default' },
- videoCompression: { value: false, from: 'default' },
- videosFolder: { value: 'cypress/videos', from: 'default' },
- viewportHeight: { value: 660, from: 'default' },
- viewportWidth: { value: 1000, from: 'default' },
- waitForAnimations: { value: true, from: 'default' },
- scrollBehavior: { value: 'top', from: 'default' },
- watchForFileChanges: { value: true, from: 'default' },
- })
-=======
bar: {
value: 'bar',
from: 'envFile',
@@ -1272,6 +1146,7 @@ describe('config/src/project/utils', () => {
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },
+ experimentalPromptCommand: { value: false, from: 'default' },
experimentalRunAllSpecs: { value: false, from: 'default' },
experimentalSingleTabRunMode: { value: false, from: 'default' },
experimentalStudio: { value: false, from: 'default' },
@@ -1317,7 +1192,6 @@ describe('config/src/project/utils', () => {
waitForAnimations: { value: true, from: 'default' },
scrollBehavior: { value: 'top', from: 'default' },
watchForFileChanges: { value: true, from: 'default' },
->>>>>>> origin/develop
})
})
diff --git a/packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts b/packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts
index b0b7ceb7828..8e751dde4dc 100644
--- a/packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts
+++ b/packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts
@@ -1,40 +1,30 @@
describe('src/cy/commands/prompt', () => {
- it('errors if wait for ready does not return success and error is ENOSPC', (done) => {
- const backendStub = cy.stub(Cypress, 'backend').log(false)
-
- const error = new Error(`no space left on device, open 'bundle.tar`)
-
- error.name = 'ENOSPC'
-
- backendStub.callThrough()
- backendStub.withArgs('wait:for:prompt:ready').resolves({ success: false, error })
-
+ it('errors if download timeout is reached', (done) => {
cy.on('fail', (err) => {
- expect(err.message).to.include('Failed to download cy.prompt Cloud code')
- expect(err.message).to.include(`no space left on device, open 'bundle.tar`)
-
+ expect(err.message).to.include('Timed out downloading `cy.prompt` Cloud code')
done()
})
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
- cy['commandFns']['prompt'].__resetPrompt()
- cy.prompt(['Hello, world!'])
+ cy['commandFns']['prompt'].__resetPrompt(10000)
+ // @ts-expect-error - _downloadTimeout is a private option
+ cy.prompt(['Click the "click me" button'], { _downloadTimeout: 10 })
})
- it('errors if wait for ready does not return success and error is ECONNREFUSED', (done) => {
+ it('errors if wait for ready does not return success and error is ENOSPC', (done) => {
const backendStub = cy.stub(Cypress, 'backend').log(false)
- const error = new Error(`'bundle.tar' timed out after 10000s`)
+ const error = new Error(`no space left on device, open /Users/ruby/dev/bundle.tar`)
- error.name = 'ECONNREFUSED'
+ ;(error as any).code = 'ENOSPC'
backendStub.callThrough()
backendStub.withArgs('wait:for:prompt:ready').resolves({ success: false, error })
cy.on('fail', (err) => {
- expect(err.message).to.include('Timed out waiting for cy.prompt Cloud code:')
- expect(err.message).to.include(`'bundle.tar' timed out after 10000s`)
+ expect(err.message).to.include('Failed to download `cy.prompt` Cloud code')
+ expect(err.message).to.include(`no space left on device, open /Users/ruby/dev/bundle.tar`)
done()
})
diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts
index 02d92d26d07..8ddd63a6e98 100644
--- a/packages/driver/src/cy/commands/prompt/index.ts
+++ b/packages/driver/src/cy/commands/prompt/index.ts
@@ -15,30 +15,43 @@ declare global {
}
}
+interface BundleResultInError {
+ error: Error
+ timedOut: false
+}
+
+interface BundleResultTimedOut {
+ error: undefined
+ timedOut: true
+}
+
+interface BundleResultSuccess {
+ error: undefined
+ bundle: Awaited>
+ timedOut: false
+}
+
+type BundleResult = BundleResultInError | BundleResultTimedOut | BundleResultSuccess
+
let initializedModule: CyPromptDriverDefaultShape | null = null
const initializeModule = async (Cypress: Cypress.Cypress): Promise => {
// Wait for the cy prompt bundle to be downloaded and ready
const { success, error } = await Cypress.backend('wait:for:prompt:ready')
if (error) {
- if (error.name === 'ENOSPC') {
- $errUtils.throwErrByPath('prompt.promptDownloadError', {
- args: {
- error,
- },
- })
- } else {
- $errUtils.throwErrByPath('prompt.promptDownloadTimedOut', {
- args: {
- error,
- },
- })
- }
+ $errUtils.throwErrByPath('prompt.promptDownloadError', {
+ args: {
+ error,
+ },
+ })
}
if (!success && !error) {
- // TODO: Generic error message
- throw new Error('error waiting for cy prompt bundle to be downloaded and ready')
+ $errUtils.throwErrByPath('prompt.promptDownloadError', {
+ args: {
+ error: new Error('error waiting for cy prompt bundle to be downloaded and ready'),
+ },
+ })
}
// Once the cy prompt bundle is downloaded and ready,
@@ -60,16 +73,19 @@ const initializeModule = async (Cypress: Cypress.Cypress): Promise('cy-prompt')
if (!module?.default) {
- // TODO: Generic error message
- throw new Error('error loading cy prompt driver')
+ $errUtils.throwErrByPath('prompt.promptDownloadError', {
+ args: {
+ error: new Error('error loading cy prompt driver'),
+ },
+ })
}
- initializedModule = module.default
+ initializedModule = module!.default
return initializedModule
}
-const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise | Error> => {
+const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise => {
try {
let cloudModule = initializedModule
@@ -84,25 +100,33 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp
})
}
- return await cloudModule.createCyPrompt({
- Cypress: Cypress as CypressInternal,
- cy,
- eventManager: window.getEventManager ? window.getEventManager() : undefined,
- errorUtils: {
- extendErrorMessages: $errUtils.extendErrorMessages,
- throwErrByPath: $errUtils.throwErrByPath,
- },
- getSourceDetailsForFirstLine: $stackUtils.getSourceDetailsForFirstLine,
- onMoreInfoNeeded: ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
- if (Cypress.isCrossOriginSpecBridge) {
- Cypress.specBridgeCommunicator.toPrimary('prompt:more-info-needed', { testId, logId, onSave, onCancel })
- } else {
- window.getEventManager!().localBus.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
- }
- },
- })
+ return {
+ bundle: await cloudModule.createCyPrompt({
+ Cypress: Cypress as CypressInternal,
+ cy,
+ eventManager: window.getEventManager ? window.getEventManager() : undefined,
+ errorUtils: {
+ extendErrorMessages: $errUtils.extendErrorMessages,
+ throwErrByPath: $errUtils.throwErrByPath,
+ },
+ getSourceDetailsForFirstLine: $stackUtils.getSourceDetailsForFirstLine,
+ onMoreInfoNeeded: ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
+ if (Cypress.isCrossOriginSpecBridge) {
+ Cypress.specBridgeCommunicator.toPrimary('prompt:more-info-needed', { testId, logId, onSave, onCancel })
+ } else {
+ window.getEventManager!().localBus.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
+ }
+ },
+ }),
+ error: undefined,
+ timedOut: false,
+ }
} catch (error) {
- return error
+ return {
+ error,
+ bundle: undefined,
+ timedOut: false,
+ }
}
}
@@ -115,12 +139,41 @@ export default (Commands: Cypress.Cypress['Commands'], Cypress: Cypress.Cypress,
prompt (steps: string[], commandOptions: object = {}) {
const promptCmd = cy.state('current')
- return cy.wrap(initializeCloudCyPromptPromise, { log: false, timeout: 45000 }).then((bundleResult: Awaited>) => {
- if (bundleResult instanceof Error) {
- throw bundleResult
+ const downloadTimeout = '_downloadTimeout' in commandOptions ? commandOptions._downloadTimeout as number : 45000
+
+ let timeoutId: NodeJS.Timeout
+ const timeoutPromise = new Promise((resolve) => {
+ timeoutId = setTimeout(() => {
+ resolve({
+ error: undefined,
+ timedOut: true,
+ })
+ }, downloadTimeout)
+ })
+ const raceBundleResult = Promise.race([
+ initializeCloudCyPromptPromise,
+ timeoutPromise,
+ ]).finally(() => {
+ clearTimeout(timeoutId)
+ }) as Promise
+
+ return cy.wrap(raceBundleResult, { log: false, timeout: 1e9 }).then((bundleResult: BundleResult) => {
+ if (bundleResult.timedOut) {
+ cy.state('current', promptCmd)
+
+ return $errUtils.throwErrByPath('prompt.promptDownloadTimedOut', {
+ args: {
+ error: new Error('cy.prompt bundle download timed out'),
+ },
+ })
+ }
+
+ if (bundleResult.error) {
+ cy.state('current', promptCmd)
+ throw bundleResult.error
}
- const cyPrompt = bundleResult
+ const cyPrompt = bundleResult.bundle
return cyPrompt({
steps,
@@ -131,9 +184,9 @@ export default (Commands: Cypress.Cypress['Commands'], Cypress: Cypress.Cypress,
},
}
- commands.prompt['__resetPrompt'] = () => {
+ commands.prompt['__resetPrompt'] = async (delay: number = 0) => {
initializedModule = null
- initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)
+ initializeCloudCyPromptPromise = new Promise((resolve) => setTimeout(resolve, delay)).then(() => initializeCloudCyPrompt(Cypress, cy))
}
Commands.addAll(commands)
diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts
index 4e9fb4cd45c..307831b990e 100644
--- a/packages/driver/src/cypress/error_messages.ts
+++ b/packages/driver/src/cypress/error_messages.ts
@@ -1335,11 +1335,11 @@ export default {
promptDownloadError (obj) {
return {
message: stripIndent`\
- Failed to download cy.prompt Cloud code:
+ Failed to download \`cy.prompt\` Cloud code:
- - ${obj.error.code}: ${obj.error.message}
+ - ${obj.error.code ? `${obj.error.code}: ` : ''}${obj.error.message}
- Check your network connection and file settings to ensure download is not interrupted.
+ Check your network connection and file settings to ensure download is not interrupted.
`,
docsUrl: 'https://on.cypress.io/prompt-download-error',
}
@@ -1347,11 +1347,9 @@ export default {
promptDownloadTimedOut (obj) {
return {
message: stripIndent`\
- Timed out waiting for cy.prompt Cloud code:
-
- - ${obj.error.code ? `${obj.error.code}: ` : ''}${obj.error.message}
+ Timed out downloading \`cy.prompt\` Cloud code.
- Check your network connection and system configuration.
+ Check your network connection and system configuration to ensure download is not interrupted.
`,
docsUrl: 'https://on.cypress.io/prompt-download-error',
}
diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts
index ae4875a4eaf..4ba70924a25 100644
--- a/packages/driver/src/cypress/stack_utils.ts
+++ b/packages/driver/src/cypress/stack_utils.ts
@@ -519,6 +519,11 @@ const normalizedUserInvocationStack = (userInvocationStack) => {
|| line.includes('Chainer.prototype[key]')
|| line.includes('cy.')
|| line.includes('$Chainer.')
+ // Note that these are hard coded for now, because they are happening in the prompt command
+ // for some reason. Long term, we should make this more dynamic if necessary.
+ || line.includes('$Cy.prompt')
+ || line.includes('$Chainer.prompt')
+ || line.includes('CyPrompt.prototype.prompt')
}).join('\n')
return normalizeStackIndentation(nonCypressStackLines)
diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json
index b2eef2fb099..ee6cbc6897a 100644
--- a/packages/frontend-shared/src/locales/en-US.json
+++ b/packages/frontend-shared/src/locales/en-US.json
@@ -615,7 +615,7 @@
},
"experimentalPromptCommand": {
"name": "Prompt command",
- "description": "Enables support for the prompt command."
+ "description": "Enables support for `cy.prompt`, an AI-powered command that turns natural language steps into executable Cypress test code."
},
"experimentalWebKitSupport": {
"name": "WebKit Support",
diff --git a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts
index 717095c0de2..fe943026f43 100644
--- a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts
+++ b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts
@@ -5,13 +5,10 @@ import { getCyPromptBundle } from '../api/cy-prompt/get_cy_prompt_bundle'
import path from 'path'
import { verifySignature } from '../encryption'
-const DOWNLOAD_TIMEOUT = 30000
-
interface EnsureCyPromptBundleOptions {
cyPromptPath: string
cyPromptUrl: string
projectId?: string
- downloadTimeoutMs?: number
}
/**
@@ -20,31 +17,19 @@ interface EnsureCyPromptBundleOptions {
* @param options.cyPromptPath - The path to extract the cy prompt bundle to
* @param options.cyPromptUrl - The URL of the cy prompt bundle
* @param options.projectId - The project ID of the cy prompt bundle
- * @param options.downloadTimeoutMs - The timeout for the cy prompt bundle download
*/
-export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId, downloadTimeoutMs = DOWNLOAD_TIMEOUT }: EnsureCyPromptBundleOptions): Promise> => {
+export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId }: EnsureCyPromptBundleOptions): Promise> => {
const bundlePath = path.join(cyPromptPath, 'bundle.tar')
// First remove cyPromptPath to ensure we have a clean slate
await remove(cyPromptPath)
await ensureDir(cyPromptPath)
- let timeoutId: NodeJS.Timeout
-
- const responseManifestSignature: string = await Promise.race([
- getCyPromptBundle({
- cyPromptUrl,
- projectId,
- bundlePath,
- }),
- new Promise((_, reject) => {
- timeoutId = setTimeout(() => {
- reject(new Error('Cy prompt bundle download timed out'))
- }, downloadTimeoutMs)
- }),
- ]).finally(() => {
- clearTimeout(timeoutId)
- }) as string
+ const responseManifestSignature: string = await getCyPromptBundle({
+ cyPromptUrl,
+ projectId,
+ bundlePath,
+ })
await tar.extract({
file: bundlePath,
diff --git a/packages/server/lib/experiments.ts b/packages/server/lib/experiments.ts
index 555bbca5c9d..e3c374ba8fd 100644
--- a/packages/server/lib/experiments.ts
+++ b/packages/server/lib/experiments.ts
@@ -60,7 +60,7 @@ const _summaries: StringValues = {
experimentalRunAllSpecs: 'Enables the "Run All Specs" UI feature, allowing the execution of multiple specs sequentially',
experimentalOriginDependencies: 'Enables support for `Cypress.require()` for including dependencies within the `cy.origin()` callback.',
experimentalMemoryManagement: 'Enables support for improved memory management within Chromium-based browsers.',
- experimentalPromptCommand: 'Enables support for the prompt command.',
+ experimentalPromptCommand: 'Enables support for `cy.prompt`, an AI-powered command that turns natural language steps into executable Cypress test code.',
}
/**
diff --git a/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts b/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts
index 054db8fb021..8cc2f349dc9 100644
--- a/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts
+++ b/packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts
@@ -101,23 +101,4 @@ describe('ensureCyPromptBundle', () => {
await expect(ensureCyPromptBundlePromise).to.be.rejectedWith('Unable to find cy-prompt manifest')
})
-
- it('should throw an error if the cy prompt bundle download times out', async () => {
- getCyPromptBundleStub.callsFake(() => {
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve(new Error('Cy prompt bundle download timed out'))
- }, 3000)
- })
- })
-
- const ensureCyPromptBundlePromise = ensureCyPromptBundle({
- cyPromptPath: '/tmp/cypress/cy-prompt/123',
- cyPromptUrl: 'https://cypress.io/cy-prompt',
- projectId: '123',
- downloadTimeoutMs: 500,
- })
-
- await expect(ensureCyPromptBundlePromise).to.be.rejectedWith('Cy prompt bundle download timed out')
- })
})
diff --git a/system-tests/__snapshots__/results_spec.ts.js b/system-tests/__snapshots__/results_spec.ts.js
index dfbf7708d64..bc18840b17c 100644
--- a/system-tests/__snapshots__/results_spec.ts.js
+++ b/system-tests/__snapshots__/results_spec.ts.js
@@ -28,10 +28,10 @@ exports['module api and after:run results'] = `
"experimentalModifyObstructiveThirdPartyCode": false,
"injectDocumentDomain": false,
"experimentalOriginDependencies": false,
+ "experimentalPromptCommand": false,
"experimentalSourceRewriting": false,
"experimentalSingleTabRunMode": false,
"experimentalStudio": false,
- "experimentalPromptCommand": false,
"experimentalWebKitSupport": false,
"fileServerFolder": "/path/to/fileServerFolder",
"fixturesFolder": "/path/to/fixturesFolder",