Skip to content

Commit 2e4c8e4

Browse files
internal: (cy.prompt) add infrastructure to support a Get Code modal (#31904)
* chore: (cy.prompt) add infrastructure to support a Get Code modal * fix tests * fix code paths * Update eject button styles * handle errors * update types * Update packages/server/lib/socket-base.ts * Fix cy test * update readme --------- Co-authored-by: estrada9166 <[email protected]>
1 parent 9a417af commit 2e4c8e4

File tree

26 files changed

+692
-84
lines changed

26 files changed

+692
-84
lines changed

guides/cy-prompt-development.md

Lines changed: 15 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. 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`.
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 prompt code via the environment variable: `CYPRESS_LOCAL_CY_PROMPT_PATH`.
44

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

@@ -30,6 +30,20 @@ To run against a deployed version of `cy.prompt`:
3030
- Set:
3131
- `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`)
3232

33+
## Types
34+
35+
The prompt bundle provides the types for the `app`, `driver`, and `server` interfaces that are used within the Cypress code. To incorporate the types into the code base, run:
36+
37+
```sh
38+
yarn gulp downloadPromptTypes
39+
```
40+
41+
or to reference a local `cypress_services` repo:
42+
43+
```sh
44+
CYPRESS_LOCAL_CY_PROMPT_PATH=<path-to-cypress-services/app/cy-prompt/dist/development-directory> yarn gulp downloadPromptTypes
45+
```
46+
3347
## Testing
3448

3549
### Unit/Component Testing
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<template>
2+
<Dialog
3+
:open="isOpen"
4+
class="inset-0 z-10 fixed overflow-y-auto"
5+
variant="bare"
6+
:initial-focus="container"
7+
@close="closeModal()"
8+
>
9+
<!-- TODO: we need to validate the styles here-->
10+
<div class="flex min-h-screen items-center justify-center">
11+
<DialogOverlay class="bg-gray-800 opacity-90 fixed sm:inset-0" />
12+
<div ref="container" />
13+
</div>
14+
</Dialog>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { Dialog, DialogOverlay } from '@headlessui/vue'
19+
import { init, loadRemote } from '@module-federation/runtime'
20+
import { ref, onMounted, onBeforeUnmount } from 'vue'
21+
import type { CyPromptAppDefaultShape, GetCodeModalContentsShape } from './prompt-app-types'
22+
import { usePromptStore } from '../store/prompt-store'
23+
24+
interface CyPromptApp { default: CyPromptAppDefaultShape }
25+
26+
// Mirrors the ReactDOM.Root type since incorporating those types
27+
// messes up vue typing elsewhere
28+
interface Root {
29+
render: (element: JSX.Element) => void
30+
unmount: () => void
31+
}
32+
33+
const emit = defineEmits<{
34+
(e: 'close'): void
35+
}>()
36+
37+
withDefaults(defineProps<{
38+
isOpen: boolean
39+
}>(), {
40+
isOpen: false,
41+
})
42+
43+
const closeModal = () => {
44+
emit('close')
45+
}
46+
47+
const container = ref<HTMLDivElement | null>(null)
48+
const error = ref<string | null>(null)
49+
const ReactGetCodeModalContents = ref<GetCodeModalContentsShape | null>(null)
50+
const reactRoot = ref<Root | null>(null)
51+
const promptStore = usePromptStore()
52+
53+
const maybeRenderReactComponent = () => {
54+
if (!ReactGetCodeModalContents.value || !!error.value) {
55+
return
56+
}
57+
58+
const panel = window.UnifiedRunner.React.createElement(ReactGetCodeModalContents.value, {
59+
Cypress,
60+
testId: promptStore.currentGetCodeModalInfo?.testId,
61+
logId: promptStore.currentGetCodeModalInfo?.logId,
62+
onClose: () => {
63+
closeModal()
64+
},
65+
})
66+
67+
if (!reactRoot.value) {
68+
reactRoot.value = window.UnifiedRunner.ReactDOM.createRoot(container.value)
69+
}
70+
71+
reactRoot.value?.render(panel)
72+
}
73+
74+
const unmountReactComponent = () => {
75+
if (!ReactGetCodeModalContents.value || !container.value) {
76+
return
77+
}
78+
79+
reactRoot.value?.unmount()
80+
}
81+
82+
onMounted(maybeRenderReactComponent)
83+
onBeforeUnmount(unmountReactComponent)
84+
85+
init({
86+
remotes: [{
87+
alias: 'cy-prompt',
88+
type: 'module',
89+
name: 'cy-prompt',
90+
entryGlobalName: 'cy-prompt',
91+
entry: '/__cypress-cy-prompt/app/cy-prompt.js',
92+
shareScope: 'default',
93+
}],
94+
name: 'app',
95+
shared: {
96+
react: {
97+
scope: 'default',
98+
version: '18.3.1',
99+
lib: () => window.UnifiedRunner.React,
100+
shareConfig: {
101+
singleton: true,
102+
requiredVersion: '^18.3.1',
103+
},
104+
},
105+
},
106+
})
107+
108+
// We are not using any kind of loading state, because when we get
109+
// to this point, prompt should have already executed, which
110+
// means that the bundle has been downloaded
111+
loadRemote<CyPromptApp>('cy-prompt').then((module) => {
112+
if (!module?.default) {
113+
error.value = 'The panel was not loaded successfully'
114+
115+
return
116+
}
117+
118+
ReactGetCodeModalContents.value = module.default.GetCodeModalContents
119+
maybeRenderReactComponent()
120+
}).catch((e) => {
121+
error.value = e.message
122+
})
123+
124+
</script>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Note: This file is owned by the cloud delivered
2+
// cy prompt bundle. It is downloaded and copied here.
3+
// It should not be modified directly here.
4+
5+
export interface CypressInternal extends Cypress.Cypress {
6+
backendRequestHandler: (
7+
backendRequestNamespace: string,
8+
eventName: string,
9+
...args: any[]
10+
) => Promise<any>
11+
}
12+
13+
export interface GetCodeModalContentsProps {
14+
Cypress: CypressInternal
15+
testId: string
16+
logId: string
17+
onClose: () => void
18+
}
19+
20+
export type GetCodeModalContentsShape = (
21+
props: GetCodeModalContentsProps
22+
) => JSX.Element
23+
24+
export interface CyPromptAppDefaultShape {
25+
// Purposefully do not use React in this signature to avoid conflicts when this type gets
26+
// transferred to the Cypress app
27+
GetCodeModalContents: GetCodeModalContentsShape
28+
}

packages/app/src/runner/SpecRunnerOpenMode.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
<template>
2+
<PromptGetCodeModal
3+
v-if="promptStore.getCodeModalIsOpen"
4+
:open="promptStore.getCodeModalIsOpen"
5+
@close="promptStore.closeGetCodeModal"
6+
/>
27
<StudioInstructionsModal
38
v-if="studioStore.instructionModalIsOpen"
49
:open="studioStore.instructionModalIsOpen"
@@ -146,6 +151,8 @@ import StudioSaveModal from './studio/StudioSaveModal.vue'
146151
import { useStudioStore } from '../store/studio-store'
147152
import StudioPanel from '../studio/StudioPanel.vue'
148153
import { useSubscription } from '../graphql'
154+
import PromptGetCodeModal from '../prompt/PromptGetCodeModal.vue'
155+
import { usePromptStore } from '../store/prompt-store'
149156
150157
const {
151158
preferredMinimumPanelWidth,
@@ -236,7 +243,7 @@ const {
236243
} = useEventManager()
237244
238245
const studioStore = useStudioStore()
239-
246+
const promptStore = usePromptStore()
240247
const handleStudioPanelClose = () => {
241248
eventManager.emit('studio:cancel', undefined)
242249
}

packages/app/src/runner/event-manager.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { addTelemetryListeners } from './events/telemetry'
1717
import { telemetry } from '@packages/telemetry/src/browser'
1818
import { addCaptureProtocolListeners } from './events/capture-protocol'
1919
import { getRunnerConfigFromWindow } from './get-runner-config-from-window'
20+
import { usePromptStore } from '../store/prompt-store'
2021

2122
export type CypressInCypressMochaEvent = Array<Array<string | Record<string, any>>>
2223

@@ -61,6 +62,7 @@ export class EventManager {
6162
ws: SocketShape
6263
specStore: ReturnType<typeof useSpecStore>
6364
studioStore: ReturnType<typeof useStudioStore>
65+
promptStore: ReturnType<typeof usePromptStore>
6466

6567
constructor (
6668
// import '@packages/driver'
@@ -75,6 +77,7 @@ export class EventManager {
7577
this.ws = ws
7678
this.specStore = useSpecStore()
7779
this.studioStore = useStudioStore()
80+
this.promptStore = usePromptStore()
7881
}
7982

8083
getCypress () {
@@ -418,6 +421,8 @@ export class EventManager {
418421
this._clearAllCookies()
419422
this._setUnload()
420423
})
424+
425+
this.addPromptListeners()
421426
}
422427

423428
start (config) {
@@ -467,6 +472,12 @@ export class EventManager {
467472
Cypress.state('isProtocolEnabled', isDefaultProtocolEnabled)
468473
}
469474

475+
if (Cypress.config('experimentalPromptCommand')) {
476+
await new Promise((resolve) => {
477+
this.ws.emit('prompt:reset', resolve)
478+
})
479+
}
480+
470481
this._addListeners()
471482
}
472483

@@ -956,6 +967,10 @@ export class EventManager {
956967
this.localBus.off(event, listener)
957968
}
958969

970+
removeAllListeners (event: string) {
971+
this.localBus.removeAllListeners(event)
972+
}
973+
959974
notifyRunningSpec (specFile) {
960975
this.ws.emit('spec:changed', specFile)
961976
}
@@ -1006,4 +1021,13 @@ export class EventManager {
10061021
_testingOnlySetCypress (cypress: any) {
10071022
Cypress = cypress
10081023
}
1024+
1025+
private addPromptListeners () {
1026+
this.reporterBus.on('prompt:get-code', ({ testId, logId }) => {
1027+
this.promptStore.openGetCodeModal({
1028+
testId,
1029+
logId,
1030+
})
1031+
})
1032+
}
10091033
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { defineStore } from 'pinia'
2+
3+
// TODO: Share this
4+
interface GetCodeModalInfo {
5+
testId: string
6+
logId: string
7+
}
8+
9+
interface PromptState {
10+
getCodeModalIsOpen: boolean
11+
currentGetCodeModalInfo: GetCodeModalInfo | null
12+
}
13+
14+
export const usePromptStore = defineStore('prompt', {
15+
state: (): PromptState => {
16+
return {
17+
getCodeModalIsOpen: false,
18+
currentGetCodeModalInfo: null,
19+
}
20+
},
21+
actions: {
22+
openGetCodeModal (getCodeModalInfo: GetCodeModalInfo) {
23+
this.getCodeModalIsOpen = true
24+
this.currentGetCodeModalInfo = getCodeModalInfo
25+
},
26+
27+
closeGetCodeModal () {
28+
this.getCodeModalIsOpen = false
29+
this.currentGetCodeModalInfo = null
30+
},
31+
},
32+
})

packages/app/src/studio/studio-app-types.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
export type RecordingState = 'recording' | 'paused' | 'disabled'
2+
13
export interface StudioPanelProps {
24
canAccessStudioAI: boolean
35
onStudioPanelClose?: () => void
46
studioSessionId?: string
57
useRunnerStatus?: RunnerStatusShape
68
useTestContentRetriever?: TestContentRetrieverShape
7-
useStudioAIStream?: StudioAIStreamShape
89
useCypress?: CypressShape
910
}
1011

@@ -53,6 +54,7 @@ export interface StudioAIStreamProps {
5354
runnerStatus: RunnerStatus
5455
testCode?: string
5556
isCreatingNewTest: boolean
57+
Cypress: CypressInternal
5658
}
5759

5860
export interface StudioAIStream {
@@ -72,17 +74,3 @@ export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => {
7274
testBlock: TestBlock | null
7375
isCreatingNewTest: boolean
7476
}
75-
76-
export interface Command {
77-
selector?: string
78-
name: string
79-
message?: string | string[]
80-
isAssertion?: boolean
81-
}
82-
83-
export interface SaveDetails {
84-
absoluteFile: string
85-
runnableTitle: string
86-
contents: string
87-
testName?: string
88-
}

packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('src/cy/commands/prompt', () => {
77
error.name = 'ENOSPC'
88

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

1212
cy.on('fail', (err) => {
1313
expect(err.message).to.include('Failed to download cy.prompt Cloud code')
@@ -31,7 +31,7 @@ describe('src/cy/commands/prompt', () => {
3131
error.name = 'ECONNREFUSED'
3232

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

3636
cy.on('fail', (err) => {
3737
expect(err.message).to.include('Timed out waiting for cy.prompt Cloud code:')

0 commit comments

Comments
 (0)