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 @@
+
+
+
+
+
Something went wrong
+
{{ errorMessage }}
+
+
+
+
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()
+ })
+})