Improve error reporting for incompatible units

The new messages more consistently include argument names and actual
values. Errors about mixed unitless and unitful numbers are now more
consistent.
This commit is contained in:
Natalie Weizenbaum 2020-12-28 17:58:55 -08:00
parent 7ba7351148
commit d532f015f6
6 changed files with 410 additions and 90 deletions

View File

@ -1,7 +1,28 @@
## 1.32.0
* Improve error messages about incompatible units.
* Properly mark some warnings emitted by `sass:color` functions as deprecation
warnings.
### Dart API
* Rename `SassNumber.valueInUnits()` to `SassNumber.coerceValue()`. The old name
remains, but is now deprecated.
* Rename `SassNumber.coerceValueToUnit()`, a shorthand for
`SassNumber.coerceValue()` that takes a single numerator unit.
* Add `SassNumber.coerceToMatch()` and `SassNumber.coerceValueToMatch()`, which
work like `SassNumber.coerce()` and `SassNumber.coerceValue()` but take a
`SassNumber` whose units should be matched rather than taking the units
explicitly. These generate better error messages than `SassNumber.coerce()`
and `SassNumber.coerceValue()`.
* Add `SassNumber.convertToMatch()` and `SassNumber.convertValueToMatch()`,
which work like `SassNumber.coerceToMatch()` and
`SassNumber.coerceValueToMatch()` except they throw exceptions when converting
unitless values to or from units.
## 1.31.0

View File

