Files
5chan/src/hooks/__tests__/use-publish-post.test.tsx
T
plebeius 1fbf715aa9 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.
2026-03-08 14:29:47 +08:00

119 lines
3.7 KiB
TypeScript

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 usePublishPost from '../use-publish-post';
import useChallengesStore from '../../stores/use-challenges-store';
import usePublishPostStore from '../../stores/use-publish-post-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(() => ({
abandonPublishMock: vi.fn(async () => undefined),
index: 12,
lastPublishOptions: undefined as Record<string, any> | undefined,
publishCommentMock: vi.fn(),
}));
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
usePublishComment: (options: Record<string, any>) => {
testState.lastPublishOptions = options;
return {
abandonPublish: testState.abandonPublishMock,
index: testState.index,
publishComment: testState.publishCommentMock,
};
},
}));
let container: HTMLDivElement;
let latestValue: ReturnType<typeof usePublishPost>;
let root: Root;
const HookHarness = () => {
latestValue = usePublishPost({ subplebbitAddress: 'music.eth' });
return null;
};
const renderHook = () => {
act(() => {
root.render(createElement(HookHarness));
});
};
describe('usePublishPost', () => {
beforeEach(() => {
vi.clearAllMocks();
testState.index = 12;
testState.lastPublishOptions = undefined;
useChallengesStore.setState({ challenges: [] });
usePublishPostStore.getState().resetPublishPostStore();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
renderHook();
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('sanitizes publish options into portable store state and exposes the publish action', async () => {
await act(async () => {
latestValue.setPublishPostOptions({
author: { displayName: 'Alice' },
content: '',
link: '',
spoiler: true,
title: 'Hello world',
} as never);
});
expect(latestValue.postIndex).toBe(12);
expect(latestValue.publishPost).toBe(testState.publishCommentMock);
expect(latestValue.publishPostOptions).toMatchObject({
author: { displayName: 'Alice' },
content: undefined,
link: undefined,
spoiler: true,
subplebbitAddress: 'music.eth',
title: 'Hello world',
});
expect(typeof latestValue.publishPostOptions.onChallengeVerification).toBe('function');
expect(typeof latestValue.publishPostOptions.onError).toBe('function');
});
it('routes publish challenges through the challenge store and abandons the current publish when requested', async () => {
await act(async () => {
latestValue.setPublishPostOptions({
content: 'Thread body',
} as never);
});
expect(typeof testState.lastPublishOptions?.onChallenge).toBe('function');
await act(async () => {
await testState.lastPublishOptions?.onChallenge('captcha', 'nonce');
});
const challenges = useChallengesStore.getState().challenges;
expect(challenges).toHaveLength(1);
expect(challenges[0]?.challenge).toEqual(['captcha', 'nonce']);
await act(async () => {
await useChallengesStore.getState().abandonCurrentChallenge();
});
expect(testState.abandonPublishMock).toHaveBeenCalledTimes(1);
await act(async () => {
latestValue.resetPublishPostOptions();
});
expect(latestValue.publishPostOptions).toEqual({});
});
});