feat: the unified card shows referenced images, their text, and an undo (4402)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 18s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m22s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m38s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Skipped

The linked drop's images and the piece's variants join the teaser's rail,
after its own images, each with a corner badge. The meta line counts them
apart ("+N from Discord"), so a Patreon card never reads as though the creator
posted Discord's files there. The text of each referenced post sits under
the teaser's body.

If FC made the link, the card says so, names the matching working name and
offers Undo. If a person accepted it, it offers Unlink and makes no claim
about FC. Both dismiss the pair. The kept row then stops the next sweep from
linking it straight back.

The announcement settings card gets the two windows: how close a drop must be
before its own card is left out of the feed, and how far to reach for
variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-24 11:25:52 -04:00
co-authored by Claude Opus 5.5
parent 30a263a47a
commit 7aa074d1a9
5 changed files with 328 additions and 9 deletions
+89
View File
@@ -4,6 +4,7 @@ import { flushPromises } from '@vue/test-utils'
import PostCard from '../../src/components/posts/PostCard.vue'
import { useModalStore } from '../../src/stores/modal.js'
import { usePostsStore } from '../../src/stores/posts.js'
import { freshPinia, mountComponent } from '../support/mountComponent.js'
const now = new Date().toISOString()
@@ -201,3 +202,91 @@ describe('PostCard', () => {
expect(openSpy).toHaveBeenCalledWith(10, { playlistIds: [10, 11] })
})
})
// #4402. A teaser is a pointer, so its card shows what it points at — by
// REFERENCE. These pin the honesty half as hard as the feature half: the
// referenced images are marked as Discord's, and a link FC made by itself says
// so and offers the undo the operator chose over a silent merge.
describe('the unified card', () => {
const UNIFIED = {
links: [{ association_id: 7, post_id: 42, linked_by: 'fc', token: '0-k' }],
thumbnails: [
{ image_id: 200, thumbnail_url: '/d0', post_id: 42, role: 'drop' },
{ image_id: 201, thumbnail_url: '/v0', post_id: 40, role: 'variant' },
],
variant_count: 1,
texts: [
{ post_id: 42, role: 'drop', date: now, text: '@everyone the full set' },
{ post_id: 40, role: 'variant', date: now, text: 'wip, feedback welcome' },
],
}
const TEASER = {
...BASE,
description_plain: 'Full set in the server',
thumbnails: [{ image_id: 10, thumbnail_url: '/a' }],
associations: [{ id: 7, role: 'announces', post_id: 42 }],
unified: UNIFIED,
}
it('shows the drop and its variants beside the teaser, marked as references', () => {
const w = mountComponent(PostCard, { props: { post: TEASER }, pinia: freshPinia() })
const refs = w.findAll('.fc-post-card__rail-cell--ref')
expect(refs).toHaveLength(2)
// Counted apart from the teaser's own, so the card never reads as though
// the creator put Discord's files on Patreon.
expect(w.text()).toContain('1 image')
expect(w.text()).toContain('+2 from Discord')
})
it('carries the text of everything it unifies', () => {
const w = mountComponent(PostCard, { props: { post: TEASER }, pinia: freshPinia() })
expect(w.text()).toContain('@everyone the full set')
expect(w.text()).toContain('wip, feedback welcome')
})
it('says FC made the link, on what, and offers an undo', () => {
const w = mountComponent(PostCard, { props: { post: TEASER }, pinia: freshPinia() })
expect(w.text()).toContain('Linked by FabledCurator')
expect(w.text()).toContain('0-k')
expect(w.find('.fc-post-card__undo').text()).toBe('Undo')
// The old text link would say the same thing twice.
expect(w.text()).not.toContain('The full set is in Discord')
})
it('never claims FC made a link a person accepted', () => {
const post = {
...TEASER,
unified: { ...UNIFIED, links: [{ ...UNIFIED.links[0], linked_by: 'operator' }] },
}
const w = mountComponent(PostCard, { props: { post }, pinia: freshPinia() })
expect(w.text()).not.toContain('Linked by FabledCurator')
expect(w.find('.fc-post-card__undo').text()).toBe('Unlink')
})
it('undo dismisses that one link', async () => {
const pinia = freshPinia()
const store = usePostsStore()
const spy = vi.spyOn(store, 'unlink').mockResolvedValue()
const w = mountComponent(PostCard, { props: { post: TEASER }, pinia })
await w.find('.fc-post-card__undo').trigger('click')
await flushPromises()
expect(spy).toHaveBeenCalledWith(1, 7)
})
it('arrows through the teaser first, then what it references', async () => {
const pinia = freshPinia()
const openSpy = vi.spyOn(useModalStore(), 'open').mockResolvedValue()
const w = mountComponent(PostCard, { props: { post: TEASER }, pinia })
await w.find('.fc-post-card__hero').trigger('click')
await flushPromises()
expect(openSpy).toHaveBeenCalledWith(10, { playlistIds: [10, 200, 201] })
})
it('an ordinary post is untouched', () => {
const w = mountComponent(PostCard, {
props: { post: { ...BASE, unified: null } }, pinia: freshPinia(),
})
expect(w.find('.fc-post-card__unified').exists()).toBe(false)
expect(w.text()).not.toContain('from Discord')
})
})