fdf652951a
#353 を再開できなかったので. Reviewed-on: #380 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
354 行
10 KiB
TypeScript
354 行
10 KiB
TypeScript
import { act, fireEvent, screen, waitFor, within } 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,
|
|
buildTag,
|
|
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 (),
|
|
}))
|
|
|
|
const postEmbed = vi.hoisted (() => ({
|
|
props: vi.fn (),
|
|
play: vi.fn (),
|
|
seek: vi.fn (),
|
|
}))
|
|
|
|
vi.mock ('@/lib/api', () => api)
|
|
vi.mock ('@/lib/posts', () => postsApi)
|
|
vi.mock ('@/components/dialogues/DialogueProvider', () => ({
|
|
useDialogue: () => dialogue,
|
|
}))
|
|
vi.mock ('@/components/PostEmbed', () => ({
|
|
default: (props: {
|
|
ref?: { current: unknown }
|
|
post: { title: string | null; url: string }
|
|
}) => {
|
|
postEmbed.props (props)
|
|
if (props.ref)
|
|
props.ref.current = {
|
|
play: postEmbed.play,
|
|
seek: postEmbed.seek,
|
|
}
|
|
|
|
return <div>Embed:{props.post.title || props.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 watchingUsers = [
|
|
{ id: 1, name: 'tester' },
|
|
{ id: 2, name: 'another' },
|
|
]
|
|
|
|
const renderPage = (user = buildUser ({ id: 1, role: 'member' })) =>
|
|
renderWithProviders (
|
|
<Routes>
|
|
<Route path="/theatres/:id" element={<TheatreDetailPage user={user}/>}/>
|
|
</Routes>,
|
|
{ route: '/theatres/7' },
|
|
)
|
|
|
|
const tagSection = (): HTMLElement =>
|
|
screen.getAllByRole ('heading', { name: 'タグ' })[0].closest ('section')!
|
|
|
|
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,
|
|
skipVote: {
|
|
votesCount: 0,
|
|
requiredCount: 2,
|
|
watchingUsersCount: watchingUsers.length,
|
|
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,
|
|
skipVote: {
|
|
votesCount: 1,
|
|
requiredCount: 2,
|
|
watchingUsersCount: watchingUsers.length,
|
|
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.useRealTimers ()
|
|
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 ('does not seek to zero while applying video length from the player', async () => {
|
|
api.apiPut.mockImplementation ((path: string) => {
|
|
switch (path)
|
|
{
|
|
case '/theatres/7/watching':
|
|
return Promise.resolve (buildTheatreInfo ({
|
|
hostFlg: true,
|
|
postId: currentPost.id,
|
|
postStartedAt: '2026-01-02T03:04:05.000Z',
|
|
postElapsedMs: 7_000,
|
|
watchingUsers: [{ id: 1, name: 'tester' }],
|
|
skipVote: {
|
|
votesCount: 0,
|
|
requiredCount: 2,
|
|
watchingUsersCount: 1,
|
|
voted: false,
|
|
},
|
|
}))
|
|
|
|
default:
|
|
return Promise.reject (new Error (`Unexpected PUT ${ path }`))
|
|
}
|
|
})
|
|
|
|
renderPage ()
|
|
await screen.findByText ('Embed:上映中の投稿')
|
|
|
|
const props = postEmbed.props.mock.calls.at (-1)![0]
|
|
|
|
act (() => {
|
|
props.onVideoReady (120_000)
|
|
props.onPlaybackChange (0)
|
|
})
|
|
|
|
const seekMs = postEmbed.seek.mock.calls[0][0]
|
|
expect (seekMs).toBeGreaterThanOrEqual (7_000)
|
|
expect (seekMs).toBeLessThan (10_000)
|
|
expect (postEmbed.seek).not.toHaveBeenCalledWith (0)
|
|
})
|
|
|
|
it ('shows child tags from the post tag tree in both vertical and horizontal layouts', async () => {
|
|
const childTag = buildTag ({ id: 12, name: '子タグ', category: 'general' })
|
|
const parentTag = buildTag ({
|
|
id: 11,
|
|
name: '親タグ',
|
|
category: 'general',
|
|
children: [childTag],
|
|
})
|
|
const duplicateParentTag = buildTag ({
|
|
id: 13,
|
|
name: '別親タグ',
|
|
category: 'general',
|
|
children: [childTag],
|
|
})
|
|
|
|
postsApi.fetchPost.mockResolvedValueOnce (buildPost ({
|
|
...currentPost,
|
|
tags: [parentTag, duplicateParentTag],
|
|
}))
|
|
|
|
renderPage ()
|
|
|
|
await screen.findByText ('Embed:上映中の投稿')
|
|
|
|
expect (within (tagSection ()).getByRole ('link', { name: '親タグ' }))
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getByRole ('link', { name: '別親タグ' }))
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getAllByRole ('link', { name: '子タグ' })[0])
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getAllByText ('↳').length).toBeGreaterThan (0)
|
|
|
|
fireEvent.click (screen.getByRole ('button', { name: '2 列 A 型' }))
|
|
fireEvent.click (screen.getAllByRole ('button', { name: '横並び' })[0])
|
|
|
|
await waitFor (() => {
|
|
expect (within (tagSection ()).getByRole ('link', { name: '親タグ' }))
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getByRole ('link', { name: '別親タグ' }))
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getByRole ('link', { name: '子タグ' }))
|
|
.toBeInTheDocument ()
|
|
expect (within (tagSection ()).getAllByRole ('link', { name: '子タグ' }))
|
|
.toHaveLength (1)
|
|
})
|
|
})
|
|
|
|
it ('does not advance host post while video length is unknown', async () => {
|
|
api.apiPut.mockImplementation ((path: string) => {
|
|
switch (path)
|
|
{
|
|
case '/theatres/7/watching':
|
|
return Promise.resolve (buildTheatreInfo ({
|
|
hostFlg: true,
|
|
postId: currentPost.id,
|
|
postStartedAt: '2026-01-02T03:04:05.000Z',
|
|
postElapsedMs: 4_000,
|
|
watchingUsers: [{ id: 1, name: 'tester' }],
|
|
skipVote: {
|
|
votesCount: 0,
|
|
requiredCount: 2,
|
|
watchingUsersCount: 1,
|
|
voted: false,
|
|
},
|
|
}))
|
|
|
|
default:
|
|
return Promise.reject (new Error (`Unexpected PUT ${ path }`))
|
|
}
|
|
})
|
|
|
|
renderPage ()
|
|
|
|
await screen.findByText ('Embed:上映中の投稿')
|
|
await waitFor (() => {
|
|
expect (api.apiPut).toHaveBeenCalledWith ('/theatres/7/watching')
|
|
})
|
|
|
|
await waitFor (() => {
|
|
expect (api.apiPut).toHaveBeenCalledTimes (2)
|
|
}, { timeout: 2_500 })
|
|
expect (api.apiPatch).not.toHaveBeenCalledWith ('/theatres/7/next_post')
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|