mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(publish): retry failed comments without duplicate replies
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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 useDeleteFailedPost, { getFailedPostRetryPublishOptions } from '../use-delete-failed-post';
|
||||
import useChallengesStore from '../../stores/use-challenges-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),
|
||||
alertChallengeVerificationFailedMock: vi.fn(),
|
||||
alertMock: vi.fn(),
|
||||
deleteCommentMock: vi.fn(async (_targetComment: string | number) => undefined),
|
||||
lastPublishOptions: undefined as Record<string, any> | undefined,
|
||||
publishCommentMock: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
||||
deleteComment: (targetComment: string | number) => testState.deleteCommentMock(targetComment),
|
||||
usePublishComment: (options: Record<string, any> | undefined) => {
|
||||
testState.lastPublishOptions = options;
|
||||
return {
|
||||
abandonPublish: testState.abandonPublishMock,
|
||||
publishComment: testState.publishCommentMock,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/utils/challenge-utils', () => ({
|
||||
alertChallengeVerificationFailed: (...args: any[]) => testState.alertChallengeVerificationFailedMock(...args),
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let latestValue: ReturnType<typeof useDeleteFailedPost>;
|
||||
let root: Root;
|
||||
|
||||
const failedPost = {
|
||||
accountId: 'account-1',
|
||||
author: {
|
||||
address: '0x123',
|
||||
displayName: 'Alice',
|
||||
shortAddress: '0x1234',
|
||||
},
|
||||
cid: 'failed-cid',
|
||||
clients: { ipfs: { gateway: { state: 'failed' } } },
|
||||
communityAddress: 'music.eth',
|
||||
content: 'retry me',
|
||||
depth: 1,
|
||||
error: new Error('boom'),
|
||||
errors: [new Error('boom')],
|
||||
index: 7,
|
||||
link: 'https://example.com/file.png',
|
||||
parentCid: 'parent-1',
|
||||
postCid: 'post-1',
|
||||
publishingState: 'failed',
|
||||
quotedCids: ['quoted-1'],
|
||||
shortCommunityAddress: 'music…eth',
|
||||
spoiler: true,
|
||||
state: 'failed',
|
||||
timestamp: 1_735_689_600,
|
||||
title: 'Hello',
|
||||
};
|
||||
|
||||
const HookHarness = ({ post }: { post: typeof failedPost }) => {
|
||||
latestValue = useDeleteFailedPost(post);
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHook = (post = failedPost) => {
|
||||
act(() => {
|
||||
root.render(createElement(HookHarness, { post }));
|
||||
});
|
||||
};
|
||||
|
||||
describe('useDeleteFailedPost', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.lastPublishOptions = undefined;
|
||||
vi.stubGlobal('alert', testState.alertMock);
|
||||
useChallengesStore.setState({ challenges: [] });
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
renderHook();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('extracts retry-safe publish options from a failed comment', () => {
|
||||
expect(getFailedPostRetryPublishOptions(failedPost)).toEqual({
|
||||
author: {
|
||||
address: '0x123',
|
||||
displayName: 'Alice',
|
||||
},
|
||||
communityAddress: 'music.eth',
|
||||
content: 'retry me',
|
||||
link: 'https://example.com/file.png',
|
||||
parentCid: 'parent-1',
|
||||
postCid: 'post-1',
|
||||
quotedCids: ['quoted-1'],
|
||||
spoiler: true,
|
||||
title: 'Hello',
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the failed row and republishes with the stored publish payload', async () => {
|
||||
expect(testState.lastPublishOptions).toMatchObject({
|
||||
author: {
|
||||
address: '0x123',
|
||||
displayName: 'Alice',
|
||||
},
|
||||
communityAddress: 'music.eth',
|
||||
content: 'retry me',
|
||||
link: 'https://example.com/file.png',
|
||||
parentCid: 'parent-1',
|
||||
postCid: 'post-1',
|
||||
quotedCids: ['quoted-1'],
|
||||
spoiler: true,
|
||||
title: 'Hello',
|
||||
});
|
||||
expect(typeof testState.lastPublishOptions?.onChallenge).toBe('function');
|
||||
expect(typeof testState.lastPublishOptions?.onChallengeVerification).toBe('function');
|
||||
expect(typeof testState.lastPublishOptions?.onError).toBe('function');
|
||||
|
||||
await act(async () => {
|
||||
await latestValue.onRetryFailedPost();
|
||||
});
|
||||
|
||||
expect(testState.deleteCommentMock).toHaveBeenCalledWith('failed-cid');
|
||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('routes retry challenges through the challenge store and preserves abandon behavior', async () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -115,4 +115,33 @@ describe('useFreshReplies', () => {
|
||||
|
||||
expect(latestValue[0]?.number).toBe(28);
|
||||
});
|
||||
|
||||
it('dedupes retried local replies when a stale reply and fresh reply share index 0', () => {
|
||||
testState.replies = [
|
||||
{
|
||||
content: 'stale failed reply',
|
||||
index: 0,
|
||||
subplebbitAddress: 'music.eth',
|
||||
},
|
||||
{
|
||||
content: 'duplicate stale failed reply',
|
||||
index: 0,
|
||||
subplebbitAddress: 'music.eth',
|
||||
},
|
||||
];
|
||||
testState.accountComments = [
|
||||
{
|
||||
content: 'retried pending reply',
|
||||
index: 0,
|
||||
number: 99,
|
||||
subplebbitAddress: 'music.eth',
|
||||
},
|
||||
];
|
||||
|
||||
renderHook();
|
||||
|
||||
expect(latestValue).toHaveLength(1);
|
||||
expect(latestValue[0]).toBe(testState.accountComments[0] as never);
|
||||
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [0] });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user