memos/web/tests/setup.ts
johnnyjoygh 5a73d7d3e5 refactor: rebuild the editor on CodeMirror as decorated source
Rebuild the memo editor as a single CodeMirror 6 "decorated source"
editor. The document is the raw markdown, stored verbatim and styled in
place (markers stay visible), so the editor never serializes a tree back
to markdown — removing the round-trip fidelity bug class (inline images,
setext headings, ordered-list indentation, HTML entities) that the old
editor needed per-case patches for.

- MemoEditor/Editor: CodeMirror 6 + lang-markdown (GFM). Tokens, heading
  lines, #tag/@mention, and the autocomplete popover are styled in plain
  CSS (Editor/editor.css) with theme tokens, not a CSS-in-JS theme.
- Tab/Shift-Tab nest/outdent list items (marker-aware, ordered items
  renumbered so nesting is CommonMark-valid); Escape blurs; #tag
  autocomplete sourced from useTagCounts.
- Focus-mode toolbar reimplemented as markdown-text ops; active state read
  from the Lezer tree via the backend-agnostic formatting/commands catalog.
- Remove the old serialize-back-to-markdown editor (its Editor dir,
  PlainEditor, the editor-mode system) and its now-unused dependencies
  (marked, textarea-caret, and the rich-text editor packages).
- Consolidate toolbar components under Toolbar/. Read-only MemoContent
  rendering is unchanged.
2026-06-28 22:59:14 +08:00

83 lines
3.2 KiB
TypeScript

import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
// With `globals: false`, @testing-library/react does not auto-register a
// cleanup hook, so unmount rendered trees between tests explicitly. This keeps
// `screen.getByTestId` from seeing DOM from prior tests in the same file.
afterEach(() => {
cleanup();
});
// ProseMirror probes layout APIs jsdom doesn't implement.
if (typeof document !== "undefined") {
if (!document.elementFromPoint) {
document.elementFromPoint = () => null;
}
if (typeof Range !== "undefined" && !Range.prototype.getClientRects) {
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
Range.prototype.getBoundingClientRect = () =>
({ x: 0, y: 0, top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, toJSON: () => ({}) }) as DOMRect;
}
// CodeMirror probes additional layout APIs jsdom doesn't implement while measuring.
if (typeof Element !== "undefined" && !Element.prototype.getClientRects) {
Element.prototype.getClientRects = () => [] as unknown as DOMRectList;
}
}
// jsdom runs with an opaque origin (no URL set), so its built-in localStorage
// implementation throws SecurityError on every access. Install a Map-backed shim
// so any test that touches localStorage directly can call getItem/setItem/removeItem/clear
// without additional per-file setup. Tests that need special behavior (hooks.test.tsx,
// memo-editor-cache.test.ts) override this via Object.defineProperty / vi.stubGlobal.
if (typeof globalThis.localStorage === "undefined" || typeof globalThis.localStorage.clear !== "function") {
let _store = new Map<string, string>();
const localStorageShim: Storage = {
get length() {
return _store.size;
},
getItem: (key: string) => _store.get(key) ?? null,
setItem: (key: string, value: string) => _store.set(key, value),
removeItem: (key: string) => _store.delete(key),
clear: () => {
_store = new Map<string, string>();
},
key: (index: number) => Array.from(_store.keys())[index] ?? null,
};
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: localStorageShim,
});
}
// Defensive shim: `@/auth-state` constructs a BroadcastChannel at module load
// to coordinate token refreshes across tabs. jsdom historically has not shipped
// BroadcastChannel, so any future test that transitively imports auth-state
// would otherwise throw. Current tests avoid that import path on purpose, but
// installing the shim keeps authoring new tests frictionless. No-op when jsdom
// already provides an implementation.
if (typeof globalThis.BroadcastChannel === "undefined") {
class NoopBroadcastChannel {
readonly name: string;
onmessage: ((event: MessageEvent) => void) | null = null;
constructor(name: string) {
this.name = name;
}
postMessage(_data: unknown): void {}
close(): void {}
addEventListener(): void {}
removeEventListener(): void {}
dispatchEvent(): boolean {
return true;
}
}
// @ts-expect-error — attach the shim to the global scope for tests.
globalThis.BroadcastChannel = NoopBroadcastChannel;
}