import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import FrameCard from '@/components/FrameCard.vue' // Mock vue-konva to avoid canvas issues if transitively imported vi.mock('vue-konva', () => ({})) const defaultProps = { deviceId: 1, name: 'Living Room', size: 'large' as const, status: 'ok' as const, orientation: 'landscape' as const, } describe('FrameCard', () => { it('renders device name', () => { const wrapper = mount(FrameCard, { props: defaultProps }) expect(wrapper.text()).toContain('Living Room') }) it('shows "Online" status when status is ok', () => { const wrapper = mount(FrameCard, { props: defaultProps }) expect(wrapper.find('.frame-card__status-line').text()).toContain('Online') }) it('shows "Offline" status when status is offline', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, status: 'offline' }, }) expect(wrapper.find('.frame-card__status-line').text()).toContain('Offline') }) it('shows "Sync issue" status when status is sync-fail', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, status: 'sync-fail' }, }) expect(wrapper.find('.frame-card__status-line').text()).toContain('Sync issue') }) it('renders lastSync and nextSync lines on the large card', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, lastSync: '2h ago', nextSync: 'next sync in 4h' }, }) const sync = wrapper.find('.frame-card__sync-line') expect(sync.exists()).toBe(true) expect(sync.text()).toContain('synced 2h ago') expect(sync.text()).toContain('next sync in 4h') }) it('omits the sync line on the large card when no sync info is provided', () => { const wrapper = mount(FrameCard, { props: defaultProps }) expect(wrapper.find('.frame-card__sync-line').exists()).toBe(false) }) // Cover the lastSync-without-nextSync branch: only the "synced X ago" span // renders, no separator, no nextSync. it('renders only lastSync when nextSync is absent', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, lastSync: '5m ago' }, }) const sync = wrapper.find('.frame-card__sync-line') expect(sync.text()).toContain('synced 5m ago') expect(sync.find('.frame-card__sync-sep').exists()).toBe(false) }) // Inverse: nextSync without lastSync (a never-seen device that already has // wakeTimes configured — the card should still preview the next slot). it('renders only nextSync when lastSync is absent', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, nextSync: 'next sync ~6 AM tomorrow' }, }) const sync = wrapper.find('.frame-card__sync-line') expect(sync.exists()).toBe(true) expect(sync.text()).toContain('next sync ~6 AM tomorrow') expect(sync.text()).not.toContain('synced') }) it('applies offline modifier class when status is offline', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, status: 'offline' }, }) expect(wrapper.classes()).toContain('frame-card--offline') }) it('applies sync-fail modifier class when status is sync-fail', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, status: 'sync-fail' }, }) expect(wrapper.classes()).toContain('frame-card--sync-fail') }) it('shows settings button in large size', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large' } }) expect(wrapper.find('.frame-card__settings-btn').exists()).toBe(true) }) it('does not show settings button in compact size', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'compact' }, }) expect(wrapper.find('.frame-card__settings-btn').exists()).toBe(false) }) it('shows img element when thumbnailUrl is provided', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, thumbnailUrl: '/thumb/test.jpg' }, }) const img = wrapper.find('img.frame-card__img') expect(img.exists()).toBe(true) expect(img.attributes('src')).toBe('/thumb/test.jpg') }) it('shows empty preview placeholder when no thumbnailUrl', () => { const wrapper = mount(FrameCard, { props: defaultProps }) expect(wrapper.find('.frame-card__empty-preview').exists()).toBe(true) expect(wrapper.find('img.frame-card__img').exists()).toBe(false) }) it('shows photo count in compact size', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'compact', photoCount: 3 }, }) expect(wrapper.text()).toContain('3 photos') }) it('uses singular "photo" when photoCount is 1', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'compact', photoCount: 1 }, }) expect(wrapper.text()).toContain('1 photo') expect(wrapper.text()).not.toContain('1 photos') }) it('emits add-photo with deviceId when add button clicked', async () => { const wrapper = mount(FrameCard, { props: defaultProps }) await wrapper.find('.frame-card__add-btn').trigger('click') expect(wrapper.emitted('add-photo')).toBeTruthy() expect(wrapper.emitted('add-photo')![0]).toEqual([1]) }) it('emits edit with deviceId when settings button clicked (large)', async () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large' } }) await wrapper.find('.frame-card__settings-btn').trigger('click') expect(wrapper.emitted('edit')).toBeTruthy() expect(wrapper.emitted('edit')![0]).toEqual([1]) }) it('reserves a V1 landscape (1600 × 960) placeholder when no thumbnail is present', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large', orientation: 'landscape', model: 'v1' }, }) const empty = wrapper.find('.frame-card__empty-preview') expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*960/) }) it('reserves a V1 portrait (960 × 1600) placeholder when no thumbnail is present', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large', orientation: 'portrait', model: 'v1' }, }) const empty = wrapper.find('.frame-card__empty-preview') expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*960\s*\/\s*1600/) }) it('reserves a V2 portrait (1200 × 1600) placeholder when no thumbnail is present', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large', orientation: 'portrait', model: 'v2' }, }) const empty = wrapper.find('.frame-card__empty-preview') expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1200\s*\/\s*1600/) }) it('reserves a V2 landscape (1600 × 1200) placeholder when no thumbnail is present', () => { const wrapper = mount(FrameCard, { props: { ...defaultProps, size: 'large', orientation: 'landscape', model: 'v2' }, }) const empty = wrapper.find('.frame-card__empty-preview') expect(empty.attributes('style')).toMatch(/aspect-ratio:\s*1600\s*\/\s*1200/) }) })