// @vitest-environment happy-dom import { describe, it, expect, vi } from 'vitest' import { flushPromises } from '@vue/test-utils' import PostCard from '../../src/components/posts/PostCard.vue' import { useModalStore } from '../../src/stores/modal.js' import { freshPinia, mountComponent } from '../support/mountComponent.js' const now = new Date().toISOString() const BASE = { id: 1, external_post_id: 'P1', post_title: 'Hello World', post_url: 'https://x/1', post_date: now, downloaded_at: now, artist: { id: 1, name: 'Sabu', slug: 'sabu' }, source: { id: 1, platform: 'subscribestar' }, thumbnails: [], thumbnails_more: 0, attachments: [], } describe('PostCard', () => { it('renders the HTML-stripped title and the artist', () => { const post = { ...BASE, description_plain: 'a description' } const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() }) const t = w.text() expect(t).toContain('Hello World') // plain title expect(t).not.toContain('') // tags stripped expect(t).toContain('Sabu') // artist (RouterLink slot) }) it('shows a "Show more" toggle only when the description is truncated', () => { const truncated = mountComponent(PostCard, { props: { post: { ...BASE, description_plain: 'lots of text…', description_truncated: true } }, pinia: freshPinia(), }) expect(truncated.text()).toContain('Show more') const full = mountComponent(PostCard, { props: { post: { ...BASE, description_plain: 'short', description_truncated: false } }, pinia: freshPinia(), }) expect(full.text()).not.toContain('Show more') }) // #388 E2. The honesty marker is the only thing standing between "FC // assembled this" and the card reading as something the artist authored, so // these pin BOTH directions: it appears when the flag is set, and — the one // that actually matters — it is absent on every ordinary post. describe('synthetic posts', () => { const SYNTH = { ...BASE, post_title: null, synthesized_by: 'discord_drop', synthesis: { message_count: 4, member_post_ids: [1, 2, 3, 4] }, } it('says FC grouped it, and what from', () => { const w = mountComponent(PostCard, { props: { post: SYNTH }, pinia: freshPinia() }) expect(w.find('.fc-post-card__synthetic').exists()).toBe(true) expect(w.text()).toContain('grouped by FabledCurator') expect(w.text()).toContain('Grouped from 4 Discord messages') }) it('never marks an ordinary post', () => { const w = mountComponent(PostCard, { props: { post: BASE }, pinia: freshPinia() }) expect(w.find('.fc-post-card__synthetic').exists()).toBe(false) expect(w.text()).not.toContain('grouped by FabledCurator') }) it('does not leak the internal drop key as a title', () => { // post_title is NULL on purpose (inventing one would put words in a // creator's mouth), so the untitled fallback must not print // `fc-drop:` the way it does for a real untitled post. const w = mountComponent(PostCard, { props: { post: { ...SYNTH, external_post_id: 'fc-drop:99887766' } }, pinia: freshPinia(), }) expect(w.text()).not.toContain('fc-drop:') }) it('degrades to unmarked when the field is absent entirely', () => { // A post dict composed before the field existed must read as "not // synthetic" rather than throw on `synthesis.message_count`. const { synthesized_by: _drop, synthesis: _also, ...legacy } = SYNTH const w = mountComponent(PostCard, { props: { post: legacy }, pinia: freshPinia() }) expect(w.find('.fc-post-card__synthetic').exists()).toBe(false) }) it('singularises a one-message drop', () => { const w = mountComponent(PostCard, { props: { post: { ...SYNTH, synthesis: { message_count: 1 } } }, pinia: freshPinia(), }) expect(w.text()).toContain('Grouped from 1 Discord message') expect(w.text()).not.toContain('1 Discord messages') }) }) const thumbs = (n) => Array.from({ length: n }, (_, i) => ({ image_id: 100 + i, thumbnail_url: `/t${i}` })) it('fills the rail to full width (5 cells, no overflow) for a 6-image post', () => { const post = { ...BASE, description_plain: 'd', thumbnails: thumbs(6), thumbnails_more: 0 } const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() }) // hero + 5 rail thumbnails, no "+N" tile. expect(w.findAll('.fc-post-card__rail-cell')).toHaveLength(5) expect(w.find('.fc-post-card__rail-more').exists()).toBe(false) // The grid spans the hero width via a column count == cells shown. expect(w.find('.fc-post-card__rail').attributes('style')).toContain('--fc-rail-cols: 5') }) it('reserves the last cell for a "+N" overflow tile when there are more images', () => { const post = { ...BASE, description_plain: 'd', thumbnails: thumbs(6), thumbnails_more: 4 } const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() }) // 4 thumbnails + 1 overflow tile == 5 columns; tile counts every hidden image. expect(w.findAll('.fc-post-card__rail-cell')).toHaveLength(4) const more = w.find('.fc-post-card__rail-more') expect(more.exists()).toBe(true) expect(more.text()).toBe('+5') expect(w.find('.fc-post-card__rail').attributes('style')).toContain('--fc-rail-cols: 5') }) it('clicking an image opens the post-scoped modal (never expands the card)', async () => { const pinia = freshPinia() const modal = useModalStore() const openSpy = vi.spyOn(modal, 'open').mockResolvedValue() const post = { ...BASE, description_plain: 'd', thumbnails: [ { image_id: 10, thumbnail_url: '/a' }, { image_id: 11, thumbnail_url: '/b' }, ], thumbnails_more: 0, } const w = mountComponent(PostCard, { props: { post }, pinia }) await w.find('.fc-post-card__hero').trigger('click') await flushPromises() expect(openSpy).toHaveBeenCalledWith(10, { playlistIds: [10, 11] }) }) })