mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
test: expand interface settings and account editor coverage
Added coverage for version check button states, language selector, fit-expanded-images toggle, and account editor warning gate, continue flow, save/reset, and JSON validation.
This commit is contained in:
@@ -1,13 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import AccountDataEditor from '../account-data-editor';
|
||||
|
||||
(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 DEFAULT_JSON = '{"account":{"name":"test"}}';
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
account: { id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } },
|
||||
alertMock: vi.fn(),
|
||||
buildEditableAccountJsonMock: vi.fn<(account: unknown) => string>(() => DEFAULT_JSON),
|
||||
buildSavePayloadMock: vi.fn<(parsed: { account: Record<string, unknown> }, id: string) => Record<string, unknown>>((parsed, id) => ({
|
||||
...parsed.account,
|
||||
id,
|
||||
})),
|
||||
locationState: null as { state?: { returnTo?: string } } | null,
|
||||
navigateMock: vi.fn(),
|
||||
safeParseAccountJsonMock: vi.fn<(text: string) => { account: Record<string, unknown> } | null>((text: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return parsed?.account ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
setAccountMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
@@ -15,36 +37,116 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useLocation: () => testState.locationState ?? { state: null },
|
||||
useNavigate: () => testState.navigateMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../lib/utils/account-editor-utils', () => ({
|
||||
buildEditableAccountJson: () => '{"account": {"name": "test"}}',
|
||||
safeParseAccountJson: vi.fn((text: string) => {
|
||||
try {
|
||||
const p = JSON.parse(text);
|
||||
return p?.account ? p : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
buildSavePayload: vi.fn((parsed: { account: Record<string, unknown> }, id: string) => ({ ...parsed.account, id })),
|
||||
buildEditableAccountJson: (account: unknown) => testState.buildEditableAccountJsonMock(account),
|
||||
buildSavePayload: (parsed: { account: Record<string, unknown> }, id: string) => testState.buildSavePayloadMock(parsed, id),
|
||||
safeParseAccountJson: (text: string) => testState.safeParseAccountJsonMock(text),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
|
||||
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
|
||||
setAccount: vi.fn(),
|
||||
setAccount: (payload: Record<string, unknown>) => testState.setAccountMock(payload),
|
||||
useAccount: () => testState.account,
|
||||
}));
|
||||
|
||||
vi.mock('react-ace', async () => {
|
||||
const ReactModule = await vi.importActual<typeof import('react')>('react');
|
||||
|
||||
return {
|
||||
default: ({ value, onChange }: { value: string; onChange: (nextValue: string) => void }) =>
|
||||
ReactModule.createElement('textarea', {
|
||||
'data-testid': 'ace-editor',
|
||||
onChange: (event: Event) => onChange((event.target as HTMLTextAreaElement).value),
|
||||
value,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('ace-builds/src-noconflict/mode-json', () => ({}));
|
||||
vi.mock('ace-builds/src-noconflict/theme-monokai', () => ({}));
|
||||
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
const render = (children: React.ReactNode) => {
|
||||
const flushEffects = async (count = 10) => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const waitForEditor = async () => {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
await flushEffects(2);
|
||||
if (container.querySelector('[data-testid="ace-editor"]')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
expect(container.querySelector('[data-testid="ace-editor"]')).toBeTruthy();
|
||||
};
|
||||
|
||||
const clickButton = async (label: string) => {
|
||||
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === label);
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
};
|
||||
|
||||
const changeEditorValue = async (value: string) => {
|
||||
const editor = container.querySelector<HTMLTextAreaElement>('[data-testid="ace-editor"]');
|
||||
expect(editor).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
if (editor) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
|
||||
descriptor?.set?.call(editor, value);
|
||||
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
editor.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderEditor = () => {
|
||||
act(() => {
|
||||
root.render(createElement(MemoryRouter, { initialEntries: ['/settings/account-data'] }, children));
|
||||
root.render(createElement(AccountDataEditor));
|
||||
});
|
||||
};
|
||||
|
||||
describe('AccountDataEditor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.account = { id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } };
|
||||
testState.alertMock.mockReset();
|
||||
testState.buildEditableAccountJsonMock.mockReturnValue(DEFAULT_JSON);
|
||||
testState.buildSavePayloadMock.mockImplementation((parsed: { account: Record<string, unknown> }, id: string) => ({ ...parsed.account, id }));
|
||||
testState.locationState = null;
|
||||
testState.navigateMock.mockReset();
|
||||
testState.safeParseAccountJsonMock.mockImplementation((text: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return parsed?.account ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
testState.setAccountMock.mockReset();
|
||||
vi.stubGlobal('alert', testState.alertMock);
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
@@ -53,27 +155,86 @@ describe('AccountDataEditor', () => {
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('renders warning gate initially', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
it('navigates back to the default settings route from the warning gate', async () => {
|
||||
renderEditor();
|
||||
expect(container.textContent).toContain('private_key_warning_title');
|
||||
|
||||
await clickButton('go_back');
|
||||
|
||||
expect(testState.navigateMock).toHaveBeenCalledWith('/subs/settings#account-settings');
|
||||
});
|
||||
|
||||
it('shows go_back and continue buttons in warning gate', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
const buttons = Array.from(container.querySelectorAll('button'));
|
||||
const texts = buttons.map((b) => b.textContent ?? '');
|
||||
expect(texts.some((t) => t.includes('go_back'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('continue'))).toBe(true);
|
||||
it('loads the editor after continue and respects custom return routes', async () => {
|
||||
testState.locationState = { state: { returnTo: '/custom/settings#account' } };
|
||||
|
||||
renderEditor();
|
||||
await clickButton('continue');
|
||||
|
||||
expect(container.textContent).toContain('loading_editor');
|
||||
await waitForEditor();
|
||||
|
||||
expect(container.textContent).not.toContain('loading_editor');
|
||||
expect(container.querySelector('[data-testid="ace-editor"]')).toBeTruthy();
|
||||
|
||||
await clickButton('return_to_settings');
|
||||
|
||||
expect(testState.navigateMock).toHaveBeenLastCalledWith('/custom/settings#account');
|
||||
});
|
||||
|
||||
it('does not show editor controls in warning phase', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
const buttons = Array.from(container.querySelectorAll('button'));
|
||||
const texts = buttons.map((b) => b.textContent ?? '');
|
||||
expect(texts.some((t) => t.includes('save'))).toBe(false);
|
||||
expect(texts.some((t) => t.includes('reset_changes'))).toBe(false);
|
||||
expect(texts.some((t) => t.includes('return_to_settings'))).toBe(false);
|
||||
it('resets edited text back to the account JSON snapshot', async () => {
|
||||
renderEditor();
|
||||
await clickButton('continue');
|
||||
await waitForEditor();
|
||||
|
||||
expect(container.querySelector<HTMLTextAreaElement>('[data-testid="ace-editor"]')?.value).toBe(DEFAULT_JSON);
|
||||
|
||||
await changeEditorValue('{"account":{"name":"changed"}}');
|
||||
expect(container.querySelector<HTMLTextAreaElement>('[data-testid="ace-editor"]')?.value).toBe('{"account":{"name":"changed"}}');
|
||||
|
||||
await clickButton('reset_changes');
|
||||
|
||||
expect(container.querySelector<HTMLTextAreaElement>('[data-testid="ace-editor"]')?.value).toBe(DEFAULT_JSON);
|
||||
});
|
||||
|
||||
it('alerts on invalid JSON without attempting to save', async () => {
|
||||
renderEditor();
|
||||
await clickButton('continue');
|
||||
await waitForEditor();
|
||||
await changeEditorValue('not valid json');
|
||||
await clickButton('save_changes');
|
||||
|
||||
expect(testState.alertMock).toHaveBeenCalledWith('Invalid JSON');
|
||||
expect(testState.setAccountMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves valid JSON and navigates back to settings', async () => {
|
||||
testState.setAccountMock.mockResolvedValueOnce(undefined);
|
||||
|
||||
renderEditor();
|
||||
await clickButton('continue');
|
||||
await waitForEditor();
|
||||
await changeEditorValue('{"account":{"name":"changed"}}');
|
||||
await clickButton('save_changes');
|
||||
await flushEffects();
|
||||
|
||||
expect(testState.buildSavePayloadMock).toHaveBeenCalledWith({ account: { name: 'changed' } }, 'test-id');
|
||||
expect(testState.setAccountMock).toHaveBeenCalledWith({ id: 'test-id', name: 'changed' });
|
||||
expect(testState.navigateMock).toHaveBeenCalledWith('/subs/settings#account-settings');
|
||||
});
|
||||
|
||||
it('surfaces save errors from setAccount', async () => {
|
||||
testState.setAccountMock.mockRejectedValueOnce(new Error('save failed'));
|
||||
|
||||
renderEditor();
|
||||
await clickButton('continue');
|
||||
await waitForEditor();
|
||||
await changeEditorValue('{"account":{"name":"changed"}}');
|
||||
await clickButton('save_changes');
|
||||
await flushEffects();
|
||||
|
||||
expect(testState.alertMock).toHaveBeenCalledWith('save failed');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user