Skip to content

chore: handle errors #31854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/driver/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const baseConfig: Cypress.ConfigOptions = {
},
component: {
experimentalSingleTabRunMode: true,
specPattern: 'cypress/component/**/*.cy.js',
specPattern: 'cypress/component/**/*.cy.{js,ts}',
supportFile: false,
devServer: (devServerOptions) => {
return cypressWebpackDevServer({
Expand Down
12 changes: 12 additions & 0 deletions packages/driver/cypress/component/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,16 @@ describe('component testing', () => {
expect(Cypress.log).to.be.calledWithMatch(sinon.match({ 'message': `Error: "Promise rejected with a string!"`, name: 'uncaught exception' }))
})
})

it('fails when trying to use cy.prompt in component tests', (done) => {
cy.spy(Cypress, 'log').log(false)

cy.on('fail', (err) => {
expect(err.message).to.include('`cy.prompt` is currently only supported in end-to-end tests.')

done()
})

cy.prompt('Hello, world!')
})
})
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
describe('src/cy/commands/prompt', () => {
it('errors if wait for ready does not return success', (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(`no space left on device, open '<stripped-path>bundle.tar`)

error.name = 'ENOSPC'

backendStub.callThrough()
backendStub.withArgs('wait:for:cy:prompt:ready').resolves({ success: false, error })

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 '<stripped-path>bundle.tar`)

done()
})

cy.visit('http://www.foobar.com:3500/fixtures/dom.html')

cy['commandFns']['prompt'].__reset()
// @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag
cy.prompt('Hello, world!')
})

it('errors if wait for ready does not return success and error is ECONNREFUSED', (done) => {
const backendStub = cy.stub(Cypress, 'backend').log(false)

const error = new Error(`'<stripped-path>bundle.tar' timed out after 10000s`)

error.name = 'ECONNREFUSED'

backendStub.callThrough()
backendStub.withArgs('wait:for:cy:prompt:ready').resolves({ success: false })
backendStub.withArgs('wait:for:cy:prompt:ready').resolves({ success: false, error })

cy.on('fail', (err) => {
expect(err.message).to.include('error waiting for cy prompt bundle to be downloaded and ready')
expect(err.message).to.include('Timed out waiting for cy.prompt Cloud code:')
expect(err.message).to.include(`'<stripped-path>bundle.tar' timed out after 10000s`)

done()
})
Expand Down
18 changes: 18 additions & 0 deletions packages/driver/cypress/e2e/commands/prompt/prompt.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,22 @@ describe('src/cy/commands/prompt', () => {
cy.prompt('Hello, world!')
})
})

it('fails when trying to use cy.prompt in a browser that is not supported', (done) => {
if (Cypress.isBrowser({ family: 'chromium' })) {
done()

return
}

cy.on('fail', (err) => {
expect(err.message).to.include('`cy.prompt` is only supported in Chromium-based browsers.')

done()
})

cy.visit('http://www.foobar.com:3500/fixtures/dom.html')

cy.prompt('Hello, world!')
})
})
31 changes: 28 additions & 3 deletions packages/driver/src/cy/commands/prompt/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { init, loadRemote } from '@module-federation/runtime'
import type { CypressInternal, CyPromptDriverDefaultShape } from './prompt-driver-types'
import type Emitter from 'component-emitter'
import $errUtils from '../../../cypress/error_utils'

interface CyPromptDriver { default: CyPromptDriverDefaultShape }

