Merge pull request #3332 from overleaf/pr-new-log-entries-ui

New log entries ui

GitOrigin-RevId: a13ccac04197e7c8f3777750b956d550c95c8f0d
This commit is contained in:
Jakob Ackermann
2020-11-06 03:04:59 +00:00
committed by Copybot
parent 0a5cc39671
commit c6a853c7ed
18 changed files with 968 additions and 75 deletions
@@ -19,6 +19,7 @@ div.full-size.pdf(ng-controller="PdfController")
output-files="pdf.outputFiles"
pdf-download-url="pdf.downloadUrl"
show-logs="shouldShowLogs"
on-log-entry-link-click="openInEditor"
)
else
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")
@@ -172,7 +173,7 @@ div.full-size.pdf(ng-controller="PdfController")
) #{translate("failed_compile_check_ignore")}
| .
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
div(ng-repeat="entry in pdf.logEntries.all")
.alert(
ng-class="{\
'alert-danger': entry.level == 'error',\
@@ -1,40 +1,43 @@
[
"download_pdf",
"download_file",
"n_warnings",
"n_warnings_plural",
"n_errors",
"n_errors_plural",
"file_outline",
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
"we_cant_find_any_sections_or_subsections_in_this_file",
"find_out_more_about_the_file_outline",
"expand",
"collapse",
"show_outline",
"hide_outline",
"compiling",
"recompile",
"auto_compile",
"on",
"off",
"other_output_files",
"collapse",
"compile_mode",
"normal",
"compiling",
"download_file",
"download_pdf",
"expand",
"fast",
"stop_on_validation_error",
"file_outline",
"find_out_more_about_the_file_outline",
"hide_outline",
"ignore_validation_errors",
"run_syntax_check_now",
"loading",
"log_entry_description",
"log_hint_extra_info",
"n_errors_plural",
"n_errors",
"n_warnings_plural",
"n_warnings",
"navigate_log_source",
"no_messages",
"normal",
"off",
"on",
"other_output_files",
"recompile_from_scratch",
"recompile",
"run_syntax_check_now",
"run_syntax_check_now",
"send_first_message",
"show_outline",
"stop_on_validation_error",
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
"toggle_compile_options_menu",
"toggle_output_files_list",
"your_message",
"your_project_has_errors",
"view_warnings",
"view_logs",
"view_pdf",
"recompile_from_scratch",
"run_syntax_check_now",
"toggle_compile_options_menu"
"view_warnings",
"we_cant_find_any_sections_or_subsections_in_this_file",
"your_message",
"your_project_has_errors"
]
@@ -1,41 +1,178 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
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, raw, level }) {
const logEntryClasses = classNames('alert', {
'alert-danger': level === 'error',
'alert-warning': level === 'warning',
'alert-info': level === 'typesetting'
function PreviewLogEntry({
file,
line,
message,
content,
column,
humanReadableHintComponent,
extraInfoURL,
level,
onLogEntryLinkClick
}) {
const { t } = useTranslation()
function handleLogEntryLinkClick() {
onLogEntryLinkClick({ file, line, column })
}
const logEntryDescription = t('log_entry_description', {
level: level
})
return (
<div className={logEntryClasses}>
<span className="line-no">
<Icon type="link" />
{file ? <span>{file}</span> : null}
{line ? <span>, {line}</span> : null}
</span>
{message ? (
<p className="entry-message">
{level} {message}
</p>
<div className="log-entry" aria-label={logEntryDescription}>
<PreviewLogEntryHeader
level={level}
file={file}
line={line}
message={message}
onLogEntryLinkClick={handleLogEntryLinkClick}
/>
{content ? (
<PreviewLogEntryContent
content={content}
humanReadableHintComponent={humanReadableHintComponent}
extraInfoURL={extraInfoURL}
/>
) : null}
{content ? <p className="entry-content">{content.trim()}</p> : null}
</div>
)
}
function PreviewLogEntryHeader({
level,
file,
line,
message,
onLogEntryLinkClick
}) {
const { t } = useTranslation()
const logEntryHeaderClasses = classNames('log-entry-header', {
'log-entry-header-error': level === 'error',
'log-entry-header-warning': level === 'warning',
'log-entry-header-typesetting': level === 'typesetting'
})
const headerLinkBtnTitle = t('navigate_log_source', {
location: file + (line ? `, ${line}` : '')
})
return (
<header className={logEntryHeaderClasses}>
<h3 className="log-entry-header-title">{message}</h3>
{file ? (
<button
className="btn-inline-link log-entry-header-link"
type="button"
title={headerLinkBtnTitle}
onClick={onLogEntryLinkClick}
>
<Icon type="chain" />
&nbsp;
<span>{file}</span>
{line ? <span>, {line}</span> : null}
</button>
) : null}
</header>
)
}
function PreviewLogEntryContent({
content,
humanReadableHintComponent,
extraInfoURL
}) {
const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
collapsedSize: 150,
classes: {
container: 'log-entry-content-raw-expandable-container'
}
})
const logContentClasses = classNames('log-entry-content-raw', {
'log-entry-content-raw-collapsed': !isExpanded
})
const buttonContainerClasses = classNames(
'log-entry-content-button-container',
{
'log-entry-content-button-container-collapsed': !isExpanded
}
)
const { t } = useTranslation()
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}
</div>
) : null}
{extraInfoURL ? (
<div className="log-entry-human-readable-hint-link">
<a
href={extraInfoURL}
target="_blank"
className="log-entry-human-readable-hint-link"
>
{t('log_hint_extra_info')}
</a>
</div>
) : null}
</div>
)
}
PreviewLogEntryHeader.propTypes = {
level: PropTypes.string.isRequired,
file: PropTypes.string,
line: PropTypes.any,
message: PropTypes.string,
onLogEntryLinkClick: PropTypes.func.isRequired
}
PreviewLogEntryContent.propTypes = {
content: PropTypes.string.isRequired,
humanReadableHintComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.element
]),
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,
raw: PropTypes.string,
level: PropTypes.string.isRequired
humanReadableHintComponent: PropTypes.node,
extraInfoURL: PropTypes.string,
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
onLogEntryLinkClick: PropTypes.func.isRequired
}
export default PreviewLogEntry
@@ -2,12 +2,16 @@ import React from 'react'
import PropTypes from 'prop-types'
import PreviewLogEntry from './preview-log-entry'
function PreviewLogsPane({ logEntries }) {
function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
return (
<div className="pdf-logs">
<div className="logs-pane">
{logEntries && logEntries.length > 0 ? (
logEntries.map((logEntry, idx) => (
<PreviewLogEntry key={idx} {...logEntry} />
<PreviewLogEntry
key={idx}
{...logEntry}
onLogEntryLinkClick={onLogEntryLinkClick}
/>
))
) : (
<div>No logs</div>
@@ -17,7 +21,8 @@ function PreviewLogsPane({ logEntries }) {
}
PreviewLogsPane.propTypes = {
logEntries: PropTypes.array
logEntries: PropTypes.array,
onLogEntryLinkClick: PropTypes.func.isRequired
}
export default PreviewLogsPane
@@ -15,10 +15,10 @@ function PreviewPane({
onToggleLogs,
outputFiles,
pdfDownloadUrl,
onLogEntryLinkClick,
showLogs
}) {
const { t } = useTranslation()
const nErrors =
compilerState.logEntries && compilerState.logEntries.errors
? compilerState.logEntries.errors.length
@@ -59,7 +59,10 @@ function PreviewPane({
: ''}
</span>
{showLogs ? (
<PreviewLogsPane logEntries={compilerState.logEntries.all} />
<PreviewLogsPane
logEntries={compilerState.logEntries.all}
onLogEntryLinkClick={onLogEntryLinkClick}
/>
) : null}
</>
)
@@ -74,6 +77,7 @@ PreviewPane.propTypes = {
logEntries: PropTypes.object.isRequired
}),
onClearCache: PropTypes.func.isRequired,
onLogEntryLinkClick: PropTypes.func.isRequired,
onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired,
@@ -878,6 +878,25 @@ App.controller('PdfController', function(
$scope.recompile({ check: true })
})
}
$scope.openInEditor = function(entry) {
let column, line
eventTracking.sendMBOnce('logs-jump-to-location-once')
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
if (entity == null || entity.type !== 'doc') {
return
}
if (entry.line != null) {
line = entry.line
}
if (entry.column != null) {
column = entry.column
}
ide.editorManager.openDoc(entity, {
gotoLine: line,
gotoColumn: column
})
}
})
App.factory('synctex', function(ide, $http, $q) {
@@ -1048,27 +1067,6 @@ App.controller('PdfSynctexController', function($scope, synctex, ide) {
}
})
App.controller('PdfLogEntryController', function($scope, ide, eventTracking) {
$scope.openInEditor = function(entry) {
let column, line
eventTracking.sendMBOnce('logs-jump-to-location-once')
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
if (entity == null || entity.type !== 'doc') {
return
}
if (entry.line != null) {
line = entry.line
}
if (entry.column != null) {
column = entry.column
}
ide.editorManager.openDoc(entity, {
gotoLine: line,
gotoColumn: column
})
}
})
App.controller('ClearCacheModalController', function($scope, $modalInstance) {
$scope.state = { error: false, inflight: false }
@@ -0,0 +1,55 @@
import { useRef, useState, useLayoutEffect } from 'react'
import classNames from 'classnames'
function useExpandCollapse({
initiallyExpanded = false,
collapsedSize = '0',
dimension = 'height',
classes = {}
} = {}) {
const ref = useRef()
const [isExpanded, setIsExpanded] = useState(initiallyExpanded)
const [size, setSize] = useState()
useLayoutEffect(
() => {
const expandCollapseEl = ref.current
if (isExpanded) {
const expandedSize =
dimension === 'height'
? expandCollapseEl.scrollHeight
: expandCollapseEl.scrollWidth
setSize(expandedSize)
} else {
setSize(collapsedSize)
}
},
[isExpanded]
)
const expandableClasses = classNames(
'expand-collapse-container',
classes.container,
!isExpanded ? classes.containerCollapsed : null
)
function handleToggle() {
setIsExpanded(!isExpanded)
}
return {
isExpanded,
expandableProps: {
ref,
style: {
[dimension === 'height' ? 'height' : 'width']: `${size}px`
},
className: expandableClasses
},
toggleProps: {
onClick: handleToggle
}
}
}
export default useExpandCollapse
@@ -0,0 +1,117 @@
import React from 'react'
import PreviewLogEntry from '../js/features/preview/components/preview-log-entry.js'
export const ErrorWithCompilerOutput = args => <PreviewLogEntry {...args} />
ErrorWithCompilerOutput.args = {
level: 'error'
}
export const ErrorWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
)
ErrorWithCompilerOutputAndHumanReadableHint.args = {
level: 'error',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const ErrorWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
ErrorWithoutCompilerOutput.args = {
level: 'error',
content: null
}
export const WarningWithCompilerOutput = args => <PreviewLogEntry {...args} />
WarningWithCompilerOutput.args = {
level: 'warning'
}
export const WarningWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
)
WarningWithCompilerOutputAndHumanReadableHint.args = {
level: 'warning',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const WarningWithoutCompilerOutput = args => (
<PreviewLogEntry {...args} />
)
WarningWithoutCompilerOutput.args = {
level: 'warning',
content: null
}
export const InfoWithCompilerOutput = args => <PreviewLogEntry {...args} />
InfoWithCompilerOutput.args = {
level: 'typesetting'
}
export const InfoWithCompilerOutputAndHumanReadableHint = args => (
<PreviewLogEntry {...args} />
)
InfoWithCompilerOutputAndHumanReadableHint.args = {
level: 'typesetting',
humanReadableHintComponent: <SampleHumanReadableHintComponent />,
extraInfoURL:
'https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr'
}
export const InfoWithoutCompilerOutput = args => <PreviewLogEntry {...args} />
InfoWithoutCompilerOutput.args = {
level: 'typesetting',
content: null
}
function SampleHumanReadableHintComponent() {
return (
<>
Human-readable hint to help LaTeX newbies. Supports <b>mark-up</b> for
e.g.{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.overleaf.com/learn/latex/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr"
>
inline links
</a>
.
</>
)
}
export default {
title: 'PreviewLogEntry',
component: PreviewLogEntry,
args: {
file: 'foo/bar.tex',
line: 10,
column: 20,
message: 'Lorem ipsum',
content: `
The LaTeX compiler output
* With a lot of details
Wrapped in an HTML <pre> element with
preformatted text which is to be presented exactly
as written in the HTML file
(whitespace included™)
The text is typically rendered using a non-proportional ("monospace") font.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <7> on input line 18.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <5> on input line 18.
! Undefined control sequence.
<recently read> \\Zlpha
main.tex, line 23
`
}
}
@@ -0,0 +1,136 @@
import React from 'react'
import PropTypes from 'prop-types'
import useExpandCollapse from '../js/shared/hooks/use-expand-collapse'
function TestUI({ children, expandableProps, toggleProps }) {
return (
<>
<div {...expandableProps}>{children}</div>
<button {...toggleProps}>Expand/collapse</button>
</>
)
}
function VerticalTestUI(props) {
return <TestUI {...props}>{verticalContents}</TestUI>
}
function HorizontalTestUI(props) {
return <TestUI {...props}>{horizontalContents}</TestUI>
}
VerticalTestUI.propTypes = {
expandableProps: PropTypes.object,
toggleProps: PropTypes.object
}
HorizontalTestUI.propTypes = VerticalTestUI.propTypes
TestUI.propTypes = {
...VerticalTestUI.propTypes,
children: PropTypes.node
}
export const Vertical = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<VerticalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
export const VerticalInitiallyExpanded = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<VerticalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
VerticalInitiallyExpanded.args = {
initiallyExpanded: true
}
export const VerticalWithMinSize = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<VerticalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
VerticalWithMinSize.args = {
collapsedSize: 200
}
export const Horizontal = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<HorizontalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
Horizontal.args = {
dimension: 'width'
}
export const HorizontalInitiallyExpanded = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<HorizontalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
HorizontalInitiallyExpanded.args = {
initiallyExpanded: true
}
export const HorizontalWithMinSize = args => {
const { expandableProps, toggleProps } = useExpandCollapse(args)
return (
<HorizontalTestUI
expandableProps={expandableProps}
toggleProps={toggleProps}
/>
)
}
HorizontalWithMinSize.args = {
dimension: 'width',
collapsedSize: 200
}
const defaultContentStyles = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '2em',
backgroundImage: 'linear-gradient(to bottom, red, blue)',
width: '500px',
height: '500px',
color: '#FFF'
}
const verticalContents = <div style={defaultContentStyles}>Vertical</div>
const horizontalContents = (
<div
style={{
...defaultContentStyles,
backgroundImage: 'linear-gradient(to right, red, blue)'
}}
>
Horizontal
</div>
)
export default {
title: 'useExpandCollapse'
}
@@ -55,6 +55,7 @@
@import 'components/nvd3.less';
@import 'components/nvd3_override.less';
@import 'components/infinite-scroll.less';
@import 'components/expand-collapse.less';
// Components w/ JavaScript
@import 'components/modals.less';
@@ -14,6 +14,7 @@
@import './editor/rich-text.less';
@import './editor/publish-modal.less';
@import './editor/outline.less';
@import './editor/logs.less';
@ui-layout-toggler-def-height: 50px;
@ui-resizer-size: 7px;
@@ -0,0 +1,131 @@
.logs-pane {
.full-size;
top: @pdf-top-offset;
padding: @padding-sm;
overflow-y: auto;
background-color: @logs-pane-bg;
}
.log-entry {
margin-bottom: @margin-sm;
}
.log-entry-header {
padding: 3px @padding-sm;
display: flex;
align-items: flex-start;
color: #fff;
border-radius: @border-radius-base @border-radius-base 0 0;
&:last-child {
border-radius: @border-radius-base;
}
}
.log-entry-header-error {
background-color: @ol-red;
}
.log-entry-header-warning {
background-color: @orange;
}
.log-entry-header-typesetting {
background-color: @ol-blue;
}
.log-entry-header-title,
.log-entry-header-link {
font-family: @font-family-sans-serif;
font-size: @font-size-base;
line-height: @line-height-base;
font-weight: 700;
flex-grow: 1;
margin: 0;
color: #fff;
}
.log-entry-header-link {
border-width: 0;
flex-grow: 0;
text-align: right;
white-space: nowrap;
padding-left: @padding-sm;
&:hover,
&:focus {
outline: 0;
color: #fff;
}
&:focus {
text-decoration: none;
}
}
.log-entry-content {
background-color: #fff;
padding: @padding-sm;
&:last-child {
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
.log-entry-content-raw-expandable-container {
position: relative;
background-color: @ol-blue-gray-1;
border-radius: @border-radius-base;
}
.log-entry-content-raw-button-container-collapsed {
padding-bottom: 0;
}
.log-entry-content-raw {
font-size: @font-size-small;
color: @ol-blue-gray-4;
padding: @padding-sm @padding-sm @padding-xl @padding-sm;
margin: 0;
}
.log-entry-content-raw-collapsed {
padding-bottom: 0;
}
.log-entry-content-button-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
padding-bottom: @padding-sm;
text-align: center;
border-radius: 0 0 @border-radius-base @border-radius-base;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-image: linear-gradient(0, @ol-blue-gray-1, transparent);
opacity: 0;
transition: opacity 0.15s ease-in-out;
}
}
.log-entry-content-button-container-collapsed::before {
opacity: 1;
}
.log-entry-btn-expand-collapse {
position: relative;
z-index: 1;
&:focus,
&:focus:active {
outline: 0;
}
}
.log-entry-human-readable-hint,
.log-entry-human-readable-hint-link {
font-size: @font-size-small;
margin-top: @margin-sm;
}
@@ -0,0 +1,4 @@
.expand-collapse-container {
overflow: hidden;
transition: height 0.15s ease-in-out, width 0.15s ease-in-out;
}
@@ -89,6 +89,7 @@
@editor-header-logo-background : url(/img/ol-brand/overleaf-o.svg) center / contain no-repeat;
@project-name-color : @ol-blue-gray-3;
@pdf-bg : @ol-blue-gray-0;
@logs-pane-bg: @ol-blue-gray-1;
// Navbar
@navbar-default-bg : #FFF;
@@ -1060,11 +1060,12 @@
@chat-new-message-textarea-color: @ol-blue-gray-6;
@chat-new-message-border-color: @editor-border-color;
// PDF
// PDF and logs
@pdf-top-offset: @toolbar-small-height;
@pdf-bg: @ol-blue-gray-1;
@pdfjs-bg: transparent;
@pdf-page-shadow-color: rgba(0, 0, 0, 0.5);
@logs-pane-bg: @ol-blue-gray-5;
@log-line-no-color: #fff;
@log-hints-color: @ol-blue-gray-4;
+2
View File
@@ -1,4 +1,6 @@
{
"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",
"n_warnings": "__count__ warning",
@@ -0,0 +1,174 @@
import React from 'react'
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'
describe('<PreviewLogEntry />', function() {
const level = 'error'
const noOp = () => {}
describe('log entry description', function() {
for (const level of ['error', 'warning', 'typesetting']) {
it(`describes the log entry with ${level} information`, function() {
render(<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />)
screen.getByLabelText(`Log entry with level: "${level}"`)
})
}
})
describe('log location link', function() {
const file = 'foo.tex'
const line = 42
const column = 21
const onLogEntryLinkClick = sinon.stub()
afterEach(function() {
onLogEntryLinkClick.reset()
})
it('renders both file and line', function() {
render(
<PreviewLogEntry
file={file}
line={line}
level={level}
onLogEntryLinkClick={noOp}
/>
)
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} onLogEntryLinkClick={noOp} />
)
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} onLogEntryLinkClick={noOp} />)
expect(
screen.queryByRole('button', {
name: `Navigate to log position in source code: `
})
).to.not.exist
})
it('calls the callback with file, line and column on click', function() {
render(
<PreviewLogEntry
file={file}
line={line}
column={column}
level={level}
onLogEntryLinkClick={onLogEntryLinkClick}
/>
)
const linkToSourceButton = screen.getByRole('button', {
name: `Navigate to log position in source code: ${file}, ${line}`
})
fireEvent.click(linkToSourceButton)
expect(onLogEntryLinkClick).to.be.calledOnce
expect(onLogEntryLinkClick).to.be.calledWith({
file,
line: line,
column: column
})
})
})
describe('log entry contents', function() {
const logContent = 'foo bar latex error stuff baz'
it('renders collapsed contents by default', function() {
render(
<PreviewLogEntry
content={logContent}
level={level}
onLogEntryLinkClick={noOp}
/>
)
screen.getByText(logContent)
screen.getByRole('button', {
name: 'Expand'
})
})
it('supports expanding contents', function() {
render(
<PreviewLogEntry
content={logContent}
level={level}
onLogEntryLinkClick={noOp}
/>
)
screen.getByText(logContent)
const expandCollapseBtn = screen.getByRole('button', {
name: 'Expand'
})
fireEvent.click(expandCollapseBtn)
screen.getByRole('button', {
name: 'Collapse'
})
})
it('should not render at all when there are no log contents', function() {
const { container } = render(
<PreviewLogEntry level={level} onLogEntryLinkClick={noOp} />
)
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}</>
const infoURL = 'www.overleaf.com/learn/latex'
it('renders the hint', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
onLogEntryLinkClick={noOp}
/>
)
screen.getByText(logHintText)
})
it('renders the link to learn more', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
onLogEntryLinkClick={noOp}
/>
)
screen.getByRole('link', { name: 'Learn more' })
})
it('does not render the link when it is not available', function() {
render(
<PreviewLogEntry
content={logContent}
humanReadableHintComponent={logHint}
level={level}
onLogEntryLinkClick={noOp}
/>
)
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist
})
})
})
@@ -0,0 +1,122 @@
import React from 'react'
import PropTypes from 'prop-types'
import { expect } from 'chai'
import { render } from '@testing-library/react'
import useExpandCollapse from '../../../../frontend/js/shared/hooks/use-expand-collapse'
const sampleContent = (
<div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<p>Maecenas maximus ultrices sollicitudin.</p>
<p>Praesent mollis arcu eget molestie viverra.</p>
<p>Pellentesque eget molestie nisl, non hendrerit lectus.</p>
</div>
)
function ExpandCollapseTestUI({ expandCollapseArgs }) {
const { expandableProps } = useExpandCollapse(expandCollapseArgs)
return (
<>
<div {...expandableProps}>{sampleContent}</div>
</>
)
}
ExpandCollapseTestUI.propTypes = {
expandCollapseArgs: PropTypes.object
}
describe('useExpandCollapse', function() {
describe('custom CSS classes', function() {
it('supports a custom CSS class', function() {
const testArgs = {
classes: {
container: 'my-custom-class'
}
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const elWithCustomCSSClass = container.querySelector('div')
expect(elWithCustomCSSClass).to.exist
})
it('supports an extra custom CSS class for the collapsed state', function() {
const testArgs = {
classes: {
containerCollapsed: 'my-custom-collapsed-class'
}
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const elWithCustomCollapsedCSSClass = container.querySelector(
'.my-custom-collapsed-class'
)
expect(elWithCustomCollapsedCSSClass).to.exist
})
it('ignores the collapsed CSS class when expanded', function() {
const testArgs = {
initiallyExpanded: true,
classes: {
containerCollapsed: 'my-custom-collapsed-class'
}
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const elWithCustomCollapsedCSSClass = container.querySelector(
'.my-custom-collapsed-class'
)
expect(elWithCustomCollapsedCSSClass).to.not.exist
})
})
describe('height and width support via dimension argument', function() {
it('defaults to height', function() {
const { container } = render(<ExpandCollapseTestUI />)
const expandCollapseEl = container.firstChild
expect(expandCollapseEl.style.height).to.not.be.empty
expect(expandCollapseEl.style.width).to.be.empty
})
it('supports width', function() {
const testArgs = {
dimension: 'width'
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const expandCollapseEl = container.firstChild
expect(expandCollapseEl.style.height).to.be.empty
expect(expandCollapseEl.style.width).to.not.be.empty
})
})
describe('collapsed size support via collapsedSize argument', function() {
it('defaults to 0px', function() {
const { container } = render(<ExpandCollapseTestUI />)
const expandCollapseEl = container.firstChild
expect(expandCollapseEl.style.height).to.equal('0px')
})
it('supports a custom collapsed size', function() {
const testArgs = {
collapsedSize: 200
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const expandCollapseEl = container.firstChild
expect(expandCollapseEl.style.height).to.equal('200px')
})
it('supports a custom collapsed size for width', function() {
const testArgs = {
collapsedSize: 200,
dimension: 'width'
}
const { container } = render(
<ExpandCollapseTestUI expandCollapseArgs={testArgs} />
)
const expandCollapseEl = container.firstChild
expect(expandCollapseEl.style.height).to.be.empty
expect(expandCollapseEl.style.width).to.equal('200px')
})
})
})