Skip to content

Commit 58e3234

Browse files
internal: (cy.prompt) handle errors better in the command definition (#31835)
* internal: (cy.prompt) handle errors better in the command definition * internal: (cy.prompt) add timeout and handle loading errors more cleanly * add process environment variable * clean up test * update JSDoc
1 parent 832867d commit 58e3234

File tree

8 files changed

+149
-38
lines changed

8 files changed

+149
-38
lines changed

guides/cy-prompt-development.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `cy.prompt` Development
22

3-
In production, the code used to facilitate the prompt command will be retrieved from the Cloud.
3+
In production, the code used to facilitate the prompt command will be retrieved from the Cloud. While `cy.prompt` is still in its early stages it is hidden behind an environment variable: `CYPRESS_ENABLE_CY_PROMPT` but can also be run against local cloud Studio code via the environment variable: `CYPRESS_LOCAL_CY_PROMPT_PATH`.
44

55
To run against locally developed `cy.prompt`:
66

@@ -10,6 +10,15 @@ To run against locally developed `cy.prompt`:
1010
- Set:
1111
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`)
1212
- `CYPRESS_LOCAL_CY_PROMPT_PATH` to the path to the `cypress-services/app/packages/cy-prompt/dist/development` directory
13+
14+
To run against a deployed version of `cy.prompt`:
15+
16+
- Set:
17+
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`)
18+
- `CYPRESS_ENABLE_CY_PROMPT=true`
19+
20+
Regardless of running against local or deployed `cy.prompt`:
21+
1322
- Clone the `cypress` repo
1423
- Run `yarn`
1524
- Run `yarn cypress:open`
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
describe('src/cy/commands/prompt', () => {
2+
it('errors if wait for ready does not return success', (done) => {
3+
const backendStub = cy.stub(Cypress, 'backend').log(false)
4+
5+
backendStub.callThrough()
6+
backendStub.withArgs('wait:for:cy:prompt:ready').resolves({ success: false })
7+
8+
cy.on('fail', (err) => {
9+
expect(err.message).to.include('error waiting for cy prompt bundle to be downloaded and ready')
10+
11+
done()
12+
})
13+
14+
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
15+
16+
cy['commandFns']['prompt'].__reset()
17+
// @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag
18+
cy.prompt('Hello, world!')
19+
})
20+
})

packages/driver/src/cy/commands/prompt/index.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ declare global {
1313
}
1414

1515
let initializedModule: CyPromptDriverDefaultShape | null = null
16-
const initializeModule = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise<CyPromptDriverDefaultShape> => {
16+
const initializeModule = async (Cypress: Cypress.Cypress): Promise<CyPromptDriverDefaultShape> => {
1717
// Wait for the cy prompt bundle to be downloaded and ready
1818
const { success } = await Cypress.backend('wait:for:cy:prompt:ready')
1919

@@ -48,45 +48,64 @@ const initializeModule = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['c
4848
return initializedModule
4949
}
5050

51-
const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']) => {
52-
let cloudModule = initializedModule
51+
const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise<ReturnType<CyPromptDriverDefaultShape['createCyPrompt']> | Error> => {
52+
try {
53+
let cloudModule = initializedModule
5354

54-
if (!cloudModule) {
55-
cloudModule = await initializeModule(Cypress, cy)
56-
}
55+
if (!cloudModule) {
56+
cloudModule = await initializeModule(Cypress)
57+
}
5758

58-
return cloudModule.createCyPrompt({
59-
Cypress: Cypress as CypressInternal,
60-
cy,
61-
eventManager: window.getEventManager ? window.getEventManager() : undefined,
62-
})
59+
return await cloudModule.createCyPrompt({
60+
Cypress: Cypress as CypressInternal,
61+
cy,
62+
eventManager: window.getEventManager ? window.getEventManager() : undefined,
63+
})
64+
} catch (error) {
65+
return error
66+
}
6367
}
6468

6569
export default (Commands, Cypress, cy) => {
6670
if (Cypress.config('experimentalPromptCommand')) {
67-
let initializeCloudCyPromptPromise: Promise<ReturnType<CyPromptDriverDefaultShape['createCyPrompt']>> | undefined
71+
let initializeCloudCyPromptPromise: Promise<ReturnType<CyPromptDriverDefaultShape['createCyPrompt']> | Error> | undefined
6872

6973
if (Cypress.browser.family === 'chromium' || Cypress.browser.name === 'electron') {
7074
initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)
7175
}
7276

73-
Commands.addAll({
74-
async prompt (message: string, options: object = {}) {
75-
if (!initializeCloudCyPromptPromise) {
76-
// TODO: (cy.prompt) We will look into supporting other browsers (and testing them)
77-
// as this is rolled out
78-
throw new Error('`cy.prompt()` is not supported in this browser.')
79-
}
77+
const prompt = async (message: string, options: object = {}) => {
78+
if (!initializeCloudCyPromptPromise) {
79+
// TODO: (cy.prompt) We will look into supporting other browsers (and testing them)
80+
// as this is rolled out
81+
throw new Error('`cy.prompt()` is not supported in this browser.')
82+
}
8083

81-
try {
82-
const cyPrompt = await initializeCloudCyPromptPromise
84+
try {
85+
const bundleResult = await initializeCloudCyPromptPromise
8386

84-
return await cyPrompt(message, options)
85-
} catch (error) {
86-
// TODO: handle this better
87-
throw new Error(`CyPromptDriver not found: ${error}`)
87+
if (bundleResult instanceof Error) {
88+
throw bundleResult
8889
}
89-
},
90+
91+
const cyPrompt = bundleResult
92+
93+
return await cyPrompt(message, options)
94+
} catch (error) {
95+
// TODO: handle this better
96+
throw new Error(`CyPromptDriver not found: ${error}`)
97+
}
98+
}
99+
100+
// For testing purposes, we can reset the prompt command initialization
101+
// by calling the __reset method.
102+
prompt.__reset = () => {
103+
initializedModule = null
104+
initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)
105+
}
106+
107+
Commands.addAll({
108+
prompt,
90109
})
91110
}
92111
}

packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,45 @@ import tar from 'tar'
44
import { getCyPromptBundle } from '../api/cy-prompt/get_cy_prompt_bundle'
55
import path from 'path'
66

7+
const DOWNLOAD_TIMEOUT = 30000
8+
79
interface EnsureCyPromptBundleOptions {
810
cyPromptPath: string
911
cyPromptUrl: string
1012
projectId?: string
13+
downloadTimeoutMs?: number
1114
}
1215

13-
export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId }: EnsureCyPromptBundleOptions) => {
16+
/**
17+
* Ensures that the cy prompt bundle is downloaded and extracted into the given path
18+
* @param options - The options for the ensure cy prompt bundle operation
19+
* @param options.cyPromptPath - The path to extract the cy prompt bundle to
20+
* @param options.cyPromptUrl - The URL of the cy prompt bundle
21+
* @param options.projectId - The project ID of the cy prompt bundle
22+
* @param options.downloadTimeoutMs - The timeout for the cy prompt bundle download
23+
*/
24+
export const ensureCyPromptBundle = async ({ cyPromptPath, cyPromptUrl, projectId, downloadTimeoutMs = DOWNLOAD_TIMEOUT }: EnsureCyPromptBundleOptions) => {
1425
const bundlePath = path.join(cyPromptPath, 'bundle.tar')
1526

1627
// First remove cyPromptPath to ensure we have a clean slate
1728
await remove(cyPromptPath)
1829
await ensureDir(cyPromptPath)
1930

20-
await getCyPromptBundle({
21-
cyPromptUrl,
22-
projectId,
23-
bundlePath,
31+
let timeoutId: NodeJS.Timeout
32+
33+
await Promise.race([
34+
getCyPromptBundle({
35+
cyPromptUrl,
36+
projectId,
37+
bundlePath,
38+
}),
39+
new Promise((_, reject) => {
40+
timeoutId = setTimeout(() => {
41+
reject(new Error('Cy prompt bundle download timed out'))
42+
}, downloadTimeoutMs)
43+
}),
44+
]).finally(() => {
45+
clearTimeout(timeoutId)
2446
})
2547

2648
await tar.extract({

packages/server/lib/project-base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class ProjectBase extends EE {
159159

160160
this._server = new ServerBase(cfg)
161161
// @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag
162-
if (cfg.experimentalPromptCommand) {
162+
if (process.env.CYPRESS_ENABLE_CY_PROMPT || cfg.experimentalPromptCommand) {
163163
const cyPromptLifecycleManager = new CyPromptLifecycleManager()
164164

165165
cyPromptLifecycleManager.initializeCyPromptManager({

packages/server/test/unit/cloud/cy-prompt/ensure_cy_prompt_bundle_spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,23 @@ describe('ensureCyPromptBundle', () => {
5757
cwd: cyPromptPath,
5858
})
5959
})
60+
61+
it('should throw an error if the cy prompt bundle download times out', async () => {
62+
getCyPromptBundleStub.callsFake(() => {
63+
return new Promise((resolve) => {
64+
setTimeout(() => {
65+
resolve(new Error('Cy prompt bundle download timed out'))
66+
}, 3000)
67+
})
68+
})
69+
70+
const ensureCyPromptBundlePromise = ensureCyPromptBundle({
71+
cyPromptPath: '/tmp/cypress/cy-prompt/123',
72+
cyPromptUrl: 'https://cypress.io/cy-prompt',
73+
projectId: '123',
74+
downloadTimeoutMs: 500,
75+
})
76+
77+
await expect(ensureCyPromptBundlePromise).to.be.rejectedWith('Cy prompt bundle download timed out')
78+
})
6079
})

packages/server/test/unit/project_spec.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,15 +450,37 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
450450
})
451451

452452
describe('CyPromptLifecycleManager', function () {
453-
it('initializes cy prompt lifecycle manager', function () {
453+
let initializeCyPromptManagerStub
454+
455+
afterEach(function () {
456+
delete process.env.CYPRESS_ENABLE_CY_PROMPT
457+
initializeCyPromptManagerStub.restore()
458+
})
459+
460+
it('initializes cy prompt lifecycle manager if experimentalPromptCommand is enabled', function () {
454461
this.config.projectId = 'abc123'
455462
this.config.experimentalPromptCommand = true
456463

457-
sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager')
464+
initializeCyPromptManagerStub = sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager')
465+
466+
return this.project.open()
467+
.then(() => {
468+
expect(initializeCyPromptManagerStub).to.be.calledWith({
469+
projectId: 'abc123',
470+
cloudDataSource: ctx.cloud,
471+
ctx,
472+
})
473+
})
474+
})
475+
476+
it('initializes cy prompt lifecycle manager if process.env.CYPRESS_ENABLE_CY_PROMPT is enabled', function () {
477+
process.env.CYPRESS_ENABLE_CY_PROMPT = 'true'
478+
479+
initializeCyPromptManagerStub = sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager')
458480

459481
return this.project.open()
460482
.then(() => {
461-
expect(CyPromptLifecycleManager.prototype.initializeCyPromptManager).to.be.calledWith({
483+
expect(initializeCyPromptManagerStub).to.be.calledWith({
462484
projectId: 'abc123',
463485
cloudDataSource: ctx.cloud,
464486
ctx,
@@ -470,11 +492,11 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
470492
this.config.projectId = 'abc123'
471493
this.config.experimentalPromptCommand = false
472494

473-
sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager')
495+
initializeCyPromptManagerStub = sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager')
474496

475497
return this.project.open()
476498
.then(() => {
477-
expect(CyPromptLifecycleManager.prototype.initializeCyPromptManager).not.to.be.called
499+
expect(initializeCyPromptManagerStub).not.to.be.called
478500
})
479501
})
480502
})

0 commit comments

Comments
 (0)