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>
190 lines
5.0 KiB
TypeScript
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!')
|
|
})
|
|
})
|
|
})
|