Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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 @@ -20,6 +20,7 @@ _Released 10/20/2025 (PENDING)_
- Add top padding for command log labels. Addressed in [#32774](https://github.com/cypress-io/cypress/pull/32774).
- The hitbox for expanding a grouped command has been widened. Addresses [#32778](https://github.com/cypress-io/cypress/issues/32778). Addressed in [#32783](https://github.com/cypress-io/cypress/pull/32783).
- Have cursor on hover of the AUT URL to show as pointer. Addresses [#32777](https://github.com/cypress-io/cypress/issues/32777). Addressed in [#32782](https://github.com/cypress-io/cypress/pull/32782).
- Make test name header sticky in studio mode and in the tests list. Addresses [#32591](https://github.com/cypress-io/cypress/issues/32591). Addressed in [#32840](https://github.com/cypress-io/cypress/pull/32840)

**Dependency Updates:**

Expand Down
6 changes: 6 additions & 0 deletions packages/reporter/src/collapsible/collapsible.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "../lib/mixins.scss";

.reporter {
.collapsible-indicator {
margin-right: 8px;
Expand All @@ -8,4 +10,8 @@
.is-open > .collapsible-header-wrapper > .collapsible-header > .collapsible-header-inner > .collapsible-indicator {
transform: rotate(0);
}

.runnable > .is-open > .collapsible-header-wrapper {
@include sticky-header-with-shadow;
}
}
24 changes: 22 additions & 2 deletions packages/reporter/src/collapsible/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cs from 'classnames'
import React, { CSSProperties, MouseEvent, ReactNode, RefObject, useCallback, useState } from 'react'
import React, { CSSProperties, MouseEvent, ReactNode, RefObject, useCallback, useEffect, useRef, useState } from 'react'
import { onEnterOrSpace } from '../lib/util'
import DocumentBlankIcon from '@packages/frontend-shared/src/assets/icons/document-blank_x16.svg'
import { IconChevronDownSmall } from '@cypress-design/react-icon'
Expand All @@ -24,6 +24,8 @@ interface CollapsibleProps {

const Collapsible: React.FC<CollapsibleProps> = ({ isOpen: isOpenAsProp = false, header, headerClass = '', headerStyle = {}, headerExtras, contentClass = '', hideExpander = false, containerRef = null, onOpenStateChangeRequested, children, HeaderComponent }) => {
const [isOpenState, setIsOpenState] = useState(isOpenAsProp)
const headerRef = useRef<HTMLDivElement>(null)
const fixedElementRef = useRef<HTMLDivElement>(null)

const toggleOpenState = useCallback((e?: MouseEvent) => {
e?.stopPropagation()
Expand All @@ -36,9 +38,27 @@ const Collapsible: React.FC<CollapsibleProps> = ({ isOpen: isOpenAsProp = false,

const isOpen = onOpenStateChangeRequested ? isOpenAsProp : isOpenState

const toggleHeaderShadow = (entries) => {
const [entry] = entries

headerRef.current?.classList.toggle('shadow-active', !entry.isIntersecting)
}

useEffect(() => {
if (!fixedElementRef?.current) return

const observer = new IntersectionObserver(toggleHeaderShadow)

observer.observe(fixedElementRef.current)

return () => observer.disconnect()
}, [])

return (
<div className={cs('collapsible', { 'is-open': isOpen })} ref={containerRef}>
<div className={cs('collapsible-header-wrapper', headerClass)}>
{/* This empty div acts as an intersection observer target to toggle the header shadow based on scroll position */}
<div ref={fixedElementRef}/>
<div className={cs('collapsible-header-wrapper', headerClass)} ref={headerRef}>
<div
aria-expanded={isOpen}
className='collapsible-header'
Expand Down
18 changes: 18 additions & 0 deletions packages/reporter/src/lib/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,22 @@
right: 0;
background: $linear-gradient;
z-index: 0;
}

@mixin sticky-header-with-shadow {
position: sticky;
top: 0;
z-index: 1;
background-color: $gray-1100;

&.shadow-active::after {
content: "";
position: absolute;
top: 101%;
left: 0;
right: 0;
height: 16px;
background: linear-gradient(to bottom, $gray-1100 0%, rgba(22, 24, 39, 0.3) 100%);
pointer-events: none;
}
}
1 change: 0 additions & 1 deletion packages/reporter/src/runnables/runnables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ $dotted-line-left-padding: 19px;
width: 100%;
color: $gray-400;
background-color: $gray-1100;
overflow: auto;
line-height: 18px;

.runnable-wrapper {
Expand Down
3 changes: 3 additions & 0 deletions packages/reporter/src/studio/StudioTest.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "../lib/mixins.scss";

.studio-single-test-container {
display: flex;
flex-direction: column;
Expand All @@ -15,6 +17,7 @@
font-size: 14px;
line-height: 20px;
flex-shrink: 0;
@include sticky-header-with-shadow;

.state-icon {
height: 32px;
Expand Down
29 changes: 25 additions & 4 deletions packages/reporter/src/studio/StudioTest.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { observer } from 'mobx-react'
import { RunnablesStore } from '../runnables/runnables-store'
import { Duration } from '../duration/duration'
Expand Down Expand Up @@ -65,6 +65,8 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
// Single we're in single test mode, the current test is the first test in the runnablesStore._tests
const currentTest = Object.values(runnablesStore._tests)[0]
const tooltipRef = useRef<HTMLUListElement>(null)
const testSectionRef = useRef<HTMLDivElement>(null)
const fixedElementRef = useRef<HTMLDivElement>(null)

const { containerRef, isMounted, scrollIntoView } = useScrollIntoView({
appState,
Expand All @@ -89,10 +91,28 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St

const testTitle = currentTest ? <span data-cy='studio-single-test-title' className='studio-header__test-title'>{currentTest.title}</span> : null

const toggleHeaderShadow = (entries) => {
const [entry] = entries

testSectionRef.current?.classList.toggle('shadow-active', !entry.isIntersecting)
}

useEffect(() => {
if (!fixedElementRef.current) return

const observer = new IntersectionObserver(toggleHeaderShadow)

observer.observe(fixedElementRef.current)

return () => observer.disconnect()
}, [])

return (
currentTest && (
<div className='studio-single-test-container' >
<div className='studio-header__test-section'>
currentTest && (<>
{/* This empty div acts as an intersection observer target to toggle the header shadow based on scroll position */}
<div ref={fixedElementRef} />
<div className='studio-single-test-container'>
<div className='studio-header__test-section' ref={testSectionRef}>
<div className='studio-header__test-section-left'>

<Tooltip placement='bottom' title={<p>All tests</p>} className='cy-tooltip'>
Expand Down Expand Up @@ -126,6 +146,7 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
<Attempts isSingleStudioTest test={currentTest} scrollIntoView={scrollIntoView} />
</div>
</div>
</>
)
)
})
Loading