Preact error dialog (#120)

Add error dialog, fix dynamic hash updates.
This commit is contained in:
Will Scullin 2022-05-31 17:41:24 -07:00 committed by GitHub
parent 04ae0327c2
commit ef404735cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 669 additions and 85 deletions

View File

@ -162,11 +162,8 @@ th {
display: flex;
}
.inset button {
.inset button, .modal-overlay button {
min-width: 36px;
}
.inset button {
margin: 0 2px;
}

View File

@ -10,14 +10,13 @@ module.exports = {
'testMatch': [
'**/?(*.)+(spec|test).+(ts|js|tsx)'
],
'transform': {
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
'^.*\\.tsx$': 'ts-jest',
},
'setupFilesAfterEnv': [
'<rootDir>/test/jest-setup.js'
'<rootDir>/test/jest-setup.ts'
],
'coveragePathIgnorePatterns': [
'/node_modules/',

View File

@ -12,6 +12,7 @@ import { Screen } from './Screen';
import { Drives } from './Drives';
import { Slinky } from './Slinky';
import { ThunderClock } from './ThunderClock';
import { ErrorModal } from './ErrorModal';
/**
* Interface for the Apple2 component.
@ -40,6 +41,7 @@ export const Apple2 = (props: Apple2Props) => {
const [apple2, setApple2] = useState<Apple2Impl>();
const [io, setIO] = useState<Apple2IO>();
const [cpu, setCPU] = useState<CPU6502>();
const [error, setError] = useState<string>();
useEffect(() => {
if (screen.current) {
@ -57,7 +59,13 @@ export const Apple2 = (props: Apple2Props) => {
setCPU(cpu);
apple2.reset();
apple2.run();
}).catch(error => console.error(error));
}).catch((e) => {
if (e instanceof Error) {
setError(e.message);
} else {
console.error(e);
}
});
}
}, [props]);
@ -74,6 +82,7 @@ export const Apple2 = (props: Apple2Props) => {
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>
<ErrorModal error={error} setError={setError} />
</div>
);
};

View File

@ -4,6 +4,8 @@ import cs from 'classnames';
import Disk2 from '../cards/disk2';
import { FileModal } from './FileModal';
import { loadJSON, loadHttpFile, getHashParts } from './util/files';
import { ErrorModal } from './ErrorModal';
import { useHash } from './hooks/useHash';
/**
* Storage structure for Disk II state returned via callbacks.
@ -38,25 +40,37 @@ export interface DiskIIProps extends DiskIIData {
export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
const label = side ? `${name} - ${side}` : name;
const [modalOpen, setModalOpen] = useState(false);
const [error, setError] = useState<string>();
const [currentHash, setCurrentHash] = useState<string>();
const hash = useHash();
const handleError = (e: unknown) => {
if (e instanceof Error) {
setError(e.message);
} else {
console.error(e);
}
};
useEffect(() => {
const hashParts = getHashParts();
if (disk2 && hashParts && hashParts[number]) {
const hashPart = decodeURIComponent(hashParts[number]);
if (hashPart.match(/^https?:/)) {
loadHttpFile(disk2, number, hashPart)
.catch((error) =>
console.error(error)
);
} else {
const filename = `/json/disks/${hashPart}.json`;
loadJSON(disk2, number, filename)
.catch((error) =>
console.error(error)
);
const hashParts = getHashParts(hash);
const newHash = hashParts[number];
if (disk2 && newHash) {
const hashPart = decodeURIComponent(newHash);
if (hashPart !== currentHash) {
if (hashPart.match(/^https?:/)) {
loadHttpFile(disk2, number, hashPart)
.catch((e) => handleError(e));
} else {
const filename = `/json/disks/${hashPart}.json`;
loadJSON(disk2, number, filename)
.catch((e) => handleError(e));
}
setCurrentHash(hashPart);
}
}
}, [disk2, number]);
}, [currentHash, disk2, hash, number]);
const doClose = useCallback(() => {
setModalOpen(false);
@ -69,6 +83,7 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
return (
<div className="disk">
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} />
<ErrorModal error={error} setError={setError} />
<div
id={`disk${number}`}
className={cs('disk-light', { on })}

View File

@ -0,0 +1,34 @@
import { h, Fragment } from 'preact';
import { useCallback } from 'preact/hooks';
import { Modal, ModalContent, ModalFooter } from './Modal';
export interface ErrorProps {
error: string | undefined;
setError: (error: string | undefined) => void;
}
export const ErrorModal = ({ error, setError } : ErrorProps) => {
const onClose = useCallback(() => setError(undefined), [setError]);
return (
<>
{ error && (
<Modal
title="Error"
icon="triangle-exclamation"
isOpen={true}
onClose={onClose}
>
<ModalContent>
<div style={{ width: 320, fontSize: '1.1em', padding: '5px 11px'}}>
{error}
</div>
</ModalContent>
<ModalFooter>
<button onClick={onClose}>OK</button>
</ModalFooter>
</Modal>
)}
</>
);
};

View File

@ -27,7 +27,7 @@ export interface FileChooserProps {
control?: typeof controlDefault;
}
const hasPicker: boolean = !!window.showOpenFilePicker;
const hasPicker = !!window.showOpenFilePicker;
const controlDefault = hasPicker ? 'picker' : 'input';
interface InputFileChooserProps {

View File

@ -1,13 +1,15 @@
import { h, JSX } from 'preact';
import { h, Fragment, JSX } from 'preact';
import { useCallback, useState } from 'preact/hooks';
import { DiskDescriptor, DriveNumber, NibbleFormat, NIBBLE_FORMATS } from '../formats/types';
import { Modal, ModalContent, ModalFooter } from './Modal';
import { loadLocalFile, loadJSON, getHashParts, setHashParts } from './util/files';
import DiskII from '../cards/disk2';
import { ErrorModal } from './ErrorModal';
import index from 'json/disks/index.json';
import { FileChooser, FilePickerAcceptType, FileSystemFileHandleLike } from './FileChooser';
import { noAwait } from './util/promises';
import { useHash } from './hooks/useHash';
const DISK_TYPES: FilePickerAcceptType[] = [
{
@ -50,39 +52,39 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
const [category, setCategory] = useState<string>();
const [handles, setHandles] = useState<FileSystemFileHandleLike[]>();
const [filename, setFilename] = useState<string>();
const [error, setError] = useState<string>();
const hash = useHash();
const doCancel = useCallback(() => onClose(true), [onClose]);
const doOpen = useCallback(async () => {
const hashParts = getHashParts();
const hashParts = getHashParts(hash);
setBusy(true);
if (disk2 && handles && handles.length === 1) {
hashParts[number] = '';
setBusy(true);
try {
try {
if (disk2 && handles?.length === 1) {
hashParts[number] = '';
await loadLocalFile(disk2, number, await handles[0].getFile());
} catch (e) {
console.error(e);
} finally {
setBusy(false);
onClose();
}
}
if (disk2 && filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
hashParts[number] = name[1];
setBusy(true);
loadJSON(disk2, number, filename)
.catch(console.error)
.finally(() => {
setBusy(false);
onClose();
});
if (disk2 && filename) {
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
hashParts[number] = name[1];
await loadJSON(disk2, number, filename);
}
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
console.error(e);
}
} finally {
setHashParts(hashParts);
setBusy(false);
onClose();
}
setHashParts(hashParts);
}, [disk2, filename, number, onClose, handles]);
}, [disk2, filename, number, onClose, handles, hash]);
const onChange = useCallback((handles: FileSystemFileHandleLike[]) => {
setEmpty(handles.length === 0);
@ -105,29 +107,32 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
const disks = category ? categories[category] : [];
return (
<Modal title="Open File" isOpen={isOpen}>
<ModalContent>
<div id="load-modal">
<select multiple onChange={doSelectCategory}>
{categoryNames.map((name) => (
<option key={name}>{name}</option>
))}
</select>
<select multiple onChange={doSelectFilename}>
{disks.map((disk) => (
<option key={disk.filename} value={disk.filename}>
{disk.name}
{disk.disk ? ` - ${disk.disk}` : ''}
</option>
))}
</select>
</div>
<FileChooser onChange={onChange} accept={DISK_TYPES} />
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
<>
<Modal title="Open File" isOpen={isOpen}>
<ModalContent>
<div id="load-modal">
<select multiple onChange={doSelectCategory}>
{categoryNames.map((name) => (
<option key={name}>{name}</option>
))}
</select>
<select multiple onChange={doSelectFilename}>
{disks.map((disk) => (
<option key={disk.filename} value={disk.filename}>
{disk.name}
{disk.disk ? ` - ${disk.disk}` : ''}
</option>
))}
</select>
</div>
<FileChooser onChange={onChange} accept={DISK_TYPES} />
</ModalContent>
<ModalFooter>
<button onClick={doCancel}>Cancel</button>
<button onClick={noAwait(doOpen)} disabled={busy || empty}>Open</button>
</ModalFooter>
</Modal>
<ErrorModal error={error} setError={setError} />
</>
);
};

View File

@ -1,4 +1,5 @@
import { h, ComponentChildren } from 'preact';
import { createPortal } from 'preact/compat';
import { useCallback } from 'preact/hooks';
import { useHotKey } from './hooks/useHotKey';
@ -69,7 +70,7 @@ const modalFooterStyle = {
*/
export const ModalOverlay = ({ children }: { children: ComponentChildren }) => {
return (
<div style={modalOverlayStyle}>
<div style={modalOverlayStyle} className="modal-overlay">
{children}
</div>
);
@ -95,9 +96,9 @@ export const ModalContent = ({ children }: { children: ComponentChildren }) => {
*/
export const ModalFooter = ({ children }: { children: ComponentChildren }) => {
return (
<div style={modalFooterStyle}>
<footer style={modalFooterStyle}>
{children}
</div>
</footer>
);
};
@ -135,6 +136,7 @@ type OnCloseCallback = (closeBox?: boolean) => void;
export interface ModalHeaderProps {
onClose?: OnCloseCallback;
title: string;
icon?: string;
}
/**
@ -144,12 +146,16 @@ export interface ModalHeaderProps {
* @param title Modal title
* @returns ModalHeader component
*/
export const ModalHeader = ({ onClose, title }: ModalHeaderProps) => {
export const ModalHeader = ({ onClose, title, icon }: ModalHeaderProps) => {
return (
<div style={modalHeaderStyle}>
<span style={modalTitleStyle}>{title}</span>
<header style={modalHeaderStyle}>
<span style={modalTitleStyle}>
{icon && <i className={`fas fa-${icon}`} role="img" />}
{' '}
{title}
</span>
{onClose && <ModalCloseButton onClose={onClose} />}
</div>
</header>
);
};
@ -161,6 +167,7 @@ export interface ModalProps {
isOpen: boolean;
title: string;
children: ComponentChildren;
icon?: string;
}
/**
@ -180,13 +187,13 @@ export const Modal = ({
...props
}: ModalProps) => {
return (
isOpen ? (
isOpen ? createPortal((
<ModalOverlay>
<div style={modalStyle}>
<div style={modalStyle} role="dialog">
{title && <ModalHeader title={title} {...props} />}
{children}
</div>
</ModalOverlay>
) : null
), document.body) : null
);
};

View File

@ -0,0 +1,19 @@
import { useEffect, useState } from 'preact/hooks';
export const useHash = () => {
const [hash, setHash] = useState(window.location.hash);
const popstateListener = () => {
const hash = window.location.hash;
setHash(hash);
};
useEffect(() => {
window.addEventListener('popstate', popstateListener);
return () => {
window.removeEventListener('popstate', popstateListener);
};
}, []);
return hash;
};

View File

@ -12,8 +12,8 @@ import DiskII from 'js/cards/disk2';
*
* @returns an padded array for 1 based indexing
*/
export const getHashParts = () => {
const parts = window.location.hash.match(/^#([^|]*)\|?(.*)$/) || ['', '', ''];
export const getHashParts = (hash: string) => {
const parts = hash.match(/^#([^|]*)\|?(.*)$/) || ['', '', ''];
return ['', parts[1], parts[2]];
};
@ -60,7 +60,7 @@ export const loadLocalFile = (
}
}
} else {
reject(`Extension ${ext} not recognized.`);
reject(`Extension "${ext}" not recognized.`);
}
};
fileReader.readAsArrayBuffer(file);
@ -139,7 +139,7 @@ export const loadHttpFile = async (
const ext = fileParts.pop()?.toLowerCase() || '[none]';
const name = decodeURIComponent(fileParts.join('.'));
if (!includes(NIBBLE_FORMATS, ext)) {
throw new Error(`Extension ${ext} not recognized.`);
throw new Error(`Extension "${ext}" not recognized.`);
}
disk2.setBinary(number, name, ext, data);
initGamepad();

355
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@babel/preset-env": "^7.9.0",
"@babel/preset-typescript": "^7.16.7",
"@testing-library/dom": "^7.30.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/preact": "^3.0.1",
"@testing-library/user-event": "^13.1.3",
"@types/jest": "^27.0.2",
@ -2906,6 +2907,104 @@
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.16.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz",
"integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"engines": {
"node": ">=8",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true,
"engines": {
"node": ">=6.0"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/jest-dom/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/@testing-library/jest-dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/preact": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@testing-library/preact/-/preact-3.1.1.tgz",
@ -3480,6 +3579,15 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.3",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
"integrity": "sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==",
"dev": true,
"dependencies": {
"@types/jest": "*"
}
},
"node_modules/@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
@ -4260,6 +4368,18 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true,
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/babel-jest": {
"version": "27.2.4",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.2.4.tgz",
@ -5099,6 +5219,32 @@
"node": ">= 8"
}
},
"node_modules/css": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
"dev": true,
"dependencies": {
"inherits": "^2.0.4",
"source-map": "^0.6.1",
"source-map-resolve": "^0.6.0"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"node_modules/css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -5160,6 +5306,15 @@
"integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
"dev": true
},
"node_modules/decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
@ -11256,6 +11411,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -12190,6 +12354,19 @@
"node": ">= 0.10"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -12755,6 +12932,17 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-resolve": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
"deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
"dev": true,
"dependencies": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
@ -12973,6 +13161,18 @@
"node": ">=6"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -16595,6 +16795,80 @@
}
}
},
"@testing-library/jest-dom": {
"version": "5.16.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz",
"integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@testing-library/preact": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@testing-library/preact/-/preact-3.1.1.tgz",
@ -17092,6 +17366,15 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"@types/testing-library__jest-dom": {
"version": "5.14.3",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
"integrity": "sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==",
"dev": true,
"requires": {
"@types/jest": "*"
}
},
"@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
@ -17683,6 +17966,12 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"babel-jest": {
"version": "27.2.4",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.2.4.tgz",
@ -18347,6 +18636,31 @@
"which": "^2.0.1"
}
},
"css": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
"dev": true,
"requires": {
"inherits": "^2.0.4",
"source-map": "^0.6.1",
"source-map-resolve": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"cssom": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@ -18396,6 +18710,12 @@
"integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
"dev": true
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
"dev": true
},
"decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
@ -23010,6 +23330,12 @@
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"dev": true
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -23716,6 +24042,16 @@
"resolve": "^1.9.0"
}
},
"redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -24162,6 +24498,16 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
"source-map-resolve": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
"dev": true,
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0"
}
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
@ -24344,6 +24690,15 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
},
"strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",

