Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4dde570
feat: add back selector playground
jennifer-shehane Dec 5, 2025
5fd84e8
update test
jennifer-shehane Dec 5, 2025
8be53d2
changelog entry
jennifer-shehane Dec 5, 2025
07e4d7a
add Studio qualifier
jennifer-shehane Dec 5, 2025
2db4e0e
add snapshot for when the Studio playground is open
jennifer-shehane Dec 8, 2025
cd9f84a
add a few more component tests
jennifer-shehane Dec 8, 2025
5708b23
update show highlight
jennifer-shehane Dec 8, 2025
2dc26bf
add defensive cleanup
jennifer-shehane Dec 8, 2025
9836add
rename test
jennifer-shehane Dec 8, 2025
92571ca
disable studio recording when selectorplayground is shown
jennifer-shehane Dec 8, 2025
2f0fbbb
Merge branch 'develop' into add-back-selector-playground
jennifer-shehane Dec 8, 2025
e733947
add code to open/close selector playground
jennifer-shehane Dec 9, 2025
82af734
Merge branch 'develop' into add-back-selector-playground
jennifer-shehane Dec 10, 2025
5bb0135
add callback, etc for selector playground to interact with panel + up…
jennifer-shehane Dec 10, 2025
f39e19c
removing some tests that are redundant
jennifer-shehane Dec 10, 2025
b3db65b
add try catch around autIframe initialization
jennifer-shehane Dec 12, 2025
8ed399f
remove disabledRecording calls since they're unused
jennifer-shehane Dec 12, 2025
f97372f
close assertions dropdown when selector playground is opened
jennifer-shehane Dec 12, 2025
b3d2b05
add a disabled style that is closer to the other disabled styles
jennifer-shehane Dec 12, 2025
72f2b06
Merge branch 'develop' into add-back-selector-playground
jennifer-shehane Dec 12, 2025
cc20775
Merge branch 'develop' into add-back-selector-playground
jennifer-shehane Dec 15, 2025
58ae592
update to use design-system purple-dark-mode button
jennifer-shehane Dec 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ _Released 12/16/2025 (PENDING)_

- `Angular` version 21 is now supported within component testing. Addressed in [#33004](https://github.com/cypress-io/cypress/pull/33004).
- Adds zoneless support for `Angular` Component Testing through the `angular-zoneless` mount function. Addresses [#31504](https://github.com/cypress-io/cypress/issues/31504) and [#30070](https://github.com/cypress-io/cypress/issues/30070).
- After receiving feedback on its usefulness outside of Studio, the Selector Playground is now available for all users in open mode. When opened, the playground automatically enables interactive mode to help you build and test selectors directly in your application. Addresses [#32672](https://github.com/cypress-io/cypress/issues/32672). Addressed in [#33073](https://github.com/cypress-io/cypress/pull/33073).

**Bugfixes:**

Expand Down
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
},
"dependencies": {},
"devDependencies": {
"@cypress-design/constants-button": "^1.12.0",
"@cypress-design/icon-registry": "^1.5.1",
"@cypress-design/vue-button": "^1.6.0",
"@cypress-design/vue-button": "^1.13.0",
"@cypress-design/vue-icon": "^1.33.0",
"@cypress-design/vue-spinner": "^1.0.0",
"@cypress-design/vue-statusicon": "^1.0.0",
Expand Down
46 changes: 40 additions & 6 deletions packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import SpecRunnerHeaderOpenMode from './SpecRunnerHeaderOpenMode.vue'
import { useAutStore } from '../store'
import { useAutStore, useSelectorPlaygroundStore } from '../store'
import { useStudioStore } from '../store/studio-store'
import { SpecRunnerHeaderFragment, SpecRunnerHeaderFragmentDoc } from '../generated/graphql-test'
import { createEventManager, createTestAutIframe } from '../../cypress/component/support/ctSupport'
import { ExternalLink_OpenExternalDocument } from '@packages/frontend-shared/src/generated/graphql'
import { cyGeneralGlobeX16 } from '@cypress-design/icon-registry'

function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton = false, studioBetaAvailable = false) {
function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton = false) {
const eventManager = createEventManager()
const autIframe = createTestAutIframe()

Expand All @@ -18,7 +18,6 @@ function renderWithGql (gqlVal: SpecRunnerHeaderFragment, shouldShowStudioButton
eventManager={eventManager}
getAutIframe={() => autIframe}
shouldShowStudioButton={shouldShowStudioButton}
studioBetaAvailable={studioBetaAvailable}
/>)
}

Expand Down Expand Up @@ -78,14 +77,14 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
cy.get('[data-cy="playground-activator"]').should('be.disabled')
})

