internal: (studio) fix studio and runner states during opening, closing, and refreshing (#32153)

* internal: (studio) fix studio states during opening, closing, and refreshing

* test title

* fix tests

* feedback

* add test coverage

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

* Update packages/app/src/store/studio-store.ts
This commit is contained in:
Adam Stone-Lord
2025-08-11 14:32:04 -06:00
committed by GitHub
parent b2992fe233
commit e59949fb79
4 changed files with 110 additions and 11 deletions

View File

@@ -760,6 +760,27 @@ describe('studio functionality', () => {
cy.location().its('hash').and('not.contain', 'testId=').and('not.contain', 'studio=')
})
it('does not prompt for a URL until studio is active', () => {
launchStudio({ specName: 'spec-w-visit.cy.js', createNewTestFromSuite: true })
cy.location().its('hash').should('contain', 'suiteId=r2').and('contain', 'studio=')
cy.waitForSpecToFinish()
cy.findByTestId('aut-url-input').should('have.value', 'http://localhost:4455/cypress/e2e/index.html')
})
it('does not reload the page if we didnt open a test in studio', () => {
launchStudio({ specName: 'spec-w-visit.cy.js', createNewTestFromSuite: true })
// set a property on the window to see if the page reloads
cy.window().then((w) => w['beforeReload'] = true)
// close new test mode
cy.findByTestId('studio-header-studio-button').click()
// if this property is still set on the window, then the page didn't reload
cy.window().then((w) => expect(w['beforeReload']).to.be.true)
})
it('removes the studio url parameters when closing studio new test', () => {
launchStudio({ specName: 'spec-w-visit.cy.js', createNewTestFromSuite: true })
@@ -770,6 +791,68 @@ describe('studio functionality', () => {
cy.location().its('hash').and('not.contain', 'suiteId=').and('not.contain', 'studio=')
})
it('stays in new test mode when studio panel is opened when the spec is running', () => {
loadProjectAndRunSpec()
cy.waitForSpecToFinish()
cy.findByTestId('studio-button').click()
cy.findByTestId('studio-panel').should('be.visible')
cy.findByTestId('new-test-button').should('be.visible')
// Verify we're initially in new test mode
cy.location().its('hash').should('contain', 'suiteId=r1').and('not.contain', 'testId=')
// Now restart the spec, which will call interceptTest with the running test
// This is where the bug would manifest - it would incorrectly switch from
// "new test" mode to "edit the running test" mode
cy.get('button.restart').click()
cy.get('.test').should('have.length', 1)
cy.get('.test').first().should('have.class', 'runnable-active')
// verify we're still in new test mode
cy.findByTestId('studio-panel').should('be.visible')
cy.findByTestId('new-test-button').should('be.visible')
// these should not exist if we stayed in new test mode
cy.findByTestId('studio-single-test-title').should('not.exist')
cy.findByTestId('record-button-recording').should('not.exist')
// verify URL still shows suite mode, not edit test mode
cy.location().its('hash').should('contain', 'suiteId=r1').and('not.contain', 'testId=')
})
it('shows test body sections correctly when studio panel is open and page is refreshed', () => {
loadProjectAndRunSpec()
cy.waitForSpecToFinish()
cy.findByTestId('studio-button').click()
cy.findByTestId('studio-panel').should('be.visible')
cy.findByTestId('new-test-button').should('be.visible')
cy.reload()
cy.waitForSpecToFinish()
cy.findByTestId('studio-panel').should('be.visible')
cy.findByTestId('new-test-button').should('be.visible')
// verify test body section is visible after refresh
cy.get('.runnable-instruments').should('be.visible')
cy.get('.runnable-commands-region').should('be.visible')
// verify the test body hook is present
cy.get('.hook-item').contains('test body').should('be.visible')
// verify commands are visible within the test body
cy.get('.command-name-visit').should('be.visible')
// Verify URL parameters show suite mode, not test mode
cy.location().its('hash').should('contain', 'suiteId=r1').and('not.contain', 'testId=')
})
describe('prompt for a new url', () => {
const autUrl = 'http://localhost:4455/cypress/e2e/index.html'
const visitUrl = 'cypress/e2e/index.html'

View File

@@ -223,6 +223,7 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
// This emulates the 'needsUrl' state in the studio store
studioStore.setActive(true)
studioStore.setUrl(undefined)
studioStore._hasStarted = true
cy.mountFragment(SpecRunnerHeaderFragmentDoc, {
render: (gqlVal) => {

View File

@@ -307,6 +307,17 @@ export class EventManager {
studioInitSuite({ suiteId })
})
const maybeCleanUpProtocol = () => {
const needsReload = this.studioStore.needsProtocolCleanup()
this.studioStore.cancel()
// only reload the page if Studio has actually been used for recording
if (needsReload) {
window.location.reload()
}
}
this.reporterBus.on('studio:cancel', () => {
this.ws.emit('studio:destroy', ({ error }) => {
if (error) {
@@ -314,9 +325,7 @@ export class EventManager {
console.error(error)
}
this.studioStore.cancel()
// Reloading for now. This is the easiest way to clear out the protocol code from the front end
window.location.reload()
maybeCleanUpProtocol()
})
})
@@ -366,9 +375,7 @@ export class EventManager {
console.error(error)
}
this.studioStore.cancel()
// Reloading for now. This is the easiest way to clear out the protocol code from the front end
window.location.reload()
maybeCleanUpProtocol()
})
})
@@ -857,7 +864,8 @@ export class EventManager {
performance.measure('run', 'run-s', 'run-e')
})
const hasRunnableId = !!this.studioStore.testId || !!this.studioStore.suiteId
const hasActiveStudio = !!this.studioStore.testId ||
!!this.studioStore.newTestLineNumber
const studioSingleTestActive = this.studioStore.newTestLineNumber != null || !!this.studioStore.testId
@@ -869,7 +877,7 @@ export class EventManager {
autoScrollingEnabled: runState.autoScrollingEnabled,
isSpecsListOpen: runState.isSpecsListOpen,
scrollTop: runState.scrollTop,
studioActive: hasRunnableId,
studioActive: hasActiveStudio,
studioSingleTestActive,
} as ReporterStartInfo)
}
@@ -928,7 +936,9 @@ export class EventManager {
}
_interceptStudio (displayProps) {
if (this.studioStore.isActive) {
// Only intercept logs when Studio is actually recording a specific test
// Don't intercept when Studio is just open in "new test" mode
if (this.studioStore.isActive && this.studioStore.testId) {
displayProps.hookId = this.studioStore.hookId
if (displayProps.name === 'visit' && displayProps.state === 'failed') {

View File

@@ -175,6 +175,11 @@ export const useStudioStore = defineStore('studioRecorder', {
this.newTestLineNumber = undefined
},
needsProtocolCleanup () {
// Protocol cleanup (page reload) is only needed if the user has actually entered single test mode in Studio
return this._hasStarted || this.testId || this._isStudioCreatedTest
},
openInstructionModal () {
this.instructionModalIsOpen = true
},
@@ -246,7 +251,7 @@ export const useStudioStore = defineStore('studioRecorder', {
interceptTest (test) {
// if this test is the one we created, we can just set the test id
if ((this.newTestLineNumber && test.invocationDetails?.line === this.newTestLineNumber) || this.suiteId) {
if ((this.newTestLineNumber && test.invocationDetails?.line === this.newTestLineNumber) || (this.suiteId && this._hasStarted)) {
this._isStudioCreatedTest = true
this.setTestId(test.id)
getCypress().runner.setIsStudioCreatedTest(true)
@@ -848,7 +853,7 @@ export const useStudioStore = defineStore('studioRecorder', {
},
needsUrl: (state) => {
return state.isActive && !state.url && !state.isFailed
return state.isActive && !state.url && !state.isFailed && state._hasStarted
},
testError: (state) => {