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() + }) +})