/* Copyright 2026 Marimo. All rights reserved. */ import React from "react"; import { act, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "@/components/ui/tooltip"; import { GlideDataEditor } from "../glide-data-editor"; interface MockDataEditorProps { portalElementRef?: React.RefObject; onCellEdited?: (cell: [number, number], value: { data: unknown }) => void; onRowAppended?: () => void; } const captureDataEditorProps = vi.hoisted(() => vi.fn<(props: MockDataEditorProps) => void>(), ); const capturedDataEditor = { get ref() { return captureDataEditorProps.mock.lastCall?.[0].portalElementRef; }, get onCellEdited() { return captureDataEditorProps.mock.lastCall?.[0].onCellEdited; }, get onRowAppended() { return captureDataEditorProps.mock.lastCall?.[0].onRowAppended; }, }; vi.mock("@glideapps/glide-data-grid", async () => { const React = await import("react"); return { default: React.forwardRef(function MockDataEditor( props: MockDataEditorProps, _ref: React.Ref, ) { captureDataEditorProps(props); return
; }), CompactSelection: { empty: () => ({}), }, GridCellKind: { Text: "text", Number: "number", Boolean: "boolean", }, GridColumnIcon: { ProtectedColumnOverlay: "protected", }, }; }); vi.mock("@/theme/useTheme", () => ({ useTheme: () => ({ theme: "light" }), })); const editorProps = { data: [{ name: "alice" }], columnFields: new Map([["name", "string"]]) as Map, editableColumns: "all" as const, onAddEdits: vi.fn(), }; describe("GlideDataEditor portal", () => { let fullscreenElement: Element | null; beforeEach(() => { vi.clearAllMocks(); fullscreenElement = null; Object.defineProperty(document, "fullscreenElement", { get: () => fullscreenElement, configurable: true, }); document.body .querySelectorAll("[data-testid='glide-data-editor-portal']") .forEach((node) => { node.remove(); }); }); it("renders a body-level portal and passes it to DataEditor", async () => { const { container } = render( , ); const portal = screen.getByTestId("glide-data-editor-portal"); expect(portal.parentElement).toBe(document.body); expect(container.contains(portal)).toBe(false); expect(capturedDataEditor.ref).toBeDefined(); await waitFor(() => { expect(capturedDataEditor.ref?.current).toBe(portal); }); }); it("emits cell edits", () => { const onAddEdits = vi.fn(); render( , ); act(() => capturedDataEditor.onCellEdited?.([0, 0], { data: "bob" })); expect(onAddEdits).toHaveBeenCalledWith([ { rowIdx: 0, columnId: "name", value: "bob" }, ]); }); it("emits one positional edit per cell when appending a row", () => { const onAddEdits = vi.fn(); render( , ); act(() => capturedDataEditor.onRowAppended?.()); expect(onAddEdits).toHaveBeenCalledWith([ { rowIdx: 1, columnId: "name", value: "" }, ]); }); it("mounts into the fullscreen element when fullscreen is already active", () => { const fullscreenContainer = document.createElement("div"); document.body.appendChild(fullscreenContainer); fullscreenElement = fullscreenContainer; render( , ); const portal = fullscreenContainer.querySelector( "[data-testid='glide-data-editor-portal']", ); expect(portal).not.toBeNull(); expect(portal?.parentElement).toBe(fullscreenContainer); fullscreenContainer.remove(); }); it("moves the portal into the fullscreen element while fullscreen is active", async () => { render( , ); expect( document.body.querySelector("[data-testid='glide-data-editor-portal']"), ).not.toBeNull(); const fullscreenContainer = document.createElement("div"); document.body.appendChild(fullscreenContainer); act(() => { fullscreenElement = fullscreenContainer; document.dispatchEvent(new Event("fullscreenchange")); }); await waitFor(() => { const portal = fullscreenContainer.querySelector( "[data-testid='glide-data-editor-portal']", ); expect(portal?.parentElement).toBe(fullscreenContainer); }); act(() => { fullscreenElement = null; document.dispatchEvent(new Event("fullscreenchange")); }); await waitFor(() => { const portal = document.body.querySelector( "[data-testid='glide-data-editor-portal']", ); expect(portal?.parentElement).toBe(document.body); }); fullscreenContainer.remove(); }); it("unmounts cleanly while fullscreen is active", async () => { const fullscreenContainer = document.createElement("div"); document.body.appendChild(fullscreenContainer); const { unmount } = render( , ); act(() => { fullscreenElement = fullscreenContainer; document.dispatchEvent(new Event("fullscreenchange")); }); await waitFor(() => { expect( fullscreenContainer.querySelector( "[data-testid='glide-data-editor-portal']", ), ).not.toBeNull(); }); expect(() => { act(() => unmount()); }).not.toThrow(); expect( fullscreenContainer.querySelector( "[data-testid='glide-data-editor-portal']", ), ).toBeNull(); expect( document.body.querySelector("[data-testid='glide-data-editor-portal']"), ).toBeNull(); fullscreenContainer.remove(); }); });