feat(unify): reporter header design (#18471)

* display duration in runnable header

* reporter header design

* fix a couple issues

* style tweaks

* fix bug

* use new icons

* split filename and style

* add unit tests for splitting filename

* fix tests

* address feedback

* update pending icon; use icons from frontend-shared

* update filename muted extension functionality

* change duration display

* fix test
This commit is contained in:
Shawn Taylor
2021-10-28 20:44:09 -04:00
committed by GitHub
parent 924ce03bd2
commit f9ddde283e
31 changed files with 392 additions and 152 deletions
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2854 8.42875L3.75725 13.5457C3.42399 13.7456 3 13.5056 3 13.1169V2.8831C3 2.49445 3.42399 2.25439 3.75725 2.45435L12.2854 7.57125C12.6091 7.76546 12.6091 8.23454 12.2854 8.42875Z" fill="#434861" class="icon-dark"/>
<path d="M13 2.6V13.4M3.75725 2.45435L12.2854 7.57125C12.6091 7.76546 12.6091 8.23454 12.2854 8.42875L3.75725 13.5457C3.42399 13.7456 3 13.5056 3 13.1169V2.8831C3 2.49445 3.42399 2.25439 3.75725 2.45435Z" class="icon-light" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2854 8.42875L3.75725 13.5457C3.42399 13.7456 3 13.5056 3 13.1169V2.8831C3 2.49445 3.42399 2.25439 3.75725 2.45435L12.2854 7.57125C12.6091 7.76546 12.6091 8.23455 12.2854 8.42875Z" fill="#434861" class="icon-light" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 9C13 11.7614 10.7614 14 8 14C5.23858 14 3 11.7614 3 9C3 6.23858 5.23858 4 8 4H11M11 4L9 6M11 4L9 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-light"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12V4C3 3.44772 3.44772 3 4 3H12C12.5523 3 13 3.44772 13 4V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12Z" fill="#434861" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-light"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 14V2C2 1.44772 2.44772 1 3 1H13C13.5523 1 14 1.44772 14 2V14C14 14.5523 13.5523 15 13 15H3C2.44772 15 2 14.5523 2 14Z" fill="#2E3247" class="icon-light"/>
<path d="M5 8H8M5 5H11M5 11H10M13 1L3 1C2.44772 1 2 1.44772 2 2V14C2 14.5523 2.44772 15 3 15H13C13.5523 15 14 14.5523 14 14V2C14 1.44772 13.5523 1 13 1Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-dark"/>
</svg>

After

Width:  |  Height:  |  Size: 530 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 3H8M14 8H2M2 8L4.5 5.5M2 8L4.5 10.5M14 13H8" stroke="#1B1E2E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-dark"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3H8M2 8H14M14 8L11.5 5.5M14 8L11.5 10.5M2 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-dark"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

@@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8.5V3.5C2 2.67157 2.67157 2 3.5 2H8.5C9.32843 2 10 2.67157 10 3.5V8.5C10 9.32843 9.32843 10 8.5 10H3.5C2.67157 10 2 9.32843 2 8.5Z" fill="#F3F4FA" stroke="#BFC2D4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-dark-stroke icon-light-fill"/>
<path d="M4.47023 2.3053C3.5245 2.69703 2.7282 3.44956 2.30552 4.47001C1.46014 6.51092 2.42932 8.85072 4.47023 9.69609C6.51114 10.5415 8.85094 9.57229 9.69631 7.53138C10.119 6.51092 10.088 5.41575 9.69631 4.47001C9.30457 3.52428 8.55205 2.72798 7.5316 2.3053" class="icon-light" stroke="#9095AD" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 418 B

@@ -85,21 +85,6 @@ describe('header', () => {
cy.get('.failed .num').should('have.text', '--')
cy.get('.pending .num').should('have.text', '--')
})
it('displays the time taken in seconds', () => {
const start = new Date(2000, 0, 1)
const now = new Date(2000, 0, 1, 0, 0, 12, 340)
cy.clock(now).then(() => {
runner.emit('reporter:start', { startTime: start.toISOString() })
})
cy.get('.duration .num').should('have.text', '12.34')
})
it('displays "--" if no time taken', () => {
cy.get('.duration .num').should('have.text', '--')
})
})
describe('controls', () => {
@@ -160,10 +145,6 @@ describe('header', () => {
})
describe('pause controls', () => {
it('does not display paused label', () => {
cy.get('.paused-label').should('not.exist')
})
it('does not display play button', () => {
cy.get('.play').should('not.exist')
})
@@ -183,10 +164,6 @@ describe('header', () => {
runner.emit('paused', 'find')
})
it('displays paused label', () => {
cy.get('.paused-label').should('be.visible')
})
it('displays play button', () => {
cy.get('.play').should('be.visible')
})
@@ -4,6 +4,6 @@ describe('special characters', () => {
it('displays file name with decoded special characters', () => {
cy.wrap(Cypress.$(window.top.document.body))
.find('.reporter .runnable-header a')
.should('have.text', 'cypress/integration/meta_&%_spec.ts')
.should('have.text', 'meta_&%_spec.ts')
})
})
@@ -114,6 +114,25 @@ describe('runnables', () => {
cy.get('.error li').should('have.length', 2)
})
it('displays the time taken in seconds', () => {
start()
const startTime = new Date(2000, 0, 1)
const now = new Date(2000, 0, 1, 0, 0, 12, 340)
cy.clock(now).then(() => {
runner.emit('reporter:start', { startTime: startTime.toISOString() })
})
cy.get('.runnable-header span:last').should('have.text', '00:12')
})
it('does not display time if no time taken', () => {
start()
cy.get('.runnable-header span:first').should('have.text', 'foo')
cy.get('.runnable-header span:last').should('not.have.text', '--')
})
describe('when there are no tests', () => {
beforeEach(() => {
runnables.suites = []
@@ -54,8 +54,8 @@ describe('spec title', () => {
})
})
it('displays relative spec path', () => {
cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js')
it('displays name without path', () => {
cy.get('.runnable-header').find('a').should('have.text', 'foo.js')
cy.percySnapshot()
})
@@ -130,7 +130,7 @@ describe('suites', () => {
describe('studio button', () => {
it('displays studio icon with half transparency when hovering over test title', () => {
cy.contains('suite 1')
cy.contains('nested suite 1')
.closest('.runnable-wrapper')
.realHover()
.find('.runnable-controls-studio')
@@ -139,7 +139,7 @@ describe('suites', () => {
})
it('displays studio icon with no transparency and tooltip on hover', () => {
cy.contains('suite 1')
cy.contains('nested suite 1')
.closest('.collapsible-header')
.find('.runnable-controls-studio')
.realHover()
@@ -0,0 +1,83 @@
import { formatDuration, getFilenameParts } from '../../../src/lib/util'
const compare = (filename, array) => {
expect(getFilenameParts(filename)).to.deep.equal(array)
}
describe('utils', () => {
context('formatDuration', () => {
it('formats no time', () => {
expect(formatDuration(0)).to.equal('--')
})
it('formats time of <1s', () => {
expect(formatDuration(1)).to.equal('1ms')
expect(formatDuration(999)).to.equal('999ms')
})
it('formats time of >=1s', () => {
expect(formatDuration(1000)).to.equal('00:01')
expect(formatDuration(1400)).to.equal('00:01')
expect(formatDuration(35620)).to.equal('00:36')
expect(formatDuration(59200)).to.equal('00:59')
})
it('formats time of >=1m', () => {
expect(formatDuration(60000)).to.equal('01:00')
expect(formatDuration(600000)).to.equal('10:00')
expect(formatDuration(3599000)).to.equal('59:59')
})
it('formats time of >=1h', () => {
expect(formatDuration(3600000)).to.equal('1:00:00')
expect(formatDuration(4200000)).to.equal('1:10:00')
expect(formatDuration(7199000)).to.equal('1:59:59')
})
it('displays larger times in hours', () => {
expect(formatDuration(360000000)).to.equal('100:00:00')
})
})
context('getFilenameParts', () => {
it('splits basic filenames', () => {
compare('something.foo.ts', ['something.foo', '.ts'])
compare('first-user.js', ['first-user', '.js'])
compare('model.coffee', ['model', '.coffee'])
})
it('handles .spec, .test, and .cy', () => {
compare('basic.spec.ts', ['basic', '.spec.ts'])
compare('spies_stubs_clocks.spec.js', ['spies_stubs_clocks', '.spec.js'])
compare('newIssuanceWorkflow.test.js', ['newIssuanceWorkflow', '.test.js'])
compare('Button.cy.js', ['Button', '.cy.js'])
})
it('does not consider "_spec" to be part of the extension', () => {
// might want to change this functionality later, but for now this is working as intended
compare('warning_spec.js', ['warning_spec', '.js'])
})
it('behaves as expected if "spec" is in the filename', () => {
compare('spec.ts', ['spec', '.ts'])
compare('spec_spec.ts', ['spec_spec', '.ts'])
})
it('handles no file extension', () => {
compare('no-extension', ['no-extension', ''])
})
it('strips directory path', () => {
compare('unit/spec_split_spec.ts', ['spec_split_spec', '.ts'])
compare('dir/unit/spec_split_spec.ts', ['spec_split_spec', '.ts'])
})
it('displays filename with special characters', () => {
compare('cypress/integration/meta_&%_spec.ts', ['meta_&%_spec', '.ts'])
})
it('handles an unexpected number of extensions', () => {
compare('reporter.hooks.spec.js', ['reporter.hooks', '.spec.js'])
})
})
})
+5
View File
@@ -16,3 +16,8 @@ declare namespace Cypress {
percySnapshot (): Chainable
}
}
declare module "*.svg" {
const content: any;
export default content;
}
+1
View File
@@ -33,6 +33,7 @@
"prop-types": "15.7.2",
"react": "16.8.6",
"react-dom": "16.8.6",
"react-svg-loader": "3.0.3",
"rimraf": "3.0.2",
"sinon": "7.5.0",
"webpack": "4.35.3",
-7
View File
@@ -233,13 +233,6 @@ $code-border-radius: 4px;
margin: 0 10px 10px;
.runnable-err-file-path {
&:before {
@extend .#{$fa-css-prefix};
@extend .#{$fa-css-prefix}-file;
color: $gray-700;
margin-right: 5px;
}
background: rgba($gray-900, 0.5);
border-top-left-radius: $code-border-radius;
border-top-right-radius: $code-border-radius;
+13 -9
View File
@@ -8,6 +8,11 @@ import Tooltip from '@cypress/react-tooltip'
import defaultEvents, { Events } from '../lib/events'
import { AppState } from '../lib/app-state'
import NextIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/action-next_x16.svg'
import PlayIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/action-play_x16.svg'
import RestartIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/action-restart_x16.svg'
import StopIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/action-stop_x16.svg'
const ifThen = (condition: boolean, component: React.ReactNode) => (
condition ? component : null
)
@@ -26,15 +31,10 @@ const Controls = observer(({ events = defaultEvents, appState }: Props) => {
return (
<div className='controls'>
{ifThen(appState.isPaused, (
<span className='paused-label'>
<label>Paused</label>
</span>
))}
{ifThen(appState.isPaused, (
<Tooltip placement='bottom' title={<p>Resume <span className='kbd'>C</span></p>} className='cy-tooltip'>
<button aria-label='Resume' className='play' onClick={emit('resume')}>
<i className='fas fa-play' />
<PlayIcon />
</button>
</Tooltip>
))}
@@ -53,21 +53,25 @@ const Controls = observer(({ events = defaultEvents, appState }: Props) => {
{ifThen(appState.isRunning && !appState.isPaused, (
<Tooltip placement='bottom' title={<p>Stop Running <span className='kbd'>S</span></p>} className='cy-tooltip' visible={appState.studioActive ? false : null}>
<button aria-label='Stop' className='stop' onClick={emit('stop')} disabled={appState.studioActive}>
<i className='fas fa-stop' />
<StopIcon />
</button>
</Tooltip>
))}
{ifThen(!appState.isRunning, (
<Tooltip placement='bottom' title={<p>Run All Tests <span className='kbd'>R</span></p>} className='cy-tooltip'>
<button aria-label='Rerun all tests' className='restart' onClick={emit('restart')}>
<i className={appState.studioActive ? 'fas fa-undo' : 'fas fa-redo'} />
{appState.studioActive ? (
<RestartIcon transform="scale(-1 1)" />
) : (
<RestartIcon />
)}
</button>
</Tooltip>
))}
{ifThen(!!appState.nextCommandName, (
<Tooltip placement='bottom' title={<p>Next <span className='kbd'>[N]:</span>{appState.nextCommandName}</p>} className='cy-tooltip'>
<button aria-label={`Next '${appState.nextCommandName}'`} className='next' onClick={emit('next')}>
<i className='fas fa-step-forward' />
<NextIcon />
</button>
</Tooltip>
))}
+59 -71
View File
@@ -1,13 +1,14 @@
.reporter {
header {
background-color: $gray-1000;
border-bottom: 1px solid $gray-900;
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
font-family: $font-system;
min-height: $header-height;
outline: 0;
overflow: hidden;
padding: 20px 16px;
width: 100%;
z-index: 1;
@@ -16,15 +17,15 @@
}
button {
background-color: $gray-1000;
background-color: transparent;
border-color: transparent;
border-radius: 0;
display: block;
display: inline-block;
font-weight: 300;
line-height: 26px;
outline: 0;
padding: 10px 0;
padding: 0 8px;
text-align: center;
width: 40px;
&:hover {
background-color: $gray-900;
@@ -46,96 +47,73 @@
.focus-tests {
display: flex;
height: 24px;
button {
border-right: 1px solid $gray-900;
font-weight: 400;
padding: 10px 12px;
color: $gray-700;
font-size: 16px;
font-weight: 300;
padding-left: 0;
padding-right: 8px;
width: auto !important;
span {
display: none;
}
i {
margin-right: 5px;
svg {
margin-right: 8px;
margin-bottom: -2px;
}
}
}
.stats {
align-items: center;
border: 1px solid $gray-900;
border-radius: 4px;
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding: 0;
height: 24px;
justify-content: center;
padding-right: 8px;
li {
display: block;
font-size: 16px;
height: 46px;
line-height: 26px;
display: inline-block;
font-size: 12px;
font-weight: 600;
line-height: 24px;
list-style-type: none;
padding: 10px 5px;
padding: 1px 0px 0 4px;
&.passed {
color: $pass;
svg {
margin: 0px 4px;
}
&.failed {
color: $fail;
}
.num {
color: $white;
line-height: 12px;
vertical-align: text-top;
&.pending {
color: $gray-400;
&.empty {
color: $gray-800;
}
}
i {
margin: 5px;
}
}
.duration {
border-left: 1px solid $gray-900;
border-right: 1px solid $gray-900;
padding-left: 10px;
padding-right: 10px;
}
}
.controls {
align-items: center;
border: 1px solid $gray-900;
border-radius: 4px;
display: flex;
flex-wrap: wrap;
> span {
display: flex;
&:last-child button {
border-right: 1px solid $gray-900;
}
}
.paused-label {
align-items: center;
margin-right: 5px;
label {
background-color: $red-500;
}
}
i {
font-size: 90%;
margin: 5px;
&.fa-redo {
font-size: 100%;
position: relative;
top: 1px;
}
}
justify-content: center;
margin-left: 8px;
.toggle-auto-scrolling {
line-height: 25px;
line-height: 22px;
padding: 0 10px;
i {
font-size: 100%;
@@ -159,21 +137,31 @@
}
}
}
span:last-child button,
.play {
padding: 1px 10px;
}
span {
button {
color: $gray-400;
height: 22px;
}
&:not(:last-child) {
button {
border-right: 1px solid $gray-900;
}
}
}
}
// utilizing element size queries: https://github.com/marcj/css-element-queries
// styles take effect when width is greater than or equal to the specified amount
&[min-width~="398px"] {
header button {
width: 50px;
}
.focus-tests button span {
display: inline;
}
.stats li {
padding: 10px;
}
}
}
+6 -3
View File
@@ -3,6 +3,8 @@ import React from 'react'
// @ts-ignore
import Tooltip from '@cypress/react-tooltip'
import MenuExpandRightIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/menu-expand-right_x16.svg'
import defaultEvents, { Events } from '../lib/events'
import { AppState } from '../lib/app-state'
@@ -20,12 +22,13 @@ const Header = observer(({ appState, events = defaultEvents, statsStore }: Repor
<header>
<Tooltip placement='bottom' title={<p>View All Tests <span className='kbd'>F</span></p>} wrapperClassName='focus-tests' className='cy-tooltip'>
<button onClick={() => events.emit('focus:tests')}>
<i className='fas fa-chevron-left' />
<span className='focus-tests-text'>Tests</span>
<MenuExpandRightIcon />
<span className='focus-tests-text'>Specs</span>
</button>
</Tooltip>
<Stats stats={statsStore} />
<div className='spacer' />
<Stats stats={statsStore} />
<Controls appState={appState} />
</header>
))
+14 -13
View File
@@ -1,10 +1,14 @@
import cs from 'classnames'
import { observer } from 'mobx-react'
import React from 'react'
import { StatsStore } from './stats-store'
import FailedIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/status-failed_x12.svg'
import PassedIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/status-passed_x12.svg'
import PendingIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/status-pending_x12.svg'
const count = (num: number) => num > 0 ? num : '--'
const formatDuration = (duration: number) => duration ? String((duration / 1000).toFixed(2)).padStart(5, '0') : '--'
interface Props {
stats: StatsStore
@@ -12,23 +16,20 @@ interface Props {
const Stats = observer(({ stats }: Props) => (
<ul aria-label='Stats' className='stats'>
<li className='pending'>
<PendingIcon aria-hidden="true" />
<span className='visually-hidden'>Pending:</span>
<span className={cs('num', { 'empty': !stats.numPending })}>{count(stats.numPending)}</span>
</li>
<li className='passed'>
<i aria-hidden="true" className='fas fa-check' />
<PassedIcon aria-hidden="true" />
<span className='visually-hidden'>Passed:</span>
<span className='num'>{count(stats.numPassed)}</span>
<span className={cs('num', { 'empty': !stats.numPassed })}>{count(stats.numPassed)}</span>
</li>
<li className='failed'>
<i aria-hidden="true" className='fas fa-times' />
<FailedIcon aria-hidden="true" />
<span className='visually-hidden'>Failed:</span>
<span className='num'>{count(stats.numFailed)}</span>
</li>
<li className='pending'>
<i aria-hidden="true" className='fas fa-circle-notch' />
<span className='visually-hidden'>Pending:</span>
<span className='num'>{count(stats.numPending)}</span>
</li>
<li className='duration'>
<span className='num'>{formatDuration(stats.duration)}</span>
<span className={cs('num', { 'empty': !stats.numFailed })}>{count(stats.numFailed)}</span>
</li>
</ul>
))
@@ -6,19 +6,25 @@ import { FileDetails } from '@packages/ui-components'
import FileOpener from './file-opener'
import TextIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/document-text_x16.svg'
interface Props {
fileDetails: FileDetails
className?: string
hasIcon?: boolean
}
const FileNameOpener = observer((props: Props) => {
const { originalFile, line, column } = props.fileDetails
const { displayFile, originalFile, line, column } = props.fileDetails
return (
<Tooltip title={'Open in IDE'} wrapperClassName={props.className} className='cy-tooltip'>
<span>
<FileOpener fileDetails={props.fileDetails}>
{originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
{props.hasIcon && (
<TextIcon />
)}
{displayFile || originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
</FileOpener>
</span>
</Tooltip>
+57
View File
@@ -16,7 +16,64 @@ const onEnterOrSpace = (f: (() => void)) => {
}
}
const formatDuration = (duration: number): string => {
if (!duration) return '--'
if (duration < 1000) {
return `${duration}ms`
}
const seconds = Math.round(duration / 1000)
const displaySeconds = String(seconds % 60).padStart(2, '0')
const displayMinutes = String(Math.floor((seconds / 60) % 60)).padStart(2, '0')
const displayHours = String(Math.floor(seconds / (60 * 60)))
if (displayHours === '0') return `${displayMinutes}:${displaySeconds}`
return `${displayHours}:${displayMinutes}:${displaySeconds}`
}
const splitFilename = (filename: string, index: number): [string, string] => {
if (index < 0) {
return [filename, '']
}
return [filename.substr(0, index), filename.substr(index)]
}
// strips directory path and then
// '.cy', '.spec', and '.test' as well as the last file extension should be split off from the main part of the filename
const getFilenameParts = (spec: string): [string, string] => {
if (!spec) {
return ['', '']
}
// remove directory path
const specWithoutPath = spec.substr(spec.lastIndexOf('/') + 1)
if (!specWithoutPath) {
return [spec, '']
}
// if it contains .cy, .spec, or .test, split it before that
const specIndex = specWithoutPath.match(/(?=(\.cy|\.spec|\.test))/)?.index
if (specIndex && specIndex > -1) {
return splitFilename(specWithoutPath, specIndex)
}
// if it didn't contain .cy, .spec, or .test, split it before the last extension
const dotIndex = specWithoutPath.lastIndexOf('.')
// if there's no extension, return the whole thing
if (dotIndex < 0) return [specWithoutPath, '']
return splitFilename(specWithoutPath, dotIndex)
}
export {
formatDuration,
getFilenameParts,
indent,
onEnterOrSpace,
}
+2 -1
View File
@@ -122,9 +122,10 @@ $warn-header-background: $orange-1000;
$warn-header-text: $orange-700;
$warn-text: $orange-600;
$header-height: 46px;
$header-height: 64px;
$reporter-contents-min-width: 170px;
$font-sans: 'Mulish', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$monospace: Consolas, Monaco, 'Andale Mono', monospace;
$font-system: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
@@ -1,16 +1,21 @@
import { observer } from 'mobx-react'
import React, { Component, ReactElement } from 'react'
import FileNameOpener from '../lib/file-name-opener'
import { StatsStore } from '../header/stats-store'
import { formatDuration, getFilenameParts } from '../lib/util'
const renderRunnableHeader = (children: ReactElement) => <div className="runnable-header">{children}</div>
interface RunnableHeaderProps {
spec: Cypress.Cypress['spec']
statsStore: StatsStore
}
@observer
class RunnableHeader extends Component<RunnableHeaderProps> {
render () {
const { spec } = this.props
const { spec, statsStore } = this.props
const relativeSpecPath = spec.relative
@@ -26,16 +31,32 @@ class RunnableHeader extends Component<RunnableHeaderProps> {
)
}
const displayFileName = () => {
const specParts = getFilenameParts(spec.name)
return (
<>
<strong>{specParts[0]}</strong>{specParts[1]}
</>
)
}
const fileDetails = {
absoluteFile: spec.absolute,
column: 0,
displayFile: displayFileName(),
line: 0,
originalFile: relativeSpecPath,
relativeFile: relativeSpecPath,
}
return renderRunnableHeader(
<FileNameOpener fileDetails={fileDetails} />,
<>
<FileNameOpener fileDetails={fileDetails} hasIcon />
{Boolean(statsStore.duration) && (
<span className='duration'>{formatDuration(statsStore.duration)}</span>
)}
</>,
)
}
}
+39 -12
View File
@@ -5,7 +5,6 @@
.container {
background-color: $gray-1000;
box-shadow: 0 1px 2px $gray-600;
flex-grow: 2;
overflow-y: auto;
}
@@ -363,28 +362,56 @@
.runnable-header {
background: $gray-1000;
border-bottom: 1px solid $gray-900;
display: block;
font-size: 13px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
font-size: 14px;
line-height: 24px;
overflow-wrap: break-word;
padding: 5px 10px;
padding: 16px;
position: sticky;
top: 0;
width: 100%;
z-index: 1;
span > span > a:before,
span > span > span:before {
@extend .#{$fa-css-prefix};
@extend .#{$fa-css-prefix}-file;
color: $gray-700;
display: inline;
margin-right: 5px;
&:before,
&:after {
background-color: $gray-900;
content: '';
position: absolute;
width: calc(100% - 32px);
height: 1px;
left: 16px;
}
&:before {
top: 0;
}
&:after {
bottom: 0;
}
span > span > a > svg {
margin-bottom: -2px;
margin-right: 8px;
}
a, a:active, a:focus, a:hover {
color: $white;
color: $gray-700;
font-weight: 300;
strong {
font-weight: 500;
}
}
.duration {
border: 1px solid $gray-900;
border-radius: 16px;
color: $gray-600;
float: right;
font-size: 12px;
line-height: 16px;
padding: 2px 6px;
}
}
@@ -8,6 +8,7 @@ import { RunnablesError, RunnablesErrorModel } from './runnable-error'
import Runnable from './runnable-and-suite'
import RunnableHeader from './runnable-header'
import { RunnablesStore, RunnableArray } from './runnables-store'
import statsStore, { StatsStore } from '../header/stats-store'
import { Scroller } from '../lib/scroller'
import { AppState } from '../lib/app-state'
import FileOpener from '../lib/file-opener'
@@ -109,6 +110,7 @@ const RunnablesContent = observer(({ runnablesStore, spec, error }: RunnablesCon
export interface RunnablesProps {
error?: RunnablesErrorModel
runnablesStore: RunnablesStore
statsStore: StatsStore
spec: Cypress.Cypress['spec']
scroller: Scroller
appState?: AppState
@@ -121,7 +123,7 @@ class Runnables extends Component<RunnablesProps> {
return (
<div ref='container' className='container'>
<RunnableHeader spec={spec} />
<RunnableHeader spec={spec} statsStore={statsStore} />
<RunnablesContent
runnablesStore={runnablesStore}
spec={spec}
+1 -1
View File
@@ -15,6 +15,6 @@
"./src/**/*.tsx"
],
"files": [
"./../ts/index.d.ts"
"./../ts/index.d.ts", "./index.d.ts"
]
}
@@ -18,8 +18,8 @@ export const ReporterHeader: React.FC<ReporterHeaderProps> = namedObserver('Repo
({ statsStore, appState }) => {
return (
<header className={styles.ctReporterHeader}>
<Stats stats={statsStore} />
<div className='spacer' />
<Stats stats={statsStore} />
<Controls appState={appState} />
</header>
)
@@ -1,6 +1,9 @@
import { ReactNode } from 'react'
export interface FileDetails {
absoluteFile?: string
column: number
displayFile?: ReactNode
line: number
originalFile: string
relativeFile: string
+26
View File
@@ -12557,6 +12557,11 @@ babel-plugin-react-docgen@^4.2.1:
lodash "^4.17.15"
react-docgen "^5.0.0"
babel-plugin-react-svg@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/babel-plugin-react-svg/-/babel-plugin-react-svg-3.0.3.tgz#7da46a0bd8319f49ac85523d259f145ce5d78321"
integrity sha512-Pst1RWjUIiV0Ykv1ODSeceCBsFOP2Y4dusjq7/XkjuzJdvS9CjpkPMUIoO4MLlvp5PiLCeMlsOC7faEUA0gm3Q==
babel-plugin-remove-console@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz#d8f24556c3a05005d42aaaafd27787f53ff013a7"
@@ -34668,6 +34673,27 @@ react-style-singleton@^2.1.0:
invariant "^2.2.4"
tslib "^1.0.0"
react-svg-core@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/react-svg-core/-/react-svg-core-3.0.3.tgz#5d856efeaa4d089b0afeebe885b20b8c9500d162"
integrity sha512-Ws3eM3xCAwcaYeqm4Ajcz3zxBYNI6BeTWWhFR0cpOT+pWuVtozgHYK9xUM0S/ilapZgYMQDe49XgOxpvooFq4w==
dependencies:
"@babel/core" "^7.4.5"
"@babel/plugin-syntax-jsx" "^7.2.0"
"@babel/preset-react" "^7.0.0"
babel-plugin-react-svg "^3.0.3"
lodash.clonedeep "^4.5.0"
lodash.isplainobject "^4.0.6"
svgo "^1.2.2"
react-svg-loader@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/react-svg-loader/-/react-svg-loader-3.0.3.tgz#8baa2d5daa32523dfd0745425ac65e0a90edae15"
integrity sha512-V1KnIUtvWVvc4xCig34n+f+/74ylMMugB2FbuAF/yq+QRi+WLi2hUYp9Ze3VylhA1D7ZgRygBh3Ojj8S3TPhJA==
dependencies:
loader-utils "^1.2.3"
react-svg-core "^3.0.3"
react-syntax-highlighter@^13.5.3:
version "13.5.3"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6"