ladybird/Tests/LibJS/Runtime/builtins/JSON/JSON.stringify.js
Andreas Kling b6bef6b688 Libraries: Use UTF-16 for JS-visible runtime strings
Produce JS-visible string results as UTF-16 at their source, including
numeric formatting, BigInt and BigFraction formatting, URI encoding,
console formatting, parser errors, regular expression errors, Intl and
Temporal records, LibUnicode locale boundaries, and LibWeb bindings.

Handle fractional radix formatting through the UTF-16 builder view.
2026-06-22 19:51:25 +02:00

176 lines
6.2 KiB
JavaScript

describe("correct behavior", () => {
test("length", () => {
expect(JSON.stringify).toHaveLength(3);
});
test("basic functionality", () => {
[
[5, "5"],
[undefined, undefined],
[null, "null"],
[NaN, "null"],
[-NaN, "null"],
[Infinity, "null"],
[-Infinity, "null"],
[true, "true"],
[false, "false"],
["test", '"test"'],
[new Number(5), "5"],
[new Boolean(false), "false"],
[new String("test"), '"test"'],
[() => {}, undefined],
[[1, 2, "foo"], '[1,2,"foo"]'],
[{ foo: 1, bar: "baz", qux() {} }, '{"foo":1,"bar":"baz"}'],
[
{
var1: 1,
var2: 2,
toJSON(key) {
let o = this;
o.var2 = 10;
return o;
},
},
'{"var1":1,"var2":10}',
],
].forEach(testCase => {
expect(JSON.stringify(testCase[0])).toEqual(testCase[1]);
});
});
test("serialize BigInt with a toJSON property", () => {
Object.defineProperty(BigInt.prototype, "toJSON", {
configurable: true, // Allows deleting this property at the end of this test case.
get() {
"use strict";
return () => typeof this;
},
});
expect(JSON.stringify(1n)).toBe('"bigint"');
delete BigInt.prototype.toJSON;
});
test("ignores non-enumerable properties", () => {
let o = { foo: "bar" };
Object.defineProperty(o, "baz", { value: "qux", enumerable: false });
expect(JSON.stringify(o)).toBe('{"foo":"bar"}');
});
test("ignores symbol properties", () => {
let o = { foo: "bar" };
let sym = Symbol("baz");
o[sym] = "qux";
expect(JSON.stringify(o)).toBe('{"foo":"bar"}');
});
test("escape surrogate codepoints in strings", () => {
expect(JSON.stringify("\ud83d\ude04")).toBe('"😄"');
expect(JSON.stringify("\ud83d")).toBe('"\\ud83d"');
expect(JSON.stringify("\ude04")).toBe('"\\ude04"');
expect(JSON.stringify("\ud83d\ud83d\ude04\ud83d\ude04\ude04")).toBe('"\\ud83d😄😄\\ude04"');
expect(JSON.stringify("\ude04\ud83d\ude04\ud83d\ude04\ud83d")).toBe('"\\ude04😄😄\\ud83d"');
});
test("special number values", () => {
expect(JSON.stringify(-0)).toBe("0");
expect(JSON.stringify(1e20)).toBe("100000000000000000000");
expect(JSON.stringify(1e21)).toBe("1e+21");
expect(JSON.stringify(Number.MAX_VALUE)).toBe("1.7976931348623157e+308");
expect(JSON.stringify(Number.MIN_VALUE)).toBe("5e-324");
});
test("string escape sequences", () => {
expect(JSON.stringify('"')).toBe('"\\""');
expect(JSON.stringify("\\")).toBe('"\\\\"');
expect(JSON.stringify("/")).toBe('"/"');
expect(JSON.stringify("\b")).toBe('"\\b"');
expect(JSON.stringify("\f")).toBe('"\\f"');
expect(JSON.stringify("\n")).toBe('"\\n"');
expect(JSON.stringify("\r")).toBe('"\\r"');
expect(JSON.stringify("\t")).toBe('"\\t"');
expect(JSON.stringify("\0")).toBe('"\\u0000"');
expect(JSON.stringify("\x1f")).toBe('"\\u001f"');
});
test("undefined and functions in arrays become null", () => {
expect(JSON.stringify([undefined])).toBe("[null]");
expect(JSON.stringify([function () {}])).toBe("[null]");
expect(JSON.stringify([Symbol()])).toBe("[null]");
expect(JSON.stringify([1, undefined, 2])).toBe("[1,null,2]");
});
test("undefined and functions in objects are omitted", () => {
expect(JSON.stringify({ a: undefined })).toBe("{}");
expect(JSON.stringify({ a: function () {} })).toBe("{}");
expect(JSON.stringify({ a: Symbol() })).toBe("{}");
expect(JSON.stringify({ a: 1, b: undefined, c: 2 })).toBe('{"a":1,"c":2}');
expect(JSON.stringify({ a: "é", b: undefined, c: 1 })).toBe('{"a":"é","c":1}');
});
test("array replacer acts as property whitelist", () => {
expect(JSON.stringify({ a: 1, b: 2, c: 3 }, ["a", "c"])).toBe('{"a":1,"c":3}');
expect(JSON.stringify({ a: { b: 1, c: 2 } }, ["a", "b"])).toBe('{"a":{"b":1}}');
});
test("space parameter formats output", () => {
expect(JSON.stringify({ a: 1 }, null, 2)).toBe('{\n "a": 1\n}');
expect(JSON.stringify({ a: 1 }, null, "\t")).toBe('{\n\t"a": 1\n}');
expect(JSON.stringify([1, 2], null, 2)).toBe("[\n 1,\n 2\n]");
// Space capped at 10
expect(JSON.stringify({ a: 1 }, null, 20)).toBe('{\n "a": 1\n}');
});
test("sparse arrays", () => {
expect(JSON.stringify(Array(3))).toBe("[null,null,null]");
});
});
describe("errors", () => {
test("cannot serialize BigInt", () => {
expect(() => {
JSON.stringify(5n);
}).toThrow(TypeError, "Cannot serialize BigInt value to JSON");
});
test("cannot serialize circular structures", () => {
let bad1 = {};
bad1.foo = bad1;
let bad2 = [];
bad2[5] = [[[bad2]]];
let bad3a = { foo: "bar" };
let bad3b = [1, 2, bad3a];
bad3a.bad = bad3b;
[bad1, bad2, bad3a].forEach(bad => {
expect(() => {
JSON.stringify(bad);
}).toThrow(TypeError, "Cannot stringify circular object");
});
});
test("should not crash when serializing deeply nested structures", () => {
const deepArray = [];
let current = deepArray;
for (let i = 0; i < 1_000_000; i++) {
current[0] = [];
current = current[0];
}
expect(() => {
JSON.stringify(deepArray);
}).toThrow(InternalError, "Call stack size limit exceeded");
const deepObject = {};
current = deepObject;
for (let i = 0; i < 1_000_000; i++) {
current.x = {};
current = current.x;
}
expect(() => {
JSON.stringify(deepObject);
}).toThrow(InternalError, "Call stack size limit exceeded");
});
});