mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
import * as React from 'react';
|
|
import { createElement } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import Directory from '../directory';
|
|
|
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
|
|
|
const testState = vi.hoisted(() => ({
|
|
boardIdentifier: 'a' as string | undefined,
|
|
communities: {} as Record<string, { address: string; name?: string; state?: string; updatedAt?: number }>,
|
|
communityIdentifierRequests: [] as Array<string | undefined>,
|
|
directoryListLoading: false,
|
|
directoryBoards: [
|
|
{
|
|
address: 'anime-and-manga.bso',
|
|
score: 12,
|
|
},
|
|
],
|
|
directories: [
|
|
{
|
|
address: 'anime-and-manga.bso',
|
|
directoryCode: 'a',
|
|
title: '/a/ - Anime & Manga',
|
|
},
|
|
],
|
|
offlineHookRequests: [] as Array<{ address?: string; communityAddressHint?: string }>,
|
|
offlineHookValue: {
|
|
isOffline: false,
|
|
isOnlineStatusLoading: false,
|
|
offlineIconClass: '',
|
|
offlineTitle: false as string | false,
|
|
},
|
|
offlineStates: {} as Record<string, { state?: string; updatedAt?: number }>,
|
|
nowSeconds: 1_704_067_210,
|
|
}));
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
Trans: ({ i18nKey }: { i18nKey: string }) => createElement(React.Fragment, null, i18nKey),
|
|
useTranslation: () => ({
|
|
t: (key: string, values?: Record<string, unknown>) => {
|
|
if (key === 'directory_status_online') return 'online';
|
|
if (key === 'directory_status_offline') return 'offline';
|
|
if (key === 'directory_heading') return `${values?.boardIdentifier} directory`;
|
|
if (key === 'view') return 'View';
|
|
return key;
|
|
},
|
|
}),
|
|
}));
|
|
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
|
return {
|
|
...actual,
|
|
useParams: () => ({
|
|
boardIdentifier: testState.boardIdentifier,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
|
useCommunity: (options?: { community?: { name?: string; publicKey?: string } }) => {
|
|
const communityAddress = options?.community?.name ?? options?.community?.publicKey;
|
|
return communityAddress ? testState.communities[communityAddress] : undefined;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../components/board-buttons/board-buttons', () => ({
|
|
BracketedCatalogButton: () => createElement('span', null, '[', createElement('a', null, 'catalog'), ']'),
|
|
BottomButton: () => createElement('button', { type: 'button' }, 'bottom'),
|
|
CatalogButton: () => createElement('a', null, 'catalog'),
|
|
ReturnButton: () => createElement('a', null, 'return'),
|
|
TopButton: () => createElement('button', { type: 'button' }, 'top'),
|
|
}));
|
|
|
|
vi.mock('../../../components/footer', () => ({
|
|
PageFooterDesktop: ({ firstRow, styleRow }: { firstRow: React.ReactNode; styleRow: React.ReactNode }) =>
|
|
createElement('footer', { 'data-testid': 'desktop-footer' }, firstRow, styleRow),
|
|
PageFooterMobile: ({ children }: { children: React.ReactNode }) => createElement('footer', { 'data-testid': 'mobile-footer' }, children),
|
|
ThreadFooterStyleRow: () => createElement('div', null, 'style'),
|
|
}));
|
|
|
|
vi.mock('../../../components/loading-ellipsis', () => ({
|
|
default: ({ string }: { string: string }) => createElement('span', null, string),
|
|
}));
|
|
|
|
vi.mock('../../../components/tooltip', () => ({
|
|
default: ({ content, children }: { content: React.ReactNode; children: React.ReactNode }) =>
|
|
createElement('span', { title: typeof content === 'string' ? content : undefined }, children),
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-directories', () => ({
|
|
useDirectories: () => testState.directories,
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-directory-list', async () => {
|
|
const actual = await vi.importActual<typeof import('../../../hooks/use-directory-list')>('../../../hooks/use-directory-list');
|
|
return {
|
|
...actual,
|
|
useDirectoryList: () => ({
|
|
list: {
|
|
directoryCode: testState.boardIdentifier,
|
|
title: '/a/ - Anime & Manga',
|
|
boards: testState.directoryBoards,
|
|
},
|
|
loading: testState.directoryListLoading,
|
|
error: null,
|
|
}),
|
|
};
|
|
});
|
|
|
|
vi.mock('../../../hooks/use-resolved-community-address', () => ({
|
|
useResolvedCommunityAddress: () => undefined,
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-community-identifiers', () => ({
|
|
useCommunityIdentifier: (address?: string) => {
|
|
testState.communityIdentifierRequests.push(address);
|
|
return address ? { name: address } : undefined;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-is-community-offline', () => ({
|
|
default: (community?: { address?: string }, communityAddressHint?: string) => {
|
|
testState.offlineHookRequests.push({ address: community?.address, communityAddressHint });
|
|
return testState.offlineHookValue;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../../hooks/use-now-seconds', () => ({
|
|
useNowSeconds: () => testState.nowSeconds,
|
|
}));
|
|
|
|
vi.mock('../../../stores/use-community-offline-store', () => ({
|
|
default: <T,>(selector: (state: { communityOfflineState: typeof testState.offlineStates }) => T) =>
|
|
selector({
|
|
communityOfflineState: testState.offlineStates,
|
|
}),
|
|
}));
|
|
|
|
vi.mock('../../../lib/snow', () => ({
|
|
shouldShowSnow: () => false,
|
|
}));
|
|
|
|
let container: HTMLDivElement;
|
|
let originalAlert: typeof window.alert;
|
|
let root: Root;
|
|
|
|
const renderDirectory = async () => {
|
|
await act(async () => {
|
|
root.render(createElement(MemoryRouter, {}, createElement(Directory)));
|
|
});
|
|
};
|
|
|
|
const createDirectoryBoard = (address: string, score = 12) => ({
|
|
address,
|
|
score,
|
|
});
|
|
|
|
const createCommunity = (address: string, updatedAt = testState.nowSeconds - 60) => ({
|
|
address,
|
|
name: address,
|
|
state: 'started',
|
|
updatedAt,
|
|
});
|
|
|
|
const getDirectoryRow = (address = 'anime-and-manga.bso') => Array.from(container.querySelectorAll('tbody tr')).find((row) => row.textContent?.includes(address));
|
|
|
|
describe('Directory', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
testState.boardIdentifier = 'a';
|
|
testState.communities = {
|
|
'anime-and-manga.bso': {
|
|
address: 'anime-and-manga.bso',
|
|
name: 'anime-and-manga.bso',
|
|
state: 'started',
|
|
updatedAt: testState.nowSeconds - 60,
|
|
},
|
|
};
|
|
testState.communityIdentifierRequests = [];
|
|
testState.directoryListLoading = false;
|
|
testState.directoryBoards = [createDirectoryBoard('anime-and-manga.bso')];
|
|
testState.directories = [
|
|
{
|
|
address: 'anime-and-manga.bso',
|
|
directoryCode: 'a',
|
|
title: '/a/ - Anime & Manga',
|
|
},
|
|
];
|
|
testState.offlineHookRequests = [];
|
|
testState.offlineHookValue = {
|
|
isOffline: false,
|
|
isOnlineStatusLoading: false,
|
|
offlineIconClass: '',
|
|
offlineTitle: false,
|
|
};
|
|
testState.offlineStates = {};
|
|
testState.nowSeconds = 1_704_067_210;
|
|
originalAlert = window.alert;
|
|
window.alert = vi.fn();
|
|
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
window.alert = originalAlert;
|
|
container.remove();
|
|
});
|
|
|
|
it('shows online status for a listed board after loading its community', async () => {
|
|
await renderDirectory();
|
|
|
|
const cells = Array.from(getDirectoryRow()?.querySelectorAll('td') ?? []).map((cell) => cell.textContent?.replace(/\s+/g, ' ').trim());
|
|
expect(cells.slice(0, 5)).toEqual(['1', 'anime-and-manga.bso', 'directory_owner_anonymous', 'online', '12']);
|
|
expect(cells[5]).toContain('+1');
|
|
expect(cells[5]).toContain('-1');
|
|
expect(cells[5]).toContain('View');
|
|
expect(getDirectoryRow()?.querySelector('td:nth-child(2) a')).toBeNull();
|
|
expect(getDirectoryRow()?.querySelector('td:nth-child(2) span')).toBeNull();
|
|
expect(testState.communityIdentifierRequests).toContain('anime-and-manga.bso');
|
|
expect(testState.offlineHookRequests).toContainEqual({
|
|
address: 'anime-and-manga.bso',
|
|
communityAddressHint: 'anime-and-manga.bso',
|
|
});
|
|
});
|
|
|
|
it('shows loading status while the listed board status is loading', async () => {
|
|
testState.communities = {};
|
|
testState.offlineHookValue = {
|
|
isOffline: false,
|
|
isOnlineStatusLoading: true,
|
|
offlineIconClass: 'yellowOfflineIcon',
|
|
offlineTitle: 'downloading board...',
|
|
};
|
|
|
|
await renderDirectory();
|
|
|
|
expect(getDirectoryRow()?.textContent).toContain('loading');
|
|
});
|
|
|
|
it('shows a placeholder when listed board status is unknown', async () => {
|
|
testState.communities = {};
|
|
|
|
await renderDirectory();
|
|
|
|
const cells = Array.from(getDirectoryRow()?.querySelectorAll('td') ?? []).map((cell) => cell.textContent?.replace(/\s+/g, ' ').trim());
|
|
expect(cells[3]).toBe('—');
|
|
});
|
|
|
|
it('shows offline status when the listed board community is stale', async () => {
|
|
testState.communities['anime-and-manga.bso'] = {
|
|
address: 'anime-and-manga.bso',
|
|
name: 'anime-and-manga.bso',
|
|
state: 'started',
|
|
updatedAt: testState.nowSeconds - 31 * 60,
|
|
};
|
|
|
|
await renderDirectory();
|
|
|
|
expect(getDirectoryRow()?.textContent).toContain('offline');
|
|
});
|
|
|
|
it('does not request status checks after the top five boards', async () => {
|
|
const boards = Array.from({ length: 6 }, (_, index) => createDirectoryBoard(`board-${index + 1}.bso`, 100 - index));
|
|
testState.directoryBoards = boards;
|
|
testState.communities = Object.fromEntries(boards.map((board) => [board.address, createCommunity(board.address)]));
|
|
|
|
await renderDirectory();
|
|
|
|
for (const board of boards.slice(0, 5)) {
|
|
expect(testState.communityIdentifierRequests).toContain(board.address);
|
|
expect(testState.offlineHookRequests).toContainEqual({
|
|
address: board.address,
|
|
communityAddressHint: board.address,
|
|
});
|
|
}
|
|
|
|
expect(testState.communityIdentifierRequests).not.toContain('board-6.bso');
|
|
expect(testState.offlineHookRequests).not.toContainEqual({
|
|
address: 'board-6.bso',
|
|
communityAddressHint: 'board-6.bso',
|
|
});
|
|
|
|
const cells = Array.from(getDirectoryRow('board-6.bso')?.querySelectorAll('td') ?? []).map((cell) => cell.textContent?.replace(/\s+/g, ' ').trim());
|
|
expect(cells[3]).toBe('—?');
|
|
expect(getDirectoryRow('board-6.bso')?.querySelector('button[aria-label="directory_status_unavailable_reason"]')).not.toBeNull();
|
|
});
|
|
});
|