From e7d3cab85af55e45c5a6e8e4844fdbd41b5a4c38 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 23:15:28 +0000 Subject: [PATCH] feat: add ErrorBoundary component with onErrorCaptured (BUG-F5) Catches runtime errors in child components, displays user-friendly error message with reload button. Wired into App.vue wrapping router-view. Tests verify error capture and button rendering. Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.vue | 5 +- frontend/src/components/ErrorBoundary.vue | 31 ++++++++++ .../__tests__/ErrorBoundary.test.ts | 58 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ErrorBoundary.vue create mode 100644 frontend/src/components/__tests__/ErrorBoundary.test.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2af8ecd..e3d0964 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,6 +2,7 @@ import { onMounted } from 'vue' import { RouterView } from 'vue-router' import NavBar from './components/NavBar.vue' +import ErrorBoundary from './components/ErrorBoundary.vue' import { ensureAudioContext } from './game/audio' // Unlock AudioContext + SpeechSynthesis on first user interaction (mobile requires gesture) @@ -21,7 +22,9 @@ onMounted(() => {
- + + +
diff --git a/frontend/src/components/ErrorBoundary.vue b/frontend/src/components/ErrorBoundary.vue new file mode 100644 index 0000000..d43e516 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/components/__tests__/ErrorBoundary.test.ts b/frontend/src/components/__tests__/ErrorBoundary.test.ts new file mode 100644 index 0000000..15b17c0 --- /dev/null +++ b/frontend/src/components/__tests__/ErrorBoundary.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { defineComponent, onMounted } from 'vue' +import ErrorBoundary from '../ErrorBoundary.vue' + +const ThrowingChild = defineComponent({ + setup() { + onMounted(() => { + throw new Error('Test error from child') + }) + }, + template: '
Should not render
', +}) + +const GoodChild = defineComponent({ + template: '
All good
', +}) + +describe('ErrorBoundary', () => { + it('renders slot content when no error', () => { + const wrapper = mount(ErrorBoundary, { + slots: { default: GoodChild }, + }) + expect(wrapper.text()).toContain('All good') + }) + + it('catches error from child and shows error message', async () => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + + const wrapper = mount(ErrorBoundary, { + slots: { default: ThrowingChild }, + }) + + // Wait for onMounted to fire + await wrapper.vm.$nextTick() + + expect(wrapper.text()).toContain('Something went wrong') + expect(wrapper.text()).toContain('Test error from child') + + vi.restoreAllMocks() + }) + + it('shows reload button on error', async () => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + + const wrapper = mount(ErrorBoundary, { + slots: { default: ThrowingChild }, + }) + + await wrapper.vm.$nextTick() + + const button = wrapper.find('button') + expect(button.exists()).toBe(true) + expect(button.text()).toBe('RELOAD') + + vi.restoreAllMocks() + }) +})