diff --git a/frontend/src/components/FrameCard.vue b/frontend/src/components/FrameCard.vue index 38ea644..a801acb 100644 --- a/frontend/src/components/FrameCard.vue +++ b/frontend/src/components/FrameCard.vue @@ -168,23 +168,18 @@ const statusText = computed(() => { opacity: 0.6; } - // ── Large (single device) ──────────────────────────────────────────────── - // Portrait frames have aspect 3:5 — at full mobile width (~360px) that would - // be 600px tall and totally dominate the screen. Cap so the card stays - // phone-friendly while still showing the photo at the frame's real shape. + // ── Large (single device or carousel slide) ────────────────────────────── &--large &__preview { background: var(--color-surface-2); display: flex; align-items: center; justify-content: center; - max-height: min(240px, 30dvh); - overflow: hidden; } &--large &__img { width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } &--large &__empty-preview { diff --git a/frontend/src/test/views/HomeView.test.ts b/frontend/src/test/views/HomeView.test.ts index 47e92dc..7033f29 100644 --- a/frontend/src/test/views/HomeView.test.ts +++ b/frontend/src/test/views/HomeView.test.ts @@ -106,22 +106,71 @@ describe('HomeView', () => { }) } - // HV-01: N devices renders N FrameCard stubs - it('renders one FrameCard per device when devices are present', async () => { + // HV-01: N devices renders a carousel of N large FrameCard stubs + N dots + it('renders one FrameCard per device in a carousel when multiple devices 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) + expect(wrapper.find('.home-view__carousel').exists()).toBe(true) + expect(wrapper.findAll('.frame-card-stub')).toHaveLength(3) + // All cards should be the large variant (no more compact stack) + const cards = wrapper.findAllComponents({ name: 'FrameCard' }) + for (const c of cards) expect(c.props('size')).toBe('large') + // One navigation dot per device + expect(wrapper.findAll('.home-view__dot')).toHaveLength(3) + }) + + it('marks the first dot active by default and updates active dot on scroll', async () => { + const devicesStore = useDevicesStore() + devicesStore.devices = [ + makeDevice({ id: 1, name: 'A' }), + makeDevice({ id: 2, name: 'B' }), + ] + vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() + + const wrapper = mountView() + await wrapper.vm.$nextTick() + let dots = wrapper.findAll('.home-view__dot') + expect(dots[0].classes()).toContain('home-view__dot--active') + expect(dots[1].classes()).not.toContain('home-view__dot--active') + + // Simulate the carousel having scrolled to the second slide + const carousel = wrapper.find('.home-view__carousel').element as HTMLElement + Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 }) + Object.defineProperty(carousel, 'scrollLeft', { configurable: true, value: 360 }) + await wrapper.find('.home-view__carousel').trigger('scroll') + await wrapper.vm.$nextTick() + + dots = wrapper.findAll('.home-view__dot') + expect(dots[1].classes()).toContain('home-view__dot--active') + expect(dots[0].classes()).not.toContain('home-view__dot--active') + }) + + it('clicking a dot scrolls the carousel to that slide', async () => { + const devicesStore = useDevicesStore() + devicesStore.devices = [makeDevice({ id: 1 }), makeDevice({ id: 2 }), makeDevice({ id: 3 })] + vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() + + const wrapper = mountView() + await wrapper.vm.$nextTick() + + const carousel = wrapper.find('.home-view__carousel').element as HTMLElement + Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 }) + const scrollToSpy = vi.fn() + ;(carousel as any).scrollTo = scrollToSpy + + await wrapper.findAll('.home-view__dot')[2].trigger('click') + await wrapper.vm.$nextTick() + + expect(scrollToSpy).toHaveBeenCalledWith(expect.objectContaining({ left: 720, behavior: 'smooth' })) }) // HV-01b: single device still renders one FrameCard (large variant branch) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index dd93061..8d4a8c5 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -37,23 +37,51 @@ /> - -
- -
+ + @@ -103,7 +131,7 @@ +