stringify

ignores undefined from replacer
strictEqual(
  stringify(1, () => undefined),
  '1',
);
ignores null from replacer
strictEqual(
  stringify(1, () => null),
  '1',
);
throws TypeError if replacer returns number
assert.throws(
  () => stringify(1, () => val),
  TypeError,
);
throws TypeError if replacer returns object
assert.throws(
  () => stringify(1, () => val),
  TypeError,
);
can replace undefined
strictEqual(
  stringify(undefined, () => 'TEST'),
  'TEST',
);
can replace with empty string
strictEqual(
  stringify(1, () => ''),
  '',
);
can replace with empty string in Array
strictEqual(
  stringify([1, 2], (k, v) => (typeof v === 'number' ? '' : undefined)),
  '[,]',
);
does not call replacer on replaced values
const child = {};
const parent = { child };
let callCount = 0;
function replacer(k, v) {
  callCount += 1;
  strictEqual(callCount, 1);
  strictEqual(v, parent);
  return 'TEST';
}
strictEqual(
  stringify(parent, replacer),
  'TEST',
);

behaves like JSON.stringify

has same .name
strictEqual(
  stringify.name,
  JSON.stringify.name,
);
has same .length
strictEqual(
  stringify.length,
  JSON.stringify.length,
);
for no arguments
strictEqual(
  stringify(),
  JSON.stringify(),
);
for one argument
strictEqual(
  stringify(mixed),
  JSON.stringify(mixed),
);
for empty Array replacer
strictEqual(
  stringify(mixed, []),
  JSON.stringify(mixed, []),
);
for non-empty Array replacer
strictEqual(
  stringify(mixed, ['true']),
  JSON.stringify(mixed, ['true']),
);
for number space
strictEqual(
  stringify(mixed, () => undefined, 2),
  JSON.stringify(mixed, null, 2),
);
for Number space
strictEqual(
  stringify(mixed, () => undefined, new Number(2)),
  JSON.stringify(mixed, null, new Number(2)),
);
for non-integer space
strictEqual(
  stringify(mixed, () => undefined, 2.6),
  JSON.stringify(mixed, null, 2.6),
);
for space larger than 10
strictEqual(
  stringify(mixed, () => undefined, 15),
  JSON.stringify(mixed, null, 15),
);
for negative space
strictEqual(
  stringify(mixed, () => undefined, -1),
  JSON.stringify(mixed, null, -1),
);
for string space
strictEqual(
  stringify(mixed, () => undefined, 'X'),
  JSON.stringify(mixed, null, 'X'),
);
for String space
strictEqual(
  stringify(mixed, () => undefined, new String('X')),
  JSON.stringify(mixed, null, new String('X')),
);
for empty string space
strictEqual(
  stringify(mixed, () => undefined, ''),
  JSON.stringify(mixed, null, ''),
);
for empty String space
strictEqual(
  stringify(mixed, () => undefined, new String('')),
  JSON.stringify(mixed, null, new String('')),
);
for string space longer than 10
strictEqual(
  stringify(mixed, () => undefined, 'XXXXXXXXXXXXXXX'),
  JSON.stringify(mixed, null, 'XXXXXXXXXXXXXXX'),
);
for Array with non-numeric keys
const arr = [];
arr.key1 = true;
arr.key2 = 1;
strictEqual(
  stringify(arr, () => undefined),
  JSON.stringify(arr),
);
calls replacer with same arguments
const actualArgs = [];
const actualThis = [];
stringify(mixed, function() {
  actualThis.push(this);
  actualArgs.push(arguments); // eslint-disable-line prefer-rest-params
});
const expectedArgs = [];
const expectedThis = [];
JSON.stringify(mixed, function(k, v) {
  expectedThis.push(this);
  expectedArgs.push(arguments); // eslint-disable-line prefer-rest-params
  return v;
});
deepStrictEqual(actualArgs, expectedArgs);
deepStrictEqual(actualThis, expectedThis);
omitting first value in object
const obj = { a: 1, b: 2 };
strictEqual(
  stringify(obj, (k) => k !== 'a', 2),
  JSON.stringify(obj, (k, v) => (k === 'a' ? undefined : v), 2),
);
omitting last value in object
const obj = { a: 1, b: 2 };
strictEqual(
  stringify(obj, (k) => k !== 'b', 2),
  JSON.stringify(obj, (k, v) => (k === 'b' ? undefined : v), 2),
);
omitting only value in object
const obj = { a: 1 };
strictEqual(
  stringify(obj, (k) => k !== 'a', 2),
  JSON.stringify(obj, (k, v) => (k === 'a' ? undefined : v), 2),
);
omitting first value in Array
const arr = [1, 2];
strictEqual(
  stringify(arr, (k) => k !== '0', 2),
  JSON.stringify(arr, (k, v) => (k === '0' ? undefined : v), 2),
);
omitting last value in Array
const arr = [1, 2];
strictEqual(
  stringify(arr, (k) => k !== '1', 2),
  JSON.stringify(arr, (k, v) => (k === '1' ? undefined : v), 2),
);
omitting only value in Array
const arr = [1];
strictEqual(
  stringify(arr, (k) => k !== '0', 2),
  JSON.stringify(arr, (k, v) => (k === '0' ? undefined : v), 2),
);
for directly cyclic object
const obj = {};
obj.obj = obj;
let errActual;
try {
  stringify(obj, () => undefined);
} catch (err) {
  errActual = err;
}
let errExpected;
try {
  JSON.stringify(obj);
} catch (err) {
  errExpected = err;
}
// Error message contains circular path since Node 12.
// Only check that our message is a prefix of built-in message
if (errActual
  && errExpected
  && errExpected.message.startsWith(errActual.message)) {
  errActual.message = errExpected.message;
}
deepStrictEqual(errActual, errExpected);
for indirectly cyclic object
const obj1 = {};
obj1.obj2 = { obj1 };
let errActual;
try {
  stringify(obj1, () => undefined);
} catch (err) {
  errActual = err;
}
let errExpected;
try {
  JSON.stringify(obj1);
} catch (err) {
  errExpected = err;
}
// Error message contains circular path since Node 12.
// Only check that our message is a prefix of built-in message
if (errActual
  && errExpected
  && errExpected.message.startsWith(errActual.message)) {
  errActual.message = errExpected.message;
}
deepStrictEqual(errActual, errExpected);
for repeated object
const obj = {};
const arr = [obj, obj];
strictEqual(
  stringify(arr, () => undefined),
  JSON.stringify(arr),
);
for .toJSON on Boolean prototype
obj.constructor.prototype.toJSON = () => 'HERE';
try {
  strictEqual(
    stringify(obj, () => undefined),
    JSON.stringify(obj),
  );
} finally {
  delete obj.constructor.prototype.toJSON;
}
for .toJSON on Boolean
obj.toJSON = () => 'HERE';
strictEqual(
  stringify(obj, () => undefined),
  JSON.stringify(obj),
);
for .toJSON on Number prototype
obj.constructor.prototype.toJSON = () => 'HERE';
try {
  strictEqual(
    stringify(obj, () => undefined),
    JSON.stringify(obj),
  );
} finally {
  delete obj.constructor.prototype.toJSON;
}
for .toJSON on Number
obj.toJSON = () => 'HERE';
strictEqual(
  stringify(obj, () => undefined),
  JSON.stringify(obj),
);
for .toJSON on String prototype
obj.constructor.prototype.toJSON = () => 'HERE';
try {
  strictEqual(
    stringify(obj, () => undefined),
    JSON.stringify(obj),
  );
} finally {
  delete obj.constructor.prototype.toJSON;
}
for .toJSON on String
obj.toJSON = () => 'HERE';
strictEqual(
  stringify(obj, () => undefined),
  JSON.stringify(obj),
);

BigInt

for bigint
throwsSame(
  () => stringify(BigInt(2), () => undefined),
  () => JSON.stringify(BigInt(2)),
  true,
);
for BigInt
throwsSame(
  () => stringify(Object(BigInt(2)), () => undefined),
  () => JSON.stringify(Object(BigInt(2))),
  true,
);
for bigint with BigInt.prototype.toJSON
// eslint-disable-next-line no-extend-native
BigInt.prototype.toJSON = (k, v) => `${v}n`;
try {
  strictEqual(
    stringify(BigInt(2), () => undefined),
    JSON.stringify(BigInt(2)),
  );
} finally {
  delete BigInt.prototype.toJSON;
}
for BigInt with BigInt.prototype.toJSON
// eslint-disable-next-line no-extend-native
BigInt.prototype.toJSON = (k, v) => `${v}n`;
try {
  strictEqual(
    stringify(Object(BigInt(2)), () => undefined),
    JSON.stringify(Object(BigInt(2))),
  );
} finally {
  delete BigInt.prototype.toJSON;
}

Symbol

for symbol
strictEqual(
  stringify(symbol, () => undefined),
  JSON.stringify(symbol),
);
for Symbol
strictEqual(
  stringify(symbolObj, () => undefined),
  JSON.stringify(symbolObj),
);
for symbol with Symbol.prototype.toJSON
// eslint-disable-next-line no-extend-native
Symbol.prototype.toJSON = (k, v) => `${v}`;
try {
  strictEqual(
    stringify(symbol, () => undefined),
    JSON.stringify(symbol),
  );
} finally {
  delete Symbol.prototype.toJSON;
}
for Symbol with Symbol.prototype.toJSON
// eslint-disable-next-line no-extend-native
Symbol.prototype.toJSON = (k, v) => `${v}`;
try {
  strictEqual(
    stringify(symbolObj, () => undefined),
    JSON.stringify(symbolObj),
  );
} finally {
  delete Symbol.prototype.toJSON;
}
for symbol properties
const obj = { [symbol]: true };
strictEqual(
  stringify(obj, () => undefined),
  JSON.stringify(obj),
);

false from replacer

produces undefined for top value
strictEqual(
  stringify({}, () => false),
  undefined,
);
omits key from object
strictEqual(
  stringify({ a: true, b: true }, (k) => k !== 'a'),
  '{"b":true}',
);
produces null in array
strictEqual(
  stringify([1, 2, 3], (k) => k !== '1'),
  '[1,null,3]',
);