From 619ec15309bcc4a27c2ff86dc32b52708ad83e7a Mon Sep 17 00:00:00 2001 From: Paulo Jorge Reis Date: Mon, 16 Nov 2020 10:01:01 +0000 Subject: [PATCH] Add first error popup (#3369) * Add first error popup * Address PR feedback GitOrigin-RevId: e924b3e6096584de6f363aae70a62328cd3de83d --- services/web/app/views/project/editor/pdf.pug | 3 +- .../frontend/extracted-translation-keys.json | 4 + .../components/preview-first-error-pop-up.js | 63 +++++ .../preview/components/preview-log-entry.js | 46 +++- .../preview/components/preview-logs-pane.js | 6 +- .../preview/components/preview-pane.js | 48 +++- .../web/frontend/js/ide/pdf/PdfManager.js | 3 +- .../js/ide/pdf/controllers/PdfController.js | 3 + .../web/frontend/stylesheets/app/base.less | 17 ++ .../frontend/stylesheets/app/editor/logs.less | 35 ++- .../stylesheets/app/editor/review-panel.less | 32 --- .../stylesheets/app/project-list.less | 17 -- .../web/frontend/stylesheets/core/mixins.less | 39 +++ services/web/locales/en.json | 4 + .../components/preview-log-entry.test.js | 34 +-- .../preview/components/preview-pane.test.js | 227 ++++++++++++++++++ 16 files changed, 494 insertions(+), 87 deletions(-) create mode 100644 services/web/frontend/js/features/preview/components/preview-first-error-pop-up.js create mode 100644 services/web/test/frontend/features/preview/components/preview-pane.test.js 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 + } + } +})