ReflectionTests = {}; ReflectionTests.start = new Date().getTime(); /** * Resolve the given URL to an absolute URL, relative to the current document's * address. There's no API that I know of that exposes this directly, so we * actually just create an element, set its href, and stitch together the * various properties. Seems to work. We don't try to reimplement the * algorithm here, because we're not concerned with its correctness -- we're * only testing HTML reflection, not Web Addresses. * * Return the input if the URL couldn't be resolved, per the spec for * reflected URL attributes. * * It seems like IE9 doesn't implement URL decomposition attributes correctly * for , which causes all these tests to fail. Ideally I'd do this in some * other way, but the failure does stem from an incorrect implementation of * HTML, so I'll leave it alone for now. * * TODO: This relies on reflection to test reflection, so it could mask bugs. * Either get a JS implementation of the "resolve a URL" algorithm, or just * specify expected values manually here. It shouldn't be too hard to write * special cases for all the values we test. */ ReflectionTests.resolveUrl = function(url) { url = String(url); var el = document.createElement("a"); el.href = url; var ret = el.protocol + "//" + el.host + el.pathname + el.search + el.hash; if (ret == "//") { return url; } else { return ret; } }; /** * The "rules for parsing non-negative integers" from the HTML spec. They're * mostly used for reflection, so here seems like as good a place to test them * as any. Returns false on error. */ ReflectionTests.parseNonneg = function(input) { var value = this.parseInt(input); if (value === false || value < 0) { return false; } return value; }; /** * The "rules for parsing integers" from the HTML spec. Returns false on * error. */ ReflectionTests.parseInt = function(input) { var position = 0; var sign = 1; // Skip whitespace while (input.length > position && /^[ \t\n\f\r]$/.test(input[position])) { position++; } if (position >= input.length) { return false; } if (input[position] == "-") { sign = -1; position++; } else if (input[position] == "+") { position++; } if (position >= input.length) { return false; } if (!/^[0-9]$/.test(input[position])) { return false; } var value = 0; while (input.length > position && /^[0-9]$/.test(input[position])) { value *= 10; // Don't use parseInt even for single-digit strings . . . value += input.charCodeAt(position) - "0".charCodeAt(0); position++; } if (value === 0) { return 0; } return sign * value; }; // Used in initializing typeMap var binaryString = "\x00\x01\x02\x03\x04\x05\x06\x07 " + "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f " + "\x10\x11\x12\x13\x14\x15\x16\x17 " + "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "; var maxInt = 2147483647; var minInt = -2147483648; var maxUnsigned = 4294967295; /** * Array containing the tests and other information for each type of reflected * attribute. Meaning of keys: * * "jsType": What typeof idlObj[idlName] is supposed to be. * "defaultVal": The default value to be returned if the attribute is not * present and no default is specifically set for this attribute. * "domTests": What values to test with setAttribute(). * "domExpected": What values to expect with IDL get after setAttribute(). * Defaults to the same as domTests. * "idlTests": What values to test with IDL set. Defaults to domTests. * "idlDomExpected": What to expect from getAttribute() after IDL set. * Defaults to idlTests. * "idlIdlExpected": What to expect from IDL get after IDL set. Defaults to * idlDomExpected. * * Note that all tests/expected values are only baselines, and can be expanded * with additional tests hardcoded into the function for particular types if * necessary. For example, a special codepath is used for enums, and for * IDL setters which throw an exception. null means "defaultVal" is the * expected value. Expected DOM values are cast to strings by adding "". * * TODO: Test strings that aren't valid UTF-16. Desired behavior is not clear * here at the time of writing, see * http://www.w3.org/Bugs/Public/show_bug.cgi?id=12100 * * TODO: Test deleting an IDL attribute, and maybe doing other fun stuff to it. * * TODO: Test IDL sets of integer types to out-of-range or other weird values. * WebIDL says to wrap, but I'm not sure offhand if that's what we want. * * TODO: tokenlist, settable tokenlist, limited */ ReflectionTests.typeMap = { /** * "If a reflecting IDL attribute is a DOMString but doesn't fall into any * of the above categories, then the getting and setting must be done in a * transparent, case-preserving manner." * * The data object passed to reflects() can contain an optional key * treatNullAsEmptyString, whose value is ignored. If it does contain the * key, null will be cast to "" instead of "null", per WebIDL * [TreatNullAs=EmptyString]. */ "string": { "jsType": "string", "defaultVal": "", "domTests": ["", " " + binaryString + " foo ", undefined, 7, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", null, {"toString":function(){return "test-toString";}}, {"valueOf":function(){return "test-valueOf";}, toString:null} ] }, /** * "If a reflecting IDL attribute is a USVString attribute whose content * attribute is defined to contain a URL, then on getting, if the content * attribute is absent, the IDL attribute must return the empty string. * Otherwise, the IDL attribute must parse the value of the content * attribute relative to the element's node document and if that is * successful, return the resulting URL string. If parsing fails, then the * value of the content attribute must be returned instead, converted to a * USVString. On setting, the content attribute must be set to the specified * new value." * * Also HTMLHyperLinkElementUtils href, used by a.href and area.href */ "url": { "jsType": "string", "defaultVal": "", "domTests": ["", " foo ", "http://site.example/", "//site.example/path???@#l", binaryString, undefined, 7, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", null, {"toString":function(){return "test-toString";}}, {"valueOf":function(){return "test-valueOf";}, toString:null}], "domExpected": ReflectionTests.resolveUrl, "idlIdlExpected": ReflectionTests.resolveUrl }, /** * "If a reflecting IDL attribute is a DOMString whose content attribute is * an enumerated attribute, and the IDL attribute is limited to only known * values, then, on getting, the IDL attribute must return the conforming * value associated with the state the attribute is in (in its canonical * case), or the empty string if the attribute is in a state that has no * associated keyword value; and on setting, if the new value is an ASCII * case-insensitive match for one of the keywords given for that attribute, * then the content attribute must be set to the conforming value * associated with the state that the attribute would be in if set to the * given new value, otherwise, if the new value is the empty string, then * the content attribute must be removed, otherwise, the content attribute * must be set to the given new value." * * "Some attributes are defined as taking one of a finite set of keywords. * Such attributes are called enumerated attributes. The keywords are each * defined to map to a particular state (several keywords might map to the * same state, in which case some of the keywords are synonyms of each * other; additionally, some of the keywords can be said to be * non-conforming, and are only in the specification for historical * reasons). In addition, two default states can be given. The first is the * invalid value default, the second is the missing value default. * * . . . * * When the attribute is specified, if its value is an ASCII * case-insensitive match for one of the given keywords then that keyword's * state is the state that the attribute represents. If the attribute value * matches none of the given keywords, but the attribute has an invalid * value default, then the attribute represents that state. Otherwise, if * the attribute value matches none of the keywords but there is a missing * value default state defined, then that is the state represented by the * attribute. Otherwise, there is no default, and invalid values must be * ignored. * * When the attribute is not specified, if there is a missing value default * state defined, then that is the state represented by the (missing) * attribute. Otherwise, the absence of the attribute means that there is * no state represented." * * This is only used for enums that are limited to known values, not other * enums (those are treated as generic strings by the spec). The data * object passed to reflects() can contain these keys: * * "defaultVal": missing value default (defaults to "") * "invalidVal": invalid value default (defaults to defaultVal) * "keywords": array of keywords as given by the spec (required) * "nonCanon": dictionary mapping non-canonical values to their * canonical equivalents (defaults to {}) * "isNullable": Indicates if attribute is nullable (defaults to false) * * Tests are mostly hardcoded into reflects(), since they depend on the * keywords. All expected values are computed in reflects() using a helper * function. */ "enum": { "jsType": "string", "defaultVal": "", "domTests": ["", " " + binaryString + " foo ", undefined, 7, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", null, {"toString":function(){return "test-toString";}}, {"valueOf":function(){return "test-valueOf";}, toString:null}] }, /** * "If a reflecting IDL attribute is a boolean attribute, then on getting * the IDL attribute must return true if the content attribute is set, and * false if it is absent. On setting, the content attribute must be removed * if the IDL attribute is set to false, and must be set to the empty * string if the IDL attribute is set to true. (This corresponds to the * rules for boolean content attributes.)" */ "boolean": { "jsType": "boolean", "defaultVal": false, "domTests": ["", " foo ", undefined, null, 7, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {"toString":function(){return "test-toString";}}, {"valueOf":function(){return "test-valueOf";}, toString:null}], "domExpected": function(val) { return true; } }, /** * "If a reflecting IDL attribute is a signed integer type (long) then, on * getting, the content attribute must be parsed according to the rules for * parsing signed integers, and if that is successful, and the value is in * the range of the IDL attribute's type, the resulting value must be * returned. If, on the other hand, it fails or returns an out of range * value, or if the attribute is absent, then the default value must be * returned instead, or 0 if there is no default value. On setting, the * given value must be converted to the shortest possible string * representing the number as a valid integer and then that string must be * used as the new content attribute value." */ "long": { "jsType": "number", "defaultVal": 0, "domTests": [-36, -1, 0, 1, maxInt, minInt, maxInt + 1, minInt - 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", " " + binaryString + " foo ", // Test various different whitespace. Only 20, 9, A, C, // and D are whitespace. "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": function(val) { var parsed = ReflectionTests.parseInt(String(val)); if (parsed === false || parsed > maxInt || parsed < minInt) { return null; } return parsed; }, "idlTests": [-36, -1, 0, 1, 2147483647, -2147483648], "idlDomExpected": [-36, -1, 0, 1, 2147483647, -2147483648] }, /** * "If a reflecting IDL attribute is a signed integer type (long) that is * limited to only non-negative numbers then, on getting, the content * attribute must be parsed according to the rules for parsing non-negative * integers, and if that is successful, and the value is in the range of * the IDL attribute's type, the resulting value must be returned. If, on * the other hand, it fails or returns an out of range value, or if the * attribute is absent, the default value must be returned instead, or −1 * if there is no default value. On setting, if the value is negative, the * user agent must fire an INDEX_SIZE_ERR exception. Otherwise, the given * value must be converted to the shortest possible string representing the * number as a valid non-negative integer and then that string must be used * as the new content attribute value." */ "limited long": { "jsType": "number", "defaultVal": -1, "domTests": [minInt - 1, minInt, -36, -1, -0, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", " " + binaryString + " foo ", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); if (parsed === false || parsed > maxInt || parsed < minInt) { return null; } return parsed; }, "idlTests": [minInt, -36, -1, 0, 1, maxInt], "idlDomExpected": [null/*exception*/, null/*exception*/, null/*exception*/, 0, 1, maxInt] }, /** * "If a reflecting IDL attribute is an unsigned integer type (unsigned * long) then, on getting, the content attribute must be parsed according * to the rules for parsing non-negative integers, and if that is * successful, and the value is in the range 0 to 2147483647 inclusive, the * resulting value must be returned. If, on the other hand, it fails or * returns an out of range value, or if the attribute is absent, the * default value must be returned instead, or 0 if there is no default * value. On setting, the given value must be converted to the shortest * possible string representing the number as a valid non-negative integer * and then that string must be used as the new content attribute value." */ "unsigned long": { "jsType": "number", "defaultVal": 0, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, 257, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. if (parsed === false || parsed < 0 || parsed > maxInt) { return null; } return parsed; }, "idlTests": [0, 1, 257, maxInt, "-0", maxInt + 1, maxUnsigned], "idlIdlExpected": [0, 1, 257, maxInt, 0, null, null], "idlDomExpected": [0, 1, 257, maxInt, 0, null, null], }, /** * "If a reflecting IDL attribute is an unsigned integer type (unsigned * long) that is limited to only non-negative numbers greater than zero, * then the behavior is similar to the previous case, but zero is not * allowed. On getting, the content attribute must first be parsed * according to the rules for parsing non-negative integers, and if that is * successful, and the value is in the range 1 to 2147483647 inclusive, the * resulting value must be returned. If, on the other hand, it fails or * returns an out of range value, or if the attribute is absent, the * default value must be returned instead, or 1 if there is no default * value. On setting, if the value is zero, the user agent must fire an * INDEX_SIZE_ERR exception. Otherwise, the given value must be converted * to the shortest possible string representing the number as a valid * non-negative integer and then that string must be used as the new * content attribute value." */ "limited unsigned long": { "jsType": "number", "defaultVal": 1, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. if (parsed === false || parsed < 1 || parsed > maxInt) { return null; } return parsed; }, "idlTests": [0, 1, maxInt, maxInt + 1, maxUnsigned], "idlDomExpected": [null/*exception*/, 1, maxInt, null, null] }, /** * "If a reflecting IDL attribute has an unsigned integer type (unsigned * long) that is limited to only non-negative numbers greater than zero * with fallback, then the behaviour is similar to the previous case, but * disallowed values are converted to the default value. On getting, the * content attribute must first be parsed according to the rules for * parsing non-negative integers, and if that is successful, and the value * is in the range 1 to 2147483647 inclusive, the resulting value must be * returned. If, on the other hand, it fails or returns an out of range * value, or if the attribute is absent, the default value must be returned * instead. On setting, first, if the new value is in the range 1 to * 2147483647, then let n be the new value, otherwise let n be the default * value; then, n must be converted to the shortest possible string * representing the number as a valid non-negative integer and that string * must be used as the new content attribute value." */ "limited unsigned long with fallback": { "jsType": "number", "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. if (parsed === false || parsed < 1 || parsed > maxInt) { return null; } return parsed; }, "idlTests": [0, 1, maxInt, maxInt + 1, maxUnsigned], "idlDomExpected": [null, 1, maxInt, null, null] }, /** * "If a reflecting IDL attribute has an unsigned integer type (unsigned * long) that is clamped to the range [min, max], then on getting, the * content attribute must first be parsed according to the rules for * parsing non-negative integers, and if that is successful, and the value * is between min and max inclusive, the resulting value must be returned. * If it fails, the default value must be returned. If it succeeds but the * value is less than min, min must be returned. If it succeeds but the * value is greater than max, max must be returned. On setting, it behaves * the same as a regular reflected unsigned integer." * * The data object passed to reflects must contain the keys defaultVal, * min, and max. As with enum, domExpected is generated later once we have * access to the min and max. */ "clamped unsigned long": { "jsType": "number", "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "idlTests": [0, 1, 257, maxInt, "-0", maxInt + 1, maxUnsigned], "idlDomExpected": [0, 1, 257, maxInt, 0, null, null], }, /** * "If a reflecting IDL attribute is a floating point number type (double), * then, on getting, the content attribute must be parsed according to the * rules for parsing floating point number values, and if that is * successful, the resulting value must be returned. If, on the other hand, * it fails, or if the attribute is absent, the default value must be * returned instead, or 0.0 if there is no default value. On setting, the * given value must be converted to the best representation of the number * as a floating point number and then that string must be used as the new * content attribute value." * * TODO: Check this: * * "Except where otherwise specified, if an IDL attribute that is a * floating point number type (double) is assigned an Infinity or * Not-a-Number (NaN) value, a NOT_SUPPORTED_ERR exception must be raised." * * TODO: Implement the actual algorithm so we can run lots more tests. For * now we're stuck with manually setting up expected values. Of course, * a lot of care has to be taken in checking equality for floats . . . * maybe we should have some tolerance for comparing them. I'm not even * sure whether setting the content attribute to 0 should return 0.0 or * -0.0 (the former, I hope). */ "double": { "jsType": "number", "defaultVal": 0.0, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, {valueOf:function() {return 3;}}], "domExpected": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, maxInt + 1, maxUnsigned, maxUnsigned + 1, null, // Leading whitespace tests 7, null, 7, 7, null, null, 7, 7, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, // End leading whitespace tests null, null, 1.5, 5, 100, 0.5, null, null, null, null, null, null, null, 2, 3], // I checked that ES ToString is well-defined for all of these (I // think). Yes, String(-0) == "0". "idlTests": [ -10000000000, -1, -0, 0, 1, 10000000000], "idlDomExpected": ["-10000000000", "-1", "0", "0", "1", "10000000000"], "idlIdlExpected": [ -10000000000, -1, -0, 0, 1, 10000000000] } }; for (var type in ReflectionTests.typeMap) { var props = ReflectionTests.typeMap[type]; var cast = window[props.jsType[0].toUpperCase() + props.jsType.slice(1)]; if (props.domExpected === undefined) { props.domExpected = props.domTests.map(cast); } else if (typeof props.domExpected == "function") { props.domExpected = props.domTests.map(props.domExpected); } if (props.idlTests === undefined) { props.idlTests = props.domTests; } if (props.idlDomExpected === undefined) { props.idlDomExpected = props.idlTests.map(cast); } else if (typeof props.idlDomExpected == "function") { props.idlDomExpected = props.idlTests.map(props.idlDomExpected); } if (props.idlIdlExpected === undefined) { props.idlIdlExpected = props.idlDomExpected; } else if (typeof props.idlIdlExpected == "function") { props.idlIdlExpected = props.idlTests.map(props.idlIdlExpected); } } /** * Tests that the JavaScript attribute named idlName on the object idlObj * reflects the DOM attribute named domName on domObj. The data argument is an * object that must contain at least one key, "type", which contains the * expected type of the IDL attribute ("string", "enum", etc.). The "comment" * key will add a parenthesized comment in the type info if there's a test * failure, to indicate that there's something special about the element you're * testing (like it has an attribute set to some value). Other keys in the * data object are type-specific, e.g., "defaultVal" for numeric types. If the * data object is a string, it's converted to {"type": data}. If idlObj is a * string, we set idlObj = domObj = document.createElement(idlObj). */ ReflectionTests.reflects = function(data, idlName, idlObj, domName, domObj) { // Do some setup first so that getTypeDescription() works in testWrapper() if (typeof data == "string") { data = {type: data}; } if (domName === undefined) { domName = idlName; } if (typeof idlObj == "string") { idlObj = document.createElement(idlObj); } if (domObj === undefined) { domObj = idlObj; } // Note: probably a hack? This kind of assumes that the variables here // won't change over the course of the tests, which is wrong, but it's // probably safe enough. Just don't read stuff that will change. ReflectionHarness.currentTestInfo = {data: data, idlName: idlName, idlObj: idlObj, domName: domName, domObj: domObj}; // If we don't recognize the type, testing is impossible. if (this.typeMap[data.type] === undefined) { if (unimplemented.indexOf(data.type) == -1) { unimplemented.push(data.type); } return; } var typeInfo = this.typeMap[data.type]; if (typeof data.isNullable == "undefined") { data.isNullable = false; } // Test that typeof idlObj[idlName] is correct. If not, further tests are // probably pointless, so bail out if we're not running conformance tests. var expectedType = data.isNullable && data.defaultVal === null ? "object" : typeInfo.jsType; ReflectionHarness.test(function() { ReflectionHarness.assertEquals(typeof idlObj[idlName], expectedType); }, "typeof IDL attribute"); if (!ReflectionHarness.conformanceTesting && typeof idlObj[idlName] !== expectedType) { return; } // Test default var defaultVal = data.defaultVal; if (defaultVal === undefined) { defaultVal = typeInfo.defaultVal; } if ((domObj.localName === "form" && domName === "action") || (["button", "input"].includes(domObj.localName) && domName === "formAction")) { // Hard-coded special case defaultVal = domObj.ownerDocument.URL; } if (!data.customGetter && (defaultVal !== null || data.isNullable)) { ReflectionHarness.test(function() { ReflectionHarness.assertEquals(idlObj[idlName], defaultVal); }, "IDL get with DOM attribute unset"); } var domTests = typeInfo.domTests.slice(0); var domExpected = typeInfo.domExpected.map(function(val) { return val === null ? defaultVal : val; }); var idlTests = typeInfo.idlTests.slice(0); var idlDomExpected = typeInfo.idlDomExpected.map(function(val) { return val === null ? defaultVal : val; }); var idlIdlExpected = typeInfo.idlIdlExpected.map(function(val) { return val === null ? defaultVal : val; }); switch (data.type) { // Extra tests and other special-casing case "boolean": domTests.push(domName); domExpected.push(true); break; case "enum": // Whee, enum is complicated. if (typeof data.invalidVal == "undefined") { data.invalidVal = defaultVal; } if (typeof data.nonCanon == "undefined") { data.nonCanon = {}; } for (var i = 0; i < data.keywords.length; i++) { if (data.keywords[i] != "") { domTests.push(data.keywords[i], "x" + data.keywords[i], data.keywords[i] + "\0"); idlTests.push(data.keywords[i], "x" + data.keywords[i], data.keywords[i] + "\0"); } if (data.keywords[i].length > 1) { var sliced = data.keywords[i].slice(1); // If slicing a value yields another valid value, then skip it since it results in duplicate tests. if (data.keywords.indexOf(sliced) == -1) { domTests.push(sliced); idlTests.push(sliced); } } if (data.keywords[i] != data.keywords[i].toLowerCase()) { domTests.push(data.keywords[i].toLowerCase()); idlTests.push(data.keywords[i].toLowerCase()); } if (data.keywords[i] != data.keywords[i].toUpperCase()) { domTests.push(data.keywords[i].toUpperCase()); idlTests.push(data.keywords[i].toUpperCase()); } if (data.keywords[i].indexOf("k") != -1) { domTests.push(data.keywords[i].replace(/k/g, "\u212A")); idlTests.push(data.keywords[i].replace(/k/g, "\u212A")); } if (data.keywords[i].indexOf("s") != -1) { domTests.push(data.keywords[i].replace(/s/g, "\u017F")); idlTests.push(data.keywords[i].replace(/s/g, "\u017F")); } } // Per spec, the expected DOM values are the same as the value we set // it to. if (!data.isNullable) { idlDomExpected = idlTests.slice(0); } else { idlDomExpected = []; for (var i = 0; i < idlTests.length; i++) { idlDomExpected.push((idlTests[i] === null || idlTests[i] === undefined) ? null : idlTests[i]); } } // Now we have the fun of calculating what the expected IDL values are. domExpected = []; idlIdlExpected = []; for (var i = 0; i < domTests.length; i++) { domExpected.push(this.enumExpected(data.keywords, data.nonCanon, data.invalidVal, domTests[i])); } for (var i = 0; i < idlTests.length; i++) { if (data.isNullable && (idlTests[i] === null || idlTests[i] === undefined)) { idlIdlExpected.push(null); } else { idlIdlExpected.push(this.enumExpected(data.keywords, data.nonCanon, data.invalidVal, idlTests[i])); } } break; case "string": if ("treatNullAsEmptyString" in data) { for (var i = 0; i < idlTests.length; i++) { if (idlTests[i] === null) { idlDomExpected[i] = idlIdlExpected[i] = ""; } } } break; case "clamped unsigned long": [data.min - 1, data.min, data.max, data.max + 1].forEach(function(val) { if (domTests.indexOf(val) == -1) { domTests.push(val); } if (idlTests.indexOf(val) == -1 && 0 <= val && val <= maxUnsigned) { idlTests.push(val); if (typeof val != "number") { val = ReflectionTests.parseNonneg(val); } idlDomExpected.push(val > maxInt ? null : val); } }); // Rewrite expected values domExpected = domTests.map(function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); if (parsed === false) { return defaultVal; } if (parsed < data.min) { return data.min; } if (parsed > data.max) { return data.max; } return parsed; }); idlIdlExpected = idlTests.map(function(val) { if (typeof val != "number") { val = ReflectionTests.parseNonneg(val); } if (val < 0 || val > maxUnsigned) { throw "Test bug: val should be an unsigned long"; } if (val > maxInt) { return defaultVal; } if (val < data.min) { return data.min; } if (val > data.max) { return data.max; } return val; }); break; } if (domObj.tagName.toLowerCase() == "canvas" && (domName == "width" || domName == "height")) { // Opera tries to allocate a canvas with the given width and height, so // it OOMs when given excessive sizes. This is permissible under the // hardware-limitations clause, so cut out those checks. TODO: Must be // a way to make this more succinct. domTests = domTests.filter(function(element, index, array) { return domExpected[index] < 1000; }); domExpected = domExpected.filter(function(element, index, array) { return element < 1000; }); idlTests = idlTests.filter(function(element, index, array) { return idlIdlExpected[index] < 1000; }); idlDomExpected = idlDomExpected.filter(function(element, index, array) { return idlIdlExpected[index] < 1000; }); idlIdlExpected = idlIdlExpected.filter(function(element, index, array) { return idlIdlExpected[index] < 1000; }); } if ((domObj.localName === "form" && domName === "action") || (["button", "input"].includes(domObj.localName) && domName === "formAction")) { // Hard-coded special case for (var i = 0; i < domTests.length; i++) { if (domTests[i] === "") { domExpected[i] = domObj.ownerDocument.URL; } } for (var i = 0; i < idlTests.length; i++) { if (idlTests[i] === "") { idlIdlExpected[i] = domObj.ownerDocument.URL; } } } if (data.customGetter) { // These are reflected only on setting, not getting domTests = []; domExpected = []; idlIdlExpected = idlIdlExpected.map(() => null); } for (var i = 0; i < domTests.length; i++) { if (domExpected[i] === null && !data.isNullable) { // If you follow all the complicated logic here, you'll find that // this will only happen if there's no expected value at all (like // for tabIndex, where the default is too complicated). So skip // the test. continue; } ReflectionHarness.test(function() { domObj.setAttribute(domName, domTests[i]); ReflectionHarness.assertEquals(domObj.getAttribute(domName), String(domTests[i]), "getAttribute()"); ReflectionHarness.assertEquals(idlObj[idlName], domExpected[i], "IDL get"); }, "setAttribute() to " + ReflectionHarness.stringRep(domTests[i])); } for (var i = 0; i < idlTests.length; i++) { ReflectionHarness.test(function() { if ((data.type == "limited long" && idlTests[i] < 0) || (data.type == "limited unsigned long" && idlTests[i] == 0)) { ReflectionHarness.assertThrows("IndexSizeError", function() { idlObj[idlName] = idlTests[i]; }); } else { idlObj[idlName] = idlTests[i]; if (data.type == "boolean") { // Special case yay ReflectionHarness.assertEquals(domObj.hasAttribute(domName), Boolean(idlTests[i]), "hasAttribute()"); } else if (idlDomExpected[i] !== null || data.isNullable) { var expected = idlDomExpected[i] + ""; if (data.isNullable && idlDomExpected[i] === null) { expected = null; } ReflectionHarness.assertEquals(domObj.getAttribute(domName), expected, "getAttribute()"); } if (idlIdlExpected[i] !== null || data.isNullable) { ReflectionHarness.assertEquals(idlObj[idlName], idlIdlExpected[i], "IDL get"); } } }, "IDL set to " + ReflectionHarness.stringRep(idlTests[i])); } }; function toASCIILowerCase(str) { return str.replace(/[A-Z]/g, function(m) { return m.toLowerCase(); }); } /** * If we have an enumerated attribute limited to the array of values in * keywords, with nonCanon being a map of non-canonical values to their * canonical equivalents, and invalidVal being the invalid value default (or "" * for none), then what would we expect from an IDL get if the content * attribute is equal to contentVal? */ ReflectionTests.enumExpected = function(keywords, nonCanon, invalidVal, contentVal) { var ret = invalidVal; for (var i = 0; i < keywords.length; i++) { if (toASCIILowerCase(String(contentVal)) === toASCIILowerCase(keywords[i])) { ret = keywords[i]; break; } } if (typeof nonCanon[ret] != "undefined") { return nonCanon[ret]; } return ret; }; /** * Now we have the data structures that tell us which elements have which * attributes. * * The elements object (which must have been defined in earlier files) is a map * from element name to an object whose keys are IDL attribute names and whose * values are types. A type is of the same format as * ReflectionTests.reflects() accepts, except that there's an extra optional * domAttrName key that gets passed as the fourth argument to reflects() if * it's provided. (TODO: drop the fourth and fifth reflects() arguments and * make it take them from the dictionary instead?) */ // Now we actually run all the tests. var unimplemented = []; for (var element in elements) { ReflectionTests.reflects("string", "title", element); ReflectionTests.reflects("string", "lang", element); ReflectionTests.reflects({type: "enum", keywords: ["ltr", "rtl", "auto"]}, "dir", element); ReflectionTests.reflects("string", "className", element, "class"); ReflectionTests.reflects("tokenlist", "classList", element, "class"); ReflectionTests.reflects("boolean", "autofocus", element); ReflectionTests.reflects("boolean", "hidden", element); ReflectionTests.reflects("string", "accessKey", element); // Don't try to test the defaultVal -- it should be either 0 or -1, but the // rules are complicated, and a lot of them are SHOULDs. ReflectionTests.reflects({type: "long", defaultVal: null}, "tabIndex", element); // TODO: classList, contextMenu, itemProp, itemRef for (var idlAttrName in elements[element]) { var type = elements[element][idlAttrName]; ReflectionTests.reflects(type, idlAttrName, element, typeof type == "object" && "domAttrName" in type ? type.domAttrName : idlAttrName); } } for (var i = 0; i < extraTests.length; i++) { extraTests[i](); } var time = document.getElementById("time"); if (time) { time.innerHTML = (new Date().getTime() - ReflectionTests.start)/1000; } if (unimplemented.length) { var p = document.createElement("p"); p.textContent = "(Note: missing tests for types " + unimplemented.join(", ") + ".)"; document.body.appendChild(p); }