Expand All @@ -15,9 +16,26 @@ declare global {
let initializedModule: CyPromptDriverDefaultShape | null = null
const initializeModule = async (Cypress: Cypress.Cypress): Promise<CyPromptDriverDefaultShape> => {
// Wait for the cy prompt bundle to be downloaded and ready
const { success } = await Cypress.backend('wait:for:cy:prompt:ready')
const { success, error } = await Cypress.backend('wait:for:cy:prompt:ready')

if (error) {
if (error.name === 'ENOSPC') {
$errUtils.throwErrByPath('prompt.promptDownloadError', {
args: {
error,
},
})
} else {
$errUtils.throwErrByPath('prompt.promptDownloadTimedOut', {
args: {
error,
},
})
}
}

if (!success) {
if (!success && !error) {
// TODO: Generic error message
throw new Error('error waiting for cy prompt bundle to be downloaded and ready')
}

Expand All @@ -40,6 +58,7 @@ const initializeModule = async (Cypress: Cypress.Cypress): Promise<CyPromptDrive
const module = await loadRemote<CyPromptDriver>('cy-prompt')

if (!module?.default) {
// TODO: Generic error message
throw new Error('error loading cy prompt driver')
}

Expand Down Expand Up @@ -75,10 +94,14 @@ export default (Commands, Cypress, cy) => {
}

const prompt = async (message: string, options: object = {}) => {
if (Cypress.testingType === 'component') {
$errUtils.throwErrByPath('prompt.promptTestingTypeError')
}

if (!initializeCloudCyPromptPromise) {
// TODO: (cy.prompt) We will look into supporting other browsers (and testing them)
// as this is rolled out
throw new Error('`cy.prompt()` is not supported in this browser.')
$errUtils.throwErrByPath('prompt.promptSupportedBrowser')
}

try {
Expand All @@ -92,6 +115,8 @@ export default (Commands, Cypress, cy) => {

return await cyPrompt(message, options)
} catch (error) {
// TODO: Check error that the user is logged in / record key

// TODO: handle this better
throw new Error(`CyPromptDriver not found: ${error}`)
}
Expand Down
35 changes: 35 additions & 0 deletions packages/driver/src/cypress/error_messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,41 @@ export default {
`,
},

prompt: {
promptDownloadError (obj) {
return {
message: stripIndent`\
Failed to download cy.prompt Cloud code:

- ${obj.error.code}: ${obj.error.message}

Check your network connection and file settings to ensure download is not interrupted.
`,
docsUrl: 'https://on.cypress.io/prompt-download-error',
}
},
promptDownloadTimedOut (obj) {
return {
message: stripIndent`\
Timed out waiting for cy.prompt Cloud code:

- ${obj.error.code}: ${obj.error.message}

Check your network connection and system configuration.
`,
docsUrl: 'https://on.cypress.io/prompt-download-error',
}
},
promptSupportedBrowser: stripIndent`\
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test for this. We should be able to flex the test logic based on the browser we're running. If we're in firefox/webkit, verify the error is the browser error. https://github.com/cypress-io/cypress/blob/feat/cy-prompt/packages/driver/cypress/e2e/commands/prompt/prompt.cy.ts#L3-L8

\`cy.prompt\` is only supported in Chromium-based browsers.

Use Chrome, Electron, Chromium, or Chrome for Testing.
`,
promptTestingTypeError: stripIndent`\
\`cy.prompt\` is currently only supported in end-to-end tests.
`,
},

proxy: {
js_rewriting_failed: stripIndent`\
An error occurred in the Cypress proxy layer while rewriting your source code. This is a bug in Cypress. Open an issue if you see this message.
Expand Down
2 changes: 1 addition & 1 deletion packages/driver/types/internal-types-lite.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ declare namespace Cypress {
(task: 'protocol:test:before:after:run:async', attributes: any, options: any): Promise<void>
(task: 'protocol:url:changed', input: any): Promise<void>
(task: 'protocol:page:loading', input: any): Promise<void>
(task: 'wait:for:cy:prompt:ready'): Promise<{ success: boolean }>
(task: 'wait:for:cy:prompt:ready'): Promise<{ success: boolean, error?: Error }>
}

interface Devices {
Expand Down
11 changes: 7 additions & 4 deletions packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const debug = Debug('cypress:server:cy-prompt-lifecycle-manager')
export class CyPromptLifecycleManager {
private static hashLoadingMap: Map<string, Promise<void>> = new Map()
private static watcher: chokidar.FSWatcher | null = null
private cyPromptManagerPromise?: Promise<CyPromptManager | null>
private cyPromptManagerPromise?: Promise<{
cyPromptManager?: CyPromptManager
error?: Error
}>
private cyPromptManager?: CyPromptManager
private listeners: ((cyPromptManager: CyPromptManager) => void)[] = []

Expand Down Expand Up @@ -72,7 +75,7 @@ export class CyPromptLifecycleManager {
// Clean up any registered listeners
this.listeners = []

return null
return { error }
})

this.cyPromptManagerPromise = cyPromptManagerPromise
Expand All @@ -99,7 +102,7 @@ export class CyPromptLifecycleManager {
}: {
projectId?: string
cloudDataSource: CloudDataSource
}): Promise<CyPromptManager> {
}): Promise<{ cyPromptManager?: CyPromptManager, error?: Error }> {
let cyPromptHash: string
let cyPromptPath: string

Expand Down Expand Up @@ -155,7 +158,7 @@ export class CyPromptLifecycleManager {
this.cyPromptManager = cyPromptManager
this.callRegisteredListeners()

return cyPromptManager
return { cyPromptManager }
}

private callRegisteredListeners () {
Expand Down
7 changes: 5 additions & 2 deletions packages/server/lib/socket-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,13 @@ export class SocketBase {
return options.closeExtraTargets()
case 'wait:for:cy:prompt:ready':
return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then(async (cyPrompt) => {
await options.onCyPromptReady(cyPrompt)
if (cyPrompt.cyPromptManager) {
await options.onCyPromptReady(cyPrompt.cyPromptManager)
}

return {
success: cyPrompt && cyPrompt.status === 'INITIALIZED',
success: cyPrompt.cyPromptManager && cyPrompt.cyPromptManager.status === 'INITIALIZED',
error: cyPrompt.error ? errors.cloneErr(cyPrompt.error) : undefined,
}
})
default:
Expand Down
5 changes: 4 additions & 1 deletion packages/types/src/cy-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export interface CyPromptManagerShape extends CyPromptServerShape {
}

export interface CyPromptLifecycleManagerShape {
getCyPrompt: () => Promise<CyPromptManagerShape | null>
getCyPrompt: () => Promise<{
cyPromptManager?: CyPromptManagerShape
error?: Error
}>
registerCyPromptReadyListener: (listener: (cyPromptManager: CyPromptManagerShape) => void) => void
}