it('is hidden when studio beta is available', () => {
it('is visible by default', () => {
cy.mountFragment(SpecRunnerHeaderFragmentDoc, {
render: (gqlVal) => {
return renderWithGql(gqlVal, true, true)
return renderWithGql(gqlVal, true)
},
})

cy.get('[data-cy="playground-activator"]').should('not.exist')
cy.get('[data-cy="playground-activator"]').should('be.visible')
})

it('opens and closes selector playground', () => {
Expand All @@ -98,6 +97,8 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
cy.findByTestId('playground-activator').click()
cy.get('#selector-playground').should('be.visible')

cy.percySnapshot()

cy.findByTestId('playground-activator').click()
cy.get('#selector-playground').should('not.exist')
})
Expand Down Expand Up @@ -410,4 +411,37 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
cy.findByTestId('studio-button').should('be.visible')
})
})

describe('selector playground and studio recording interaction', () => {
it('allows selector playground to remain open when studio is active', () => {
const studioStore = useStudioStore()
const selectorPlaygroundStore = useSelectorPlaygroundStore()

// Start with Studio already active
studioStore.setActive(true)

cy.mountFragment(SpecRunnerHeaderFragmentDoc, {
render: (gqlVal) => {
return renderWithGql(gqlVal, true)
},
})

// Open Selector Playground while Studio is active
cy.findByTestId('playground-activator').click()

cy.then(() => {
expect(selectorPlaygroundStore.show).to.be.true
expect(studioStore.isActive).to.be.true
})

cy.get('#selector-playground').should('be.visible')

// Playground should remain open - Studio will handle closing it when recording starts
cy.then(() => {
expect(selectorPlaygroundStore.show).to.be.true
})

cy.get('#selector-playground').should('be.visible')
})
})
})
36 changes: 22 additions & 14 deletions packages/app/src/runner/SpecRunnerHeaderOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,7 @@
ref="autHeaderEl"
class="h-full bg-gray-1100 border-l-[1px] border-gray-900 min-h-[64px] text-[14px]"
>
<div class="flex flex-wrap grow p-[16px] gap-[12px] h-[64px] flex-nowrap">
<button
v-if="!studioBetaAvailable"
data-cy="playground-activator"
:disabled="isDisabled"
class="bg-gray-1100 border rounded-md flex h-full border-gray-800 outline-solid outline-indigo-500 transition w-[40px] duration-150 items-center justify-center hover:bg-gray-800"
:aria-label="t('runner.selectorPlayground.toggle')"
:class="[selectorPlaygroundStore.show ? 'bg-gray-800 border-gray-700' : 'bg-gray-1100']"
@click="togglePlayground"
>
<i-cy-crosshairs_x16 class="icon-dark-gray-300" />
</button>
<div class="flex grow p-[16px] gap-[12px] h-[64px] flex-nowrap">
<div
data-cy="aut-url"
class="aut-url-container border rounded flex bg-gray-950 grow border-gray-800 h-[32px] align-middle"
Expand Down Expand Up @@ -79,6 +68,25 @@
</span>
</Tag>
</div>
<Button
data-cy="playground-activator"
:disabled="isDisabled"
:variant="isDisabled ? 'purple-dark-mode' : (selectorPlaygroundStore.show ? 'purple-dark-mode' : 'outline-dark')"
size="32"
square
:aria-label="t('runner.selectorPlayground.toggle')"
:class="{
'playground-button-purple': selectorPlaygroundStore.show && !isDisabled,
'playground-button-disabled': isDisabled
}"
@click="togglePlayground"
>
<i-cy-crosshairs_x16
:class="isDisabled
? 'icon-dark-gray-700'
: (selectorPlaygroundStore.show ? 'icon-dark-white' : 'icon-dark-gray-300')"
/>
</Button>
<StudioButton
v-if="shouldShowStudioButton"
:event-manager="eventManager"
Expand Down Expand Up @@ -116,8 +124,9 @@ import { useI18n } from 'vue-i18n'
import type { SpecRunnerHeaderFragment } from '../generated/graphql'
import type { EventManager } from './event-manager'
import type { AutIframe } from './aut-iframe'
import { togglePlayground as _togglePlayground } from './utils'
import { togglePlayground as _togglePlayground } from './selector-playground/utils'
import Tag from '@cypress-design/vue-tag'
import Button from '@cypress-design/vue-button'
import SelectorPlayground from './selector-playground/SelectorPlayground.vue'
import ExternalLink from '@packages/frontend-shared/src/gql-components/ExternalLink.vue'
import Alert from '@packages/frontend-shared/src/components/Alert.vue'
Expand Down Expand Up @@ -163,7 +172,6 @@ const props = defineProps<{
eventManager: EventManager
getAutIframe: () => AutIframe
shouldShowStudioButton: boolean
studioBetaAvailable: boolean
}>()

