Files
plspace/src/components/blog/BlogEntry.test.ts
T
2026-07-30 20:22:52 +09:00

268 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof status>[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:
'<p>Watch this: <a href="https://www.youtube.com/watch?v=pyNCigSulNs">https://www.youtube.com/watch?v=pyNCigSulNs</a></p>',
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: '<p><a href="https://youtu.be/pyNCigSulNs">video</a></p>',
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:
'<p>It <span class="mfm-spin" data-mfm-speed="2s" data-mfm-left>works</span></p>',
})
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: '<p>Signed through <em>HTML</em><br>&mdash; Helene</p>',
}),
},
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: '<p>Quoted words</p>',
account: account({ id: 'quoted-author', display_name: 'Quoted Author' }),
})
const outer = status({
id: 'outer-entry',
content: '<p>My commentary</p>',
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: '<p>Do not display me</p>' })
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 viewers 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))
})
})