Merge pull request #1681 from emilyanndavis/feature/1368-display-changelog-in-workbench

Feature/1368 display changelog in workbench
This commit is contained in:
Dave Fisher 2024-11-13 06:27:59 -08:00 committed by GitHub
commit 3fc95d21e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 3632 additions and 37 deletions

View File

@ -53,7 +53,24 @@ jobs:
perl -0777 -i -pe \
"s/Unreleased Changes\n------------------/..\n Unreleased Changes\n ------------------\n\n${HEADER}\n${UNDERLINE}/g" \
HISTORY.rst
- name: Install dependencies
run: pip install rst2html5
- name: Generate changelog.html
run: rst2html5 HISTORY.rst workbench/changelog.html
- name: Update package.json version
uses: BellCubeDev/update-package-version-by-release-tag@v2
with:
version: ${{ inputs.version }}
package-json-path: workbench/package.json
- name: Commit updated HISTORY.rst, changelog.html, and package.json
run: |
git add HISTORY.rst
git add workbench/changelog.html
git add workbench/package.json
git commit -m "Committing the $VERSION release."
- name: Tag and push

View File

@ -48,6 +48,21 @@ Unreleased Changes
reflect changes in how InVEST is installed on modern systems, and also to
include images of the InVEST workbench instead of just broken links.
https://github.com/natcap/invest/issues/1660
* Workbench
* Several small updates to the model input form UI to improve usability
and visual consistency (https://github.com/natcap/invest/issues/912).
* Fixed a bug that caused the application to crash when attempting to
open a workspace without a valid logfile
(https://github.com/natcap/invest/issues/1598).
* Fixed a bug that was allowing readonly workspace directories on Windows
(https://github.com/natcap/invest/issues/1599).
* Fixed a bug that, in certain scenarios, caused a datastack to be saved
with relative paths when the Relative Paths checkbox was left unchecked
(https://github.com/natcap/invest/issues/1609).
* Improved error handling when a datastack cannot be saved with relative
paths across drives (https://github.com/natcap/invest/issues/1608).
* The InVEST changelog now displays in the Workbench the first time a new
version is launched (https://github.com/natcap/invest/issues/1368).
* Coastal Vulnerability
* Fixed a regression where an AOI with multiple features could raise a
TypeError after intersecting with the landmass polygon.
@ -75,19 +90,6 @@ Unreleased Changes
* The model now works as expected when the user provides an LULC raster
that does not have a nodata value defined.
https://github.com/natcap/invest/issues/1293
* Workbench
* Several small updates to the model input form UI to improve usability
and visual consistency (https://github.com/natcap/invest/issues/912).
* Fixed a bug that caused the application to crash when attempting to
open a workspace without a valid logfile
(https://github.com/natcap/invest/issues/1598).
* Fixed a bug that was allowing readonly workspace directories on Windows
(https://github.com/natcap/invest/issues/1599).
* Fixed a bug that, in certain scenarios, caused a datastack to be saved
with relative paths when the Relative Paths checkbox was left unchecked
(https://github.com/natcap/invest/issues/1609).
* Improved error handling when a datastack cannot be saved with relative
paths across drives (https://github.com/natcap/invest/issues/1608).
3.14.2 (2024-05-29)
-------------------

View File

@ -66,6 +66,7 @@ PYTHON_ARCH := $(shell $(PYTHON) -c "import sys; print('x86' if sys.maxsize <= 2
GSUTIL := gsutil
SIGNTOOL := SignTool
RST2HTML5 := rst2html5
# local directory names
DIST_DIR := dist
@ -73,6 +74,8 @@ DIST_DATA_DIR := $(DIST_DIR)/data
BUILD_DIR := build
WORKBENCH := workbench
WORKBENCH_DIST_DIR := $(WORKBENCH)/dist
CHANGELOG_SRC := HISTORY.rst
CHANGELOG_DEST := $(WORKBENCH)/changelog.html
# The fork name and user here are derived from the git path on github.
# The fork name will need to be set manually (e.g. make FORKNAME=natcap/invest)
@ -141,6 +144,7 @@ help:
@echo " binaries to build pyinstaller binaries"
@echo " apidocs to build HTML API documentation"
@echo " userguide to build HTML version of the users guide"
@echo " changelog to build HTML version of the changelog"
@echo " python_packages to build natcap.invest wheel and source distributions"
@echo " codesign_mac to sign the mac disk image using the codesign utility"
@echo " codesign_windows to sign the windows installer using the SignTool utility"
@ -366,6 +370,9 @@ deploy:
@echo "Application binaries (if they were created) can be downloaded from:"
@echo " * $(DOWNLOAD_DIR_URL)"
changelog:
$(RST2HTML5) $(CHANGELOG_SRC) $(CHANGELOG_DEST)
# Notes on Makefile development
#
# * Use the -drR to show the decision tree (and none of the implicit rules)

View File

@ -26,3 +26,4 @@ requests
coverage
xlwt
build # pip-only
rst2html5

3233
workbench/changelog.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "invest-workbench",
"version": "0.1.0",
"version": "3.14.2",
"description": "Models that map and value the goods and services from nature that sustain and fulfill human life",
"main": "build/main/main.js",
"homepage": "./",

View File

@ -1,4 +1,5 @@
export const ipcMainChannels = {
BASE_URL: 'base-url',
CHANGE_LANGUAGE: 'change-language',
CHECK_FILE_PERMISSIONS: 'check-file-permissions',
CHECK_STORAGE_TOKEN: 'check-storage-token',
@ -12,6 +13,7 @@ export const ipcMainChannels = {
INVEST_RUN: 'invest-run',
INVEST_VERSION: 'invest-version',
IS_FIRST_RUN: 'is-first-run',
IS_NEW_VERSION: 'is-new-version',
LOGGER: 'logger',
OPEN_EXTERNAL_URL: 'open-external-url',
OPEN_PATH: 'open-path',

View File

@ -1,5 +1,5 @@
import path from 'path';
import i18n from './i18n/i18n';
// eslint-disable-next-line import/no-extraneous-dependencies
import {
app,
@ -9,35 +9,37 @@ import {
ipcMain
} from 'electron';
import BASE_URL from './baseUrl';
import {
createPythonFlaskProcess,
getFlaskIsReady,
shutdownPythonProcess
shutdownPythonProcess,
} from './createPythonFlaskProcess';
import findInvestBinaries from './findInvestBinaries';
import setupDownloadHandlers from './setupDownloadHandlers';
import setupDialogs from './setupDialogs';
import setupContextMenu from './setupContextMenu';
import { ipcMainChannels } from './ipcMainChannels';
import ELECTRON_DEV_MODE from './isDevMode';
import { getLogger } from './logger';
import menuTemplate from './menubar';
import pkg from '../../package.json';
import { settingsStore, setupSettingsHandlers } from './settingsStore';
import { setupBaseUrl } from './setupBaseUrl';
import setupCheckFilePermissions from './setupCheckFilePermissions';
import { setupCheckFirstRun } from './setupCheckFirstRun';
import { setupCheckStorageToken } from './setupCheckStorageToken';
import {
setupInvestRunHandlers,
setupInvestLogReaderHandler
} from './setupInvestHandlers';
import setupContextMenu from './setupContextMenu';
import setupDialogs from './setupDialogs';
import setupDownloadHandlers from './setupDownloadHandlers';
import setupGetElectronPaths from './setupGetElectronPaths';
import setupGetNCPUs from './setupGetNCPUs';
import {
setupInvestLogReaderHandler,
setupInvestRunHandlers,
} from './setupInvestHandlers';
import { setupIsNewVersion } from './setupIsNewVersion';
import setupOpenExternalUrl from './setupOpenExternalUrl';
import setupOpenLocalHtml from './setupOpenLocalHtml';
import { settingsStore, setupSettingsHandlers } from './settingsStore';
import setupGetElectronPaths from './setupGetElectronPaths';
import setupRendererLogger from './setupRendererLogger';
import { ipcMainChannels } from './ipcMainChannels';
import menuTemplate from './menubar';
import ELECTRON_DEV_MODE from './isDevMode';
import BASE_URL from './baseUrl';
import { getLogger } from './logger';
import i18n from './i18n/i18n';
import pkg from '../../package.json';
const logger = getLogger(__filename.split('/').slice(-1)[0]);
@ -87,6 +89,7 @@ export const createWindow = async () => {
setupDialogs();
setupCheckFilePermissions();
setupCheckFirstRun();
setupIsNewVersion();
setupCheckStorageToken();
setupSettingsHandlers();
setupGetElectronPaths();
@ -94,6 +97,7 @@ export const createWindow = async () => {
setupInvestLogReaderHandler();
setupOpenExternalUrl();
setupRendererLogger();
setupBaseUrl();
await getFlaskIsReady();
const devModeArg = ELECTRON_DEV_MODE ? '--devmode' : '';

View File

@ -0,0 +1,13 @@
import {
ipcMain,
} from 'electron';
import { ipcMainChannels } from './ipcMainChannels';
import baseUrl from './baseUrl';
export function setupBaseUrl() {
ipcMain.handle(
ipcMainChannels.BASE_URL, () => baseUrl
);
}

View File

@ -0,0 +1,52 @@
import fs from 'fs';
import path from 'path';
import {
app,
ipcMain,
} from 'electron';
import { ipcMainChannels } from './ipcMainChannels';
import { getLogger } from './logger';
import pkg from '../../package.json';
const logger = getLogger(__filename.split('/').slice(-1)[0]);
export const APP_VERSION_TOKEN = 'app-version-token.txt';
/** Determine whether this is the first run of the current running version.
*
* @returns {boolean} true if this version has not run before, otherwise false
*/
export async function isNewVersion() {
// Getting version from package.json is simplest because there is no need to
// spawn an invest process simply to get the version of the installed binary.
const currentVersion = pkg.version;
const userDataPath = app.getPath('userData');
const tokenPath = path.join(userDataPath, APP_VERSION_TOKEN);
try {
if (fs.existsSync(tokenPath)) {
const tokenString = fs.readFileSync(tokenPath, {encoding: 'utf8'});
if (tokenString) {
const installedVersionList = tokenString.split(',');
if (installedVersionList.includes(currentVersion)) {
return false;
}
// If current version not found, add it
fs.writeFileSync(tokenPath, `${tokenString},${currentVersion}`);
return true;
}
}
// If file does not exist, create it
fs.writeFileSync(tokenPath, currentVersion);
} catch (error) {
logger.warn(`Unable to write app-version token: ${error}`);
}
return true;
}
export function setupIsNewVersion() {
ipcMain.handle(
ipcMainChannels.IS_NEW_VERSION, () => isNewVersion()
);
}

View File

@ -24,6 +24,7 @@ import DownloadProgressBar from './components/DownloadProgressBar';
import { getInvestModelNames } from './server_requests';
import InvestJob from './InvestJob';
import { dragOverHandlerNone } from './utils';
import Changelog from './components/Changelog';
const { ipcRenderer } = window.Workbench.electron;
@ -42,6 +43,8 @@ export default class App extends React.Component {
recentJobs: [],
showDownloadModal: false,
downloadedNofN: null,
showChangelog: false,
changelogDismissed: false,
};
this.switchTabs = this.switchTabs.bind(this);
this.openInvestModel = this.openInvestModel.bind(this);
@ -65,6 +68,9 @@ export default class App extends React.Component {
.includes(job.modelRunName)
)),
showDownloadModal: this.props.isFirstRun,
// Show changelog if this is a new version,
// but if it's the first run ever, wait until after download modal closes.
showChangelog: this.props.isNewVersion && !this.props.isFirstRun,
});
await i18n.changeLanguage(window.Workbench.LANGUAGE);
ipcRenderer.on('download-status', (downloadedNofN) => {
@ -92,6 +98,20 @@ export default class App extends React.Component {
this.setState({
showDownloadModal: shouldShow,
});
// After close, show changelog if new version and app has just launched
// (i.e., show changelog only once, after the first time the download modal closes).
if (!shouldShow && this.props.isNewVersion && !this.state.changelogDismissed) {
this.setState({
showChangelog: true,
});
}
}
closeChangelogModal() {
this.setState({
showChangelog: false,
changelogDismissed: true,
});
}
/** Push data for a new InvestTab component to an array.
@ -183,6 +203,7 @@ export default class App extends React.Component {
openTabIDs,
activeTab,
showDownloadModal,
showChangelog,
downloadedNofN,
} = this.state;
@ -277,6 +298,13 @@ export default class App extends React.Component {
show={showDownloadModal}
closeModal={() => this.showDownloadModal(false)}
/>
{
showChangelog &&
<Changelog
show={showChangelog}
close={() => this.closeChangelogModal()}
/>
}
<TabContainer activeKey={activeTab}>
<Navbar
onDragOver={dragOverHandlerNone}
@ -357,6 +385,7 @@ export default class App extends React.Component {
App.propTypes = {
isFirstRun: PropTypes.bool,
isNewVersion: PropTypes.bool,
nCPU: PropTypes.number,
};
@ -364,5 +393,6 @@ App.propTypes = {
// can be undefined for unrelated tests.
App.defaultProps = {
isFirstRun: false,
isNewVersion: false,
nCPU: 1,
};

View File

@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';
import { MdClose } from 'react-icons/md';
import { useTranslation } from 'react-i18next';
import pkg from '../../../../package.json';
import { ipcMainChannels } from '../../../main/ipcMainChannels';
const { ipcRenderer } = window.Workbench.electron;
const { logger } = window.Workbench;
export default function Changelog(props) {
const { t } = useTranslation();
const [htmlContent, setHtmlContent] = useState('');
// Load HTML from external file (which is generated by Python build process).
useEffect(() => {
async function loadHtml() {
const baseUrl = await ipcRenderer.invoke(ipcMainChannels.BASE_URL);
const response = await fetch(`${baseUrl}/changelog.html`);
if (!response.ok) {
logger.debug(`Error fetching changelog HTML: ${response.status} ${response.statusText}`);
return;
}
try {
const htmlString = await response.text();
// Find the section whose heading explicitly matches the current version.
const versionStr = pkg.version;
const escapedVersionStr = versionStr.split('.').join('\\.');
const sectionRegex = new RegExp(
`<section.*?>[\\s]*?<h1>${escapedVersionStr}\\b[\\s\\S]*?</h1>[\\s\\S]*?</section>`
);
const sectionMatches = htmlString.match(sectionRegex);
if (sectionMatches && sectionMatches.length) {
let latestVersionSection = sectionMatches[0];
const linkRegex = /<a\shref/g;
// Ensure all links open in a new window and are styled with a relevant icon.
latestVersionSection = latestVersionSection.replaceAll(
linkRegex,
'<a target="_blank" class="link-external" href'
);
setHtmlContent({
__html: latestVersionSection
});
}
} catch(error) {
logger.debug(error);
}
}
loadHtml();
}, []);
// Once HTML content has loaded, set up links to open in browser
// (instead of in an Electron window).
useEffect(() => {
const openLinkInBrowser = (event) => {
event.preventDefault();
ipcRenderer.send(
ipcMainChannels.OPEN_EXTERNAL_URL, event.currentTarget.href
);
};
document.querySelectorAll('.link-external').forEach(link => {
link.addEventListener('click', openLinkInBrowser);
});
}, [htmlContent]);
return (
<Modal
show={props.show && htmlContent !== ''}
onHide={props.close}
size="lg"
aria-labelledby="changelog-modal-title"
>
<Modal.Header>
<Modal.Title id="changelog-modal-title">
{t('New in this version')}
</Modal.Title>
<Button
variant="secondary-outline"
onClick={props.close}
className="float-right"
aria-label="Close modal"
>
<MdClose />
</Button>
</Modal.Header>
{/* Setting inner HTML in this way is OK because
the HTML content is controlled by our build process
and not, for example, sourced from user input. */}
<Modal.Body
dangerouslySetInnerHTML={htmlContent}
>
</Modal.Body>
</Modal>
);
}
Changelog.propTypes = {
show: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
};

View File

@ -211,6 +211,7 @@ class DataDownloadModal extends React.Component {
show={this.props.show}
onHide={this.closeDialog}
size="lg"
aria-labelledby="download-modal-title"
>
<Form>
<Modal.Header>
@ -230,7 +231,9 @@ class DataDownloadModal extends React.Component {
<p className="mb-0"><em>{this.state.alertPath}</em></p>
</Alert>
)
: <Modal.Title>{t("Download InVEST sample data")}</Modal.Title>
: <Modal.Title id="download-modal-title">
{t("Download InVEST sample data")}
</Modal.Title>
}
</Modal.Header>
<Modal.Body>

View File

@ -9,6 +9,7 @@ import { ipcMainChannels } from '../main/ipcMainChannels';
const { ipcRenderer } = window.Workbench.electron;
const isFirstRun = await ipcRenderer.invoke(ipcMainChannels.IS_FIRST_RUN);
const isNewVersion = await ipcRenderer.invoke(ipcMainChannels.IS_NEW_VERSION);
const nCPU = await ipcRenderer.invoke(ipcMainChannels.GET_N_CPUS);
const root = createRoot(document.getElementById('App'));
@ -16,6 +17,7 @@ root.render(
<ErrorBoundary>
<App
isFirstRun={isFirstRun}
isNewVersion={isNewVersion}
nCPU={nCPU}
/>
</ErrorBoundary>

View File

@ -253,7 +253,7 @@ exceed 100% of window.*/
}
.recent-job-card {
width: inherit;
width: inherit;
margin-bottom: 1rem;
padding: 0;
height: fit-content;
@ -617,3 +617,18 @@ input[type=text]::placeholder {
.error-boundary .btn {
margin: 1rem;
}
/* Changelog modal */
.link-external {
&:after {
content: '';
display: inline-block;
width: 1rem;
height: 1rem;
/* Icon is react-icons/md/MdOpenInNew, as a URL-encoded SVG */
background-image: url("data:image/svg+xml,%3Csvg stroke='currentColor' fill='currentColor' stroke-width='0' viewBox='0 0 24 24' class='mr-1' height='1em' width='1em' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' d='M0 0h24v24H0z'%3E%3C/path%3E%3Cpath d='M19 19H5V5h7V3H5a2 2 0 00-2 2v14a2 2 0 002 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: left 0.125rem top 0.125rem;
background-size: 1rem 1rem;
}
}

View File

@ -13,6 +13,7 @@ import puppeteer from 'puppeteer-core';
import pkg from '../../package.json';
import { APP_HAS_RUN_TOKEN } from '../../src/main/setupCheckFirstRun';
import { APP_VERSION_TOKEN } from '../../src/main/setupIsNewVersion';
jest.setTimeout(240000);
const PORT = 9009;
@ -26,9 +27,10 @@ let BINARY_PATH;
// append to this prefix and the image will be uploaded to github artifacts
// E.g. page.screenshot({ path: `${SCREENSHOT_PREFIX}screenshot.png` })
let SCREENSHOT_PREFIX;
// We'll clear this token before launching the app so we can have a
// We'll clear these tokens before launching the app so we can have a
// predictable startup page.
let APP_HAS_RUN_TOKEN_PATH;
let APP_VERSION_TOKEN_PATH;
// On GHA macos, invest validation can time-out reading from os.tmpdir
// So on GHA, use the homedir instead.
@ -45,6 +47,9 @@ if (process.platform === 'darwin') {
APP_HAS_RUN_TOKEN_PATH = path.join(
os.homedir(), 'Library/Application Support', pkg.name, APP_HAS_RUN_TOKEN
);
APP_VERSION_TOKEN_PATH = path.join(
os.homedir(), 'Library/Application Support', pkg.name, APP_VERSION_TOKEN
);
} else if (process.platform === 'win32') {
[BINARY_PATH] = glob.sync('./dist/win-unpacked/InVEST*.exe');
SCREENSHOT_PREFIX = path.join(
@ -53,6 +58,9 @@ if (process.platform === 'darwin') {
APP_HAS_RUN_TOKEN_PATH = path.join(
os.homedir(), 'AppData/Roaming', pkg.name, APP_HAS_RUN_TOKEN
);
APP_VERSION_TOKEN_PATH = path.join(
os.homedir(), 'AppData/Roaming', pkg.name, APP_VERSION_TOKEN
);
}
if (!fs.existsSync(BINARY_PATH)) {
@ -97,6 +105,7 @@ afterAll(() => {
// https://github.com/facebook/jest/issues/8688
beforeEach(() => {
try { fs.unlinkSync(APP_HAS_RUN_TOKEN_PATH); } catch {}
try { fs.unlinkSync(APP_VERSION_TOKEN_PATH); } catch {}
// start the invest app and forward stderr to console
ELECTRON_PROCESS = spawn(
`"${BINARY_PATH}"`,
@ -164,11 +173,22 @@ test('Run a real invest model', async () => {
});
await page.screenshot({ path: `${SCREENSHOT_PREFIX}1-page-load.png` });
const downloadModal = await page.waitForSelector('.modal-dialog');
const downloadModal = await page.waitForSelector(
'aria/[name="Download InVEST sample data"][role="dialog"]'
);
const downloadModalCancel = await downloadModal.waitForSelector(
'aria/[name="Cancel"][role="button"]');
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
await downloadModalCancel.click();
const changelogModal = await page.waitForSelector(
'aria/[name="New in this version"][role="dialog"]'
);
const changelogModalClose = await changelogModal.waitForSelector(
'aria/[name="Close modal"][role="button"]');
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
await changelogModalClose.click();
// We need to get the modelButton from w/in this list-group because there
// are buttons with the same name in the Recent Jobs container.
const investModels = await page.waitForSelector('.invest-list-group');
@ -233,12 +253,23 @@ test('Check local userguide links', async () => {
page.on('error', (err) => {
console.log(err);
});
const downloadModal = await page.waitForSelector('.modal-dialog');
const downloadModal = await page.waitForSelector(
'aria/[name="Download InVEST sample data"][role="dialog"]'
);
const downloadModalCancel = await downloadModal.waitForSelector(
'aria/[name="Cancel"][role="button"]');
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
await downloadModalCancel.click();
const changelogModal = await page.waitForSelector(
'aria/[name="New in this version"][role="dialog"]'
);
const changelogModalClose = await changelogModal.waitForSelector(
'aria/[name="Close modal"][role="button"]');
await page.waitForTimeout(WAIT_TO_CLICK); // waiting for click handler to be ready
await changelogModalClose.click();
const investList = await page.waitForSelector('.invest-list-group');
const modelButtons = await investList.$$('aria/[role="button"]');

View File

@ -180,12 +180,14 @@ describe('createWindow', () => {
test('should register various ipcMain listeners', async () => {
await createWindow();
const expectedHandleChannels = [
ipcMainChannels.BASE_URL,
ipcMainChannels.CHANGE_LANGUAGE,
ipcMainChannels.CHECK_STORAGE_TOKEN,
ipcMainChannels.CHECK_FILE_PERMISSIONS,
ipcMainChannels.GET_SETTING,
ipcMainChannels.GET_N_CPUS,
ipcMainChannels.INVEST_VERSION,
ipcMainChannels.IS_NEW_VERSION,
ipcMainChannels.IS_FIRST_RUN,
ipcMainChannels.OPEN_PATH,
ipcMainChannels.SHOW_OPEN_DIALOG,

View File

@ -0,0 +1,77 @@
import React from 'react';
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../../src/renderer/app';
import pkg from '../../package.json';
describe('Changelog', () => {
const currentVersion = pkg.version;
const nonexistentVersion = 'nonexistent-version';
beforeEach(() => {
jest.spyOn(window, 'fetch')
.mockResolvedValue({
ok: true,
text: () => `
<html>
<head></head>
<body>
<section>
<h1>${currentVersion}</h1>
</section>
<section>
<h1>${nonexistentVersion}</h1>
</section>
</body>
</html>
`
});
});
test('Changelog modal opens immediately on launch of a new version', async () => {
const { findByRole } = render(<App isNewVersion />);
const changelogModal = await findByRole('dialog', { name: 'New in this version' });
expect(changelogModal).toBeInTheDocument();
});
test('On first run (of any version), Changelog modal opens after Download modal is closed', async () => {
const { findByRole, getByText } = render(<App isFirstRun isNewVersion />);
let changelogModalFound = true;
try {
await findByRole('dialog', { name: 'New in this version' });
} catch {
changelogModalFound = false;
}
expect(changelogModalFound).toBe(false);
const downloadModal = await findByRole('dialog', { name: 'Download InVEST sample data' });
expect(downloadModal).toBeInTheDocument();
await userEvent.click(getByText('Cancel'));
expect(downloadModal).not.toBeInTheDocument();
const changelogModal = await findByRole('dialog', { name: 'New in this version' });
expect(changelogModal).toBeInTheDocument();
});
test('Changelog modal does not open when current version has been run before', async () => {
const { findByRole } = render(<App isNewVersion={false} />);
let changelogModalFound = true;
try {
await findByRole('dialog', { name: 'New in this version' });
} catch {
changelogModalFound = false;
}
expect(changelogModalFound).toBe(false);
});
test('Changelog modal contains only content relevant to the current version', async () => {
const { findByRole, queryByRole } = render(<App isNewVersion />);
const currentVersionSectionHeading = await findByRole('heading', { name: currentVersion });
expect(currentVersionSectionHeading).toBeInTheDocument();
const nonexistentVersionSectionHeading = queryByRole('heading', { name: nonexistentVersion });
expect(nonexistentVersionSectionHeading).not.toBeInTheDocument();
});
});

View File

@ -24,6 +24,7 @@ export default defineConfig({
path.resolve(PROJECT_ROOT, 'splash.html'),
path.resolve(PROJECT_ROOT, 'report_a_problem.html'),
path.resolve(PROJECT_ROOT, 'about.html'),
path.resolve(PROJECT_ROOT, 'changelog.html'),
],
},
emptyOutDir: true,