mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(fortune): scope s5s fortune markup (#1150)
* fix(markdown): scope fortune markup to fortune boards * fix(fortune): store fortune output as bbcode * fix(fortune): keep legacy fortune rendering * fix(tests): resolve catalog button mock merge * fix(fortune): validate hidden fortune length
This commit is contained in:
@@ -337,6 +337,12 @@ const waitForOptionsValidation = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const waitForContentLengthValidation = async () => {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1020));
|
||||
});
|
||||
};
|
||||
|
||||
const clickButtonByText = async (text: string) => {
|
||||
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text);
|
||||
await act(async () => {
|
||||
@@ -713,7 +719,7 @@ describe('ReplyModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('validates unsupported options and stores fortune output in reply content', async () => {
|
||||
it('validates unsupported options and keeps fortune output out of preview state until reply publish', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = '';
|
||||
@@ -740,15 +746,38 @@ describe('ReplyModal', () => {
|
||||
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
||||
content: 'reply body<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>',
|
||||
content: 'reply body',
|
||||
});
|
||||
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: 'reply body[fortune color=#fd4d32]Excellent Luck[/fortune]',
|
||||
});
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('counts hidden fortune output in reply length validation without revealing it', async () => {
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = '';
|
||||
|
||||
await renderReplyModal('/b/thread/post-1', 'random-nsfw.bso');
|
||||
|
||||
const optionsInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea') as HTMLTextAreaElement;
|
||||
const longContent = 'x'.repeat(1930);
|
||||
|
||||
await dispatchInput(optionsInput, 'fortune');
|
||||
await dispatchInput(textarea, longContent);
|
||||
await waitForContentLengthValidation();
|
||||
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: longContent });
|
||||
expect(container.textContent).toContain('comment_field_too_long:2001');
|
||||
expect(container.textContent).not.toContain('[fortune color=');
|
||||
expect(testState.publishReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('links the unsupported sage option to its FAQ entry in reply modal', async () => {
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = '';
|
||||
@@ -784,9 +813,10 @@ describe('ReplyModal', () => {
|
||||
await clickButtonByText('post');
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
||||
content: 'silly reply<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>',
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: 'silly reply[fortune color=#fd4d32]Excellent Luck[/fortune]',
|
||||
});
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: 'silly reply' });
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||
getContentWithPostOptionState as getContentWithOptions,
|
||||
getPostOptionsDirectoryCode,
|
||||
getPostOptionsPublishContentLength,
|
||||
getPostOptionsValidationError,
|
||||
hasNonokoOption,
|
||||
isPostOptionsValidationError,
|
||||
@@ -120,8 +121,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const [bbcodePreviewContent, setBbcodePreviewContent] = useState('');
|
||||
|
||||
const checkContentLengthRef = useRef(
|
||||
debounce((content: string, t: TFunction) => {
|
||||
const length = content.trim().length;
|
||||
debounce((content: string, t: TFunction, options: string, directoryCode: string | undefined) => {
|
||||
const length = getPostOptionsPublishContentLength(content, options, directoryCode);
|
||||
if (length > 2000) {
|
||||
setError(null);
|
||||
setLengthError(`${t('error')}: ${t('comment_field_too_long', { length })}`);
|
||||
@@ -324,9 +325,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const len = textRef.current.value.length;
|
||||
lastSelectionStartRef.current = len;
|
||||
lastSelectionEndRef.current = len;
|
||||
const publishContent = getContentWithOptions(initialContent, optionsRef.current?.value || '', fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(initialContent, optionsRef.current?.value || '', fortuneEntryRef, diceRollRef, postOptionsDirectoryCode, {
|
||||
includeFortune: false,
|
||||
});
|
||||
setPublishReplyOptions({ content: publishContent });
|
||||
checkContentLengthRef.current(publishContent, t);
|
||||
checkContentLengthRef.current(publishContent, t, optionsRef.current?.value || '', postOptionsDirectoryCode);
|
||||
|
||||
const spellcheckTimeout = window.setTimeout(() => {
|
||||
if (textRef.current) {
|
||||
@@ -368,9 +371,9 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
lastSelectionStartRef.current = selectionStart;
|
||||
lastSelectionEndRef.current = selectionEnd ?? selectionStart;
|
||||
}
|
||||
const publishContent = getContentWithOptions(content, options, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(content, options, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode, { includeFortune: false });
|
||||
setPublishReplyOptions({ content: publishContent });
|
||||
checkContentLengthRef.current(publishContent, t);
|
||||
checkContentLengthRef.current(publishContent, t, options, postOptionsDirectoryCode);
|
||||
};
|
||||
|
||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -467,9 +470,11 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
lastSelectionStartRef.current = nextCursor;
|
||||
lastSelectionEndRef.current = nextCursor;
|
||||
|
||||
const publishContent = getContentWithOptions(nextValue, optionsRef.current?.value || '', fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(nextValue, optionsRef.current?.value || '', fortuneEntryRef, diceRollRef, postOptionsDirectoryCode, {
|
||||
includeFortune: false,
|
||||
});
|
||||
setPublishReplyOptions({ content: publishContent });
|
||||
checkContentLengthRef.current(publishContent, t);
|
||||
checkContentLengthRef.current(publishContent, t, optionsRef.current?.value || '', postOptionsDirectoryCode);
|
||||
}, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, postOptionsDirectoryCode, setPublishReplyOptions, t]);
|
||||
|
||||
const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({
|
||||
|
||||
Reference in New Issue
Block a user