View File

@ -29,6 +29,7 @@
"@babel/preset-env": "^7.9.0",
"@babel/preset-typescript": "^7.16.7",
"@testing-library/dom": "^7.30.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/preact": "^3.0.1",
"@testing-library/user-event": "^13.1.3",
"@types/jest": "^27.0.2",

View File

@ -0,0 +1,45 @@
/** @jest-environment jsdom */
import { h } from 'preact';
import { fireEvent, render, screen } from '@testing-library/preact';
import {
ErrorModal,
} from 'js/components/ErrorModal';
describe('ErrorModal', () => {
it('renders when there is an error', () => {
const setError = jest.fn();
render(
<ErrorModal error="My Error" setError={setError} />
);
expect(screen.queryByRole('banner')).toBeVisible();
expect(screen.queryByRole('banner')).toHaveTextContent('Error');
expect(screen.queryByText('My Error')).toBeVisible();
});
it('does not render when there is not an error', () => {
const setError = jest.fn();
render(
<ErrorModal error={undefined} setError={setError} />
);
expect(screen.queryByRole('banner')).not.toBeInTheDocument();
expect(screen.queryByText('My Error')).not.toBeInTheDocument();
});
it('calls setError when close is clicked', () => {
const setError = jest.fn();
render(
<ErrorModal error="My Error" setError={setError} />
);
fireEvent.click(screen.getByTitle('Close'));
expect(setError).toHaveBeenCalledWith(undefined);
});
it('calls setError when OK is clicked', () => {
const setError = jest.fn();
render(
<ErrorModal error="My Error" setError={setError} />
);
fireEvent.click(screen.getByText('OK'));
expect(setError).toHaveBeenCalledWith(undefined);
});
});

