mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
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:
parent
619ec15309
commit
01cc057f6b
9 changed files with 139 additions and 49 deletions
|
@ -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"
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
"off",
|
||||
"on",
|
||||
"other_output_files",
|
||||
"raw_logs_description",
|
||||
"raw_logs",
|
||||
"recompile_from_scratch",
|
||||
"recompile",
|
||||
"run_syntax_check_now",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue