Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/hooks/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {} }; },
};
Expand Down Expand Up @@ -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 ───────────────────────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<App>` in `index.html`; wraps crash in a recoverable UI with Reload and Reset buttons; 2 new tests in `run-tests.js` (107 total). |
24 changes: 23 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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))
);

</script>
</body>
Expand Down