From 8b938250b827cf2f8577455fb8563c27165668b8 Mon Sep 17 00:00:00 2001 From: Alan Williams Date: Sun, 8 Mar 2026 13:10:27 -0500 Subject: [PATCH] Add React ErrorBoundary with crash recovery UI and tests - Wraps in an ErrorBoundary component in index.html - On crash: shows error message with Reload and Reset All Data buttons - Uses ES5-compatible prototype pattern (var assignment) so the class is accessible as a global in the headless test runner - Adds React.Component stub to run-tests.js for class-based components - Adds 2 new tests: render-children path and getDerivedStateFromError - Tests: 107/107 passed (up from 105) - Marks Task 1 complete in BACKLOG.md Co-Authored-By: Claude Sonnet 4.6 --- .claude/hooks/run-tests.js | 22 ++++++++++++++++++++++ BACKLOG.md | 4 +++- index.html | 24 +++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.claude/hooks/run-tests.js b/.claude/hooks/run-tests.js index c9d1f3c..7819053 100755 --- a/.claude/hooks/run-tests.js +++ b/.claude/hooks/run-tests.js @@ -661,7 +661,10 @@ test("passage: all reading-type days have text_jp and text_en", function (a) { }, useEffect: function () {}, useRef: function () { return { current: null }; }, + Component: function () {}, }; + global.React.Component.prototype.setState = function () {}; + global.React.Component.prototype.render = function () { return null; }; global.ReactDOM = { createRoot: function () { return { render: function () {} }; }, }; @@ -718,6 +721,25 @@ test("passage: all reading-type days have text_jp and text_en", function (a) { a.ok(true); } catch (e) { a.ok(false, e.message); } }); + + test("React render: ErrorBoundary renders children when no error", function (a) { + try { + var eb = new ErrorBoundary({ children: {} }); + eb.state = { hasError: false, error: null }; + eb.props = { children: {} }; + var out = eb.render(); + a.ok(out !== null && out !== undefined, "ErrorBoundary.render() returns children"); + } catch (e) { a.ok(false, e.message); } + }); + + test("React render: ErrorBoundary getDerivedStateFromError sets hasError", function (a) { + try { + var err = new Error("test crash"); + var state = ErrorBoundary.getDerivedStateFromError(err); + a.ok(state.hasError === true, "hasError is true"); + a.ok(state.error === err, "error is set"); + } catch (e) { a.ok(false, e.message); } + }); }()); // ── summary ─────────────────────────────────────────────────────────────────── diff --git a/BACKLOG.md b/BACKLOG.md index 4d72dbc..e674e73 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -241,4 +241,6 @@ excessively long input, Unicode normalization differences, or mixed-script input ## Completed Tasks -_Move tasks here as they are finished, with the date and a one-line summary._ +| Date | Task | Summary | +|------|------|---------| +| 2026-03-08 | Task 1 — Add React Error Boundary and Crash Recovery | Added `ErrorBoundary` class wrapping `` in `index.html`; wraps crash in a recoverable UI with Reload and Reset buttons; 2 new tests in `run-tests.js` (107 total). | diff --git a/index.html b/index.html index d04bd06..bce5500 100644 --- a/index.html +++ b/index.html @@ -1292,7 +1292,29 @@ }, isDone ? '✓' : d); }))); } -ReactDOM.createRoot(document.getElementById('root')).render(/*#__PURE__*/React.createElement(App, null)); +var ErrorBoundary = (function () { + function ErrorBoundary(props) { this.props = props; this.state = { hasError: false, error: null }; } + ErrorBoundary.prototype = Object.create(React.Component.prototype); + ErrorBoundary.prototype.constructor = ErrorBoundary; + ErrorBoundary.getDerivedStateFromError = function (error) { return { hasError: true, error: error }; }; + ErrorBoundary.prototype.componentDidCatch = function (error, info) { console.error('App crash:', error, info); }; + ErrorBoundary.prototype.render = function () { + if (this.state.hasError) { + var self = this; + return React.createElement('div', { style: { padding: 40, textAlign: 'center' } }, + React.createElement('h2', null, 'Something went wrong'), + React.createElement('p', null, self.state.error && self.state.error.message), + React.createElement('button', { onClick: function() { location.reload(); } }, 'Reload'), + React.createElement('button', { onClick: function() { localStorage.clear(); location.reload(); } }, 'Reset all data') + ); + } + return this.props.children; + }; + return ErrorBoundary; +}()); +ReactDOM.createRoot(document.getElementById('root')).render( + /*#__PURE__*/React.createElement(ErrorBoundary, null, React.createElement(App, null)) +);