Add error and validation issues (#3400)

* Remove references to the duplicatePaths validation

* Make the log entries more generic, to support validation and CLSI errors

* Add validation issues to the new logs UI

* Add CLSI errors to the new logs UI

* Update tests; accessibility fixes

* Disable PDF viewing when compile fails; address PR feedback.

* Add accessible description for error and validation failed compiles

GitOrigin-RevId: 8b0597af8857712d47c20e4915470e8e745bb315
This commit is contained in:
Paulo Jorge Reis 2020-11-26 09:58:42 +00:00 committed by Copybot
parent 64e19085b4
commit 081f4212a8
20 changed files with 771 additions and 342 deletions

View file

@ -8,8 +8,11 @@ div.full-size.pdf(ng-controller="PdfController")
isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error,
lastCompileTimestamp: pdf.lastCompileTimestamp,
logEntries: pdf.logEntries ? pdf.logEntries : {},
rawLog: pdf.rawLog ? pdf.rawLog : ''
logEntries: pdf.logEntries,
validationIssues: pdf.validation,
errors: clsiErrors,
rawLog: pdf.rawLog,
compileFailed: pdf.compileFailed
}`
on-clear-cache="clearCache"
on-recompile="recompile"
@ -269,115 +272,111 @@ div.full-size.pdf(ng-controller="PdfController")
ng-if="settings.pdfViewer == 'native'"
)
.pdf-validation-problems(ng-switch-when="validation-problems")
if !showNewLogsUI
.pdf-validation-problems(ng-switch-when="validation-problems")
.alert.alert-danger(ng-show="pdf.validation.duplicatePaths")
strong #{translate("latex_error")}
span #{translate("duplicate_paths_found")}
.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
strong #{translate("project_too_large")}
div #{translate("project_too_large_please_reduce")}
div
li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
div
strong #{translate("conflicting_paths_found")}
div !{translate("following_paths_conflict")}
div
li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
.alert.alert-danger(ng-show="pdf.validation.mainFile")
strong #{translate("main_file_not_found")}
span #{translate("please_set_main_file")}
.pdf-errors(ng-switch-when="errors")
.alert.alert-danger(ng-show="pdf.error")
strong #{translate("server_error")}
span #{translate("somthing_went_wrong_compiling")}
.alert.alert-danger(ng-show="pdf.renderingError")
strong #{translate("pdf_rendering_error")}
span #{translate("something_went_wrong_rendering_pdf")}
.alert.alert-danger(ng-show="pdf.clsiMaintenance")
strong #{translate("server_error")}
span #{translate("clsi_maintenance")}
.alert.alert-danger(ng-show="pdf.clsiUnavailable")
strong #{translate("server_error")}
span #{translate("clsi_unavailable")}
.alert.alert-danger(ng-show="pdf.tooRecentlyCompiled")
strong #{translate("server_error")}
span #{translate("too_recently_compiled")}
.alert.alert-danger(ng-show="pdf.compileTerminated")
strong #{translate("terminated")}.
span #{translate("compile_terminated_by_user")}
.alert.alert-danger(ng-show="pdf.rateLimited")
strong #{translate("pdf_compile_rate_limit_hit")}
span #{translate("project_flagged_too_many_compiles")}
.alert.alert-danger(ng-show="pdf.compileInProgress")
strong #{translate("pdf_compile_in_progress_error")}.
span #{translate("pdf_compile_try_again")}
.alert.alert-danger(ng-show="pdf.timedout")
p
strong #{translate("timedout")}.
span #{translate("proj_timed_out_reason")}
p
a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank")
| #{translate("learn_how_to_make_documents_compile_quickly")}
if settings.enableSubscriptions
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
p(ng-if="project.owner._id == user.id")
strong #{translate("upgrade_for_longer_compiles")}
p(ng-if="project.owner._id != user.id")
strong #{translate("ask_proj_owner_to_upgrade_for_longer_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p #{translate("plus_upgraded_accounts_receive")}:
.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
strong #{translate("project_too_large")}
div #{translate("project_too_large_please_reduce")}
div
ul.list-unstyled
li
i.fa.fa-check  
| #{translate("unlimited_projects")}
li
i.fa.fa-check  
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check  
| #{translate("full_doc_history")}
li
i.fa.fa-check  
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check  
| #{translate("sync_to_github")}
li
i.fa.fa-check  
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
div
strong #{translate("conflicting_paths_found")}
div !{translate("following_paths_conflict")}
div
li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
.alert.alert-danger(ng-show="pdf.autoCompileDisabled")
p
strong #{translate("autocompile_disabled")}.
span #{translate("autocompile_disabled_reason")}
.alert.alert-danger(ng-show="pdf.validation.mainFile")
strong #{translate("main_file_not_found")}
span #{translate("please_set_main_file")}
.alert.alert-danger(ng-show="pdf.projectTooLarge")
strong #{translate("project_too_large")}
span #{translate("project_too_large_please_reduce")}
.pdf-errors(ng-switch-when="errors")
.alert.alert-danger(ng-show="pdf.error")
strong #{translate("server_error")}
span #{translate("somthing_went_wrong_compiling")}
.alert.alert-danger(ng-show="pdf.renderingError")
strong #{translate("pdf_rendering_error")}
span #{translate("something_went_wrong_rendering_pdf")}
.alert.alert-danger(ng-show="pdf.clsiMaintenance")
strong #{translate("server_error")}
span #{translate("clsi_maintenance")}
.alert.alert-danger(ng-show="pdf.clsiUnavailable")
strong #{translate("server_error")}
span #{translate("clsi_unavailable")}
.alert.alert-danger(ng-show="pdf.tooRecentlyCompiled")
strong #{translate("server_error")}
span #{translate("too_recently_compiled")}
.alert.alert-danger(ng-show="pdf.compileTerminated")
strong #{translate("terminated")}.
span #{translate("compile_terminated_by_user")}
.alert.alert-danger(ng-show="pdf.rateLimited")
strong #{translate("pdf_compile_rate_limit_hit")}
span #{translate("project_flagged_too_many_compiles")}
.alert.alert-danger(ng-show="pdf.compileInProgress")
strong #{translate("pdf_compile_in_progress_error")}.
span #{translate("pdf_compile_try_again")}
.alert.alert-danger(ng-show="pdf.timedout")
p
strong #{translate("timedout")}.
span #{translate("proj_timed_out_reason")}
p
a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank")
| #{translate("learn_how_to_make_documents_compile_quickly")}
if settings.enableSubscriptions
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
p(ng-if="project.owner._id == user.id")
strong #{translate("upgrade_for_longer_compiles")}
p(ng-if="project.owner._id != user.id")
strong #{translate("ask_proj_owner_to_upgrade_for_longer_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p #{translate("plus_upgraded_accounts_receive")}:
div
ul.list-unstyled
li
i.fa.fa-check  
| #{translate("unlimited_projects")}
li
i.fa.fa-check  
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check  
| #{translate("full_doc_history")}
li
i.fa.fa-check  
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check  
| #{translate("sync_to_github")}
li
i.fa.fa-check  
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
.alert.alert-danger(ng-show="pdf.autoCompileDisabled")
p
strong #{translate("autocompile_disabled")}.
span #{translate("autocompile_disabled_reason")}
.alert.alert-danger(ng-show="pdf.projectTooLarge")
strong #{translate("project_too_large")}
span #{translate("project_too_large_please_reduce")}
script(type='text/ng-template', id='clearCacheModalTemplate')
@ -398,3 +397,6 @@ script(type='text/ng-template', id='clearCacheModalTemplate')
)
span(ng-show="!state.inflight") #{translate("clear_cache")}
span(ng-show="state.inflight") #{translate("clearing")}…
script.
window.showNewLogsUI = #{showNewLogsUI}

View file

@ -1,22 +1,35 @@
[
"auto_compile",
"autocompile_disabled_reason",
"autocompile_disabled",
"clsi_maintenance",
"clsi_unavailable",
"collapse",
"compile_error_description",
"compile_error_entry_description",
"compile_mode",
"compile_terminated_by_user",
"compiling",
"conflicting_paths_found",
"dismiss_error_popup",
"download_file",
"download_pdf",
"duplicate_paths_found",
"expand",
"fast",
"file_outline",
"find_out_more_about_the_file_outline",
"first_error_popup_label",
"following_paths_conflict",
"go_to_error_location",
"hide_outline",
"ignore_validation_errors",
"latex_error",
"learn_how_to_make_documents_compile_quickly",
"loading",
"log_entry_description",
"log_hint_extra_info",
"main_file_not_found",
"n_errors_plural",
"n_errors",
"n_warnings_plural",
@ -27,18 +40,34 @@
"off",
"on",
"other_output_files",
"pdf_compile_in_progress_error",
"pdf_compile_rate_limit_hit",
"pdf_compile_try_again",
"pdf_rendering_error",
"please_set_main_file",
"proj_timed_out_reason",
"project_flagged_too_many_compiles",
"project_too_large_please_reduce",
"project_too_large",
"raw_logs_description",
"raw_logs",
"recompile_from_scratch",
"recompile",
"run_syntax_check_now",
"run_syntax_check_now",
"send_first_message",
"server_error",
"show_outline",
"something_went_wrong_rendering_pdf",
"somthing_went_wrong_compiling",
"stop_on_validation_error",
"terminated",
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
"timedout",
"toggle_compile_options_menu",
"toggle_output_files_list",
"too_recently_compiled",
"validation_issue_description",
"validation_issue_entry_description",
"view_all_errors",
"view_logs",
"view_pdf",

View file

@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
function PreviewError({ name }) {
const { t } = useTranslation()
let errorTitle
let errorContent
if (name === 'error') {
errorTitle = t('server_error')
errorContent = <>{t('somthing_went_wrong_compiling')}</>
} else if (name === 'renderingError') {
errorTitle = t('pdf_rendering_error')
errorContent = <>{t('something_went_wrong_rendering_pdf')}</>
} else if (name === 'clsiMaintenance') {
errorTitle = t('server_error')
errorContent = <>{t('clsi_maintenance')}</>
} else if (name === 'clsiUnavailable') {
errorTitle = t('server_error')
errorContent = <>{t('clsi_unavailable')}</>
} else if (name === 'tooRecentlyCompiled') {
errorTitle = t('server_error')
errorContent = <>{t('too_recently_compiled')}</>
} else if (name === 'compileTerminated') {
errorTitle = t('terminated')
errorContent = <>{t('compile_terminated_by_user')}</>
} else if (name === 'rateLimited') {
errorTitle = t('pdf_compile_rate_limit_hit')
errorContent = <>{t('project_flagged_too_many_compiles')}</>
} else if (name === 'compileInProgress') {
errorTitle = t('pdf_compile_in_progress_error')
errorContent = <>{t('pdf_compile_try_again')}</>
} else if (name === 'timedout') {
errorTitle = t('timedout')
errorContent = (
<>
{t('proj_timed_out_reason')}
<div>
<a
href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F"
target="_blank"
>
{t('learn_how_to_make_documents_compile_quickly')}
</a>
</div>
</>
)
} else if (name === 'autoCompileDisabled') {
errorTitle = t('autocompile_disabled')
errorContent = <>{t('autocompile_disabled_reason')}</>
}
return errorTitle ? (
<PreviewLogsPaneEntry
headerTitle={errorTitle}
formattedContent={errorContent}
entryAriaLabel={t('compile_error_entry_description')}
level="error"
/>
) : null
}
PreviewError.propTypes = {
name: PropTypes.string.isRequired
}
export default PreviewError

View file

@ -2,7 +2,7 @@ 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'
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
function PreviewFirstErrorPopUp({
logEntry,
@ -23,8 +23,12 @@ function PreviewFirstErrorPopUp({
role="alertdialog"
aria-label={t('first_error_popup_label')}
>
<PreviewLogEntry
{...logEntry}
<PreviewLogsPaneEntry
headerTitle={logEntry.message}
rawContent={logEntry.content}
formattedContent={logEntry.humanReadableHintComponent}
extraInfoURL={logEntry.extraInfoURL}
level={logEntry.level}
showLineAndNoLink={false}
showCloseButton
onClose={onClose}

View file

@ -5,46 +5,38 @@ import { useTranslation } from 'react-i18next'
import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
import Icon from '../../../shared/components/icon'
function PreviewLogEntry({
file,
line,
message,
content,
column,
humanReadableHintComponent,
function PreviewLogsPaneEntry({
headerTitle,
rawContent,
formattedContent,
extraInfoURL,
level,
showLineAndNoLink = true,
sourceLocation,
showSourceLocationLink = true,
showCloseButton = false,
onLogEntryLocationClick,
entryAriaLabel = null,
onSourceLocationClick,
onClose
}) {
const { t } = useTranslation()
function handleLogEntryLinkClick() {
onLogEntryLocationClick({ file, line, column })
onSourceLocationClick(sourceLocation)
}
const logEntryDescription =
level === 'raw'
? t('raw_logs_description')
: t('log_entry_description', {
level: level
})
return (
<div className="log-entry" aria-label={logEntryDescription}>
<div className="log-entry" aria-label={entryAriaLabel}>
<PreviewLogEntryHeader
level={level}
file={file}
line={line}
message={message}
showLineAndNoLink={showLineAndNoLink}
onLogEntryLocationClick={handleLogEntryLinkClick}
sourceLocation={sourceLocation}
headerTitle={headerTitle}
showSourceLocationLink={showSourceLocationLink}
onSourceLocationClick={handleLogEntryLinkClick}
showCloseButton={showCloseButton}
onClose={onClose}
/>
{content ? (
{rawContent || formattedContent ? (
<PreviewLogEntryContent
content={content}
humanReadableHintComponent={humanReadableHintComponent}
rawContent={rawContent}
formattedContent={formattedContent}
extraInfoURL={extraInfoURL}
/>
) : null}
@ -53,16 +45,17 @@ function PreviewLogEntry({
}
function PreviewLogEntryHeader({
sourceLocation,
level,
file,
line,
message,
showLineAndNoLink = true,
headerTitle,
showSourceLocationLink = true,
showCloseButton = false,
onLogEntryLocationClick,
onSourceLocationClick,
onClose
}) {
const { t } = useTranslation()
const file = sourceLocation ? sourceLocation.file : null
const line = sourceLocation ? sourceLocation.line : null
const logEntryHeaderClasses = classNames('log-entry-header', {
'log-entry-header-error': level === 'error',
'log-entry-header-warning': level === 'warning',
@ -75,13 +68,13 @@ function PreviewLogEntryHeader({
return (
<header className={logEntryHeaderClasses}>
<h3 className="log-entry-header-title">{message}</h3>
{showLineAndNoLink && file ? (
<h3 className="log-entry-header-title">{headerTitle}</h3>
{showSourceLocationLink && file ? (
<button
className="btn-inline-link log-entry-header-link"
type="button"
title={headerLogLocationTitle}
onClick={onLogEntryLocationClick}
onClick={onSourceLocationClick}
>
<Icon type="chain" />
&nbsp;
@ -89,7 +82,7 @@ function PreviewLogEntryHeader({
{line ? <span>, {line}</span> : null}
</button>
) : null}
{showCloseButton && file ? (
{showCloseButton ? (
<button
className="btn-inline-link log-entry-header-link"
type="button"
@ -104,8 +97,8 @@ function PreviewLogEntryHeader({
}
function PreviewLogEntryContent({
content,
humanReadableHintComponent,
rawContent,
formattedContent,
extraInfoURL
}) {
const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
@ -127,38 +120,34 @@ function PreviewLogEntryContent({
return (
<div className="log-entry-content">
<div {...expandableProps}>
<pre className={logContentClasses}>{content.trim()}</pre>
<div className={buttonContainerClasses}>
<button
type="button"
className="btn btn-xs btn-default log-entry-btn-expand-collapse"
{...toggleProps}
>
{isExpanded ? (
<>
<Icon type="angle-up" /> {t('collapse')}
</>
) : (
<>
<Icon type="angle-down" /> {t('expand')}
</>
)}
</button>
</div>
</div>
{humanReadableHintComponent ? (
<div className="log-entry-human-readable-hint">
{humanReadableHintComponent}
{rawContent ? (
<div {...expandableProps}>
<pre className={logContentClasses}>{rawContent.trim()}</pre>
<div className={buttonContainerClasses}>
<button
type="button"
className="btn btn-xs btn-default log-entry-btn-expand-collapse"
{...toggleProps}
>
{isExpanded ? (
<>
<Icon type="angle-up" /> {t('collapse')}
</>
) : (
<>
<Icon type="angle-down" /> {t('expand')}
</>
)}
</button>
</div>
</div>
) : null}
{formattedContent ? (
<div className="log-entry-formatted-content">{formattedContent}</div>
) : null}
{extraInfoURL ? (
<div className="log-entry-human-readable-hint-link">
<a
href={extraInfoURL}
target="_blank"
className="log-entry-human-readable-hint-link"
>
<div className="log-entry-content-link">
<a href={extraInfoURL} target="_blank">
{t('log_hint_extra_info')}
</a>
</div>
@ -168,41 +157,40 @@ function PreviewLogEntryContent({
}
PreviewLogEntryHeader.propTypes = {
sourceLocation: PropTypes.shape({
file: PropTypes.string,
// `line should be either a number or null (i.e. not required), but currently sometimes we get
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
// to PropTypes.number (not required) once we fix that.
line: PropTypes.any,
column: PropTypes.any
}),
level: PropTypes.string.isRequired,
file: PropTypes.string,
line: PropTypes.any,
message: PropTypes.string,
showLineAndNoLink: PropTypes.bool,
headerTitle: PropTypes.string,
showSourceLocationLink: PropTypes.bool,
showCloseButton: PropTypes.bool,
onLogEntryLocationClick: PropTypes.func,
onSourceLocationClick: PropTypes.func,
onClose: PropTypes.func
}
PreviewLogEntryContent.propTypes = {
content: PropTypes.string.isRequired,
humanReadableHintComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.element
]),
rawContent: PropTypes.string,
formattedContent: PropTypes.node,
extraInfoURL: PropTypes.string
}
PreviewLogEntry.propTypes = {
file: PropTypes.string,
// `line should be either a number or null (i.e. not required), but currently sometimes we get
// an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
// to PropTypes.number (not required) once we fix that.
line: PropTypes.any,
column: PropTypes.any,
message: PropTypes.string,
content: PropTypes.string,
humanReadableHintComponent: PropTypes.node,
PreviewLogsPaneEntry.propTypes = {
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
headerTitle: PropTypes.string,
rawContent: PropTypes.string,
formattedContent: PropTypes.node,
extraInfoURL: PropTypes.string,
level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired,
showLineAndNoLink: PropTypes.bool,
showSourceLocationLink: PropTypes.bool,
showCloseButton: PropTypes.bool,
onLogEntryLocationClick: PropTypes.func,
entryAriaLabel: PropTypes.string,
onSourceLocationClick: PropTypes.func,
onClose: PropTypes.func
}
export default PreviewLogEntry
export default PreviewLogsPaneEntry

View file

@ -1,26 +1,68 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import PreviewLogEntry from './preview-log-entry'
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
import PreviewValidationIssue from './preview-validation-issue'
import PreviewError from './preview-error'
function PreviewLogsPane({ logEntries, rawLog, onLogEntryLocationClick }) {
function PreviewLogsPane({
logEntries = [],
rawLog = '',
validationIssues = {},
errors = {},
onLogEntryLocationClick
}) {
const { t } = useTranslation()
const errorsUI = Object.keys(errors).map((name, index) => (
<PreviewError key={index} name={name} />
))
const validationIssuesUI = Object.keys(validationIssues).map(
(name, index) => (
<PreviewValidationIssue
key={index}
name={name}
details={validationIssues[name]}
/>
)
)
const logEntriesUI = logEntries.map((logEntry, idx) => (
<PreviewLogsPaneEntry
key={idx}
headerTitle={logEntry.message}
rawContent={logEntry.content}
formattedContent={logEntry.humanReadableHintComponent}
extraInfoURL={logEntry.extraInfoURL}
level={logEntry.level}
entryAriaLabel={t('log_entry_description', {
level: logEntry.level
})}
sourceLocation={{
file: logEntry.file,
line: logEntry.line,
column: logEntry.column
}}
onSourceLocationClick={onLogEntryLocationClick}
/>
))
const rawLogUI = (
<PreviewLogsPaneEntry
headerTitle={t('raw_logs')}
rawContent={rawLog}
entryAriaLabel={t('raw_logs_description')}
level="raw"
/>
)
return (
<div className="logs-pane">
{logEntries && logEntries.length > 0 ? (
logEntries.map((logEntry, idx) => (
<PreviewLogEntry
key={idx}
{...logEntry}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
))
) : (
<div>No logs</div>
)}
<PreviewLogEntry content={rawLog} level="raw" message={t('raw_logs')} />
{errors ? errorsUI : null}
{validationIssues ? validationIssuesUI : null}
{logEntries ? logEntriesUI : null}
{rawLog && rawLog !== '' ? rawLogUI : null}
</div>
)
}
@ -28,7 +70,9 @@ function PreviewLogsPane({ logEntries, rawLog, onLogEntryLocationClick }) {
PreviewLogsPane.propTypes = {
logEntries: PropTypes.array,
rawLog: PropTypes.string,
onLogEntryLocationClick: PropTypes.func.isRequired
onLogEntryLocationClick: PropTypes.func.isRequired,
validationIssues: PropTypes.object,
errors: PropTypes.object
}
export default PreviewLogsPane

View file

@ -8,6 +8,7 @@ import Icon from '../../../shared/components/icon'
function PreviewLogsToggleButton({
onToggle,
showLogs,
compileFailed = false,
logsState: { nErrors, nWarnings },
showText
}) {
@ -41,6 +42,7 @@ function PreviewLogsToggleButton({
<button
id="logs-toggle"
type="button"
disabled={compileFailed}
className={toggleButtonClasses}
onClick={handleOnClick}
>
@ -136,7 +138,8 @@ PreviewLogsToggleButton.propTypes = {
nLogEntries: PropTypes.number.isRequired
}),
showLogs: PropTypes.bool.isRequired,
showText: PropTypes.bool.isRequired
showText: PropTypes.bool.isRequired,
compileFailed: PropTypes.bool
}
LogsCompilationResultIndicator.propTypes = {

View file

@ -53,6 +53,18 @@ function PreviewPane({
? compilerState.logEntries.all.length
: 0
const hasCLSIErrors =
compilerState.errors &&
Object.keys(compilerState.errors).length > 0 &&
compilerState.compileFailed &&
!compilerState.isCompiling
const hasValidationIssues =
compilerState.validationIssues &&
Object.keys(compilerState.validationIssues).length > 0 &&
compilerState.compileFailed &&
!compilerState.isCompiling
const showFirstErrorPopUp =
nErrors > 0 &&
!seenLogsForCurrentCompile &&
@ -79,6 +91,12 @@ function PreviewPane({
outputFiles={outputFiles}
pdfDownloadUrl={pdfDownloadUrl}
/>
<span aria-live="polite" className="sr-only">
{hasCLSIErrors ? t('compile_error_description') : ''}
</span>
<span aria-live="polite" className="sr-only">
{hasValidationIssues ? t('validation_issue_description') : ''}
</span>
<span aria-live="polite" className="sr-only">
{nErrors && !compilerState.isCompiling
? t('n_errors', { count: nErrors })
@ -101,6 +119,8 @@ function PreviewPane({
<PreviewLogsPane
logEntries={compilerState.logEntries.all}
rawLog={compilerState.rawLog}
validationIssues={compilerState.validationIssues}
errors={compilerState.errors}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
) : null}
@ -115,8 +135,11 @@ PreviewPane.propTypes = {
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired,
lastCompileTimestamp: PropTypes.number,
logEntries: PropTypes.object.isRequired,
rawLog: PropTypes.string
logEntries: PropTypes.object,
validationIssues: PropTypes.object,
errors: PropTypes.object,
rawLog: PropTypes.string,
compileFailed: PropTypes.bool
}),
onClearCache: PropTypes.func.isRequired,
onLogEntryLocationClick: PropTypes.func.isRequired,

View file

@ -213,6 +213,7 @@ function PreviewToolbar({
<PreviewLogsToggleButton
logsState={logsState}
showLogs={showLogs}
compileFailed={compilerState.compileFailed}
onToggle={onToggleLogs}
showText={showToggleText}
/>
@ -227,6 +228,7 @@ PreviewToolbar.propTypes = {
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired,
compileFailed: PropTypes.bool,
logEntries: PropTypes.object.isRequired
}),
logsState: PropTypes.shape({

View file

@ -0,0 +1,62 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
function PreviewValidationIssue({ name, details }) {
const { t } = useTranslation()
let validationTitle
let validationContent
if (name === 'sizeCheck') {
validationTitle = t('project_too_large')
validationContent = (
<>
<div>{t('project_too_large_please_reduce')}</div>
<ul className="list-no-margin-bottom">
{details.resources.map((resource, index) => (
<li key={index}>
{resource.path} &mdash; {resource.kbSize}
kb
</li>
))}
</ul>
</>
)
} else if (name === 'conflictedPaths') {
validationTitle = t('conflicting_paths_found')
validationContent = (
<>
<div>{t('following_paths_conflict')}</div>
<ul className="list-no-margin-bottom">
{details.map((detail, index) => (
<li key={index}>/{detail.path}</li>
))}
</ul>
</>
)
} else if (name === 'mainFile') {
validationTitle = t('main_file_not_found')
validationContent = <>{t('please_set_main_file')}</>
}
return validationTitle ? (
<PreviewLogsPaneEntry
headerTitle={validationTitle}
formattedContent={validationContent}
entryAriaLabel={t('validation_issue_entry_description')}
level="error"
/>
) : null
}
PreviewValidationIssue.propTypes = {
name: PropTypes.string.isRequired,
details: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
PropTypes.bool
])
}
export default PreviewValidationIssue

View file

@ -28,6 +28,7 @@ export default (PdfManager = class PdfManager {
logEntries: {},
logEntryAnnotations: {},
rawLog: '',
validation: {},
view: null, // 'pdf' 'logs'
showRawLog: false,
highlights: [],

View file

@ -338,6 +338,9 @@ App.controller('PdfController', function(
$scope.pdf.failedCheck = false
$scope.pdf.compileInProgress = false
$scope.pdf.autoCompileDisabled = false
if (window.showNewLogsUI) {
$scope.clsiErrors = {}
}
// make a cache to look up files by name
const fileByPath = {}
@ -362,6 +365,7 @@ App.controller('PdfController', function(
$scope.pdf.view = 'pdf'
$scope.shouldShowLogs = false
$scope.pdf.lastCompileTimestamp = Date.now()
$scope.pdf.validation = {}
// 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) {
@ -479,6 +483,33 @@ App.controller('PdfController', function(
$scope.pdf.error = true
}
if (window.showNewLogsUI) {
$scope.pdf.compileFailed = false
// `$scope.clsiErrors` stores the error states nested within `$scope.pdf`
// for use with React's <PreviewPane errors={$scope.clsiErrors}/>
$scope.clsiErrors = Object.assign(
{},
$scope.pdf.error ? { error: true } : null,
$scope.pdf.renderingError ? { renderingError: true } : null,
$scope.pdf.clsiMaintenance ? { clsiMaintenance: true } : null,
$scope.pdf.clsiUnavailable ? { clsiUnavailable: true } : null,
$scope.pdf.tooRecentlyCompiled ? { tooRecentlyCompiled: true } : null,
$scope.pdf.compileTerminated ? { compileTerminated: true } : null,
$scope.pdf.rateLimited ? { rateLimited: true } : null,
$scope.pdf.compileInProgress ? { compileInProgress: true } : null,
$scope.pdf.timedout ? { timedout: true } : null,
$scope.pdf.autoCompileDisabled ? { autoCompileDisabled: true } : null
)
if (
$scope.pdf.view === 'errors' ||
$scope.pdf.view === 'validation-problems'
) {
$scope.shouldShowLogs = true
$scope.pdf.compileFailed = true
}
}
const IGNORE_FILES = ['output.fls', 'output.fdb_latexmk']
$scope.pdf.outputFiles = []
@ -765,6 +796,11 @@ App.controller('PdfController', function(
$scope.pdf.renderingError = false
$scope.pdf.error = true
$scope.pdf.view = 'errors'
if (window.showNewLogsUI) {
$scope.clsiErrors = { error: true }
$scope.shouldShowLogs = true
$scope.pdf.compileFailed = true
}
})
.finally(() => {
$scope.lastFinishedCompileAt = Date.now()

View file

@ -1,69 +1,77 @@
import React from 'react'
import PreviewLogEntry from '../js/features/preview/components/preview-log-entry.js'
import PreviewLogsPaneEntry from '../js/features/preview/components/preview-logs-pane-entry.js'
export const ErrorWithCompilerOutput = args => <PreviewLogEntry {...args} />
export const ErrorWithCompilerOutput = args => (
<PreviewLogsPaneEntry {...args} />
)
ErrorWithCompilerOutput.args = {
level: 'error'
}
export const ErrorWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
<PreviewLogsPaneEntry {...args} />
)
ErrorWithCompilerOutputAndHumanReadableHint.args = {
level: 'error',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
formattedContent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const ErrorWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
export const ErrorWithoutCompilerOutput = args => (
<PreviewLogsPaneEntry {...args} />
)
ErrorWithoutCompilerOutput.args = {
level: 'error',
content: null
rawContent: null
}
export const WarningWithCompilerOutput = args => <PreviewLogEntry {...args} />
export const WarningWithCompilerOutput = args => (
<PreviewLogsPaneEntry {...args} />
)
WarningWithCompilerOutput.args = {
level: 'warning'
}
export const WarningWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
<PreviewLogsPaneEntry {...args} />
)
WarningWithCompilerOutputAndHumanReadableHint.args = {
level: 'warning',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
formattedContent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const WarningWithoutCompilerOutput = args => (
<PreviewLogEntry {...args} />
<PreviewLogsPaneEntry {...args} />
)
WarningWithoutCompilerOutput.args = {
level: 'warning',
content: null
rawContent: null
}
export const InfoWithCompilerOutput = args => <PreviewLogEntry {...args} />
export const InfoWithCompilerOutput = args => <PreviewLogsPaneEntry {...args} />
InfoWithCompilerOutput.args = {
level: 'typesetting'
}
export const InfoWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
<PreviewLogsPaneEntry {...args} />
)
InfoWithCompilerOutputAndHumanReadableHint.args = {
level: 'typesetting',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
formattedContent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const InfoWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
export const InfoWithoutCompilerOutput = args => (
<PreviewLogsPaneEntry {...args} />
)
InfoWithoutCompilerOutput.args = {
level: 'typesetting',
content: null
rawContent: null
}
function SampleHumanReadableHintComponent() {
@ -84,14 +92,16 @@ function SampleHumanReadableHintComponent() {
}
export default {
title: 'PreviewLogEntry',
component: PreviewLogEntry,
title: 'PreviewLogsPaneEntry',
component: PreviewLogsPaneEntry,
args: {
file: 'foo/bar.tex',
line: 10,
column: 20,
message: 'Lorem ipsum',
content: `
sourceLocation: {
file: 'foo/bar.tex',
line: 10,
column: 20
},
headerTitle: 'Lorem ipsum',
rawContent: `
The LaTeX compiler output
* With a lot of details

View file

@ -125,10 +125,13 @@
.no-outline-ring-on-click;
}
.log-entry-human-readable-hint,
.log-entry-human-readable-hint-link {
.log-entry-formatted-content,
.log-entry-content-link {
font-size: @font-size-small;
margin-top: @margin-sm;
margin-top: @margin-xs;
&:first-of-type {
margin-top: 0;
}
}
.first-error-popup {

View file

@ -227,6 +227,9 @@ ol {
}
// List options
.list-no-margin-bottom {
margin-bottom: 0;
}
// Unstyled keeps list items block level, just removes default browser padding and list-style
.list-unstyled {

View file

@ -1,4 +1,8 @@
{
"compile_error_description": "Your project did not compile because of an error",
"validation_issue_description": "Your project did not compile because of a validation issue",
"compile_error_entry_description": "An error which prevented your project from compiling",
"validation_issue_entry_description": "A validation issue which prevented your project from compiling",
"raw_logs_description": "Raw logs from the LaTeX compiler",
"raw_logs": "Raw logs",
"first_error_popup_label": "Your project has errors. This is the first one.",
@ -472,7 +476,7 @@
"tc_guests": "Guests",
"select_all_projects": "Select all",
"select_project": "Select",
"main_file_not_found": "Unknown main document.",
"main_file_not_found": "Unknown main document",
"please_set_main_file": "Please choose the main file for this project in the project menu. ",
"link_sharing_is_off": "Link sharing is off, only invited users can view this project.",
"turn_on_link_sharing": "Turn on link sharing",
@ -649,7 +653,7 @@
"beta_program_benefits": "We're always improving __appName__. By joining our Beta program you can have early access to new features and help us understand your needs better.",
"beta_program_opt_in_action": "Opt-In to Beta Program",
"conflicting_paths_found": "Conflicting Paths Found",
"following_paths_conflict": "The following files &amp; folders conflict with the same path",
"following_paths_conflict": "The following files and folders conflict with the same path",
"open_a_file_on_the_left": "Open a file on the left",
"reference_error_relink_hint": "If this error persists, try re-linking your account here:",
"pdf_rendering_error": "PDF Rendering Error",

View file

@ -3,50 +3,47 @@ import { expect } from 'chai'
import sinon from 'sinon'
import { screen, render, fireEvent } from '@testing-library/react'
import PreviewLogEntry from '../../../../../frontend/js/features/preview/components/preview-log-entry.js'
import PreviewLogsPaneEntry from '../../../../../frontend/js/features/preview/components/preview-logs-pane-entry.js'
describe('<PreviewLogEntry />', function() {
describe('<PreviewLogsPaneEntry />', function() {
const level = 'error'
describe('log entry description', function() {
for (const level of ['error', 'warning', 'typesetting', 'raw']) {
it(`describes the log entry with ${level} information`, function() {
render(<PreviewLogEntry level={level} />)
const expectedLabel =
level === 'raw'
? 'Raw logs from the LaTeX compiler'
: `Log entry with level: ${level}`
screen.getByLabelText(expectedLabel)
})
}
it('renders a configurable aria-label', function() {
const sampleAriaLabel = 'lorem ipsum dolor sit amet'
render(
<PreviewLogsPaneEntry entryAriaLabel={sampleAriaLabel} level={level} />
)
screen.getByLabelText(sampleAriaLabel)
})
describe('log location link', function() {
describe('logs pane source location link', function() {
const file = 'foo.tex'
const line = 42
const column = 21
const onLogEntryLocationClick = sinon.stub()
const onSourceLocationClick = sinon.stub()
afterEach(function() {
onLogEntryLocationClick.reset()
onSourceLocationClick.reset()
})
it('renders both file and line', function() {
render(<PreviewLogEntry file={file} line={line} level={level} />)
render(
<PreviewLogsPaneEntry sourceLocation={{ file, line }} level={level} />
)
screen.getByRole('button', {
name: `Navigate to log position in source code: ${file}, ${line}`
})
})
it('renders only file when line information is not available', function() {
render(<PreviewLogEntry file={file} level={level} />)
render(<PreviewLogsPaneEntry sourceLocation={{ file }} level={level} />)
screen.getByRole('button', {
name: `Navigate to log position in source code: ${file}`
})
})
it('does not render when file information is not available', function() {
render(<PreviewLogEntry level={level} />)
render(<PreviewLogsPaneEntry level={level} />)
expect(
screen.queryByRole('button', {
name: `Navigate to log position in source code: `
@ -56,12 +53,10 @@ describe('<PreviewLogEntry />', function() {
it('calls the callback with file, line and column on click', function() {
render(
<PreviewLogEntry
file={file}
line={line}
column={column}
<PreviewLogsPaneEntry
sourceLocation={{ file, line, column }}
level={level}
onLogEntryLocationClick={onLogEntryLocationClick}
onSourceLocationClick={onSourceLocationClick}
/>
)
const linkToSourceButton = screen.getByRole('button', {
@ -69,29 +64,29 @@ describe('<PreviewLogEntry />', function() {
})
fireEvent.click(linkToSourceButton)
expect(onLogEntryLocationClick).to.be.calledOnce
expect(onLogEntryLocationClick).to.be.calledWith({
expect(onSourceLocationClick).to.be.calledOnce
expect(onSourceLocationClick).to.be.calledWith({
file,
line: line,
column: column
line,
column
})
})
})
describe('log entry contents', function() {
const logContent = 'foo bar latex error stuff baz'
describe('logs pane entry raw contents', function() {
const rawContent = 'foo bar latex error stuff baz'
it('renders collapsed contents by default', function() {
render(<PreviewLogEntry content={logContent} level={level} />)
screen.getByText(logContent)
render(<PreviewLogsPaneEntry rawContent={rawContent} level={level} />)
screen.getByText(rawContent)
screen.getByRole('button', {
name: 'Expand'
})
})
it('supports expanding contents', function() {
render(<PreviewLogEntry content={logContent} level={level} />)
screen.getByText(logContent)
render(<PreviewLogsPaneEntry rawContent={rawContent} level={level} />)
screen.getByText(rawContent)
const expandCollapseBtn = screen.getByRole('button', {
name: 'Expand'
})
@ -102,34 +97,34 @@ describe('<PreviewLogEntry />', function() {
})
it('should not render at all when there are no log contents', function() {
const { container } = render(<PreviewLogEntry level={level} />)
const { container } = render(<PreviewLogsPaneEntry level={level} />)
expect(container.querySelector('.log-entry-content')).to.not.exist
})
})
describe('human-readable hints', function() {
const logContent = 'foo bar latex error stuff baz'
const logHintText = 'foo bar baz'
const logHint = <>{logHintText}</>
describe('formatted content', function() {
const rawContent = 'foo bar latex error stuff baz'
const formattedContentText = 'foo bar baz'
const formattedContent = <>{formattedContentText}</>
const infoURL = 'www.overleaf.com/learn/latex'
it('renders the hint', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
<PreviewLogsPaneEntry
rawContent={rawContent}
formattedContent={formattedContent}
extraInfoURL={infoURL}
level={level}
/>
)
screen.getByText(logHintText)
screen.getByText(formattedContentText)
})
it('renders the link to learn more', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
<PreviewLogsPaneEntry
rawContent={rawContent}
formattedContent={formattedContent}
extraInfoURL={infoURL}
level={level}
/>
@ -139,9 +134,9 @@ describe('<PreviewLogEntry />', function() {
it('does not render the link when it is not available', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
<PreviewLogsPaneEntry
rawContent={rawContent}
formattedContent={formattedContent}
level={level}
/>
)

View file

@ -26,6 +26,12 @@ describe('<PreviewLogsPane />', function() {
line: 30,
message: "Reference `idontexist' on page 1 undefined on input line 30."
}
const sampleTypesettingIssue = {
file: 'main.tex',
level: 'typesetting',
line: 12,
message: "Reference `idontexist' on page 1 undefined on input line 30."
}
const sampleRawLog = `
This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 6 NOV 2020 15:23
entering extended mode
@ -42,55 +48,138 @@ entering extended mode
)`
const errors = [sampleError1, sampleError2]
const warnings = [sampleWarning]
const logEntries = [...errors, ...warnings]
const typesetting = [sampleTypesettingIssue]
const logEntries = [...errors, ...warnings, ...typesetting]
const onLogEntryLocationClick = sinon.stub()
beforeEach(function() {
render(
<PreviewLogsPane
logEntries={logEntries}
rawLog={sampleRawLog}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
})
it('renders all log entries with appropriate labels', function() {
const errorEntries = screen.getAllByLabelText(`Log entry with level: error`)
const warningEntries = screen.getAllByLabelText(
`Log entry with level: warning`
)
expect(errorEntries).to.have.lengthOf(errors.length)
expect(warningEntries).to.have.lengthOf(warnings.length)
})
describe('with logs', function() {
beforeEach(function() {
render(
<PreviewLogsPane
logEntries={logEntries}
rawLog={sampleRawLog}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
})
it('renders all log entries with appropriate labels', function() {
const errorEntries = screen.getAllByLabelText(
`Log entry with level: error`
)
const warningEntries = screen.getAllByLabelText(
`Log entry with level: warning`
)
const typesettingEntries = screen.getAllByLabelText(
`Log entry with level: typesetting`
)
expect(errorEntries).to.have.lengthOf(errors.length)
expect(warningEntries).to.have.lengthOf(warnings.length)
expect(typesettingEntries).to.have.lengthOf(typesetting.length)
})
it('renders the raw log', function() {
screen.getByLabelText('Raw logs from the LaTeX compiler')
})
it('renders the raw log', function() {
screen.getByLabelText('Raw logs from the LaTeX compiler')
})
it('renders a link to location button for every error and warning log entry', function() {
logEntries.forEach((entry, index) => {
const linkToSourceButton = screen.getByRole('button', {
name: `Navigate to log position in source code: ${entry.file}, ${
entry.line
}`
})
fireEvent.click(linkToSourceButton)
expect(onLogEntryLocationClick).to.have.callCount(index + 1)
const call = onLogEntryLocationClick.getCall(index)
expect(
call.calledWith({
file: entry.file,
line: entry.line,
column: entry.column
it('renders a link to location button for every error and warning log entry', function() {
logEntries.forEach((entry, index) => {
const linkToSourceButton = screen.getByRole('button', {
name: `Navigate to log position in source code: ${entry.file}, ${
entry.line
}`
})
).to.be.true
fireEvent.click(linkToSourceButton)
expect(onLogEntryLocationClick).to.have.callCount(index + 1)
const call = onLogEntryLocationClick.getCall(index)
expect(
call.calledWith({
file: entry.file,
line: entry.line,
column: entry.column
})
).to.be.true
})
})
it(' does not render a link to location button for the raw log entry', function() {
const rawLogEntry = screen.getByLabelText(
'Raw logs from the LaTeX compiler'
)
expect(rawLogEntry.querySelector('.log-entry-header-link')).to.not.exist
})
})
it('does not render a link to location button for the raw log entry', function() {
const rawLogEntry = screen.getByLabelText(
'Raw logs from the LaTeX compiler'
)
expect(rawLogEntry.querySelector('.log-entry-header-link')).to.not.exist
describe('with validation issues', function() {
const sampleValidationIssues = {
sizeCheck: {
resources: [
{ path: 'foo/bar', kbSize: 76221 },
{ path: 'bar/baz', kbSize: 2342 }
]
},
mainFile: true
}
it('renders a validation entry for known issues', function() {
render(
<PreviewLogsPane
validationIssues={sampleValidationIssues}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
const validationEntries = screen.getAllByLabelText(
'A validation issue which prevented your project from compiling'
)
expect(validationEntries).to.have.lengthOf(
Object.keys(sampleValidationIssues).length
)
})
it('ignores unknown issues', function() {
render(
<PreviewLogsPane
validationIssues={{ unknownIssue: true }}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
const validationEntries = screen.queryAllByLabelText(
'A validation issue prevented your project from compiling'
)
expect(validationEntries).to.have.lengthOf(0)
})
})
describe('with compilation errors', function() {
const sampleErrors = {
clsiMaintenance: true,
tooRecentlyCompiled: true,
compileTerminated: true
}
it('renders an error entry for known errors', function() {
render(
<PreviewLogsPane
errors={sampleErrors}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
const errorEntries = screen.getAllByLabelText(
'An error which prevented your project from compiling'
)
expect(errorEntries).to.have.lengthOf(Object.keys(sampleErrors).length)
})
it('ignores unknown errors', function() {
render(
<PreviewLogsPane
errors={{ unknownIssue: true }}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
const errorEntries = screen.queryAllByLabelText(
'There was an error compiling your project'
)
expect(errorEntries).to.have.lengthOf(0)
})
})
})

View file

@ -197,11 +197,70 @@ describe('<PreviewPane />', function() {
})
})
describe('accessible description of the compile result', function() {
it('renders an accessible description with the errors and warnings count', function() {
const errors = [sampleError1, sampleError2]
const warnings = [sampleWarning]
const propsWithErrorsAndWarnings = getProps(false, {
errors,
warnings
})
render(<PreviewPane {...propsWithErrorsAndWarnings} />)
screen.getByText(`${errors.length} error${errors.length > 1 ? 's' : ''}`)
screen.getByText(
`${warnings.length} warning${warnings.length > 1 ? 's' : ''}`
)
})
it('renders an accessible description for failed compiles with CLSI errors', function() {
const sampleCLSIError = {
clsiMaintenance: true
}
const propsWithCLSIError = getProps(
false,
{},
Date.now(),
false,
true,
{},
sampleCLSIError
)
render(<PreviewPane {...propsWithCLSIError} />)
screen.getByText('Your project did not compile because of an error')
})
it('renders an accessible description for failed compiles with validation issues', function() {
const sampleValidationIssue = {
clsiMaintenance: true
}
const propsWithValidationIssue = getProps(
false,
{},
Date.now(),
false,
true,
sampleValidationIssue,
{}
)
render(<PreviewPane {...propsWithValidationIssue} />)
screen.getByText(
'Your project did not compile because of a validation issue'
)
})
})
function getProps(
isCompiling = false,
logEntries = {},
lastCompileTimestamp = Date.now(),
isShowingLogs = false
isShowingLogs = false,
compileFailed = false,
validationIssues = {},
errors = {}
) {
return {
compilerState: {
@ -210,8 +269,11 @@ describe('<PreviewPane />', function() {
isClearingCache: false,
isDraftModeOn: false,
isSyntaxCheckOn: false,
lastCompileTimestamp: lastCompileTimestamp,
logEntries: logEntries
lastCompileTimestamp,
logEntries,
compileFailed,
validationIssues,
errors
},
onClearCache: () => {},
onLogEntryLocationClick: () => {},

View file

@ -57,7 +57,7 @@ describe('ClsiFormatChecker', function() {
])
})
it('should call _checkForDuplicatePaths and _checkForConflictingPaths', function(done) {
it('should call _checkDocsAreUnderSizeLimit and _checkForConflictingPaths', function(done) {
this.ClsiFormatChecker._checkForConflictingPaths = sinon
.stub()
.callsArgWith(1, null)