import { act, render, waitFor } from '@testing-library/svelte' import { describe, expect, it, vi } from 'vitest' import { APP_SERVICES } from '$lib/app-services' import type { Account } from '$lib/api/types' import { TIMELINE_REFRESH, TimelineRefreshController } from '$lib/timeline-refresh' import { account, deferred, session, status, testServices, theme } from '$test/fixtures' import Profile from './Profile.svelte' describe('Profile', () => { it('renders account custom emoji in the public profile heading', async () => { const profileAccount = account({ display_name: ':smugkura: Kura :disconnecting: :kura_explode:', emojis: [ { shortcode: 'smugkura', url: 'https://cdn.example/smugkura.png', static_url: 'https://cdn.example/smugkura.png', visible_in_picker: true, }, { shortcode: 'disconnecting', url: 'https://cdn.example/disconnecting.png', static_url: 'https://cdn.example/disconnecting.png', visible_in_picker: true, }, { shortcode: 'kura_explode', url: 'https://cdn.example/kura_explode.png', static_url: 'https://cdn.example/kura_explode.png', visible_in_picker: true, }, ], }) const services = testServices({ session: session(), theme: theme(), endpoints: { lookupAccount: vi.fn().mockResolvedValue(profileAccount), fetchAccountStatuses: vi.fn().mockResolvedValue({ items: [], links: {} }), }, }) const view = render(Profile, { props: { acct: 'alice' }, context: new Map([[APP_SERVICES, services]]), }) const headingEmoji = (await view.findAllByAltText(':smugkura:')).find((node) => node.closest('h1.profile-name'), ) expect(headingEmoji).toBeDefined() expect(view.getAllByAltText(':disconnecting:').length).toBeGreaterThan(0) expect(view.getAllByAltText(':kura_explode:').length).toBeGreaterThan(0) }) it('does not apply profile CSS after it has unmounted', async () => { const lookup = deferred() const applyProfileCss = vi.fn() const services = testServices({ session: session(), theme: theme({ applyProfileCss }), endpoints: { lookupAccount: vi.fn(() => lookup.promise) }, }) const view = render(Profile, { props: { acct: 'alice' }, context: new Map([[APP_SERVICES, services]]), }) view.unmount() await act(() => lookup.resolve(account({ fields: [{ name: 'css', value: '.x { color: red }' }] }))) expect(applyProfileCss).not.toHaveBeenCalled() }) it('shows only original top-level notes in Latest Blog Entries', async () => { const original = status({ id: 'original', content: '

Original note

', }) const boostedNote = status({ id: 'boost-wrapper', content: '', reblog: status({ id: 'boosted', content: '

Someone else’s note

' }), }) const reply = status({ id: 'reply', content: '

A reply

', in_reply_to_id: 'parent', }) const fetchAccountStatuses = vi.fn().mockResolvedValue({ items: [original, boostedNote, reply], links: {}, }) const services = testServices({ session: session(), theme: theme(), endpoints: { lookupAccount: vi.fn().mockResolvedValue(account()), fetchAccountStatuses, }, }) const view = render(Profile, { props: { acct: 'alice', view: 'profile' }, context: new Map([[APP_SERVICES, services]]), }) await waitFor(() => expect(fetchAccountStatuses).toHaveBeenCalledOnce()) expect(fetchAccountStatuses.mock.calls[0][3]).toEqual({ exclude_replies: true, exclude_reblogs: true, }) expect(await view.findByText('Original note')).toBeInTheDocument() expect(view.queryByText('Someone else’s note')).not.toBeInTheDocument() expect(view.queryByText('A reply')).not.toBeInTheDocument() }) it('builds Pics from the account’s own image attachments', async () => { const ownPicture = status({ id: 'picture-entry', content: '

A day at the beach

', media_attachments: [ { id: 'picture', type: 'image', url: 'https://media.example/beach.jpg', preview_url: 'https://media.example/beach-small.jpg', description: 'Sunset over the beach', }, ], }) const video = status({ id: 'video-entry', content: '

A video that does not belong in Pics

', media_attachments: [ { id: 'video', type: 'video', url: 'https://media.example/movie.mp4', preview_url: 'https://media.example/movie.jpg', description: 'A movie', }, ], }) const repostedPicture = status({ id: 'repost', reblog: status({ id: 'someone-elses-picture', content: '

Someone else’s picture

', media_attachments: [ { id: 'reposted-picture', type: 'image', url: 'https://media.example/reposted.jpg', preview_url: null, description: 'A reposted picture', }, ], }), }) const fetchAccountStatuses = vi.fn().mockResolvedValue({ items: [ownPicture, video, repostedPicture], links: {}, }) const services = testServices({ session: session(), theme: theme(), endpoints: { lookupAccount: vi.fn().mockResolvedValue(account()), fetchAccountStatuses, }, }) const view = render(Profile, { props: { acct: 'alice', view: 'pics' }, context: new Map([[APP_SERVICES, services]]), }) expect(await view.findByRole('img', { name: 'Sunset over the beach' })).toHaveAttribute( 'src', 'https://media.example/beach-small.jpg', ) expect(fetchAccountStatuses.mock.calls[0][3]).toEqual({ only_media: true, exclude_reblogs: true, }) expect(view.getByText('Sunset over the beach')).toBeInTheDocument() expect(view.queryByText('A movie')).not.toBeInTheDocument() expect(view.queryByText('A reposted picture')).not.toBeInTheDocument() }) it('refreshes only the entry feed without reloading the profile', async () => { const lookupAccount = vi.fn().mockResolvedValue(account()) const fetchAccountStatuses = vi .fn() .mockResolvedValueOnce({ items: [ status({ id: 'timeline-note', content: '

Timeline note

', favourites_count: 0, }), ], links: {}, }) .mockResolvedValueOnce({ items: [ status({ id: 'timeline-note', content: '

Timeline note

', favourites_count: 5, }), ], links: {}, }) const services = testServices({ session: session(), theme: theme(), endpoints: { lookupAccount, fetchAccountStatuses }, }) const refresh = new TimelineRefreshController() const context = new Map() context.set(APP_SERVICES, services) context.set(TIMELINE_REFRESH, refresh) const view = render(Profile, { props: { acct: 'alice', view: 'blog' }, context, }) expect(await view.findByText('Timeline note')).toBeInTheDocument() expect(refresh.refresh()).toBe(true) await waitFor(() => expect(fetchAccountStatuses).toHaveBeenCalledTimes(2)) expect(lookupAccount).toHaveBeenCalledOnce() await waitFor(() => expect(view.getByRole('button', { name: 'Kudos (5)' })).toBeInTheDocument(), ) }) })