diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index b204fe72b3..ea82646f2f 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -15,6 +15,8 @@ _Released 10/07/2025 (PENDING)_ **Misc:** +- Added a dropdown menu in the Command Log that includes actions like Open in IDE and Add New Test in Studio, along with test preferences such as Auto-Scroll. Addresses [#32556](https://github.com/cypress-io/cypress/issues/32556) and [#32558](https://github.com/cypress-io/cypress/issues/32558). Addressed in [#32611](https://github.com/cypress-io/cypress/pull/32611). +- Updated the Studio test editing header to include a Back button. This change ensures the Specs button remains functional for expanding or collapsing the specs panel. Addresses [#32556](https://github.com/cypress-io/cypress/issues/32556) and [#32558](https://github.com/cypress-io/cypress/issues/32558). Addressed in [#32611](https://github.com/cypress-io/cypress/pull/32611). - Fixed the Studio panel resizing when dragging. Addressed in [#32584](https://github.com/cypress-io/cypress/pull/32584). ## 15.3.0 diff --git a/packages/app/cypress/e2e/reporter_header.cy.ts b/packages/app/cypress/e2e/reporter_header.cy.ts index 5c473596e6..b8219334d4 100644 --- a/packages/app/cypress/e2e/reporter_header.cy.ts +++ b/packages/app/cypress/e2e/reporter_header.cy.ts @@ -40,7 +40,7 @@ describe('Reporter Header', () => { }) }) - context('Testing Preferences', () => { + context('More actions button', () => { const switchSelector = '[data-cy=auto-scroll-switch]' context('preferences menu', () => { @@ -54,19 +54,23 @@ describe('Reporter Header', () => { cy.waitForSpecToFinish() }) - it('clicking the down arrow will open a panel showing Testing Preferences', () => { - cy.get('[data-cy=testing-preferences-toggle]').trigger('mouseover') - cy.get('.cy-tooltip').should('have.text', 'Open Testing Preferences') + it('clicking down more options will open a popover with more options', () => { + cy.get('[data-cy="runnable-options-button"]').trigger('mouseover') + cy.get('.cy-tooltip').should('have.text', 'Options') - cy.get('.testing-preferences').should('not.exist') - cy.get('[data-cy=testing-preferences-toggle]').click() - cy.get('.testing-preferences').should('be.visible') - cy.get('[data-cy=testing-preferences-toggle]').click() - cy.get('.testing-preferences').should('not.exist') + cy.get('[data-cy="more-options-runnable-popover"]').should('not.exist') + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('not.exist') }) - it('will show a toggle beside the auto-scrolling option', () => { - cy.get('[data-cy=testing-preferences-toggle]').click() + it('will show multiples actions in the popover', () => { + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="more-options-runnable-popover"]').should('contain', 'Open in IDE') + cy.get('[data-cy="more-options-runnable-popover"]').should('contain', 'New test') + cy.get('[data-cy="more-options-runnable-popover"]').should('contain', 'Auto-scrolling') cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'true') cy.get(switchSelector).click() cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'false') @@ -95,7 +99,9 @@ describe('Reporter Header', () => { }) }) - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'true') }) }) diff --git a/packages/app/cypress/e2e/runner/runner.ui.cy.ts b/packages/app/cypress/e2e/runner/runner.ui.cy.ts index 88c3cedb96..b2490b7c51 100644 --- a/packages/app/cypress/e2e/runner/runner.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/runner.ui.cy.ts @@ -171,9 +171,9 @@ describe('src/cypress/runner', () => { o.sinon.stub(ctx.actions.file, 'openFile') }) - cy.get('.open-in-ide-button').should('have.css', 'opacity', '0') - cy.get('.spec-file-name').realHover() - cy.get('.open-in-ide-button').first().should('have.css', 'opacity', '1').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="runnable-popover-open-ide"]').click() cy.withCtx((ctx, o) => { expect(ctx.actions.file.openFile).to.have.been.calledWith(o.sinon.match(new RegExp(`simple-cy-assert\.runner\.cy\.js$`)), 1, 1) diff --git a/packages/app/cypress/e2e/studio/helper.ts b/packages/app/cypress/e2e/studio/helper.ts index 4c21826aa9..86b603f30a 100644 --- a/packages/app/cypress/e2e/studio/helper.ts +++ b/packages/app/cypress/e2e/studio/helper.ts @@ -30,7 +30,9 @@ export function launchStudio ({ specName = 'spec.cy.js', createNewTestFromSuite if (createNewTestFromSuite || createNewTestFromSpecHeader) { if (createNewTestFromSpecHeader) { - cy.findByTestId('create-new-test-from-spec-header').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="runnable-popover-new-test"]').click() } else { cy.get('@runnable-wrapper').realHover().findByTestId('create-new-test-from-suite').click() } diff --git a/packages/app/cypress/e2e/studio/studio.cy.ts b/packages/app/cypress/e2e/studio/studio.cy.ts index 6b2a1ef227..5874f7316c 100644 --- a/packages/app/cypress/e2e/studio/studio.cy.ts +++ b/packages/app/cypress/e2e/studio/studio.cy.ts @@ -623,10 +623,13 @@ describe('studio functionality', () => { o.sinon.stub(ctx.actions.file, 'openFile') }) - cy.get('.open-in-ide-button').should('have.css', 'opacity', '0') - cy.get('.spec-file-name').first().realHover() - cy.get('.open-in-ide-button').first().should('have.css', 'opacity', '1').click() - cy.get('.open-in-ide-button').first().contains('Open in IDE') + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + + cy.get('[data-cy="runnable-popover-open-ide"]').contains('Open in IDE') + cy.get('[data-cy="runnable-popover-open-ide"]').click() + + cy.contains('External editor preferences') cy.percySnapshot() }) diff --git a/packages/reporter/cypress/e2e/header.cy.ts b/packages/reporter/cypress/e2e/header.cy.ts index 36c50ff21d..fa1ecaa9d4 100755 --- a/packages/reporter/cypress/e2e/header.cy.ts +++ b/packages/reporter/cypress/e2e/header.cy.ts @@ -103,24 +103,28 @@ describe('header', () => { runner.emit('run:start') }) - describe('preferences menu', () => { - it('can be toggled', () => { - cy.get('.testing-preferences').should('not.exist') - cy.get('[data-cy=testing-preferences-toggle]').click() - cy.get('.testing-preferences').should('be.visible') - cy.get('[data-cy=testing-preferences-toggle]').click() - cy.get('.testing-preferences').should('not.exist') + describe('more options menu', () => { + it('can be opened', () => { + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('not.exist') + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') }) it('has tooltip', () => { - cy.get('[data-cy=testing-preferences-toggle]').trigger('mouseover') - cy.get('.cy-tooltip').should('have.text', 'Open Testing Preferences') + cy.get('[data-cy="runnable-options-button"]').trigger('mouseover') + cy.get('.cy-tooltip').should('have.text', 'Options') }) it('shows when auto-scrolling is enabled and can disable it', () => { const switchSelector = '[data-cy=auto-scroll-switch]' - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'true') cy.get(switchSelector).click() cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'false') @@ -129,7 +133,9 @@ describe('header', () => { it('can be toggled with shortcut', () => { const switchSelector = '[data-cy=auto-scroll-switch]' - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'true') cy.get('body').type('a').then(() => { cy.get(switchSelector).invoke('attr', 'aria-checked').should('eq', 'false') @@ -138,11 +144,34 @@ describe('header', () => { it('the auto-scroll toggle emits save:state event when clicked', () => { cy.spy(runner, 'emit') - cy.get('[data-cy=testing-preferences-toggle]').click() + + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy=auto-scroll-switch]').click() cy.wrap(runner.emit).should('be.calledWith', 'save:state') cy.percySnapshot() }) + + it('opens the open in IDE button', () => { + cy.spy(runner, 'emit') + + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + + cy.get('[data-cy="runnable-popover-open-ide"]').click() + cy.wrap(runner.emit).should('be.calledWith', 'open:file:unified') + }) + + it('opens the new test button', () => { + cy.spy(runner, 'emit') + + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + + cy.get('[data-cy="runnable-popover-new-test"]').click() + cy.wrap(runner.emit).should('be.calledWith', 'studio:init:suite') + }) }) describe('stop button', () => { diff --git a/packages/reporter/cypress/e2e/runnables.cy.ts b/packages/reporter/cypress/e2e/runnables.cy.ts index 54393ef6eb..a0e42e373a 100644 --- a/packages/reporter/cypress/e2e/runnables.cy.ts +++ b/packages/reporter/cypress/e2e/runnables.cy.ts @@ -132,7 +132,7 @@ describe('runnables', () => { runner.emit('reporter:start', { startTime: startTime.toISOString() }) }) - cy.get('.runnable-header span:last').should('have.text', '00:12') + cy.get('.runnable-header .duration').should('have.text', '00:12') }) it('does not display time if no time taken', () => { @@ -208,8 +208,9 @@ describe('runnables', () => { cy.stub(runner, 'emit').callThrough() - cy.get(selector).as('spec-title').contains('foo.js').realHover() - cy.get('.open-in-ide-button').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="runnable-popover-open-ide"]').click() cy.get(selector).click().then(() => { expect(runner.emit).to.be.calledWith('open:file:unified') }) diff --git a/packages/reporter/cypress/e2e/shortcuts.cy.ts b/packages/reporter/cypress/e2e/shortcuts.cy.ts index 44ba907fbb..a8796247d4 100755 --- a/packages/reporter/cypress/e2e/shortcuts.cy.ts +++ b/packages/reporter/cypress/e2e/shortcuts.cy.ts @@ -118,11 +118,15 @@ describe('shortcuts', function () { it('toggles auto-scrolling', () => { cy.get('body').type('a') - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') cy.get('[data-cy=auto-scroll-switch]').invoke('attr', 'aria-checked').should('eq', 'false') - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy=runnable-options-button]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('not.exist') + cy.get('body').type('a') - cy.get('[data-cy=testing-preferences-toggle]').click() + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') cy.get('[data-cy=auto-scroll-switch]').invoke('attr', 'aria-checked').should('eq', 'true') }) diff --git a/packages/reporter/cypress/e2e/spec_title.cy.ts b/packages/reporter/cypress/e2e/spec_title.cy.ts index ff82fe8263..83aaf14575 100644 --- a/packages/reporter/cypress/e2e/spec_title.cy.ts +++ b/packages/reporter/cypress/e2e/spec_title.cy.ts @@ -60,19 +60,18 @@ describe('spec title', () => { cy.percySnapshot() }) - it('displays Open in IDE button on spec name hover', () => { - cy.get('.open-in-ide-button').should('have.css', 'opacity', '0') - - cy.get('.spec-file-name').realHover() - cy.get('.open-in-ide-button').should('have.css', 'opacity', '1') - cy.get('.open-in-ide-button').contains('Open in IDE') + it('displays Open in IDE button on more actions button', () => { + cy.get('[data-cy="runnable-options-button"]').click() + cy.get('[data-cy="more-options-runnable-popover"]').should('be.visible') + cy.get('[data-cy="runnable-popover-open-ide"]').contains('Open in IDE') cy.percySnapshot() }) itHandlesFileOpening({ getRunner: () => runner, - selector: '.open-in-ide-button', + previousClickSelector: '[data-cy="runnable-options-button"]', + selector: '[data-cy="runnable-popover-open-ide"]', file: { file: '/absolute/path/to/foo.js', line: 0, diff --git a/packages/reporter/cypress/e2e/unit/app_state.cy.ts b/packages/reporter/cypress/e2e/unit/app_state.cy.ts index 1171ecc8a6..4c43bdd049 100644 --- a/packages/reporter/cypress/e2e/unit/app_state.cy.ts +++ b/packages/reporter/cypress/e2e/unit/app_state.cy.ts @@ -141,25 +141,6 @@ describe('app state', () => { }) }) - context('#togglePreferencesMenu', () => { - it('toggles isPreferencesMenuOpen', () => { - const instance = new AppState() - - instance.togglePreferencesMenu() - expect(instance.isPreferencesMenuOpen).to.be.true - instance.togglePreferencesMenu() - expect(instance.isPreferencesMenuOpen).to.be.false - }) - - it('sets reset value for autoScrollingEnabled', () => { - const instance = new AppState() - - instance.togglePreferencesMenu() - instance.reset() - expect(instance.autoScrollingEnabled).to.be.true - }) - }) - context('#setStudioActive', () => { it('sets studioActive', () => { const instance = new AppState() diff --git a/packages/reporter/cypress/support/utils.ts b/packages/reporter/cypress/support/utils.ts index f4bbd0e9f9..0f2f19e70b 100644 --- a/packages/reporter/cypress/support/utils.ts +++ b/packages/reporter/cypress/support/utils.ts @@ -14,9 +14,10 @@ interface HandlesFileOpeningProps { selector: string file: File stackTrace?: boolean + previousClickSelector?: string } -export const itHandlesFileOpening = ({ getRunner, selector, file, stackTrace = false }: HandlesFileOpeningProps) => { +export const itHandlesFileOpening = ({ getRunner, selector, file, stackTrace = false, previousClickSelector }: HandlesFileOpeningProps) => { describe('it handles file opening', () => { it('emits unified file open event', () => { cy.stub(getRunner(), 'emit').callThrough() @@ -25,6 +26,10 @@ export const itHandlesFileOpening = ({ getRunner, selector, file, stackTrace = f cy.contains('Stack trace').click() } + if (previousClickSelector) { + cy.get(previousClickSelector).click() + } + cy.get(selector).first().click().then(() => { expect(getRunner().emit).to.be.calledWith('open:file:unified') }) diff --git a/packages/reporter/src/header/controls.tsx b/packages/reporter/src/header/controls.tsx index 3754bf074f..a71f4f3a14 100755 --- a/packages/reporter/src/header/controls.tsx +++ b/packages/reporter/src/header/controls.tsx @@ -1,4 +1,3 @@ -import { action } from 'mobx' import { observer } from 'mobx-react' import React from 'react' import Button from '@cypress-design/react-button' @@ -8,7 +7,7 @@ import Tooltip from '@cypress/react-tooltip' import defaultEvents, { Events } from '../lib/events' import type { AppState } from '../lib/app-state' -import { IconChevronDownSmall, IconChevronUpSmall, IconActionNext, IconActionPlayLarge, IconActionRestart, IconActionStopCircle } from '@cypress-design/react-icon' +import { IconActionNext, IconActionPlayLarge, IconActionRestart, IconActionStopCircle } from '@cypress-design/react-icon' const iconStrokeColor = 'gray-500' const iconFillColor = 'gray-900' @@ -16,37 +15,13 @@ const iconFillColor = 'gray-900' interface Props { events?: Events appState: AppState - displayPreferencesButton?: boolean } -const Controls: React.FC = observer(({ events = defaultEvents, appState, displayPreferencesButton = true }: Props) => { +const Controls: React.FC = observer(({ events = defaultEvents, appState }: Props) => { const emit = (event: string) => () => events.emit(event) - const togglePreferencesMenu = () => { - appState.togglePreferencesMenu() - events.emit('save:state') - } return (
- {displayPreferencesButton && ( - Open Testing Preferences

} className='cy-tooltip'> -
- -
-
- )} {appState.isPaused && ( Resume C

} className='cy-tooltip'>
diff --git a/packages/reporter/src/header/header.tsx b/packages/reporter/src/header/header.tsx index 7d97924865..3c5a3126a5 100644 --- a/packages/reporter/src/header/header.tsx +++ b/packages/reporter/src/header/header.tsx @@ -22,6 +22,8 @@ export interface ReporterHeaderProps { } const Header: React.FC = observer(({ appState, events = defaultEvents, statsStore, runnablesStore, spec }: ReporterHeaderProps) => { + const isStudioSingleTest = appState?.studioActive && appState.studioSingleTestActive + return
{appState.isSpecsListOpen ? 'Collapse' : 'Expand'} Specs List F

} wrapperClassName='toggle-specs-wrapper' className='cy-tooltip'> @@ -46,10 +48,10 @@ const Header: React.FC = observer(({ appState, events = def
{spec && }
-
+ {!isStudioSingleTest &&
-
+
}
}) diff --git a/packages/reporter/src/lib/app-state.ts b/packages/reporter/src/lib/app-state.ts index 5ee77c5b79..95a02304ba 100644 --- a/packages/reporter/src/lib/app-state.ts +++ b/packages/reporter/src/lib/app-state.ts @@ -4,7 +4,6 @@ import { observable, makeObservable } from 'mobx' interface DefaultAppState { isPaused: boolean isRunning: boolean - isPreferencesMenuOpen: boolean nextCommandName: string | null | undefined pinnedSnapshotId: number | string | null studioActive: boolean @@ -16,7 +15,6 @@ interface DefaultAppState { const defaults: DefaultAppState = { isPaused: false, isRunning: false, - isPreferencesMenuOpen: false, nextCommandName: null, pinnedSnapshotId: null, studioActive: false, @@ -29,7 +27,6 @@ class AppState { isSpecsListOpen = false isPaused = defaults.isPaused isRunning = defaults.isRunning - isPreferencesMenuOpen = defaults.isPreferencesMenuOpen nextCommandName = defaults.nextCommandName pinnedSnapshotId = defaults.pinnedSnapshotId studioActive = defaults.studioActive @@ -45,7 +42,6 @@ class AppState { isSpecsListOpen: observable, isPaused: observable, isRunning: observable, - isPreferencesMenuOpen: observable, nextCommandName: observable, pinnedSnapshotId: observable, studioActive: observable, @@ -99,10 +95,6 @@ class AppState { this.isSpecsListOpen = !this.isSpecsListOpen } - togglePreferencesMenu () { - this.isPreferencesMenuOpen = !this.isPreferencesMenuOpen - } - setSpecsList (status: boolean) { this.isSpecsListOpen = status } diff --git a/packages/reporter/src/main.tsx b/packages/reporter/src/main.tsx index 9da21dad75..3bd3cac584 100644 --- a/packages/reporter/src/main.tsx +++ b/packages/reporter/src/main.tsx @@ -15,9 +15,7 @@ import shortcuts from './lib/shortcuts' import Header, { ReporterHeaderProps } from './header/header' import Runnables from './runnables/runnables' -import TestingPreferences from './preferences/testing-preferences' import type { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store' -import { StudioTestHeader } from './studio/StudioTestHeader' function usePrevious (value) { const ref = useRef() @@ -105,18 +103,12 @@ const Reporter: React.FC = observer(({ appState = appStateD } }, [runnerStore.spec, runnerStore.specRunId, resetStatsOnSpecChange, previousSpecRunId]) - const isStudioSingleTest = appState?.studioActive && appState.studioSingleTestActive - return (
- {isStudioSingleTest && runnerStore.spec ? : renderReporterHeader({ appState, statsStore, runnablesStore, spec: runnerStore.spec })} - {appState?.isPreferencesMenuOpen ? ( - - ) : ( + {renderReporterHeader({ appState, statsStore, runnablesStore, spec: runnerStore.spec })} + { runnerStore.spec && = observer(({ appState = appStateD studioEnabled={studioEnabled} canSaveStudioLogs={runnerStore.canSaveStudioLogs} /> - )} + }
) }) diff --git a/packages/reporter/src/preferences/testing-preferences.scss b/packages/reporter/src/preferences/testing-preferences.scss deleted file mode 100644 index 67c3ad6219..0000000000 --- a/packages/reporter/src/preferences/testing-preferences.scss +++ /dev/null @@ -1,26 +0,0 @@ -.testing-preferences { - .testing-preferences-header { - @include inner-header; - font-size: 16px; - font-weight: 400; - color: $gray-700; - - &::before { - width: 0; - } - } - - .testing-preference { - color: $gray-600; - margin: 16px; - - .testing-preference-header { - color: $white; - display: flex; - font-size: 16px; - font-weight: 600; - justify-content: space-between; - margin-bottom: 8px; - } - } -} \ No newline at end of file diff --git a/packages/reporter/src/preferences/testing-preferences.tsx b/packages/reporter/src/preferences/testing-preferences.tsx deleted file mode 100644 index 049a8fc04b..0000000000 --- a/packages/reporter/src/preferences/testing-preferences.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { action } from 'mobx' -import { observer } from 'mobx-react' -import React from 'react' - -import type { AppState } from '../lib/app-state' -import defaultEvents, { Events } from '../lib/events' -import Switch from '../lib/switch' - -interface Props { - events?: Events - appState: AppState -} - -const TestingPreferences: React.FC = observer(({ - events = defaultEvents, - appState, -}: Props) => { - const toggleAutoScrollingUserPref = () => { - appState.toggleAutoScrollingUserPref() - events.emit('save:state') - } - - return ( -
-
- Testing Preferences -
- -
-
- Auto-scrolling - -
-
- Automatically scroll the command log while the tests are running. -
-
-
- ) -}) - -TestingPreferences.displayName = 'TestingPreferences' - -export default TestingPreferences diff --git a/packages/reporter/src/runnables/runnable-header.tsx b/packages/reporter/src/runnables/runnable-header.tsx index 88a9a2bd25..838fe8d440 100644 --- a/packages/reporter/src/runnables/runnable-header.tsx +++ b/packages/reporter/src/runnables/runnable-header.tsx @@ -6,6 +6,8 @@ import { RunnablesStore } from './runnables-store' import { DebugDismiss } from '../header/DebugDismiss' import { Duration } from '../duration/duration' import { SpecFileName } from '../shared/SpecFileName' +import { RunnablePopoverOptions } from './runnable-popover-options' +import appState from '../lib/app-state' const renderRunnableHeader = (children: ReactElement) =>
{children}
@@ -28,11 +30,14 @@ const RunnableHeader: React.FC = observer(({ spec, statsSto ) } + const isStudioSingleTest = appState?.studioActive && appState.studioSingleTestActive + return renderRunnableHeader( <> {runnablesStore.testFilter && runnablesStore.totalTests > 0 && } - + {!isStudioSingleTest && } + , ) }) diff --git a/packages/reporter/src/runnables/runnable-popover-options.tsx b/packages/reporter/src/runnables/runnable-popover-options.tsx new file mode 100644 index 0000000000..fb4d16a401 --- /dev/null +++ b/packages/reporter/src/runnables/runnable-popover-options.tsx @@ -0,0 +1,206 @@ +import { action } from 'mobx' +import { observer } from 'mobx-react' +import React, { useState, useRef, useEffect } from 'react' +import { createPortal } from 'react-dom' +import cs from 'classnames' + +import Tooltip from '@cypress/react-tooltip' +import Button from '@cypress-design/react-button' +import { IconActionAddMedium, IconWindowCodeEditor, IconMenuDotsVertical } from '@cypress-design/react-icon' +import defaultEvents, { Events } from '../lib/events' +import Switch from '../lib/switch' +import appState from '../lib/app-state' + +interface Props { + events?: Events + spec: Cypress.Cypress['spec'] +} + +export const RunnablePopoverOptions: React.FC = observer(({ + events = defaultEvents, + spec, +}: Props) => { + const relativeSpecPath = spec.relative + + const isStudioSingleTest = appState?.studioActive && appState.studioSingleTestActive + + const fileDetails = { + absoluteFile: spec.absolute, + column: 0, + displayFile: spec.name, + line: 0, + originalFile: relativeSpecPath, + relativeFile: relativeSpecPath, + } + + const [isOpen, setIsOpen] = useState(false) + const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 }) + const popoverRef = useRef(null) + const buttonContainerRef = useRef(null) + + const togglePopover = () => { + if (!isOpen && buttonContainerRef.current) { + const rect = buttonContainerRef.current.getBoundingClientRect() + + setPopoverPosition({ + top: rect.bottom + 4, + left: rect.right - 250, // 250px is the popover width + }) + } + + setIsOpen(!isOpen) + } + + const handleOpenInIDE = () => { + events.emit('open:file:unified', fileDetails) + setIsOpen(false) + } + + const handleNewTest = () => { + events.emit('studio:init:suite', { suiteId: 'r1' }) + setIsOpen(false) + } + + const toggleAutoScrollingUserPref = () => { + appState.toggleAutoScrollingUserPref() + events.emit('save:state') + } + + // TODO: to be implemented + // const toggleShowHttpRequests = () => {} + + // Close popover when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + isOpen && + popoverRef.current && + buttonContainerRef.current && + !popoverRef.current.contains(event.target as Node) && + !buttonContainerRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + const handleScroll = () => { + if (isOpen) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside) + window.addEventListener('scroll', handleScroll, true) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + window.removeEventListener('scroll', handleScroll, true) + } + }, [isOpen]) + + const popoverContent = isOpen && ( +
+
+
This spec
+ + + + {!isStudioSingleTest && } +
+ +
+
Testing preferences
+ + {/* // TODO: to be implemented */} + {/*
+
+
+ Show HTTP requests +
+ +
+
*/} + +
+
+
+ Auto-scrolling +
+ +
+ + Automatically scroll the command log while the tests are running. + +
+ +
+
+ ) + + const buttonComponent = () => ( +
+ +
+ ) + + return ( + <> +
+ { + isOpen ? buttonComponent() : ( + Options

} className='cy-tooltip'> + {buttonComponent()} +
+ ) + } +
+ {isOpen && createPortal(popoverContent, document.body)} + + ) +}) diff --git a/packages/reporter/src/runnables/runnables.scss b/packages/reporter/src/runnables/runnables.scss index bf0495a8d4..cf8dfa3783 100644 --- a/packages/reporter/src/runnables/runnables.scss +++ b/packages/reporter/src/runnables/runnables.scss @@ -579,3 +579,114 @@ $dotted-line-left-padding: 19px; } } } + +.runnable-popover-container { + position: relative; + display: inline-block; + + .runnable-options-button { + padding: 0; + width: 32px; + justify-content: center; + color: $gray-500; + } + + .runnable-options-button-border { + border-color: rgb(255 255 255 / 0.2); + } + + .runnable-options-button-icon { + rotate: 90deg; + } +} + +.runnable-popover { + position: fixed; + z-index: 9999; + background: white; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + width: 250px; + max-width: 250px; + padding: 8px; + font-family: $font-system; + + .runnable-popover-section { + &:not(:last-child) { + border-bottom: 1px solid $gray-50; + } + + .runnable-popover-section-title { + font-size: 14px; + font-weight: 500; + color: $gray-600; + padding: 8px 6px 6px 6px; + } + } + + .runnable-popover-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 6px; + border: none; + background: transparent; + cursor: pointer; + font-size: 14px; + font-weight: 400; + color: $gray-900; + text-align: left; + + &:hover { + background-color: $gray-100; + } + + svg { + flex-shrink: 0; + width: 16px; + height: 16px; + } + + span { + flex: 1; + } + } + + .runnable-popover-item-with-toggle { + .runnable-popover-item-with-toggle-content { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + color: $gray-900; + padding: 8px 6px; + + .runnable-popover-item-text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + + .runnable-popover-item-label { + font-size: 14px; + font-weight: 400; + } + } + } + + .runnable-popover-item-description { + font-size: 14px; + color: $gray-700; + line-height: 20px; + font-weight: 400; + padding: 0 6px; + display: block; + } + + .switch { + flex-shrink: 0; + margin-top: 2px; + } + } +} diff --git a/packages/reporter/src/shared/SpecFileName.tsx b/packages/reporter/src/shared/SpecFileName.tsx index a82a0b24a3..d63edab26d 100644 --- a/packages/reporter/src/shared/SpecFileName.tsx +++ b/packages/reporter/src/shared/SpecFileName.tsx @@ -1,7 +1,5 @@ import React from 'react' import { getFilenameParts } from '../lib/util' -import { OpenFileInIDEButton } from '../header/OpenFileInIDEButton' -import { CreateNewTestButton } from '../header/CreateNewTestButton' const displayFileName = (spec: Cypress.Cypress['spec']) => { const specParts = getFilenameParts(spec.name) @@ -27,7 +25,5 @@ export const SpecFileName = ({ spec }: { spec: Cypress.Cypress['spec'] }) => { return
{fileDetails.displayFile || fileDetails.originalFile}{!!fileDetails.line && `:${fileDetails.line}`}{!!fileDetails.column && `:${fileDetails.column}`} - -
} diff --git a/packages/reporter/src/studio/StudioTest.cy.tsx b/packages/reporter/src/studio/StudioTest.cy.tsx index 9c4a2952f1..06d20f9cff 100644 --- a/packages/reporter/src/studio/StudioTest.cy.tsx +++ b/packages/reporter/src/studio/StudioTest.cy.tsx @@ -5,6 +5,7 @@ import { RunnablesStore } from '../runnables/runnables-store' import { StatsStore } from '../header/stats-store' import Test from '../test/test-model' import scroller from '../lib/scroller' +import events from '../lib/events' describe('StudioTest', () => { let appState: AppState @@ -97,6 +98,8 @@ describe('StudioTest', () => { statsStore = { duration: 1500, } as unknown as StatsStore + + cy.spy(events, 'emit').as('emitSpy') }) it('renders component with test information', () => { @@ -295,4 +298,21 @@ describe('StudioTest', () => { // Should not render anything when no test is available cy.get('.studio-single-test-container').should('not.exist') }) + + it('handles back button click', () => { + cy.mount( + , + ) + + cy.get('[data-cy="studio-back-button"]').realHover() + cy.get('.cy-tooltip').should('be.visible') + cy.get('.cy-tooltip').should('contain.text', 'All tests') + + cy.get('[data-cy="studio-back-button"]').click() + cy.get('@emitSpy').should('have.been.calledWith', 'studio:cancel', undefined) + }) }) diff --git a/packages/reporter/src/studio/StudioTest.scss b/packages/reporter/src/studio/StudioTest.scss index e920456e82..a45f9e05b8 100644 --- a/packages/reporter/src/studio/StudioTest.scss +++ b/packages/reporter/src/studio/StudioTest.scss @@ -7,7 +7,9 @@ .studio-header__test-section { display: flex; align-items: flex-start; - border-bottom: 1px solid $gray-900; + border: 1px solid $gray-900; + border-left: none; + border-right: none; padding: 16px; gap: 8px; font-size: 14px; @@ -15,7 +17,8 @@ flex-shrink: 0; .state-icon { - margin-top: 3px; + height: 32px; + min-height: 32px; } svg { @@ -30,8 +33,19 @@ .studio-header__test-section-left { display: flex; flex-grow: 1; - gap: 8px; - margin-top: 5px; + gap: 16px; + + .studio-header__back-button { + padding: 0; + width: 32px; + min-width: 32px; + justify-content: center; + } + + .studio-header__test-section-left-content { + display: flex; + gap: 8px; + } } .studio-header__test-section-right { @@ -44,6 +58,8 @@ color: $white; font-weight: 500; flex-grow: 1; + min-height: 32px; + margin-top: 5px; } .studio-header__test-tooltip-wrapper { diff --git a/packages/reporter/src/studio/StudioTest.tsx b/packages/reporter/src/studio/StudioTest.tsx index 2c2e42dd1c..1509991106 100644 --- a/packages/reporter/src/studio/StudioTest.tsx +++ b/packages/reporter/src/studio/StudioTest.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from 'react' +import React, { useCallback, useMemo, useRef } from 'react' import { observer } from 'mobx-react' import { RunnablesStore } from '../runnables/runnables-store' import { Duration } from '../duration/duration' @@ -8,9 +8,11 @@ import Tooltip from '@cypress/react-tooltip' import cx from 'classnames' import Attempts from '../attempts/attempts' import { useScrollIntoView } from '../lib/useScrollIntoView' -import { IconChevronDownSmall, IconStatusFailedSolid, IconStatusPassedSolid, IconStatusQueuedOutline, IconStatusRunningOutline } from '@cypress-design/react-icon' +import { IconArrowLeft, IconChevronDownSmall, IconStatusFailedSolid, IconStatusPassedSolid, IconStatusQueuedOutline, IconStatusRunningOutline } from '@cypress-design/react-icon' import Test from '../test/test-model' import { StatsStore } from '../header/stats-store' +import Button from '@cypress-design/react-button' +import events from '../lib/events' const getConnectors = (num: number) => { let connectors: JSX.Element[] = [] @@ -70,6 +72,12 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St isStudioActive: appState.studioActive, }) + const handleBackButton = useCallback((e: React.MouseEvent) => { + e.preventDefault() + + events.emit('studio:cancel', undefined) + }, []) + // Call callbackAfterUpdate when mounted and test changes React.useEffect(() => { if (isMounted && currentTest) { @@ -86,21 +94,32 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
- - {parentTitles.length > 0 ? ( - - {getParentTitlesListElements(parentTitles)} - } - wrapperClassName='studio-header__test-tooltip-wrapper' className={cx( - 'studio-tooltip cy-tooltip', - )}> - {testTitle} - - ) : testTitle} + + All tests

} className='cy-tooltip'> +
+ +
+
+ +
+ + {parentTitles.length > 0 ? ( + + {getParentTitlesListElements(parentTitles)} + } + wrapperClassName='studio-header__test-tooltip-wrapper' className={cx( + 'studio-tooltip cy-tooltip', + )}> + {testTitle} + + ) : testTitle} +
- +
diff --git a/packages/reporter/src/studio/StudioTestHeader.cy.tsx b/packages/reporter/src/studio/StudioTestHeader.cy.tsx deleted file mode 100644 index ec250de752..0000000000 --- a/packages/reporter/src/studio/StudioTestHeader.cy.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import { StudioTestHeader } from './StudioTestHeader' -import events from '../lib/events' - -describe('StudioTestHeader', () => { - let mockSpec: Cypress.Cypress['spec'] - - beforeEach(() => { - // Mock the spec - mockSpec = { - name: 'cypress/e2e/example.cy.ts', - relative: 'cypress/e2e/example.cy.ts', - absolute: '/Users/test/cypress/e2e/example.cy.ts', - } as Cypress.Cypress['spec'] - - cy.spy(events, 'emit').as('emitSpy') - }) - - it('renders studio header with spec information', () => { - cy.mount( - , - ) - - cy.get('.studio-header').should('be.visible') - cy.get('.studio-header__file-section').should('be.visible') - cy.get('.spec-file-name').should('be.visible') - cy.get('.spec-file-name').should('contain.text', 'example.cy.ts') - cy.get('[data-cy="studio-back-button"]').should('be.visible') - cy.percySnapshot() - }) - - it('handles back button click', () => { - cy.mount( - , - ) - - cy.get('[data-cy="studio-back-button"]').click() - cy.get('@emitSpy').should('have.been.calledWith', 'studio:cancel', undefined) - }) -}) diff --git a/packages/reporter/src/studio/StudioTestHeader.scss b/packages/reporter/src/studio/StudioTestHeader.scss deleted file mode 100644 index 291d17697f..0000000000 --- a/packages/reporter/src/studio/StudioTestHeader.scss +++ /dev/null @@ -1,25 +0,0 @@ -.studio-header { - display: flex; - flex-direction: column; - flex-shrink: 0; - - .studio-header__file-section { - display: flex; - width: 100%; - align-items: center; - gap: 13px; - padding: 0 16px; - border-top: 1px solid $gray-900; - border-bottom: 1px solid $gray-900; - font-size: 14px; - line-height: 20px; - color: $gray-500; - min-height: $header-height; - - .studio-header__back-button { - padding: 0; - width: 32px; - justify-content: center; - } - } -} diff --git a/packages/reporter/src/studio/StudioTestHeader.tsx b/packages/reporter/src/studio/StudioTestHeader.tsx deleted file mode 100644 index 01e5e42847..0000000000 --- a/packages/reporter/src/studio/StudioTestHeader.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useCallback } from 'react' -import { observer } from 'mobx-react' -import Button from '@cypress-design/react-button' -import { IconArrowLeft } from '@cypress-design/react-icon' -import events from '../lib/events' -import { SpecFileName } from '../shared/SpecFileName' - -interface StudioHeaderProps { - spec: Cypress.Cypress['spec'] -} - -export const StudioTestHeader = observer(({ spec }: StudioHeaderProps) => { - const handleBackButton = useCallback((e: React.MouseEvent) => { - e.preventDefault() - - events.emit('studio:cancel', undefined) - }, []) - - return ( - <> -
-
- - -
-
- - ) -})