@ -41,20 +41,17 @@ final _clamp = _function("clamp", r"$min, $number, $max", (arguments) {
var min = arguments[0].assertNumber("min");
var number = arguments[1].assertNumber("number");
var max = arguments[2].assertNumber("max");
if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) {
if (min.greaterThanOrEquals(max).isTruthy) return min;
if (min.greaterThanOrEquals(number).isTruthy) return min;
if (number.greaterThanOrEquals(max).isTruthy) return max;
return number;
}
var arg2 = min.hasUnits != number.hasUnits ? number : max;
var arg2Name = min.hasUnits != number.hasUnits ? "\$number" : "\$max";
var unit1 = min.hasUnits ? "has unit ${min.unitString}" : "is unitless";
var unit2 = arg2.hasUnits ? "has unit ${arg2.unitString}" : "is unitless";
throw SassScriptException(
"\$min $unit1 but $arg2Name $unit2. Arguments must all have units or all "
"be unitless.");
// Even though we don't use the resulting values, `convertValueToMatch`
// generates more user-friendly exceptions than [greaterThanOrEquals] since it
// has more context about parameter names.
number.convertValueToMatch(min, "number", "min");
max.convertValueToMatch(min, "max", "min");
if (min.greaterThanOrEquals(max).isTruthy) return min;
if (min.greaterThanOrEquals(number).isTruthy) return min;
if (number.greaterThanOrEquals(max).isTruthy) return max;
return number;
});
final _floor = _numberFunction("floor", (value) => value.floor());
@ -94,27 +91,16 @@ final _hypot = _function("hypot", r"$numbers...", (arguments) {
throw SassScriptException("At least one argument must be passed.");
}
var numeratorUnits = numbers[0].numeratorUnits;
var denominatorUnits = numbers[0].denominatorUnits;
var subtotal = 0.0;
for (var i = 0; i < numbers.length; i++) {
var number = numbers[i];
if (number.hasUnits == numbers[0].hasUnits) {
number = number.coerce(numeratorUnits, denominatorUnits);
subtotal += math.pow(number.value, 2);
} else {
var unit1 = numbers[0].hasUnits
? "has unit ${numbers[0].unitString}"
: "is unitless";
var unit2 =
number.hasUnits ? "has unit ${number.unitString}" : "is unitless";
throw SassScriptException(
"Argument 1 $unit1 but argument ${i + 1} $unit2. Arguments must all "
"have units or all be unitless.");
}
var value = number.convertValueToMatch(
numbers[0], "numbers[${i + 1}]", "numbers[1]");
subtotal += math.pow(value, 2);
}
return SassNumber.withUnits(math.sqrt(subtotal),
numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits);
numeratorUnits: numbers[0].numeratorUnits,
denominatorUnits: numbers[0].denominatorUnits);
});
///
@ -232,42 +218,36 @@ final _atan = _function("atan", r"$number", (arguments) {
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
var y = arguments[0].assertNumber("y");
var x = arguments[1].assertNumber("x");
if (y.hasUnits != x.hasUnits) {
var unit1 = y.hasUnits ? "has unit ${y.unitString}" : "is unitless";
var unit2 = x.hasUnits ? "has unit ${x.unitString}" : "is unitless";
throw SassScriptException(
"\$y $unit1 but \$x $unit2. Arguments must all have units or all be "
"unitless.");
}
x = x.coerce(y.numeratorUnits, y.denominatorUnits);
var xValue = _fuzzyRoundIfZero(x.value);
var xValue = _fuzzyRoundIfZero(x.convertValueToMatch(y, 'x', 'y'));
var yValue = _fuzzyRoundIfZero(y.value);
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
});
final _cos = _function("cos", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
return SassNumber(math.cos(number.value));
var value =
arguments[0].assertNumber("number").coerceValueToUnit("rad", "number");
return SassNumber(math.cos(value));
});
final _sin = _function("sin", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var numberValue = _fuzzyRoundIfZero(number.value);
return SassNumber(math.sin(numberValue));
var value = _fuzzyRoundIfZero(
arguments[0].assertNumber("number").coerceValueToUnit("rad", "number"));
return SassNumber(math.sin(value));
});
final _tan = _function("tan", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var value =
arguments[0].assertNumber("number").coerceValueToUnit("rad", "number");
var asymptoteInterval = 0.5 * math.pi;
var tanPeriod = 2 * math.pi;
if (fuzzyEquals((number.value - asymptoteInterval) % tanPeriod, 0)) {
if (fuzzyEquals((value - asymptoteInterval) % tanPeriod, 0)) {
return SassNumber(double.infinity);
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
} else if (fuzzyEquals((value + asymptoteInterval) % tanPeriod, 0)) {
return SassNumber(double.negativeInfinity);
} else {
var numberValue = _fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(value);
return SassNumber(math.tan(numberValue));
}
});
@ -322,15 +302,6 @@ num _fuzzyRoundIfZero(num number) {
return number.isNegative ? -0.0 : 0;
}
SassNumber _coerceToRad(SassNumber number) {
try {
return number.coerce(['rad'], []);
} on SassScriptException catch (error) {
if (!error.message.startsWith('Incompatible units')) rethrow;
throw SassScriptException('\$number: Expected ${number} to be an angle.');
}
}
/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, num transform(num value)) {

View File

@ -37,6 +37,11 @@ String pluralize(String name, int number, {String plural}) {
return '${name}s';
}
/// Returns `a $word` or `an $word` depending on whether [word] starts with a
/// vowel.
String a(String word) =>
[$a, $e, $i, $o, $u].contains(word.codeUnitAt(0)) ? "an $word" : "a $word";
/// Returns a bulleted list of items in [bullets].
String bulletedList(Iterable<String> bullets) {
return bullets.map((element) {

View File

@ -96,20 +96,105 @@ abstract class SassNumber extends Value {
/// (without the `$`). It's used for error reporting.
void assertNoUnits([String name]);
/// Returns a copy of this number, converted to the same units as [other].
///
/// Unlike [convertToMatch], this does *not* throw an error if this number is
/// unitless and [other] is not, or vice versa. Instead, it treats all
/// unitless numbers as convertable to and from all units without changing the
/// value.
///
/// Note that [coerceValueToMatch] is generally more efficient if the value is
/// going to be accessed directly.
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [other]'s units.
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`) and [otherName] is the argument name for [other]. These
/// are used for error reporting.
SassNumber coerceToMatch(SassNumber other, [String name, String otherName]);
/// Returns [value], converted to the same units as [other].
///
/// Unlike [convertValueToMatch], this does *not* throw an error if this
/// number is unitless and [other] is not, or vice versa. Instead, it treats
/// all unitless numbers as convertable to and from all units without changing
/// the value.
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [other]'s units.
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`) and [otherName] is the argument name for [other]. These
/// are used for error reporting.
num coerceValueToMatch(SassNumber other, [String name, String otherName]);
/// Returns a copy of this number, converted to the same units as [other].
///
/// Note that [convertValueToMatch] is generally more efficient if the value
/// is going to be accessed directly.
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [other]'s units, or if either number is unitless but the other is
/// not.
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`) and [otherName] is the argument name for [other]. These
/// are used for error reporting.
SassNumber convertToMatch(SassNumber other, [String name, String otherName]);
/// Returns [value], converted to the same units as [other].
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [other]'s units, or if either number is unitless but the other is
/// not.
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`) and [otherName] is the argument name for [other]. These
/// are used for error reporting.
num convertValueToMatch(SassNumber other, [String name, String otherName]);
/// Returns a copy of this number, converted to the units represented by
/// [newNumerators] and [newDenominators].
///
/// Note that [valueInUnits] is generally more efficient if the value is going
/// This does *not* throw an error if this number is unitless and
/// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead,
/// it treats all unitless numbers as convertable to and from all units
/// without changing the value.
///
/// Note that [coerceValue] is generally more efficient if the value is going
/// to be accessed directly.
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [newNumerators] and [newDenominators].
SassNumber coerce(List<String> newNumerators, List<String> newDenominators);
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for error reporting.
SassNumber coerce(List<String> newNumerators, List<String> newDenominators,
[String name]);
/// Returns [value], converted to the units represented by [newNumerators] and
/// [newDenominators].
///
/// This does *not* throw an error if this number is unitless and
/// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead,
/// it treats all unitless numbers as convertable to and from all units
/// without changing the value.
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [newNumerators] and [newDenominators].
num valueInUnits(List<String> newNumerators, List<String> newDenominators);
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for error reporting.
num coerceValue(List<String> newNumerators, List<String> newDenominators,
[String name]);
/// This has been renamed [coerceValue] for consistency with [coerceToMatch],
/// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch].
@deprecated
num valueInUnits(List<String> newNumerators, List<String> newDenominators,
[String name]);
/// A shorthand for [coerceValue] with only one numerator unit.
num coerceValueToUnit(String unit, [String name]);
}

View File

@ -4,6 +4,7 @@
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:tuple/tuple.dart';
import '../exception.dart';
@ -16,7 +17,7 @@ import 'external/value.dart' as ext;
/// A nested map containing unit conversion rates.
///
/// `1unit1 * _conversions[unit1][unit2] = 1unit2`.
final _conversions = {
const _conversions = {
// Length
"in": {
"in": 1,
@ -140,6 +141,22 @@ final _conversions = {
},
};
/// A map from human-readable names of unit types to the units that fall into
/// those types.
const _unitsByType = {
"length": ["in", "cm", "pc", "mm", "q", "pt", "px"],
"angle": ["deg", "grad", "rad", "turn"],
"time": ["s", "ms"],
"frequency": ["Hz", "kHz"],
"pixel density": ["dpi", "dpcm", "dppx"]
};
/// A map from units to the human-readable names of those unit types.
final _typesByUnit = {
for (var entry in _unitsByType.entries)
for (var unit in entry.value) unit: entry.key
};
// TODO(nweiz): If it's faster, add subclasses specifically for unitless numbers
// and numbers with only a single numerator unit. These should be opaque to
// users of SassNumber.
@ -228,18 +245,113 @@ class SassNumber extends Value implements ext.SassNumber {
throw _exception('Expected $this to have no units.', name);
}
SassNumber coerce(List<String> newNumerators, List<String> newDenominators) =>
SassNumber.withUnits(valueInUnits(newNumerators, newDenominators),
SassNumber coerceToMatch(ext.SassNumber other,
[String name, String otherName]) =>
SassNumber.withUnits(coerceValueToMatch(other, name, otherName),
numeratorUnits: other.numeratorUnits,
denominatorUnits: other.denominatorUnits);
num coerceValueToMatch(ext.SassNumber other,
[String name, String otherName]) =>
_coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits,
coerceUnitless: true, name: name, other: other, otherName: otherName);
SassNumber convertToMatch(ext.SassNumber other,
[String name, String otherName]) =>
SassNumber.withUnits(convertValueToMatch(other, name, otherName),
numeratorUnits: other.numeratorUnits,
denominatorUnits: other.denominatorUnits);
num convertValueToMatch(ext.SassNumber other,
[String name, String otherName]) =>
_coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits,
coerceUnitless: false,
name: name,
other: other,
otherName: otherName);
SassNumber coerce(List<String> newNumerators, List<String> newDenominators,
[String name]) =>
SassNumber.withUnits(coerceValue(newNumerators, newDenominators, name),
numeratorUnits: newNumerators, denominatorUnits: newDenominators);
num valueInUnits(List<String> newNumerators, List<String> newDenominators) {
if ((newNumerators.isEmpty && newDenominators.isEmpty) ||
(numeratorUnits.isEmpty && denominatorUnits.isEmpty) ||
(listEquals(numeratorUnits, newNumerators) &&
listEquals(denominatorUnits, newDenominators))) {
num coerceValue(List<String> newNumerators, List<String> newDenominators,
[String name]) =>
_coerceOrConvertValue(newNumerators, newDenominators,
coerceUnitless: true, name: name);
num coerceValueToUnit(String unit, [String name]) =>
coerceValue([unit], [], name);
@deprecated
num valueInUnits(List<String> newNumerators, List<String> newDenominators,
[String name]) =>
coerceValue(newNumerators, newDenominators, name);
/// Converts [value] to [newNumerators] and [newDenominators].
///
/// If [coerceUnitless] is `true`, this considers unitless numbers convertable
/// to and from any unit. Otherwise, it will throw a [SassScriptException] for
/// such a conversion.
///
/// If [other] is passed, it should be the number from which [newNumerators]
/// and [newDenominators] are derived. The [name] and [otherName] are the Sass
/// function parameter names of [this] and [other], respectively, used for
/// error reporting.
num _coerceOrConvertValue(
List<String> newNumerators, List<String> newDenominators,
{@required bool coerceUnitless,
String name,
ext.SassNumber other,
String otherName}) {
assert(
other == null ||
(listEquals(other.numeratorUnits, newNumerators) &&
listEquals(other.denominatorUnits, newDenominators)),
"Expected $other to have units "
"${_unitString(newNumerators, newDenominators)}.");
if (listEquals(numeratorUnits, newNumerators) &&
listEquals(denominatorUnits, newDenominators)) {
return this.value;
}
var otherHasUnits = newNumerators.isNotEmpty || newDenominators.isNotEmpty;
if (coerceUnitless && (!hasUnits || !otherHasUnits)) return this.value;
SassScriptException _compatibilityException() {
if (other != null) {
var message = StringBuffer("$this and");
if (otherName != null) message.write(" \$$otherName:");
message.write(" $other have incompatible units");
if (!hasUnits || !otherHasUnits) {
message.write(" (one has units and the other doesn't)");
}
return _exception("$message.", name);
} else if (!otherHasUnits) {
return _exception("Expected $this to have no units.", name);
} else {
if (newNumerators.length == 1 && newDenominators.isEmpty) {
var type = _typesByUnit[newNumerators.first];
if (type != null) {
// If we're converting to a unit of a named type, use that type name
// and make it clear exactly which units are convertible.
return _exception(
"Expected $this to have ${a(type)} unit "
"(${_unitsByType[type].join(', ')}).",
name);
}
}
var unit =
pluralize('unit', newNumerators.length + newDenominators.length);
return _exception(
"Expected $this to have $unit "
"${_unitString(newNumerators, newDenominators)}.",
name);
}
}
var value = this.value;
var oldNumerators = numeratorUnits.toList();
for (var newNumerator in newNumerators) {
@ -248,11 +360,7 @@ class SassNumber extends Value implements ext.SassNumber {
if (factor == null) return false;
value *= factor;
return true;
}, orElse: () {
throw SassScriptException("Incompatible units "
"${_unitString(numeratorUnits, denominatorUnits)} and "
"${_unitString(newNumerators, newDenominators)}.");
});
}, orElse: () => throw _compatibilityException());
}
var oldDenominators = denominatorUnits.toList();
@ -262,17 +370,11 @@ class SassNumber extends Value implements ext.SassNumber {
if (factor == null) return false;
value /= factor;
return true;
}, orElse: () {
throw SassScriptException("Incompatible units "
"${_unitString(numeratorUnits, denominatorUnits)} and "
"${_unitString(newNumerators, newDenominators)}.");
});
}, orElse: () => throw _compatibilityException());
}
if (oldNumerators.isNotEmpty || oldDenominators.isNotEmpty) {
throw SassScriptException("Incompatible units "
"${_unitString(numeratorUnits, denominatorUnits)} and "
"${_unitString(newNumerators, newDenominators)}.");
throw _compatibilityException();
}
return value;
@ -394,9 +496,9 @@ class SassNumber extends Value implements ext.SassNumber {
num num2;
if (hasUnits) {
num1 = value;
num2 = other.valueInUnits(numeratorUnits, denominatorUnits);
num2 = other.coerceValueToMatch(this);
} else {
num1 = valueInUnits(other.numeratorUnits, other.denominatorUnits);
num1 = coerceValueToMatch(other);
num2 = other.value;
}

View File

@ -37,6 +37,10 @@ void main() {
expect(value.assertInt(), equals(123));
});
test("can be coerced to unitless", () {
expect(value.coerce([], []), equals(SassNumber.withUnits(123)));
});
test("can be coerced to any units", () {
expect(
value.coerce(["abc"], ["def"]),
@ -44,8 +48,31 @@ void main() {
numeratorUnits: ["abc"], denominatorUnits: ["def"])));
});
test("can return its value in any units", () {
expect(value.valueInUnits(["abc"], ["def"]), equals(123));
test("can be converted to unitless", () {
expect(value.convertToMatch(SassNumber(456)),
equals(SassNumber.withUnits(123)));
});
test("can't be converted to a unit", () {
expect(() => value.convertToMatch(SassNumber(456, "px")),
throwsSassScriptException);
});
test("can coerce its value to unitless", () {
expect(value.coerceValue([], []), equals(123));
});
test("can coerce its value to any units", () {
expect(value.coerceValue(["abc"], ["def"]), equals(123));
});
test("can convert its value to unitless", () {
expect(value.convertValueToMatch(SassNumber(456)), equals(123));
});
test("can't convert its value to any units", () {
expect(() => value.convertValueToMatch(SassNumber(456, "px")),
throwsSassScriptException);
});
group("valueInRange()", () {
@ -183,14 +210,61 @@ void main() {
expect(() => value.assertUnit("in"), throwsSassScriptException);
});
test("can be coerced to unitless", () {
expect(value.coerce([], []), equals(SassNumber(123)));
});
test("can be coerced to compatible units", () {
expect(value.coerce(["px"], []), equals(value));
expect(value.coerce(["in"], []), equals(SassNumber(1.28125, "in")));
});
test("can return its value in compatible units", () {
expect(value.valueInUnits(["px"], []), equals(123));
expect(value.valueInUnits(["in"], []), equals(1.28125));
test("can't be coerced to incompatible units", () {
expect(() => value.coerce(["abc"], []), throwsSassScriptException);
});
test("can't be converted to unitless", () {
expect(() => value.convertToMatch(SassNumber(456)),
throwsSassScriptException);
});
test("can be converted to compatible units", () {
expect(value.convertToMatch(SassNumber(456, "px")), equals(value));
expect(value.convertToMatch(SassNumber(456, "in")),
equals(SassNumber(1.28125, "in")));
});
test("can't be converted to incompatible units", () {
expect(() => value.convertToMatch(SassNumber(456, "abc")),
throwsSassScriptException);
});
test("can coerce its value to unitless", () {
expect(value.coerceValue([], []), equals(123));
});
test("can coerce its value to compatible units", () {
expect(value.coerceValue(["px"], []), equals(123));
expect(value.coerceValue(["in"], []), equals(1.28125));
});
test("can't coerce its value to incompatible units", () {
expect(() => value.coerceValue(["abc"], []), throwsSassScriptException);
});
test("can't convert its value to unitless", () {
expect(() => value.convertValueToMatch(SassNumber(456)),
throwsSassScriptException);
});
test("can convert its value to compatible units", () {
expect(value.convertValueToMatch(SassNumber(456, "px")), equals(123));
expect(value.convertValueToMatch(SassNumber(456, "in")), equals(1.28125));
});
test("can't convert its value to incompatible units", () {
expect(() => value.convertValueToMatch(SassNumber(456, "abc")),
throwsSassScriptException);
});
test("equals the same number", () {
@ -238,6 +312,10 @@ void main() {
expect(() => value.assertUnit("px"), throwsSassScriptException);
});
test("can be coerced to unitless", () {
expect(value.coerce([], []), equals(SassNumber(24.6)));
});
test("can be coerced to compatible units", () {
expect(value.coerce(["px"], ["ms"]), equals(value));
expect(
@ -246,9 +324,67 @@ void main() {
numeratorUnits: ["in"], denominatorUnits: ["s"])));
});
test("can return its value in compatible units", () {
expect(value.valueInUnits(["px"], ["ms"]), equals(24.6));
expect(value.valueInUnits(["in"], ["s"]), equals(256.25));
test("can coerce to match another number", () {
expect(
value.coerceToMatch(SassNumber.withUnits(456,
numeratorUnits: ["in"], denominatorUnits: ["s"])),
equals(SassNumber.withUnits(256.25,
numeratorUnits: ["in"], denominatorUnits: ["s"])));
});
test("can't be coerced to incompatible units", () {
expect(() => value.coerce(["abc"], []), throwsSassScriptException);
});
test("can't be converted to unitless", () {
expect(() => value.convertToMatch(SassNumber(456)),
throwsSassScriptException);
});
test("can be converted to compatible units", () {
expect(
value.convertToMatch(SassNumber.withUnits(456,
numeratorUnits: ["px"], denominatorUnits: ["ms"])),
equals(value));
expect(
value.convertToMatch(SassNumber.withUnits(456,
numeratorUnits: ["in"], denominatorUnits: ["s"])),
equals(SassNumber.withUnits(256.25,
numeratorUnits: ["in"], denominatorUnits: ["s"])));
});
test("can coerce its value to unitless", () {
expect(value.coerceValue([], []), equals(24.6));
});
test("can coerce its value to compatible units", () {
expect(value.coerceValue(["px"], ["ms"]), equals(24.6));
expect(value.coerceValue(["in"], ["s"]), equals(256.25));
});
test("can't coerce its value to incompatible units", () {
expect(() => value.coerceValue(["abc"], []), throwsSassScriptException);
});
test("can't convert its value to unitless", () {
expect(() => value.convertValueToMatch(SassNumber(456)),
throwsSassScriptException);
});
test("can convert its value to compatible units", () {
expect(
value.convertValueToMatch(SassNumber.withUnits(456,
numeratorUnits: ["px"], denominatorUnits: ["ms"])),
equals(24.6));
expect(
value.convertValueToMatch(SassNumber.withUnits(456,
numeratorUnits: ["in"], denominatorUnits: ["s"])),
equals(256.25));
});
test("can't convert its value to incompatible units", () {
expect(() => value.convertValueToMatch(SassNumber(456, "abc")),
throwsSassScriptException);
});
test("equals the same number", () {