Add raw logs to new errors UI (#3378)

* Add raw logs to new compile UI

* Address feedback

* Update test/frontend/features/preview/components/preview-logs-pane.test.js

Co-authored-by: John Lees-Miller <jdleesmiller@gmail.com>

Co-authored-by: John Lees-Miller <jdleesmiller@gmail.com>
GitOrigin-RevId: af9c653e13d93434467b122f4c388493e786212c
This commit is contained in:
Paulo Jorge Reis 2020-11-16 10:15:29 +00:00 committed by Copybot
parent 619ec15309
commit 01cc057f6b
9 changed files with 139 additions and 49 deletions

View file

@ -8,7 +8,8 @@ div.full-size.pdf(ng-controller="PdfController")
isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error,
lastCompileTimestamp: pdf.lastCompileTimestamp,
logEntries: pdf.logEntries ? pdf.logEntries : {}
logEntries: pdf.logEntries ? pdf.logEntries : {},
rawLog: pdf.rawLog ? pdf.rawLog : ''
}`
on-clear-cache="clearCache"
on-recompile="recompile"

View file

@ -27,6 +27,8 @@
"off",
"on",
"other_output_files",
"raw_logs_description",
"raw_logs",
"recompile_from_scratch",
"recompile",
"run_syntax_check_now",

View file

@ -23,9 +23,12 @@ function PreviewLogEntry({
function handleLogEntryLinkClick() {
onLogEntryLocationClick({ file, line, column })
}
const logEntryDescription = t('log_entry_description', {
level: level
})
const logEntryDescription =
level === 'raw'
? t('raw_logs_description')
: t('log_entry_description', {
level: level
})
return (
<div className="log-entry" aria-label={logEntryDescription}>
<PreviewLogEntryHeader
@ -63,7 +66,8 @@ function PreviewLogEntryHeader({
const logEntryHeaderClasses = classNames('log-entry-header', {
'log-entry-header-error': level === 'error',
'log-entry-header-warning': level === 'warning',
'log-entry-header-typesetting': level === 'typesetting'
'log-entry-header-typesetting': level === 'typesetting',
'log-entry-header-raw': level === 'raw'
})
const headerLogLocationTitle = t('navigate_log_source', {
location: file + (line ? `, ${line}` : '')
@ -194,7 +198,7 @@ PreviewLogEntry.propTypes = {
content: PropTypes.string,
humanReadableHintComponent: PropTypes.node,
extraInfoURL: PropTypes.string,
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired,
showLineAndNoLink: PropTypes.bool,
showCloseButton: PropTypes.bool,
onLogEntryLocationClick: PropTypes.func,

View file

@ -1,8 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import PreviewLogEntry from './preview-log-entry'
function PreviewLogsPane({ logEntries, onLogEntryLocationClick }) {
function PreviewLogsPane({ logEntries, rawLog, onLogEntryLocationClick }) {
const { t } = useTranslation()
return (
<div className="logs-pane">
{logEntries && logEntries.length > 0 ? (
@ -16,12 +19,15 @@ function PreviewLogsPane({ logEntries, onLogEntryLocationClick }) {
) : (
<div>No logs</div>
)}
<PreviewLogEntry content={rawLog} level="raw" message={t('raw_logs')} />
</div>
)
}
PreviewLogsPane.propTypes = {
logEntries: PropTypes.array,
rawLog: PropTypes.string,
onLogEntryLocationClick: PropTypes.func.isRequired
}

View file

@ -100,6 +100,7 @@ function PreviewPane({
{showLogs ? (
<PreviewLogsPane
logEntries={compilerState.logEntries.all}
rawLog={compilerState.rawLog}
onLogEntryLocationClick={onLogEntryLocationClick}
/>
) : null}
@ -114,7 +115,8 @@ PreviewPane.propTypes = {
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired,
lastCompileTimestamp: PropTypes.number,
logEntries: PropTypes.object.isRequired
logEntries: PropTypes.object.isRequired,
rawLog: PropTypes.string
}),
onClearCache: PropTypes.func.isRequired,
onLogEntryLocationClick: PropTypes.func.isRequired,

View file

@ -33,6 +33,10 @@
background-color: @ol-blue;
}
.log-entry-header-raw {
background-color: @ol-blue-gray-4;
}
.log-entry-header-title,
.log-entry-header-link {
font-family: @font-family-sans-serif;

View file

@ -1,9 +1,11 @@
{
"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.",
"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__",
"other_output_files": "Other output files",
"toggle_output_files_list": "Toggle output files list",

View file

@ -7,13 +7,16 @@ import PreviewLogEntry from '../../../../../frontend/js/features/preview/compone
describe('<PreviewLogEntry />', function() {
const level = 'error'
const noOp = () => {}
describe('log entry description', function() {
for (const level of ['error', 'warning', 'typesetting']) {
for (const level of ['error', 'warning', 'typesetting', 'raw']) {
it(`describes the log entry with ${level} information`, function() {
render(<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />)
screen.getByLabelText(`Log entry with level: "${level}"`)
render(<PreviewLogEntry level={level} />)
const expectedLabel =
level === 'raw'
? 'Raw logs from the LaTeX compiler'
: `Log entry with level: ${level}`
screen.getByLabelText(expectedLabel)
})
}
})
@ -29,34 +32,21 @@ describe('<PreviewLogEntry />', function() {
})
it('renders both file and line', function() {
render(
<PreviewLogEntry
file={file}
line={line}
level={level}
onLogEntryLocationClick={noOp}
/>
)
render(<PreviewLogEntry file={file} line={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}
onLogEntryLocationClick={noOp}
/>
)
render(<PreviewLogEntry file={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} onLogEntryLocationClick={noOp} />)
render(<PreviewLogEntry level={level} />)
expect(
screen.queryByRole('button', {
name: `Navigate to log position in source code: `
@ -92,13 +82,7 @@ describe('<PreviewLogEntry />', function() {
const logContent = 'foo bar latex error stuff baz'
it('renders collapsed contents by default', function() {
render(
<PreviewLogEntry
content={logContent}
level={level}
onLogEntryLocationClick={noOp}
/>
)
render(<PreviewLogEntry content={logContent} level={level} />)
screen.getByText(logContent)
screen.getByRole('button', {
name: 'Expand'
@ -106,13 +90,7 @@ describe('<PreviewLogEntry />', function() {
})
it('supports expanding contents', function() {
render(
<PreviewLogEntry
content={logContent}
level={level}
onLogEntryLocationClick={noOp}
/>
)
render(<PreviewLogEntry content={logContent} level={level} />)
screen.getByText(logContent)
const expandCollapseBtn = screen.getByRole('button', {
name: 'Expand'
@ -124,9 +102,7 @@ describe('<PreviewLogEntry />', function() {
})
it('should not render at all when there are no log contents', function() {
const { container } = render(
<PreviewLogEntry level={level} onLogEntryLocationClick={noOp} />
)
const { container } = render(<PreviewLogEntry level={level} />)
expect(container.querySelector('.log-entry-content')).to.not.exist
})
})
@ -144,7 +120,6 @@ describe('<PreviewLogEntry />', function() {
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
onLogEntryLocationClick={noOp}
/>
)
screen.getByText(logHintText)
@ -157,7 +132,6 @@ describe('<PreviewLogEntry />', function() {
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
onLogEntryLocationClick={noOp}
/>
)
screen.getByRole('link', { name: 'Learn more' })
@ -169,7 +143,6 @@ describe('<PreviewLogEntry />', function() {
content={logContent}
humanReadableHintComponent={logHint}
level={level}
onLogEntryLocationClick={noOp}
/>
)
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist

View file

@ -0,0 +1,96 @@
import React from 'react'
import { screen, render, fireEvent } from '@testing-library/react'
import PreviewLogsPane from '../../../../../frontend/js/features/preview/components/preview-logs-pane'
import sinon from 'sinon'
const { expect } = require('chai')
describe('<PreviewLogsPane />', 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."
}
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
\\write18 enabled.
%&-line parsing enabled.
**main.tex
(/compile/main.tex
LaTeX2e <2020-02-02> patch level 5
L3 programming layer <2020-07-17> (/usr/local/texlive/2020/texmf-dist/tex/latex
/base/article.cls
Document Class: article 2019/12/20 v1.4l Standard LaTeX document class
(/usr/local/texlive/2020/texmf-dist/tex/latex/base/size10.clo
File: size10.clo 2019/12/20 v1.4l Standard LaTeX file (size option)
)`
const errors = [sampleError1, sampleError2]
const warnings = [sampleWarning]
const logEntries = [...errors, ...warnings]
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)
})
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
})
).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
})
})