mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(pending posts): show publish errors
This commit is contained in:
@@ -22,6 +22,8 @@ type TestComment = {
|
||||
edit?: {
|
||||
timestamp: number;
|
||||
};
|
||||
error?: unknown;
|
||||
errors?: unknown[];
|
||||
number?: number;
|
||||
original?: {
|
||||
content?: string;
|
||||
@@ -126,6 +128,19 @@ vi.mock('../../loading-ellipsis', () => ({
|
||||
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
|
||||
}));
|
||||
|
||||
vi.mock('../../error-display/error-display', () => ({
|
||||
default: ({ error, inline, showImmediately }: { error?: Error; inline?: boolean; showImmediately?: boolean }) =>
|
||||
createElement(
|
||||
'button',
|
||||
{
|
||||
'data-testid': 'error-display',
|
||||
'data-inline': String(Boolean(inline)),
|
||||
'data-show-immediately': String(Boolean(showImmediately)),
|
||||
},
|
||||
error?.message || String(error),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../reply-quote-preview', () => ({
|
||||
default: ({
|
||||
isOP,
|
||||
@@ -393,4 +408,20 @@ describe('CommentContent', () => {
|
||||
});
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('Publishing');
|
||||
});
|
||||
|
||||
it('renders failed unpublished comment errors through ErrorDisplay', async () => {
|
||||
testState.stateString = 'Failed';
|
||||
await renderContent({
|
||||
content: 'still pending',
|
||||
errors: [new Error('spam blocker server error')],
|
||||
postCid: 'post-2',
|
||||
state: 'failed',
|
||||
});
|
||||
|
||||
const errorDisplay = container.querySelector('[data-testid="error-display"]');
|
||||
expect(errorDisplay?.textContent).toBe('spam blocker server error');
|
||||
expect(errorDisplay?.getAttribute('data-inline')).toBe('true');
|
||||
expect(errorDisplay?.getAttribute('data-show-immediately')).toBe('true');
|
||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isPostPageView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useStateString from '../../hooks/use-state-string';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import ErrorDisplay from '../../components/error-display/error-display';
|
||||
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
||||
import Markdown from '../../components/markdown';
|
||||
import Tooltip from '../../components/tooltip';
|
||||
@@ -63,6 +64,12 @@ const useScopedCidToNumber = (cids: string[]) => {
|
||||
return cidToNumber;
|
||||
};
|
||||
|
||||
const getFailedCommentError = (comment: Comment | undefined): unknown => {
|
||||
if (comment?.state !== 'failed') return undefined;
|
||||
if (comment.error) return comment.error;
|
||||
return Array.isArray(comment.errors) ? comment.errors.find(Boolean) : undefined;
|
||||
};
|
||||
|
||||
const CommentContent = ({ comment: post, prependContent }: { comment: Comment; prependContent?: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
@@ -123,9 +130,18 @@ const CommentContent = ({ comment: post, prependContent }: { comment: Comment; p
|
||||
|
||||
const stateString = useStateString(resolvedPost);
|
||||
const hasFailedState = state === 'failed';
|
||||
const failedError = getFailedCommentError(resolvedPost);
|
||||
|
||||
const loadingString = (
|
||||
<div className={styles.stateString}>{!hasFailedState ? <LoadingEllipsis string={stateString || t('loading')} /> : stateString || capitalize(t('failed'))}</div>
|
||||
<div className={styles.stateString}>
|
||||
{failedError ? (
|
||||
<ErrorDisplay error={failedError} inline={true} showImmediately={true} />
|
||||
) : !hasFailedState ? (
|
||||
<LoadingEllipsis string={stateString || t('loading')} />
|
||||
) : (
|
||||
stateString || capitalize(t('failed'))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -102,6 +102,39 @@ describe('ErrorDisplay', () => {
|
||||
expect(container.textContent).toContain('copy failed');
|
||||
});
|
||||
|
||||
it('copies native Error instances with message, stack, and extra fields', async () => {
|
||||
testState.copyToClipboardMock.mockResolvedValue(undefined);
|
||||
const error = Object.assign(new Error('native failure'), {
|
||||
details: { status: 504 },
|
||||
});
|
||||
error.stack = 'Error: native failure\n at publish';
|
||||
|
||||
await renderDisplay(error);
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const button = container.querySelector('button');
|
||||
expect(button?.textContent).toContain('error: native failure');
|
||||
|
||||
await act(async () => {
|
||||
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(testState.copyToClipboardMock).toHaveBeenCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: 'Error',
|
||||
message: 'native failure',
|
||||
stack: 'Error: native failure\n at publish',
|
||||
details: { status: 504 },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('supports compact custom labels that copy plain string errors immediately', async () => {
|
||||
testState.copyToClipboardMock.mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -17,8 +17,20 @@ const serializeErrorForClipboard = (error: unknown): string => {
|
||||
return error;
|
||||
}
|
||||
|
||||
const serializableError =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...Object.fromEntries(Object.entries(error)),
|
||||
...('cause' in error && error.cause ? { cause: error.cause } : {}),
|
||||
}
|
||||
: error;
|
||||
|
||||
try {
|
||||
return JSON.stringify(error, null, 2);
|
||||
const serializedError = JSON.stringify(serializableError, null, 2);
|
||||
return serializedError && serializedError !== '{}' ? serializedError : String(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
@@ -54,8 +66,8 @@ const ErrorDisplay = ({ error, displayMessage, inline = false, showImmediately =
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalDisplayMessage = displayMessage || (error?.message ? `${t('error')}: ${error.message}` : typeof error === 'string' ? error : null);
|
||||
const canCopyError = !!error && (!!displayMessage || !!error?.message);
|
||||
const originalDisplayMessage = displayMessage || (error?.message ? `${t('error')}: ${error.message}` : typeof error === 'string' ? error : error ? t('error') : null);
|
||||
const canCopyError = !!error && !!originalDisplayMessage;
|
||||
|
||||
const handleMessageClick = async () => {
|
||||
if (!canCopyError || state.feedbackMessageKey) return;
|
||||
|
||||
@@ -235,6 +235,10 @@ export const Post = memo(
|
||||
prev?.postNumber === next?.postNumber &&
|
||||
prev?.replyCount === next?.replyCount &&
|
||||
prev?.updatedAt === next?.updatedAt &&
|
||||
prev?.state === next?.state &&
|
||||
prev?.publishingState === next?.publishingState &&
|
||||
prev?.error === next?.error &&
|
||||
prev?.errors === next?.errors &&
|
||||
prev?.approved === next?.approved &&
|
||||
prev?.locked === next?.locked &&
|
||||
prev?.pinned === next?.pinned &&
|
||||
|
||||
Reference in New Issue
Block a user