mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
268 lines
8.9 KiB
TypeScript
268 lines
8.9 KiB
TypeScript
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>— 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 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))
|
||
})
|
||
})
|