mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
test: expand honest whole-repo coverage
Adds broad coverage across stores, hooks, and runtime utilities while switching `vitest.config.ts` to an explicit whole-repo include list. This turns the coverage report into a real repo-wide baseline instead of an imported-file subset.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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 { useCurrentTime } from '../use-current-time';
|
||||
import useIsMobile from '../use-is-mobile';
|
||||
import useWindowWidth from '../use-window-width';
|
||||
|
||||
(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>;
|
||||
|
||||
let latestValue: unknown;
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
const HookHarness = ({ useValue }: { useValue: () => unknown }) => {
|
||||
const value = useValue();
|
||||
latestValue = value;
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHookValue = (useValue: () => unknown) => {
|
||||
act(() => {
|
||||
root.render(createElement(HookHarness, { useValue }));
|
||||
});
|
||||
|
||||
return latestValue;
|
||||
};
|
||||
|
||||
describe('browser hooks', () => {
|
||||
beforeEach(() => {
|
||||
latestValue = undefined;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 1024,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('tracks the window width across resize events', () => {
|
||||
expect(renderHookValue(() => useWindowWidth())).toBe(1024);
|
||||
|
||||
act(() => {
|
||||
window.innerWidth = 480;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(latestValue).toBe(480);
|
||||
});
|
||||
|
||||
it('derives the mobile breakpoint from the current window width', () => {
|
||||
renderHookValue(() => useIsMobile());
|
||||
expect(latestValue).toBe(false);
|
||||
|
||||
act(() => {
|
||||
window.innerWidth = 639;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(latestValue).toBe(true);
|
||||
});
|
||||
|
||||
it('updates the current time on the configured interval', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
|
||||
|
||||
expect(renderHookValue(() => useCurrentTime(30))).toBe(1_704_067_200);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(30_000);
|
||||
});
|
||||
|
||||
expect(latestValue).toBe(1_704_067_230);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
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 { useAccountSubplebbitAddresses } from '../use-account-subplebbit-addresses';
|
||||
import { useAccountSubplebbitsWithMetadata } from '../use-account-subplebbits-with-metadata';
|
||||
import useAuthorPrivileges from '../use-author-privileges';
|
||||
import { useBoardFeedPageSize } from '../use-board-feed-page-size';
|
||||
import { useBoardPseudonymityMode } from '../use-board-pseudonymity-mode';
|
||||
import useCountLinksInReplies from '../use-count-links-in-replies';
|
||||
import { useFilteredDirectoryAddresses } from '../use-filtered-directory-addresses';
|
||||
import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store';
|
||||
|
||||
(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(() => ({
|
||||
account: undefined as unknown,
|
||||
accountSubplebbits: {} as Record<string, unknown>,
|
||||
directories: [] as Array<{ address: string; nsfw?: boolean }>,
|
||||
directoryLookup: {} as Record<string, unknown>,
|
||||
flattenedReplies: [] as unknown[],
|
||||
subplebbitSnapshot: undefined as unknown,
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
|
||||
useAccount: () => testState.account,
|
||||
useAccountSubplebbits: () => ({ accountSubplebbits: testState.accountSubplebbits }),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/lib/utils', () => ({
|
||||
flattenCommentsPages: () => testState.flattenedReplies,
|
||||
}));
|
||||
|
||||
vi.mock('../use-directories', () => ({
|
||||
useDirectories: () => testState.directories,
|
||||
useDirectoryByAddress: (address: string | undefined) => (address ? testState.directoryLookup[address] : undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../use-stable-subplebbit', () => ({
|
||||
useSubplebbitField: (_address: string | undefined, selector: (subplebbit: unknown) => unknown) => selector(testState.subplebbitSnapshot),
|
||||
}));
|
||||
|
||||
let latestValue: unknown;
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
let renderCount = 0;
|
||||
|
||||
const HookHarness = ({ useValue }: { useValue: () => unknown }) => {
|
||||
const value = useValue();
|
||||
latestValue = value;
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHookValue = (useValue: () => unknown) => {
|
||||
act(() => {
|
||||
root.render(createElement(HookHarness, { key: renderCount++, useValue }));
|
||||
});
|
||||
|
||||
return latestValue;
|
||||
};
|
||||
|
||||
describe('selector hooks', () => {
|
||||
beforeEach(() => {
|
||||
latestValue = undefined;
|
||||
renderCount = 0;
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
testState.account = undefined;
|
||||
testState.accountSubplebbits = {};
|
||||
testState.directories = [];
|
||||
testState.directoryLookup = {};
|
||||
testState.flattenedReplies = [];
|
||||
testState.subplebbitSnapshot = undefined;
|
||||
useAllFeedFilterStore.getState().setFilter('all');
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('derives account board addresses and metadata from cached account subplebbits', () => {
|
||||
testState.accountSubplebbits = {
|
||||
'music.eth': { address: 'music.eth', title: '/mu/ - Music' },
|
||||
'tech.eth': { address: 'tech.eth', title: '/g/ - Technology' },
|
||||
};
|
||||
|
||||
expect(renderHookValue(() => useAccountSubplebbitAddresses())).toEqual(['music.eth', 'tech.eth']);
|
||||
expect(renderHookValue(() => useAccountSubplebbitsWithMetadata())).toEqual([
|
||||
{ address: 'music.eth', title: '/mu/ - Music' },
|
||||
{ address: 'tech.eth', title: '/g/ - Technology' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('computes moderator privileges and whether the current account authored the comment', () => {
|
||||
testState.account = { author: { address: '0xme' } };
|
||||
testState.subplebbitSnapshot = {
|
||||
roles: {
|
||||
'0xauthor': { role: 'moderator' },
|
||||
'0xme': { role: 'admin' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(renderHookValue(() => useAuthorPrivileges({ commentAuthorAddress: '0xauthor', subplebbitAddress: 'music.eth' }))).toEqual({
|
||||
isCommentAuthorMod: true,
|
||||
isAccountMod: true,
|
||||
isAccountCommentAuthor: false,
|
||||
commentAuthorRole: 'moderator',
|
||||
accountAuthorRole: 'admin',
|
||||
});
|
||||
|
||||
expect(renderHookValue(() => useAuthorPrivileges({ commentAuthorAddress: '0xme', subplebbitAddress: 'music.eth' }))).toEqual({
|
||||
isCommentAuthorMod: true,
|
||||
isAccountMod: true,
|
||||
isAccountCommentAuthor: true,
|
||||
commentAuthorRole: 'admin',
|
||||
accountAuthorRole: 'admin',
|
||||
});
|
||||
});
|
||||
|
||||
it('derives board page sizes from directory metadata and falls back when unavailable', () => {
|
||||
expect(renderHookValue(() => useBoardFeedPageSize({ features: { postsPerPage: 22 } } as never))).toEqual({
|
||||
guiPostsPerPage: 22,
|
||||
maxGuiPages: 10,
|
||||
paginationFeedPostsPerPage: 220,
|
||||
infiniteFeedPostsPerPage: 22,
|
||||
});
|
||||
|
||||
expect(renderHookValue(() => useBoardFeedPageSize(undefined))).toEqual({
|
||||
guiPostsPerPage: 15,
|
||||
maxGuiPages: 10,
|
||||
paginationFeedPostsPerPage: 150,
|
||||
infiniteFeedPostsPerPage: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers live pseudonymity metadata and falls back to directory entries', () => {
|
||||
testState.directoryLookup = {
|
||||
'music.eth': {
|
||||
address: 'music.eth',
|
||||
features: { pseudonymityMode: 'directory-mode' },
|
||||
},
|
||||
};
|
||||
testState.subplebbitSnapshot = {
|
||||
features: { pseudonymityMode: 'live-mode' },
|
||||
};
|
||||
|
||||
expect(renderHookValue(() => useBoardPseudonymityMode('music.eth'))).toBe('live-mode');
|
||||
|
||||
testState.subplebbitSnapshot = {
|
||||
features: {},
|
||||
};
|
||||
|
||||
expect(renderHookValue(() => useBoardPseudonymityMode('music.eth'))).toBe('directory-mode');
|
||||
});
|
||||
|
||||
it('counts link-bearing replies and supports a reply preview limit', () => {
|
||||
testState.flattenedReplies = [{ link: 'https://a.test' }, { link: undefined }, { link: 'https://b.test' }];
|
||||
|
||||
expect(renderHookValue(() => useCountLinksInReplies({ replies: {} } as never))).toBe(2);
|
||||
expect(renderHookValue(() => useCountLinksInReplies({ replies: {} } as never, 2))).toBe(1);
|
||||
});
|
||||
|
||||
it('filters directory addresses according to the all-feed mode', () => {
|
||||
testState.directories = [{ address: 'music.eth', nsfw: false }, { address: 'flash.eth', nsfw: true }, { address: 'tech.eth' }];
|
||||
|
||||
expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['music.eth', 'flash.eth', 'tech.eth']);
|
||||
|
||||
act(() => {
|
||||
useAllFeedFilterStore.getState().setFilter('nsfw');
|
||||
});
|
||||
expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['flash.eth']);
|
||||
|
||||
act(() => {
|
||||
useAllFeedFilterStore.getState().setFilter('sfw');
|
||||
});
|
||||
expect(renderHookValue(() => useFilteredDirectoryAddresses())).toEqual(['music.eth', 'tech.eth']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import useAuthorAddressClick from '../use-author-address-click';
|
||||
|
||||
describe('useAuthorAddressClick', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('highlights matching author nodes except the op post and clears previous highlights', () => {
|
||||
document.body.innerHTML = `
|
||||
<span class="highlight" data-author-address="old" data-post-cid="post-1" data-cid="reply-old"></span>
|
||||
<span data-author-address="0x1...3" data-post-cid="post-1" data-cid="post-1"></span>
|
||||
<span data-author-address="0x1...3" data-post-cid="post-1" data-cid="reply-1"></span>
|
||||
<span data-author-address="0x1...3" data-post-cid="post-1" data-cid="reply-2"></span>
|
||||
`;
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
handleUserAddressClick('0x1...3', 'post-1');
|
||||
|
||||
const matches = document.querySelectorAll('[data-author-address="0x1...3"][data-post-cid="post-1"]');
|
||||
expect(matches[0].classList.contains('highlight')).toBe(false);
|
||||
expect(matches[1].classList.contains('highlight')).toBe(true);
|
||||
expect(matches[2].classList.contains('highlight')).toBe(true);
|
||||
expect(document.querySelector('[data-author-address="old"]')?.classList.contains('highlight')).toBe(false);
|
||||
});
|
||||
|
||||
it('toggles the highlight off when the same address is clicked again', () => {
|
||||
document.body.innerHTML = `
|
||||
<span data-author-address="0x1...3" data-post-cid="post-1" data-cid="reply-1"></span>
|
||||
<span data-author-address="0x1...3" data-post-cid="post-1" data-cid="reply-2"></span>
|
||||
`;
|
||||
|
||||
const handleUserAddressClick = useAuthorAddressClick();
|
||||
handleUserAddressClick('0x1...3', 'post-1');
|
||||
handleUserAddressClick('0x1...3', 'post-1');
|
||||
|
||||
const matches = document.querySelectorAll('[data-author-address="0x1...3"][data-post-cid="post-1"]');
|
||||
expect(matches[0].classList.contains('highlight')).toBe(false);
|
||||
expect(matches[1].classList.contains('highlight')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user