Skip to content

Commit a621b42

Browse files
Merge branch 'develop' into release/15.0.0
2 parents f58aabd + 53f7aae commit a621b42

File tree

94 files changed

+3404
-1229
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+3404
-1229
lines changed

.circleci/workflows.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: 2.1
22

33
chrome-stable-version: &chrome-stable-version "135.0.7049.114"
4-
chrome-beta-version: &chrome-beta-version "136.0.7103.33"
4+
chrome-beta-version: &chrome-beta-version "136.0.7103.48"
55
firefox-stable-version: &firefox-stable-version "137.0"
66

77
orbs:

cli/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,18 @@ _Released 07/01/2025 (PENDING)_
2121

2222
_Released 5/6/2025 (PENDING)_
2323

24+
**Performance:**
25+
26+
- Ensure the previous pausing event handlers are removed before new ones are added. Addressed in [#31596](https://github.com/cypress-io/cypress/pull/31596).
27+
28+
**Bugfixes:**
29+
30+
- Fixed an issue where the configuration setting `trashAssetsBeforeRuns=false` was ignored for assets in the `videosFolder`. These assets were incorrectly deleted before running tests with `cypress run`. Addresses [#8280](https://github.com/cypress-io/cypress/issues/8280).
31+
- Fixed a potential hang condition when navigating to `about:blank`. Addressed in [#31634](https://github.com/cypress-io/cypress/pull/31634).
32+
2433
**Misc:**
2534

35+
- The Assertions menu when you right click in `experimentalStudio` tests now displays in dark mode. Addresses [#10621](https://github.com/cypress-io/cypress-services/issues/10621). Addressed in [#31598](https://github.com/cypress-io/cypress/pull/31598).
2636
- The URL in the Cypress App no longer displays a white background when the URL is loading. Fixes [#31556](https://github.com/cypress-io/cypress/issues/31556).
2737

2838
## 14.3.2

cli/types/cypress.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3324,7 +3324,6 @@ declare namespace Cypress {
33243324
spec: Cypress['spec'] | null
33253325
specs: Array<Cypress['spec']>
33263326
isDefaultProtocolEnabled: boolean
3327-
isStudioProtocolEnabled: boolean
33283327
hideCommandLog: boolean
33293328
hideRunnerUi: boolean
33303329
}

guides/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI
1919
* [Error handling](./error-handling.md)
2020
* [GraphQL Subscriptions - Overview and Test Guide](./graphql-subscriptions.md)
2121
* [Patching packages](./patch-package.md)
22+
* [Protocol development](./protocol-development.md)
2223
* [Release process](./release-process.md)
24+
* [Studio development](./studio-development.md)
2325
* [Testing other projects](./testing-other-projects.md)
2426
* [Testing strategy and style guide (draft)](./testing-strategy-and-styleguide.md)
2527
* [Writing cross-platform JavaScript](./writing-cross-platform-javascript.md)

guides/studio-development.md

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
# Studio Development
22

3-
In production, the code used to facilitate Studio functionality will be retrieved from the Cloud. However, in order to develop locally, developers will:
3+
In production, the code used to facilitate Studio functionality will be retrieved from the Cloud. While Studio is still in its early stages it is hidden behind an environment variable: `CYPRESS_ENABLE_CLOUD_STUDIO` but can also be run against local cloud Studio code via the environment variable: `CYPRESS_LOCAL_STUDIO_PATH`.
4+
5+
To run against locally developed Studio:
46

57
- Clone the `cypress-services` repo (this requires that you be a member of the Cypress organization)
68
- Run `yarn`
7-
- Run `yarn watch` in `app/studio`
8-
- Set `CYPRESS_LOCAL_STUDIO_PATH` to the path to the `cypress-services/app/studio/dist/development` directory
9+
- Run `yarn watch` in `app/packages/studio`
10+
- Set:
11+
- `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`)
12+
- `CYPRESS_LOCAL_STUDIO_PATH` to the path to the `cypress-services/app/packages/studio/dist/development` directory
13+
14+
To run against a deployed version of studio:
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_CLOUD_STUDIO=true`
19+
20+
Regardless of running against local or deployed studio:
21+
922
- Clone the `cypress` repo
1023
- Run `yarn`
1124
- Run `yarn cypress:open`
25+
- Log In to the Cloud via the App
26+
- Ensure the project has been setup in the `Cypress (staging)` if in staging environment or `Cypress Internal Org` if in production environment and has a `projectId` that represents that. If developing against locally running `cypress-services`, ensure that the project has the feature `studio-ai` enabled for it.
27+
- Open a project that has `experimentalStudio: true` set in the `e2e` config of the `cypress.config.js|ts` file.
28+
- Click to 'Add Commands to Test' after hovering over a test command.
29+
30+
Note: When using the `CYPRESS_LOCAL_STUDIO_PATH` environment variable or when running the Cypress app via the locally cloned repository, we bypass our error reporting and instead log errors to the browser or node console.
1231

1332
## Types
1433

@@ -23,3 +42,59 @@ or to reference a local `cypress_services` repo:
2342
```sh
2443
CYPRESS_LOCAL_STUDIO_PATH=<path-to-cypress-services/app/studio/dist/development-directory> yarn gulp downloadStudioTypes
2544
```
45+
46+
## Testing
47+
48+
### Unit/Component Testing
49+
50+
The code that supports cloud Studio and lives in the `cypress` monorepo is unit and component 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.
51+
52+
The code that supports cloud Studio and lives in the `cypress-services` monorepo has unit and component tests that live alongside the code in that monorepo.
53+
54+
### Cypress in Cypress Testing
55+
56+
Several helpers are provided to facilitate testing cloud Studio using Cypress in Cypress tests. The [helper file](https://github.com/cypress-io/cypress/blob/ad353fcc0f7fdc51b8e624a2a1ef4e76ef9400a0/packages/app/cypress/e2e/studio/helper.ts) provides a method, `launchStudio` that:
57+
58+
1. Loads a project (by default [`experimental-studio`](https://github.com/cypress-io/cypress/tree/develop/system-tests/projects/experimental-studio)).
59+
2. Navigates to the appropriate spec (by default `specName.cy.js`).
60+
3. Enters Studio either by creating a new test or entering from an existing test via the `createNewTest` parameter
61+
4. Waits for the test to finish executing again in Studio mode.
62+
63+
The above steps actually download the studio code from the cloud and use it for the test. Note that `experimental-studio` is set up to be a `canary` project so it will always get the latest and greatest of the cloud Studio code, whether or not it has been fully promoted to production. Note that this means that if you are writing Cypress in Cypress tests that depend on new functionality delivered from the cloud, the Cypress in Cypress tests cannot be merged until the code lands and is built in the cloud. Local development is still possible however by setting `process.env.CYPRESS_LOCAL_STUDIO_PATH` to your local studio path where we enable studio [here](https://github.com/cypress-io/cypress/blob/develop/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts#L424).
64+
65+
In order to properly engage with Studio AI, we choose to simulate the cloud interactions that enable it via something like:
66+
67+
```js
68+
cy.mockNodeCloudRequest({
69+
url: '/studio/testgen/n69px6/enabled',
70+
method: 'get',
71+
body: { enabled: true },
72+
})
73+
```
74+
75+
To ensure that we get the same results from our Studio AI calls every time, we simulate them via something like:
76+
77+
```js
78+
const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')'
79+
cy.mockNodeCloudStreamingRequest({
80+
url: '/studio/testgen/n69px6/generate',
81+
method: 'post',
82+
body: { recommendations: [{ content: aiOutput }] },
83+
})
84+
```
85+
86+
The above two helpers actually mock out the Node requests so we still test the interface between the browser and node with these tests.
87+
88+
Also, since protocol does not work properly on the inner Cypress of Cypress in Cypress tests, we choose to create a dummy protocol which means we need to provide a simulated CDP full snapshot that will be sent to AI via something like:
89+
90+
```js
91+
cy.mockStudioFullSnapshot({
92+
id: 1,
93+
nodeType: 1,
94+
nodeName: 'div',
95+
localName: 'div',
96+
nodeValue: 'div',
97+
children: [],
98+
shadowRoots: [],
99+
})
100+
```

packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,7 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout:
214214
cy.specsPageIsVisible()
215215
cy.contains('withFailure.spec').click()
216216
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
217-
// A bit of a hack, but our cy-in-cy test needs to wait for the reporter to fully render before pressing the "f" key to expand the "Search specs" menu.
218-
// Otherwise, the "f" keypress happens before the event is registered, which causes the "Search Specs" menu to not expand.
217+
219218
cy.get('[data-cy="runnable-header"]').should('be.visible')
220219
cy.get('body').type('f')
221220
cy.contains('Search specs')

packages/app/cypress/e2e/reporter_header.cy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('Reporter Header', () => {
1111
})
1212

1313
it('selects the correct spec in the Specs List', () => {
14+
cy.get('[data-cy="runnable-header"]').should('be.visible')
1415
cy.get('body').type('f')
1516

1617
cy.get('[data-selected-spec="true"]').should('contain', 'dom-content').should('have.length', '1')
@@ -19,6 +20,7 @@ describe('Reporter Header', () => {
1920

2021
// TODO: Reenable as part of https://github.com/cypress-io/cypress/issues/23902
2122
it.skip('filters the list of specs when searching for specs', () => {
23+
cy.get('[data-cy="runnable-header"]').should('be.visible')
2224
cy.get('body').type('f')
2325

2426
cy.findByTestId('specs-list-panel').within(() => {

packages/app/cypress/e2e/runner/event-manager.cy.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { loadSpec } from './support/spec-loader'
1+
import { loadSpec, shouldHaveTestResults } from './support/spec-loader'
22

33
describe('event-manager', () => {
44
it('emits the cypress:created event when spec is rerun', () => {
55
// load the spec initially
66
loadSpec({
77
filePath: 'hooks/basic.cy.js',
8-
passCount: 1,
8+
passCount: 2,
99
})
1010

1111
cy.window().then((win) => {
@@ -26,4 +26,28 @@ describe('event-manager', () => {
2626
cy.wrap(() => eventReceived).invoke('call').should('be.true')
2727
})
2828
})
29+
30+
it('clears the pause listeners when the spec is rerun', () => {
31+
loadSpec({
32+
filePath: 'hooks/basic.cy.js',
33+
passCount: 2,
34+
})
35+
36+
cy.window().then((win) => {
37+
const eventManager = win.getEventManager()
38+
39+
cy.wrap(() => eventManager.reporterBus.listeners('runner:next').length).invoke('call').should('equal', 1)
40+
41+
// trigger a rerun
42+
cy.get('.restart').click()
43+
44+
shouldHaveTestResults({
45+
passCount: 2,
46+
failCount: 0,
47+
pendingCount: 0,
48+
})
49+
50+
cy.wrap(() => eventManager.reporterBus.listeners('runner:next').length).invoke('call').should('equal', 1)
51+
})
52+
})
2953
})

packages/app/cypress/e2e/runner/pluginEvents.cy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('plugin events', () => {
5757
})
5858
})
5959

60+
cy.get('[data-cy="runnable-header"]').should('be.visible')
6061
cy.get('body').type('f')
6162
cy.get('div[title="run_events_spec_2.cy.js"]').click()
6263
cy.waitForSpecToFinish({

packages/app/cypress/e2e/runner/sessions.ui.cy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ describe('runner/cypress sessions.open_mode.spec', () => {
772772
})
773773

774774
it('persists global session and does not persists spec session when selecting a different spec', () => {
775+
cy.get('[data-cy="runnable-header"]').should('be.visible')
775776
cy.get('body').type('f')
776777
cy.get('div[title="blank_session.cy.js"]').click()
777778

packages/app/cypress/e2e/specs_list_component.cy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('App: Spec List (Component)', () => {
2727
it('highlights the currently running spec', () => {
2828
cy.contains('fails').click()
2929
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
30+
cy.get('[data-cy="runnable-header"]').should('be.visible')
3031
cy.get('body').type('f')
3132
cy.get('[data-selected-spec="true"]').should('contain', 'fails')
3233
cy.get('[data-selected-spec="false"]').should('contain', 'foo')

packages/app/cypress/e2e/specs_list_e2e.cy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ describe('App: Spec List (E2E)', () => {
121121

122122
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
123123
cy.findByText('Your tests are loading...').should('not.be.visible')
124+
cy.get('[data-cy="runnable-header"]').should('be.visible')
124125
cy.get('body').type('f')
125126

126127
cy.get('[data-selected-spec="true"]').contains('dom-content.spec.js')
@@ -136,8 +137,6 @@ describe('App: Spec List (E2E)', () => {
136137
cy.findByText('Your tests are loading...').should('not.be.visible')
137138

138139
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
139-
// A bit of a hack, but our cy-in-cy test needs to wait for the reporter to fully render before pressing the "f" key to expand the "Search specs" menu.
140-
// Otherwise, the "f" keypress happens before the event is registered, which causes the "Search Specs" menu to not expand.
141140
cy.get('[data-cy="runnable-header"]').should('be.visible')
142141
// open the inline spec list
143142
cy.get('body').type('f')

packages/app/cypress/e2e/studio/studio.cy.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,52 @@ describe('studio functionality', () => {
5656

5757
cy.window().then((win) => {
5858
expect(win.Cypress.config('isDefaultProtocolEnabled')).to.be.false
59-
expect(win.Cypress.config('isStudioProtocolEnabled')).to.be.true
6059
expect(win.Cypress.state('isProtocolEnabled')).to.be.true
6160
})
6261
})
6362

63+
it('loads the studio UI correctly when studio bundle is taking too long to load', () => {
64+
loadProjectAndRunSpec({ enableCloudStudio: false })
65+
66+
cy.window().then(() => {
67+
cy.withCtx((ctx) => {
68+
// Mock the studioLifecycleManager.getStudio method to return a hanging promise
69+
if (ctx.coreData.studioLifecycleManager) {
70+
const neverResolvingPromise = new Promise<null>(() => {})
71+
72+
ctx.coreData.studioLifecycleManager.getStudio = () => neverResolvingPromise
73+
ctx.coreData.studioLifecycleManager.isStudioReady = () => false
74+
}
75+
})
76+
})
77+
78+
cy.contains('visits a basic html page')
79+
.closest('.runnable-wrapper')
80+
.findByTestId('launch-studio')
81+
.click()
82+
83+
cy.waitForSpecToFinish()
84+
85+
// Verify the cloud studio panel is not present
86+
cy.findByTestId('studio-panel').should('not.exist')
87+
88+
cy.get('[data-cy="loading-studio-panel"]').should('not.exist')
89+
90+
cy.get('[data-cy="hook-name-studio commands"]').should('exist')
91+
92+
cy.getAutIframe().within(() => {
93+
cy.get('#increment').realClick()
94+
})
95+
96+
cy.findByTestId('hook-name-studio commands').closest('.hook-studio').within(() => {
97+
cy.get('.command').should('have.length', 2)
98+
cy.get('.command-name-get').should('contain.text', '#increment')
99+
cy.get('.command-name-click').should('contain.text', 'click')
100+
})
101+
102+
cy.get('button').contains('Save Commands').should('not.be.disabled')
103+
})
104+
64105
it('does not display Studio button when not using cloud studio', () => {
65106
loadProjectAndRunSpec({ })
66107

@@ -126,6 +167,74 @@ describe('studio functionality', () => {
126167

127168
cy.percySnapshot()
128169
})
170+
171+
it('opens a cloud studio session with AI enabled', () => {
172+
cy.mockNodeCloudRequest({
173+
url: '/studio/testgen/n69px6/enabled',
174+
method: 'get',
175+
body: { enabled: true },
176+
})
177+
178+
const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')'
179+
180+
cy.mockNodeCloudStreamingRequest({
181+
url: '/studio/testgen/n69px6/generate',
182+
method: 'post',
183+
body: { recommendations: [{ content: aiOutput }] },
184+
})
185+
186+
cy.mockStudioFullSnapshot({
187+
id: 1,
188+
nodeType: 1,
189+
nodeName: 'div',
190+
localName: 'div',
191+
nodeValue: 'div',
192+
children: [],
193+
shadowRoots: [],
194+
})
195+
196+
const deferred = pDefer()
197+
198+
loadProjectAndRunSpec({ enableCloudStudio: true })
199+
200+
cy.findByTestId('studio-panel').should('not.exist')
201+
202+
cy.intercept('/cypress/e2e/index.html', () => {
203+
// wait for the promise to resolve before responding
204+
// this will ensure the studio panel is loaded before the test finishes
205+
return deferred.promise
206+
}).as('indexHtml')
207+
208+
cy.contains('visits a basic html page')
209+
.closest('.runnable-wrapper')
210+
.findByTestId('launch-studio')
211+
.click()
212+
213+
// regular studio is not loaded until after the test finishes
214+
cy.get('[data-cy="hook-name-studio commands"]').should('not.exist')
215+
// cloud studio is loaded immediately
216+
cy.findByTestId('studio-panel').then(() => {
217+
// check for the loading panel from the app first
218+
cy.get('[data-cy="loading-studio-panel"]').should('be.visible')
219+
// we've verified the studio panel is loaded, now resolve the promise so the test can finish
220+
deferred.resolve()
221+
})
222+
223+
cy.wait('@indexHtml')
224+
225+
// Studio re-executes spec before waiting for commands - wait for the spec to finish executing.
226+
cy.waitForSpecToFinish()
227+
228+
// Verify the studio panel is still open
229+
cy.findByTestId('studio-panel')
230+
cy.get('[data-cy="hook-name-studio commands"]')
231+
232+
// Verify that AI is enabled
233+
cy.get('[data-cy="ai-status-text"]').should('contain.text', 'Enabled')
234+
235+
// Verify that the AI output is correct
236+
cy.get('[data-cy="studio-ai-output-textarea"]').should('contain.text', aiOutput)
237+
})
129238
})
130239

131240
it('updates an existing test with an action', () => {

0 commit comments

Comments
 (0)