Merge pull request #3330 from overleaf/jel-toolbar-btns

Hide toolbar text and show tooltip when out of space

GitOrigin-RevId: 5a73b69e7d92695c4f8691a747307908550e3790
This commit is contained in:
Jessica Lawshe 2020-11-09 08:52:22 -06:00 committed by Copybot
parent da8663fd0f
commit 21ffe27bdd
10 changed files with 593 additions and 172 deletions

View file

@ -1,12 +1,17 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Dropdown, MenuItem } from 'react-bootstrap' import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
export const topFileTypes = ['bbl', 'gls', 'ind'] export const topFileTypes = ['bbl', 'gls', 'ind']
function PreviewDownloadButton({ isCompiling, outputFiles, pdfDownloadUrl }) { function PreviewDownloadButton({
isCompiling,
outputFiles,
pdfDownloadUrl,
showText
}) {
let topFiles = [] let topFiles = []
let otherFiles = [] let otherFiles = []
const { t } = useTranslation() const { t } = useTranslation()
@ -26,16 +31,46 @@ function PreviewDownloadButton({ isCompiling, outputFiles, pdfDownloadUrl }) {
}) })
} }
return ( let textStyle = {}
<Dropdown id="download-dropdown" disabled={isCompiling}> if (!showText) {
textStyle = {
position: 'absolute',
right: '-100vw'
}
}
const buttonElement = (
<a <a
className="btn btn-xs btn-info" className="btn btn-xs btn-info"
disabled={isCompiling || !pdfDownloadUrl} disabled={isCompiling || !pdfDownloadUrl}
download download
href={pdfDownloadUrl || '#'} href={pdfDownloadUrl || '#'}
> >
<Icon type="download" modifier="fw" /> {t('download_pdf')} <Icon type="download" modifier="fw" />
<span className="toolbar-text" style={textStyle}>
{t('download_pdf')}
</span>
</a> </a>
)
return (
<Dropdown
id="download-dropdown"
className="toolbar-item"
disabled={isCompiling}
>
{showText ? (
buttonElement
) : (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="tooltip-download-pdf">{t('download_pdf')}</Tooltip>
}
>
{buttonElement}
</OverlayTrigger>
)}
<Dropdown.Toggle <Dropdown.Toggle
className="btn btn-xs btn-info dropdown-toggle" className="btn btn-xs btn-info dropdown-toggle"
aria-label={t('toggle_output_files_list')} aria-label={t('toggle_output_files_list')}
@ -79,7 +114,8 @@ function FileList({ listType, list }) {
PreviewDownloadButton.propTypes = { PreviewDownloadButton.propTypes = {
isCompiling: PropTypes.bool.isRequired, isCompiling: PropTypes.bool.isRequired,
outputFiles: PropTypes.array, outputFiles: PropTypes.array,
pdfDownloadUrl: PropTypes.string pdfDownloadUrl: PropTypes.string,
showText: PropTypes.bool.isRequired
} }
FileList.propTypes = { FileList.propTypes = {

View file

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames' import classNames from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -7,77 +8,122 @@ import Icon from '../../../shared/components/icon'
function PreviewLogsToggleButton({ function PreviewLogsToggleButton({
onToggle, onToggle,
showLogs, showLogs,
logsState: { nErrors, nWarnings } logsState: { nErrors, nWarnings },
showText
}) { }) {
const toggleButtonClasses = classNames('btn', 'btn-xs', 'btn-toggle-logs', { const { t } = useTranslation()
const toggleButtonClasses = classNames(
'btn',
'btn-xs',
'btn-toggle-logs',
'toolbar-item',
{
'btn-danger': !showLogs && nErrors, 'btn-danger': !showLogs && nErrors,
'btn-warning': !showLogs && !nErrors && nWarnings, 'btn-warning': !showLogs && !nErrors && nWarnings,
'btn-default': showLogs || (!nErrors && !nWarnings) 'btn-default': showLogs || (!nErrors && !nWarnings)
}) }
)
let textStyle = {}
if (!showText) {
textStyle = {
position: 'absolute',
right: '-100vw'
}
}
function handleOnClick(e) { function handleOnClick(e) {
e.currentTarget.blur() e.currentTarget.blur()
onToggle() onToggle()
} }
return ( const buttonElement = (
<button <button
id="logs-toggle"
type="button" type="button"
className={toggleButtonClasses} className={toggleButtonClasses}
onClick={handleOnClick} onClick={handleOnClick}
> >
{showLogs ? ( {showLogs ? (
<ViewPdfButton /> <ViewPdfButton textStyle={textStyle} />
) : ( ) : (
<CompilationResultIndicator nErrors={nErrors} nWarnings={nWarnings} /> <CompilationResultIndicator
textStyle={textStyle}
nErrors={nErrors}
nWarnings={nWarnings}
/>
)} )}
</button> </button>
) )
return showText ? (
buttonElement
) : (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="tooltip-logs-toggle">
{showLogs ? t('view_pdf') : t('view_logs')}
</Tooltip>
}
>
{buttonElement}
</OverlayTrigger>
)
} }
function CompilationResultIndicator({ nErrors, nWarnings }) { function CompilationResultIndicator({ textStyle, nErrors, nWarnings }) {
if (nErrors || nWarnings) { if (nErrors || nWarnings) {
return ( return (
<LogsCompilationResultIndicator <LogsCompilationResultIndicator
logType={nErrors ? 'errors' : 'warnings'} logType={nErrors ? 'errors' : 'warnings'}
nLogs={nErrors || nWarnings} nLogs={nErrors || nWarnings}
textStyle={textStyle}
/> />
) )
} else { } else {
return <ViewLogsButton /> return <ViewLogsButton textStyle={textStyle} />
} }
} }
function LogsCompilationResultIndicator({ logType, nLogs }) { function LogsCompilationResultIndicator({ textStyle, logType, nLogs }) {
const { t } = useTranslation() const { t } = useTranslation()
const label = const label =
logType === 'errors' ? t('your_project_has_errors') : t('view_warnings') logType === 'errors' ? t('your_project_has_errors') : t('view_warnings')
return ( return (
<> <>
<Icon type="file-text-o" /> <Icon type="file-text-o" />
<span className="btn-toggle-logs-label" aria-label={label}> <span
className="btn-toggle-logs-label toolbar-text"
aria-label={label}
style={textStyle}
>
{`${label} (${nLogs > 9 ? '9+' : nLogs})`} {`${label} (${nLogs > 9 ? '9+' : nLogs})`}
</span> </span>
</> </>
) )
} }
function ViewLogsButton() { function ViewLogsButton({ textStyle }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<> <>
<Icon type="file-text-o" /> <Icon type="file-text-o" />
<span className="btn-toggle-logs-label">{t('view_logs')}</span> <span className="toolbar-text" style={textStyle}>
{t('view_logs')}
</span>
</> </>
) )
} }
function ViewPdfButton() { function ViewPdfButton({ textStyle }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<> <>
<Icon type="file-pdf-o" /> <Icon type="file-pdf-o" />
<span className="btn-toggle-logs-label">{t('view_pdf')}</span> <span className="toolbar-text" style={textStyle}>
{t('view_pdf')}
</span>
</> </>
) )
} }
@ -89,12 +135,22 @@ PreviewLogsToggleButton.propTypes = {
nWarnings: PropTypes.number.isRequired, nWarnings: PropTypes.number.isRequired,
nLogEntries: PropTypes.number.isRequired nLogEntries: PropTypes.number.isRequired
}), }),
showLogs: PropTypes.bool.isRequired showLogs: PropTypes.bool.isRequired,
showText: PropTypes.bool.isRequired
} }
LogsCompilationResultIndicator.propTypes = { LogsCompilationResultIndicator.propTypes = {
logType: PropTypes.string.isRequired, logType: PropTypes.string.isRequired,
nLogs: PropTypes.number.isRequired nLogs: PropTypes.number.isRequired,
textStyle: PropTypes.object.isRequired
}
ViewLogsButton.propTypes = {
textStyle: PropTypes.object.isRequired
}
ViewPdfButton.propTypes = {
textStyle: PropTypes.object.isRequired
} }
export default PreviewLogsToggleButton export default PreviewLogsToggleButton

View file

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Dropdown, MenuItem } from 'react-bootstrap' import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
@ -17,7 +17,8 @@ function PreviewRecompileButton({
onRunSyntaxCheckNow, onRunSyntaxCheckNow,
onSetAutoCompile, onSetAutoCompile,
onSetDraftMode, onSetDraftMode,
onSetSyntaxCheck onSetSyntaxCheck,
showText
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
@ -55,18 +56,43 @@ function PreviewRecompileButton({
onSetSyntaxCheck(false) onSetSyntaxCheck(false)
} }
return ( let compilingProps = {}
<Dropdown id="pdf-recompile-dropdown" className="btn-recompile-group"> let recompileProps = {}
function _hideText(keepAria) {
return {
'aria-hidden': !keepAria,
style: {
position: 'absolute',
right: '-100vw'
}
}
}
if (!showText) {
compilingProps = _hideText(isCompiling || isClearingCache)
recompileProps = _hideText(!isCompiling || !isClearingCache)
} else if (isCompiling || isClearingCache) {
recompileProps = _hideText()
} else {
compilingProps = _hideText()
}
const buttonElement = (
<Dropdown
id="pdf-recompile-dropdown"
className="btn-recompile-group toolbar-item"
>
<button className="btn btn-recompile" onClick={onRecompile}> <button className="btn btn-recompile" onClick={onRecompile}>
<Icon type="refresh" spin={isCompiling} /> <Icon type="refresh" spin={isCompiling} />
{isCompiling || isClearingCache ? (
<span className="btn-recompile-label"> <span id="text-compiling" className="toolbar-text" {...compilingProps}>
{t('compiling')} {t('compiling')}
&hellip; &hellip;
</span> </span>
) : (
<span className="btn-recompile-label">{t('recompile')}</span> <span id="text-recompile" className="toolbar-text" {...recompileProps}>
)} {t('recompile')}
</span>
</button> </button>
<Dropdown.Toggle <Dropdown.Toggle
aria-label={t('toggle_compile_options_menu')} aria-label={t('toggle_compile_options_menu')}
@ -115,6 +141,21 @@ function PreviewRecompileButton({
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
) )
return showText ? (
buttonElement
) : (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="tooltip-download-pdf">
{isCompiling || isClearingCache ? t('compiling') : t('recompile')}
</Tooltip>
}
>
{buttonElement}
</OverlayTrigger>
)
} }
PreviewRecompileButton.propTypes = { PreviewRecompileButton.propTypes = {
@ -131,7 +172,8 @@ PreviewRecompileButton.propTypes = {
onRunSyntaxCheckNow: PropTypes.func.isRequired, onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired, onSetAutoCompile: PropTypes.func.isRequired,
onSetDraftMode: PropTypes.func.isRequired, onSetDraftMode: PropTypes.func.isRequired,
onSetSyntaxCheck: PropTypes.func.isRequired onSetSyntaxCheck: PropTypes.func.isRequired,
showText: PropTypes.bool.isRequired
} }
export default PreviewRecompileButton export default PreviewRecompileButton

View file

@ -1,8 +1,14 @@
import React from 'react' import React, { useRef, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PreviewDownloadButton from './preview-download-button' import PreviewDownloadButton from './preview-download-button'
import PreviewRecompileButton from './preview-recompile-button' import PreviewRecompileButton from './preview-recompile-button'
import PreviewLogsToggleButton from './preview-logs-toggle-button' import PreviewLogsToggleButton from './preview-logs-toggle-button'
import useResizeObserver from '../../../shared/hooks/use-resize-observer'
function _getElementWidth(element) {
if (!element) return 0
return Math.ceil(element.getBoundingClientRect().width)
}
function PreviewToolbar({ function PreviewToolbar({
compilerState, compilerState,
@ -18,8 +24,173 @@ function PreviewToolbar({
pdfDownloadUrl, pdfDownloadUrl,
showLogs showLogs
}) { }) {
const showTextRef = useRef(true)
const showToggleTextRef = useRef(true)
const toolbarRef = useRef()
const recompileWidthDifferenceRef = useRef()
const recompileLongerTextRef = useRef()
const [showText, setShowText] = useState(showTextRef.current)
const [showToggleText, setShowToggleText] = useState(
showToggleTextRef.current
)
function checkCanShowText(observedElement) {
// toolbar items can be in 3 states:
// all text, only toggle logs w/text and icons on others, all icons
// states depend on available space in the toolbar
const toolbarWidth =
observedElement &&
observedElement.contentRect &&
observedElement.contentRect.width
if (!toolbarWidth) return
_checkRecompileStateWidths()
let textWidths = 0
let itemsWidth = _getItemsWidth() // could be with or without text
// get widths of text only
// required for _checkToggleText, even if currently showing text
const textElements = toolbarRef.current.querySelectorAll('.toolbar-text')
textElements.forEach(item => {
if (item.getAttribute('aria-hidden') !== 'true') {
textWidths += _getElementWidth(item)
}
})
const logsToggleText = toolbarRef.current.querySelector(
'#logs-toggle .toolbar-text'
)
const logsToggleTextWidth = _getElementWidth(logsToggleText)
if (!showTextRef.current && !showToggleTextRef.current) {
// itemsWidth was calculated without any text shown
itemsWidth += textWidths
} else if (!showTextRef.current && showToggleTextRef.current) {
// itemsWidth was calculated with toggle text but no other text
// only add text width for other items and then
// subtract toggle text width, since it is already in itemsWidth
itemsWidth += parseInt(textWidths - logsToggleTextWidth, 10)
}
// only add extra if recompile button is in state with smaller length
if (
recompileWidthDifferenceRef.current &&
recompileLongerTextRef.current &&
((!compilerState.isCompiling &&
recompileLongerTextRef.current === 'compiling') ||
(compilerState.isCompiling &&
recompileLongerTextRef.current === 'recompile'))
) {
itemsWidth += recompileWidthDifferenceRef.current
}
itemsWidth += 10 // add extra for some spacing between items
let canShowText = itemsWidth < toolbarWidth
if (!canShowText) {
_checkToggleText(
toolbarWidth,
logsToggleTextWidth,
itemsWidth,
textWidths
)
} else if (showToggleTextRef.current !== true) {
setShowToggleText(true)
showToggleTextRef.current = true
}
setShowText(canShowText)
showTextRef.current = canShowText
}
function _checkRecompileStateWidths() {
// check recompile/compiling button text.
// Do not want to hide and then show text when recompiling
if (
recompileWidthDifferenceRef.current ||
recompileWidthDifferenceRef.current === 0
)
return
const textCompiling = toolbarRef.current.querySelector('#text-compiling')
const textRecompile = toolbarRef.current.querySelector('#text-recompile')
const textCompilingWidth = _getElementWidth(textCompiling)
const textRecompileWidth = _getElementWidth(textRecompile)
const textLengthDifference = Math.abs(
parseInt(textCompilingWidth - textRecompileWidth, 10)
)
recompileWidthDifferenceRef.current = textLengthDifference
// ignore if equal
if (textRecompileWidth > textCompilingWidth) {
recompileLongerTextRef.current = 'recompile'
} else if (textRecompileWidth < textCompilingWidth) {
recompileLongerTextRef.current = 'compiling'
}
}
function _checkToggleText(
toolbarWidth,
logsToggleTextWidth,
itemsWithTextWidth,
textWidths
) {
// check to see if we can still show the toggle button text
let toggleWithTextWidth = 0
let toggleWithoutTextWidth = 0
const itemsWithoutTextWidth = parseInt(itemsWithTextWidth - textWidths, 10)
const logsToggle = toolbarRef.current.querySelector('#logs-toggle')
const logsToggleWidth = _getElementWidth(logsToggle)
// logsToggleWidth could be with or without text
if (showToggleTextRef.current) {
toggleWithTextWidth = logsToggleWidth
toggleWithoutTextWidth = parseInt(
logsToggleWidth - logsToggleTextWidth,
10
)
} else {
toggleWithTextWidth = parseInt(logsToggleWidth + logsToggleTextWidth, 10)
toggleWithoutTextWidth = logsToggleWidth
}
const itemsWithoutTextAndToggleWidth = parseInt(
itemsWithoutTextWidth - toggleWithoutTextWidth,
10
)
const itemsWithIconsExceptToggleWidth = parseInt(
itemsWithoutTextAndToggleWidth + toggleWithTextWidth,
10
)
const canShowToggleText = itemsWithIconsExceptToggleWidth < toolbarWidth
if (canShowToggleText !== showToggleTextRef.current) {
setShowToggleText(canShowToggleText)
showToggleTextRef.current = canShowToggleText
}
}
function _getItemsWidth() {
const toolbarItems = toolbarRef.current.querySelectorAll('.toolbar-item')
let itemWidth = 0
toolbarItems.forEach(item => {
itemWidth += _getElementWidth(item)
})
return itemWidth
}
useResizeObserver(toolbarRef, logsState, checkCanShowText)
return ( return (
<div className="toolbar toolbar-pdf"> <div
className="toolbar toolbar-pdf"
id="toolbar-preview"
data-testid="toolbar-preview"
ref={toolbarRef}
>
<div className="toolbar-pdf-left"> <div className="toolbar-pdf-left">
<PreviewRecompileButton <PreviewRecompileButton
compilerState={compilerState} compilerState={compilerState}
@ -29,11 +200,13 @@ function PreviewToolbar({
onSetDraftMode={onSetDraftMode} onSetDraftMode={onSetDraftMode}
onSetSyntaxCheck={onSetSyntaxCheck} onSetSyntaxCheck={onSetSyntaxCheck}
onClearCache={onClearCache} onClearCache={onClearCache}
showText={showText}
/> />
<PreviewDownloadButton <PreviewDownloadButton
isCompiling={compilerState.isCompiling} isCompiling={compilerState.isCompiling}
outputFiles={outputFiles} outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl} pdfDownloadUrl={pdfDownloadUrl}
showText={showText}
/> />
</div> </div>
<div className="toolbar-pdf-right"> <div className="toolbar-pdf-right">
@ -41,6 +214,7 @@ function PreviewToolbar({
logsState={logsState} logsState={logsState}
showLogs={showLogs} showLogs={showLogs}
onToggle={onToggleLogs} onToggle={onToggleLogs}
showText={showToggleText}
/> />
</div> </div>
</div> </div>

View file

@ -0,0 +1,38 @@
import { useLayoutEffect, useRef } from 'react'
function useResizeObserver(observedElement, observedData, callback) {
const resizeObserver = useRef()
function observe() {
resizeObserver.current = new ResizeObserver(function(elementsObserved) {
callback(elementsObserved[0])
})
}
function unobserve(observedCurrent) {
resizeObserver.current.unobserve(observedCurrent)
}
useLayoutEffect(
() => {
if ('ResizeObserver' in window) {
const observedCurrent = observedElement && observedElement.current
if (observedCurrent) {
observe(observedElement.current)
}
if (resizeObserver.current && observedCurrent) {
resizeObserver.current.observe(observedCurrent)
}
return () => {
unobserve(observedCurrent)
}
}
},
[observedElement, observedData]
)
}
export default useResizeObserver

View file

@ -103,6 +103,10 @@
margin-left: @line-height-computed / 4; margin-left: @line-height-computed / 4;
} }
.toolbar-text {
padding-left: @padding-xs;
}
.pdf-viewer { .pdf-viewer {
iframe { iframe {
width: 100%; width: 100%;

View file

@ -19,17 +19,36 @@ describe('<PreviewDownloadButton />', function() {
} }
} }
it('should disable the button and dropdown toggle when compiling', function() { function renderPreviewDownloadButton(
const isCompiling = true isCompiling,
const outputFiles = undefined outputFiles,
pdfDownloadUrl,
showText
) {
if (isCompiling === undefined) isCompiling = false
if (showText === undefined) showText = true
render( render(
<PreviewDownloadButton <PreviewDownloadButton
isCompiling={isCompiling} isCompiling={isCompiling}
outputFiles={outputFiles} outputFiles={outputFiles || []}
pdfDownloadUrl={undefined} pdfDownloadUrl={pdfDownloadUrl}
showText={showText}
/> />
) )
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.exist }
it('should disable the button and dropdown toggle when compiling', function() {
const isCompiling = true
const outputFiles = undefined
renderPreviewDownloadButton(isCompiling, outputFiles)
expect(
screen
.getByText('Download PDF')
.closest('a')
.getAttribute('disabled')
).to.exist
const buttons = screen.getAllByRole('button') const buttons = screen.getAllByRole('button')
expect(buttons.length).to.equal(1) // the dropdown toggle expect(buttons.length).to.equal(1) // the dropdown toggle
expect(buttons[0].getAttribute('disabled')).to.exist expect(buttons[0].getAttribute('disabled')).to.exist
@ -40,41 +59,35 @@ describe('<PreviewDownloadButton />', function() {
it('should disable the PDF button when there is no PDF', function() { it('should disable the PDF button when there is no PDF', function() {
const isCompiling = false const isCompiling = false
const outputFiles = [] const outputFiles = []
render( renderPreviewDownloadButton(isCompiling, outputFiles)
<PreviewDownloadButton expect(
isCompiling={isCompiling} screen
outputFiles={outputFiles} .getByText('Download PDF')
pdfDownloadUrl={undefined} .closest('a')
/> .getAttribute('disabled')
) ).to.exist
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.exist
}) })
it('should enable the PDF button when there is a main PDF', function() { it('should enable the PDF button when there is a main PDF', function() {
const isCompiling = false const isCompiling = false
const outputFiles = [] const outputFiles = []
render( renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
<PreviewDownloadButton expect(
isCompiling={isCompiling} screen
outputFiles={outputFiles} .getByText('Download PDF')
pdfDownloadUrl={pdfDownloadUrl} .closest('a')
/> .getAttribute('href')
) ).to.equal(pdfDownloadUrl)
expect(screen.getByText('Download PDF').getAttribute('href')).to.equal( expect(
pdfDownloadUrl screen
) .getByText('Download PDF')
expect(screen.getByText('Download PDF').getAttribute('disabled')).to.not .closest('a')
.exist .getAttribute('disabled')
).to.not.exist
}) })
it('should enable the dropdown when not compiling', function() { it('should enable the dropdown when not compiling', function() {
const isCompiling = false const isCompiling = false
const outputFiles = [] const outputFiles = []
render( renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
<PreviewDownloadButton
isCompiling={isCompiling}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
const buttons = screen.getAllByRole('button') const buttons = screen.getAllByRole('button')
expect(buttons[0]).to.exist expect(buttons[0]).to.exist
expect(buttons[0].getAttribute('disabled')).to.not.exist expect(buttons[0].getAttribute('disabled')).to.not.exist
@ -93,13 +106,7 @@ describe('<PreviewDownloadButton />', function() {
makeFile('output.blg') makeFile('output.blg')
] ]
render( renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
<PreviewDownloadButton
isCompiling={isCompiling}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
const menuItems = screen.getAllByRole('menuitem') const menuItems = screen.getAllByRole('menuitem')
expect(menuItems.length).to.equal(outputFiles.length - 1) // main PDF is listed separately expect(menuItems.length).to.equal(outputFiles.length - 1) // main PDF is listed separately
@ -138,13 +145,9 @@ describe('<PreviewDownloadButton />', function() {
const pdfFile = makeFile('output.pdf', true) const pdfFile = makeFile('output.pdf', true)
const bblFile = makeFile('output.bbl') const bblFile = makeFile('output.bbl')
const outputFiles = [Object.assign({}, { ...bblFile }), bblFile, pdfFile] const outputFiles = [Object.assign({}, { ...bblFile }), bblFile, pdfFile]
render(
<PreviewDownloadButton renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
isCompiling={isCompiling}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
const bblMenuItems = screen.getAllByText((content, element) => { const bblMenuItems = screen.getAllByText((content, element) => {
return ( return (
content !== '' && element.textContent === 'Download output.bbl file' content !== '' && element.textContent === 'Download output.bbl file'
@ -157,16 +160,23 @@ describe('<PreviewDownloadButton />', function() {
const pdfFile = makeFile('output.pdf', true) const pdfFile = makeFile('output.pdf', true)
const pdfAltFile = makeFile('alt.pdf') const pdfAltFile = makeFile('alt.pdf')
const outputFiles = [pdfFile, pdfAltFile] const outputFiles = [pdfFile, pdfAltFile]
renderPreviewDownloadButton(isCompiling, outputFiles, pdfDownloadUrl)
render(
<PreviewDownloadButton
isCompiling={isCompiling}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
screen.getAllByRole('menuitem', { name: 'Download alt.pdf file' }) screen.getAllByRole('menuitem', { name: 'Download alt.pdf file' })
}) })
it('should show the button text when prop showText=true', function() {
const isCompiling = false
const showText = true
renderPreviewDownloadButton(isCompiling, [], pdfDownloadUrl, showText)
expect(screen.getByText('Download PDF').getAttribute('style')).to.be.null
})
it('should not show the button text when prop showText=false', function() {
const isCompiling = false
const showText = false
renderPreviewDownloadButton(isCompiling, [], pdfDownloadUrl, showText)
expect(screen.getByText('Download PDF').getAttribute('style')).to.equal(
'position: absolute; right: -100vw;'
)
})
describe('list divider and header', function() { describe('list divider and header', function() {
it('should display when there are top files and other files', function() { it('should display when there are top files and other files', function() {
const outputFiles = [ const outputFiles = [
@ -176,13 +186,7 @@ describe('<PreviewDownloadButton />', function() {
makeFile('output.log') makeFile('output.log')
] ]
render( renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
<PreviewDownloadButton
isCompiling={false}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
screen.getByText('Other output files') screen.getByText('Other output files')
screen.getByRole('separator') screen.getByRole('separator')
@ -194,13 +198,7 @@ describe('<PreviewDownloadButton />', function() {
makeFile('output.gls') makeFile('output.gls')
] ]
render( renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
<PreviewDownloadButton
isCompiling={false}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
expect(screen.queryByText('Other output files')).to.not.exist expect(screen.queryByText('Other output files')).to.not.exist
expect(screen.queryByRole('separator')).to.not.exist expect(screen.queryByRole('separator')).to.not.exist
@ -208,13 +206,7 @@ describe('<PreviewDownloadButton />', function() {
it('should not display when there are other files and no top files', function() { it('should not display when there are other files and no top files', function() {
const outputFiles = [makeFile('output.log')] const outputFiles = [makeFile('output.log')]
render( renderPreviewDownloadButton(false, outputFiles, pdfDownloadUrl, true)
<PreviewDownloadButton
isCompiling={false}
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
)
expect(screen.queryByText('Other output files')).to.not.exist expect(screen.queryByText('Other output files')).to.not.exist
expect(screen.queryByRole('separator')).to.not.exist expect(screen.queryByRole('separator')).to.not.exist

View file

@ -1,9 +1,27 @@
import React from 'react' import React from 'react'
import { expect } from 'chai'
import { screen, render } from '@testing-library/react' import { screen, render } from '@testing-library/react'
import PreviewLogsToggleButton from '../../../../../frontend/js/features/preview/components/preview-logs-toggle-button' import PreviewLogsToggleButton from '../../../../../frontend/js/features/preview/components/preview-logs-toggle-button'
describe('<PreviewLogsToggleButton />', function() { describe('<PreviewLogsToggleButton />', function() {
function renderPreviewLogsToggleButton(
logsState,
onToggleLogs,
showLogs,
showText
) {
if (showText === undefined) showText = true
render(
<PreviewLogsToggleButton
logsState={logsState}
onToggle={onToggleLogs}
showLogs={showLogs}
showText={showText}
/>
)
}
describe('basic toggle functionality', function() { describe('basic toggle functionality', function() {
const logsState = { const logsState = {
nErrors: 0, nErrors: 0,
@ -13,24 +31,12 @@ describe('<PreviewLogsToggleButton />', function() {
const onToggleLogs = () => {} const onToggleLogs = () => {}
it('should render a view logs button when previewing the PDF', function() { it('should render a view logs button when previewing the PDF', function() {
const showLogs = false const showLogs = false
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText('View logs') screen.getByText('View logs')
}) })
it('should render a view PDF button when viewing logs', function() { it('should render a view PDF button when viewing logs', function() {
const showLogs = true const showLogs = true
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText('View PDF') screen.getByText('View PDF')
}) })
}) })
@ -43,13 +49,7 @@ describe('<PreviewLogsToggleButton />', function() {
nWarnings: 0, nWarnings: 0,
nLogEntries: 0 nLogEntries: 0
} }
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText('View logs') screen.getByText('View logs')
}) })
@ -59,13 +59,7 @@ describe('<PreviewLogsToggleButton />', function() {
nWarnings: 0, nWarnings: 0,
nLogEntries: 0 nLogEntries: 0
} }
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText(`Your project has errors (${logsState.nErrors})`) screen.getByText(`Your project has errors (${logsState.nErrors})`)
}) })
@ -75,13 +69,7 @@ describe('<PreviewLogsToggleButton />', function() {
nWarnings: 1, nWarnings: 1,
nLogEntries: 0 nLogEntries: 0
} }
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText(`Your project has errors (${logsState.nErrors})`) screen.getByText(`Your project has errors (${logsState.nErrors})`)
}) })
@ -91,13 +79,7 @@ describe('<PreviewLogsToggleButton />', function() {
nWarnings: 1, nWarnings: 1,
nLogEntries: 0 nLogEntries: 0
} }
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText(`View warnings (${logsState.nWarnings})`) screen.getByText(`View warnings (${logsState.nWarnings})`)
}) })
@ -107,14 +89,30 @@ describe('<PreviewLogsToggleButton />', function() {
nWarnings: 0, nWarnings: 0,
nLogEntries: 0 nLogEntries: 0
} }
render( renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs)
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
onToggle={onToggleLogs}
/>
)
screen.getByText('Your project has errors (9+)') screen.getByText('Your project has errors (9+)')
}) })
it('should show the button text when prop showText=true', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
const showText = true
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs, showText)
expect(screen.getByText('View logs').getAttribute('style')).to.be.null
})
it('should not show the button text when prop showText=false', function() {
const logsState = {
nErrors: 0,
nWarnings: 0,
nLogEntries: 0
}
const showText = false
renderPreviewLogsToggleButton(logsState, onToggleLogs, showLogs, showText)
expect(screen.getByText('View logs').getAttribute('style')).to.equal(
'position: absolute; right: -100vw;'
)
})
}) })
}) })

View file

@ -95,10 +95,24 @@ describe('<PreviewRecompileButton />', function() {
}) })
}) })
function renderPreviewRecompileButton(compilerState = {}) { it('should show the button text when prop showText=true', function() {
const showText = true
renderPreviewRecompileButton({}, showText)
expect(screen.getByText('Recompile').getAttribute('style')).to.be.null
})
it('should not show the button text when prop showText=false', function() {
const showText = false
renderPreviewRecompileButton({}, showText)
expect(screen.getByText('Recompile').getAttribute('style')).to.equal(
'position: absolute; right: -100vw;'
)
})
function renderPreviewRecompileButton(compilerState = {}, showText) {
if (!compilerState.logEntries) { if (!compilerState.logEntries) {
compilerState.logEntries = {} compilerState.logEntries = {}
} }
if (showText === undefined) showText = true
render( render(
<PreviewRecompileButton <PreviewRecompileButton
compilerState={{ compilerState={{
@ -115,6 +129,7 @@ describe('<PreviewRecompileButton />', function() {
onSetDraftMode={() => {}} onSetDraftMode={() => {}}
onSetSyntaxCheck={() => {}} onSetSyntaxCheck={() => {}}
onClearCache={onClearCache} onClearCache={onClearCache}
showText={showText}
/> />
) )
} }

View file

@ -0,0 +1,66 @@
import React from 'react'
import sinon from 'sinon'
import { expect } from 'chai'
import { screen, render } from '@testing-library/react'
import PreviewToolbar from '../../../../../frontend/js/features/preview/components/preview-toolbar'
describe('<PreviewToolbar />', function() {
const onClearCache = sinon.stub()
const onRecompile = sinon.stub()
const onRunSyntaxCheckNow = sinon.stub()
const onSetAutoCompile = sinon.stub()
const onSetDraftMode = sinon.stub()
const onSetSyntaxCheck = sinon.stub()
const onToggleLogs = sinon.stub()
function renderPreviewToolbar(compilerState = {}, logState = {}, showLogs) {
render(
<PreviewToolbar
compilerState={{
isAutoCompileOn: true,
isClearingCache: false,
isCompiling: false,
isDraftModeOn: false,
isSyntaxCheckOn: false,
logEntries: {},
...compilerState
}}
logsState={{ nErrors: 0, nWarnings: 0, nLogEntries: 0, ...logState }}
onClearCache={onClearCache}
onRecompile={onRecompile}
onRunSyntaxCheckNow={onRunSyntaxCheckNow}
onSetAutoCompile={onSetAutoCompile}
onSetDraftMode={onSetDraftMode}
onSetSyntaxCheck={onSetSyntaxCheck}
onToggleLogs={onToggleLogs}
outputFiles={[]}
pdfDownloadUrl="/download-pdf-url"
showLogs={showLogs || false}
/>
)
}
it('renders the toolbar', function() {
renderPreviewToolbar()
screen.getByText('Recompile')
screen.getByText('Download PDF')
screen.getByText('View logs')
})
it('all toolbar items have "toolbar-item" class and text has "toolbar-text"', function() {
renderPreviewToolbar()
const toolbar = screen.getByTestId('toolbar-preview')
for (const toolbarSection of toolbar.children) {
for (const toolbarItem of toolbarSection.children) {
expect(toolbarItem.className).to.contain('toolbar-item')
for (const parts of toolbarItem.children) {
for (const part of parts.children) {
if (part.nodeName !== 'LI' && part.textContent) {
expect(part.className).to.contain('toolbar-text')
}
}
}
}
}
})
})