chore: stage all in-progress work before repo split
CI / test (push) Has been cancelled

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:
2026-05-06 12:11:31 -04:00
parent 062c52eec7
commit 12245759ac
149 changed files with 14846 additions and 92 deletions
@@ -0,0 +1,131 @@
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('does not show status badge when status is ok', () => {
const wrapper = mount(FrameCard, { props: defaultProps })
expect(wrapper.find('.frame-card__status-badge').exists()).toBe(false)
})
it('shows "Offline" badge when status is offline', () => {
const wrapper = mount(FrameCard, {
props: { ...defaultProps, status: 'offline' },
})
const badge = wrapper.find('.frame-card__status-badge')
expect(badge.exists()).toBe(true)
expect(badge.text()).toContain('Offline')
})
it('shows "Sync issue" badge when status is sync-fail', () => {
const wrapper = mount(FrameCard, {
props: { ...defaultProps, status: 'sync-fail' },
})
const badge = wrapper.find('.frame-card__status-badge')
expect(badge.exists()).toBe(true)
expect(badge.text()).toContain('Sync issue')
})
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('sets landscape aspect ratio style in large mode', () => {
const wrapper = mount(FrameCard, {
props: { ...defaultProps, size: 'large', orientation: 'landscape' },
})
const preview = wrapper.find('.frame-card__preview')
// Vue serializes { aspectRatio: '5/3' } as 'aspect-ratio: 5 / 3;'
expect(preview.attributes('style')).toMatch(/aspect-ratio:\s*5\s*\/\s*3/)
})
it('sets portrait aspect ratio style in large mode', () => {
const wrapper = mount(FrameCard, {
props: { ...defaultProps, size: 'large', orientation: 'portrait' },
})
const preview = wrapper.find('.frame-card__preview')
expect(preview.attributes('style')).toMatch(/aspect-ratio:\s*3\s*\/\s*5/)
})
})