このコミットが含まれているのは:
2026-06-07 02:50:04 +09:00
コミット 546a212e74
5個のファイルの変更319行の追加2行の削除
+204
ファイルの表示
@@ -0,0 +1,204 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
import { buildPost,
buildTheatre,
buildTheatreComment,
buildTheatreInfo,
buildTheatrePostSelectionWeights,
buildTheatreProgramme,
buildUser } from '@/test/factories'
import { renderWithProviders } from '@/test/render'
import type { ReactNode } from 'react'
const api = vi.hoisted (() => ({
apiDelete: vi.fn (),
apiGet: vi.fn (),
apiPatch: vi.fn (),
apiPost: vi.fn (),
apiPut: vi.fn (),
isApiError: vi.fn (() => false),
}))
const postsApi = vi.hoisted (() => ({
fetchPost: vi.fn (),
}))
const dialogue = vi.hoisted (() => ({
confirm: vi.fn (),
}))
vi.mock ('@/lib/api', () => api)
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/components/dialogues/DialogueProvider', () => ({
useDialogue: () => dialogue,
}))
vi.mock ('@/components/PostEmbed', () => ({
default: ({ post }: {
post: { title: string | null; url: string }
}) => <div>Embed:{post.title || post.url}</div>,
}))
vi.mock ('@/components/PostEditForm', () => ({
default: () => <div>Post edit form</div>,
}))
vi.mock ('framer-motion', () => ({
motion: {
aside: ({ children }: { children?: ReactNode }) => <aside>{children}</aside>,
div: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
main: ({ children }: { children?: ReactNode }) => <main>{children}</main>,
},
}))
const currentPost = buildPost ({
id: 10,
title: '上映中の投稿',
url: 'https://www.nicovideo.jp/watch/sm10',
})
const theatre = buildTheatre ({ id: 7, name: '上映室' })
const programme = buildTheatreProgramme ({
theatreId: 7,
position: 3,
post: currentPost,
})
const weights = buildTheatrePostSelectionWeights ({
lightestPosts: [{
post: currentPost,
penalty: 2,
weight: 0.5,
tags: [],
}],
})
const renderPage = (user = buildUser ({ id: 1, role: 'member' })) =>
renderWithProviders (
<Routes>
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
</Routes>,
{ route: '/theatres/7' },
)
const mockDefaultApi = () => {
api.apiGet.mockImplementation ((path: string) => {
switch (path)
{
case '/theatres/7':
return Promise.resolve (theatre)
case '/theatres/7/comments':
return Promise.resolve ([
buildTheatreComment ({
theatreId: 7,
no: 2,
user: { id: 1, name: 'tester' },
content: '視聴コメント',
}),
])
case '/theatres/7/programmes':
return Promise.resolve ([programme])
case '/theatres/7/post_selection_weights':
return Promise.resolve (weights)
default:
return Promise.reject (new Error (`Unexpected GET ${ path }`))
}
})
api.apiPut.mockImplementation ((path: string) => {
switch (path)
{
case '/theatres/7/watching':
return Promise.resolve (buildTheatreInfo ({
postId: currentPost.id,
postStartedAt: '2026-01-02T03:04:05.000Z',
postElapsedMs: 1_000,
watchingUsers: [{ id: 1, name: 'tester' }],
skipVote: {
votesCount: 0,
requiredCount: 2,
watchingUsersCount: 1,
voted: false,
},
}))
case '/theatres/7/skip_vote':
return Promise.resolve (buildTheatreInfo ({
postId: currentPost.id,
postStartedAt: '2026-01-02T03:04:05.000Z',
postElapsedMs: 2_000,
watchingUsers: [{ id: 1, name: 'tester' }],
skipVote: {
votesCount: 1,
requiredCount: 2,
watchingUsersCount: 1,
voted: true,
},
}))
default:
return Promise.reject (new Error (`Unexpected PUT ${ path }`))
}
})
api.apiDelete.mockResolvedValue (undefined)
api.apiPatch.mockResolvedValue (undefined)
api.apiPost.mockResolvedValue (undefined)
postsApi.fetchPost.mockResolvedValue (currentPost)
dialogue.confirm.mockResolvedValue (true)
}
describe ('TheatreDetailPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
mockDefaultApi ()
})
it ('loads theatre state, comments, current post, programme history, and weights', async () => {
renderPage ()
expect (await screen.findByText ('上映会場『上映室』')).toBeInTheDocument ()
expect (await screen.findByText ('Embed:上映中の投稿')).toBeInTheDocument ()
expect (screen.getAllByText ('視聴コメント')[0]).toBeInTheDocument ()
expect (screen.getAllByText ('上映中の投稿')[0]).toBeInTheDocument ()
expect (screen.getByText ('penalty 2')).toBeInTheDocument ()
await waitFor (() => {
expect (postsApi.fetchPost).toHaveBeenCalledWith ('10')
})
})
it ('votes to skip the current post', async () => {
renderPage ()
await screen.findByText ('Embed:上映中の投稿')
fireEvent.click (screen.getByRole ('button', { name: 'スキップ 0 / 2' }))
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith (
'/theatres/7/skip_vote',
{ post_id: 10 },
)
})
expect (await screen.findByRole ('button', { name: 'スキップ取消 1 / 2' }))
.toBeInTheDocument ()
})
it ('deletes an owned comment after confirmation', async () => {
renderPage ()
fireEvent.click ((await screen.findAllByLabelText ('コメントを削除'))[0])
await waitFor (() => {
expect (dialogue.confirm).toHaveBeenCalled ()
})
await waitFor (() => {
expect (api.apiDelete).toHaveBeenCalledWith ('/theatres/7/comments/2')
})
expect (await screen.findAllByText ('削除されました.')).toHaveLength (2)
})
})
+69 -1
ファイルの表示
@@ -1,4 +1,13 @@
import type { Material, Post, Tag, User, WikiPage } from '@/types'
import type { Material,
Post,
Tag,
Theatre,
TheatreComment,
TheatreInfo,
TheatrePostSelectionWeights,
TheatreProgramme,
User,
WikiPage } from '@/types'
export const buildTag = (overrides: Partial<Tag> = {}): Tag => ({
id: 1,
@@ -72,3 +81,62 @@ export const buildMaterial = (overrides: Partial<Material> = {}): Material => ({
updatedByUser: { id: 2, name: 'updater' },
...overrides,
})
export const buildTheatre = (overrides: Partial<Theatre> = {}): Theatre => ({
id: 1,
name: 'テスト劇場',
opensAt: '2026-01-02T03:04:05.000Z',
closesAt: null,
createdByUser: { id: 1, name: 'creator' },
createdAt: '2026-01-02T03:04:05.000Z',
updatedAt: '2026-01-03T03:04:05.000Z',
...overrides,
})
export const buildTheatreInfo = (
overrides: Partial<TheatreInfo> = {},
): TheatreInfo => ({
hostFlg: false,
postId: null,
postStartedAt: null,
postElapsedMs: null,
watchingUsers: [],
skipVote: {
votesCount: 0,
requiredCount: 1,
watchingUsersCount: 0,
voted: false,
},
...overrides,
})
export const buildTheatreComment = (
overrides: Partial<TheatreComment> = {},
): TheatreComment => ({
theatreId: 1,
no: 1,
deleted: false,
user: { id: 1, name: 'tester' },
content: 'テストコメント',
createdAt: '2026-01-02T03:04:05.000Z',
...overrides,
} as TheatreComment)
export const buildTheatreProgramme = (
overrides: Partial<TheatreProgramme> = {},
): TheatreProgramme => ({
theatreId: 1,
position: 1,
post: buildPost (),
createdAt: '2026-01-02T03:04:05.000Z',
...overrides,
})
export const buildTheatrePostSelectionWeights = (
overrides: Partial<TheatrePostSelectionWeights> = {},
): TheatrePostSelectionWeights => ({
tagPenalties: [],
lightestPosts: [],
heaviestPosts: [],
...overrides,
})