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?: {
|
edit?: {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
error?: unknown;
|
||||||
|
errors?: unknown[];
|
||||||
number?: number;
|
number?: number;
|
||||||
original?: {
|
original?: {
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -126,6 +128,19 @@ vi.mock('../../loading-ellipsis', () => ({
|
|||||||
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
|
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', () => ({
|
vi.mock('../../reply-quote-preview', () => ({
|
||||||
default: ({
|
default: ({
|
||||||
isOP,
|
isOP,
|
||||||
@@ -393,4 +408,20 @@ describe('CommentContent', () => {
|
|||||||
});
|
});
|
||||||
expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('Publishing');
|
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 useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import useStateString from '../../hooks/use-state-string';
|
import useStateString from '../../hooks/use-state-string';
|
||||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||||
|
import ErrorDisplay from '../../components/error-display/error-display';
|
||||||
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
||||||
import Markdown from '../../components/markdown';
|
import Markdown from '../../components/markdown';
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
@@ -63,6 +64,12 @@ const useScopedCidToNumber = (cids: string[]) => {
|
|||||||
return cidToNumber;
|
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 CommentContent = ({ comment: post, prependContent }: { comment: Comment; prependContent?: ReactNode }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -123,9 +130,18 @@ const CommentContent = ({ comment: post, prependContent }: { comment: Comment; p
|
|||||||
|
|
||||||
const stateString = useStateString(resolvedPost);
|
const stateString = useStateString(resolvedPost);
|
||||||
const hasFailedState = state === 'failed';
|
const hasFailedState = state === 'failed';
|
||||||
|
const failedError = getFailedCommentError(resolvedPost);
|
||||||
|
|
||||||
const loadingString = (
|
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 (
|
return (
|
||||||
|
|||||||
@@ -102,6 +102,39 @@ describe('ErrorDisplay', () => {
|
|||||||
expect(container.textContent).toContain('copy failed');
|
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 () => {
|
it('supports compact custom labels that copy plain string errors immediately', async () => {
|
||||||
testState.copyToClipboardMock.mockResolvedValue(undefined);
|
testState.copyToClipboardMock.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,20 @@ const serializeErrorForClipboard = (error: unknown): string => {
|
|||||||
return error;
|
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 {
|
try {
|
||||||
return JSON.stringify(error, null, 2);
|
const serializedError = JSON.stringify(serializableError, null, 2);
|
||||||
|
return serializedError && serializedError !== '{}' ? serializedError : String(error);
|
||||||
} catch {
|
} catch {
|
||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
@@ -54,8 +66,8 @@ const ErrorDisplay = ({ error, displayMessage, inline = false, showImmediately =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalDisplayMessage = displayMessage || (error?.message ? `${t('error')}: ${error.message}` : typeof error === 'string' ? error : null);
|
const originalDisplayMessage = displayMessage || (error?.message ? `${t('error')}: ${error.message}` : typeof error === 'string' ? error : error ? t('error') : null);
|
||||||
const canCopyError = !!error && (!!displayMessage || !!error?.message);
|
const canCopyError = !!error && !!originalDisplayMessage;
|
||||||
|
|
||||||
const handleMessageClick = async () => {
|
const handleMessageClick = async () => {
|
||||||
if (!canCopyError || state.feedbackMessageKey) return;
|
if (!canCopyError || state.feedbackMessageKey) return;
|
||||||
|
|||||||
@@ -235,6 +235,10 @@ export const Post = memo(
|
|||||||
prev?.postNumber === next?.postNumber &&
|
prev?.postNumber === next?.postNumber &&
|
||||||
prev?.replyCount === next?.replyCount &&
|
prev?.replyCount === next?.replyCount &&
|
||||||
prev?.updatedAt === next?.updatedAt &&
|
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?.approved === next?.approved &&
|
||||||
prev?.locked === next?.locked &&
|
prev?.locked === next?.locked &&
|
||||||
prev?.pinned === next?.pinned &&
|
prev?.pinned === next?.pinned &&
|
||||||
|
|||||||
Reference in New Issue
Block a user