このコミットが含まれているのは:
2026-07-18 21:59:22 +09:00
コミット 2f87669699
38個のファイルの変更869行の追加1307行の削除
+5 -3
ファイルの表示
@@ -78,16 +78,18 @@ describe ('PostEditForm', () => {
render (<PostEditForm post={post} onSave={vi.fn ()}/>)
expect (screen.getByPlaceholderText ('例: 2 / 2.5 / 1:23')).toHaveValue ('180.5')
expect (screen.getByText ('動画時間').parentElement?.querySelector ('input'))
.toHaveValue ('180.5')
const tags = screen.getAllByRole ('textbox')[2]
fireEvent.change (tags, { target: { value: 'general-tag' } })
expect (screen.queryByPlaceholderText ('例: 2 / 2.5 / 1:23')).not.toBeInTheDocument ()
expect (screen.queryByText ('動画時間')).not.toBeInTheDocument ()
fireEvent.change (tags, {
target: { value: '動画 general-tag' },
})
expect (screen.getByPlaceholderText ('例: 2 / 2.5 / 1:23')).toHaveValue ('180.5')
expect (screen.getByText ('動画時間').parentElement?.querySelector ('input'))
.toHaveValue ('180.5')
})
it ('shows deduplicated original-created endpoint errors on the shared datetime field', async () => {
+1 -1
ファイルの表示
@@ -73,7 +73,7 @@ describe ('PostOriginalCreatedTimeField', () => {
setOriginalCreatedBefore={vi.fn ()}/>,
)
const input = screen.getDisplayValue ('2024-01-01T12:34')
const input = screen.getByDisplayValue ('2024-01-01T12:34')
fireEvent.change (input, { target: { value: '2024-01-01T12:35' } })
expect (setFrom).toHaveBeenCalledWith ('2024-01-01T03:35Z')
+2 -4
ファイルの表示
@@ -17,21 +17,19 @@ describe ('menuOutline', () => {
for (const role of ['member', 'admin'] as const)
{
expect (submenuItem (role, '広場', '追加')?.visible).toBe (true)
expect (submenuItem (role, '広場', '取込')?.visible).toBe (true)
expect (submenuItem (role, '素材', '追加')?.visible).toBe (true)
expect (submenuItem (role, 'Wiki', '新規')?.visible).toBe (true)
expect (submenuItem (role, 'Wiki', '編輯')?.visible).toBe (true)
}
expect (submenuItem ('guest', '広場', '追加')?.visible).toBe (false)
expect (submenuItem ('guest', '広場', '取込')?.visible).toBe (false)
expect (submenuItem ('guest', '素材', '追加')?.visible).toBe (false)
expect (submenuItem ('guest', 'Wiki', '新規')?.visible).toBe (false)
expect (submenuItem ('guest', 'Wiki', '編輯')?.visible).toBe (false)
})
it ('uses /posts/new as the import entrypoint', () => {
expect (submenuItem ('member', '広場', '取込')?.to).toBe ('/posts/new')
it ('uses /posts/new as the post creation entrypoint', () => {
expect (submenuItem ('member', '広場', '追加')?.to).toBe ('/posts/new')
})
it ('keeps material suppression admin-only', () => {
+3 -1
ファイルの表示
@@ -80,7 +80,9 @@ describe ('DialogueProvider', () => {
expect (dialogue).toHaveClass ('max-h-[calc(100dvh-1rem)]', 'flex-col', 'max-w-3xl')
await waitFor (() => expect (screen.getByRole ('button', { name: '左操作' }))
.toBeInTheDocument ())
expect (screen.getByRole ('button', { name: '左操作' })).toHaveClass ('w-full', 'sm:w-auto')
expect (screen.getByRole ('button', { name: '左操作' })).toHaveClass (
'w-full',
'md:w-auto')
fireEvent.click (screen.getByRole ('button', { name: '左操作' }))
await waitFor (() => expect (action).toHaveBeenCalledTimes (1))
-10
ファイルの表示
@@ -2,8 +2,6 @@ import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { buildPostImportRow } from '@/test/postImportFactories'
import { buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
@@ -14,14 +12,6 @@ vi.mock ('@/components/posts/PostCreationDataFields', () => ({
}))
describe ('PostCreationDataFields usage', () => {
it ('is used by PostNewPage', async () => {
const { default: PostNewPage } = await import ('@/pages/posts/PostNewPage')
renderWithProviders (<PostNewPage user={buildUser ({ role: 'member' })}/>)
expect (screen.getByTestId ('shared-fields')).toBeInTheDocument ()
})
it ('is used by PostImportRowForm', async () => {
const { default: PostImportRowForm } = await import (
'@/components/posts/import/PostImportRowForm')
+4 -2
ファイルの表示
@@ -135,7 +135,7 @@ describe ('PostImportRowForm', () => {
})
it (
'shows the shared creation field order without duration and without file upload UI',
'shows upload input only when the thumbnail URL is blank',
async () => {
let actions: DialogueFormAction[] = []
const controls: DialogueFormControls = {
@@ -164,7 +164,9 @@ describe ('PostImportRowForm', () => {
'オリジナルの作成日時',
'タグ',
'親投稿'])
expect (container.querySelector ('input[type="file"]')).toBeNull ()
expect (container.querySelector ('input[type="file"]')).toHaveAttribute (
'accept',
'image/*')
expect (screen.queryByPlaceholderText ('例: 2 / 2.5 / 1:23')).not.toBeInTheDocument ()
expect (screen.getByDisplayValue ('tag1')).toBeInTheDocument ()
+32 -57
ファイルの表示
@@ -1,14 +1,8 @@
import { render, screen, waitFor } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
describe ('PostImportThumbnailPreview', () => {
beforeEach (() => {
vi.clearAllMocks ()
@@ -16,69 +10,50 @@ describe ('PostImportThumbnailPreview', () => {
globalThis.URL.revokeObjectURL = vi.fn ()
})
it ('uses a backend-fetched blob URL instead of the external thumbnail URL directly', async () => {
api.apiGet.mockResolvedValueOnce (new Blob (['img'], { type: 'image/png' }))
it ('renders the remote URL directly without a backend proxy', () => {
render (
<PostImportThumbnailPreview
url="https://example.com/thumbnail.jpg"
className="h-10 w-10"/>)
<PostImportThumbnailPreview
url="https://example.com/thumbnail.jpg"
className="h-10 w-10"/>)
await waitFor (() => {
expect (screen.getByRole ('img')).toHaveAttribute ('src', 'blob:preview')
})
expect (screen.getByRole ('img')).not.toHaveAttribute (
'src',
'https://example.com/thumbnail.jpg')
expect (api.apiGet).toHaveBeenCalledWith ('/preview/thumbnail', {
params: { url: 'https://example.com/thumbnail.jpg' },
responseType: 'blob' })
expect (screen.getByRole ('img')).toHaveAttribute (
'src',
'https://example.com/thumbnail.jpg')
expect (screen.getByRole ('img')).toHaveAttribute (
'referrerpolicy',
'no-referrer')
})
it ('does not render the unsafe URL directly when preview fetching fails', async () => {
api.apiGet.mockRejectedValueOnce (new Error ('unsafe'))
it ('shows the empty frame after the remote image fails', () => {
const { container } = render (
<PostImportThumbnailPreview
url="http://127.0.0.1/private.png"
className="h-10 w-10"/>)
<PostImportThumbnailPreview
url="https://example.com/missing.jpg"
className="h-10 w-10"/>)
await waitFor (() => {
expect (screen.queryByRole ('img')).toBeNull ()
})
expect (screen.queryByText ('サムネールを表示できません')).toBeNull ()
expect (screen.queryByText ('なし')).toBeNull ()
fireEvent.error (screen.getByRole ('img'))
expect (screen.queryByRole ('img')).toBeNull ()
expect (container.querySelector ('div.rounded.border.bg-muted')).not.toBeNull ()
expect (container.textContent).toBe ('')
expect (screen.queryByRole ('img')).toBeNull ()
})
it ('revokes the old object URL when the source URL changes', async () => {
const createObjectUrlMock =
globalThis.URL.createObjectURL as unknown as ReturnType<typeof vi.fn>
createObjectUrlMock
.mockReturnValueOnce ('blob:first')
.mockReturnValueOnce ('blob:second')
api.apiGet
.mockResolvedValueOnce (new Blob (['first'], { type: 'image/png' }))
.mockResolvedValueOnce (new Blob (['second'], { type: 'image/png' }))
it ('uses and revokes an object URL only when the remote URL is blank', () => {
const file = new File (['image'], 'thumbnail.png', { type: 'image/png' })
const { rerender, unmount } = render (
<PostImportThumbnailPreview url="" file={file} className="h-10 w-10"/>)
const { rerender } = render (
<PostImportThumbnailPreview
url="https://example.com/first.jpg"
className="h-10 w-10"/>)
await waitFor (() => {
expect (screen.getByRole ('img')).toHaveAttribute ('src', 'blob:first')
})
expect (screen.getByRole ('img')).toHaveAttribute ('src', 'blob:preview')
rerender (
<PostImportThumbnailPreview
url="https://example.com/second.jpg"
className="h-10 w-10"/>)
<PostImportThumbnailPreview
url="https://example.com/remote.jpg"
file={file}
className="h-10 w-10"/>)
expect (screen.getByRole ('img')).toHaveAttribute (
'src',
'https://example.com/remote.jpg')
await waitFor (() => {
expect (screen.getByRole ('img')).toHaveAttribute ('src', 'blob:second')
})
expect (globalThis.URL.revokeObjectURL).toHaveBeenCalledWith ('blob:first')
unmount ()
expect (globalThis.URL.revokeObjectURL).toHaveBeenCalledWith ('blob:preview')
})
})
+4 -4
ファイルの表示
@@ -16,14 +16,14 @@ describe ('displayPostImportStatus', () => {
skipReason: 'manual' }))).toBe ('skipped')
})
it ('does not expose validation, failure, or created states as badges', () => {
it ('distinguishes validation, failure, and created states', () => {
expect (displayPostImportStatus (buildPostImportRow ({
status: 'error',
validationErrors: { title: ['invalid'] } }))).toBeNull ()
validationErrors: { title: ['invalid'] } }))).toBe ('error')
expect (displayPostImportStatus (buildPostImportRow ({
importStatus: 'failed' }))).toBeNull ()
importStatus: 'failed' }))).toBe ('failed')
expect (displayPostImportStatus (buildPostImportRow ({
importStatus: 'created',
createdPostId: 3 }))).toBeNull ()
createdPostId: 3 }))).toBe ('created')
})
})