| // Copyright (C) 2017 Josh Wolfe. All rights reserved. |
| // This code is governed by the BSD license found in the LICENSE file. |
| /*--- |
| description: | |
| Functions to help generate test cases for testing type coercion abstract |
| operations like ToNumber. |
| defines: |
| - testCoercibleToIndexZero |
| - testCoercibleToIndexOne |
| - testCoercibleToIndexFromIndex |
| - testCoercibleToIntegerZero |
| - testCoercibleToIntegerOne |
| - testCoercibleToNumberZero |
| - testCoercibleToNumberNan |
| - testCoercibleToNumberOne |
| - testCoercibleToIntegerFromInteger |
| - testPrimitiveWrappers |
| - testCoercibleToPrimitiveWithMethod |
| - testNotCoercibleToIndex |
| - testNotCoercibleToInteger |
| - testNotCoercibleToNumber |
| - testNotCoercibleToPrimitive |
| - testCoercibleToString |
| - testNotCoercibleToString |
| - testCoercibleToBooleanTrue |
| - testCoercibleToBooleanFalse |
| - testCoercibleToBigIntZero |
| - testCoercibleToBigIntOne |
| - testCoercibleToBigIntFromBigInt |
| - testNotCoercibleToBigInt |
| ---*/ |
| |
| function testCoercibleToIndexZero(test) { |
| testCoercibleToIntegerZero(test); |
| } |
| |
| function testCoercibleToIndexOne(test) { |
| testCoercibleToIntegerOne(test); |
| } |
| |
| function testCoercibleToIndexFromIndex(nominalIndex, test) { |
| assert(Number.isInteger(nominalIndex)); |
| assert(0 <= nominalIndex && nominalIndex <= 2**53 - 1); |
| testCoercibleToIntegerFromInteger(nominalIndex, test); |
| } |
| |
| function testCoercibleToIntegerZero(test) { |
| testCoercibleToNumberZero(test); |
| |
| testCoercibleToIntegerFromInteger(0, test); |
| |
| // NaN -> +0 |
| testCoercibleToNumberNan(test); |
| |
| // When toString() returns a string that parses to NaN: |
| test({}); |
| test([]); |
| } |
| |
| function testCoercibleToIntegerOne(test) { |
| testCoercibleToNumberOne(test); |
| |
| testCoercibleToIntegerFromInteger(1, test); |
| |
| // When toString() returns "1" |
| test([1]); |
| test(["1"]); |
| } |
| |
| function testCoercibleToNumberZero(test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testPrimitiveValue(null); |
| testPrimitiveValue(false); |
| testPrimitiveValue(0); |
| testPrimitiveValue("0"); |
| } |
| |
| function testCoercibleToNumberNan(test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testPrimitiveValue(undefined); |
| testPrimitiveValue(NaN); |
| testPrimitiveValue(""); |
| testPrimitiveValue("foo"); |
| testPrimitiveValue("true"); |
| } |
| |
| function testCoercibleToNumberOne(test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testPrimitiveValue(true); |
| testPrimitiveValue(1); |
| testPrimitiveValue("1"); |
| } |
| |
| function testCoercibleToIntegerFromInteger(nominalInteger, test) { |
| assert(Number.isInteger(nominalInteger)); |
| |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| |
| // Non-primitive values that coerce to the nominal integer: |
| // toString() returns a string that parsers to a primitive value. |
| test([value]); |
| } |
| |
| function testPrimitiveNumber(number) { |
| testPrimitiveValue(number); |
| // ToNumber: String -> Number |
| testPrimitiveValue(number.toString()); |
| } |
| |
| testPrimitiveNumber(nominalInteger); |
| |
| // ToInteger: floor(abs(number)) |
| if (nominalInteger >= 0) { |
| testPrimitiveNumber(nominalInteger + 0.9); |
| } |
| if (nominalInteger <= 0) { |
| testPrimitiveNumber(nominalInteger - 0.9); |
| } |
| } |
| |
| function testPrimitiveWrappers(primitiveValue, hint, test) { |
| if (primitiveValue != null) { |
| // null and undefined result in {} rather than a proper wrapper, |
| // so skip this case for those values. |
| test(Object(primitiveValue)); |
| } |
| |
| testCoercibleToPrimitiveWithMethod(hint, function() { |
| return primitiveValue; |
| }, test); |
| } |
| |
| function testCoercibleToPrimitiveWithMethod(hint, method, test) { |
| var methodNames; |
| if (hint === "number") { |
| methodNames = ["valueOf", "toString"]; |
| } else if (hint === "string") { |
| methodNames = ["toString", "valueOf"]; |
| } else { |
| throw new Test262Error(); |
| } |
| // precedence order |
| test({ |
| [Symbol.toPrimitive]: method, |
| [methodNames[0]]: function() { throw new Test262Error(); }, |
| [methodNames[1]]: function() { throw new Test262Error(); }, |
| }); |
| test({ |
| [methodNames[0]]: method, |
| [methodNames[1]]: function() { throw new Test262Error(); }, |
| }); |
| if (hint === "number") { |
| // The default valueOf returns an object, which is unsuitable. |
| // The default toString returns a String, which is suitable. |
| // Therefore this test only works for valueOf falling back to toString. |
| test({ |
| // this is toString: |
| [methodNames[1]]: method, |
| }); |
| } |
| |
| // GetMethod: if func is undefined or null, return undefined. |
| test({ |
| [Symbol.toPrimitive]: undefined, |
| [methodNames[0]]: method, |
| [methodNames[1]]: method, |
| }); |
| test({ |
| [Symbol.toPrimitive]: null, |
| [methodNames[0]]: method, |
| [methodNames[1]]: method, |
| }); |
| |
| // if methodNames[0] is not callable, fallback to methodNames[1] |
| test({ |
| [methodNames[0]]: null, |
| [methodNames[1]]: method, |
| }); |
| test({ |
| [methodNames[0]]: 1, |
| [methodNames[1]]: method, |
| }); |
| test({ |
| [methodNames[0]]: {}, |
| [methodNames[1]]: method, |
| }); |
| |
| // if methodNames[0] returns an object, fallback to methodNames[1] |
| test({ |
| [methodNames[0]]: function() { return {}; }, |
| [methodNames[1]]: method, |
| }); |
| test({ |
| [methodNames[0]]: function() { return Object(1); }, |
| [methodNames[1]]: method, |
| }); |
| } |
| |
| function testNotCoercibleToIndex(test) { |
| function testPrimitiveValue(value) { |
| test(RangeError, value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", function(value) { |
| test(RangeError, value); |
| }); |
| } |
| |
| // Let integerIndex be ? ToInteger(value). |
| testNotCoercibleToInteger(test); |
| |
| // If integerIndex < 0, throw a RangeError exception. |
| testPrimitiveValue(-1); |
| testPrimitiveValue(-2.5); |
| testPrimitiveValue("-2.5"); |
| testPrimitiveValue(-Infinity); |
| |
| // Let index be ! ToLength(integerIndex). |
| // If SameValueZero(integerIndex, index) is false, throw a RangeError exception. |
| testPrimitiveValue(2 ** 53); |
| testPrimitiveValue(Infinity); |
| } |
| |
| function testNotCoercibleToInteger(test) { |
| // ToInteger only throws from ToNumber. |
| testNotCoercibleToNumber(test); |
| } |
| |
| function testNotCoercibleToNumber(test) { |
| function testPrimitiveValue(value) { |
| test(TypeError, value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", function(value) { |
| test(TypeError, value); |
| }); |
| } |
| |
| // ToNumber: Symbol -> TypeError |
| testPrimitiveValue(Symbol("1")); |
| |
| if (typeof BigInt !== "undefined") { |
| // ToNumber: BigInt -> TypeError |
| testPrimitiveValue(BigInt(0)); |
| } |
| |
| // ToPrimitive |
| testNotCoercibleToPrimitive("number", test); |
| } |
| |
| function testNotCoercibleToPrimitive(hint, test) { |
| function MyError() {} |
| |
| // ToPrimitive: input[@@toPrimitive] is not callable (and non-null) |
| test(TypeError, {[Symbol.toPrimitive]: 1}); |
| test(TypeError, {[Symbol.toPrimitive]: {}}); |
| |
| // ToPrimitive: input[@@toPrimitive] returns object |
| test(TypeError, {[Symbol.toPrimitive]: function() { return Object(1); }}); |
| test(TypeError, {[Symbol.toPrimitive]: function() { return {}; }}); |
| |
| // ToPrimitive: input[@@toPrimitive] throws |
| test(MyError, {[Symbol.toPrimitive]: function() { throw new MyError(); }}); |
| |
| // OrdinaryToPrimitive: method throws |
| testCoercibleToPrimitiveWithMethod(hint, function() { |
| throw new MyError(); |
| }, function(value) { |
| test(MyError, value); |
| }); |
| |
| // OrdinaryToPrimitive: both methods are unsuitable |
| function testUnsuitableMethod(method) { |
| test(TypeError, {valueOf:method, toString:method}); |
| } |
| // not callable: |
| testUnsuitableMethod(null); |
| testUnsuitableMethod(1); |
| testUnsuitableMethod({}); |
| // returns object: |
| testUnsuitableMethod(function() { return Object(1); }); |
| testUnsuitableMethod(function() { return {}; }); |
| } |
| |
| function testCoercibleToString(test) { |
| function testPrimitiveValue(value, expectedString) { |
| test(value, expectedString); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "string", function(value) { |
| test(value, expectedString); |
| }); |
| } |
| |
| testPrimitiveValue(undefined, "undefined"); |
| testPrimitiveValue(null, "null"); |
| testPrimitiveValue(true, "true"); |
| testPrimitiveValue(false, "false"); |
| testPrimitiveValue(0, "0"); |
| testPrimitiveValue(-0, "0"); |
| testPrimitiveValue(Infinity, "Infinity"); |
| testPrimitiveValue(-Infinity, "-Infinity"); |
| testPrimitiveValue(123.456, "123.456"); |
| testPrimitiveValue(-123.456, "-123.456"); |
| testPrimitiveValue("", ""); |
| testPrimitiveValue("foo", "foo"); |
| |
| if (typeof BigInt !== "undefined") { |
| // BigInt -> TypeError |
| testPrimitiveValue(BigInt(0), "0"); |
| } |
| |
| // toString of a few objects |
| test([], ""); |
| test(["foo", "bar"], "foo,bar"); |
| test({}, "[object Object]"); |
| } |
| |
| function testNotCoercibleToString(test) { |
| function testPrimitiveValue(value) { |
| test(TypeError, value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "string", function(value) { |
| test(TypeError, value); |
| }); |
| } |
| |
| // Symbol -> TypeError |
| testPrimitiveValue(Symbol("1")); |
| |
| // ToPrimitive |
| testNotCoercibleToPrimitive("string", test); |
| } |
| |
| function testCoercibleToBooleanTrue(test) { |
| test(true); |
| test(1); |
| test("string"); |
| test(Symbol("1")); |
| test({}); |
| } |
| |
| function testCoercibleToBooleanFalse(test) { |
| test(undefined); |
| test(null); |
| test(false); |
| test(0); |
| test(-0); |
| test(NaN); |
| test(""); |
| } |
| |
| function testCoercibleToBigIntZero(test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testCoercibleToBigIntFromBigInt(BigInt(0), test); |
| testPrimitiveValue(-BigInt(0)); |
| testPrimitiveValue("-0"); |
| testPrimitiveValue(false); |
| testPrimitiveValue(""); |
| testPrimitiveValue(" "); |
| |
| // toString() returns "" |
| test([]); |
| |
| // toString() returns "0" |
| test([0]); |
| } |
| |
| function testCoercibleToBigIntOne(test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testCoercibleToBigIntFromBigInt(BigInt(1), test); |
| testPrimitiveValue(true); |
| |
| // toString() returns "1" |
| test([1]); |
| } |
| |
| function testCoercibleToBigIntFromBigInt(nominalBigInt, test) { |
| function testPrimitiveValue(value) { |
| test(value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", test); |
| } |
| |
| testPrimitiveValue(nominalBigInt); |
| testPrimitiveValue(nominalBigInt.toString()); |
| testPrimitiveValue("0b" + nominalBigInt.toString(2)); |
| testPrimitiveValue("0o" + nominalBigInt.toString(8)); |
| testPrimitiveValue("0x" + nominalBigInt.toString(16)); |
| testPrimitiveValue(" " + nominalBigInt.toString() + " "); |
| |
| // toString() returns the decimal string representation |
| test([nominalBigInt]); |
| test([nominalBigInt.toString()]); |
| } |
| |
| function testNotCoercibleToBigInt(test) { |
| function testPrimitiveValue(error, value) { |
| test(error, value); |
| // ToPrimitive |
| testPrimitiveWrappers(value, "number", function(value) { |
| test(error, value); |
| }); |
| } |
| |
| // Undefined, Null, Number, Symbol -> TypeError |
| testPrimitiveValue(TypeError, undefined); |
| testPrimitiveValue(TypeError, null); |
| testPrimitiveValue(TypeError, 0); |
| testPrimitiveValue(TypeError, NaN); |
| testPrimitiveValue(TypeError, Infinity); |
| testPrimitiveValue(TypeError, Symbol("1")); |
| |
| // when a String parses to NaN -> SyntaxError |
| function testStringValue(string) { |
| testPrimitiveValue(SyntaxError, string); |
| testPrimitiveValue(SyntaxError, " " + string); |
| testPrimitiveValue(SyntaxError, string + " "); |
| testPrimitiveValue(SyntaxError, " " + string + " "); |
| } |
| testStringValue("a"); |
| testStringValue("0b2"); |
| testStringValue("0o8"); |
| testStringValue("0xg"); |
| testStringValue("1n"); |
| } |