2026-03-08 15:19:24 +08:00
|
|
|
import * as React from 'react';
|
|
|
|
|
import { createElement } from 'react';
|
|
|
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
|
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
|
import ReplyModal from '../reply-modal';
|
|
|
|
|
|
|
|
|
|
(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(() => ({
|
|
|
|
|
account: { author: { displayName: 'Alice' } } as { author?: { displayName?: string } },
|
|
|
|
|
closeModalMock: vi.fn(),
|
|
|
|
|
directoryByAddress: {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
features: {},
|
|
|
|
|
},
|
|
|
|
|
} as Record<string, { address: string; features?: Record<string, unknown> }>,
|
|
|
|
|
handleUploadMock: vi.fn(),
|
|
|
|
|
isMobile: false,
|
|
|
|
|
isUploading: false,
|
2026-03-08 17:10:48 +08:00
|
|
|
offlineTitle: '' as string | false,
|
2026-03-08 19:02:49 +08:00
|
|
|
offlineStates: {} as Record<string, { isOffline: boolean; isOnlineStatusLoading: boolean; offlineTitle: string | false }>,
|
2026-03-08 17:10:48 +08:00
|
|
|
offlineStatusLoading: false,
|
|
|
|
|
offlineWarningVisible: false,
|
2026-03-08 15:19:24 +08:00
|
|
|
openEmpty: false,
|
|
|
|
|
publishReplyMock: vi.fn(),
|
|
|
|
|
quoteInsertNumber: undefined as number | undefined,
|
|
|
|
|
quoteInsertRequestId: 0,
|
|
|
|
|
quoteInsertSelectedText: '',
|
|
|
|
|
replyIndex: undefined as number | undefined,
|
|
|
|
|
resetPublishReplyOptionsMock: vi.fn(),
|
2026-03-08 19:02:49 +08:00
|
|
|
resolvedSubplebbitAddress: undefined as string | undefined,
|
2026-03-08 15:19:24 +08:00
|
|
|
selectedText: 'selected text',
|
|
|
|
|
setAccountMock: vi.fn(),
|
|
|
|
|
setPublishReplyOptionsMock: vi.fn(),
|
|
|
|
|
springStartMock: vi.fn(),
|
2026-03-08 17:10:48 +08:00
|
|
|
subplebbits: {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
},
|
|
|
|
|
} as Record<string, { address: string }>,
|
2026-03-08 15:19:24 +08:00
|
|
|
showUploadControls: true,
|
|
|
|
|
uploadComplete: undefined as ((url: string) => void) | undefined,
|
|
|
|
|
uploadedFileName: null as string | null,
|
|
|
|
|
uploadMode: 'always',
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
|
|
|
Trans: ({ i18nKey, values }: { i18nKey: string; values?: Record<string, unknown> }) =>
|
|
|
|
|
createElement('span', { 'data-testid': `trans-${i18nKey}` }, `${i18nKey}:${JSON.stringify(values || {})}`),
|
|
|
|
|
useTranslation: () => ({
|
|
|
|
|
t: (key: string, options?: Record<string, unknown>) => {
|
|
|
|
|
if (!options) {
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
if (typeof options.no !== 'undefined') {
|
|
|
|
|
return `${key}:${options.no}`;
|
|
|
|
|
}
|
|
|
|
|
if (typeof options.length !== 'undefined') {
|
|
|
|
|
return `${key}:${options.length}`;
|
|
|
|
|
}
|
|
|
|
|
return `${key}:${JSON.stringify(options)}`;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-03-09 17:01:35 +08:00
|
|
|
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
2026-03-08 15:19:24 +08:00
|
|
|
setAccount: (account: unknown) => testState.setAccountMock(account),
|
|
|
|
|
useAccount: () => testState.account,
|
|
|
|
|
}));
|
|
|
|
|
|
2026-03-09 17:01:35 +08:00
|
|
|
vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/stores/subplebbits', () => ({
|
2026-03-08 17:10:48 +08:00
|
|
|
default: <T,>(selector: (state: { subplebbits: typeof testState.subplebbits }) => T) =>
|
|
|
|
|
selector({
|
|
|
|
|
subplebbits: testState.subplebbits,
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-is-subplebbit-offline', () => ({
|
2026-03-08 19:02:49 +08:00
|
|
|
default: (subplebbit?: { address?: string }) =>
|
|
|
|
|
(subplebbit?.address ? testState.offlineStates[subplebbit.address] : undefined) || {
|
|
|
|
|
isOffline: testState.offlineWarningVisible,
|
|
|
|
|
isOnlineStatusLoading: testState.offlineStatusLoading,
|
|
|
|
|
offlineTitle: testState.offlineTitle,
|
|
|
|
|
},
|
2026-03-08 15:19:24 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../stores/use-selected-text-store', () => ({
|
|
|
|
|
default: () => ({
|
|
|
|
|
selectedText: testState.selectedText,
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../stores/use-reply-modal-store', () => ({
|
|
|
|
|
default: <T,>(selector?: (state: { openEmpty: boolean; quoteInsertNumber?: number; quoteInsertRequestId: number; quoteInsertSelectedText: string }) => T) => {
|
|
|
|
|
const state = {
|
|
|
|
|
openEmpty: testState.openEmpty,
|
|
|
|
|
quoteInsertNumber: testState.quoteInsertNumber,
|
|
|
|
|
quoteInsertRequestId: testState.quoteInsertRequestId,
|
|
|
|
|
quoteInsertSelectedText: testState.quoteInsertSelectedText,
|
|
|
|
|
};
|
|
|
|
|
return selector ? selector(state) : (state as T);
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({
|
|
|
|
|
getShowUploadControls: () => testState.showUploadControls,
|
|
|
|
|
isWebRuntime: () => true,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../stores/use-media-hosting-store', () => ({
|
|
|
|
|
default: (selector: (state: { uploadMode: string }) => unknown) =>
|
|
|
|
|
selector({
|
|
|
|
|
uploadMode: testState.uploadMode,
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-directories', () => ({
|
|
|
|
|
useDirectoryByAddress: (address: string) => testState.directoryByAddress[address],
|
2026-03-08 19:02:49 +08:00
|
|
|
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-resolved-subplebbit-address', () => ({
|
|
|
|
|
useResolvedSubplebbitAddress: () => testState.resolvedSubplebbitAddress,
|
2026-03-08 15:19:24 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-publish-reply', () => ({
|
|
|
|
|
default: () => ({
|
|
|
|
|
publishReply: testState.publishReplyMock,
|
|
|
|
|
replyIndex: testState.replyIndex,
|
|
|
|
|
resetPublishReplyOptions: testState.resetPublishReplyOptionsMock,
|
|
|
|
|
setPublishReplyOptions: (options: Record<string, unknown>) => testState.setPublishReplyOptionsMock(options),
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-is-mobile', () => ({
|
|
|
|
|
default: () => testState.isMobile,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('../../../hooks/use-file-upload', () => ({
|
|
|
|
|
useFileUpload: ({ onUploadComplete }: { onUploadComplete: (url: string) => void }) => {
|
|
|
|
|
testState.uploadComplete = onUploadComplete;
|
|
|
|
|
return {
|
|
|
|
|
handleUpload: testState.handleUploadMock,
|
|
|
|
|
isUploading: testState.isUploading,
|
|
|
|
|
uploadedFileName: testState.uploadedFileName,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('lodash/debounce', () => ({
|
|
|
|
|
default: <T extends (...args: any[]) => void>(fn: T) => {
|
|
|
|
|
const wrapped = ((...args: Parameters<T>) => fn(...args)) as T & { cancel: () => void };
|
|
|
|
|
wrapped.cancel = () => undefined;
|
|
|
|
|
return wrapped;
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('@react-spring/web', async () => {
|
|
|
|
|
const React = await vi.importActual<typeof import('react')>('react');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
animated: {
|
|
|
|
|
div: React.forwardRef(({ style, ...props }: any, ref) => React.createElement('div', { ...props, ref, style: { touchAction: style?.touchAction } })),
|
|
|
|
|
},
|
|
|
|
|
useSpring: () => [
|
|
|
|
|
{
|
|
|
|
|
x: { get: () => 120 },
|
|
|
|
|
y: { get: () => 80 },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
start: testState.springStartMock,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.mock('@use-gesture/react', () => ({
|
|
|
|
|
useDrag: () => () => ({}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
let container: HTMLDivElement;
|
|
|
|
|
let root: Root;
|
|
|
|
|
|
|
|
|
|
const flushEffects = async (count = 4) => {
|
|
|
|
|
for (let i = 0; i < count; i += 1) {
|
|
|
|
|
await act(async () => {
|
|
|
|
|
await Promise.resolve();
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-08 19:02:49 +08:00
|
|
|
const renderReplyModal = async (initialEntry = '/mu/thread/post-1', subplebbitAddress = 'music-posting.eth') => {
|
2026-03-08 15:19:24 +08:00
|
|
|
await act(async () => {
|
|
|
|
|
root.render(
|
|
|
|
|
createElement(
|
|
|
|
|
MemoryRouter,
|
|
|
|
|
{ initialEntries: [initialEntry] },
|
|
|
|
|
createElement(ReplyModal, {
|
|
|
|
|
closeModal: testState.closeModalMock,
|
|
|
|
|
parentCid: 'parent-cid',
|
|
|
|
|
parentNumber: 42,
|
|
|
|
|
postCid: 'post-cid',
|
|
|
|
|
scrollY: 120,
|
|
|
|
|
showReplyModal: true,
|
2026-03-08 19:02:49 +08:00
|
|
|
subplebbitAddress,
|
2026-03-08 15:19:24 +08:00
|
|
|
threadNumber: 42,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
await flushEffects();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rerenderReplyModal = async (initialEntry = '/mu/thread/post-1') => {
|
|
|
|
|
await renderReplyModal(initialEntry);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const dispatchInput = async (element: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
|
|
|
|
await act(async () => {
|
|
|
|
|
const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
|
|
|
|
descriptor?.set?.call(element, value);
|
|
|
|
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const clickButtonByText = async (text: string) => {
|
|
|
|
|
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text);
|
|
|
|
|
await act(async () => {
|
|
|
|
|
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('ReplyModal', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
testState.account = { author: { displayName: 'Alice' } };
|
|
|
|
|
testState.closeModalMock.mockReset();
|
|
|
|
|
testState.directoryByAddress = {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
features: {},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
testState.handleUploadMock.mockReset();
|
|
|
|
|
testState.isMobile = false;
|
|
|
|
|
testState.isUploading = false;
|
2026-03-08 17:10:48 +08:00
|
|
|
testState.offlineTitle = '';
|
2026-03-08 19:02:49 +08:00
|
|
|
testState.offlineStates = {};
|
2026-03-08 17:10:48 +08:00
|
|
|
testState.offlineStatusLoading = false;
|
|
|
|
|
testState.offlineWarningVisible = false;
|
2026-03-08 15:19:24 +08:00
|
|
|
testState.openEmpty = false;
|
|
|
|
|
testState.publishReplyMock.mockReset();
|
|
|
|
|
testState.quoteInsertNumber = undefined;
|
|
|
|
|
testState.quoteInsertRequestId = 0;
|
|
|
|
|
testState.quoteInsertSelectedText = '';
|
|
|
|
|
testState.replyIndex = undefined;
|
|
|
|
|
testState.resetPublishReplyOptionsMock.mockReset();
|
2026-03-08 19:02:49 +08:00
|
|
|
testState.resolvedSubplebbitAddress = undefined;
|
2026-03-08 15:19:24 +08:00
|
|
|
testState.selectedText = 'selected text';
|
|
|
|
|
testState.setAccountMock.mockReset();
|
|
|
|
|
testState.setPublishReplyOptionsMock.mockReset();
|
|
|
|
|
testState.springStartMock.mockReset();
|
2026-03-08 17:10:48 +08:00
|
|
|
testState.subplebbits = {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-03-08 15:19:24 +08:00
|
|
|
testState.showUploadControls = true;
|
|
|
|
|
testState.uploadComplete = undefined;
|
|
|
|
|
testState.uploadedFileName = null;
|
|
|
|
|
testState.uploadMode = 'always';
|
|
|
|
|
container = document.createElement('div');
|
|
|
|
|
document.body.appendChild(container);
|
|
|
|
|
root = createRoot(container);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
act(() => root.unmount());
|
|
|
|
|
container.remove();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-08 17:10:48 +08:00
|
|
|
it('initializes quoted content, display name, upload controls, and shared offline warning on board routes', async () => {
|
|
|
|
|
testState.offlineTitle = 'posts_last_synced_info:{"time":"ago:1000"}';
|
|
|
|
|
testState.offlineWarningVisible = true;
|
2026-03-08 15:19:24 +08:00
|
|
|
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
const nameInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[0];
|
|
|
|
|
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
|
|
|
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
|
|
|
|
|
|
|
|
|
expect(nameInput?.value).toBe('Alice');
|
|
|
|
|
expect(linkInput?.getAttribute('placeholder')).toContain('Link');
|
|
|
|
|
expect(textarea?.value).toBe('>>42\nselected text');
|
|
|
|
|
expect(container.textContent).toContain('choose_file');
|
|
|
|
|
expect(container.textContent).toContain('Spoiler?');
|
|
|
|
|
expect(container.textContent).toContain('posts_last_synced_info:{"time":"ago:1000"}');
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: '>>42\nselected text' });
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-08 17:10:48 +08:00
|
|
|
it('does not render an offline warning when the shared offline hook reports the board as online', async () => {
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
expect(container.querySelector('[class*="offlineBoard"]')).toBeNull();
|
2026-03-08 19:02:49 +08:00
|
|
|
expect(container.textContent).not.toContain('subplebbit_offline_info');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prefers the resolved board entry when the modal prop address uses a different alias', async () => {
|
|
|
|
|
testState.offlineTitle = 'subplebbit_offline_info';
|
|
|
|
|
testState.offlineWarningVisible = true;
|
|
|
|
|
testState.resolvedSubplebbitAddress = 'music-posting.eth';
|
|
|
|
|
testState.subplebbits = {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
testState.offlineStates = {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
isOffline: false,
|
|
|
|
|
isOnlineStatusLoading: false,
|
|
|
|
|
offlineTitle: '',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1', 'music-posting.bso');
|
|
|
|
|
|
|
|
|
|
expect(container.querySelector('[class*="offlineBoard"]')).toBeNull();
|
2026-03-08 17:10:48 +08:00
|
|
|
expect(container.textContent).not.toContain('subplebbit_offline_info');
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-08 15:19:24 +08:00
|
|
|
it('validates empty and invalid replies, then publishes once the payload is valid', async () => {
|
|
|
|
|
testState.openEmpty = true;
|
|
|
|
|
testState.selectedText = '';
|
|
|
|
|
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
await clickButtonByText('post');
|
|
|
|
|
expect(container.textContent).toContain('error: empty_comment_alert');
|
|
|
|
|
expect(testState.publishReplyMock).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
|
|
|
|
const spoilerCheckbox = container.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
|
|
|
|
await dispatchInput(linkInput, 'not-a-url');
|
|
|
|
|
await act(async () => {
|
|
|
|
|
spoilerCheckbox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
await clickButtonByText('post');
|
|
|
|
|
|
|
|
|
|
expect(container.textContent).toContain('error: invalid_url_alert');
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ spoiler: true });
|
|
|
|
|
|
|
|
|
|
await dispatchInput(linkInput, 'https://example.com/file.png');
|
|
|
|
|
await clickButtonByText('post');
|
|
|
|
|
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: 'https://example.com/file.png' });
|
|
|
|
|
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('updates account state, applies upload completions, and closes once publishing succeeds', async () => {
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
const nameInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[0];
|
|
|
|
|
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
|
|
|
|
|
|
|
|
|
await dispatchInput(nameInput, 'Alicia');
|
|
|
|
|
expect(testState.setAccountMock).toHaveBeenCalledWith({
|
|
|
|
|
author: { displayName: 'Alicia' },
|
|
|
|
|
});
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alicia' });
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
testState.uploadComplete?.('https://cdn.example/uploaded.png');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(linkInput?.value).toBe('https://cdn.example/uploaded.png');
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ link: 'https://cdn.example/uploaded.png' });
|
|
|
|
|
|
|
|
|
|
testState.replyIndex = 3;
|
|
|
|
|
await rerenderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
expect(testState.resetPublishReplyOptionsMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(testState.closeModalMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('inserts quote requests only once and keeps the textarea content stable across rerenders', async () => {
|
|
|
|
|
testState.isMobile = true;
|
|
|
|
|
testState.openEmpty = true;
|
|
|
|
|
testState.selectedText = 'Existing line';
|
|
|
|
|
|
|
|
|
|
await renderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
|
|
|
|
expect(textarea?.value).toBe('Existing line');
|
|
|
|
|
|
|
|
|
|
testState.quoteInsertNumber = 77;
|
|
|
|
|
testState.quoteInsertRequestId = 1;
|
|
|
|
|
testState.quoteInsertSelectedText = 'Quoted line';
|
|
|
|
|
await rerenderReplyModal('/mu/thread/post-1');
|
|
|
|
|
|
|
|
|
|
expect(textarea?.value).toBe('Existing line\n>>77\nQuoted line\n');
|
|
|
|
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
|
|
|
|
content: 'Existing line\n>>77\nQuoted line\n',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await rerenderReplyModal('/mu/thread/post-1');
|
|
|
|
|
expect(textarea?.value).toBe('Existing line\n>>77\nQuoted line\n');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses file-link placeholder defaults in all view and hides board-specific warnings or spoiler controls when disabled', async () => {
|
|
|
|
|
testState.directoryByAddress = {
|
|
|
|
|
'music-posting.eth': {
|
|
|
|
|
address: 'music-posting.eth',
|
|
|
|
|
features: { noSpoilerReplies: true },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await renderReplyModal('/all/thread/post-1');
|
|
|
|
|
|
|
|
|
|
const linkInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
|
|
|
|
expect(linkInput?.getAttribute('placeholder')).toContain('Link_to_file');
|
|
|
|
|
expect(container.textContent).not.toContain('warning');
|
|
|
|
|
expect(container.textContent).not.toContain('Spoiler?');
|
|
|
|
|
});
|
|
|
|
|
});
|