Web app: new entities (Image, RenderedAsset, SharedImage, Token, DeviceImageHistory), enums, repositories, controllers, message handlers, migrations, tests, frontend upload/library/sticker UI, Vue components. Firmware: EPD background screen binaries + gen scripts, setup_bg header. Infra: ddev config, test bundle, gitignore coverage dir. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useDevicesStore } from '@/stores/devices'
|
||||
import { useUploadStore } from '@/stores/upload'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import type { Device } from '@/types'
|
||||
|
||||
// Stub heavy child components so tests focus on HomeView logic
|
||||
vi.mock('@/components/FrameCard.vue', () => ({
|
||||
default: {
|
||||
name: 'FrameCard',
|
||||
template: '<div class="frame-card-stub" :data-device-id="deviceId" :data-name="name" />',
|
||||
props: ['deviceId', 'name', 'size', 'status', 'orientation'],
|
||||
emits: ['add-photo', 'edit'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/BaseBottomSheet.vue', () => ({
|
||||
default: {
|
||||
name: 'BaseBottomSheet',
|
||||
template: '<div><slot /></div>',
|
||||
props: ['modelValue', 'label'],
|
||||
emits: ['update:modelValue'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/BaseButton.vue', () => ({
|
||||
default: {
|
||||
name: 'BaseButton',
|
||||
template: '<button><slot /></button>',
|
||||
props: ['variant', 'disabled'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/BaseInput.vue', () => ({
|
||||
default: {
|
||||
name: 'BaseInput',
|
||||
template: '<input />',
|
||||
props: ['modelValue', 'label', 'maxlength'],
|
||||
emits: ['update:modelValue'],
|
||||
},
|
||||
}))
|
||||
vi.mock('@/components/OrientationPicker.vue', () => ({
|
||||
default: {
|
||||
name: 'OrientationPicker',
|
||||
template: '<div />',
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
},
|
||||
}))
|
||||
|
||||
// Stub vue-router so HomeView can call useRouter() without a real router
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
// Stub URL.createObjectURL used by upload store
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: vi.fn(() => 'blob:mock-url'),
|
||||
revokeObjectURL: vi.fn(),
|
||||
})
|
||||
|
||||
const makeDevice = (overrides: Partial<Device> = {}): Device => ({
|
||||
id: 1,
|
||||
mac: 'AA:BB:CC:DD:EE:FF',
|
||||
name: 'Living Room',
|
||||
orientation: 'landscape',
|
||||
rotationIntervalMinutes: 60,
|
||||
wakeHour: null,
|
||||
timezone: 'America/Chicago',
|
||||
uniquenessWindow: 30,
|
||||
linkedAt: '2026-01-01T00:00:00Z',
|
||||
lastSeenAt: null,
|
||||
lockedImageId: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('HomeView', () => {
|
||||
let pinia: ReturnType<typeof createPinia>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
pinia = createPinia()
|
||||
setActivePinia(pinia)
|
||||
|
||||
// Re-stub URL after unstubAllGlobals
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: vi.fn(() => 'blob:mock-url'),
|
||||
revokeObjectURL: vi.fn(),
|
||||
})
|
||||
|
||||
// Stub fetch so onMounted fetchDevices doesn't fail
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
}))
|
||||
})
|
||||
|
||||
function mountView() {
|
||||
return mount(HomeView, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// HV-01: N devices renders N FrameCard stubs
|
||||
it('renders one FrameCard per device when devices are present', async () => {
|
||||
const devicesStore = useDevicesStore()
|
||||
devicesStore.devices = [
|
||||
makeDevice({ id: 1, name: 'Frame A' }),
|
||||
makeDevice({ id: 2, name: 'Frame B' }),
|
||||
makeDevice({ id: 3, name: 'Frame C' }),
|
||||
]
|
||||
// Mock fetchDevices so onMounted doesn't overwrite devices
|
||||
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mountView()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cards = wrapper.findAll('.frame-card-stub')
|
||||
expect(cards).toHaveLength(3)
|
||||
})
|
||||
|
||||
// HV-01b: single device still renders one FrameCard (large variant branch)
|
||||
it('renders one FrameCard for a single device', async () => {
|
||||
const devicesStore = useDevicesStore()
|
||||
devicesStore.devices = [makeDevice({ id: 1, name: 'Solo Frame' })]
|
||||
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mountView()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cards = wrapper.findAll('.frame-card-stub')
|
||||
expect(cards).toHaveLength(1)
|
||||
})
|
||||
|
||||
// HV-02: empty state shown when no devices
|
||||
it('shows empty state when devices list is empty', async () => {
|
||||
const devicesStore = useDevicesStore()
|
||||
devicesStore.devices = []
|
||||
devicesStore.loading = false
|
||||
vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue()
|
||||
|
||||
const wrapper = mountView()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.home-view__empty').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Set up your first frame')
|
||||
})
|
||||
|
||||
// HV-03: loading state shown while fetching
|
||||
it('shows loading indicator when store is loading', async () => {
|
||||
const devicesStore = useDevicesStore()
|
||||
devicesStore.loading = true
|
||||
// Keep fetchDevices pending so loading stays true
|
||||
vi.spyOn(devicesStore, 'fetchDevices').mockReturnValue(new Promise(() => {}))
|
||||
|
||||
const wrapper = mountView()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('.home-view__loading').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Loading')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user