Add first error popup (#3369)

* Add first error popup

* Address PR feedback

GitOrigin-RevId: e924b3e6096584de6f363aae70a62328cd3de83d
This commit is contained in:
Paulo Jorge Reis 2020-11-16 10:01:01 +00:00 committed by Copybot
parent addaa355d0
commit 619ec15309
16 changed files with 494 additions and 87 deletions

View file

@ -7,6 +7,7 @@ div.full-size.pdf(ng-controller="PdfController")
isCompiling: pdf.compiling, isCompiling: pdf.compiling,
isDraftModeOn: draft, isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error, isSyntaxCheckOn: stop_on_validation_error,
lastCompileTimestamp: pdf.lastCompileTimestamp,
logEntries: pdf.logEntries ? pdf.logEntries : {} logEntries: pdf.logEntries ? pdf.logEntries : {}
}` }`
on-clear-cache="clearCache" on-clear-cache="clearCache"
@ -19,7 +20,7 @@ div.full-size.pdf(ng-controller="PdfController")
output-files="pdf.outputFiles" output-files="pdf.outputFiles"
pdf-download-url="pdf.downloadUrl" pdf-download-url="pdf.downloadUrl"
show-logs="shouldShowLogs" show-logs="shouldShowLogs"
on-log-entry-link-click="openInEditor" on-log-entry-location-click="openInEditor"
) )
else else
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }") .toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")

View file

@ -3,12 +3,15 @@
"collapse", "collapse",
"compile_mode", "compile_mode",
"compiling", "compiling",
"dismiss_error_popup",
"download_file", "download_file",
"download_pdf", "download_pdf",
"expand", "expand",
"fast", "fast",
"file_outline", "file_outline",
"find_out_more_about_the_file_outline", "find_out_more_about_the_file_outline",
"first_error_popup_label",
"go_to_error_location",
"hide_outline", "hide_outline",
"ignore_validation_errors", "ignore_validation_errors",
"loading", "loading",
@ -34,6 +37,7 @@
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more", "the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
"toggle_compile_options_menu", "toggle_compile_options_menu",
"toggle_output_files_list", "toggle_output_files_list",
"view_all_errors",
"view_logs", "view_logs",
"view_pdf", "view_pdf",
"view_warnings", "view_warnings",

View file

@ -0,0 +1,63 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import PreviewLogEntry from './preview-log-entry'
function PreviewFirstErrorPopUp({
logEntry,
onGoToErrorLocation,
onViewLogs,
onClose
}) {
const { t } = useTranslation()
function handleGoToErrorLocation() {
const { file, line, column } = logEntry
onGoToErrorLocation({ file, line, column })
}
return (
<div
className="first-error-popup"
role="alertdialog"
aria-label={t('first_error_popup_label')}
>
<PreviewLogEntry
{...logEntry}
showLineAndNoLink={false}
showCloseButton
onClose={onClose}
/>
<div className="first-error-popup-actions">
<button
className="btn btn-info btn-xs first-error-btn"
type="button"
onClick={handleGoToErrorLocation}
>
<Icon type="chain" />
&nbsp;
{t('go_to_error_location')}
</button>
<button
className="btn btn-info btn-xs first-error-btn"
type="button"
onClick={onViewLogs}
>
<Icon type="file-text-o" />
&nbsp;
{t('view_all_errors')}
</button>
</div>
</div>
)
}
PreviewFirstErrorPopUp.propTypes = {
logEntry: PropTypes.object.isRequired,
onGoToErrorLocation: PropTypes.func.isRequired,
onViewLogs: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
}
export default PreviewFirstErrorPopUp

View file

