Merge pull request #1681 from emilyanndavis/feature/1368-display-changelog-in-workbench
Feature/1368 display changelog in workbench
This commit is contained in:
commit
3fc95d21e2
|
@ -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
|
||||
|
|
28
HISTORY.rst
28
HISTORY.rst
|
@ -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)
|
||||
-------------------
|
||||
|
|
7
Makefile
7
Makefile
|
@ -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)
|
||||
|
|
|
@ -26,3 +26,4 @@ requests
|
|||
coverage
|
||||
xlwt
|
||||
build # pip-only
|
||||
rst2html5
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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": "./",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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' : '';
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]');
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue