numeric-testcommon.js (8544B)
1 'use strict'; 2 3 /* 4 Provides functions to help test that two numeric values are equivalent. 5 These *do not* rely on you predicting what one value will serialize to; 6 instead, they set and serialize *both* values, 7 and just ensure that they serialize to the same thing. 8 9 They rely on a #target element existing in the document, 10 as this might rely on layout to resolve styles, 11 and so it needs to be in the document. 12 13 Three main functions are defined, with the same signatures: 14 test_math_used() (for testing used values), 15 test_math_computed() (for testing computed values), 16 and test_math_specified() (for testing specified values). 17 Signature for all is: 18 19 test_math_X( 20 testString, // A numeric value; required. 21 expectedString, // A hopefully-equivalent numeric value; required. 22 { // all of these are optional 23 type, // "number", "length", etc. See impl for full list. Defaults to "length". 24 msg, // The message to display for the test; autogenned if not provided. 25 msgExtra, // Extra info to put after the auto-genned message. 26 prop, // If you want to override the automatic choice of tested property. 27 extraStyle, // Styles that need to be set at the same time to properly test the value. 28 approx, // The epsilon in order to compare numeric-ish values. 29 // Note that it'd use parseFloat in order to extract the 30 // values, so they can drop units or what not. 31 } 32 ); 33 34 35 Additionally, five specialized functions are provided 36 to test that a given value is ±∞, ±0, or NaN: 37 38 * test_plus_infinity(testString) 39 * test_minus_infinity(testString) 40 * test_plus_zero(testString) 41 * test_minus_zero(testString) 42 * test_nan(testString) 43 44 */ 45 46 47 48 function test_math_used(testString, expectedString, {approx, msg, msgExtra, type, prop, extraStyle={}}={}) { 49 if(type === undefined) type = "length"; 50 if(!prop) { 51 switch(type) { 52 case "number": prop = "scale"; break; 53 case "integer": prop = "z-index"; extraStyle.position="absolute"; break; 54 case "length": prop = "margin-left"; break; 55 case "angle": prop = "rotate"; break; 56 case "time": prop = "transition-delay"; break; 57 case "resolution": prop = "image-resolution"; break; 58 case "flex": prop = "grid-template-rows"; break; 59 default: throw Exception(`Value type '${type}' isn't capable of math.`); 60 } 61 62 } 63 _test_math({stage:'used', testString, expectedString, type, approx, msg, msgExtra, prop, extraStyle}); 64 } 65 66 function test_math_computed(testString, expectedString, {approx, msg, msgExtra, type, prop, extraStyle={}}={}) { 67 if(type === undefined) type = "length"; 68 if(!prop) { 69 switch(type) { 70 case "number": prop = "scale"; break; 71 case "integer": prop = "z-index"; extraStyle.position="absolute"; break; 72 case "length": prop = "flex-basis"; break; 73 case "angle": prop = "rotate"; break; 74 case "time": prop = "transition-delay"; break; 75 case "resolution": prop = "image-resolution"; break; 76 case "flex": prop = "grid-template-rows"; break; 77 default: throw Exception(`Value type '${type}' isn't capable of math.`); 78 } 79 80 } 81 _test_math({stage:'computed', testString, expectedString, type, approx, msg, msgExtra, prop, extraStyle}); 82 } 83 84 function test_math_specified(testString, expectedString, {approx, msg, msgExtra, type, prop, extraStyle={}}={}) { 85 if(type === undefined) type = "length"; 86 const stage = "specified"; 87 if(!prop) { 88 switch(type) { 89 case "number": prop = "scale"; break; 90 case "integer": prop = "z-index"; extraStyle.position="absolute"; break; 91 case "length": prop = "flex-basis"; break; 92 case "angle": prop = "rotate"; break; 93 case "time": prop = "transition-delay"; break; 94 case "resolution": prop = "image-resolution"; break; 95 case "flex": prop = "grid-template-rows"; break; 96 default: throw Exception(`Value type '${type}' isn't capable of math.`); 97 } 98 99 } 100 // Find the test element 101 const testEl = document.getElementById('target'); 102 if(testEl == null) throw "Couldn't find #target element to run tests on."; 103 // Then reset its styles 104 testEl.style = ""; 105 for(const p in extraStyle) { 106 testEl.style[p] = extraStyle[p]; 107 } 108 if(!msg) { 109 msg = `${testString} should be ${stage}-value-equivalent to ${expectedString}`; 110 if(msgExtra) msg += "; " + msgExtra; 111 } 112 let t = testString; 113 let e = expectedString; 114 test(()=>{ 115 testEl.style[prop] = ''; 116 testEl.style[prop] = t; 117 const usedValue = testEl.style[prop]; 118 assert_not_equals(usedValue, '', `${testString} isn't valid in '${prop}'; got the default value instead.`); 119 testEl.style[prop] = ''; 120 testEl.style[prop] = e; 121 const expectedValue = testEl.style[prop]; 122 assert_not_equals(expectedValue, '', `${expectedString} isn't valid in '${prop}'; got the default value instead.`) 123 if (approx) { 124 let extractValue = function(value) { 125 if (value.startsWith("calc(")) { 126 value = value.slice("calc(".length, -1); 127 } 128 return parseFloat(value); 129 }; 130 let parsedUsed = extractValue(usedValue); 131 let parsedExpected = extractValue(expectedValue); 132 assert_approx_equals(parsedUsed, parsedExpected, approx, `${testString} and ${expectedString} ${approx} serialize to the same thing in ${stage} values.`); 133 } else { 134 assert_equals(usedValue, expectedValue, `${testString} and ${expectedString} serialize to the same thing in ${stage} values.`); 135 } 136 }, msg); 137 } 138 139 /* 140 All of these expect the testString to evaluate to a <number>. 141 */ 142 function test_plus_infinity(testString) { 143 test_math_used(testString, "calc(infinity)", {type:"number"}); 144 } 145 function test_minus_infinity(testString) { 146 test_math_used(testString, "calc(-infinity)", {type:"number"}); 147 } 148 function test_plus_zero(testString) { 149 test_math_used(`calc(1 / ${testString})`, "calc(infinity)", {type:"number"}); 150 } 151 function test_minus_zero(testString) { 152 test_math_used(`calc(1 / ${testString})`, "calc(-infinity)", {type:"number"}); 153 } 154 function test_nan(testString) { 155 // Make sure that it's NaN, not an infinity, 156 // by making sure that it's the same value both pos and neg. 157 test_math_used(testString, "calc(NaN)", {type:"number"}); 158 test_math_used(`calc(-1 * ${testString})`, "calc(NaN)", {type:"number"}); 159 } 160 161 162 function _test_math({stage, testEl, testString, expectedString, type, approx, msg, msgExtra, prop, extraStyle}={}) { 163 // Find the test element 164 if(!testEl) testEl = document.getElementById('target'); 165 if(testEl == null) throw "Couldn't find #target element to run tests on."; 166 // Then reset its styles 167 testEl.style = ""; 168 for(const p in extraStyle) { 169 testEl.style[p] = extraStyle[p]; 170 } 171 if(!msg) { 172 msg = `${testString} should be ${stage}-value-equivalent to ${expectedString}`; 173 if(msgExtra) msg += "; " + msgExtra; 174 } 175 let t = testString; 176 let e = expectedString; 177 test(()=>{ 178 testEl.style[prop] = ''; 179 const defaultValue = getComputedStyle(testEl)[prop]; 180 testEl.style[prop] = t; 181 const usedValue = getComputedStyle(testEl)[prop]; 182 assert_not_equals(usedValue, defaultValue, `${testString} isn't valid in '${prop}'; got the default value instead.`); 183 testEl.style[prop] = ''; 184 testEl.style[prop] = e; 185 const expectedValue = getComputedStyle(testEl)[prop]; 186 assert_not_equals(expectedValue, defaultValue, `${expectedString} isn't valid in '${prop}'; got the default value instead.`) 187 if (approx) { 188 let extractValue = function(value) { 189 return parseFloat(value); 190 }; 191 let parsedUsed = extractValue(usedValue); 192 let parsedExpected = extractValue(expectedValue); 193 assert_approx_equals(parsedUsed, parsedExpected, approx, `${testString} and ${expectedString} ${approx} serialize to the same thing in ${stage} values.`); 194 } else { 195 assert_equals(usedValue, expectedValue, `${testString} and ${expectedString} serialize to the same thing in ${stage} values.`); 196 } 197 }, msg); 198 }