Skip to content

Commit f20d548

Browse files
ryanthemanuelestrada9166Copilot
authored
feat: add cy prompt more info needed modal (WIP) (#31970)
* feat: add cy prompt more info needed modal * Reset promptStore * additional things exposed for more info * rework * fix tests * fix build * fix types * fix types * Update packages/app/src/runner/event-manager.ts Co-authored-by: Copilot <[email protected]> * reefactor * chore: (cy.prompt) rework the file save lifecycle * rework types * add unit tests --------- Co-authored-by: estrada9166 <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 468d9a4 commit f20d548

File tree

17 files changed

+676
-191
lines changed

17 files changed

+676
-191
lines changed

packages/app/src/prompt/PromptGetCodeModal.vue

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

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// 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.
2+
// cy-prompt bundle. It is downloaded and copied to the app.
3+
// It should not be modified directly in the app.
4+
5+
import type Emitter from 'component-emitter'
46

57
export interface CypressInternal extends Cypress.Cypress {
68
backendRequestHandler: (
79
backendRequestNamespace: string,
810
eventName: string,
911
...args: any[]
1012
) => Promise<any>
13+
preserveRunState: (testId: string) => Promise<void>
1114
}
1215

1316
export interface GetCodeModalContentsProps {
@@ -21,8 +24,27 @@ export type GetCodeModalContentsShape = (
2124
props: GetCodeModalContentsProps
2225
) => JSX.Element
2326

27+
export interface CyPromptEventManager {
28+
ws: Emitter
29+
localBus: Emitter
30+
rerunSpec: () => void
31+
}
32+
33+
export interface MoreInfoNeededModalContentsProps {
34+
Cypress: CypressInternal
35+
eventManager: CyPromptEventManager
36+
testId: string
37+
logId: string
38+
onClose: () => void
39+
}
40+
41+
export type MoreInfoNeededModalContentsShape = (
42+
props: MoreInfoNeededModalContentsProps
43+
) => JSX.Element
44+
2445
export interface CyPromptAppDefaultShape {
2546
// Purposefully do not use React in this signature to avoid conflicts when this type gets
2647
// transferred to the Cypress app
2748
GetCodeModalContents: GetCodeModalContentsShape
49+
MoreInfoNeededModalContents: MoreInfoNeededModalContentsShape
2850
}

packages/app/src/runner/SpecRunnerOpenMode.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
:open="promptStore.getCodeModalIsOpen"
55
@close="promptStore.closeGetCodeModal"
66
/>
7+
<PromptMoreInfoNeededModal
8+
v-if="promptStore.moreInfoNeededModalIsOpen"
9+
:open="promptStore.moreInfoNeededModalIsOpen"
10+
@close="promptStore.closeMoreInfoNeededModal"
11+
/>
712
<StudioInstructionsModal
813
v-if="studioStore.instructionModalIsOpen"
914
:open="studioStore.instructionModalIsOpen"
@@ -152,6 +157,7 @@ import { useStudioStore } from '../store/studio-store'
152157
import StudioPanel from '../studio/StudioPanel.vue'
153158
import { useSubscription } from '../graphql'
154159
import PromptGetCodeModal from '../prompt/PromptGetCodeModal.vue'
160+
import PromptMoreInfoNeededModal from '../prompt/PromptMoreInfoNeededModal.vue'
155161
import { usePromptStore } from '../store/prompt-store'
156162
157163
const {

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,13 @@ export class EventManager {
472472
Cypress.state('isProtocolEnabled', isDefaultProtocolEnabled)
473473
}
474474

475+
this._addListeners()
476+
475477
if (Cypress.config('experimentalPromptCommand')) {
476478
await new Promise((resolve) => {
477479
this.ws.emit('prompt:reset', resolve)
478480
})
479481
}
480-
481-
this._addListeners()
482482
}
483483

484484
isBrowserFamily (family: string) {
@@ -919,6 +919,8 @@ export class EventManager {
919919
return
920920
}
921921

922+
this.promptStore.resetState()
923+
922924
await this.resetReporter()
923925

924926
// this probably isn't 100% necessary since Cypress will fall out of scope
@@ -1029,5 +1031,15 @@ export class EventManager {
10291031
logId,
10301032
})
10311033
})
1034+
1035+
this.localBus.removeAllListeners('prompt:more-info-needed')
1036+
this.localBus.on('prompt:more-info-needed', ({ testId, logId, onSave, onCancel }) => {
1037+
this.promptStore.openMoreInfoNeededModal({
1038+
testId,
1039+
logId,
1040+
onSave,
1041+
onCancel,
1042+
})
1043+
})
10321044
}
10331045
}

packages/app/src/store/prompt-store.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,27 @@ interface GetCodeModalInfo {
66
logId: string
77
}
88

9+
interface MoreInfoNeededModalInfo {
10+
testId: string
11+
logId: string
12+
onSave: () => void
13+
onCancel: () => void
14+
}
15+
916
interface PromptState {
1017
getCodeModalIsOpen: boolean
18+
moreInfoNeededModalIsOpen: boolean
1119
currentGetCodeModalInfo: GetCodeModalInfo | null
20+
currentMoreInfoNeededModalInfo: MoreInfoNeededModalInfo | null
1221
}
1322

