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,259 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import useChallengesStore from '../use-challenges-store';
|
||||
import useCreateBoardModalStore from '../use-create-board-modal-store';
|
||||
import useDirectoryModalStore from '../use-directory-modal-store';
|
||||
import useDisclaimerModalStore, { DISCLAIMER_ACCEPTED_KEY } from '../use-disclaimer-modal-store';
|
||||
import useFeedResetStore from '../use-feed-reset-store';
|
||||
import usePostNumberStore from '../use-post-number-store';
|
||||
import useReplyModalStore from '../use-reply-modal-store';
|
||||
import useSelectedTextStore from '../use-selected-text-store';
|
||||
import useSortingStore from '../use-sorting-store';
|
||||
|
||||
const resetReplyModalStore = () => {
|
||||
useReplyModalStore.setState({
|
||||
showReplyModal: false,
|
||||
openEmpty: false,
|
||||
activeCid: null,
|
||||
parentNumber: null,
|
||||
threadNumber: null,
|
||||
threadCid: null,
|
||||
subplebbitAddress: null,
|
||||
scrollY: 0,
|
||||
quoteInsertRequestId: 0,
|
||||
quoteInsertNumber: null,
|
||||
quoteInsertSelectedText: null,
|
||||
});
|
||||
};
|
||||
|
||||
describe('interaction stores', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
useChallengesStore.setState({ challenges: [] });
|
||||
useCreateBoardModalStore.getState().closeCreateBoardModal();
|
||||
useDirectoryModalStore.getState().closeDirectoryModal();
|
||||
useDisclaimerModalStore.getState().closeDisclaimerModal();
|
||||
useFeedResetStore.setState({ reset: null });
|
||||
usePostNumberStore.setState({ numberToCid: {}, cidToNumber: {} });
|
||||
useSelectedTextStore.getState().resetSelectedText();
|
||||
useSortingStore.getState().setSortType('active');
|
||||
resetReplyModalStore();
|
||||
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 1024,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'scrollY', {
|
||||
configurable: true,
|
||||
value: 0,
|
||||
writable: true,
|
||||
});
|
||||
vi.spyOn(document, 'getSelection').mockReturnValue({ toString: () => '' } as Selection);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
useSelectedTextStore.getState().resetSelectedText();
|
||||
resetReplyModalStore();
|
||||
});
|
||||
|
||||
it('opens and closes basic modal stores and keeps reset callbacks addressable', () => {
|
||||
expect(useCreateBoardModalStore.getState().showModal).toBe(false);
|
||||
useCreateBoardModalStore.getState().openCreateBoardModal();
|
||||
expect(useCreateBoardModalStore.getState().showModal).toBe(true);
|
||||
useCreateBoardModalStore.getState().closeCreateBoardModal();
|
||||
expect(useCreateBoardModalStore.getState().showModal).toBe(false);
|
||||
|
||||
expect(useDirectoryModalStore.getState().showModal).toBe(false);
|
||||
useDirectoryModalStore.getState().openDirectoryModal();
|
||||
expect(useDirectoryModalStore.getState().showModal).toBe(true);
|
||||
useDirectoryModalStore.getState().closeDirectoryModal();
|
||||
expect(useDirectoryModalStore.getState().showModal).toBe(false);
|
||||
|
||||
const resetMock = vi.fn();
|
||||
useFeedResetStore.getState().setResetFunction(resetMock);
|
||||
useFeedResetStore.getState().reset?.();
|
||||
expect(resetMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(useSortingStore.getState().sortType).toBe('active');
|
||||
useSortingStore.getState().setSortType('replyCount');
|
||||
expect(useSortingStore.getState().sortType).toBe('replyCount');
|
||||
});
|
||||
|
||||
it('queues challenges, abandons the current one, and logs abandon failures', async () => {
|
||||
const abandonMock = vi.fn().mockResolvedValue(undefined);
|
||||
const failingAbandonMock = vi.fn().mockRejectedValue(new Error('stop failed'));
|
||||
|
||||
useChallengesStore.getState().addChallenge({ type: 'captcha' } as never, abandonMock);
|
||||
useChallengesStore.getState().addChallenge({ type: 'math' } as never, failingAbandonMock);
|
||||
|
||||
const [first, second] = useChallengesStore.getState().challenges;
|
||||
expect(first.challenge).toEqual({ type: 'captcha' });
|
||||
expect(second.challenge).toEqual({ type: 'math' });
|
||||
expect(first.id).not.toBe(second.id);
|
||||
|
||||
await useChallengesStore.getState().abandonCurrentChallenge();
|
||||
expect(abandonMock).toHaveBeenCalledTimes(1);
|
||||
expect(useChallengesStore.getState().challenges).toHaveLength(1);
|
||||
|
||||
await useChallengesStore.getState().abandonCurrentChallenge();
|
||||
expect(failingAbandonMock).toHaveBeenCalledTimes(1);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to abandon challenge publication:', expect.any(Error));
|
||||
expect(useChallengesStore.getState().challenges).toHaveLength(0);
|
||||
|
||||
useChallengesStore.getState().addChallenge({ type: 'again' } as never);
|
||||
useChallengesStore.getState().removeChallenge();
|
||||
expect(useChallengesStore.getState().challenges).toEqual([]);
|
||||
});
|
||||
|
||||
it('shows the disclaimer modal until accepted, then navigates directly on later opens', () => {
|
||||
const navigate = vi.fn();
|
||||
|
||||
useDisclaimerModalStore.getState().showDisclaimerModal('board.eth', navigate, 'biz');
|
||||
|
||||
expect(useDisclaimerModalStore.getState()).toMatchObject({
|
||||
showModal: true,
|
||||
targetAddress: 'board.eth',
|
||||
targetBoardPath: 'biz',
|
||||
});
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
|
||||
useDisclaimerModalStore.getState().acceptDisclaimer(navigate);
|
||||
expect(localStorage.getItem(DISCLAIMER_ACCEPTED_KEY)).toBe('true');
|
||||
expect(navigate).toHaveBeenCalledWith('/biz');
|
||||
expect(useDisclaimerModalStore.getState().showModal).toBe(false);
|
||||
|
||||
navigate.mockClear();
|
||||
useDisclaimerModalStore.getState().showDisclaimerModal('music-posting.eth', navigate);
|
||||
expect(useDisclaimerModalStore.getState().showModal).toBe(false);
|
||||
expect(navigate).toHaveBeenCalledWith('/music-posting.eth');
|
||||
});
|
||||
|
||||
it('still navigates when saving disclaimer acceptance fails', () => {
|
||||
const navigate = vi.fn();
|
||||
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
||||
throw new Error('storage is locked');
|
||||
});
|
||||
|
||||
useDisclaimerModalStore.getState().showDisclaimerModal('board.eth', navigate, 'board-path');
|
||||
useDisclaimerModalStore.getState().acceptDisclaimer(navigate);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to save disclaimer acceptance to localStorage:', expect.any(Error));
|
||||
expect(navigate).toHaveBeenCalledWith('/board-path');
|
||||
expect(useDisclaimerModalStore.getState().showModal).toBe(false);
|
||||
|
||||
setItemSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('registers post numbers per board and ignores unchanged or invalid comments', () => {
|
||||
const comments = [
|
||||
{ cid: 'cid-1', number: 1, subplebbitAddress: 'music.eth' },
|
||||
{ cid: 'cid-2', number: 2, subplebbitAddress: 'music.eth' },
|
||||
{ cid: 'cid-1-tech', number: 1, subplebbitAddress: 'tech.eth' },
|
||||
{ cid: '', number: 3, subplebbitAddress: 'music.eth' },
|
||||
{ cid: 'cid-no-number', subplebbitAddress: 'music.eth' },
|
||||
] as never[];
|
||||
|
||||
usePostNumberStore.getState().registerComments(comments);
|
||||
|
||||
const firstState = usePostNumberStore.getState();
|
||||
expect(firstState.numberToCid).toEqual({
|
||||
'music.eth': { 1: 'cid-1', 2: 'cid-2' },
|
||||
'tech.eth': { 1: 'cid-1-tech' },
|
||||
});
|
||||
expect(firstState.cidToNumber).toEqual({
|
||||
'cid-1': 1,
|
||||
'cid-2': 2,
|
||||
'cid-1-tech': 1,
|
||||
});
|
||||
|
||||
const numberToCidRef = firstState.numberToCid;
|
||||
usePostNumberStore.getState().registerComments(comments);
|
||||
expect(usePostNumberStore.getState().numberToCid).toBe(numberToCidRef);
|
||||
});
|
||||
|
||||
it('opens reply modals with quoted selection and mobile scroll state', () => {
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 600,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'scrollY', {
|
||||
configurable: true,
|
||||
value: 140,
|
||||
writable: true,
|
||||
});
|
||||
vi.spyOn(document, 'getSelection').mockReturnValue({ toString: () => 'alpha\nbeta\n' } as Selection);
|
||||
|
||||
useReplyModalStore.getState().openReplyModal('parent-cid', 12, 'thread-cid', 34, 'music.eth');
|
||||
|
||||
expect(useSelectedTextStore.getState().selectedText).toBe('>alpha\n>beta\n');
|
||||
expect(useReplyModalStore.getState()).toMatchObject({
|
||||
showReplyModal: true,
|
||||
openEmpty: false,
|
||||
activeCid: 'thread-cid',
|
||||
parentNumber: 12,
|
||||
threadNumber: 34,
|
||||
threadCid: 'thread-cid',
|
||||
subplebbitAddress: 'music.eth',
|
||||
scrollY: 140,
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts quote requests into an already-open reply modal and can reopen empty', () => {
|
||||
useReplyModalStore.getState().openReplyModal('parent-cid', 12, 'thread-cid', 34, 'music.eth');
|
||||
vi.spyOn(document, 'getSelection').mockReturnValue({ toString: () => 'quoted text' } as Selection);
|
||||
|
||||
useReplyModalStore.getState().openReplyModal('parent-cid-2', 77, 'thread-cid', 34, 'music.eth');
|
||||
|
||||
expect(useReplyModalStore.getState().quoteInsertRequestId).toBe(1);
|
||||
expect(useReplyModalStore.getState().quoteInsertNumber).toBe(77);
|
||||
expect(useReplyModalStore.getState().quoteInsertSelectedText).toBe('>quoted text');
|
||||
|
||||
useSelectedTextStore.getState().setSelectedText('stale quote');
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 500,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(window, 'scrollY', {
|
||||
configurable: true,
|
||||
value: 32,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
useReplyModalStore.getState().openReplyModalEmpty('thread-cid', 34, 'music.eth');
|
||||
|
||||
expect(useSelectedTextStore.getState().selectedText).toBe('');
|
||||
expect(useReplyModalStore.getState()).toMatchObject({
|
||||
showReplyModal: true,
|
||||
openEmpty: true,
|
||||
activeCid: 'thread-cid',
|
||||
threadNumber: 34,
|
||||
threadCid: 'thread-cid',
|
||||
subplebbitAddress: 'music.eth',
|
||||
scrollY: 32,
|
||||
quoteInsertNumber: null,
|
||||
quoteInsertSelectedText: null,
|
||||
});
|
||||
|
||||
useSelectedTextStore.getState().setSelectedText('cleanup');
|
||||
useReplyModalStore.getState().closeModal();
|
||||
expect(useSelectedTextStore.getState().selectedText).toBe('');
|
||||
expect(useReplyModalStore.getState()).toMatchObject({
|
||||
showReplyModal: false,
|
||||
openEmpty: false,
|
||||
activeCid: null,
|
||||
parentNumber: null,
|
||||
threadNumber: null,
|
||||
quoteInsertNumber: null,
|
||||
quoteInsertSelectedText: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const flushMicrotasks = async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
const loadBlotterVisibilityStore = async () => (await import('../use-blotter-visibility-store')).default;
|
||||
const loadModQueueStore = async () => (await import('../use-mod-queue-store')).default;
|
||||
const loadPopularThreadsOptionsStore = async () => (await import('../use-popular-threads-options-store')).default;
|
||||
|
||||
const loadSpecialThemeStore = async (isChristmas: boolean) => {
|
||||
vi.resetModules();
|
||||
vi.doMock('../../lib/utils/time-utils', () => ({
|
||||
isChristmas: () => isChristmas,
|
||||
}));
|
||||
const module = await import('../use-special-theme-store');
|
||||
await flushMicrotasks();
|
||||
return module.default;
|
||||
};
|
||||
|
||||
describe('persisted extra stores', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock('../../lib/utils/time-utils');
|
||||
});
|
||||
|
||||
it('toggles blotter visibility and persists the hidden flag', async () => {
|
||||
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
|
||||
const useBlotterVisibilityStore = await loadBlotterVisibilityStore();
|
||||
|
||||
expect(useBlotterVisibilityStore.getState().isHidden).toBe(false);
|
||||
|
||||
useBlotterVisibilityStore.getState().toggleVisibility();
|
||||
expect(useBlotterVisibilityStore.getState().isHidden).toBe(true);
|
||||
expect(setItemSpy).toHaveBeenCalledWith('blotter-visibility', expect.stringContaining('"isHidden":true'));
|
||||
|
||||
setItemSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('loads popular thread options from localStorage defaults and persists changes', async () => {
|
||||
const usePopularThreadsOptionsStore = await loadPopularThreadsOptionsStore();
|
||||
|
||||
expect(usePopularThreadsOptionsStore.getState().showWorksafeContentOnly).toBe(true);
|
||||
expect(usePopularThreadsOptionsStore.getState().showNsfwContentOnly).toBe(false);
|
||||
|
||||
usePopularThreadsOptionsStore.getState().setShowWorksafeContentOnly(false);
|
||||
usePopularThreadsOptionsStore.getState().setShowNsfwContentOnly(true);
|
||||
|
||||
expect(localStorage.getItem('showWorksafeContentOnly')).toBe('false');
|
||||
expect(localStorage.getItem('showNsfwContentOnly')).toBe('true');
|
||||
});
|
||||
|
||||
it('migrates legacy mod queue storage and computes threshold seconds from the active unit', async () => {
|
||||
localStorage.setItem(
|
||||
'mod-queue-storage',
|
||||
JSON.stringify({
|
||||
state: {
|
||||
alertThresholdHours: 3,
|
||||
selectedBoardFilter: 'music.eth',
|
||||
viewMode: 'feed',
|
||||
},
|
||||
version: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const useModQueueStore = await loadModQueueStore();
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(useModQueueStore.getState()).toMatchObject({
|
||||
alertThresholdValue: 3,
|
||||
alertThresholdUnit: 'hours',
|
||||
selectedBoardFilter: 'music.eth',
|
||||
viewMode: 'feed',
|
||||
});
|
||||
expect(useModQueueStore.getState().getAlertThresholdSeconds()).toBe(10_800);
|
||||
|
||||
useModQueueStore.getState().setAlertThreshold(15, 'minutes');
|
||||
useModQueueStore.getState().setSelectedBoardFilter('tech.eth');
|
||||
useModQueueStore.getState().setViewMode('compact');
|
||||
|
||||
expect(useModQueueStore.getState()).toMatchObject({
|
||||
alertThresholdValue: 15,
|
||||
alertThresholdUnit: 'minutes',
|
||||
selectedBoardFilter: 'tech.eth',
|
||||
viewMode: 'compact',
|
||||
});
|
||||
expect(useModQueueStore.getState().getAlertThresholdSeconds()).toBe(900);
|
||||
});
|
||||
|
||||
it('blocks special theme enablement outside christmas and clears persisted enabled state on rehydrate', async () => {
|
||||
localStorage.setItem(
|
||||
'Special-theme-storage',
|
||||
JSON.stringify({
|
||||
state: { isEnabled: true },
|
||||
version: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
const useSpecialThemeStore = await loadSpecialThemeStore(false);
|
||||
|
||||
expect(useSpecialThemeStore.getState().isEnabled).toBeNull();
|
||||
|
||||
useSpecialThemeStore.getState().setIsEnabled(true);
|
||||
expect(useSpecialThemeStore.getState().isEnabled).toBeNull();
|
||||
});
|
||||
|
||||
it('allows opting into the special theme during christmas', async () => {
|
||||
const useSpecialThemeStore = await loadSpecialThemeStore(true);
|
||||
|
||||
expect(useSpecialThemeStore.getState().isEnabled).toBeNull();
|
||||
|
||||
useSpecialThemeStore.getState().setIsEnabled(true);
|
||||
expect(useSpecialThemeStore.getState().isEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getAllBoardCodes } from '../../constants/board-codes';
|
||||
|
||||
describe('preference stores', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('useAllFeedFilterStore loads and persists the selected filter', async () => {
|
||||
localStorage.setItem('5chan-all-feed-filter', 'nsfw');
|
||||
|
||||
const store = (await import('../use-all-feed-filter-store')).default;
|
||||
|
||||
expect(store.getState().filter).toBe('nsfw');
|
||||
|
||||
store.getState().setFilter('sfw');
|
||||
|
||||
expect(store.getState().filter).toBe('sfw');
|
||||
expect(localStorage.getItem('5chan-all-feed-filter')).toBe('sfw');
|
||||
});
|
||||
|
||||
it('useBoardsFilterStore restores board preferences and saves updates', async () => {
|
||||
localStorage.setItem('5chan-boards-use-catalog', 'true');
|
||||
localStorage.setItem('5chan-boards-filter', 'worksafe');
|
||||
|
||||
const store = (await import('../use-boards-filter-store')).default;
|
||||
|
||||
expect(store.getState().useCatalogLinks).toBe(true);
|
||||
expect(store.getState().boardFilter).toBe('worksafe');
|
||||
|
||||
store.getState().setUseCatalogLinks(false);
|
||||
store.getState().setBoardFilter('nsfw');
|
||||
|
||||
expect(store.getState().useCatalogLinks).toBe(false);
|
||||
expect(store.getState().boardFilter).toBe('nsfw');
|
||||
expect(localStorage.getItem('5chan-boards-use-catalog')).toBe('false');
|
||||
expect(localStorage.getItem('5chan-boards-filter')).toBe('nsfw');
|
||||
});
|
||||
|
||||
it('useCatalogStyleStore reads existing preferences and updates both settings', async () => {
|
||||
localStorage.setItem('imageSize', 'Large');
|
||||
localStorage.setItem('showOPComment', 'false');
|
||||
|
||||
const store = (await import('../use-catalog-style-store')).default;
|
||||
|
||||
expect(store.getState().imageSize).toBe('Large');
|
||||
expect(store.getState().showOPComment).toBe(false);
|
||||
|
||||
store.getState().setImageSize('Small');
|
||||
store.getState().setShowOPComment(true);
|
||||
|
||||
expect(store.getState().imageSize).toBe('Small');
|
||||
expect(store.getState().showOPComment).toBe(true);
|
||||
expect(localStorage.getItem('imageSize')).toBe('Small');
|
||||
expect(localStorage.getItem('showOPComment')).toBe('true');
|
||||
});
|
||||
|
||||
it('useBoardsBarVisibilityStore loads legacy keys and updates visibility settings', async () => {
|
||||
const boardCodes = getAllBoardCodes();
|
||||
localStorage.setItem('5chan-topbar-directories-visible', JSON.stringify([boardCodes[0]]));
|
||||
localStorage.setItem('5chan-topbar-subscriptions-visible', JSON.stringify(['sub-1']));
|
||||
|
||||
const store = (await import('../use-boards-bar-visibility-store')).default;
|
||||
|
||||
expect(Array.from(store.getState().visibleDirectories)).toEqual([boardCodes[0]]);
|
||||
expect(store.getState().showSubscriptionsInBoardsBar).toBe(true);
|
||||
|
||||
store.getState().toggleDirectory(boardCodes[1]);
|
||||
expect(store.getState().visibleDirectories.has(boardCodes[1])).toBe(true);
|
||||
|
||||
store.getState().setDirectoryVisibility(boardCodes[0], false);
|
||||
expect(store.getState().visibleDirectories.has(boardCodes[0])).toBe(false);
|
||||
|
||||
store.getState().setShowSubscriptionsInBoardsBar(false);
|
||||
expect(store.getState().showSubscriptionsInBoardsBar).toBe(false);
|
||||
expect(localStorage.getItem('5chan-boardsbar-subscriptions-visible')).toBe('false');
|
||||
});
|
||||
|
||||
it('useBoardsBarVisibilityStore reinitializes from current storage keys', async () => {
|
||||
const boardCodes = getAllBoardCodes();
|
||||
const store = (await import('../use-boards-bar-visibility-store')).default;
|
||||
|
||||
localStorage.setItem('5chan-boardsbar-directories-visible', JSON.stringify([boardCodes[2], boardCodes[3]]));
|
||||
localStorage.setItem('5chan-boardsbar-subscriptions-visible', JSON.stringify(true));
|
||||
|
||||
store.getState().initialize();
|
||||
|
||||
expect(Array.from(store.getState().visibleDirectories)).toEqual([boardCodes[2], boardCodes[3]]);
|
||||
expect(store.getState().showSubscriptionsInBoardsBar).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import usePublishPostStore from '../use-publish-post-store';
|
||||
import usePublishReplyStore from '../use-publish-reply-store';
|
||||
|
||||
type PublishPostInput = Parameters<ReturnType<typeof usePublishPostStore.getState>['setPublishPostStore']>[0];
|
||||
type PublishReplyInput = Parameters<ReturnType<typeof usePublishReplyStore.getState>['setPublishReplyStore']>[0];
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
alertChallengeVerificationFailedMock: vi.fn(),
|
||||
alertMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/utils/challenge-utils', () => ({
|
||||
alertChallengeVerificationFailed: (challengeVerification: unknown, comment: unknown) => testState.alertChallengeVerificationFailedMock(challengeVerification, comment),
|
||||
}));
|
||||
|
||||
describe('publish stores', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
testState.alertChallengeVerificationFailedMock.mockReset();
|
||||
testState.alertMock.mockReset();
|
||||
vi.stubGlobal('alert', testState.alertMock);
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
usePublishPostStore.getState().resetPublishPostStore();
|
||||
usePublishReplyStore.getState().resetPublishReplyStore('parent-1');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
vi.unstubAllGlobals();
|
||||
usePublishPostStore.getState().resetPublishPostStore();
|
||||
usePublishReplyStore.getState().resetPublishReplyStore('parent-1');
|
||||
});
|
||||
|
||||
it('usePublishPostStore derives display name, payload options, and resets cleanly', () => {
|
||||
const comment: PublishPostInput = {
|
||||
author: { address: '0x123', displayName: 'Author Name', role: 'mod' },
|
||||
content: 'post body',
|
||||
displayName: 'Poster Alias',
|
||||
link: 'https://example.com',
|
||||
spoiler: true,
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
title: 'Hello',
|
||||
};
|
||||
|
||||
usePublishPostStore.getState().setPublishPostStore(comment);
|
||||
|
||||
const state = usePublishPostStore.getState();
|
||||
expect(state.displayName).toBe('Poster Alias');
|
||||
expect(state.author).toEqual({ address: '0x123', role: 'mod', displayName: 'Poster Alias' });
|
||||
expect(state.publishCommentOptions.author).toEqual({ address: '0x123', role: 'mod', displayName: 'Poster Alias' });
|
||||
expect(state.publishCommentOptions.subplebbitAddress).toBe('music-posting.eth');
|
||||
expect(state.publishCommentOptions.title).toBe('Hello');
|
||||
|
||||
state.publishCommentOptions.onChallengeVerification?.({} as never, comment);
|
||||
expect(testState.alertChallengeVerificationFailedMock).toHaveBeenCalledWith({}, comment);
|
||||
|
||||
state.publishCommentOptions.onError?.(new Error('publish failed'));
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(testState.alertMock).toHaveBeenCalledWith('publish failed');
|
||||
|
||||
state.resetPublishPostStore();
|
||||
expect(usePublishPostStore.getState().publishCommentOptions).toEqual({});
|
||||
expect(usePublishPostStore.getState().title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('usePublishReplyStore stores reply data per parentCid and resets a single thread', () => {
|
||||
const comment: PublishReplyInput = {
|
||||
author: { address: '0x123', displayName: 'Author Name', role: 'mod' },
|
||||
content: 'reply body',
|
||||
displayName: 'Reply Alias',
|
||||
link: 'https://example.com/reply',
|
||||
parentCid: 'parent-1',
|
||||
spoiler: false,
|
||||
subplebbitAddress: 'music-posting.eth',
|
||||
};
|
||||
|
||||
usePublishReplyStore.getState().setPublishReplyStore(comment);
|
||||
|
||||
const state = usePublishReplyStore.getState();
|
||||
expect(state.displayName['parent-1']).toBe('Reply Alias');
|
||||
expect(state.author['parent-1']).toEqual({ address: '0x123', role: 'mod', displayName: 'Reply Alias' });
|
||||
expect(state.publishCommentOptions['parent-1']?.parentCid).toBe('parent-1');
|
||||
expect(state.publishCommentOptions['parent-1']?.postCid).toBe('parent-1');
|
||||
|
||||
state.publishCommentOptions['parent-1']?.onChallengeVerification?.({ token: 'challenge' } as never, comment);
|
||||
expect(testState.alertChallengeVerificationFailedMock).toHaveBeenCalledWith({ token: 'challenge' }, comment);
|
||||
|
||||
state.publishCommentOptions['parent-1']?.onError?.(new Error('reply failed'));
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
expect(testState.alertMock).toHaveBeenCalledWith('reply failed');
|
||||
|
||||
state.resetPublishReplyStore('parent-1');
|
||||
expect(usePublishReplyStore.getState().publishCommentOptions['parent-1']).toBeUndefined();
|
||||
expect(usePublishReplyStore.getState().content['parent-1']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
entriesMock: vi.fn(async () => [] as Array<['nsfw' | 'sfw', string]>),
|
||||
setItemMock: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/lib/localforage-lru/index.js', () => ({
|
||||
default: {
|
||||
createInstance: () => ({
|
||||
entries: testState.entriesMock,
|
||||
setItem: testState.setItemMock,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const waitFor = async (predicate: () => boolean) => {
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
await Promise.resolve();
|
||||
if (predicate()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('useThemeStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
testState.entriesMock.mockReset();
|
||||
testState.entriesMock.mockResolvedValue([]);
|
||||
testState.setItemMock.mockReset();
|
||||
testState.setItemMock.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('loads stored themes on initialization', async () => {
|
||||
testState.entriesMock.mockResolvedValue([
|
||||
['nsfw', 'tomorrow'],
|
||||
['sfw', 'photon'],
|
||||
]);
|
||||
|
||||
const store = (await import('../use-theme-store')).default;
|
||||
await waitFor(() => store.getState().themes.nsfw === 'tomorrow');
|
||||
|
||||
expect(store.getState().themes).toEqual({
|
||||
nsfw: 'tomorrow',
|
||||
sfw: 'photon',
|
||||
});
|
||||
expect(store.getState().currentTheme).toBeNull();
|
||||
});
|
||||
|
||||
it('setTheme persists the updated theme and updates currentTheme', async () => {
|
||||
const store = (await import('../use-theme-store')).default;
|
||||
await waitFor(() => testState.entriesMock.mock.calls.length > 0);
|
||||
|
||||
await store.getState().setTheme('nsfw', 'photon');
|
||||
|
||||
expect(testState.setItemMock).toHaveBeenCalledWith('nsfw', 'photon');
|
||||
expect(store.getState().themes.nsfw).toBe('photon');
|
||||
expect(store.getState().currentTheme).toBe('photon');
|
||||
});
|
||||
|
||||
it('getTheme can skip updating currentTheme when requested', async () => {
|
||||
const store = (await import('../use-theme-store')).default;
|
||||
await waitFor(() => testState.entriesMock.mock.calls.length > 0);
|
||||
|
||||
expect(store.getState().getTheme('sfw', false)).toBe('yotsuba-b');
|
||||
expect(store.getState().currentTheme).toBeNull();
|
||||
|
||||
expect(store.getState().getTheme('sfw')).toBe('yotsuba-b');
|
||||
expect(store.getState().currentTheme).toBe('yotsuba-b');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('ui state stores', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('useBoardsBarEditModalStore opens and closes the modal', async () => {
|
||||
const store = (await import('../use-boards-bar-edit-modal-store')).default;
|
||||
|
||||
expect(store.getState().showModal).toBe(false);
|
||||
|
||||
store.getState().openBoardsBarEditModal();
|
||||
expect(store.getState().showModal).toBe(true);
|
||||
|
||||
store.getState().closeBoardsBarEditModal();
|
||||
expect(store.getState().showModal).toBe(false);
|
||||
});
|
||||
|
||||
it('useSelectedTextStore sets and resets selected text', async () => {
|
||||
const store = (await import('../use-selected-text-store')).default;
|
||||
|
||||
store.getState().setSelectedText('>quoted line');
|
||||
expect(store.getState().selectedText).toBe('>quoted line');
|
||||
|
||||
store.getState().resetSelectedText();
|
||||
expect(store.getState().selectedText).toBe('');
|
||||
});
|
||||
|
||||
it('useExpandedMediaStore persists fitExpandedImagesToScreen', async () => {
|
||||
const store = (await import('../use-expanded-media-store')).default;
|
||||
|
||||
expect(store.getState().fitExpandedImagesToScreen).toBe(false);
|
||||
|
||||
store.getState().setFitExpandedImagesToScreen(true);
|
||||
|
||||
expect(store.getState().fitExpandedImagesToScreen).toBe(true);
|
||||
expect(localStorage.getItem('expanded-media-store')).toContain('fitExpandedImagesToScreen');
|
||||
});
|
||||
|
||||
it('useSubplebbitOfflineStore merges updates and clears initialLoad after the timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const store = (await import('../use-subplebbit-offline-store')).default;
|
||||
|
||||
store.getState().initializesubplebbitOfflineState('music-posting.eth');
|
||||
expect(store.getState().subplebbitOfflineState['music-posting.eth']).toEqual({ initialLoad: true });
|
||||
|
||||
store.getState().setSubplebbitOfflineState('music-posting.eth', {
|
||||
state: 'offline',
|
||||
updatedAt: 123,
|
||||
updatingState: 'recovering',
|
||||
});
|
||||
|
||||
expect(store.getState().subplebbitOfflineState['music-posting.eth']).toEqual({
|
||||
initialLoad: true,
|
||||
state: 'offline',
|
||||
updatedAt: 123,
|
||||
updatingState: 'recovering',
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
expect(store.getState().subplebbitOfflineState['music-posting.eth']).toEqual({
|
||||
initialLoad: false,
|
||||
state: 'offline',
|
||||
updatedAt: 123,
|
||||
updatingState: 'recovering',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
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';
|
||||
|
||||
(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: number[] = [];
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
const flushEffects = async (count = 3) => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
describe('useSubplebbitsLoadingStartTimestamps', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
|
||||
latestValue = [];
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('stores first-seen timestamps per board and only adds new addresses on rerender', async () => {
|
||||
const useSubplebbitsLoadingStartTimestamps = (await import('../use-subplebbits-loading-start-timestamps-store')).default;
|
||||
|
||||
const HookHarness = ({ addresses }: { addresses?: string[] }) => {
|
||||
const value = useSubplebbitsLoadingStartTimestamps(addresses);
|
||||
React.useLayoutEffect(() => {
|
||||
latestValue = value;
|
||||
}, [value]);
|
||||
return null;
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(HookHarness, { addresses: ['music.eth', 'tech.eth'] }));
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(latestValue).toEqual([1_704_067_200, 1_704_067_200]);
|
||||
|
||||
vi.setSystemTime(new Date('2024-01-01T00:10:00Z'));
|
||||
|
||||
await act(async () => {
|
||||
root.render(createElement(HookHarness, { addresses: ['music.eth', 'biz.eth'] }));
|
||||
});
|
||||
await flushEffects();
|
||||
|
||||
expect(latestValue).toEqual([1_704_067_200, 1_704_067_800]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user