import { fireEvent, render, waitFor } from '@testing-library/svelte' import { describe, expect, it, vi } from 'vitest' import { APP_SERVICES } from '$lib/app-services' import { account, preferences, session, status, testServices } from '$test/fixtures' import BlogEntry from './BlogEntry.svelte' function renderEntry(overrides: Parameters[0]) { return render(BlogEntry, { props: { status: status(overrides) }, context: new Map([[APP_SERVICES, testServices()]]), }) } describe('BlogEntry YouTube embeds', () => { it('turns a YouTube note link into an attachment-style embed', () => { const view = renderEntry({ content: '

Watch this: https://www.youtube.com/watch?v=pyNCigSulNs

', card: { url: 'https://www.youtube.com/watch?v=pyNCigSulNs', title: 'Server-generated YouTube preview', description: 'This should be replaced by the player.', type: 'video', }, }) const frame = view.getByTitle('YouTube video') expect(frame).toHaveAttribute( 'src', 'https://www.youtube-nocookie.com/embed/pyNCigSulNs', ) expect(frame.closest('.attachment')).toHaveAttribute('data-type', 'youtube') expect(view.getByRole('link', { name: 'Watch on YouTube' })).toHaveAttribute( 'href', 'https://www.youtube.com/watch?v=pyNCigSulNs', ) expect(view.queryByText('Server-generated YouTube preview')).not.toBeInTheDocument() }) it('does not load a sensitive player before the reader reveals it', async () => { const view = renderEntry({ content: '

video

', sensitive: true, }) expect(view.queryByTitle('YouTube video')).not.toBeInTheDocument() await fireEvent.click(view.getByRole('button', { name: 'Show sensitive video' })) expect(view.getByTitle('YouTube video')).toBeInTheDocument() }) }) describe('BlogEntry MFM rendering', () => { it('renders the Pleroma API MFM HTML inside a backend-free blog entry', () => { const view = renderEntry({ content: '

It works

', }) const effect = view.getByText('works') expect(effect).toHaveClass('mfm', '-pause') expect(effect).toHaveAttribute('data-mfm-operator', 'spin') expect(effect).toHaveStyle({ animationName: 'mfm-spin', animationDuration: '2s', animationDirection: 'reverse', }) }) }) describe('BlogEntry Heleneposting', () => { it('replaces only the displayed author name and avatar when enabled', () => { const original = account({ acct: 'actual-author', display_name: 'Actual Author', avatar: 'https://example.test/actual.png', avatar_static: 'https://example.test/actual.png', }) const services = testServices({ preferences: preferences({ heleneposting: true }), }) const view = render(BlogEntry, { props: { status: status({ account: original, content: '

Signed through HTML
— Helene

', }), }, context: new Map([[APP_SERVICES, services]]), }) expect(view.container.querySelector('.blog-entry-author')).toHaveAttribute( 'href', '#/@actual-author', ) expect(view.queryByText('Actual Author')).not.toBeInTheDocument() expect(view.container.querySelector('.blog-entry-avatar img')).toHaveAttribute( 'src', expect.stringContaining('helene'), ) expect(view.getByText('@actual-author@example.test')).toBeInTheDocument() }) }) describe('BlogEntry quote posts', () => { it('renders a Pleroma quote and links to quote composition and the quote list', () => { const quoted = status({ id: 'quoted-entry', content: '

Quoted words

', account: account({ id: 'quoted-author', display_name: 'Quoted Author' }), }) const outer = status({ id: 'outer-entry', content: '

My commentary

', quotes_count: 3, pleroma: { quote: quoted, quote_id: quoted.id, quote_visible: true }, }) const services = testServices({ session: session({ signedIn: true, token: 'token', me: account() }), }) const view = render(BlogEntry, { props: { status: outer }, context: new Map([[APP_SERVICES, services]]), }) expect(view.getByText('Quoted words')).toBeInTheDocument() expect(view.getByRole('link', { name: 'Quote' })).toHaveAttribute( 'href', '#/compose?quote=outer-entry', ) expect(view.getByRole('link', { name: 'Quotes (3)' })).toHaveAttribute( 'href', '#/blog/outer-entry/quotes', ) }) it('shows a Mastodon quote authorization placeholder without leaking content', () => { const hidden = status({ id: 'hidden-quote', content: '

Do not display me

' }) const view = renderEntry({ quote: { state: 'blocked_account', quoted_status: hidden }, }) expect(view.getByText('The quoted account is blocked.')).toBeInTheDocument() expect(view.queryByText('Do not display me')).not.toBeInTheDocument() }) it('does not offer quoting when Mastodon says the viewer is denied', () => { const view = renderEntry({ quote_approval: { automatic: [], manual: [], current_user: 'denied', }, }) expect(view.queryByRole('link', { name: 'Quote' })).not.toBeInTheDocument() }) }) describe('BlogEntry emoji reactions', () => { it('collapses the reaction bar when the entry has no reactions', () => { const view = renderEntry({ pleroma: { emoji_reactions: [] } }) expect(view.queryByLabelText('Emoji reactions')).not.toBeInTheDocument() }) it('shows counts for Unicode and custom Pleroma/Akkoma reactions', () => { const view = renderEntry({ pleroma: { emoji_reactions: [ { name: 'πŸŽ‰', count: 2, me: false }, { name: 'party_blob@remote.example', count: 3, me: true, url: 'https://cdn.example/emoji/party_blob.png', }, ], }, }) expect(view.getByLabelText('Emoji reactions')).toBeInTheDocument() expect( view.getByRole('button', { name: 'Add πŸŽ‰ reaction, 2 reactions' }), ).toHaveTextContent('2') expect( view.getByRole('button', { name: 'Remove :party_blob@remote.example: reaction, 3 reactions', }), ).toHaveAttribute('aria-pressed', 'true') expect(view.getByAltText(':party_blob@remote.example:')).toHaveAttribute( 'src', 'https://cdn.example/emoji/party_blob.png', ) }) it('optimistically adds an existing reaction and applies the returned status', async () => { const original = status({ pleroma: { emoji_reactions: [{ name: 'πŸŽ‰', count: 2, me: false }] }, }) const confirmed = status({ pleroma: { emoji_reactions: [{ name: 'πŸŽ‰', count: 3, me: true }] }, }) const onupdate = vi.fn() const setEmojiReaction = vi.fn().mockResolvedValue(confirmed) const services = testServices({ session: session({ signedIn: true, me: account() }), endpoints: { setEmojiReaction }, }) const view = render(BlogEntry, { props: { status: original, onupdate }, context: new Map([[APP_SERVICES, services]]), }) await fireEvent.click( view.getByRole('button', { name: 'Add πŸŽ‰ reaction, 2 reactions' }), ) expect(setEmojiReaction).toHaveBeenCalledWith( services.session.api, original.id, 'πŸŽ‰', true, ) expect(onupdate.mock.calls[0][0].pleroma?.emoji_reactions).toEqual([ { name: 'πŸŽ‰', count: 3, me: true }, ]) await waitFor(() => expect(onupdate).toHaveBeenLastCalledWith(confirmed)) }) it('removes the viewer’s existing custom reaction', async () => { const customReaction = { name: 'party_blob@remote.example', count: 1, me: true, url: 'https://cdn.example/emoji/party_blob.png', } const original = status({ pleroma: { emoji_reactions: [customReaction] }, }) const confirmed = status({ pleroma: { emoji_reactions: [] } }) const onupdate = vi.fn() const setEmojiReaction = vi.fn().mockResolvedValue(confirmed) const services = testServices({ session: session({ signedIn: true, me: account() }), endpoints: { setEmojiReaction }, }) const view = render(BlogEntry, { props: { status: original, onupdate }, context: new Map([[APP_SERVICES, services]]), }) await fireEvent.click( view.getByRole('button', { name: 'Remove :party_blob@remote.example: reaction, 1 reaction', }), ) expect(setEmojiReaction).toHaveBeenCalledWith( services.session.api, original.id, customReaction.name, false, ) expect(onupdate.mock.calls[0][0].pleroma?.emoji_reactions).toEqual([]) await waitFor(() => expect(onupdate).toHaveBeenLastCalledWith(confirmed)) }) })