Create responsive layout system based on route meta: - DefaultLayout: sidebar (desktop) / bottom tabs (mobile) - MinimalLayout: centered content for auth pages - GameLayout: full viewport for Phaser game Navigation components: - NavSidebar: desktop sidebar with main nav + user menu - NavBottomTabs: mobile bottom tab bar UI components (tied to UI store): - LoadingOverlay: full-screen overlay with spinner - ToastContainer: stacked notification toasts Also adds Vue Router meta type declarations. Phase F0 is now complete (8/8 tasks). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
|
|
import { useUiStore } from '@/stores/ui'
|
|
import LoadingOverlay from './LoadingOverlay.vue'
|
|
|
|
describe('LoadingOverlay', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
// Clear any teleported content from previous tests
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
describe('visibility', () => {
|
|
it('is not visible when not loading', () => {
|
|
/**
|
|
* Test that the overlay is hidden by default.
|
|
*
|
|
* When no loading operations are active, the overlay should
|
|
* not be visible to avoid blocking the UI.
|
|
*/
|
|
mount(LoadingOverlay)
|
|
const ui = useUiStore()
|
|
|
|
expect(ui.isLoading).toBe(false)
|
|
expect(document.body.querySelector('.fixed')).toBeNull()
|
|
})
|
|
|
|
it('is visible when loading', async () => {
|
|
/**
|
|
* Test that the overlay appears when loading starts.
|
|
*
|
|
* The overlay should be teleported to body and be visible
|
|
* when showLoading() is called.
|
|
*/
|
|
mount(LoadingOverlay)
|
|
const ui = useUiStore()
|
|
|
|
ui.showLoading()
|
|
|
|
// Wait for the teleport and transition
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
|
|
expect(ui.isLoading).toBe(true)
|
|
expect(document.body.querySelector('.fixed')).not.toBeNull()
|
|
})
|
|
|
|
it('shows loading message when provided', async () => {
|
|
/**
|
|
* Test that loading messages are displayed.
|
|
*
|
|
* Components can provide context about what's loading,
|
|
* which should be shown to the user.
|
|
*/
|
|
mount(LoadingOverlay)
|
|
const ui = useUiStore()
|
|
|
|
ui.showLoading('Saving game...')
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
|
|
expect(document.body.textContent).toContain('Saving game...')
|
|
})
|
|
|
|
it('hides when loading count reaches zero', async () => {
|
|
/**
|
|
* Test that the overlay hides after all loading calls complete.
|
|
*
|
|
* Multiple components may trigger loading simultaneously.
|
|
* The overlay should stay visible until all are done.
|
|
*/
|
|
mount(LoadingOverlay)
|
|
const ui = useUiStore()
|
|
|
|
ui.showLoading()
|
|
ui.showLoading() // Second concurrent load
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
expect(document.body.querySelector('.fixed')).not.toBeNull()
|
|
|
|
ui.hideLoading() // First load done
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
expect(document.body.querySelector('.fixed')).not.toBeNull() // Still visible
|
|
|
|
ui.hideLoading() // Second load done
|
|
await new Promise(resolve => setTimeout(resolve, 250)) // Wait for transition
|
|
expect(document.body.querySelector('.fixed')).toBeNull() // Now hidden
|
|
})
|
|
})
|
|
})
|