Skip to content

feat: prompt (WIP - do not merge) #31752

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

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dc7a054
Create cy-prompt-development.md
ryanthemanuel May 20, 2025
c26b182
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel May 20, 2025
aa543ae
chore: cy prompt infrastructure (#31748)
ryanthemanuel May 28, 2025
ae8fb6a
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel May 30, 2025
8785623
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel Jun 2, 2025
18b0d9d
fix test
ryanthemanuel Jun 2, 2025
3bc98e0
Delete packages/server/lib/cloud/StudioLifecycleManager.ts
ryanthemanuel Jun 2, 2025
93430dc
Delete packages/server/test/unit/cloud/StudioLifecycleManager_spec.ts
ryanthemanuel Jun 2, 2025
0051c3d
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel Jun 2, 2025
ebba6e4
chore: add cdp connection to cy prompt (#31806)
ryanthemanuel Jun 3, 2025
22737d2
chore: create infrastructure to support backend function in cy.prompt…
ryanthemanuel Jun 3, 2025
e18d31a
chore: add watcher for cy-prompt development (#31810)
ryanthemanuel Jun 3, 2025
832867d
chore: turn on beta deployments for cy-prompt
ryanthemanuel Jun 4, 2025
58e3234
internal: (cy.prompt) handle errors better in the command definition …
ryanthemanuel Jun 6, 2025
b4a663a
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel Jun 9, 2025
30e48d1
chore: handle errors (#31854)
estrada9166 Jun 10, 2025
6c3b69e
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel Jun 13, 2025
04e8212
chores: (cy.prompt) refactor routing to support both app and driver (…
ryanthemanuel Jun 13, 2025
33277c1
chore: Share error utils with the cloud (#31887)
estrada9166 Jun 13, 2025
9a417af
Merge branch 'develop' into feat/cy-prompt
ryanthemanuel Jun 18, 2025
2e4c8e4
internal: (cy.prompt) add infrastructure to support a Get Code modal …
ryanthemanuel Jun 20, 2025
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
6 changes: 5 additions & 1 deletion .circleci/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mainBuildFilters: &mainBuildFilters
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40'
- 'feat/cy-prompt'

# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
Expand All @@ -49,6 +50,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'feat/cy-prompt', << pipeline.git.branch >> ]
- equal:
[
'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40',
Expand All @@ -64,6 +66,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'feat/cy-prompt', << pipeline.git.branch >> ]
- equal:
[
'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40',
Expand Down Expand Up @@ -91,6 +94,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'feat/cy-prompt', << pipeline.git.branch >> ]
- equal:
[
'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40',
Expand Down Expand Up @@ -169,7 +173,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40" && "$CIRCLE_BRANCH" != "feat/cy-prompt" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command
Expand Down
53 changes: 53 additions & 0 deletions guides/cy-prompt-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# `cy.prompt` Development

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`.

To run against locally developed `cy.prompt`:

- Clone the `cypress-services` repo
- Run `yarn`
- Run `yarn watch` in `app/packages/cy-prompt`
- Set:
- `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`)
- `CYPRESS_LOCAL_CY_PROMPT_PATH` to the path to the `cypress-services/app/packages/cy-prompt/dist/development` directory

To run against a deployed version of `cy.prompt`:

- Set:
- `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`)
- `CYPRESS_ENABLE_CY_PROMPT=true`

Regardless of running against local or deployed `cy.prompt`:

- Clone the `cypress` repo
- Run `yarn`
- Run `yarn cypress:open`
- Log In to the Cloud via the App
- Open a project that has `experimentalPromptCommand: true` set in the `e2e` config of the `cypress.config.js|ts` file.

To run against a deployed version of `cy.prompt`:

- Set:
- `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`)

## Types

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:

```sh
yarn gulp downloadPromptTypes
```

or to reference a local `cypress_services` repo:

```sh
CYPRESS_LOCAL_CY_PROMPT_PATH=<path-to-cypress-services/app/cy-prompt/dist/development-directory> yarn gulp downloadPromptTypes
```

## Testing

### Unit/Component Testing

The code that supports cloud `cy.prompt` and lives in the `cypress` monorepo is unit, integration, and e2e tested in a similar fashion to the rest of the code in the repo. See the [contributing guide](https://github.com/cypress-io/cypress/blob/ad353fcc0f7fdc51b8e624a2a1ef4e76ef9400a0/CONTRIBUTING.md?plain=1#L366) for more specifics.

The code that supports cloud `cy.prompt` and lives in the `cypress-services` monorepo has unit tests that live alongside the code in that monorepo.
124 changes: 124 additions & 0 deletions packages/app/src/prompt/PromptGetCodeModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<Dialog
:open="isOpen"
class="inset-0 z-10 fixed overflow-y-auto"
variant="bare"
:initial-focus="container"
@close="closeModal()"
>
<!-- TODO: we need to validate the styles here-->
<div class="flex min-h-screen items-center justify-center">
<DialogOverlay class="bg-gray-800 opacity-90 fixed sm:inset-0" />
<div ref="container" />
</div>
</Dialog>
</template>

<script setup lang="ts">
import { Dialog, DialogOverlay } from '@headlessui/vue'
import { init, loadRemote } from '@module-federation/runtime'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import type { CyPromptAppDefaultShape, GetCodeModalContentsShape } from './prompt-app-types'
import { usePromptStore } from '../store/prompt-store'

interface CyPromptApp { default: CyPromptAppDefaultShape }

// Mirrors the ReactDOM.Root type since incorporating those types
// messes up vue typing elsewhere
interface Root {
render: (element: JSX.Element) => void
unmount: () => void
}

const emit = defineEmits<{
(e: 'close'): void
}>()

withDefaults(defineProps<{
isOpen: boolean
}>(), {
isOpen: false,
})

const closeModal = () => {
emit('close')
}

const container = ref<HTMLDivElement | null>(null)
const error = ref<string | null>(null)
const ReactGetCodeModalContents = ref<GetCodeModalContentsShape | null>(null)
const reactRoot = ref<Root | null>(null)
const promptStore = usePromptStore()

const maybeRenderReactComponent = () => {
if (!ReactGetCodeModalContents.value || !!error.value) {
return
}

const panel = window.UnifiedRunner.React.createElement(ReactGetCodeModalContents.value, {
Cypress,
testId: promptStore.currentGetCodeModalInfo?.testId,
logId: promptStore.currentGetCodeModalInfo?.logId,
onClose: () => {
closeModal()
},
})

if (!reactRoot.value) {
reactRoot.value = window.UnifiedRunner.ReactDOM.createRoot(container.value)
}

reactRoot.value?.render(panel)
}

const unmountReactComponent = () => {
if (!ReactGetCodeModalContents.value || !container.value) {
return
}

reactRoot.value?.unmount()
}

onMounted(maybeRenderReactComponent)
onBeforeUnmount(unmountReactComponent)

init({
remotes: [{
alias: 'cy-prompt',
type: 'module',
name: 'cy-prompt',
entryGlobalName: 'cy-prompt',
entry: '/__cypress-cy-prompt/app/cy-prompt.js',
shareScope: 'default',
}],
name: 'app',
shared: {
react: {
scope: 'default',
version: '18.3.1',
lib: () => window.UnifiedRunner.React,
shareConfig: {
singleton: true,
requiredVersion: '^18.3.1',
},
},
},
})

// We are not using any kind of loading state, because when we get
// to this point, prompt should have already executed, which
// means that the bundle has been downloaded
loadRemote<CyPromptApp>('cy-prompt').then((module) => {
if (!module?.default) {
error.value = 'The panel was not loaded successfully'

return
}

ReactGetCodeModalContents.value = module.default.GetCodeModalContents
maybeRenderReactComponent()
}).catch((e) => {
error.value = e.message
})

</script>
28 changes: 28 additions & 0 deletions packages/app/src/prompt/prompt-app-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Note: This file is owned by the cloud delivered
// cy prompt bundle. It is downloaded and copied here.
// It should not be modified directly here.

export interface CypressInternal extends Cypress.Cypress {
backendRequestHandler: (
backendRequestNamespace: string,
eventName: string,
...args: any[]
) => Promise<any>
}

export interface GetCodeModalContentsProps {
Cypress: CypressInternal
testId: string
logId: string
onClose: () => void
}

export type GetCodeModalContentsShape = (
props: GetCodeModalContentsProps
) => JSX.Element

export interface CyPromptAppDefaultShape {
// Purposefully do not use React in this signature to avoid conflicts when this type gets
// transferred to the Cypress app
GetCodeModalContents: GetCodeModalContentsShape
}
9 changes: 8 additions & 1 deletion packages/app/src/runner/SpecRunnerOpenMode.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<template>
<PromptGetCodeModal
v-if="promptStore.getCodeModalIsOpen"
:open="promptStore.getCodeModalIsOpen"
@close="promptStore.closeGetCodeModal"
/>
<StudioInstructionsModal
v-if="studioStore.instructionModalIsOpen"
:open="studioStore.instructionModalIsOpen"
Expand Down Expand Up @@ -146,6 +151,8 @@ import StudioSaveModal from './studio/StudioSaveModal.vue'
import { useStudioStore } from '../store/studio-store'
import StudioPanel from '../studio/StudioPanel.vue'
import { useSubscription } from '../graphql'
import PromptGetCodeModal from '../prompt/PromptGetCodeModal.vue'
import { usePromptStore } from '../store/prompt-store'

const {
preferredMinimumPanelWidth,
Expand Down Expand Up @@ -236,7 +243,7 @@ const {
} = useEventManager()

const studioStore = useStudioStore()

const promptStore = usePromptStore()
const handleStudioPanelClose = () => {
eventManager.emit('studio:cancel', undefined)
}
Expand Down
40 changes: 25 additions & 15 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { addTelemetryListeners } from './events/telemetry'
import { telemetry } from '@packages/telemetry/src/browser'
import { addCaptureProtocolListeners } from './events/capture-protocol'
import { getRunnerConfigFromWindow } from './get-runner-config-from-window'
import { usePromptStore } from '../store/prompt-store'

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

Expand Down Expand Up @@ -61,6 +62,7 @@ export class EventManager {
ws: SocketShape
specStore: ReturnType<typeof useSpecStore>
studioStore: ReturnType<typeof useStudioStore>
promptStore: ReturnType<typeof usePromptStore>

constructor (
// import '@packages/driver'
Expand All @@ -75,6 +77,7 @@ export class EventManager {
this.ws = ws
this.specStore = useSpecStore()
this.studioStore = useStudioStore()
this.promptStore = usePromptStore()
}

getCypress () {
Expand Down Expand Up @@ -418,6 +421,8 @@ export class EventManager {
this._clearAllCookies()
this._setUnload()
})

this.addPromptListeners()
}

start (config) {
Expand Down Expand Up @@ -467,6 +472,12 @@ export class EventManager {
Cypress.state('isProtocolEnabled', isDefaultProtocolEnabled)
}

if (Cypress.config('experimentalPromptCommand')) {
await new Promise((resolve) => {
this.ws.emit('prompt:reset', resolve)
})
}

this._addListeners()
}

Expand Down Expand Up @@ -800,21 +811,7 @@ export class EventManager {
},
)

/**
* Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges.
* Return it's response.
*/
Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => {
let response

try {
response = await Cypress.backend(...args)
} catch (error) {
response = { error }
}

Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response)
})
Cypress.handlePrimaryOriginSocketEvent(Cypress, 'backend:request')

/**
* Call an automation request for the requesting spec bridge since we cannot have websockets in the spec bridges.
Expand Down Expand Up @@ -970,6 +967,10 @@ export class EventManager {
this.localBus.off(event, listener)
}

removeAllListeners (event: string) {
this.localBus.removeAllListeners(event)
}

notifyRunningSpec (specFile) {
this.ws.emit('spec:changed', specFile)
}
Expand Down Expand Up @@ -1020,4 +1021,13 @@ export class EventManager {
_testingOnlySetCypress (cypress: any) {
Cypress = cypress
}

private addPromptListeners () {
this.reporterBus.on('prompt:get-code', ({ testId, logId }) => {
this.promptStore.openGetCodeModal({
testId,
logId,
})
})
}
}
Loading
Loading