diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug
index 9bb196a5cb..ed4ac3b4a1 100644
--- a/services/web/app/views/project/editor/pdf.pug
+++ b/services/web/app/views/project/editor/pdf.pug
@@ -7,6 +7,7 @@ div.full-size.pdf(ng-controller="PdfController")
isCompiling: pdf.compiling,
isDraftModeOn: draft,
isSyntaxCheckOn: stop_on_validation_error,
+ lastCompileTimestamp: pdf.lastCompileTimestamp,
logEntries: pdf.logEntries ? pdf.logEntries : {}
}`
on-clear-cache="clearCache"
@@ -19,7 +20,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"
+ on-log-entry-location-click="openInEditor"
)
else
.toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }")
diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json
index 77ec3f01f3..5e3a25b57a 100644
--- a/services/web/frontend/extracted-translation-keys.json
+++ b/services/web/frontend/extracted-translation-keys.json
@@ -3,12 +3,15 @@
"collapse",
"compile_mode",
"compiling",
+ "dismiss_error_popup",
"download_file",
"download_pdf",
"expand",
"fast",
"file_outline",
"find_out_more_about_the_file_outline",
+ "first_error_popup_label",
+ "go_to_error_location",
"hide_outline",
"ignore_validation_errors",
"loading",
@@ -34,6 +37,7 @@
"the_file_outline_is_a_new_feature_click_the_icon_to_learn_more",
"toggle_compile_options_menu",
"toggle_output_files_list",
+ "view_all_errors",
"view_logs",
"view_pdf",
"view_warnings",
diff --git a/services/web/frontend/js/features/preview/components/preview-first-error-pop-up.js b/services/web/frontend/js/features/preview/components/preview-first-error-pop-up.js
new file mode 100644
index 0000000000..f3043ad1ec
--- /dev/null
+++ b/services/web/frontend/js/features/preview/components/preview-first-error-pop-up.js
@@ -0,0 +1,63 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { useTranslation } from 'react-i18next'
+import Icon from '../../../shared/components/icon'
+import PreviewLogEntry from './preview-log-entry'
+
+function PreviewFirstErrorPopUp({
+ logEntry,
+ onGoToErrorLocation,
+ onViewLogs,
+ onClose
+}) {
+ const { t } = useTranslation()
+
+ function handleGoToErrorLocation() {
+ const { file, line, column } = logEntry
+ onGoToErrorLocation({ file, line, column })
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+PreviewFirstErrorPopUp.propTypes = {
+ logEntry: PropTypes.object.isRequired,
+ onGoToErrorLocation: PropTypes.func.isRequired,
+ onViewLogs: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired
+}
+
+export default PreviewFirstErrorPopUp
diff --git a/services/web/frontend/js/features/preview/components/preview-log-entry.js b/services/web/frontend/js/features/preview/components/preview-log-entry.js
index 25d1b07b83..192844bb41 100644
--- a/services/web/frontend/js/features/preview/components/preview-log-entry.js
+++ b/services/web/frontend/js/features/preview/components/preview-log-entry.js
@@ -14,11 +14,14 @@ function PreviewLogEntry({
humanReadableHintComponent,
extraInfoURL,
level,
- onLogEntryLinkClick
+ showLineAndNoLink = true,
+ showCloseButton = false,
+ onLogEntryLocationClick,
+ onClose
}) {
const { t } = useTranslation()
function handleLogEntryLinkClick() {
- onLogEntryLinkClick({ file, line, column })
+ onLogEntryLocationClick({ file, line, column })
}
const logEntryDescription = t('log_entry_description', {
level: level
@@ -30,7 +33,10 @@ function PreviewLogEntry({
file={file}
line={line}
message={message}
- onLogEntryLinkClick={handleLogEntryLinkClick}
+ showLineAndNoLink={showLineAndNoLink}
+ onLogEntryLocationClick={handleLogEntryLinkClick}
+ showCloseButton={showCloseButton}
+ onClose={onClose}
/>
{content ? (
{message}
- {file ? (
+ {showLineAndNoLink && file ? (
) : null}
+ {showCloseButton && file ? (
+
+ ) : null}
)
}
@@ -148,7 +168,10 @@ PreviewLogEntryHeader.propTypes = {
file: PropTypes.string,
line: PropTypes.any,
message: PropTypes.string,
- onLogEntryLinkClick: PropTypes.func.isRequired
+ showLineAndNoLink: PropTypes.bool,
+ showCloseButton: PropTypes.bool,
+ onLogEntryLocationClick: PropTypes.func,
+ onClose: PropTypes.func
}
PreviewLogEntryContent.propTypes = {
@@ -172,7 +195,10 @@ PreviewLogEntry.propTypes = {
humanReadableHintComponent: PropTypes.node,
extraInfoURL: PropTypes.string,
level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
- onLogEntryLinkClick: PropTypes.func.isRequired
+ showLineAndNoLink: PropTypes.bool,
+ showCloseButton: PropTypes.bool,
+ onLogEntryLocationClick: PropTypes.func,
+ onClose: PropTypes.func
}
export default PreviewLogEntry
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane.js b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
index 82748a37a6..c4013c6134 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import PreviewLogEntry from './preview-log-entry'
-function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
+function PreviewLogsPane({ logEntries, onLogEntryLocationClick }) {
return (
{logEntries && logEntries.length > 0 ? (
@@ -10,7 +10,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
))
) : (
@@ -22,7 +22,7 @@ function PreviewLogsPane({ logEntries, onLogEntryLinkClick }) {
PreviewLogsPane.propTypes = {
logEntries: PropTypes.array,
- onLogEntryLinkClick: PropTypes.func.isRequired
+ onLogEntryLocationClick: PropTypes.func.isRequired
}
export default PreviewLogsPane
diff --git a/services/web/frontend/js/features/preview/components/preview-pane.js b/services/web/frontend/js/features/preview/components/preview-pane.js
index 8677dcdd2a..d7be97b551 100644
--- a/services/web/frontend/js/features/preview/components/preview-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-pane.js
@@ -1,7 +1,8 @@
-import React from 'react'
+import React, { useState } from 'react'
import PropTypes from 'prop-types'
import PreviewToolbar from './preview-toolbar'
import PreviewLogsPane from './preview-logs-pane'
+import PreviewFirstErrorPopUp from './preview-first-error-pop-up'
import { useTranslation } from 'react-i18next'
function PreviewPane({
@@ -15,10 +16,30 @@ function PreviewPane({
onToggleLogs,
outputFiles,
pdfDownloadUrl,
- onLogEntryLinkClick,
+ onLogEntryLocationClick,
showLogs
}) {
const { t } = useTranslation()
+
+ const [lastCompileTimestamp, setLastCompileTimestamp] = useState(
+ compilerState.lastCompileTimestamp
+ )
+ const [seenLogsForCurrentCompile, setSeenLogsForCurrentCompile] = useState(
+ false
+ )
+ const [dismissedFirstErrorPopUp, setDismissedFirstErrorPopUp] = useState(
+ false
+ )
+
+ if (lastCompileTimestamp < compilerState.lastCompileTimestamp) {
+ setLastCompileTimestamp(compilerState.lastCompileTimestamp)
+ setSeenLogsForCurrentCompile(false)
+ }
+
+ if (showLogs && !seenLogsForCurrentCompile) {
+ setSeenLogsForCurrentCompile(true)
+ }
+
const nErrors =
compilerState.logEntries && compilerState.logEntries.errors
? compilerState.logEntries.errors.length
@@ -32,6 +53,16 @@ function PreviewPane({
? compilerState.logEntries.all.length
: 0
+ const showFirstErrorPopUp =
+ nErrors > 0 &&
+ !seenLogsForCurrentCompile &&
+ !dismissedFirstErrorPopUp &&
+ !compilerState.isCompiling
+
+ function handleFirstErrorPopUpClose() {
+ setDismissedFirstErrorPopUp(true)
+ }
+
return (
<>
+ {showFirstErrorPopUp ? (
+
+ ) : null}
{showLogs ? (
) : null}
>
@@ -74,10 +113,11 @@ PreviewPane.propTypes = {
isCompiling: PropTypes.bool.isRequired,
isDraftModeOn: PropTypes.bool.isRequired,
isSyntaxCheckOn: PropTypes.bool.isRequired,
+ lastCompileTimestamp: PropTypes.number,
logEntries: PropTypes.object.isRequired
}),
onClearCache: PropTypes.func.isRequired,
- onLogEntryLinkClick: PropTypes.func.isRequired,
+ onLogEntryLocationClick: PropTypes.func.isRequired,
onRecompile: PropTypes.func.isRequired,
onRunSyntaxCheckNow: PropTypes.func.isRequired,
onSetAutoCompile: PropTypes.func.isRequired,
diff --git a/services/web/frontend/js/ide/pdf/PdfManager.js b/services/web/frontend/js/ide/pdf/PdfManager.js
index ea2a661b2d..00be66ca21 100644
--- a/services/web/frontend/js/ide/pdf/PdfManager.js
+++ b/services/web/frontend/js/ide/pdf/PdfManager.js
@@ -31,7 +31,8 @@ export default (PdfManager = class PdfManager {
view: null, // 'pdf' 'logs'
showRawLog: false,
highlights: [],
- position: null
+ position: null,
+ lastCompileTimestamp: null
}
}
})
diff --git a/services/web/frontend/js/ide/pdf/controllers/PdfController.js b/services/web/frontend/js/ide/pdf/controllers/PdfController.js
index ea799e5b1b..c003660972 100644
--- a/services/web/frontend/js/ide/pdf/controllers/PdfController.js
+++ b/services/web/frontend/js/ide/pdf/controllers/PdfController.js
@@ -361,6 +361,7 @@ App.controller('PdfController', function(
if (response.status === 'success') {
$scope.pdf.view = 'pdf'
$scope.shouldShowLogs = false
+ $scope.pdf.lastCompileTimestamp = Date.now()
// define the base url. if the pdf file has a build number, pass it to the clsi in the url
if (fileByPath['output.pdf'] && fileByPath['output.pdf'].url) {
@@ -516,6 +517,8 @@ App.controller('PdfController', function(
function fetchLogs(fileByPath, options) {
let blgFile, chktexFile, logFile
+ $scope.pdf.logEntries = {}
+
if (options != null ? options.validation : undefined) {
chktexFile = fileByPath['output.chktex']
} else {
diff --git a/services/web/frontend/stylesheets/app/base.less b/services/web/frontend/stylesheets/app/base.less
index 2030ed45ec..0552e8e925 100644
--- a/services/web/frontend/stylesheets/app/base.less
+++ b/services/web/frontend/stylesheets/app/base.less
@@ -111,6 +111,23 @@
}
}
+@keyframes pulse {
+ 0% {
+ opacity: 0.7;
+ }
+ 100% {
+ opacity: 0.9;
+ }
+}
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
.bounce {
-webkit-animation-duration: 2s;
animation-duration: 2s;
diff --git a/services/web/frontend/stylesheets/app/editor/logs.less b/services/web/frontend/stylesheets/app/editor/logs.less
index f9ec0f74d3..45e2873533 100644
--- a/services/web/frontend/stylesheets/app/editor/logs.less
+++ b/services/web/frontend/stylesheets/app/editor/logs.less
@@ -118,10 +118,7 @@
.log-entry-btn-expand-collapse {
position: relative;
z-index: 1;
- &:focus,
- &:focus:active {
- outline: 0;
- }
+ .no-outline-ring-on-click;
}
.log-entry-human-readable-hint,
@@ -129,3 +126,33 @@
font-size: @font-size-small;
margin-top: @margin-sm;
}
+
+.first-error-popup {
+ position: absolute;
+ z-index: 1;
+ top: @toolbar-small-height + 2px;
+ right: @padding-xs;
+ border-radius: @border-radius-base;
+ width: 90%;
+ max-width: 450px;
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
+ animation: fade-in 0.15s linear 0s 1 none;
+ background-color: #fff;
+ &::before {
+ content: '';
+ .triangle(top, @padding-sm, @padding-xs, @ol-red);
+ top: -@padding-xs;
+ right: @padding-md;
+ }
+}
+
+.first-error-popup-actions {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 @padding-sm @padding-sm @padding-sm;
+ margin-top: -@margin-sm;
+}
+
+.first-error-btn {
+ .no-outline-ring-on-click;
+}
diff --git a/services/web/frontend/stylesheets/app/editor/review-panel.less b/services/web/frontend/stylesheets/app/editor/review-panel.less
index 05af99a52c..302298323e 100644
--- a/services/web/frontend/stylesheets/app/editor/review-panel.less
+++ b/services/web/frontend/stylesheets/app/editor/review-panel.less
@@ -64,38 +64,6 @@
}
}
-.triangle(@_, @width, @height, @color) {
- position: absolute;
- border-color: transparent;
- border-style: solid;
- width: 0;
- height: 0;
-}
-.triangle(top, @width, @height, @color) {
- border-width: 0 @width / 2 @height @width / 2;
- border-bottom-color: @color;
- border-left-color: transparent;
- border-right-color: transparent;
-}
-.triangle(bottom, @width, @height, @color) {
- border-width: @height @width / 2 0 @width / 2;
- border-top-color: @color;
- border-left-color: transparent;
- border-right-color: transparent;
-}
-.triangle(right, @width, @height, @color) {
- border-width: @height / 2 0 @height / 2 @width;
- border-left-color: @color;
- border-top-color: transparent;
- border-bottom-color: transparent;
-}
-.triangle(left, @width, @height, @color) {
- border-width: @height / 2 @width @height / 2 0;
- border-right-color: @color;
- border-top-color: transparent;
- border-bottom-color: transparent;
-}
-
#review-panel {
display: block;
.rp-size-expanded & {
diff --git a/services/web/frontend/stylesheets/app/project-list.less b/services/web/frontend/stylesheets/app/project-list.less
index 903b2aca00..b74fb5d607 100644
--- a/services/web/frontend/stylesheets/app/project-list.less
+++ b/services/web/frontend/stylesheets/app/project-list.less
@@ -1,22 +1,5 @@
@import './list/v1-import-modal.less';
-@keyframes pulse {
- 0% {
- opacity: 0.7;
- }
- 100% {
- opacity: 0.9;
- }
-}
-@keyframes fade-in {
- 0% {
- opacity: 0;
- }
- 100% {
- opacity: 1;
- }
-}
-
.project-list-page {
position: absolute;
top: @header-height;
diff --git a/services/web/frontend/stylesheets/core/mixins.less b/services/web/frontend/stylesheets/core/mixins.less
index 4f69b7344c..8610aa96d0 100755
--- a/services/web/frontend/stylesheets/core/mixins.less
+++ b/services/web/frontend/stylesheets/core/mixins.less
@@ -1095,3 +1095,42 @@
height: auto;
}
}
+
+.triangle(@_, @width, @height, @color) {
+ position: absolute;
+ border-color: transparent;
+ border-style: solid;
+ width: 0;
+ height: 0;
+}
+.triangle(top, @width, @height, @color) {
+ border-width: 0 @width / 2 @height @width / 2;
+ border-bottom-color: @color;
+ border-left-color: transparent;
+ border-right-color: transparent;
+}
+.triangle(bottom, @width, @height, @color) {
+ border-width: @height @width / 2 0 @width / 2;
+ border-top-color: @color;
+ border-left-color: transparent;
+ border-right-color: transparent;
+}
+.triangle(right, @width, @height, @color) {
+ border-width: @height / 2 0 @height / 2 @width;
+ border-left-color: @color;
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+}
+.triangle(left, @width, @height, @color) {
+ border-width: @height / 2 @width @height / 2 0;
+ border-right-color: @color;
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+}
+
+.no-outline-ring-on-click {
+ &:focus,
+ &:focus:active {
+ outline: 0;
+ }
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 05a2389abd..85e3dd625d 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1,4 +1,8 @@
{
+ "first_error_popup_label": "Your project has errors. This is the first one.",
+ "dismiss_error_popup": "Dismiss first error alert",
+ "go_to_error_location": "Go to error location",
+ "view_all_errors": "View all errors",
"log_entry_description": "Log entry with level: \"__level__\"",
"navigate_log_source": "Navigate to log position in source code: __location__",
"other_output_files": "Other output files",
diff --git a/services/web/test/frontend/features/preview/components/preview-log-entry.test.js b/services/web/test/frontend/features/preview/components/preview-log-entry.test.js
index 28362f44f0..638c2140c9 100644
--- a/services/web/test/frontend/features/preview/components/preview-log-entry.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-log-entry.test.js
@@ -12,7 +12,7 @@ describe('', function() {
describe('log entry description', function() {
for (const level of ['error', 'warning', 'typesetting']) {
it(`describes the log entry with ${level} information`, function() {
- render()
+ render()
screen.getByLabelText(`Log entry with level: "${level}"`)
})
}
@@ -22,10 +22,10 @@ describe('', function() {
const file = 'foo.tex'
const line = 42
const column = 21
- const onLogEntryLinkClick = sinon.stub()
+ const onLogEntryLocationClick = sinon.stub()
afterEach(function() {
- onLogEntryLinkClick.reset()
+ onLogEntryLocationClick.reset()
})
it('renders both file and line', function() {
@@ -34,7 +34,7 @@ describe('', function() {
file={file}
line={line}
level={level}
- onLogEntryLinkClick={noOp}
+ onLogEntryLocationClick={noOp}
/>
)
screen.getByRole('button', {
@@ -44,7 +44,11 @@ describe('', function() {
it('renders only file when line information is not available', function() {
render(
-
+
)
screen.getByRole('button', {
name: `Navigate to log position in source code: ${file}`
@@ -52,7 +56,7 @@ describe('', function() {
})
it('does not render when file information is not available', function() {
- render()
+ render()
expect(
screen.queryByRole('button', {
name: `Navigate to log position in source code: `
@@ -67,7 +71,7 @@ describe('', function() {
line={line}
column={column}
level={level}
- onLogEntryLinkClick={onLogEntryLinkClick}
+ onLogEntryLocationClick={onLogEntryLocationClick}
/>
)
const linkToSourceButton = screen.getByRole('button', {
@@ -75,8 +79,8 @@ describe('', function() {
})
fireEvent.click(linkToSourceButton)
- expect(onLogEntryLinkClick).to.be.calledOnce
- expect(onLogEntryLinkClick).to.be.calledWith({
+ expect(onLogEntryLocationClick).to.be.calledOnce
+ expect(onLogEntryLocationClick).to.be.calledWith({
file,
line: line,
column: column
@@ -92,7 +96,7 @@ describe('', function() {
)
screen.getByText(logContent)
@@ -106,7 +110,7 @@ describe('', function() {
)
screen.getByText(logContent)
@@ -121,7 +125,7 @@ describe('', function() {
it('should not render at all when there are no log contents', function() {
const { container } = render(
-
+
)
expect(container.querySelector('.log-entry-content')).to.not.exist
})
@@ -140,7 +144,7 @@ describe('', function() {
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
- onLogEntryLinkClick={noOp}
+ onLogEntryLocationClick={noOp}
/>
)
screen.getByText(logHintText)
@@ -153,7 +157,7 @@ describe('', function() {
humanReadableHintComponent={logHint}
extraInfoURL={infoURL}
level={level}
- onLogEntryLinkClick={noOp}
+ onLogEntryLocationClick={noOp}
/>
)
screen.getByRole('link', { name: 'Learn more' })
@@ -165,7 +169,7 @@ describe('', function() {
content={logContent}
humanReadableHintComponent={logHint}
level={level}
- onLogEntryLinkClick={noOp}
+ onLogEntryLocationClick={noOp}
/>
)
expect(screen.queryByRole('link', { name: 'Learn more' })).to.not.exist
diff --git a/services/web/test/frontend/features/preview/components/preview-pane.test.js b/services/web/test/frontend/features/preview/components/preview-pane.test.js
new file mode 100644
index 0000000000..9396e461d3
--- /dev/null
+++ b/services/web/test/frontend/features/preview/components/preview-pane.test.js
@@ -0,0 +1,227 @@
+import React from 'react'
+import { screen, render, fireEvent } from '@testing-library/react'
+import PreviewPane from '../../../../../frontend/js/features/preview/components/preview-pane'
+const { expect } = require('chai')
+
+describe('', function() {
+ const sampleError1 = {
+ content: 'error 1 content',
+ file: 'main.tex',
+ level: 'error',
+ line: 17,
+ message: 'Misplaced alignment tab character &.'
+ }
+ const sampleError2 = {
+ content: 'error 1 content',
+ file: 'main.tex',
+ level: 'error',
+ line: 22,
+ message: 'Extra alignment tab has been changed to cr.'
+ }
+ const sampleWarning = {
+ file: 'main.tex',
+ level: 'warning',
+ line: 30,
+ message: "Reference `idontexist' on page 1 undefined on input line 30."
+ }
+
+ describe('first error pop-up', function() {
+ it('renders a first error pop-up with the first error', function() {
+ const propsAfterCompileWithErrors = getProps(false, {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ })
+ render()
+ screen.getByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ screen.getByText(sampleError1.message)
+ })
+
+ it('does not render a first error pop-up when there are only warnings', function() {
+ const propsAfterCompileWithWarningsOnly = getProps(false, {
+ errors: [],
+ warnings: [sampleWarning]
+ })
+ render()
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+
+ it('does not render a first error pop-up when a compile is ongoing', function() {
+ const propsWhileCompiling = getProps(true, {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ })
+ render()
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+
+ it('does not render a first error pop-up when viewing logs', function() {
+ const propsWithErrorsViewingLogs = getProps(
+ false,
+ {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ },
+ Date.now(),
+ true
+ )
+ render()
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+
+ it('does not render a first error pop-up when going back to the PDF view after viewing logs', function() {
+ const nowTimestamp = Date.now()
+ const propsWithErrorsViewingLogs = getProps(
+ false,
+ {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ },
+ nowTimestamp,
+ true
+ )
+ const propsWithErrorsAfterViewingLogs = getProps(
+ false,
+ {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ },
+ nowTimestamp,
+ false
+ )
+ const { rerender } = render(
+
+ )
+ rerender()
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+
+ it('renders a first error pop-up with updated errors after recompiling', function() {
+ const nowTimestamp = Date.now()
+ const laterTimestamp = Date.now() + 1000
+ const propsWithErrorsAfterFirstCompile = getProps(
+ false,
+ {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ },
+ nowTimestamp,
+ true
+ )
+ const propsWithErrorsAfterSecondCompile = getProps(
+ false,
+ {
+ errors: [sampleError2],
+ warnings: [sampleWarning]
+ },
+ laterTimestamp,
+ false
+ )
+ const { rerender } = render(
+
+ )
+ rerender()
+ screen.getByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ screen.getByText(sampleError2.message)
+ })
+
+ it('allows dismissing the first error pop-up', function() {
+ const propsWithErrors = getProps(false, {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ })
+ render()
+ const dismissPopUpButton = screen.getByRole('button', {
+ name: 'Dismiss first error alert'
+ })
+
+ fireEvent.click(dismissPopUpButton)
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+
+ it('does not render the first error pop-up with new recompiles after it being dismissed once', function() {
+ const nowTimestamp = Date.now()
+ const laterTimestamp = Date.now() + 1000
+ const propsWithErrorsForFirstCompile = getProps(
+ false,
+ {
+ errors: [sampleError1, sampleError2],
+ warnings: [sampleWarning]
+ },
+ nowTimestamp
+ )
+ const propsWithErrorsForSecondCompile = getProps(
+ false,
+ {
+ errors: [sampleError2],
+ warnings: [sampleWarning]
+ },
+ laterTimestamp
+ )
+ const { rerender } = render(
+
+ )
+ const dismissPopUpButton = screen.getByRole('button', {
+ name: 'Dismiss first error alert'
+ })
+ fireEvent.click(dismissPopUpButton)
+ rerender()
+ expect(
+ screen.queryByRole('alertdialog', {
+ name: 'Your project has errors. This is the first one.'
+ })
+ ).to.not.exist
+ })
+ })
+
+ function getProps(
+ isCompiling = false,
+ logEntries = {},
+ lastCompileTimestamp = Date.now(),
+ isShowingLogs = false
+ ) {
+ return {
+ compilerState: {
+ isAutoCompileOn: false,
+ isCompiling: isCompiling,
+ isClearingCache: false,
+ isDraftModeOn: false,
+ isSyntaxCheckOn: false,
+ lastCompileTimestamp: lastCompileTimestamp,
+ logEntries: logEntries
+ },
+ onClearCache: () => {},
+ onLogEntryLocationClick: () => {},
+ onRecompile: () => {},
+ onRunSyntaxCheckNow: () => {},
+ onSetAutoCompile: () => {},
+ onSetDraftMode: () => {},
+ onSetSyntaxCheck: () => {},
+ onToggleLogs: () => {},
+ showLogs: isShowingLogs
+ }
+ }
+})