var Sinon = require("sinon") var stringify = require("..") function jsonify(obj) { return JSON.stringify(obj, null, 2) } describe("Stringify", function() { it("must stringify circular objects", function() { var obj = {name: "Alice"} obj.self = obj var json = stringify(obj, null, 2) json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) }) it("must stringify circular objects with intermediaries", function() { var obj = {name: "Alice"} obj.identity = {self: obj} var json = stringify(obj, null, 2) json.must.eql(jsonify({name: "Alice", identity: {self: "[Circular ~]"}})) }) it("must stringify circular objects deeper", function() { var obj = {name: "Alice", child: {name: "Bob"}} obj.child.self = obj.child stringify(obj, null, 2).must.eql(jsonify({ name: "Alice", child: {name: "Bob", self: "[Circular ~.child]"} })) }) it("must stringify circular objects deeper with intermediaries", function() { var obj = {name: "Alice", child: {name: "Bob"}} obj.child.identity = {self: obj.child} stringify(obj, null, 2).must.eql(jsonify({ name: "Alice", child: {name: "Bob", identity: {self: "[Circular ~.child]"}} })) }) it("must stringify circular objects in an array", function() { var obj = {name: "Alice"} obj.self = [obj, obj] stringify(obj, null, 2).must.eql(jsonify({ name: "Alice", self: ["[Circular ~]", "[Circular ~]"] })) }) it("must stringify circular objects deeper in an array", function() { var obj = {name: "Alice", children: [{name: "Bob"}, {name: "Eve"}]} obj.children[0].self = obj.children[0] obj.children[1].self = obj.children[1] stringify(obj, null, 2).must.eql(jsonify({ name: "Alice", children: [ {name: "Bob", self: "[Circular ~.children.0]"}, {name: "Eve", self: "[Circular ~.children.1]"} ] })) }) it("must stringify circular arrays", function() { var obj = [] obj.push(obj) obj.push(obj) var json = stringify(obj, null, 2) json.must.eql(jsonify(["[Circular ~]", "[Circular ~]"])) }) it("must stringify circular arrays with intermediaries", function() { var obj = [] obj.push({name: "Alice", self: obj}) obj.push({name: "Bob", self: obj}) stringify(obj, null, 2).must.eql(jsonify([ {name: "Alice", self: "[Circular ~]"}, {name: "Bob", self: "[Circular ~]"} ])) }) it("must stringify repeated objects in objects", function() { var obj = {} var alice = {name: "Alice"} obj.alice1 = alice obj.alice2 = alice stringify(obj, null, 2).must.eql(jsonify({ alice1: {name: "Alice"}, alice2: {name: "Alice"} })) }) it("must stringify repeated objects in arrays", function() { var alice = {name: "Alice"} var obj = [alice, alice] var json = stringify(obj, null, 2) json.must.eql(jsonify([{name: "Alice"}, {name: "Alice"}])) }) it("must call given decycler and use its output", function() { var obj = {} obj.a = obj obj.b = obj var decycle = Sinon.spy(function() { return decycle.callCount }) var json = stringify(obj, null, 2, decycle) json.must.eql(jsonify({a: 1, b: 2}, null, 2)) decycle.callCount.must.equal(2) decycle.thisValues[0].must.equal(obj) decycle.args[0][0].must.equal("a") decycle.args[0][1].must.equal(obj) decycle.thisValues[1].must.equal(obj) decycle.args[1][0].must.equal("b") decycle.args[1][1].must.equal(obj) }) it("must call replacer and use its output", function() { var obj = {name: "Alice", child: {name: "Bob"}} var replacer = Sinon.spy(bangString) var json = stringify(obj, replacer, 2) json.must.eql(jsonify({name: "Alice!", child: {name: "Bob!"}})) replacer.callCount.must.equal(4) replacer.args[0][0].must.equal("") replacer.args[0][1].must.equal(obj) replacer.thisValues[1].must.equal(obj) replacer.args[1][0].must.equal("name") replacer.args[1][1].must.equal("Alice") replacer.thisValues[2].must.equal(obj) replacer.args[2][0].must.equal("child") replacer.args[2][1].must.equal(obj.child) replacer.thisValues[3].must.equal(obj.child) replacer.args[3][0].must.equal("name") replacer.args[3][1].must.equal("Bob") }) it("must call replacer after describing circular references", function() { var obj = {name: "Alice"} obj.self = obj var replacer = Sinon.spy(bangString) var json = stringify(obj, replacer, 2) json.must.eql(jsonify({name: "Alice!", self: "[Circular ~]!"})) replacer.callCount.must.equal(3) replacer.args[0][0].must.equal("") replacer.args[0][1].must.equal(obj) replacer.thisValues[1].must.equal(obj) replacer.args[1][0].must.equal("name") replacer.args[1][1].must.equal("Alice") replacer.thisValues[2].must.equal(obj) replacer.args[2][0].must.equal("self") replacer.args[2][1].must.equal("[Circular ~]") }) it("must call given decycler and use its output for nested objects", function() { var obj = {} obj.a = obj obj.b = {self: obj} var decycle = Sinon.spy(function() { return decycle.callCount }) var json = stringify(obj, null, 2, decycle) json.must.eql(jsonify({a: 1, b: {self: 2}})) decycle.callCount.must.equal(2) decycle.args[0][0].must.equal("a") decycle.args[0][1].must.equal(obj) decycle.args[1][0].must.equal("self") decycle.args[1][1].must.equal(obj) }) it("must use decycler's output when it returned null", function() { var obj = {a: "b"} obj.self = obj obj.selves = [obj, obj] function decycle() { return null } stringify(obj, null, 2, decycle).must.eql(jsonify({ a: "b", self: null, selves: [null, null] })) }) it("must use decycler's output when it returned undefined", function() { var obj = {a: "b"} obj.self = obj obj.selves = [obj, obj] function decycle() {} stringify(obj, null, 2, decycle).must.eql(jsonify({ a: "b", selves: [null, null] })) }) it("must throw given a decycler that returns a cycle", function() { var obj = {} obj.self = obj var err function identity(key, value) { return value } try { stringify(obj, null, 2, identity) } catch (ex) { err = ex } err.must.be.an.instanceof(TypeError) }) describe(".getSerialize", function() { it("must stringify circular objects", function() { var obj = {a: "b"} obj.circularRef = obj obj.list = [obj, obj] var json = JSON.stringify(obj, stringify.getSerialize(), 2) json.must.eql(jsonify({ "a": "b", "circularRef": "[Circular ~]", "list": ["[Circular ~]", "[Circular ~]"] })) }) // This is the behavior as of Mar 3, 2015. // The serializer function keeps state inside the returned function and // so far I'm not sure how to not do that. JSON.stringify's replacer is not // called _after_ serialization. xit("must return a function that could be called twice", function() { var obj = {name: "Alice"} obj.self = obj var json var serializer = stringify.getSerialize() json = JSON.stringify(obj, serializer, 2) json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) json = JSON.stringify(obj, serializer, 2) json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"})) }) }) }) function bangString(key, value) { return typeof value == "string" ? value + "!" : value }