diff --git a/frontend/src/pages/theatres/TheatreDetailPage.test.tsx b/frontend/src/pages/theatres/TheatreDetailPage.test.tsx index 7adab24..ccb5407 100644 --- a/frontend/src/pages/theatres/TheatreDetailPage.test.tsx +++ b/frontend/src/pages/theatres/TheatreDetailPage.test.tsx @@ -1,9 +1,10 @@ -import { act, fireEvent, screen, waitFor } from '@testing-library/react' +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, @@ -96,6 +97,9 @@ const renderPage = (user = buildUser ({ id: 1, role: 'member' })) => { route: '/theatres/7' }, ) +const tagSection = (): HTMLElement => + screen.getAllByRole ('heading', { name: 'タグ' })[0].closest ('section')! + const mockDefaultApi = () => { api.apiGet.mockImplementation ((path: string) => { switch (path) @@ -245,6 +249,53 @@ describe ('TheatreDetailPage', () => { 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) diff --git a/frontend/src/pages/theatres/TheatreDetailPage.tsx b/frontend/src/pages/theatres/TheatreDetailPage.tsx index f3f27aa..e4db3d2 100644 --- a/frontend/src/pages/theatres/TheatreDetailPage.tsx +++ b/frontend/src/pages/theatres/TheatreDetailPage.tsx @@ -102,6 +102,28 @@ const compareTagName = (a: Tag, b: Tag): number => a.name === b.name ? 0 : (a.name < b.name ? -1 : 1) +const flattenTags = (tags: Tag[]): Tag[] => { + const flattened: Tag[] = [] + const seen = new Set () + + const visit = (tag: Tag) => { + if (seen.has (tag.id)) + return + + seen.add (tag.id) + flattened.push (tag) + + for (const child of tag.children ?? []) + visit (child) + } + + for (const tag of tags) + visit (tag) + + return flattened +} + + const tagsByCategory = (tags: Tag[]): Partial> => { const grouped: Partial> = { } @@ -118,15 +140,39 @@ const tagsByCategory = (tags: Tag[]): Partial> => { } +const renderReadonlyTagTree = ( + tag: Tag, + nestLevel: number, + path: string, + ancestors: Set = new Set (), +): ReactNode[] => { + const key = `${ path }-${ tag.id }` + const nextAncestors = new Set (ancestors) + nextAncestors.add (tag.id) + + return [ + ( +
  • + +
  • ), + ...((tag.children ?? []) + .filter (child => !(nextAncestors.has (child.id))) + .sort (compareTagName) + .flatMap (child => + renderReadonlyTagTree (child, nestLevel + 1, key, nextAncestors)))] +} + + const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = ( { tags, compact, flow = 'vertical' }) => { - const grouped = tagsByCategory (tags) + const horizontalGrouped = tagsByCategory (flattenTags (tags)) + const verticalGrouped = tagsByCategory (tags) if (flow === 'horizontal') { return (
      - {CATEGORIES.flatMap (cat => grouped[cat] ?? []).map (tag => ( + {CATEGORIES.flatMap (cat => horizontalGrouped[cat] ?? []).map (tag => (
    • ))} @@ -136,7 +182,7 @@ const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = ( return (
      {CATEGORIES.map (cat => { - const rows = grouped[cat] ?? [] + const rows = verticalGrouped[cat] ?? [] if (rows.length === 0) return null @@ -146,10 +192,8 @@ const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = ( {CATEGORY_NAMES[cat]}
        - {rows.map (tag => ( -
      • - -
      • ))} + {rows.flatMap (tag => + renderReadonlyTagTree (tag, 0, `cat-${ cat }`))}
      ) })}