mantimon-tcg/frontend/src/components/ui/ErrorBoundary.spec.ts
Cal Corum 913b1e7eae Fix audit issues: OAuth login, remove dead code, add error boundary
Code audit fixes:
- Update LoginPage for OAuth (Google/Discord buttons, no password)
- Delete RegisterPage.vue (OAuth-only app)
- Delete AppHeader.vue (superseded by NavSidebar, had bugs)
- Add ErrorBoundary component for graceful error handling

Also adds Phase F1 (Authentication Flow) plan with 10 tasks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 11:42:26 -06:00

190 lines
5.0 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import ErrorBoundary from './ErrorBoundary.vue'
describe('ErrorBoundary', () => {
describe('normal operation', () => {
it('renders slot content when no error', () => {
/**
* Test that child content is displayed normally.
*
* When no errors occur, the error boundary should be
* transparent and render its slot content as-is.
*/
const wrapper = mount(ErrorBoundary, {
slots: {
default: '<div class="child">Hello World</div>',
},
})
expect(wrapper.find('.child').exists()).toBe(true)
expect(wrapper.text()).toContain('Hello World')
})
})
describe('error handling', () => {
it('shows fallback UI when child throws', async () => {
/**
* Test that errors in children are caught and handled.
*
* When a child component throws, the error boundary should
* display a friendly fallback UI instead of crashing.
*/
const ThrowingComponent = defineComponent({
setup() {
throw new Error('Test error')
},
render() {
return h('div', 'Should not render')
},
})
const wrapper = mount(ErrorBoundary, {
slots: {
default: h(ThrowingComponent),
},
})
await flushPromises()
expect(wrapper.text()).toContain('Oops!')
expect(wrapper.text()).toContain('Something went wrong')
expect(wrapper.find('.child').exists()).toBe(false)
})
it('shows custom fallback message', async () => {
/**
* Test that custom fallback messages are displayed.
*
* Components can customize the error message to provide
* context-specific guidance to users.
*/
const ThrowingComponent = defineComponent({
setup() {
throw new Error('Test error')
},
render() {
return h('div')
},
})
const wrapper = mount(ErrorBoundary, {
props: {
fallbackMessage: 'Failed to load game data.',
},
slots: {
default: h(ThrowingComponent),
},
})
await flushPromises()
expect(wrapper.text()).toContain('Failed to load game data.')
})
it('emits error event when catching', async () => {
/**
* Test that error events are emitted for parent handling.
*
* Parent components may want to log errors or take
* additional recovery actions.
*/
const ThrowingComponent = defineComponent({
setup() {
throw new Error('Test error message')
},
render() {
return h('div')
},
})
const wrapper = mount(ErrorBoundary, {
slots: {
default: h(ThrowingComponent),
},
})
await flushPromises()
const errorEvents = wrapper.emitted('error')
expect(errorEvents).toBeTruthy()
expect(errorEvents![0][0]).toBeInstanceOf(Error)
expect((errorEvents![0][0] as Error).message).toBe('Test error message')
})
})
describe('recovery', () => {
it('has retry button', async () => {
/**
* Test that retry button is available.
*
* Users should be able to attempt recovery without
* refreshing the entire page.
*/
const ThrowingComponent = defineComponent({
setup() {
throw new Error('Test error')
},
render() {
return h('div')
},
})
const wrapper = mount(ErrorBoundary, {
slots: {
default: h(ThrowingComponent),
},
})
await flushPromises()
const retryButton = wrapper.find('button')
expect(retryButton.exists()).toBe(true)
expect(retryButton.text()).toContain('Try Again')
})
it('resets error state on retry', async () => {
/**
* Test that clicking retry clears the error state.
*
* After retry, the boundary should attempt to re-render
* the slot content.
*/
let shouldThrow = true
const ConditionalThrowComponent = defineComponent({
setup() {
if (shouldThrow) {
throw new Error('Test error')
}
},
render() {
return h('div', { class: 'success' }, 'Success!')
},
})
const wrapper = mount(ErrorBoundary, {
slots: {
default: h(ConditionalThrowComponent),
},
})
await flushPromises()
// Error state active
expect(wrapper.text()).toContain('Oops!')
// Disable throwing and retry
shouldThrow = false
await wrapper.find('button').trigger('click')
await flushPromises()
// Should attempt to render slot again
// Note: The slot is still the throwing component from mount time,
// so we just verify error state is cleared
expect(wrapper.text()).not.toContain('Oops!')
})
})
})