mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -0,0 +1,118 @@
|
||||
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({});
|
||||
});
|
||||
});
|
||||
@@ -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 usePublishReply from '../use-publish-reply';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
import usePostNumberStore from '../../stores/use-post-number-store';
|
||||
import usePublishReplyStore from '../../stores/use-publish-reply-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: 7,
|
||||
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 usePublishReply>;
|
||||
let root: Root;
|
||||
|
||||
const HookHarness = () => {
|
||||
latestValue = usePublishReply({ cid: 'parent-cid', subplebbitAddress: 'music.eth' });
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHook = () => {
|
||||
act(() => {
|
||||
root.render(createElement(HookHarness));
|
||||
});
|
||||
};
|
||||
|
||||
describe('usePublishReply', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.index = 7;
|
||||
testState.lastPublishOptions = undefined;
|
||||
useChallengesStore.setState({ challenges: [] });
|
||||
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } } });
|
||||
usePublishReplyStore.setState({
|
||||
author: {},
|
||||
content: {},
|
||||
displayName: {},
|
||||
link: {},
|
||||
publishCommentOptions: {},
|
||||
spoiler: {},
|
||||
});
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderHook();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('builds reply publish options with derived quoted cids and exposes the publish action', async () => {
|
||||
await act(async () => {
|
||||
latestValue.setPublishReplyOptions({
|
||||
author: { displayName: 'Bob' },
|
||||
content: 'Replying to >>12',
|
||||
link: '',
|
||||
spoiler: true,
|
||||
} as never);
|
||||
});
|
||||
|
||||
expect(latestValue.replyIndex).toBe(7);
|
||||
expect(latestValue.publishReply).toBe(testState.publishCommentMock);
|
||||
expect(testState.lastPublishOptions).toMatchObject({
|
||||
author: { displayName: 'Bob' },
|
||||
content: 'Replying to >>12',
|
||||
link: undefined,
|
||||
parentCid: 'parent-cid',
|
||||
postCid: 'parent-cid',
|
||||
quotedCids: ['quoted-cid'],
|
||||
spoiler: true,
|
||||
subplebbitAddress: 'music.eth',
|
||||
});
|
||||
});
|
||||
|
||||
it('queues reply challenges and clears the scoped reply store on reset', async () => {
|
||||
await act(async () => {
|
||||
latestValue.setPublishReplyOptions({
|
||||
content: 'Body',
|
||||
} as never);
|
||||
});
|
||||
|
||||
expect(typeof testState.lastPublishOptions?.onChallenge).toBe('function');
|
||||
|
||||
await act(async () => {
|
||||
await testState.lastPublishOptions?.onChallenge('captcha');
|
||||
});
|
||||
|
||||
expect(useChallengesStore.getState().challenges).toHaveLength(1);
|
||||
|
||||
await act(async () => {
|
||||
await useChallengesStore.getState().abandonCurrentChallenge();
|
||||
});
|
||||
|
||||
expect(testState.abandonPublishMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
latestValue.resetPublishReplyOptions();
|
||||
});
|
||||
|
||||
expect(usePublishReplyStore.getState().publishCommentOptions['parent-cid']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user