test: cover board, catalog, and publish flows

Board browsing, catalog filtering, post publishing, and reply publishing had little or no honest whole-repo coverage, which let route-level regressions and duplicated form side effects slip through. This pass adds targeted tests for those user flows and fixes the duplicate `PostFormTable` mounting bug they exposed.
This commit is contained in:
plebeius
2026-03-08 14:29:47 +08:00
parent 76eb4b537e
commit 1fbf715aa9
12 changed files with 2455 additions and 37 deletions
@@ -0,0 +1,261 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import BoardsBar from '../boards-bar';
(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(() => ({
accountComment: undefined as { subplebbitAddress?: string } | undefined,
accountSubplebbitAddresses: ['music-posting.eth'] as string[],
directories: [
{ address: 'music-posting.eth', title: '/mu/ - Music' },
{ address: 'tech-posting.eth', title: '/g/ - Technology' },
] as Array<{ address: string; title?: string }>,
directoriesMetadata: { title: '/all/ - All Boards' } as { title?: string } | null,
initializeVisibilityMock: vi.fn(),
navigateMock: vi.fn(),
openBoardsBarEditModalMock: vi.fn(),
openCreateBoardModalMock: vi.fn(),
openDirectoryModalMock: vi.fn(),
resolvedSubplebbitAddress: 'music-posting.eth' as string | undefined,
showSubscriptionsInBoardsBar: true,
subscriptions: ['custom.eth'] as string[],
visibleDirectories: new Set<string>(['mu']),
}));
function useBoardsBarVisibilityStoreMock() {
return {
visibleDirectories: testState.visibleDirectories,
showSubscriptionsInBoardsBar: testState.showSubscriptionsInBoardsBar,
};
}
useBoardsBarVisibilityStoreMock.getState = () => ({
initialize: testState.initializeVisibilityMock,
});
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
return {
...actual,
useNavigate: () => testState.navigateMock,
};
});
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
useAccountComment: () => testState.accountComment,
}));
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/accounts', () => ({
default: (selector: (state: { activeAccountId: string; accounts: Record<string, { subscriptions: string[] }> }) => unknown) =>
selector({
activeAccountId: 'account-1',
accounts: {
'account-1': {
subscriptions: testState.subscriptions,
},
},
}),
}));
vi.mock('../../../hooks/use-account-subplebbit-addresses', () => ({
useAccountSubplebbitAddresses: () => testState.accountSubplebbitAddresses,
}));
vi.mock('../../../hooks/use-directories', async () => {
const actual = await vi.importActual<typeof import('../../../hooks/use-directories')>('../../../hooks/use-directories');
return {
...actual,
useDirectories: () => testState.directories,
useDirectoriesMetadata: () => testState.directoriesMetadata,
};
});
vi.mock('../../../hooks/use-resolved-subplebbit-address', () => ({
useBoardPath: (subplebbitAddress: string | undefined) => {
if (subplebbitAddress === 'music-posting.eth') return 'mu';
if (subplebbitAddress === 'tech-posting.eth') return 'g';
return subplebbitAddress;
},
useResolvedSubplebbitAddress: () => testState.resolvedSubplebbitAddress,
}));
vi.mock('../../../stores/use-create-board-modal-store', () => ({
default: () => ({
openCreateBoardModal: testState.openCreateBoardModalMock,
}),
}));
vi.mock('../../../stores/use-boards-bar-edit-modal-store', () => ({
default: () => ({
openBoardsBarEditModal: testState.openBoardsBarEditModalMock,
}),
}));
vi.mock('../../../stores/use-boards-bar-visibility-store', () => ({
default: useBoardsBarVisibilityStoreMock,
}));
vi.mock('../../../stores/use-directory-modal-store', () => ({
default: () => ({
openDirectoryModal: testState.openDirectoryModalMock,
}),
}));
vi.mock('lodash/debounce', () => ({
default: <T extends (...args: any[]) => void>(fn: T) => {
const wrapped = ((...args: Parameters<T>) => fn(...args)) as T & { cancel: () => void };
wrapped.cancel = () => undefined;
return wrapped;
},
}));
let container: HTMLDivElement;
let root: Root;
const findExactText = (text: string) => Array.from(container.querySelectorAll<HTMLElement>('*')).find((element) => element.textContent?.trim() === text);
const renderBoardsBar = async (initialEntry: string) => {
await act(async () => {
root.render(
createElement(
MemoryRouter,
{ initialEntries: [initialEntry] },
createElement(
Routes,
{},
createElement(Route, { path: '/:boardIdentifier/*', element: createElement(BoardsBar) }),
createElement(Route, { path: '/pending/:accountCommentIndex/*', element: createElement(BoardsBar) }),
createElement(Route, { path: '*', element: createElement(BoardsBar) }),
),
),
);
});
await act(async () => {
await Promise.resolve();
});
};
describe('BoardsBar', () => {
beforeEach(() => {
vi.clearAllMocks();
testState.accountComment = undefined;
testState.accountSubplebbitAddresses = ['music-posting.eth'];
testState.directories = [
{ address: 'music-posting.eth', title: '/mu/ - Music' },
{ address: 'tech-posting.eth', title: '/g/ - Technology' },
];
testState.directoriesMetadata = { title: '/all/ - All Boards' };
testState.navigateMock.mockReset();
testState.openBoardsBarEditModalMock.mockReset();
testState.openCreateBoardModalMock.mockReset();
testState.openDirectoryModalMock.mockReset();
testState.initializeVisibilityMock.mockReset();
testState.resolvedSubplebbitAddress = 'music-posting.eth';
testState.showSubscriptionsInBoardsBar = true;
testState.subscriptions = ['custom.eth'];
testState.visibleDirectories = new Set(['mu']);
Object.defineProperty(window, 'scrollY', {
configurable: true,
value: 0,
writable: true,
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('renders desktop board groups and subscription links, then opens edit/create/directory actions', async () => {
await renderBoardsBar('/mu');
expect(testState.initializeVisibilityMock).toHaveBeenCalledTimes(1);
expect(container.textContent).toContain('all');
expect(container.textContent).toContain('subs');
expect(container.textContent).toContain('mod');
expect(container.textContent).toContain('custom.eth');
expect(container.textContent).toContain('mu');
expect(container.textContent).not.toContain('g /');
await act(async () => {
findExactText('...')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(container.textContent).toContain('g');
await act(async () => {
findExactText('Edit')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
findExactText('create_board')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(testState.openBoardsBarEditModalMock).toHaveBeenCalledTimes(1);
expect(testState.openCreateBoardModalMock).toHaveBeenCalledTimes(1);
await act(async () => {
findExactText('biz')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(testState.openDirectoryModalMock).toHaveBeenCalledTimes(1);
});
it('opens the desktop search bar and submits entered board addresses', async () => {
await renderBoardsBar('/mu');
await act(async () => {
findExactText('search')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
const inputs = container.querySelectorAll<HTMLInputElement>('input[type="text"]');
const desktopSearchInput = inputs[0];
expect(desktopSearchInput).toBeTruthy();
await act(async () => {
desktopSearchInput.value = 'new-board.eth';
desktopSearchInput.form?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
});
expect(testState.navigateMock).toHaveBeenCalledWith('/new-board.eth');
});
it('keeps catalog navigation on mobile board changes and hides the navbar on downward scroll', async () => {
await renderBoardsBar('/mu/catalog');
const select = container.querySelector('select');
expect(select).toBeTruthy();
await act(async () => {
if (select) {
select.value = 'all';
select.dispatchEvent(new Event('change', { bubbles: true }));
}
});
expect(testState.navigateMock).toHaveBeenCalledWith('/all/catalog');
const mobileNav = container.querySelector<HTMLElement>('[style*="translateY"]');
expect(mobileNav?.style.transform).toBe('translateY(0)');
await act(async () => {
window.scrollY = 100;
window.dispatchEvent(new Event('scroll'));
});
expect(mobileNav?.style.transform).toBe('translateY(-23px)');
});
});
@@ -0,0 +1,456 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import PostForm, { LinkTypePreviewer } from '../post-form';
(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: {
author: { displayName: 'Alice' },
subscriptions: ['music-posting.eth'],
},
accountComment: undefined as { subplebbitAddress?: string } | undefined,
accountSubplebbitAddresses: ['mod.eth'] as string[],
comments: {} as Record<string, { deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean }>,
directories: [
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
] as Array<{ address: string; features?: Record<string, unknown>; title?: string }>,
editedComment: undefined as { deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean } | undefined,
gifFrameStatus: 'idle' as 'idle' | 'ready',
handleUploadMock: vi.fn(),
isOffline: false,
isOnlineStatusLoading: false,
navigateMock: vi.fn(),
offlineTitle: 'offline board',
postIndex: undefined as number | undefined,
publishPostMock: vi.fn(),
publishReplyMock: vi.fn(),
replyIndex: undefined as number | undefined,
resetPublishPostOptionsMock: vi.fn(),
resetPublishReplyOptionsMock: vi.fn(),
resolvedSubplebbitAddress: undefined as string | undefined,
setAccountMock: vi.fn(),
setPublishPostOptionsMock: vi.fn(),
setPublishReplyOptionsMock: vi.fn(),
showUploadControls: true,
subplebbits: {
'music-posting.eth': { address: 'music-posting.eth' },
} as Record<string, unknown>,
uploadComplete: undefined as ((uploadedUrl: string) => void) | undefined,
uploadMode: 'always',
uploadedFileName: 'picked.png' as string | null,
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
return {
...actual,
useNavigate: () => testState.navigateMock,
};
});
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
setAccount: (account: unknown) => testState.setAccountMock(account),
useAccount: () => testState.account,
useAccountComment: () => testState.accountComment,
useEditedComment: () => ({ editedComment: testState.editedComment }),
}));
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits', () => ({
default: (selector: (state: { subplebbits: typeof testState.subplebbits }) => unknown) => selector({ subplebbits: testState.subplebbits }),
}));
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits-pages', () => ({
default: (selector: (state: { comments: typeof testState.comments }) => unknown) => selector({ comments: testState.comments }),
}));
vi.mock('../../../hooks/use-account-subplebbit-addresses', () => ({
useAccountSubplebbitAddresses: () => testState.accountSubplebbitAddresses,
}));
vi.mock('../../../hooks/use-directories', () => ({
useDirectories: () => testState.directories,
useDirectoryByAddress: (address: string | undefined) => testState.directories.find((entry) => entry.address === address),
}));
vi.mock('../../../hooks/use-resolved-subplebbit-address', () => ({
useResolvedSubplebbitAddress: () => testState.resolvedSubplebbitAddress,
}));
vi.mock('../../../hooks/use-fetch-gif-first-frame', () => ({
default: () => ({
status: testState.gifFrameStatus,
}),
}));
vi.mock('../../../hooks/use-is-subplebbit-offline', () => ({
default: () => ({
isOffline: testState.isOffline,
isOnlineStatusLoading: testState.isOnlineStatusLoading,
offlineTitle: testState.offlineTitle,
}),
}));
vi.mock('../../../hooks/use-is-mobile', () => ({
default: () => false,
}));
vi.mock('../../../hooks/use-publish-post', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
default: ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
const [publishPostOptions, setPublishPostOptionsState] = React.useState<Record<string, unknown>>(subplebbitAddress ? { subplebbitAddress } : {});
return {
postIndex: testState.postIndex,
publishPost: testState.publishPostMock,
publishPostOptions,
resetPublishPostOptions: testState.resetPublishPostOptionsMock,
setPublishPostOptions: (options: Record<string, unknown>) => {
testState.setPublishPostOptionsMock(options);
setPublishPostOptionsState((previous) => ({ ...previous, ...options }));
},
};
},
};
});
vi.mock('../../../hooks/use-publish-reply', async () => {
const React = await vi.importActual<typeof import('react')>('react');
return {
default: ({ cid, postCid, subplebbitAddress }: { cid: string; postCid?: string; subplebbitAddress: string }) => {
const [publishReplyOptions, setPublishReplyOptionsState] = React.useState<Record<string, unknown>>({
parentCid: cid,
postCid: postCid ?? cid,
subplebbitAddress,
});
return {
publishReply: testState.publishReplyMock,
replyIndex: testState.replyIndex,
resetPublishReplyOptions: testState.resetPublishReplyOptionsMock,
setPublishReplyOptions: (options: Record<string, unknown>) => {
testState.setPublishReplyOptionsMock(options);
setPublishReplyOptionsState((previous) => ({ ...previous, ...options }));
},
_publishReplyOptions: publishReplyOptions,
};
},
};
});
vi.mock('../../../hooks/use-file-upload', () => ({
useFileUpload: ({ onUploadComplete }: { onUploadComplete: (uploadedUrl: string) => void }) => {
testState.uploadComplete = onUploadComplete;
return {
handleUpload: testState.handleUploadMock,
isUploading: false,
uploadedFileName: testState.uploadedFileName,
};
},
}));
vi.mock('../../../lib/utils/media-utils', () => ({
getLinkMediaInfo: (link: string) => {
if (link.endsWith('.gif')) {
return { type: 'gif', url: link };
}
if (link.endsWith('.png')) {
return { type: 'image', url: link };
}
return { type: 'link', url: link };
},
}));
vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({
getShowUploadControls: () => testState.showUploadControls,
isWebRuntime: () => true,
}));
vi.mock('../../../stores/use-media-hosting-store', () => ({
default: (selector: (state: { uploadMode: string }) => unknown) =>
selector({
uploadMode: testState.uploadMode,
}),
}));
vi.mock('lodash/debounce', () => ({
default: <T extends (...args: any[]) => void>(fn: T) => {
const wrapped = ((...args: Parameters<T>) => fn(...args)) as T & { cancel: () => void };
wrapped.cancel = () => undefined;
return wrapped;
},
}));
let container: HTMLDivElement;
let root: Root;
const flushEffects = async (count = 4) => {
for (let i = 0; i < count; i += 1) {
await act(async () => {
await Promise.resolve();
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
};
const renderPostForm = async (initialEntry: string) => {
await act(async () => {
root.render(
createElement(
MemoryRouter,
{ initialEntries: [initialEntry] },
createElement(
Routes,
{},
createElement(Route, { path: '/all/*', element: createElement(PostForm) }),
createElement(Route, { path: '/subs/*', element: createElement(PostForm) }),
createElement(Route, { path: '/mod/*', element: createElement(PostForm) }),
createElement(Route, { path: '/:boardIdentifier/thread/:commentCid/*', element: createElement(PostForm) }),
createElement(Route, { path: '/:boardIdentifier/*', element: createElement(PostForm) }),
),
),
);
});
await flushEffects();
};
const clickByText = async (scope: ParentNode, text: string, index = 0) => {
const button = Array.from(scope.querySelectorAll('button')).filter((candidate) => candidate.textContent === text)[index] as HTMLButtonElement | undefined;
await act(async () => {
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
};
const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, value: string) => {
await act(async () => {
element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
});
};
const dispatchChange = async (element: HTMLInputElement | HTMLSelectElement, value: string | boolean) => {
await act(async () => {
if (typeof value === 'boolean' && 'checked' in element) {
element.checked = value;
} else {
element.value = String(value);
}
element.dispatchEvent(new Event('change', { bubbles: true }));
});
};
describe('PostForm', () => {
beforeEach(() => {
vi.clearAllMocks();
testState.account = {
author: { displayName: 'Alice' },
subscriptions: ['music-posting.eth'],
};
testState.accountComment = undefined;
testState.accountSubplebbitAddresses = ['mod.eth'];
testState.comments = {};
testState.directories = [
{ address: 'music-posting.eth', features: {}, title: '/mu/ - Music' },
{ address: 'mod.eth', features: {}, title: '/mod/ - Moderation' },
];
testState.editedComment = undefined;
testState.gifFrameStatus = 'idle';
testState.isOffline = false;
testState.isOnlineStatusLoading = false;
testState.offlineTitle = 'offline board';
testState.postIndex = undefined;
testState.replyIndex = undefined;
testState.resolvedSubplebbitAddress = undefined;
testState.showUploadControls = true;
testState.uploadComplete = undefined;
testState.uploadMode = 'always';
testState.uploadedFileName = 'picked.png';
testState.subplebbits = {
'music-posting.eth': { address: 'music-posting.eth' },
};
testState.handleUploadMock.mockReset();
testState.navigateMock.mockReset();
testState.publishPostMock.mockReset();
testState.publishReplyMock.mockReset();
testState.resetPublishPostOptionsMock.mockReset();
testState.resetPublishReplyOptionsMock.mockReset();
testState.setAccountMock.mockReset();
testState.setPublishPostOptionsMock.mockReset();
testState.setPublishReplyOptionsMock.mockReset();
Object.defineProperty(globalThis, 'alert', {
configurable: true,
value: vi.fn(),
writable: true,
});
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('shows the closed-thread notice when the current post can no longer receive replies', async () => {
testState.comments = {
'thread-cid': {
locked: true,
postCid: 'thread-cid',
},
};
await renderPostForm('/mu/thread/thread-cid');
expect(container.textContent).toContain('thread_closed');
expect(container.textContent).toContain('may_not_reply');
});
it('opens the new-thread form, validates all-view requirements, and publishes a board post', async () => {
await renderPostForm('/all');
await clickByText(container, 'start_new_thread');
const table = container.querySelector('table');
expect(table).toBeTruthy();
await clickByText(table as HTMLTableElement, 'choose_file');
expect(testState.handleUploadMock).toHaveBeenCalledTimes(1);
await clickByText(table as HTMLTableElement, 'post');
expect(globalThis.alert).toHaveBeenCalledWith('empty_comment_alert');
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
const nameInput = textInputs[0];
const subjectInput = textInputs[1];
const linkInput = textInputs[2];
const textarea = table?.querySelector('textarea');
const select = table?.querySelector('select');
expect(nameInput).toBeTruthy();
expect(subjectInput).toBeTruthy();
expect(linkInput).toBeTruthy();
expect(textarea).toBeTruthy();
expect(select).toBeTruthy();
(globalThis.alert as ReturnType<typeof vi.fn>).mockClear();
await dispatchInput(linkInput as HTMLInputElement, 'not-a-url');
await clickByText(table as HTMLTableElement, 'post');
expect(globalThis.alert).toHaveBeenCalledWith('invalid_url_alert');
(globalThis.alert as ReturnType<typeof vi.fn>).mockClear();
await dispatchInput(linkInput as HTMLInputElement, '');
await dispatchInput(textarea as HTMLTextAreaElement, 'A valid body');
await clickByText(table as HTMLTableElement, 'post');
expect(globalThis.alert).toHaveBeenCalledWith('no_board_selected_warning');
await dispatchChange(select as HTMLSelectElement, 'music-posting.eth');
await dispatchInput(nameInput as HTMLInputElement, 'Alice Cooper');
await dispatchInput(subjectInput as HTMLInputElement, 'A thread');
const spoilerToggle = table?.querySelector<HTMLInputElement>('input[type="checkbox"]');
if (spoilerToggle) {
await dispatchChange(spoilerToggle, true);
}
(globalThis.alert as ReturnType<typeof vi.fn>).mockClear();
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ subplebbitAddress: 'music-posting.eth' });
});
it('redirects to the pending route when a post publish index is already available on mount', async () => {
testState.postIndex = 7;
testState.resolvedSubplebbitAddress = 'music-posting.eth';
await renderPostForm('/mu');
await clickByText(container, 'start_new_thread');
await flushEffects();
expect(testState.resetPublishPostOptionsMock).toHaveBeenCalledTimes(1);
expect(testState.navigateMock).toHaveBeenCalledWith('/pending/7');
});
it('resets the reply form after a completed reply publish', async () => {
testState.comments = {
'thread-cid': {
postCid: 'thread-cid',
},
};
testState.replyIndex = 4;
testState.resolvedSubplebbitAddress = 'music-posting.eth';
await renderPostForm('/mu/thread/thread-cid');
await clickByText(container, 'post_a_reply');
await flushEffects();
expect(testState.resetPublishReplyOptionsMock).toHaveBeenCalledTimes(1);
expect(container.querySelector('table')).toBeNull();
});
it('publishes replies from the open reply form', async () => {
testState.comments = {
'thread-cid': {
postCid: 'thread-cid',
},
};
testState.isOffline = true;
testState.resolvedSubplebbitAddress = 'music-posting.eth';
await renderPostForm('/mu/thread/thread-cid');
await clickByText(container, 'post_a_reply');
const table = container.querySelector('table');
const textarea = table?.querySelector('textarea');
expect(table).toBeTruthy();
expect(textarea).toBeTruthy();
expect(container.textContent).toContain('offline board');
await dispatchInput(textarea as HTMLTextAreaElement, 'Reply body');
await clickByText(table as HTMLTableElement, 'post');
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
});
});
describe('LinkTypePreviewer', () => {
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('describes gif previews and invalid links for the post form link helper', async () => {
testState.gifFrameStatus = 'ready';
await act(async () => {
root.render(createElement(LinkTypePreviewer, { link: 'https://example.com/file.gif' }));
});
expect(container.textContent).toBe('animated_gif');
await act(async () => {
root.render(createElement(LinkTypePreviewer, { link: 'not-a-url' }));
});
expect(container.textContent).toBe('invalid_url');
});
});
+33 -26
View File
@@ -10,6 +10,7 @@ import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { useAccountSubplebbitAddresses } from '../../hooks/use-account-subplebbit-addresses';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
import useIsMobile from '../../hooks/use-is-mobile';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
@@ -513,6 +514,7 @@ const PostForm = () => {
const isInModQueueView = isModQueueView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInCatalogView = isCatalogView(location.pathname, params);
const isMobile = useIsMobile();
const commentCid = params?.commentCid;
const post = useSubplebbitsPagesStore((state) => state.comments[commentCid as string]);
@@ -532,32 +534,12 @@ const PostForm = () => {
const resolvedAddress = useResolvedSubplebbitAddress();
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
return (
<>
<div className={styles.postFormDesktop}>
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{isInModQueueView ? (
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
) : isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
<br />
{t('may_not_reply')}
</div>
) : !showForm ? (
<div>
[
<button className='button' onClick={() => setShowForm(true)}>
{isInPostView ? t('post_a_reply') : t('start_new_thread')}
</button>
]
</div>
) : (
<PostFormTable closeForm={() => setShowForm(false)} postCid={postCid} />
)}
</div>
const shouldShowOfflineAlert = !(isInAllView || isInSubscriptionsView || isInModView) && showForm;
if (isMobile) {
return (
<div className={styles.postFormMobile}>
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{shouldShowOfflineAlert && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{isInModQueueView ? (
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
) : isThreadClosed ? (
@@ -576,7 +558,32 @@ const PostForm = () => {
)}
{isInCatalogView && <hr />}
</div>
</>
);
}
return (
<div className={styles.postFormDesktop}>
{shouldShowOfflineAlert && <OfflineAlert subplebbitAddress={subplebbitAddress} />}
{isInModQueueView ? (
<div className={styles.modQueueTitle}>{t('moderation_queue')}</div>
) : isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
<br />
{t('may_not_reply')}
</div>
) : !showForm ? (
<div>
[
<button className='button' onClick={() => setShowForm(true)}>
{isInPostView ? t('post_a_reply') : t('start_new_thread')}
</button>
]
</div>
) : (
<PostFormTable closeForm={() => setShowForm(false)} postCid={postCid} />
)}
</div>
);
};