diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index 1ff23de..9d36a67 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -41,7 +41,7 @@ export const Apple2 = (props: Apple2Props) => { const [apple2, setApple2] = useState(); const [io, setIO] = useState(); const [cpu, setCPU] = useState(); - const [error, setError] = useState(); + const [error, setError] = useState(); useEffect(() => { if (screen.current) { @@ -59,13 +59,7 @@ export const Apple2 = (props: Apple2Props) => { setCPU(cpu); apple2.reset(); apple2.run(); - }).catch((e) => { - if (e instanceof Error) { - setError(e.message); - } else { - console.error(e); - } - }); + }).catch((e) => setError(e)); } }, [props]); diff --git a/js/components/DiskII.tsx b/js/components/DiskII.tsx index d22a275..e622a3e 100644 --- a/js/components/DiskII.tsx +++ b/js/components/DiskII.tsx @@ -40,19 +40,11 @@ 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(); + const [error, setError] = useState(); const [currentHash, setCurrentHash] = useState(); const hash = useHash(); - const handleError = (e: unknown) => { - if (e instanceof Error) { - setError(e.message); - } else { - console.error(e); - } - }; - useEffect(() => { const hashParts = getHashParts(hash); const newHash = hashParts[number]; @@ -61,11 +53,11 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => { if (hashPart !== currentHash) { if (hashPart.match(/^https?:/)) { loadHttpFile(disk2, number, hashPart) - .catch((e) => handleError(e)); + .catch((e) => setError(e)); } else { const filename = `/json/disks/${hashPart}.json`; loadJSON(disk2, number, filename) - .catch((e) => handleError(e)); + .catch((e) => setError(e)); } setCurrentHash(hashPart); } diff --git a/js/components/ErrorModal.tsx b/js/components/ErrorModal.tsx index dea54c4..0e58d1c 100644 --- a/js/components/ErrorModal.tsx +++ b/js/components/ErrorModal.tsx @@ -1,34 +1,45 @@ -import { h, Fragment } from 'preact'; +import { h } from 'preact'; import { useCallback } from 'preact/hooks'; import { Modal, ModalContent, ModalFooter } from './Modal'; export interface ErrorProps { - error: string | undefined; + error: unknown | undefined; setError: (error: string | undefined) => void; } -export const ErrorModal = ({ error, setError } : ErrorProps) => { +export const ErrorModal = ({ error, setError }: ErrorProps) => { const onClose = useCallback(() => setError(undefined), [setError]); + let errorStr = null; + if (error) { + if (error instanceof Error) { + errorStr = error.message; + } else if (typeof error === 'string') { + errorStr = error; + } else { + errorStr = 'Unknown Error'; + console.error(error); + } + } - return ( - <> - { error && ( - - -
- {error} -
-
- - - -
- )} - - ); + if (errorStr) { + return ( + + +
+ {errorStr} +
+
+ + + +
+ ); + } else { + return null; + } }; diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index 3c4ace4..243bacb 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -52,7 +52,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => const [category, setCategory] = useState(); const [handles, setHandles] = useState(); const [filename, setFilename] = useState(); - const [error, setError] = useState(); + const [error, setError] = useState(); const hash = useHash(); const doCancel = useCallback(() => onClose(true), [onClose]); @@ -72,11 +72,7 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) => await loadJSON(disk2, number, filename); } } catch (e) { - if (e instanceof Error) { - setError(e.message); - } else { - console.error(e); - } + setError(e); } finally { setHashParts(hashParts); setBusy(false); diff --git a/js/formats/woz.ts b/js/formats/woz.ts index cd90f2f..12eeb60 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -19,10 +19,10 @@ const WOZ_INTEGRITY_CHECK = 0x0a0d0aff; * @returns ASCII string */ function stringFromBytes(data: DataView, start: number, end: number): string { - return String.fromCharCode.apply( - null, - new Uint8Array(data.buffer.slice(data.byteOffset + start, data.byteOffset + end)) - ) as string; + const byteArray = new Uint8Array( + data.buffer.slice(data.byteOffset + start, data.byteOffset + end) + ); + return String.fromCharCode(...byteArray); } export class InfoChunk { diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index e47572a..91050ef 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -622,7 +622,7 @@ function loadDisk(drive: DriveNumber, disk: JSONDisk) { */ function updateLocalStorage() { - const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; const names = Object.keys(diskIndex); const cat: DiskDescriptor[] = disk_categories['Local Saves'] = []; @@ -654,12 +654,12 @@ type LocalDiskIndex = { }; function saveLocalStorage(drive: DriveNumber, name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; const json = _disk2.getJSON(drive); diskIndex[name] = json; - window.localStorage.diskIndex = JSON.stringify(diskIndex); + window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex)); driveLights.label(drive, name); driveLights.dirty(drive, false); @@ -667,17 +667,17 @@ function saveLocalStorage(drive: DriveNumber, name: string) { } function deleteLocalStorage(name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; if (diskIndex[name]) { delete diskIndex[name]; openAlert('Deleted'); } - window.localStorage.diskIndex = JSON.stringify(diskIndex); + window.localStorage.setItem('diskIndex', JSON.stringify(diskIndex)); updateLocalStorage(); } function loadLocalStorage(drive: DriveNumber, name: string) { - const diskIndex = JSON.parse(window.localStorage.diskIndex as string || '{}') as LocalDiskIndex; + const diskIndex = JSON.parse(window.localStorage.getItem('diskIndex') || '{}') as LocalDiskIndex; if (diskIndex[name]) { _disk2.setJSON(drive, diskIndex[name]); driveLights.label(drive, name); @@ -887,11 +887,12 @@ function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, print keyboard.setFunction('F3', () => io.keyDown(0x1b)); // Escape keyboard.setFunction('F4', optionsModal.openModal); keyboard.setFunction('F6', () => { - window.localStorage.state = base64_json_stringify(_apple2.getState()); + window.localStorage.setItem('state', base64_json_stringify(_apple2.getState())); }); keyboard.setFunction('F9', () => { - if (window.localStorage.state) { - _apple2.setState(base64_json_parse(window.localStorage.state as string) as Apple2State); + const localState = window.localStorage.getItem('state'); + if (localState) { + _apple2.setState(base64_json_parse(localState) as Apple2State); } }); diff --git a/test/components/ErrorModal.spec.tsx b/test/components/ErrorModal.spec.tsx index 2e23de4..d11a7ff 100644 --- a/test/components/ErrorModal.spec.tsx +++ b/test/components/ErrorModal.spec.tsx @@ -9,7 +9,17 @@ describe('ErrorModal', () => { it('renders when there is an error', () => { const setError = jest.fn(); render( - + + ); + expect(screen.queryByRole('banner')).toBeVisible(); + expect(screen.queryByRole('banner')).toHaveTextContent('Error'); + expect(screen.queryByText('My Error')).toBeVisible(); + }); + + it('renders when there is an error string', () => { + const setError = jest.fn(); + render( + ); expect(screen.queryByRole('banner')).toBeVisible(); expect(screen.queryByRole('banner')).toHaveTextContent('Error'); @@ -18,11 +28,20 @@ describe('ErrorModal', () => { it('does not render when there is not an error', () => { const setError = jest.fn(); - render( + const { container } = render( ); - expect(screen.queryByRole('banner')).not.toBeInTheDocument(); - expect(screen.queryByText('My Error')).not.toBeInTheDocument(); + expect(container).toBeEmptyDOMElement(); + }); + + it('handles an invalid error', () => { + jest.spyOn(console, 'error').mockImplementation(); + const setError = jest.fn(); + render( + + ); + expect(screen.queryByText('Unknown Error')).toBeVisible(); + expect(console.error).toHaveBeenCalledWith({foo: 'My Error'}); }); it('calls setError when close is clicked', () => {