91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
import { describe, test, expect, vi, afterEach } from 'vitest';
|
|||
|
|
import React from 'react';
|
||
|
|
import { createRoot, Root } from 'react-dom/client';
|
||
|
|
import { act } from 'react-dom/test-utils';
|
||
|
|
import { CodeEditor } from './CodeEditor';
|
||
|
|
|
||
|
|
/* ---------- DOM test harness (no @testing-library/react in this repo; see
|
||
|
|
src/ui/AssetPicker.test.tsx / src/ui/Modal.test.tsx for the same
|
||
|
|
react-dom/client + react-dom/test-utils `act` pattern). ----------
|
||
|
|
|
||
|
|
CodeMirror is loaded via dynamic import() (see CodeEditor.tsx), which is
|
||
|
|
always async -- even for an already-resolved/cached module, `import()`
|
||
|
|
only settles on a later microtask. That means immediately after the
|
||
|
|
initial synchronous `act(() => root.render(...))` below, the component is
|
||
|
|
still in its 'loading' state and renders the <textarea> fallback. These
|
||
|
|
tests deliberately assert against that first-tick DOM (never awaiting
|
||
|
|
the CodeMirror promise), so they exercise exactly the fallback path a
|
||
|
|
real headless/offline environment would fall back to, deterministically
|
||
|
|
and without needing to mock @codemirror/*. */
|
||
|
|
|
||
|
|
let container: HTMLDivElement;
|
||
|
|
let root: Root;
|
||
|
|
|
||
|
|
function render(ui: React.ReactElement) {
|
||
|
|
container = document.createElement('div');
|
||
|
|
document.body.appendChild(container);
|
||
|
|
act(() => {
|
||
|
|
root = createRoot(container);
|
||
|
|
root.render(ui);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
if (container) {
|
||
|
|
act(() => { root.unmount(); });
|
||
|
|
container.remove();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
function fallbackTextarea(): HTMLTextAreaElement {
|
||
|
|
const el = container.querySelector<HTMLTextAreaElement>('[data-testid="code-editor-fallback"]');
|
||
|
|
if (!el) throw new Error('fallback textarea not found');
|
||
|
|
return el;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('CodeEditor', () => {
|
||
|
|
test('shows the textarea fallback immediately (CodeMirror loads async)', () => {
|
||
|
|
render(<CodeEditor value="<p>hi</p>" onChange={vi.fn()} />);
|
||
|
|
const textarea = fallbackTextarea();
|
||
|
|
expect(textarea.value).toBe('<p>hi</p>');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the CodeMirror mount root is hidden while the fallback is showing', () => {
|
||
|
|
render(<CodeEditor value="" onChange={vi.fn()} />);
|
||
|
|
const cmRoot = container.querySelector<HTMLDivElement>('[data-testid="code-editor-cm-root"]');
|
||
|
|
expect(cmRoot?.style.display).toBe('none');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('onChange fires with the new value when typing in the fallback textarea', () => {
|
||
|
|
const onChange = vi.fn();
|
||
|
|
render(<CodeEditor value="<p>hi</p>" onChange={onChange} />);
|
||
|
|
const textarea = fallbackTextarea();
|
||
|
|
act(() => {
|
||
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')!.set!;
|
||
|
|
setter.call(textarea, '<p>updated</p>');
|
||
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
|
});
|
||
|
|
expect(onChange).toHaveBeenCalledWith('<p>updated</p>');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('accepts a language prop without throwing (html/css/javascript/auto)', () => {
|
||
|
|
for (const language of ['html', 'css', 'javascript', 'auto'] as const) {
|
||
|
|
expect(() => render(<CodeEditor value="" onChange={vi.fn()} language={language} />)).not.toThrow();
|
||
|
|
const textarea = fallbackTextarea();
|
||
|
|
expect(textarea.dataset.language).toBe(language);
|
||
|
|
act(() => { root.unmount(); });
|
||
|
|
container.remove();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defaults to html language when none is passed', () => {
|
||
|
|
render(<CodeEditor value="" onChange={vi.fn()} />);
|
||
|
|
expect(fallbackTextarea().dataset.language).toBe('html');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('respects a custom height', () => {
|
||
|
|
render(<CodeEditor value="" onChange={vi.fn()} height={480} />);
|
||
|
|
expect((container.firstElementChild as HTMLElement).style.height).toBe('480px');
|
||
|
|
});
|
||
|
|
});
|