diff --git a/packages/app/cypress/e2e/studio/studio.cy.ts b/packages/app/cypress/e2e/studio/studio.cy.ts index 9317eb382943..aea6da8ce70e 100644 --- a/packages/app/cypress/e2e/studio/studio.cy.ts +++ b/packages/app/cypress/e2e/studio/studio.cy.ts @@ -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' diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx index a651e0d72e63..6eea7eb0b059 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx @@ -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) => { diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 3b5516bda2be..a51070b55f2f 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -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') { diff --git a/packages/app/src/store/studio-store.ts b/packages/app/src/store/studio-store.ts index 281635c44bb8..1b4a6caf7fa6 100644 --- a/packages/app/src/store/studio-store.ts +++ b/packages/app/src/store/studio-store.ts @@ -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) => {