const showAlert = ref(false)
Expand Down
5 changes: 0 additions & 5 deletions packages/app/src/runner/SpecRunnerOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
:event-manager="eventManager"
:get-aut-iframe="getAutIframeModel"
:should-show-studio-button="shouldShowStudioButton"
:studio-beta-available="studioBetaAvailable"
/>
</HideDuringScreenshot>

Expand Down Expand Up @@ -330,10 +329,6 @@ const cloudStudioRequested = computed(() => {
return props.gql.cloudStudioRequested
})

const studioBetaAvailable = computed(() => {
return !!cloudStudioRequested.value
})

Comment on lines -333 to -336
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used anywhere else in this component

const shouldShowStudioButton = computed(() => {
// Check if we're running all specs by looking at the route query
const isRunningAllSpecs = route.query.file === '__all'
Expand Down
111 changes: 106 additions & 5 deletions packages/app/src/runner/selector-playground/SelectorPlayground.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,29 @@ describe('SelectorPlayground', () => {
cy.get('[data-cy="playground-selector"]').should('have.value', 'body')
})

it('toggles enabled', () => {
it('is enabled when playground is open', () => {
const selectorPlaygroundStore = useSelectorPlaygroundStore()

expect(selectorPlaygroundStore.isEnabled).to.be.false
// Reset to disabled state before mounting
selectorPlaygroundStore.setEnabled(false)
selectorPlaygroundStore.setShowingHighlight(false)

const { autIframe } = mountSelectorPlayground()
// Create autIframe and set up spies BEFORE mounting, since onMounted will call these methods
const autIframe = createTestAutIframe()

cy.spy(autIframe, 'toggleSelectorPlayground')
cy.spy(autIframe, 'toggleSelectorHighlight')
cy.spy(selectorPlaygroundStore, 'setShowingHighlight')

cy.get('[data-cy="playground-toggle"]').click().then(() => {
mountSelectorPlayground(createEventManager(), autIframe)

// When the playground component is mounted (visible), it should automatically be enabled
// and initialize highlighting functionality
cy.then(() => {
expect(selectorPlaygroundStore.isEnabled).to.be.true
expect(autIframe.toggleSelectorPlayground).to.have.been.called
expect(autIframe.toggleSelectorPlayground).to.have.been.calledWith(true)
expect(selectorPlaygroundStore.setShowingHighlight).to.have.been.calledWith(true)
expect(autIframe.toggleSelectorHighlight).to.have.been.calledWith(true)
})
})

Expand Down Expand Up @@ -189,4 +200,94 @@ describe('SelectorPlayground', () => {

cy.get('[data-cy="playground-selector"]').should('have.attr', 'autocomplete', 'off')
})

it('triggers highlight on mouseover', () => {
Comment on lines +203 to +204
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just added some CT coverage that was never there

const selectorPlaygroundStore = useSelectorPlaygroundStore()
const { autIframe } = mountSelectorPlayground()

cy.spy(autIframe, 'toggleSelectorHighlight')
cy.spy(selectorPlaygroundStore, 'setShowingHighlight')

cy.get('[data-cy="playground-selector"]').parent().trigger('mouseover')

cy.then(() => {
expect(selectorPlaygroundStore.setShowingHighlight).to.have.been.calledWith(true)
expect(autIframe.toggleSelectorHighlight).to.have.been.calledWith(true)
})
})

it('updates store and triggers highlight when typing', () => {
const selectorPlaygroundStore = useSelectorPlaygroundStore()
const { autIframe } = mountSelectorPlayground()

cy.spy(autIframe, 'toggleSelectorHighlight')

cy.get('[data-cy="playground-selector"]').clear().type('.test-selector')

cy.then(() => {
expect(selectorPlaygroundStore.getSelector).to.eq('.test-selector')
expect(autIframe.toggleSelectorHighlight).to.have.been.calledWith(true)
})
})

it('shows correct selector value when switching methods', () => {
const selectorPlaygroundStore = useSelectorPlaygroundStore()

selectorPlaygroundStore.getSelector = '.get-selector'
selectorPlaygroundStore.containsSelector = '.contains-selector'

mountSelectorPlayground()

cy.get('[data-cy="playground-selector"]').should('have.value', '.get-selector')

cy.get('[aria-label="Selector methods"]').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()

cy.get('[data-cy="playground-selector"]').should('have.value', '.contains-selector')

cy.get('[aria-label="Selector methods"]').click()
cy.findByRole('menuitem', { name: 'cy.get' }).click()

cy.get('[data-cy="playground-selector"]').should('have.value', '.get-selector')
})

it('has correct input attributes to prevent autocomplete', () => {
mountSelectorPlayground()

cy.get('[data-cy="playground-selector"]')
.should('have.attr', 'autocomplete', 'off')
.should('have.attr', 'autocapitalize', 'none')
.should('have.attr', 'autocorrect', 'off')
.should('have.attr', 'spellcheck', 'false')
})

it('resets show state when component unmounts to prevent inconsistent state', () => {
const selectorPlaygroundStore = useSelectorPlaygroundStore()

// Set up initial state: show=true and component is enabled
selectorPlaygroundStore.setShow(true)
selectorPlaygroundStore.setEnabled(true)

const { element } = mountSelectorPlayground()

// Verify component is visible and state is consistent
cy.get('#selector-playground').should('be.visible')
cy.then(() => {
expect(selectorPlaygroundStore.show).to.be.true
expect(selectorPlaygroundStore.isEnabled).to.be.true
})

// Unmount the component (simulating navigation or parent unmount)
// This should trigger onUnmounted which calls setShow(false)
// In Cypress Vue component testing, cy.mount returns { wrapper, component }
element.then(({ wrapper }) => {
wrapper.unmount()
})

// After unmount, show should be false to prevent inconsistent state
// where show=true but component is not rendered, causing unexpected re-appearance
cy.then(() => {
expect(selectorPlaygroundStore.show).to.be.false
})
})
})
Loading
Loading