mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
test: expand hook and utility runtime coverage
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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 type { CommentMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { useCommentMediaInfo } from '../use-comment-media-info';
|
||||
|
||||
(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(() => ({
|
||||
fetchWebpageThumbnailIfNeededMock: vi.fn<(mediaInfo: CommentMediaInfo) => Promise<CommentMediaInfo>>(),
|
||||
getCommentMediaInfoMock: vi.fn<(link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number) => CommentMediaInfo | undefined>(),
|
||||
imageHeight: 360,
|
||||
imageWidth: 640,
|
||||
mediaInfo: undefined as CommentMediaInfo | undefined,
|
||||
params: {} as Record<string, string>,
|
||||
pathname: '/all',
|
||||
thumbnailMediaInfo: undefined as CommentMediaInfo | undefined,
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useLocation: () => ({ pathname: testState.pathname }),
|
||||
useParams: () => testState.params,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../lib/utils/media-utils', () => ({
|
||||
fetchWebpageThumbnailIfNeeded: (mediaInfo: CommentMediaInfo) => testState.fetchWebpageThumbnailIfNeededMock(mediaInfo),
|
||||
getCommentMediaInfo: (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number) =>
|
||||
testState.getCommentMediaInfoMock(link, thumbnailUrl, linkWidth, linkHeight),
|
||||
}));
|
||||
|
||||
let latestValue: CommentMediaInfo | undefined;
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let originalImage: typeof globalThis.Image;
|
||||
|
||||
class MockImage {
|
||||
height = testState.imageHeight;
|
||||
onerror: ((this: GlobalEventHandlers, ev: Event | string) => any) | null = null;
|
||||
onload: ((this: GlobalEventHandlers, ev: Event) => any) | null = null;
|
||||
width = testState.imageWidth;
|
||||
|
||||
set src(_value: string) {
|
||||
setTimeout(() => {
|
||||
this.onload?.call(this as never, new Event('load'));
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const HookHarness = ({ link, linkHeight, linkWidth, thumbnailUrl }: { link: string; linkHeight: number; linkWidth: number; thumbnailUrl: string }) => {
|
||||
latestValue = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
return null;
|
||||
};
|
||||
|
||||
const flushEffects = async (count = 5) => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderHook = async (props = { link: 'https://example.com', linkHeight: 360, linkWidth: 640, thumbnailUrl: '' }) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(HookHarness, props));
|
||||
});
|
||||
await flushEffects();
|
||||
};
|
||||
|
||||
describe('useCommentMediaInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
latestValue = undefined;
|
||||
testState.imageHeight = 360;
|
||||
testState.imageWidth = 640;
|
||||
testState.mediaInfo = undefined;
|
||||
testState.params = {};
|
||||
testState.pathname = '/all';
|
||||
testState.thumbnailMediaInfo = undefined;
|
||||
testState.getCommentMediaInfoMock.mockImplementation(() => testState.mediaInfo);
|
||||
testState.fetchWebpageThumbnailIfNeededMock.mockImplementation(async () => testState.thumbnailMediaInfo ?? (testState.mediaInfo as CommentMediaInfo));
|
||||
|
||||
originalImage = globalThis.Image;
|
||||
globalThis.Image = MockImage as never;
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
globalThis.Image = originalImage;
|
||||
});
|
||||
|
||||
it('returns the current media info without fetching thumbnails outside post and pending views', async () => {
|
||||
testState.mediaInfo = {
|
||||
thumbnail: 'https://cdn.example.com/thumb.jpg',
|
||||
type: 'image',
|
||||
url: 'https://cdn.example.com/image.jpg',
|
||||
};
|
||||
|
||||
await renderHook({
|
||||
link: 'https://cdn.example.com/image.jpg',
|
||||
linkHeight: 360,
|
||||
linkWidth: 640,
|
||||
thumbnailUrl: 'https://cdn.example.com/thumb.jpg',
|
||||
});
|
||||
|
||||
expect(latestValue).toEqual(testState.mediaInfo);
|
||||
expect(testState.fetchWebpageThumbnailIfNeededMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches webpage thumbnails on thread routes and stores the loaded dimensions', async () => {
|
||||
testState.pathname = '/biz/thread/post-1';
|
||||
testState.params = {
|
||||
boardIdentifier: 'biz',
|
||||
commentCid: 'post-1',
|
||||
};
|
||||
testState.mediaInfo = {
|
||||
type: 'webpage',
|
||||
url: 'https://example.com',
|
||||
};
|
||||
testState.thumbnailMediaInfo = {
|
||||
...testState.mediaInfo,
|
||||
thumbnail: 'https://cdn.example.com/thumb.jpg',
|
||||
};
|
||||
|
||||
await renderHook({
|
||||
link: 'https://example.com',
|
||||
linkHeight: 360,
|
||||
linkWidth: 640,
|
||||
thumbnailUrl: '',
|
||||
});
|
||||
|
||||
expect(testState.fetchWebpageThumbnailIfNeededMock).toHaveBeenCalledWith(testState.mediaInfo);
|
||||
expect(latestValue).toMatchObject({
|
||||
thumbnailHeight: 360,
|
||||
thumbnailWidth: 640,
|
||||
type: 'webpage',
|
||||
url: 'https://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips webpage thumbnail fetching when the route is pending but a thumbnail already exists', async () => {
|
||||
testState.pathname = '/pending/42';
|
||||
testState.params = {
|
||||
accountCommentIndex: '42',
|
||||
};
|
||||
testState.mediaInfo = {
|
||||
thumbnail: 'https://cdn.example.com/existing-thumb.jpg',
|
||||
type: 'webpage',
|
||||
url: 'https://example.com/already-thumbnailed',
|
||||
};
|
||||
|
||||
await renderHook({
|
||||
link: 'https://example.com/already-thumbnailed',
|
||||
linkHeight: 360,
|
||||
linkWidth: 640,
|
||||
thumbnailUrl: 'https://cdn.example.com/existing-thumb.jpg',
|
||||
});
|
||||
|
||||
expect(latestValue).toEqual(testState.mediaInfo);
|
||||
expect(testState.fetchWebpageThumbnailIfNeededMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
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 useIsSubplebbitOffline from '../use-is-subplebbit-offline';
|
||||
|
||||
(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(() => ({
|
||||
initializeMock: vi.fn(),
|
||||
loadingTimestamps: [0] as number[],
|
||||
requestedAddresses: undefined as string[] | undefined,
|
||||
setOfflineStateMock: vi.fn(),
|
||||
subplebbitOfflineState: {} as Record<string, { initialLoad: boolean; state?: string; updatedAt?: number; updatingState?: string }>,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (key === 'posts_last_synced_info') {
|
||||
return `posts_last_synced_info:${options?.time}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../stores/use-subplebbit-offline-store', () => ({
|
||||
default: () => ({
|
||||
initializesubplebbitOfflineState: testState.initializeMock,
|
||||
setSubplebbitOfflineState: testState.setOfflineStateMock,
|
||||
subplebbitOfflineState: testState.subplebbitOfflineState,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../stores/use-subplebbits-loading-start-timestamps-store', () => ({
|
||||
default: (addresses?: string[]) => {
|
||||
testState.requestedAddresses = addresses;
|
||||
return testState.loadingTimestamps;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/utils/time-utils', () => ({
|
||||
getFormattedTimeAgo: (timestamp: number) => `ago:${timestamp}`,
|
||||
}));
|
||||
|
||||
let latestValue: ReturnType<typeof useIsSubplebbitOffline>;
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const HookHarness = ({ subplebbit }: { subplebbit?: { address?: string; state?: string; updatedAt?: number; updatingState?: string } }) => {
|
||||
latestValue = useIsSubplebbitOffline(subplebbit as never);
|
||||
return null;
|
||||
};
|
||||
|
||||
const flushEffects = async (count = 3) => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderHook = async (subplebbit?: { address?: string; state?: string; updatedAt?: number; updatingState?: string }) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(HookHarness, { subplebbit }));
|
||||
});
|
||||
await flushEffects();
|
||||
};
|
||||
|
||||
describe('useIsSubplebbitOffline', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-01T00:00:10Z'));
|
||||
|
||||
latestValue = {
|
||||
isOffline: false,
|
||||
isOnlineStatusLoading: false,
|
||||
offlineIconClass: '',
|
||||
offlineTitle: '',
|
||||
};
|
||||
testState.loadingTimestamps = [1_704_067_200];
|
||||
testState.requestedAddresses = undefined;
|
||||
testState.subplebbitOfflineState = {};
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('initializes unseen boards and reports a loading state during the first sync window', async () => {
|
||||
await renderHook({ address: 'music.eth', state: 'updating', updatingState: 'fetching' });
|
||||
|
||||
expect(testState.requestedAddresses).toEqual(['music.eth']);
|
||||
expect(testState.initializeMock).toHaveBeenCalledWith('music.eth');
|
||||
expect(testState.setOfflineStateMock).toHaveBeenCalledWith('music.eth', {
|
||||
state: 'updating',
|
||||
updatedAt: undefined,
|
||||
updatingState: 'fetching',
|
||||
});
|
||||
expect(latestValue).toEqual({
|
||||
isOffline: false,
|
||||
isOnlineStatusLoading: true,
|
||||
offlineIconClass: 'yellowOfflineIcon',
|
||||
offlineTitle: 'downloading board...',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports boards with stale updates as offline and includes the last synced time', async () => {
|
||||
const staleUpdatedAt = 1_704_052_000;
|
||||
testState.subplebbitOfflineState = {
|
||||
'music.eth': {
|
||||
initialLoad: false,
|
||||
updatedAt: staleUpdatedAt,
|
||||
},
|
||||
};
|
||||
testState.loadingTimestamps = [1_704_067_000];
|
||||
|
||||
await renderHook({ address: 'music.eth', state: 'stopped', updatedAt: staleUpdatedAt });
|
||||
|
||||
expect(testState.initializeMock).not.toHaveBeenCalled();
|
||||
expect(latestValue).toEqual({
|
||||
isOffline: true,
|
||||
isOnlineStatusLoading: false,
|
||||
offlineIconClass: 'redOfflineIcon',
|
||||
offlineTitle: `posts_last_synced_info:ago:${staleUpdatedAt}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('marks boards without an update timestamp as offline once the loading timeout has elapsed', async () => {
|
||||
testState.subplebbitOfflineState = {
|
||||
'music.eth': {
|
||||
initialLoad: false,
|
||||
},
|
||||
};
|
||||
testState.loadingTimestamps = [1_704_067_100];
|
||||
|
||||
await renderHook({ address: 'music.eth' });
|
||||
|
||||
expect(latestValue).toEqual({
|
||||
isOffline: true,
|
||||
isOnlineStatusLoading: false,
|
||||
offlineIconClass: 'redOfflineIcon',
|
||||
offlineTitle: 'subplebbit_offline_info',
|
||||
});
|
||||
});
|
||||
|
||||
it('treats recently updated boards as online', async () => {
|
||||
const freshUpdatedAt = 1_704_067_205;
|
||||
testState.subplebbitOfflineState = {
|
||||
'music.eth': {
|
||||
initialLoad: false,
|
||||
updatedAt: freshUpdatedAt,
|
||||
},
|
||||
};
|
||||
|
||||
await renderHook({ address: 'music.eth', state: 'started', updatedAt: freshUpdatedAt });
|
||||
|
||||
expect(latestValue).toEqual({
|
||||
isOffline: false,
|
||||
isOnlineStatusLoading: false,
|
||||
offlineIconClass: '',
|
||||
offlineTitle: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 { usePostPageNumber } from '../use-post-page-number';
|
||||
|
||||
(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(() => ({
|
||||
community: { address: 'music.eth', features: {} },
|
||||
feedsOptions: {} as Record<string, unknown>,
|
||||
loadedFeeds: {} as Record<string, unknown>,
|
||||
preloadFeed: undefined as Array<{ cid: string }> | undefined,
|
||||
preloadOptions: undefined as Record<string, unknown> | undefined,
|
||||
sizes: {
|
||||
guiPostsPerPage: 2,
|
||||
paginationFeedPostsPerPage: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
|
||||
useFeed: (options: Record<string, unknown> | undefined) => {
|
||||
testState.preloadOptions = options;
|
||||
return { feed: testState.preloadFeed };
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/feeds', () => ({
|
||||
default: (selector: (state: { feedsOptions: Record<string, unknown>; loadedFeeds: Record<string, unknown> }) => unknown) =>
|
||||
selector({
|
||||
feedsOptions: testState.feedsOptions,
|
||||
loadedFeeds: testState.loadedFeeds,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../use-directories', () => ({
|
||||
useDirectoryByAddress: () => testState.community,
|
||||
}));
|
||||
|
||||
vi.mock('../use-board-feed-page-size', () => ({
|
||||
useBoardFeedPageSize: () => testState.sizes,
|
||||
}));
|
||||
|
||||
let latestValue: number | undefined;
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const HookHarness = ({ enabled = true, postCid, subplebbitAddress }: { enabled?: boolean; postCid?: string; subplebbitAddress?: string }) => {
|
||||
latestValue = usePostPageNumber({ enabled, postCid, subplebbitAddress });
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHook = (props: { enabled?: boolean; postCid?: string; subplebbitAddress?: string }) => {
|
||||
act(() => {
|
||||
root.render(createElement(HookHarness, props));
|
||||
});
|
||||
|
||||
return latestValue;
|
||||
};
|
||||
|
||||
describe('usePostPageNumber', () => {
|
||||
beforeEach(() => {
|
||||
latestValue = undefined;
|
||||
testState.community = { address: 'music.eth', features: {} };
|
||||
testState.feedsOptions = {};
|
||||
testState.loadedFeeds = {};
|
||||
testState.preloadFeed = undefined;
|
||||
testState.preloadOptions = undefined;
|
||||
testState.sizes = {
|
||||
guiPostsPerPage: 2,
|
||||
paginationFeedPostsPerPage: 20,
|
||||
};
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('returns the cached board page when the post already exists in loaded feeds', () => {
|
||||
testState.feedsOptions = {
|
||||
boardFeed: {
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
},
|
||||
};
|
||||
testState.loadedFeeds = {
|
||||
boardFeed: [{ cid: 'post-1' }, { cid: 'post-2' }, { cid: 'post-3' }],
|
||||
};
|
||||
|
||||
expect(renderHook({ postCid: 'post-3', subplebbitAddress: 'music.eth' })).toBe(2);
|
||||
expect(testState.preloadOptions).toEqual({
|
||||
postsPerPage: 20,
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to the preloaded feed when cached feeds do not contain the post yet', () => {
|
||||
testState.preloadFeed = [{ cid: 'post-1' }, { cid: 'post-2' }, { cid: 'post-3' }, { cid: 'post-4' }];
|
||||
|
||||
expect(renderHook({ postCid: 'post-4', subplebbitAddress: 'music.eth' })).toBe(2);
|
||||
expect(testState.preloadOptions).toEqual({
|
||||
postsPerPage: 20,
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
});
|
||||
});
|
||||
|
||||
it('skips resolution entirely when the hook is disabled or required inputs are missing', () => {
|
||||
testState.preloadFeed = [{ cid: 'post-1' }];
|
||||
|
||||
expect(renderHook({ enabled: false, postCid: 'post-1', subplebbitAddress: 'music.eth' })).toBeUndefined();
|
||||
expect(testState.preloadOptions).toBeUndefined();
|
||||
|
||||
expect(renderHook({ enabled: true, postCid: undefined, subplebbitAddress: 'music.eth' })).toBeUndefined();
|
||||
expect(testState.preloadOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import * as React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import useScrollToReply from '../use-scroll-to-reply';
|
||||
|
||||
(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 warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
loadMoreMock: vi.fn(),
|
||||
scrollToIndexMock: vi.fn(),
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
const HookHarness = ({
|
||||
enabled = true,
|
||||
hasMore,
|
||||
replies,
|
||||
targetReplyCid,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
hasMore: boolean;
|
||||
replies: Array<{ cid?: string | null }>;
|
||||
targetReplyCid?: string;
|
||||
}) => {
|
||||
useScrollToReply({
|
||||
enabled,
|
||||
hasMore,
|
||||
loadMore: testState.loadMoreMock,
|
||||
replies,
|
||||
targetReplyCid,
|
||||
virtuosoRef: {
|
||||
current: {
|
||||
scrollToIndex: testState.scrollToIndexMock,
|
||||
} as unknown as VirtuosoHandle,
|
||||
} as React.RefObject<VirtuosoHandle | null>,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHook = async (props: { enabled?: boolean; hasMore: boolean; replies: Array<{ cid?: string | null }>; targetReplyCid?: string }) => {
|
||||
await act(async () => {
|
||||
root.render(createElement(HookHarness, props));
|
||||
});
|
||||
};
|
||||
|
||||
describe('useScrollToReply', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
document.querySelectorAll('[data-cid]').forEach((node) => node.remove());
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('scrolls directly to the target reply when it is already loaded', async () => {
|
||||
await renderHook({
|
||||
hasMore: true,
|
||||
replies: [{ cid: 'reply-1' }, { cid: 'reply-2' }],
|
||||
targetReplyCid: 'reply-2',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350);
|
||||
});
|
||||
|
||||
expect(testState.scrollToIndexMock).toHaveBeenCalledWith({
|
||||
align: 'center',
|
||||
behavior: 'smooth',
|
||||
index: 1,
|
||||
});
|
||||
expect(testState.loadMoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('scrolls to the latest loaded reply and schedules loadMore while searching', async () => {
|
||||
await renderHook({
|
||||
hasMore: true,
|
||||
replies: [{ cid: 'reply-1' }, { cid: 'reply-2' }],
|
||||
targetReplyCid: 'reply-3',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350);
|
||||
});
|
||||
|
||||
expect(testState.scrollToIndexMock).toHaveBeenCalledWith({
|
||||
align: 'end',
|
||||
behavior: 'smooth',
|
||||
index: 1,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(testState.loadMoreMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('uses DOM scrolling once all replies are loaded and the target element exists', async () => {
|
||||
const replyElement = document.createElement('div');
|
||||
const scrollIntoViewMock = vi.fn();
|
||||
replyElement.dataset.cid = 'reply-3';
|
||||
replyElement.scrollIntoView = scrollIntoViewMock;
|
||||
document.body.appendChild(replyElement);
|
||||
|
||||
await renderHook({
|
||||
hasMore: false,
|
||||
replies: [{ cid: 'reply-1' }],
|
||||
targetReplyCid: 'reply-3',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350);
|
||||
});
|
||||
|
||||
expect(scrollIntoViewMock).toHaveBeenCalledWith({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
expect(testState.scrollToIndexMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('warns once the target reply cannot be found after all pages are loaded', async () => {
|
||||
await renderHook({
|
||||
hasMore: false,
|
||||
replies: [{ cid: 'reply-1' }],
|
||||
targetReplyCid: 'reply-404',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(350);
|
||||
});
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('[scroll-to-reply] Could not find reply with CID "reply-404" in the feed.');
|
||||
expect(testState.loadMoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user