1423
export const usePromptStore = defineStore('prompt', {
1524
state: (): PromptState => {
1625
return {
1726
getCodeModalIsOpen: false,
27+
moreInfoNeededModalIsOpen: false,
1828
currentGetCodeModalInfo: null,
29+
currentMoreInfoNeededModalInfo: null,
1930
}
2031
},
2132
actions: {
@@ -28,5 +39,22 @@ export const usePromptStore = defineStore('prompt', {
2839
this.getCodeModalIsOpen = false
2940
this.currentGetCodeModalInfo = null
3041
},
42+
43+
openMoreInfoNeededModal (moreInfoNeededModalInfo: MoreInfoNeededModalInfo) {
44+
this.moreInfoNeededModalIsOpen = true
45+
this.currentMoreInfoNeededModalInfo = moreInfoNeededModalInfo
46+
},
47+
48+
closeMoreInfoNeededModal () {
49+
this.moreInfoNeededModalIsOpen = false
50+
this.currentMoreInfoNeededModalInfo = null
51+
},
52+
53+
resetState () {
54+
this.getCodeModalIsOpen = false
55+
this.moreInfoNeededModalIsOpen = false
56+
this.currentGetCodeModalInfo = null
57+
this.currentMoreInfoNeededModalInfo = null
58+
},
3159
},
3260
})

packages/driver/src/cy/commands/navigation.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import Promise from 'bluebird'
44

55
import $utils from '../../cypress/utils'
66
import $errUtils from '../../cypress/error_utils'
7-
import { LogUtils, Log } from '../../cypress/log'
7+
import type { Log } from '../../cypress/log'
88
import { bothUrlsMatchAndOneHasHash } from '../navigation'
99
import { $Location, LocationObject } from '../../cypress/location'
1010
import { isRunnerAbleToCommunicateWithAut } from '../../util/commandAUTCommunication'
1111
import { whatIsCircular } from '../../util/what-is-circular'
1212

13-
import type { RunState } from '@packages/types'
14-
1513
import debugFn from 'debug'
1614
const debug = debugFn('cypress:driver:navigation')
1715

@@ -1069,30 +1067,7 @@ export default (Commands, Cypress, cy, state, config) => {
10691067
})
10701068
}
10711069

1072-
// tell our backend we're changing origins
1073-
// TODO: add in other things we want to preserve
1074-
// state for like scrollTop
1075-
let runState: RunState = {
1076-
currentId: id,
1077-
tests: Cypress.runner.getTestsState(id),
1078-
startTime: Cypress.runner.getStartTime(),
1079-
emissions: Cypress.runner.getEmissions(),
1080-
}
1081-
1082-
runState.passed = Cypress.runner.countByTestState(runState.tests, 'passed')
1083-
runState.failed = Cypress.runner.countByTestState(runState.tests, 'failed')
1084-
runState.pending = Cypress.runner.countByTestState(runState.tests, 'pending')
1085-
runState.numLogs = LogUtils.countLogsByTests(runState.tests)
1086-
1087-
return Cypress.action('cy:collect:run:state')
1088-
.then((otherRunStates = []) => {
1089-
// merge all the states together holla'
1090-
runState = _.reduce(otherRunStates, (memo, obj) => {
1091-
return _.extend(memo, obj)
1092-
}, runState)
1093-
1094-
return Cypress.backend('preserve:run:state', runState)
1095-
})
1070+
return Cypress.preserveRunState(id)
10961071
.then(() => {
10971072
// and now we must change the url to be the new
10981073
// origin but include the test that we're currently on

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { init, loadRemote } from '@module-federation/runtime'
2-
import type { CypressInternal, CyPromptDriverDefaultShape } from './prompt-driver-types'
2+
import type { CypressInternal, CyPromptDriverDefaultShape, CyPromptMoreInfoNeededOptions } from './prompt-driver-types'
33
import type Emitter from 'component-emitter'
44
import $errUtils from '../../../cypress/error_utils'
55
import $stackUtils from '../../../cypress/stack_utils'
@@ -10,6 +10,7 @@ declare global {
1010
interface Window {
1111
getEventManager?: () => {
1212
ws: Emitter
13+
localBus: Emitter
1314
}
1415
}
1516
}
@@ -76,6 +77,13 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp
7677
cloudModule = await initializeModule(Cypress)
7778
}
7879

80+
if (!Cypress.isCrossOriginSpecBridge) {
81+
Cypress.primaryOriginCommunicator.removeAllListeners('prompt:more-info-needed')
82+
Cypress.primaryOriginCommunicator.on('prompt:more-info-needed', ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
83+
window.getEventManager!().ws.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
84+
})
85+
}
86+
7987
return await cloudModule.createCyPrompt({
8088
Cypress: Cypress as CypressInternal,
8189
cy,
@@ -85,6 +93,13 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp
8593
throwErrByPath: $errUtils.throwErrByPath,
8694
},
8795
getSourceDetailsForFirstLine: $stackUtils.getSourceDetailsForFirstLine,
96+
onMoreInfoNeeded: ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
97+
if (Cypress.isCrossOriginSpecBridge) {
98+
Cypress.specBridgeCommunicator.toPrimary('prompt:more-info-needed', { testId, logId, onSave, onCancel })
99+
} else {
100+
window.getEventManager!().localBus.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
101+
}
102+
},
88103
})
89104
} catch (error) {
90105
return error
@@ -100,7 +115,7 @@ export default (Commands: Cypress.Cypress['Commands'], Cypress: Cypress.Cypress,
100115
initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)
101116
}
102117

103-
const prompt = (steps: string | string[], commandOptions: object = {}) => {
118+
const prompt = (steps: string[], commandOptions: object = {}) => {
104119
const promptCmd = cy.state('current')
105120

106121
if (Cypress.testingType === 'component') {

0 commit comments

Comments
 (0)