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
@@ -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'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,3 +16,8 @@ declare namespace Cypress {
|
||||
percySnapshot (): Chainable
|
||||
}
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||