From 91783f01a788076922847c8da88fbe6d113d73ba Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Thu, 8 May 2025 09:37:39 -0400 Subject: [PATCH 1/3] hide selector playground and studio toolbar when studio beta enabled --- packages/app/cypress/e2e/studio/helper.ts | 28 +++ .../app/cypress/e2e/studio/studio-cloud.cy.ts | 191 +++++++++++++++ packages/app/cypress/e2e/studio/studio.cy.ts | 218 +----------------- .../runner/SpecRunnerHeaderOpenMode.cy.tsx | 65 +++--- .../src/runner/SpecRunnerHeaderOpenMode.vue | 4 +- .../app/src/runner/SpecRunnerOpenMode.vue | 7 +- 6 files changed, 271 insertions(+), 242 deletions(-) create mode 100644 packages/app/cypress/e2e/studio/studio-cloud.cy.ts diff --git a/packages/app/cypress/e2e/studio/helper.ts b/packages/app/cypress/e2e/studio/helper.ts index 1addc507d76f..88acea87c91d 100644 --- a/packages/app/cypress/e2e/studio/helper.ts +++ b/packages/app/cypress/e2e/studio/helper.ts @@ -43,3 +43,31 @@ export function launchStudio ({ specName = 'spec.cy.js', createNewTest = false, cy.get('[data-cy="hook-name-studio commands"]').should('exist') } } + +export function assertClosingPanelWithoutChanges () { + // Cypress re-runs after you cancel Studio. + // Original spec should pass + cy.waitForSpecToFinish({ passCount: 1 }) + + cy.get('.command').should('have.length', 1) + + // Assert the spec was executed without any new commands. + cy.get('.command-name-visit').within(() => { + cy.contains('visit') + cy.contains('cypress/e2e/index.html') + }) + + cy.findByTestId('hook-name-studio commands').should('not.exist') + + cy.withCtx(async (ctx) => { + const spec = await ctx.actions.file.readFileInProject('cypress/e2e/spec.cy.js') + + // No change, since we closed studio + expect(spec.trim().replace(/\r/g, '')).to.eq(` +describe('studio functionality', () => { +it('visits a basic html page', () => { + cy.visit('cypress/e2e/index.html') +}) +})`.trim()) + }) +} diff --git a/packages/app/cypress/e2e/studio/studio-cloud.cy.ts b/packages/app/cypress/e2e/studio/studio-cloud.cy.ts new file mode 100644 index 000000000000..ea3286fb4991 --- /dev/null +++ b/packages/app/cypress/e2e/studio/studio-cloud.cy.ts @@ -0,0 +1,191 @@ +import { launchStudio, loadProjectAndRunSpec, assertClosingPanelWithoutChanges } from './helper' +import pDefer from 'p-defer' + +describe('Studio Cloud', () => { + it('enables protocol for cloud studio', () => { + launchStudio({ enableCloudStudio: true }) + + cy.window().then((win) => { + expect(win.Cypress.config('isDefaultProtocolEnabled')).to.be.false + expect(win.Cypress.state('isProtocolEnabled')).to.be.true + }) + }) + + it('loads the studio UI correctly when studio bundle is taking too long to load', () => { + loadProjectAndRunSpec({ enableCloudStudio: false }) + + cy.window().then(() => { + cy.withCtx((ctx) => { + // Mock the studioLifecycleManager.getStudio method to return a hanging promise + if (ctx.coreData.studioLifecycleManager) { + const neverResolvingPromise = new Promise(() => {}) + + ctx.coreData.studioLifecycleManager.getStudio = () => neverResolvingPromise + ctx.coreData.studioLifecycleManager.isStudioReady = () => false + } + }) + }) + + cy.contains('visits a basic html page') + .closest('.runnable-wrapper') + .findByTestId('launch-studio') + .click() + + cy.waitForSpecToFinish() + + // Verify the cloud studio panel is not present + cy.findByTestId('studio-panel').should('not.exist') + + cy.get('[data-cy="loading-studio-panel"]').should('not.exist') + + cy.get('[data-cy="hook-name-studio commands"]').should('exist') + + cy.getAutIframe().within(() => { + cy.get('#increment').realClick() + }) + + cy.findByTestId('hook-name-studio commands').closest('.hook-studio').within(() => { + cy.get('.command').should('have.length', 2) + cy.get('.command-name-get').should('contain.text', '#increment') + cy.get('.command-name-click').should('contain.text', 'click') + }) + + cy.get('button').contains('Save Commands').should('not.be.disabled') + }) + + it('immediately loads the studio panel', () => { + const deferred = pDefer() + + loadProjectAndRunSpec({ enableCloudStudio: true }) + + cy.findByTestId('studio-panel').should('not.exist') + + cy.intercept('/cypress/e2e/index.html', () => { + // wait for the promise to resolve before responding + // this will ensure the studio panel is loaded before the test finishes + return deferred.promise + }).as('indexHtml') + + cy.contains('visits a basic html page') + .closest('.runnable-wrapper') + .findByTestId('launch-studio') + .click() + + // regular studio is not loaded until after the test finishes + cy.get('[data-cy="hook-name-studio commands"]').should('not.exist') + // cloud studio is loaded immediately + cy.findByTestId('studio-panel').then(() => { + // check for the loading panel from the app first + cy.get('[data-cy="loading-studio-panel"]').should('be.visible') + // we've verified the studio panel is loaded, now resolve the promise so the test can finish + deferred.resolve() + }) + + cy.wait('@indexHtml') + + // Studio re-executes spec before waiting for commands - wait for the spec to finish executing. + cy.waitForSpecToFinish() + + // Verify the studio panel is still open + cy.findByTestId('studio-panel') + cy.get('[data-cy="hook-name-studio commands"]') + }) + + it('hides selector playground and studio controls when studio beta is available', () => { + launchStudio({ enableCloudStudio: true }) + cy.get('[data-cy="studio-header-studio-button"]').click() + + cy.get('[data-cy="playground-activator"]').should('not.exist') + cy.get('[data-cy="studio-toolbar"]').should('not.exist') + }) + + it('closes studio panel when clicking studio button (from the cloud)', () => { + launchStudio({ enableCloudStudio: true }) + + cy.get('[data-cy="studio-header-studio-button"]').click() + + assertClosingPanelWithoutChanges() + }) + + it('opens studio panel to new test when clicking on studio button (from the app) next to url', () => { + cy.viewport(1500, 1000) + loadProjectAndRunSpec({ enableCloudStudio: true }) + // studio button should be visible when using cloud studio + cy.get('[data-cy="studio-button"]').should('be.visible').click() + cy.get('[data-cy="studio-panel"]').should('be.visible') + + cy.contains('New Test') + + cy.get('[data-cy="studio-url-prompt"]').should('not.exist') + + cy.percySnapshot() + }) + + it('opens a cloud studio session with AI enabled', () => { + cy.mockNodeCloudRequest({ + url: '/studio/testgen/n69px6/enabled', + method: 'get', + body: { enabled: true }, + }) + + const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')' + + cy.mockNodeCloudStreamingRequest({ + url: '/studio/testgen/n69px6/generate', + method: 'post', + body: { recommendations: [{ content: aiOutput }] }, + }) + + cy.mockStudioFullSnapshot({ + id: 1, + nodeType: 1, + nodeName: 'div', + localName: 'div', + nodeValue: 'div', + children: [], + shadowRoots: [], + }) + + const deferred = pDefer() + + loadProjectAndRunSpec({ enableCloudStudio: true }) + + cy.findByTestId('studio-panel').should('not.exist') + + cy.intercept('/cypress/e2e/index.html', () => { + // wait for the promise to resolve before responding + // this will ensure the studio panel is loaded before the test finishes + return deferred.promise + }).as('indexHtml') + + cy.contains('visits a basic html page') + .closest('.runnable-wrapper') + .findByTestId('launch-studio') + .click() + + // regular studio is not loaded until after the test finishes + cy.get('[data-cy="hook-name-studio commands"]').should('not.exist') + // cloud studio is loaded immediately + cy.findByTestId('studio-panel').then(() => { + // check for the loading panel from the app first + cy.get('[data-cy="loading-studio-panel"]').should('be.visible') + // we've verified the studio panel is loaded, now resolve the promise so the test can finish + deferred.resolve() + }) + + cy.wait('@indexHtml') + + // Studio re-executes spec before waiting for commands - wait for the spec to finish executing. + cy.waitForSpecToFinish() + + // Verify the studio panel is still open + cy.findByTestId('studio-panel') + cy.get('[data-cy="hook-name-studio commands"]') + + // Verify that AI is enabled + cy.get('[data-cy="ai-status-text"]').should('contain.text', 'Enabled') + + // Verify that the AI output is correct + cy.get('[data-cy="recommendation-editor"]').should('contain', aiOutput) + }) +}) diff --git a/packages/app/cypress/e2e/studio/studio.cy.ts b/packages/app/cypress/e2e/studio/studio.cy.ts index c78d758d7b75..54d89070c085 100644 --- a/packages/app/cypress/e2e/studio/studio.cy.ts +++ b/packages/app/cypress/e2e/studio/studio.cy.ts @@ -1,5 +1,4 @@ -import { launchStudio, loadProjectAndRunSpec } from './helper' -import pDefer from 'p-defer' +import { launchStudio, loadProjectAndRunSpec, assertClosingPanelWithoutChanges } from './helper' describe('Cypress Studio', () => { function incrementCounter (initialCount: number) { @@ -22,219 +21,10 @@ describe('Cypress Studio', () => { }) } - function assertClosingPanelWithoutChanges () { - // Cypress re-runs after you cancel Studio. - // Original spec should pass - cy.waitForSpecToFinish({ passCount: 1 }) - - cy.get('.command').should('have.length', 1) - - // Assert the spec was executed without any new commands. - cy.get('.command-name-visit').within(() => { - cy.contains('visit') - cy.contains('cypress/e2e/index.html') - }) - - cy.findByTestId('hook-name-studio commands').should('not.exist') - - cy.withCtx(async (ctx) => { - const spec = await ctx.actions.file.readFileInProject('cypress/e2e/spec.cy.js') - - // No change, since we closed studio - expect(spec.trim().replace(/\r/g, '')).to.eq(` -describe('studio functionality', () => { - it('visits a basic html page', () => { - cy.visit('cypress/e2e/index.html') - }) -})`.trim()) - }) - } - - context('cloud studio', () => { - it('loads the studio page', () => { - launchStudio({ enableCloudStudio: true }) - - cy.window().then((win) => { - expect(win.Cypress.config('isDefaultProtocolEnabled')).to.be.false - expect(win.Cypress.state('isProtocolEnabled')).to.be.true - }) - }) - - it('loads the studio UI correctly when studio bundle is taking too long to load', () => { - loadProjectAndRunSpec({ enableCloudStudio: false }) - - cy.window().then(() => { - cy.withCtx((ctx) => { - // Mock the studioLifecycleManager.getStudio method to return a hanging promise - if (ctx.coreData.studioLifecycleManager) { - const neverResolvingPromise = new Promise(() => {}) - - ctx.coreData.studioLifecycleManager.getStudio = () => neverResolvingPromise - ctx.coreData.studioLifecycleManager.isStudioReady = () => false - } - }) - }) - - cy.contains('visits a basic html page') - .closest('.runnable-wrapper') - .findByTestId('launch-studio') - .click() - - cy.waitForSpecToFinish() - - // Verify the cloud studio panel is not present - cy.findByTestId('studio-panel').should('not.exist') - - cy.get('[data-cy="loading-studio-panel"]').should('not.exist') - - cy.get('[data-cy="hook-name-studio commands"]').should('exist') - - cy.getAutIframe().within(() => { - cy.get('#increment').realClick() - }) - - cy.findByTestId('hook-name-studio commands').closest('.hook-studio').within(() => { - cy.get('.command').should('have.length', 2) - cy.get('.command-name-get').should('contain.text', '#increment') - cy.get('.command-name-click').should('contain.text', 'click') - }) - - cy.get('button').contains('Save Commands').should('not.be.disabled') - }) - - it('does not display Studio button when not using cloud studio', () => { - loadProjectAndRunSpec({ }) - - cy.get('[data-cy="studio-button"]').should('not.exist') - }) - - it('immediately loads the studio panel', () => { - const deferred = pDefer() - - loadProjectAndRunSpec({ enableCloudStudio: true }) - - cy.findByTestId('studio-panel').should('not.exist') - - cy.intercept('/cypress/e2e/index.html', () => { - // wait for the promise to resolve before responding - // this will ensure the studio panel is loaded before the test finishes - return deferred.promise - }).as('indexHtml') - - cy.contains('visits a basic html page') - .closest('.runnable-wrapper') - .findByTestId('launch-studio') - .click() - - // regular studio is not loaded until after the test finishes - cy.get('[data-cy="hook-name-studio commands"]').should('not.exist') - // cloud studio is loaded immediately - cy.findByTestId('studio-panel').then(() => { - // check for the loading panel from the app first - cy.get('[data-cy="loading-studio-panel"]').should('be.visible') - // we've verified the studio panel is loaded, now resolve the promise so the test can finish - deferred.resolve() - }) - - cy.wait('@indexHtml') - - // Studio re-executes spec before waiting for commands - wait for the spec to finish executing. - cy.waitForSpecToFinish() - - // Verify the studio panel is still open - cy.findByTestId('studio-panel') - cy.get('[data-cy="hook-name-studio commands"]') - }) - - it('closes studio panel when clicking studio button (from the cloud)', () => { - launchStudio({ enableCloudStudio: true }) - - cy.get('[data-cy="studio-header-studio-button"]').click() - - assertClosingPanelWithoutChanges() - }) - - it('opens studio panel to new test when clicking on studio button (from the app) next to url', () => { - cy.viewport(1500, 1000) - loadProjectAndRunSpec({ enableCloudStudio: true }) - // studio button should be visible when using cloud studio - cy.get('[data-cy="studio-button"]').should('be.visible').click() - cy.get('[data-cy="studio-panel"]').should('be.visible') - - cy.contains('New Test') - - cy.get('[data-cy="studio-url-prompt"]').should('not.exist') + it('does not display Studio button when not using cloud studio', () => { + loadProjectAndRunSpec({ }) - cy.percySnapshot() - }) - - it('opens a cloud studio session with AI enabled', () => { - cy.mockNodeCloudRequest({ - url: '/studio/testgen/n69px6/enabled', - method: 'get', - body: { enabled: true }, - }) - - const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')' - - cy.mockNodeCloudStreamingRequest({ - url: '/studio/testgen/n69px6/generate', - method: 'post', - body: { recommendations: [{ content: aiOutput }] }, - }) - - cy.mockStudioFullSnapshot({ - id: 1, - nodeType: 1, - nodeName: 'div', - localName: 'div', - nodeValue: 'div', - children: [], - shadowRoots: [], - }) - - const deferred = pDefer() - - loadProjectAndRunSpec({ enableCloudStudio: true }) - - cy.findByTestId('studio-panel').should('not.exist') - - cy.intercept('/cypress/e2e/index.html', () => { - // wait for the promise to resolve before responding - // this will ensure the studio panel is loaded before the test finishes - return deferred.promise - }).as('indexHtml') - - cy.contains('visits a basic html page') - .closest('.runnable-wrapper') - .findByTestId('launch-studio') - .click() - - // regular studio is not loaded until after the test finishes - cy.get('[data-cy="hook-name-studio commands"]').should('not.exist') - // cloud studio is loaded immediately - cy.findByTestId('studio-panel').then(() => { - // check for the loading panel from the app first - cy.get('[data-cy="loading-studio-panel"]').should('be.visible') - // we've verified the studio panel is loaded, now resolve the promise so the test can finish - deferred.resolve() - }) - - cy.wait('@indexHtml') - - // Studio re-executes spec before waiting for commands - wait for the spec to finish executing. - cy.waitForSpecToFinish() - - // Verify the studio panel is still open - cy.findByTestId('studio-panel') - cy.get('[data-cy="hook-name-studio commands"]') - - // Verify that AI is enabled - cy.get('[data-cy="ai-status-text"]').should('contain.text', 'Enabled') - - // Verify that the AI output is correct - cy.get('[data-cy="recommendation-editor"]').should('contain', aiOutput) - }) + cy.get('[data-cy="studio-button"]').should('not.exist') }) it('updates an existing test with an action', () => { diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx index 959dbc5ea200..3a57142da4a2 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx @@ -5,7 +5,7 @@ import { createEventManager, createTestAutIframe } from '../../cypress/component import { ExternalLink_OpenExternalDocument } from '@packages/frontend-shared/src/generated/graphql' import { cyGeneralGlobeX16 } from '@cypress-design/icon-registry' -function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton = false) { +function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton = false, studioBetaAvailable = false) { const eventManager = createEventManager() const autIframe = createTestAutIframe() @@ -17,6 +17,7 @@ function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton eventManager={eventManager} getAutIframe={() => autIframe} shouldShowStudioButton={shouldShowStudioButton} + studioBetaAvailable={studioBetaAvailable} />) } @@ -37,42 +38,54 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { cy.findByTestId('viewport-size').should('be.visible').contains('500x500') }) - it('disabled selector playground button when isRunning is true', () => { - const autStore = useAutStore() - - autStore.setIsRunning(true) + describe('selector playground button', () => { + it('is enabled by default', () => { + cy.mountFragment(SpecRunnerHeaderFragmentDoc, { + render: (gqlVal) => { + return renderWithGql(gqlVal) + }, + }) - cy.mountFragment(SpecRunnerHeaderFragmentDoc, { - render: (gqlVal) => { - return renderWithGql(gqlVal) - }, + cy.get('[data-cy="playground-activator"]').should('not.be.disabled') }) - cy.get('[data-cy="playground-activator"]').should('be.disabled') - }) + it('is disabled when isRunning is true', () => { + const autStore = useAutStore() - it('disabled selector playground button when isLoading is true', () => { - const autStore = useAutStore() + autStore.setIsRunning(true) - autStore.setIsLoading(true) + cy.mountFragment(SpecRunnerHeaderFragmentDoc, { + render: (gqlVal) => { + return renderWithGql(gqlVal) + }, + }) - cy.mountFragment(SpecRunnerHeaderFragmentDoc, { - render: (gqlVal) => { - return renderWithGql(gqlVal) - }, + cy.get('[data-cy="playground-activator"]').should('be.disabled') }) - cy.get('[data-cy="playground-activator"]').should('be.disabled') - }) + it('is disabled when isLoading is true', () => { + const autStore = useAutStore() - it('enables selector playground button by default', () => { - cy.mountFragment(SpecRunnerHeaderFragmentDoc, { - render: (gqlVal) => { - return renderWithGql(gqlVal) - }, + autStore.setIsLoading(true) + + cy.mountFragment(SpecRunnerHeaderFragmentDoc, { + render: (gqlVal) => { + return renderWithGql(gqlVal) + }, + }) + + cy.get('[data-cy="playground-activator"]').should('be.disabled') }) - cy.get('[data-cy="playground-activator"]').should('not.be.disabled') + it('is hidden when studio beta is available', () => { + cy.mountFragment(SpecRunnerHeaderFragmentDoc, { + render: (gqlVal) => { + return renderWithGql(gqlVal, true, true) + }, + }) + + cy.get('[data-cy="playground-activator"]').should('not.exist') + }) }) it('shows url section if currentTestingType is e2e', () => { diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue index 0fce79fd65d5..861e103560b5 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue @@ -6,6 +6,7 @@ >