diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index 1b931926..1b67ddf6 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -50,8 +50,10 @@ export default function CodeBlockView(props: NodeViewProps) { {/* #146: the editable
(contentDOM) MUST come first in the DOM.
With the non-editable menu rendered before it, the browser's click
hit-testing snapped the caret up one line. Render content first; the
- menu follows and is positioned as a top-right overlay via CSS (the
- transclusion pattern), so it no longer sits in-flow above the code. */}
+ menu is rendered after it and lifted back above visually via flex
+ `order: -1` (the `.codeBlock` wrapper is a flex column — see
+ code-block.module.css). It stays fully in flow as a full-width row
+ above the code: no overlay/absolute positioning. */}
({
+ NodeViewWrapper: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ // Mirror the real contentDOM marker so the guard matches production output.
+ NodeViewContent: (props: any) => ,
+}));
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
+}));
+
+// footnote-definition-view reads a cached number from the numbering plugin;
+// stub it so we don't need a live ProseMirror state.
+vi.mock("@docmost/editor-ext", () => ({
+ getFootnoteNumber: () => 1,
+}));
+
+import FootnotesListView from "./footnotes-list-view";
+import FootnoteDefinitionView from "./footnote-definition-view";
+
+// Minimal NodeViewProps stub: definition view only touches node.attrs.id and
+// editor.state (the latter unused once getFootnoteNumber is mocked).
+const props = {
+ node: { attrs: { id: "fn-1" }, textContent: "" },
+ editor: { state: {}, isEditable: true, commands: {} },
+ getPos: () => 0,
+ updateAttributes: () => {},
+ deleteNode: () => {},
+} as any;
+
+const cases: Array<{ name: string; ui: React.ReactElement }> = [
+ { name: "FootnotesListView", ui: },
+ { name: "FootnoteDefinitionView", ui: },
+];
+
+describe("#146 footnote NodeView DOM-order invariant", () => {
+ it.each(cases)(
+ "$name renders contentDOM as the first child",
+ ({ ui }) => {
+ const { getByTestId } = render(ui);
+ const wrapper = getByTestId("nvw");
+
+ const firstEl = wrapper.firstElementChild;
+ expect(firstEl).not.toBeNull();
+ // The editable content must be physically first.
+ expect(firstEl?.hasAttribute("data-node-view-content")).toBe(true);
+ },
+ );
+
+ it.each(cases)(
+ "$name has no contentEditable=false chrome BEFORE the content",
+ ({ ui }) => {
+ const { getByTestId } = render(ui);
+ const wrapper = getByTestId("nvw");
+
+ const content = wrapper.querySelector("[data-node-view-content]")!;
+ const nonEditable = wrapper.querySelectorAll('[contenteditable="false"]');
+ expect(nonEditable.length).toBeGreaterThan(0); // chrome exists...
+
+ for (const el of Array.from(nonEditable)) {
+ // ...but every non-editable element must come AFTER the content node.
+ const pos = content.compareDocumentPosition(el);
+ expect(pos & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ }
+ },
+ );
+});