diff --git a/src/views/rules/__tests__/rules.test.tsx b/src/views/rules/__tests__/rules.test.tsx new file mode 100644 index 00000000..1a685bca --- /dev/null +++ b/src/views/rules/__tests__/rules.test.tsx @@ -0,0 +1,124 @@ +import * as React from 'react'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import Rules from '../rules'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; + +const testState = vi.hoisted(() => ({ + boardIdentifier: undefined as string | undefined, + communities: {} as Record, + directories: [ + { address: 'anime-posting.eth', title: '/a/ - Anime & Manga' }, + { address: 'random-posting.eth', title: '/b/ - Random' }, + ] as Array<{ address: string; title?: string }>, + navigateMock: vi.fn(), +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => testState.navigateMock, + useParams: () => ({ + boardIdentifier: testState.boardIdentifier, + }), + }; +}); + +vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({ + useCommunity: ({ communityAddress }: { communityAddress?: string }) => (communityAddress ? testState.communities[communityAddress] : undefined), +})); + +vi.mock('../../../hooks/use-directories', async () => { + const actual = await vi.importActual('../../../hooks/use-directories'); + return { + ...actual, + useDirectories: () => testState.directories, + }; +}); + +vi.mock('../../home', () => ({ + Footer: () => createElement('div', { 'data-testid': 'footer' }, 'footer'), + HomeLogo: () => createElement('div', { 'data-testid': 'home-logo' }, 'home-logo'), +})); + +vi.mock('../../../components/markdown', () => ({ + default: ({ content }: { content: string }) => createElement('div', { 'data-testid': 'markdown' }, content), +})); + +let container: HTMLDivElement; +let root: Root; + +const renderRules = async () => { + await act(async () => { + root.render(createElement(Rules)); + }); +}; + +describe('Rules', () => { + beforeEach(() => { + vi.clearAllMocks(); + testState.boardIdentifier = undefined; + testState.communities = {}; + testState.directories = [ + { address: 'anime-posting.eth', title: '/a/ - Anime & Manga' }, + { address: 'random-posting.eth', title: '/b/ - Random' }, + ]; + window.scrollTo = vi.fn(); + + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + it('keeps custom-address routes out of the default board select', async () => { + testState.boardIdentifier = 'custom-board.eth'; + testState.communities = { + 'custom-board.eth': { + rules: ['No custom options in the select.'], + shortAddress: 'custom-board.eth', + state: 'succeeded', + }, + }; + + await renderRules(); + + const select = container.querySelector('select'); + expect(select).toBeTruthy(); + expect(select?.value).toBe(''); + expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']); + expect(container.textContent).toContain('Rules for: custom-board.eth'); + }); + + it('keeps the canonical default board selected for known directories', async () => { + testState.boardIdentifier = 'a'; + testState.communities = { + 'anime-posting.eth': { + rules: ['Stay on topic.'], + state: 'succeeded', + }, + }; + + await renderRules(); + + const select = container.querySelector('select'); + expect(select).toBeTruthy(); + expect(select?.value).toBe('anime-posting.eth'); + expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual(['', 'anime-posting.eth', 'random-posting.eth']); + expect(container.textContent).toContain('Rules for: /a/ - Anime & Manga'); + }); +}); diff --git a/src/views/rules/rules.tsx b/src/views/rules/rules.tsx index e12cc4eb..9cded9fc 100644 --- a/src/views/rules/rules.tsx +++ b/src/views/rules/rules.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, FormEvent } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useCommunity } from '@bitsocialnet/bitsocial-react-hooks'; import { Footer, HomeLogo } from '../home'; -import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories'; +import { useDirectories, DirectoryCommunity, findDirectoryByAddress } from '../../hooks/use-directories'; import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils'; import Markdown from '../../components/markdown'; import styles from './rules.module.css'; @@ -103,6 +103,8 @@ const BoardSelector = ({ onSelect: (address: string) => void; }) => { const [customAddress, setCustomAddress] = useState(''); + const selectedDefaultBoard = findDirectoryByAddress(directories, selectedAddress); + const selectedBoardValue = selectedDefaultBoard?.address ?? ''; const handleSelectChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -129,7 +131,7 @@ const BoardSelector = ({
- {[...directories] .sort((a, b) => getBoardShortCode(a.title).localeCompare(getBoardShortCode(b.title))) @@ -142,7 +144,6 @@ const BoardSelector = ({ ); })} - {selectedAddress && !directories.some((sub) => sub.address === selectedAddress) && } or