/* Copyright 2026 Marimo. All rights reserved. */
import { describe, expect, it } from "vitest";
import { getCopyValue } from "../json-output/formatting";
describe("getCopyValue", () => {
it("should handle strings without MIME prefixes", () => {
const value = "simple string";
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`""simple string""`);
});
it("should handle strings with MIME prefixes", () => {
const value = "text/plain:Hello, World!";
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`""Hello, World!""`);
});
it("should handle booleans", () => {
expect(getCopyValue(true)).toMatchInlineSnapshot(`"True"`);
expect(getCopyValue(false)).toMatchInlineSnapshot(`"False"`);
});
it("should handle null and undefined", () => {
expect(getCopyValue(null)).toMatchInlineSnapshot(`"None"`);
expect(getCopyValue(undefined)).toMatchInlineSnapshot(`"None"`);
});
it("should handle arrays", () => {
const value = ["text/plain:Hello", true, null];
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`
"[
"Hello",
True,
None
]"
`);
});
it("should handle objects", () => {
const value = {
key1: "text/plain:Hello",
key2: false,
key3: null,
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"key1": "Hello",
"key2": False,
"key3": None
}"
`,
);
});
it("should handle a string called true and None", () => {
const value = {
true: "true",
None: "none",
null: "null",
sentence: "something true none null something",
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"true": "true",
"None": "none",
"null": "null",
"sentence": "something true none null something"
}"
`,
);
});
it("should handle nested objects", () => {
const value = {
key1: {
nestedKey1: "text/plain:Nested Hello",
nestedKey2: true,
},
key2: false,
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"key1": {
"nestedKey1": "Nested Hello",
"nestedKey2": True
},
"key2": False
}"
`,
);
});
it("should handle nested arrays", () => {
const value = ["text/plain:Hello", [true, null, "text/plain:World"]];
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`
"[
"Hello",
[
True,
None,
"World"
]
]"
`);
});
it("should handle empty objects", () => {
const value = {};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"{}"`);
});
it("should handle empty arrays", () => {
const value: string[] = [];
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"[]"`);
});
it("should handle numbers", () => {
const value = 42;
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"42"`);
});
it("should handle special characters in strings", () => {
const value = "text/plain:Hello, \nWorld!";
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`""Hello, \\nWorld!""`);
});
it("should handle mixed types in arrays", () => {
const value = [42, "text/plain:Hello", true, null];
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`
"[
42,
"Hello",
True,
None
]"
`);
});
it("should handle mixed types in objects", () => {
const value = {
key1: 42,
key2: "text/plain:Hello",
key3: true,
key4: null,
key5: "text/plain+float:1.23",
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"key1": 42,
"key2": "Hello",
"key3": True,
"key4": None,
"key5": 1.23
}"
`,
);
});
it("should handle sets", () => {
const value = "text/plain+set:[1,2,3]";
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"{1, 2, 3}"`);
});
it("should handle empty set", () => {
expect(getCopyValue("text/plain+set:[]")).toMatchInlineSnapshot(`"set()"`);
});
it("should handle frozenset values", () => {
expect(getCopyValue("text/plain+frozenset:[1,2]")).toMatchInlineSnapshot(
`"frozenset({1, 2})"`,
);
expect(getCopyValue("text/plain+frozenset:[]")).toMatchInlineSnapshot(
`"frozenset()"`,
);
});
it("should handle sets in mixed types", () => {
const value = {
key1: 42,
key2: "text/plain+set:[1,2,3]",
key3: true,
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"key1": 42,
"key2": {1, 2, 3},
"key3": True
}"
`,
);
});
it("should handle tuples", () => {
const value = "text/plain+tuple:[1,2,3]";
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"(1, 2, 3)"`);
});
it("should handle tuples in mixed types", () => {
const value = {
key1: 42,
key2: "text/plain+tuple:[1,2,3]",
key3: true,
};
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(
`
"{
"key1": 42,
"key2": (1, 2, 3),
"key3": True
}"
`,
);
});
it("should handle bigint", () => {
const bigint = String(BigInt(2 ** 64));
const value = `text/plain+bigint:${bigint}`;
const result = getCopyValue(value);
expect(result).toMatchInlineSnapshot(`"18446744073709551616"`);
const nestedBigInt = {
key1: bigint,
key2: `text/plain+bigint:${bigint}`,
key3: true,
};
const nestedResult = getCopyValue(nestedBigInt);
expect(nestedResult).toMatchInlineSnapshot(
`
"{
"key1": "18446744073709551616",
"key2": 18446744073709551616,
"key3": True
}"
`,
);
const bigintRaw = BigInt(2 ** 64);
const bigintRawResult = getCopyValue(bigintRaw);
expect(bigintRawResult).toMatchInlineSnapshot(`"18446744073709551616"`);
const nestedBigIntRaw = {
key1: bigintRaw,
key2: `text/plain+bigint:${bigintRaw}`,
key3: true,
};
const nestedBigIntRawResult = getCopyValue(nestedBigIntRaw);
expect(nestedBigIntRawResult).toMatchInlineSnapshot(
`
"{
"key1": 18446744073709551616,
"key2": 18446744073709551616,
"key3": True
}"
`,
);
});
});
describe("getCopyValue with encoded non-string keys", () => {
it("decodes int keys unquoted", () => {
const value = { "text/plain+int:2": "no", "2": "oh" };
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
"2": "oh",
2: "no"
}"
`);
});
it("decodes large int keys unquoted (no BigInt precision concern)", () => {
const value = { "text/plain+int:18446744073709551616": "v" };
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
18446744073709551616: "v"
}"
`);
});
it("decodes float, bool, None, tuple, frozenset keys", () => {
const value = {
"text/plain+float:2.5": "f",
"text/plain+bool:True": "t",
"text/plain+bool:False": "b",
"text/plain+none:": "n",
"text/plain+tuple:[1, 2]": "tup",
"text/plain+frozenset:[3, 4]": "fs",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
2.5: "f",
True: "t",
False: "b",
None: "n",
(1, 2): "tup",
frozenset({3, 4}): "fs"
}"
`);
});
it("emits 1-element tuple keys with a trailing comma (Python syntax)", () => {
const value = {
"text/plain+tuple:[1]": "one",
"text/plain+tuple:[]": "empty",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
(1,): "one",
(): "empty"
}"
`);
});
it("emits empty frozenset keys as `frozenset()` not `frozenset({})`", () => {
const value = {
"text/plain+frozenset:[]": "empty",
"text/plain+frozenset:[1]": "single",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
frozenset(): "empty",
frozenset({1}): "single"
}"
`);
});
it("decodes NaN/Inf float keys to valid Python literals", () => {
const value = {
"text/plain+float:nan": "n",
"text/plain+float:inf": "p",
"text/plain+float:-inf": "m",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
float('nan'): "n",
float('inf'): "p",
-float('inf'): "m"
}"
`);
});
it("parses tuple/frozenset payloads containing bare NaN/Infinity", () => {
const value = {
"text/plain+tuple:[NaN]": "tn",
"text/plain+tuple:[Infinity, -Infinity]": "ti",
k: "text/plain+frozenset:[Infinity, 1]",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
(float('nan'),): "tn",
(float('inf'), -float('inf')): "ti",
"k": frozenset({float('inf'), 1})
}"
`);
});
it("falls back to the raw payload for malformed tuple/frozenset", () => {
const value = {
"text/plain+tuple:not a json list": "t",
k: "text/plain+frozenset:also broken",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
not a json list: "t",
"k": also broken
}"
`);
});
it("unescapes string keys that looked encoded", () => {
const value = {
"text/plain+str:text/plain+int:2": "hello",
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
"text/plain+int:2": "hello"
}"
`);
});
it("decodes keys at every nesting level", () => {
const value = {
outer: {
"text/plain+int:1": "inner",
"text/plain+tuple:[2, 3]": "tup",
},
};
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
"outer": {
1: "inner",
(2, 3): "tup"
}
}"
`);
});
it("leaves plain string keys untouched", () => {
const value = { foo: 1, bar: 2 };
expect(getCopyValue(value)).toMatchInlineSnapshot(`
"{
"foo": 1,
"bar": 2
}"
`);
});
});
describe("getCopyValue with application/ mimetypes", () => {
it("should strip application/ mimetype prefix from leaf data", () => {
expect(getCopyValue("application/json:{data}")).toBe('"{data}"');
expect(getCopyValue("application/custom:some-data")).toBe('"some-data"');
expect(getCopyValue("application/vnd.marimo+error:error")).toBe('"error"');
});
it("should handle application/ mimetypes in mixed objects", () => {
const value = {
appMime: "application/custom:data",
plainText: "text/plain:hello",
number: 42,
};
const result = getCopyValue(value);
expect(result).toContain('"appMime": "data"');
expect(result).toContain('"plainText": "hello"');
expect(result).toContain('"number": 42');
});
});
describe("Python copy fidelity", () => {
it.each([
"True",
"\u0000show_more\u000042|$",
'quotes " backslash \\ newline\n',
])("preserves literal text %j", (value) => {
expect(getCopyValue(value)).toBe(JSON.stringify(value));
});
it("preserves keys which used to collide after decoding", () => {
const data = JSON.parse(
'{"__proto__":1,"2":2,"text/plain+int:2":3}',
);
expect(getCopyValue(data)).toBe(
'{\n "__proto__": 1,\n "2": 2,\n 2: 3\n}',
);
});
it.each([
["text/plain+tuple:[42]", "(42,)"],
["text/plain+tuple:[]", "()"],
[
'text/plain+tuple:["quote\\\"","back\\\\slash",true,null]',
'("quote\\\"", "back\\\\slash", True, None)',
],
[
'text/plain+set:[true,null,"text/plain:literal"]',
'{True, None, "text/plain:literal"}',
],
['text/plain+tuple:["text/plain+int:2"]', '("text/plain+int:2",)'],
["text/plain+float:nan", "float('nan')"],
["text/plain+float:inf", "float('inf')"],
["text/plain+float:-inf", "-float('inf')"],
["text/plain+float:1.0", "1.0"],
])("copies %s as valid Python", (data, expected) => {
expect(getCopyValue(data)).toBe(expected);
});
it("distinguishes shared references from cycles", () => {
const shared = { x: true };
expect(getCopyValue([shared, shared])).toBe(
'[\n {\n "x": True\n },\n {\n "x": True\n }\n]',
);
const cyclic: unknown[] = [];
cyclic.push(cyclic);
expect(() => getCopyValue(cyclic)).toThrow("Cannot copy circular data");
});
it("copies sparse arrays and non-finite numbers as Python literals", () => {
const values = [0, Number.NaN, Infinity, -Infinity];
Reflect.deleteProperty(values, "0");
expect(getCopyValue(values)).toBe(
"[\n None,\n float('nan'),\n float('inf'),\n -float('inf')\n]",
);
});
});
describe("Python copy negative cases", () => {
it.each([
"text/plain+unknown:payload",
"text/plain+tuple",
"not-text/plain:payload",
"https://example.com:a:b",
])("does not decode unrecognized leaf %j", (value) => {
expect(getCopyValue(value)).toBe(JSON.stringify(value));
expect(getCopyValue({ [value]: "literal" })).toBe(
`{\n ${JSON.stringify(value)}: "literal"\n}`,
);
});
it.each(["tuple", "set", "frozenset"])(
"does not invent a %s from malformed or non-array JSON",
(kind) => {
for (const payload of ["[broken", "null", "42", '{"x":1}']) {
expect(getCopyValue(`text/plain+${kind}:${payload}`)).toBe(payload);
}
},
);
it("decodes escaped keys and text exactly once", () => {
const data = {
"text/plain+str:text/plain+str:text/plain+int:2":
"text/plain:text/plain+tuple:[42]",
};
expect(getCopyValue(data)).toBe(
'{\n "text/plain+str:text/plain+int:2": "text/plain+tuple:[42]"\n}',
);
});
it("does not decode dictionary keys inside raw tuple payloads", () => {
expect(
getCopyValue(
'text/plain+tuple:[{"text/plain+int:2":"text/plain:literal"}]',
),
).toBe('({"text/plain+int:2": "text/plain:literal"},)');
});
it("rejects indirect cycles without poisoning later copies", () => {
const data: Record = { child: [] };
data.child = [data];
expect(() => getCopyValue(data)).toThrow("Cannot copy circular data");
expect(getCopyValue({ child: [true] })).toBe(
'{\n "child": [\n True\n ]\n}',
);
});
});