// @vitest-environment happy-dom import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { setActiveAccount, setupJazzTestSync } from "jazz-tools/testing"; import { co, z } from "jazz-tools"; import { cleanup, fireEvent, render, screen, waitFor, } from "@testing-library/react"; import { CoValueEditor } from "../../viewer/co-value-editor"; import { setup } from "goober"; import React from "react"; describe("CoValueEditor", async () => { const account = await setupJazzTestSync(); setActiveAccount(account); beforeAll(() => { setup(React.createElement); }); afterEach(() => { cleanup(); }); describe("Initial Rendering", () => { it("should render with number value", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); expect(screen.getByLabelText("Type")).toBeDefined(); expect(screen.getByDisplayValue("number")).toBeDefined(); expect(screen.getByDisplayValue("42")).toBeDefined(); expect(screen.getByText("Cancel")).toBeDefined(); expect(screen.getByText("Submit")).toBeDefined(); }); it("should render with string value", async () => { const value = co .map({ name: z.string(), }) .create({ name: "test" }); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("string")).toBeDefined(); expect(screen.getByDisplayValue("test")).toBeDefined(); }); it("should render with boolean true value", async () => { const value = co .map({ active: z.boolean(), }) .create({ active: true }); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("true")).toBeDefined(); expect(screen.queryByRole("textbox")).toBeNull(); }); it("should render with boolean false value", async () => { const value = co .map({ active: z.boolean(), }) .create({ active: false }); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("false")).toBeDefined(); expect(screen.queryByRole("textbox")).toBeNull(); }); it("should render with null value", async () => { const value = co .map({ data: z.string().nullable(), }) .create({ data: null }); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("null")).toBeDefined(); expect(screen.queryByRole("textbox")).toBeNull(); }); it("should render with undefined value", async () => { const value = co .map({ optional: z.string().optional(), }) .create({}); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("undefined")).toBeDefined(); expect(screen.queryByRole("textbox")).toBeNull(); }); it("should render with object value", async () => { const value = co .map({ config: z.json(), }) .create({ config: { foo: "bar", num: 123 } }); const onCancel = vi.fn(); render( , ); expect(screen.getByDisplayValue("object")).toBeDefined(); const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; expect(textarea).toBeDefined(); expect(textarea.value).toBe( JSON.stringify({ foo: "bar", num: 123 }, null, 2), ); }); }); describe("Type Selection", () => { it("should show textarea when number type is selected", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); expect(screen.getByRole("textbox")).toBeDefined(); fireEvent.change(select, { target: { value: "number" } }); expect(screen.getByRole("textbox")).toBeDefined(); }); it("should show textarea when string type is selected", async () => { const value = co .map({ name: z.string(), }) .create({ name: "test" }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); expect(screen.getByRole("textbox")).toBeDefined(); fireEvent.change(select, { target: { value: "string" } }); expect(screen.getByRole("textbox")).toBeDefined(); }); it("should show textarea when object type is selected", async () => { const value = co .map({ data: z.json(), }) .create({ data: {} }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); expect(screen.getByRole("textbox")).toBeDefined(); fireEvent.change(select, { target: { value: "object" } }); expect(screen.getByRole("textbox")).toBeDefined(); }); it("should hide textarea when boolean type is selected", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "true" } }); await waitFor(() => { expect(screen.queryByRole("textbox")).toBeNull(); }); }); it("should hide textarea when null type is selected", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "null" } }); await waitFor(() => { expect(screen.queryByRole("textbox")).toBeNull(); }); }); it("should hide textarea when undefined type is selected", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "undefined" } }); await waitFor(() => { expect(screen.queryByRole("textbox")).toBeNull(); }); }); }); describe("Form Submission", () => { it("should submit number value", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); fireEvent.change(textarea, { target: { value: "100" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "count", value: 100 }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit string value", async () => { const value = co .map({ name: z.string(), }) .create({ name: "test" }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); fireEvent.change(textarea, { target: { value: "updated" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "name", value: "updated" }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit boolean true value", async () => { const value = co .map({ active: z.boolean(), }) .create({ active: false }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "true" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "active", value: true }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit boolean false value", async () => { const value = co .map({ active: z.boolean(), }) .create({ active: true }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "false" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "active", value: false }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit null value", async () => { const value = co .map({ data: z.string().nullable(), }) .create({ data: "test" }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "null" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "data", value: null }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit undefined value", async () => { const value = co .map({ optional: z.string().optional(), }) .create({ optional: "test" }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const select = screen.getByLabelText("Type"); fireEvent.change(select, { target: { value: "undefined" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "optional", value: undefined }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should submit object value", async () => { const value = co .map({ config: z.json(), }) .create({ config: {} }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); const newObject = { foo: "bar", nested: { value: 123 } }; fireEvent.change(textarea, { target: { value: JSON.stringify(newObject, null, 2) }, }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "config", value: newObject }], "private", ); expect(onCancel).toHaveBeenCalled(); }); }); it("should prevent default form submission", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const form = screen.getByRole("textbox").closest("form"); expect(form).toBeDefined(); const submitEvent = new Event("submit", { bubbles: true, cancelable: true, }); const preventDefaultSpy = vi.spyOn(submitEvent, "preventDefault"); const stopPropagationSpy = vi.spyOn(submitEvent, "stopPropagation"); if (form) { fireEvent(form, submitEvent); } expect(preventDefaultSpy).toHaveBeenCalled(); expect(stopPropagationSpy).toHaveBeenCalled(); }); }); describe("Cancel Button", () => { it("should call onCancel when cancel button is clicked", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const cancelButton = screen.getByText("Cancel"); fireEvent.click(cancelButton); expect(onCancel).toHaveBeenCalledTimes(1); }); it("should not make transaction when cancel is clicked", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const cancelButton = screen.getByText("Cancel"); fireEvent.click(cancelButton); expect(makeTransactionSpy).not.toHaveBeenCalled(); }); }); describe("Event Propagation", () => { it("should stop propagation on select click", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const select = screen.getByLabelText("Type"); const clickEvent = new MouseEvent("click", { bubbles: true }); const stopPropagationSpy = vi.spyOn(clickEvent, "stopPropagation"); fireEvent(select, clickEvent); expect(stopPropagationSpy).toHaveBeenCalled(); }); it("should stop propagation on textarea click", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); render( , ); const textarea = screen.getByRole("textbox"); const clickEvent = new MouseEvent("click", { bubbles: true }); const stopPropagationSpy = vi.spyOn(clickEvent, "stopPropagation"); fireEvent(textarea, clickEvent); expect(stopPropagationSpy).toHaveBeenCalled(); }); }); describe("Edge Cases", () => { it("should handle decimal number input", async () => { const value = co .map({ count: z.number(), }) .create({ count: 42 }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); fireEvent.change(textarea, { target: { value: "3.14" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "count", value: 3.14 }], "private", ); }); }); it("should handle empty string input", async () => { const value = co .map({ name: z.string(), }) .create({ name: "test" }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); fireEvent.change(textarea, { target: { value: "" } }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "name", value: "" }], "private", ); }); }); it("should handle complex nested object", async () => { const value = co .map({ config: z.json(), }) .create({ config: {} }); const onCancel = vi.fn(); const makeTransactionSpy = vi.spyOn( value.$jazz.raw.core, "makeTransaction", ); render( , ); const textarea = screen.getByRole("textbox"); const complexObject = { nested: { deep: { value: [1, 2, 3], items: [{ id: 1 }, { id: 2 }], }, }, }; fireEvent.change(textarea, { target: { value: JSON.stringify(complexObject, null, 2) }, }); const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { expect(makeTransactionSpy).toHaveBeenCalledWith( [{ op: "set", key: "config", value: complexObject }], "private", ); }); }); }); });