mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post-form): clear stale draft state on form remount (#1131)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createElement } from 'react';
|
import { createElement } from 'react';
|
||||||
import { createRoot, type Root } from 'react-dom/client';
|
import { createRoot, type Root } from 'react-dom/client';
|
||||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import PostForm, { LinkTypePreviewer } from '../post-form';
|
import PostForm, { LinkTypePreviewer } from '../post-form';
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ const testState = vi.hoisted(() => ({
|
|||||||
navigateMock: vi.fn(),
|
navigateMock: vi.fn(),
|
||||||
offlineTitle: 'offline board',
|
offlineTitle: 'offline board',
|
||||||
postIndex: undefined as number | undefined,
|
postIndex: undefined as number | undefined,
|
||||||
|
publishedPostOptions: undefined as Record<string, unknown> | undefined,
|
||||||
|
publishPostOptions: {} as Record<string, unknown>,
|
||||||
publishPostMock: vi.fn(),
|
publishPostMock: vi.fn(),
|
||||||
publishReplyMock: vi.fn(),
|
publishReplyMock: vi.fn(),
|
||||||
publishReplyError: null as string | null,
|
publishReplyError: null as string | null,
|
||||||
@@ -124,17 +126,49 @@ vi.mock('../../../hooks/use-publish-post', async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
default: ({ communityAddress }: { communityAddress?: string }) => {
|
default: ({ communityAddress }: { communityAddress?: string }) => {
|
||||||
const [publishPostOptions, setPublishPostOptionsState] = React.useState<Record<string, unknown>>(communityAddress ? { communityAddress } : {});
|
const [, forceUpdate] = React.useReducer((value: number) => value + 1, 0);
|
||||||
|
const getPublishPostOptions = React.useCallback(
|
||||||
|
() => ({
|
||||||
|
...(communityAddress ? { communityAddress } : {}),
|
||||||
|
...testState.publishPostOptions,
|
||||||
|
}),
|
||||||
|
[communityAddress],
|
||||||
|
);
|
||||||
|
const publishPost = React.useCallback(() => {
|
||||||
|
testState.publishedPostOptions = getPublishPostOptions();
|
||||||
|
return testState.publishPostMock();
|
||||||
|
}, [getPublishPostOptions]);
|
||||||
|
const resetPublishPostOptions = React.useCallback(() => {
|
||||||
|
testState.publishPostOptions = {};
|
||||||
|
testState.resetPublishPostOptionsMock();
|
||||||
|
forceUpdate();
|
||||||
|
}, []);
|
||||||
|
const setPublishPostOptions = React.useCallback(
|
||||||
|
(options: Record<string, unknown>) => {
|
||||||
|
const sanitizedOptions = Object.entries(options).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
acc[key] = value === '' ? undefined : value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
|
||||||
|
testState.setPublishPostOptionsMock(options);
|
||||||
|
testState.publishPostOptions = {
|
||||||
|
...getPublishPostOptions(),
|
||||||
|
...sanitizedOptions,
|
||||||
|
};
|
||||||
|
forceUpdate();
|
||||||
|
},
|
||||||
|
[getPublishPostOptions],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
postIndex: testState.postIndex,
|
postIndex: testState.postIndex,
|
||||||
publishPost: testState.publishPostMock,
|
publishPost,
|
||||||
publishPostOptions,
|
publishPostOptions: getPublishPostOptions(),
|
||||||
resetPublishPostOptions: testState.resetPublishPostOptionsMock,
|
resetPublishPostOptions,
|
||||||
setPublishPostOptions: (options: Record<string, unknown>) => {
|
setPublishPostOptions,
|
||||||
testState.setPublishPostOptionsMock(options);
|
|
||||||
setPublishPostOptionsState((previous) => ({ ...previous, ...options }));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -150,18 +184,21 @@ vi.mock('../../../hooks/use-publish-reply', async () => {
|
|||||||
postCid: postCid ?? cid,
|
postCid: postCid ?? cid,
|
||||||
communityAddress,
|
communityAddress,
|
||||||
});
|
});
|
||||||
|
const publishReply = React.useCallback(() => testState.publishReplyMock(), []);
|
||||||
|
const resetPublishReplyOptions = React.useCallback(() => testState.resetPublishReplyOptionsMock(), []);
|
||||||
|
const setPublishReplyOptions = React.useCallback((options: Record<string, unknown>) => {
|
||||||
|
testState.setPublishReplyOptionsMock(options);
|
||||||
|
setPublishReplyOptionsState((previous) => ({ ...previous, ...options }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isResolvingExternalQuotes: testState.isResolvingExternalQuotes,
|
isResolvingExternalQuotes: testState.isResolvingExternalQuotes,
|
||||||
publishReply: testState.publishReplyMock,
|
publishReply,
|
||||||
publishReplyError: testState.publishReplyError,
|
publishReplyError: testState.publishReplyError,
|
||||||
publishReplyStateMessage: testState.publishReplyStateMessage,
|
publishReplyStateMessage: testState.publishReplyStateMessage,
|
||||||
replyIndex: testState.replyIndex,
|
replyIndex: testState.replyIndex,
|
||||||
resetPublishReplyOptions: testState.resetPublishReplyOptionsMock,
|
resetPublishReplyOptions,
|
||||||
setPublishReplyOptions: (options: Record<string, unknown>) => {
|
setPublishReplyOptions,
|
||||||
testState.setPublishReplyOptionsMock(options);
|
|
||||||
setPublishReplyOptionsState((previous) => ({ ...previous, ...options }));
|
|
||||||
},
|
|
||||||
_publishReplyOptions: publishReplyOptions,
|
_publishReplyOptions: publishReplyOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -252,6 +289,29 @@ const renderPostForm = async (initialEntry: string) => {
|
|||||||
await flushEffects();
|
await flushEffects();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const KeyedPostForm = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
return createElement(PostForm, { key: location.pathname });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNavigablePostForm = async (initialEntry: string) => {
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
createElement(
|
||||||
|
MemoryRouter,
|
||||||
|
{ initialEntries: [initialEntry] },
|
||||||
|
createElement(
|
||||||
|
React.Fragment,
|
||||||
|
{},
|
||||||
|
createElement(Link, { to: '/biz' }, 'go_biz'),
|
||||||
|
createElement(Routes, {}, createElement(Route, { path: '/:boardIdentifier/*', element: createElement(KeyedPostForm) })),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushEffects();
|
||||||
|
};
|
||||||
|
|
||||||
const clickByText = async (scope: ParentNode, text: string, index = 0) => {
|
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;
|
const button = Array.from(scope.querySelectorAll('button')).filter((candidate) => candidate.textContent === text)[index] as HTMLButtonElement | undefined;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -259,6 +319,14 @@ const clickByText = async (scope: ParentNode, text: string, index = 0) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clickLinkByText = async (scope: ParentNode, text: string, index = 0) => {
|
||||||
|
const link = Array.from(scope.querySelectorAll('a')).filter((candidate) => candidate.textContent === text)[index] as HTMLAnchorElement | undefined;
|
||||||
|
await act(async () => {
|
||||||
|
link?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||||
|
});
|
||||||
|
await flushEffects();
|
||||||
|
};
|
||||||
|
|
||||||
const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
||||||
@@ -302,6 +370,8 @@ describe('PostForm', () => {
|
|||||||
testState.isResolvingExternalQuotes = false;
|
testState.isResolvingExternalQuotes = false;
|
||||||
testState.offlineTitle = 'offline board';
|
testState.offlineTitle = 'offline board';
|
||||||
testState.postIndex = undefined;
|
testState.postIndex = undefined;
|
||||||
|
testState.publishedPostOptions = undefined;
|
||||||
|
testState.publishPostOptions = {};
|
||||||
testState.publishReplyError = null;
|
testState.publishReplyError = null;
|
||||||
testState.publishReplyStateMessage = null;
|
testState.publishReplyStateMessage = null;
|
||||||
testState.replyIndex = undefined;
|
testState.replyIndex = undefined;
|
||||||
@@ -429,6 +499,40 @@ describe('PostForm', () => {
|
|||||||
expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ communityAddress: 'music-posting.eth' });
|
expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ communityAddress: 'music-posting.eth' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('drops stale thread content when board navigation remounts the form before a link-only post', async () => {
|
||||||
|
await renderNavigablePostForm('/mu');
|
||||||
|
await clickByText(container, 'start_new_thread');
|
||||||
|
|
||||||
|
let table = container.querySelector('table');
|
||||||
|
let textarea = table?.querySelector('textarea');
|
||||||
|
|
||||||
|
expect(table).toBeTruthy();
|
||||||
|
expect(textarea).toBeTruthy();
|
||||||
|
|
||||||
|
await dispatchInput(textarea as HTMLTextAreaElement, 'stale draft body');
|
||||||
|
expect(testState.publishPostOptions.content).toBe('stale draft body');
|
||||||
|
|
||||||
|
await clickLinkByText(container, 'go_biz');
|
||||||
|
expect(testState.resetPublishPostOptionsMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await clickByText(container, 'start_new_thread');
|
||||||
|
|
||||||
|
table = container.querySelector('table');
|
||||||
|
textarea = table?.querySelector('textarea');
|
||||||
|
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||||
|
const linkInput = textInputs[2];
|
||||||
|
|
||||||
|
expect(textarea?.value).toBe('');
|
||||||
|
expect(linkInput).toBeTruthy();
|
||||||
|
|
||||||
|
await dispatchInput(linkInput as HTMLInputElement, 'https://example.com/fresh.png');
|
||||||
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
|
|
||||||
|
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(testState.publishedPostOptions?.link).toBe('https://example.com/fresh.png');
|
||||||
|
expect(testState.publishedPostOptions?.content).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('shows the pasted file-link filename next to the upload button', async () => {
|
it('shows the pasted file-link filename next to the upload button', async () => {
|
||||||
testState.uploadedFileName = null;
|
testState.uploadedFileName = null;
|
||||||
|
|
||||||
|
|||||||
@@ -424,6 +424,17 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } =
|
||||||
usePublishReply({ cid, communityAddress, postCid });
|
usePublishReply({ cid, communityAddress, postCid });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
checkContentLength.cancel();
|
||||||
|
if (isInPostView) {
|
||||||
|
resetPublishReplyOptions();
|
||||||
|
} else {
|
||||||
|
resetPublishPostOptions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [checkContentLength, isInPostView, resetPublishPostOptions, resetPublishReplyOptions]);
|
||||||
|
|
||||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const content = e.target.value;
|
const content = e.target.value;
|
||||||
if (isInPostView) {
|
if (isInPostView) {
|
||||||
@@ -467,11 +478,10 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof replyIndex === 'number') {
|
if (typeof replyIndex === 'number') {
|
||||||
resetPublishReplyOptions();
|
|
||||||
resetFields();
|
resetFields();
|
||||||
closeForm();
|
closeForm();
|
||||||
}
|
}
|
||||||
}, [replyIndex, resetPublishReplyOptions, closeForm]);
|
}, [replyIndex, closeForm]);
|
||||||
|
|
||||||
const { isUploading, uploadedFileName, handleUpload } = useFileUpload({
|
const { isUploading, uploadedFileName, handleUpload } = useFileUpload({
|
||||||
onUploadComplete: (uploadedUrl: string) => {
|
onUploadComplete: (uploadedUrl: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user