Files
plspace/src/components/blog/Composer.test.ts
T
2026-07-30 19:45:40 +09:00

208 lines
7.0 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, session, status, testServices } from '$test/fixtures'
import Composer from './Composer.svelte'
describe('Composer', () => {
it('posts through an in-memory endpoint double', async () => {
const postStatus = vi.fn().mockResolvedValue(status())
const services = testServices({
session: session({ token: 'token', me: account(), signedIn: true }),
endpoints: { postStatus },
})
const view = render(Composer, {
props: { initialText: 'A backend-free component test' },
context: new Map([[APP_SERVICES, services]]),
})
await fireEvent.click(view.getByRole('button', { name: 'Post Entry' }))
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
expect(postStatus.mock.calls[0][1]).toMatchObject({
status: 'A backend-free component test',
visibility: 'public',
})
})
it('can submit a structured quote without commentary', async () => {
const quoted = status({
id: 'quoted-entry',
content: '<p>The original thought</p>',
})
const postStatus = vi.fn().mockResolvedValue(status({ id: 'new-quote' }))
const services = testServices({
session: session({ token: 'token', me: account(), signedIn: true }),
endpoints: { postStatus },
})
const view = render(Composer, {
props: { quote: quoted, submitLabel: 'Post Quote' },
context: new Map([[APP_SERVICES, services]]),
})
expect(view.getByText('The original thought')).toBeInTheDocument()
await fireEvent.click(view.getByRole('button', { name: 'Post Quote' }))
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
expect(postStatus.mock.calls[0][1]).toMatchObject({
status: '',
quoted_status_id: 'quoted-entry',
})
})
it('composes a multiple-choice poll without a backend', async () => {
const postStatus = vi.fn().mockResolvedValue(status())
const services = testServices({
session: session({
token: 'token',
me: account(),
signedIn: true,
instance: {
title: 'Pleroma',
version: '2.9.0',
configuration: {
polls: {
max_options: 4,
max_characters_per_option: 30,
min_expiration: 300,
max_expiration: 604_800,
},
},
},
}),
endpoints: { postStatus },
})
const view = render(Composer, {
props: { initialText: 'Which old web feature should return?' },
context: new Map([[APP_SERVICES, services]]),
})
await fireEvent.click(view.getByRole('button', { name: 'Poll' }))
await fireEvent.input(view.getByRole('textbox', { name: 'Poll option 1' }), {
target: { value: 'Guestbooks' },
})
await fireEvent.input(view.getByRole('textbox', { name: 'Poll option 2' }), {
target: { value: 'Webrings' },
})
await fireEvent.click(view.getByRole('button', { name: 'Add option' }))
await fireEvent.input(view.getByRole('textbox', { name: 'Poll option 3' }), {
target: { value: 'Blink tags' },
})
await fireEvent.click(view.getByRole('checkbox', { name: 'Allow multiple choices' }))
await fireEvent.change(view.getByLabelText('Keep open for'), {
target: { value: '3600' },
})
await fireEvent.click(view.getByRole('button', { name: 'Post Entry' }))
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
expect(postStatus.mock.calls[0][1]).toMatchObject({
status: 'Which old web feature should return?',
poll: {
options: ['Guestbooks', 'Webrings', 'Blink tags'],
expires_in: 3600,
multiple: true,
},
})
})
it.each([
['Command+Enter', { metaKey: true }],
['Ctrl+Enter', { ctrlKey: true }],
])('submits with %s from the focused compose field', async (_label, modifier) => {
const postStatus = vi.fn().mockResolvedValue(status())
const services = testServices({
session: session({ token: 'token', me: account(), signedIn: true }),
endpoints: { postStatus },
})
const view = render(Composer, {
props: { initialText: 'Keyboard-posted entry' },
context: new Map([[APP_SERVICES, services]]),
})
await fireEvent.keyDown(view.getByRole('textbox', { name: 'Entry text' }), {
key: 'Enter',
...modifier,
})
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
expect(postStatus.mock.calls[0][1].status).toBe('Keyboard-posted entry')
})
it('uploads a pasted clipboard image and attaches it to the post', async () => {
const pastedImage = new File(['image bytes'], 'pasted-image.png', { type: 'image/png' })
const uploadMedia = vi.fn().mockResolvedValue({
id: 'pasted-media',
type: 'image',
url: 'https://media.example/pasted-image.png',
preview_url: 'https://media.example/pasted-image-preview.png',
description: null,
})
const postStatus = vi.fn().mockResolvedValue(status())
const services = testServices({
session: session({
token: 'token',
me: account(),
signedIn: true,
instance: {
title: 'Test server',
version: '1.0.0',
configuration: { statuses: { max_media_attachments: 4 } },
},
}),
endpoints: { uploadMedia, postStatus },
})
const view = render(Composer, {
props: { initialText: 'A pasted picture' },
context: new Map([[APP_SERVICES, services]]),
})
const textarea = view.getByRole('textbox', { name: 'Entry text' })
await fireEvent.paste(textarea, {
clipboardData: {
items: [
{
kind: 'file',
type: 'image/png',
getAsFile: () => pastedImage,
},
],
files: [pastedImage],
},
})
await waitFor(() => expect(uploadMedia).toHaveBeenCalledOnce())
expect(uploadMedia.mock.calls[0][1]).toBe(pastedImage)
expect(view.container.querySelector('.composer-attachment img')).toHaveAttribute(
'src',
'https://media.example/pasted-image-preview.png',
)
await fireEvent.click(view.getByRole('button', { name: 'Post Entry' }))
await waitFor(() => expect(postStatus).toHaveBeenCalledOnce())
expect(postStatus.mock.calls[0][1].media_ids).toEqual(['pasted-media'])
})
it('leaves ordinary text-only paste alone', async () => {
const uploadMedia = vi.fn()
const services = testServices({
session: session({ token: 'token', me: account(), signedIn: true }),
endpoints: { uploadMedia },
})
const view = render(Composer, {
context: new Map([[APP_SERVICES, services]]),
})
const textarea = view.getByRole('textbox', { name: 'Entry text' })
const wasNotCancelled = await fireEvent.paste(textarea, {
clipboardData: {
items: [{ kind: 'string', type: 'text/plain', getAsFile: () => null }],
files: [],
},
})
expect(wasNotCancelled).toBe(true)
expect(uploadMedia).not.toHaveBeenCalled()
})
})