View File

@ -0,0 +1,97 @@
/** @jest-environment jsdom */
import { h } from 'preact';
import { fireEvent, render, screen } from '@testing-library/preact';
import {
Modal,
ModalContent,
ModalFooter,
} from 'js/components/Modal';
describe('Modal', () => {
it('renders a title and content when open', () => {
render(
<Modal
title="My Title"
isOpen={true}
>
<ModalContent>
My Content
</ModalContent>
</Modal>
);
expect(screen.queryByRole('banner')).toBeVisible();
expect(screen.queryByRole('banner')).toHaveTextContent('My Title');
expect(screen.queryByText('My Content')).toBeVisible();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.queryByTitle('Close')).not.toBeInTheDocument();
});
it('does not render a title and content when not open', () => {
render(
<Modal
title="My Title"
isOpen={false}
>
<ModalContent>
My Content
</ModalContent>
</Modal>
);
expect(screen.queryByRole('banner')).not.toBeInTheDocument();
expect(screen.queryByText('My Content')).not.toBeInTheDocument();
});
it('renders a footer', () => {
render(
<Modal
title="My Title"
isOpen={true}
>
<ModalContent>
My Content
</ModalContent>
<ModalFooter>
My Footer
</ModalFooter>
</Modal>
);
expect(screen.queryByRole('banner')).toHaveTextContent('My Title');
expect(screen.queryByText('My Content')).toBeVisible();
expect(screen.getByRole('contentinfo')).toHaveTextContent('My Footer');
});
it('can have a close button', () => {
const onClose = jest.fn();
render(
<Modal
title="My Title"
isOpen={true}
onClose={onClose}
>
<ModalContent>
My Content
</ModalContent>
</Modal>
);
const button = screen.getByTitle('Close');
expect(button).toBeVisible();
fireEvent.click(button);
expect(onClose).toHaveBeenCalledWith(true);
});
it('can have an icon', () => {
render(
<Modal
title="My Title"
isOpen={true}
icon="warning"
>
<ModalContent>
My Content
</ModalContent>
</Modal>
);
expect(screen.getByRole('img')).toBeVisible();
});
});

View File

@ -1,3 +1,4 @@
import { toMatchImageSnapshot } from 'jest-image-snapshot';
import '@testing-library/jest-dom';
expect.extend({ toMatchImageSnapshot });