@ -14,11 +14,14 @@ function PreviewLogEntry({
humanReadableHintComponent, humanReadableHintComponent,
extraInfoURL, extraInfoURL,
level, level,
onLogEntryLinkClick showLineAndNoLink = true,
showCloseButton = false,
onLogEntryLocationClick,
onClose
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
function handleLogEntryLinkClick() { function handleLogEntryLinkClick() {
onLogEntryLinkClick({ file, line, column }) onLogEntryLocationClick({ file, line, column })
} }
const logEntryDescription = t('log_entry_description', { const logEntryDescription = t('log_entry_description', {
level: level level: level
@ -30,7 +33,10 @@ function PreviewLogEntry({
file={file} file={file}
line={line} line={line}
message={message} message={message}
onLogEntryLinkClick={handleLogEntryLinkClick} showLineAndNoLink={showLineAndNoLink}
onLogEntryLocationClick={handleLogEntryLinkClick}
showCloseButton={showCloseButton}
onClose={onClose}
/> />
{content ? ( {content ? (
<PreviewLogEntryContent <PreviewLogEntryContent
@ -48,7 +54,10 @@ function PreviewLogEntryHeader({
file, file,
line, line,
message, message,
onLogEntryLinkClick showLineAndNoLink = true,
showCloseButton = false,
onLogEntryLocationClick,
onClose
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const logEntryHeaderClasses = classNames('log-entry-header', { const logEntryHeaderClasses = classNames('log-entry-header', {
@ -56,18 +65,19 @@ function PreviewLogEntryHeader({
'log-entry-header-warning': level === 'warning', 'log-entry-header-warning': level === 'warning',
'log-entry-header-typesetting': level === 'typesetting' 'log-entry-header-typesetting': level === 'typesetting'
}) })
const headerLinkBtnTitle = t('navigate_log_source', { const headerLogLocationTitle = t('navigate_log_source', {
location: file + (line ? `, ${line}` : '') location: file + (line ? `, ${line}` : '')
}) })
return ( return (
<header className={logEntryHeaderClasses}> <header className={logEntryHeaderClasses}>
<h3 className="log-entry-header-title">{message}</h3> <h3 className="log-entry-header-title">{message}</h3>
{file ? ( {showLineAndNoLink && file ? (
<button <button
className="btn-inline-link log-entry-header-link" className="btn-inline-link log-entry-header-link"
type="button" type="button"
title={headerLinkBtnTitle} title={headerLogLocationTitle}
onClick={onLogEntryLinkClick} onClick={onLogEntryLocationClick}
> >
<Icon type="chain" /> <Icon type="chain" />
&nbsp; &nbsp;
@ -75,6 +85,16 @@ function PreviewLogEntryHeader({
{line ? <span>, {line}</span> : null} {line ? <span>, {line}</span> : null}
</button> </button>
) : null} ) : null}
{showCloseButton && file ? (
<button
className="btn-inline-link log-entry-header-link"
type="button"
aria-label={t('dismiss_error_popup')}
onClick={onClose}
>
<span aria-hidden="true">&times;</span>
</button>
) : null}
</header> </header>
) )
} }
@ -148,7 +168,10 @@ PreviewLogEntryHeader.propTypes = {
file: PropTypes.string, file: PropTypes.string,
line: PropTypes.any, line: PropTypes.any,
message: PropTypes.string, message: PropTypes.string,
onLogEntryLinkClick: PropTypes.func.isRequired showLineAndNoLink: PropTypes.bool,
showCloseButton: PropTypes.bool,
onLogEntryLocationClick: PropTypes.func,
onClose: PropTypes.func
} }
PreviewLogEntryContent.propTypes = { PreviewLogEntryContent.propTypes = {
@ -172,7 +195,10 @@ PreviewLogEntry.propTypes = {
humanReadableHintComponent: PropTypes.node, humanReadableHintComponent: PropTypes.node,
extraInfoURL: PropTypes.string, extraInfoURL: PropTypes.string,
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired, level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
onLogEntryLinkClick: PropTypes.func.isRequired showLineAndNoLink: PropTypes.bool,
showCloseButton: PropTypes.bool,
onLogEntryLocationClick: PropTypes.func,
onClose: PropTypes.func
} }
export default PreviewLogEntry export default PreviewLogEntry

View file

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PreviewLogEntry from './preview-log-entry' import PreviewLogEntry from './preview-log-entry'
function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) { function PreviewLogsPane({ logEntries, onLogEntryLocationClick }) {
return ( return (
<div className="logs-pane"> <div className="logs-pane">
{logEntries && logEntries.length > 0 ? ( {logEntries && logEntries.length > 0 ? (
@ -10,7 +10,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
<PreviewLogEntry <PreviewLogEntry
key={idx} key={idx}
{...logEntry} {...logEntry}
onLogEntryLinkClick={onLogEntryLinkClick} onLogEntryLocationClick={onLogEntryLocationClick}
/> />
)) ))
) : ( ) : (
@ -22,7 +22,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
PreviewLogsPane.propTypes = { PreviewLogsPane.propTypes = {
logEntries: PropTypes.array, logEntries: PropTypes.array,
onLogEntryLinkClick: PropTypes.func.isRequired onLogEntryLocationClick: PropTypes.func.isRequired
} }
export default PreviewLogsPane export default PreviewLogsPane

View file

@ -1,7 +1,8 @@
import React from 'react' import React, { useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PreviewToolbar from './preview-toolbar' import PreviewToolbar from './preview-toolbar'
import PreviewLogsPane from './preview-logs-pane' import PreviewLogsPane from './preview-logs-pane'
import PreviewFirstErrorPopUp from './preview-first-error-pop-up'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
function PreviewPane({ function PreviewPane({
@ -15,10 +16,30 @@ function PreviewPane({
onToggleLogs, onToggleLogs,
outputFiles, outputFiles,
pdfDownloadUrl, pdfDownloadUrl,
onLogEntryLinkClick, onLogEntryLocationClick,
showLogs showLogs
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const [lastCompileTimestamp, setLastCompileTimestamp] = useState(
compilerState.lastCompileTimestamp
)
const [seenLogsForCurrentCompile, setSeenLogsForCurrentCompile] = useState(
false
)
const [dismissedFirstErrorPopUp, setDismissedFirstErrorPopUp] = useState(
false
)
if (lastCompileTimestamp < compilerState.lastCompileTimestamp) {
setLastCompileTimestamp(compilerState.lastCompileTimestamp)
setSeenLogsForCurrentCompile(false)
}
if (showLogs && !seenLogsForCurrentCompile) {
setSeenLogsForCurrentCompile(true)
}
const nErrors = const nErrors =
compilerState.logEntries && compilerState.logEntries.errors compilerState.logEntries && compilerState.logEntries.errors
? compilerState.logEntries.errors.length ? compilerState.logEntries.errors.length
@ -32,6 +53,16 @@ function PreviewPane({
? compilerState.logEntries.all.length ? compilerState.logEntries.all.length
: 0 : 0
const showFirstErrorPopUp =
nErrors > 0 &&
!seenLogsForCurrentCompile &&
!dismissedFirstErrorPopUp &&
!compilerState.isCompiling
function handleFirstErrorPopUpClose() {
setDismissedFirstErrorPopUp(true)
}
return ( return (
<> <>
<PreviewToolbar <PreviewToolbar
@ -58,10 +89,18 @@ function PreviewPane({
? t('n_warnings', { count: nWarnings }) ? t('n_warnings', { count: nWarnings })
: ''} : ''}
</span> </span>
{showFirstErrorPopUp ? (
<PreviewFirstErrorPopUp
logEntry={compilerState.logEntries.errors[0]}
onGoToErrorLocation={onLogEntryLocationClick}
onViewLogs={onToggleLogs}
onClose={handleFirstErrorPopUpClose}
/>
) : null}
{showLogs ? ( {showLogs ? (
<PreviewLogsPane <PreviewLogsPane
logEntries={compilerState.logEntries.all} logEntries={compilerState.logEntries.all}
onLogEntryLinkClick={onLogEntryLinkClick} onLogEntryLocationClick={onLogEntryLocationClick}
/> />
) : null} ) : null}
</> </>
@ -74,10 +113,11 @@ PreviewPane.propTypes = {
isCompiling: PropTypes.bool.isRequired, isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired, isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired, isSyntaxCheckOn: PropTypes.bool.isRequired,
lastCompileTimestamp: PropTypes.number,
logEntries: PropTypes.object.isRequired logEntries: PropTypes.object.isRequired
}), }),
onClearCache: PropTypes.func.isRequired, onClearCache: PropTypes.func.isRequired,
onLogEntryLinkClick: PropTypes.func.isRequired, onLogEntryLocationClick: PropTypes.func.isRequired,
onRecompile: PropTypes.func.isRequired, onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired, onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired, onSetAutoCompile: PropTypes.func.isRequired,

View file

@ -31,7 +31,8 @@ export default (PdfManager = class PdfManager {
view: null, // 'pdf' 'logs' view: null, // 'pdf' 'logs'
showRawLog: false, showRawLog: false,
highlights: [], highlights: [],
position: null position: null,
lastCompileTimestamp: null
} }
} }
}) })

View file

@ -361,6 +361,7 @@ App.controller('PdfController', function(
if (response.status === 'success') { if (response.status === 'success') {
$scope.pdf.view = 'pdf' $scope.pdf.view = 'pdf'
$scope.shouldShowLogs = false $scope.shouldShowLogs = false
$scope.pdf.lastCompileTimestamp = Date.now()
// define the base url. if the pdf file has a build number, pass it to the clsi in the url // define the base url. if the pdf file has a build number, pass it to the clsi in the url
if (fileByPath['output.pdf'] && fileByPath['output.pdf'].url) { if (fileByPath['output.pdf'] && fileByPath['output.pdf'].url) {
@ -516,6 +517,8 @@ App.controller('PdfController', function(
function fetchLogs(fileByPath, options) { function fetchLogs(fileByPath, options) {
let blgFile, chktexFile, logFile let blgFile, chktexFile, logFile
$scope.pdf.logEntries = {}
if (options != null ? options.validation : undefined) { if (options != null ? options.validation : undefined) {
chktexFile = fileByPath['output.chktex'] chktexFile = fileByPath['output.chktex']
} else { } else {

View file

@ -111,6 +111,23 @@
} }
} }
@keyframes pulse {
0% {
opacity: 0.7;
}
100% {
opacity: 0.9;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.bounce { .bounce {
-webkit-animation-duration: 2s; -webkit-animation-duration: 2s;
animation-duration: 2s; animation-duration: 2s;

View file

@ -118,10 +118,7 @@
.log-entry-btn-expand-collapse { .log-entry-btn-expand-collapse {
position: relative; position: relative;
z-index: 1; z-index: 1;
&:focus, .no-outline-ring-on-click;
&:focus:active {
outline: 0;
}
} }
.log-entry-human-readable-hint, .log-entry-human-readable-hint,
@ -129,3 +126,33 @@
font-size: @font-size-small; font-size: @font-size-small;
margin-top: @margin-sm; margin-top: @margin-sm;
} }
.first-error-popup {
position: absolute;
z-index: 1;
top: @toolbar-small-height + 2px;
right: @padding-xs;
border-radius: @border-radius-base;
width: 90%;
max-width: 450px;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
animation: fade-in 0.15s linear 0s 1 none;
background-color: #fff;
&::before {
content: '';
.triangle(top, @padding-sm, @padding-xs, @ol-red);
top: -@padding-xs;
right: @padding-md;
}
}
.first-error-popup-actions {
display: flex;
justify-content: space-between;
padding: 0 @padding-sm @padding-sm @padding-sm;
margin-top: -@margin-sm;
}
.first-error-btn {
.no-outline-ring-on-click;
}

View file

@ -64,38 +64,6 @@
} }
} }
.triangle(@_, @width, @height, @color) {
position: absolute;
border-color: transparent;
border-style: solid;
width: 0;
height: 0;
}
.triangle(top, @width, @height, @color) {
border-width: 0 @width / 2 @height @width / 2;
border-bottom-color: @color;
border-left-color: transparent;
border-right-color: transparent;
}
.triangle(bottom, @width, @height, @color) {
border-width: @height @width / 2 0 @width / 2;
border-top-color: @color;
border-left-color: transparent;
border-right-color: transparent;
}
.triangle(right, @width, @height, @color) {
border-width: @height / 2 0 @height / 2 @width;
border-left-color: @color;
border-top-color: transparent;
border-bottom-color: transparent;
}
.triangle(left, @width, @height, @color) {
border-width: @height / 2 @width @height / 2 0;
border-right-color: @color;
border-top-color: transparent;
border-bottom-color: transparent;
}
#review-panel { #review-panel {
display: block; display: block;
.rp-size-expanded & { .rp-size-expanded & {

View file

@ -1,22 +1,5 @@
@import './list/v1-import-modal.less'; @import './list/v1-import-modal.less';
@keyframes pulse {
0% {
opacity: 0.7;
}
100% {
opacity: 0.9;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.project-list-page { .project-list-page {
position: absolute; position: absolute;
top: @header-height; top: @header-height;

View file

@ -1095,3 +1095,42 @@
height: auto; height: auto;
} }
} }
.triangle(@_, @width, @height, @color) {
position: absolute;
border-color: transparent;
border-style: solid;
width: 0;
height: 0;
}
.triangle(top, @width, @height, @color) {
border-width: 0 @width / 2 @height @width / 2;
border-bottom-color: @color;
border-left-color: transparent;
border-right-color: transparent;
}
.triangle(bottom, @width, @height, @color) {
border-width: @height @width / 2 0 @width / 2;
border-top-color: @color;
border-left-color: transparent;
border-right-color: transparent;
}
.triangle(right, @width, @height, @color) {
border-width: @height / 2 0 @height / 2 @width;
border-left-color: @color;
border-top-color: transparent;
border-bottom-color: transparent;
}
.triangle(left, @width, @height, @color) {
border-width: @height / 2 @width @height / 2 0;
border-right-color: @color;
border-top-color: transparent;
border-bottom-color: transparent;
}
.no-outline-ring-on-click {
&:focus,
&:focus:active {
outline: 0;
}
}

View file

@ -1,4 +1,8 @@
{ {
"first_error_popup_label": "Your project has errors. This is the first one.",
"dismiss_error_popup": "Dismiss first error alert",
"go_to_error_location": "Go to error location",
"view_all_errors": "View all errors",
"log_entry_description": "Log entry with level: \"__level__\"", "log_entry_description": "Log entry with level: \"__level__\"",
"navigate_log_source": "Navigate to log position in source code: __location__", "navigate_log_source": "Navigate to log position in source code: __location__",
"other_output_files": "Other output files", "other_output_files": "Other output files",

View file

@ -12,7 +12,7 @@ describe('<PreviewLogEntry />', function() {
describe('log entry description', function() { describe('log entry description', function() {
for (const level of ['error', 'warning', 'typesetting']) { for (const level of ['error', 'warning', 'typesetting']) {
it(`describes the log entry with ${level} information`, function() { it(`describes the log entry with ${level} information`, function() {
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />) render(<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />)
screen.getByLabelText(`Log entry with level: "${level}"`) screen.getByLabelText(`Log entry with level: "${level}"`)
}) })
} }
@ -22,10 +22,10 @@ describe('<PreviewLogEntry />', function() {
const file = 'foo.tex' const file = 'foo.tex'
const line = 42 const line = 42
const column = 21 const column = 21
const onLogEntryLinkClick = sinon.stub() const onLogEntryLocationClick = sinon.stub()
afterEach(function() { afterEach(function() {
onLogEntryLinkClick.reset() onLogEntryLocationClick.reset()
}) })
it('renders both file and line', function() { it('renders both file and line', function() {
@ -34,7 +34,7 @@ describe('<PreviewLogEntry />', function() {
file={file} file={file}
line={line} line={line}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
screen.getByRole('button', { screen.getByRole('button', {
@ -44,7 +44,11 @@ describe('<PreviewLogEntry />', function() {
it('renders only file when line information is not available', function() { it('renders only file when line information is not available', function() {
render( render(
<PreviewLogEntry file={file} level={level} onLogEntryLinkClick={noOp} /> <PreviewLogEntry
file={file}
level={level}
onLogEntryLocationClick={noOp}
/>
) )
screen.getByRole('button', { screen.getByRole('button', {
name: `Navigate to log position in source code: ${file}` name: `Navigate to log position in source code: ${file}`
@ -52,7 +56,7 @@ describe('<PreviewLogEntry />', function() {
}) })
it('does not render when file information is not available', function() { it('does not render when file information is not available', function() {
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />) render(<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />)
expect( expect(
screen.queryByRole('button', { screen.queryByRole('button', {
name: `Navigate to log position in source code: ` name: `Navigate to log position in source code: `
@ -67,7 +71,7 @@ describe('<PreviewLogEntry />', function() {
line={line} line={line}
column={column} column={column}
level={level} level={level}
onLogEntryLinkClick={onLogEntryLinkClick} onLogEntryLocationClick={onLogEntryLocationClick}
/> />
) )
const linkToSourceButton = screen.getByRole('button', { const linkToSourceButton = screen.getByRole('button', {
@ -75,8 +79,8 @@ describe('<PreviewLogEntry />', function() {
}) })
fireEvent.click(linkToSourceButton) fireEvent.click(linkToSourceButton)
expect(onLogEntryLinkClick).to.be.calledOnce expect(onLogEntryLocationClick).to.be.calledOnce
expect(onLogEntryLinkClick).to.be.calledWith({ expect(onLogEntryLocationClick).to.be.calledWith({
file, file,
line: line, line: line,
column: column column: column
@ -92,7 +96,7 @@ describe('<PreviewLogEntry />', function() {
<PreviewLogEntry <PreviewLogEntry
content={logContent} content={logContent}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
screen.getByText(logContent) screen.getByText(logContent)
@ -106,7 +110,7 @@ describe('<PreviewLogEntry />', function() {
<PreviewLogEntry <PreviewLogEntry
content={logContent} content={logContent}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
screen.getByText(logContent) screen.getByText(logContent)
@ -121,7 +125,7 @@ describe('<PreviewLogEntry />', function() {
it('should not render at all when there are no log contents', function() { it('should not render at all when there are no log contents', function() {
const { container } = render( const { container } = render(
<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} /> <PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />
) )
expect(container.querySelector('.log-entry-content')).to.not.exist expect(container.querySelector('.log-entry-content')).to.not.exist
}) })
@ -140,7 +144,7 @@ describe('<PreviewLogEntry />', function() {
humanReadableHintComponent={logHint} humanReadableHintComponent={logHint}
extraInfoURL={infoURL} extraInfoURL={infoURL}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
screen.getByText(logHintText) screen.getByText(logHintText)
@ -153,7 +157,7 @@ describe('<PreviewLogEntry />', function() {
humanReadableHintComponent={logHint} humanReadableHintComponent={logHint}
extraInfoURL={infoURL} extraInfoURL={infoURL}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
screen.getByRole('link', { name: 'Learn more' }) screen.getByRole('link', { name: 'Learn more' })
@ -165,7 +169,7 @@ describe('<PreviewLogEntry />', function() {
content={logContent} content={logContent}
humanReadableHintComponent={logHint} humanReadableHintComponent={logHint}
level={level} level={level}
onLogEntryLinkClick={noOp} onLogEntryLocationClick={noOp}
/> />
) )
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist

View file

@ -0,0 +1,227 @@
import React from 'react'
import { screen, render, fireEvent } from '@testing-library/react'
import PreviewPane from '../../../../../frontend/js/features/preview/components/preview-pane'
const { expect } = require('chai')
describe('<PreviewPane />', function() {
const sampleError1 = {
content: 'error 1 content',
file: 'main.tex',
level: 'error',
line: 17,
message: 'Misplaced alignment tab character &.'
}
const sampleError2 = {
content: 'error 1 content',
file: 'main.tex',
level: 'error',
line: 22,
message: 'Extra alignment tab has been changed to cr.'
}
const sampleWarning = {
file: 'main.tex',
level: 'warning',
line: 30,
message: "Reference `idontexist' on page 1 undefined on input line 30."
}
describe('first error pop-up', function() {
it('renders a first error pop-up with the first error', function() {
const propsAfterCompileWithErrors = getProps(false, {
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
})
render(<PreviewPane {...propsAfterCompileWithErrors} />)
screen.getByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
screen.getByText(sampleError1.message)
})
it('does not render a first error pop-up when there are only warnings', function() {
const propsAfterCompileWithWarningsOnly = getProps(false, {
errors: [],
warnings: [sampleWarning]
})
render(<PreviewPane {...propsAfterCompileWithWarningsOnly} />)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
it('does not render a first error pop-up when a compile is ongoing', function() {
const propsWhileCompiling = getProps(true, {
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
})
render(<PreviewPane {...propsWhileCompiling} />)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
it('does not render a first error pop-up when viewing logs', function() {
const propsWithErrorsViewingLogs = getProps(
false,
{
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
},
Date.now(),
true
)
render(<PreviewPane {...propsWithErrorsViewingLogs} />)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
it('does not render a first error pop-up when going back to the PDF view after viewing logs', function() {
const nowTimestamp = Date.now()
const propsWithErrorsViewingLogs = getProps(
false,
{
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
},
nowTimestamp,
true
)
const propsWithErrorsAfterViewingLogs = getProps(
false,
{
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
},
nowTimestamp,
false
)
const { rerender } = render(
<PreviewPane {...propsWithErrorsViewingLogs} />
)
rerender(<PreviewPane {...propsWithErrorsAfterViewingLogs} />)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
it('renders a first error pop-up with updated errors after recompiling', function() {
const nowTimestamp = Date.now()
const laterTimestamp = Date.now() + 1000
const propsWithErrorsAfterFirstCompile = getProps(
false,
{
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
},
nowTimestamp,
true
)
const propsWithErrorsAfterSecondCompile = getProps(
false,
{
errors: [sampleError2],
warnings: [sampleWarning]
},
laterTimestamp,
false
)
const { rerender } = render(
<PreviewPane {...propsWithErrorsAfterFirstCompile} />
)
rerender(<PreviewPane {...propsWithErrorsAfterSecondCompile} />)
screen.getByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
screen.getByText(sampleError2.message)
})
it('allows dismissing the first error pop-up', function() {
const propsWithErrors = getProps(false, {
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
})
render(<PreviewPane {...propsWithErrors} />)
const dismissPopUpButton = screen.getByRole('button', {
name: 'Dismiss first error alert'
})
fireEvent.click(dismissPopUpButton)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
it('does not render the first error pop-up with new recompiles after it being dismissed once', function() {
const nowTimestamp = Date.now()
const laterTimestamp = Date.now() + 1000
const propsWithErrorsForFirstCompile = getProps(
false,
{
errors: [sampleError1, sampleError2],
warnings: [sampleWarning]
},
nowTimestamp
)
const propsWithErrorsForSecondCompile = getProps(
false,
{
errors: [sampleError2],
warnings: [sampleWarning]
},
laterTimestamp
)
const { rerender } = render(
<PreviewPane {...propsWithErrorsForFirstCompile} />
)
const dismissPopUpButton = screen.getByRole('button', {
name: 'Dismiss first error alert'
})
fireEvent.click(dismissPopUpButton)
rerender(<PreviewPane {...propsWithErrorsForSecondCompile} />)
expect(
screen.queryByRole('alertdialog', {
name: 'Your project has errors. This is the first one.'
})
).to.not.exist
})
})
function getProps(
isCompiling = false,
logEntries = {},
lastCompileTimestamp = Date.now(),
isShowingLogs = false
) {
return {
compilerState: {
isAutoCompileOn: false,
isCompiling: isCompiling,
isClearingCache: false,
isDraftModeOn: false,
isSyntaxCheckOn: false,
lastCompileTimestamp: lastCompileTimestamp,
logEntries: logEntries
},
onClearCache: () => {},
onLogEntryLocationClick: () => {},
onRecompile: () => {},
onRunSyntaxCheckNow: () => {},
onSetAutoCompile: () => {},
onSetDraftMode: () => {},
onSetSyntaxCheck: () => {},
onToggleLogs: () => {},
showLogs: isShowingLogs
}
}
})