/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * reflect.js is a collection of methods to test HTML attribute reflection. * Each of attribute is reflected differently, depending on various parameters, * see: * http://www.whatwg.org/html/#reflecting-content-attributes-in-idl-attributes * * Do not forget to add these line at the beginning of each new reflect* method: * ok(attr in element, attr + " should be an IDL attribute of this element"); * is(typeof element[attr], <type>, attr + " IDL attribute should be a <type>"); */ /** * Checks that a given attribute is correctly reflected as a string. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test * - attribute String name of the attribute * OR * attribute Object object containing two attributes, 'content' and 'idl' * - otherValues Array [optional] other values to test in addition of the default ones * - extendedAttributes Object object which can have 'TreatNullAs': "EmptyString" */ function reflectString(aParameters) { var element = aParameters.element; var contentAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.content; var idlAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.idl; var otherValues = aParameters.otherValues !== undefined ? aParameters.otherValues : []; var treatNullAs = aParameters.extendedAttributes ? aParameters.extendedAttributes.TreatNullAs : null; ok(idlAttr in element, idlAttr + " should be an IDL attribute of this element"); is(typeof element[idlAttr], "string", "'" + idlAttr + "' IDL attribute should be a string"); // Tests when the attribute isn't set. is(element.getAttribute(contentAttr), null, "When not set, the content attribute should be null."); is(element[idlAttr], "", "When not set, the IDL attribute should return the empty string"); /** * TODO: as long as null stringification doesn't follow the WebIDL * specifications, don't add it to the loop below and keep it here. */ element.setAttribute(contentAttr, null); is(element.getAttribute(contentAttr), "null", "null should have been stringified to 'null' for '" + contentAttr + "'"); is(element[idlAttr], "null", "null should have been stringified to 'null' for '" + idlAttr + "'"); element.removeAttribute(contentAttr); element[idlAttr] = null; if (treatNullAs == "EmptyString") { is(element.getAttribute(contentAttr), "", "null should have been stringified to '' for '" + contentAttr + "'"); is(element[idlAttr], "", "null should have been stringified to '' for '" + idlAttr + "'"); } else { is(element.getAttribute(contentAttr), "null", "null should have been stringified to 'null' for '" + contentAttr + "'"); is(element[idlAttr], "null", "null should have been stringified to 'null' for '" + contentAttr + "'"); } element.removeAttribute(contentAttr); // Tests various strings. var stringsToTest = [ // [ test value, expected result ] [ "", "" ], [ "null", "null" ], [ "undefined", "undefined" ], [ "foo", "foo" ], [ contentAttr, contentAttr ], [ idlAttr, idlAttr ], // TODO: uncomment this when null stringification will follow the specs. // [ null, "null" ], [ undefined, "undefined" ], [ true, "true" ], [ false, "false" ], [ 42, "42" ], // ES5, verse 8.12.8. [ { toString: function() { return "foo" } }, "foo" ], [ { valueOf: function() { return "foo" } }, "[object Object]" ], [ { valueOf: function() { return "quux" }, toString: undefined }, "quux" ], [ { valueOf: function() { return "foo" }, toString: function() { return "bar" } }, "bar" ] ]; otherValues.forEach(function(v) { stringsToTest.push([v, v]) }); stringsToTest.forEach(function([v, r]) { element.setAttribute(contentAttr, v); is(element[idlAttr], r, "IDL attribute '" + idlAttr + "' should return the value it has been set to."); is(element.getAttribute(contentAttr), r, "Content attribute '" + contentAttr + "'should return the value it has been set to."); element.removeAttribute(contentAttr); element[idlAttr] = v; is(element[idlAttr], r, "IDL attribute '" + idlAttr + "' should return the value it has been set to."); is(element.getAttribute(contentAttr), r, "Content attribute '" + contentAttr + "' should return the value it has been set to."); element.removeAttribute(contentAttr); }); // Tests after removeAttribute() is called. Should be equivalent with not set. is(element.getAttribute(contentAttr), null, "When not set, the content attribute should be null."); is(element[idlAttr], "", "When not set, the IDL attribute should return the empty string"); } /** * Checks that a given attribute name for a given element is correctly reflected * as an unsigned int. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test on * - attribute String name of the attribute * - nonZero Boolean whether the attribute should be non-null * - defaultValue Integer [optional] default value, if different from the default one */ function reflectUnsignedInt(aParameters) { var element = aParameters.element; var attr = aParameters.attribute; var nonZero = aParameters.nonZero; var defaultValue = aParameters.defaultValue; var fallback = aParameters.fallback; if (defaultValue === undefined) { if (nonZero) { defaultValue = 1; } else { defaultValue = 0; } } if (fallback === undefined) { fallback = false; } ok(attr in element, attr + " should be an IDL attribute of this element"); is(typeof element[attr], "number", attr + " IDL attribute should be a number"); // Check default value. is(element[attr], defaultValue, "default value should be " + defaultValue); ok(!element.hasAttribute(attr), attr + " shouldn't be present"); var values = [ 1, 3, 42, 2147483647 ]; for (var value of values) { element[attr] = value; is(element[attr], value, "." + attr + " should be equals " + value); is(element.getAttribute(attr), String(value), "@" + attr + " should be equals " + value); element.setAttribute(attr, value); is(element[attr], value, "." + attr + " should be equals " + value); is(element.getAttribute(attr), String(value), "@" + attr + " should be equals " + value); } // -3000000000 is equivalent to 1294967296 when using the IDL attribute. element[attr] = -3000000000; is(element[attr], 1294967296, "." + attr + " should be equals to 1294967296"); is(element.getAttribute(attr), "1294967296", "@" + attr + " should be equals to 1294967296"); // When setting the content attribute, it's a string so it will be invalid. element.setAttribute(attr, -3000000000); is(element.getAttribute(attr), "-3000000000", "@" + attr + " should be equals to " + -3000000000); is(element[attr], defaultValue, "." + attr + " should be equals to " + defaultValue); // When interpreted as unsigned 32-bit integers, all of these fall between // 2^31 and 2^32 - 1, so per spec they return the default value. var nonValidValues = [ -2147483648, -1, 3147483647]; for (var value of nonValidValues) { element[attr] = value; is(element.getAttribute(attr), String(defaultValue), "@" + attr + " should be equals to " + defaultValue); is(element[attr], defaultValue, "." + attr + " should be equals to " + defaultValue); } for (var values of nonValidValues) { element.setAttribute(attr, values[0]); is(element.getAttribute(attr), String(values[0]), "@" + attr + " should be equals to " + values[0]); is(element[attr], defaultValue, "." + attr + " should be equals to " + defaultValue); } // Setting to 0 should throw an error if nonZero is true. var caught = false; try { element[attr] = 0; } catch(e) { caught = true; is(e.name, "IndexSizeError", "exception should be IndexSizeError"); is(e.code, DOMException.INDEX_SIZE_ERR, "exception code should be INDEX_SIZE_ERR"); } if (nonZero && !fallback) { ok(caught, "an exception should have been caught"); } else { ok(!caught, "no exception should have been caught"); } // If 0 is set in @attr, it will be ignored when calling .attr. element.setAttribute(attr, "0"); is(element.getAttribute(attr), "0", "@" + attr + " should be equals to 0"); if (nonZero) { is(element[attr], defaultValue, "." + attr + " should be equals to " + defaultValue); } else { is(element[attr], 0, "." + attr + " should be equals to 0"); } } /** * Checks that a given attribute is correctly reflected as limited to known * values enumerated attribute. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test on * - attribute String name of the attribute * OR * attribute Object object containing two attributes, 'content' and 'idl' * - validValues Array valid values we support * - invalidValues Array invalid values * - defaultValue String [optional] default value when no valid value is set * OR * defaultValue Object [optional] object containing two attributes, 'invalid' and 'missing' * - unsupportedValues Array [optional] valid values we do not support * - nullable boolean [optional] whether the attribute is nullable */ function reflectLimitedEnumerated(aParameters) { var element = aParameters.element; var contentAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.content; var idlAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.idl; var validValues = aParameters.validValues; var invalidValues = aParameters.invalidValues; var defaultValueInvalid = aParameters.defaultValue === undefined ? "" : typeof aParameters.defaultValue === "string" ? aParameters.defaultValue : aParameters.defaultValue.invalid var defaultValueMissing = aParameters.defaultValue === undefined ? "" : typeof aParameters.defaultValue === "string" ? aParameters.defaultValue : aParameters.defaultValue.missing var unsupportedValues = aParameters.unsupportedValues !== undefined ? aParameters.unsupportedValues : []; var nullable = aParameters.nullable; ok(idlAttr in element, idlAttr + " should be an IDL attribute of this element"); if (nullable) { // The missing value default is null, which is typeof == "object" is(typeof element[idlAttr], "object", "'" + idlAttr + "' IDL attribute should be null, which has typeof == object"); is(element[idlAttr], null, "'" + idlAttr + "' IDL attribute should be null"); } else { is(typeof element[idlAttr], "string", "'" + idlAttr + "' IDL attribute should be a string"); } if (nullable) { element.setAttribute(contentAttr, "something"); // Now it will be a string is(typeof element[idlAttr], "string", "'" + idlAttr + "' IDL attribute should be a string"); } // Explicitly check the default value. element.removeAttribute(contentAttr); is(element[idlAttr], defaultValueMissing, "When no attribute is set, the value should be the default value."); // Check valid values. validValues.forEach(function (v) { element.setAttribute(contentAttr, v); is(element[idlAttr], v, "'" + v + "' should be accepted as a valid value for " + idlAttr); is(element.getAttribute(contentAttr), v, "Content attribute should return the value it has been set to."); element.removeAttribute(contentAttr); element.setAttribute(contentAttr, v.toUpperCase()); is(element[idlAttr], v, "Enumerated attributes should be case-insensitive."); is(element.getAttribute(contentAttr), v.toUpperCase(), "Content attribute should not be lower-cased."); element.removeAttribute(contentAttr); element[idlAttr] = v; is(element[idlAttr], v, "'" + v + "' should be accepted as a valid value for " + idlAttr); is(element.getAttribute(contentAttr), v, "Content attribute should return the value it has been set to."); element.removeAttribute(contentAttr); element[idlAttr] = v.toUpperCase(); is(element[idlAttr], v, "Enumerated attributes should be case-insensitive."); is(element.getAttribute(contentAttr), v.toUpperCase(), "Content attribute should not be lower-cased."); element.removeAttribute(contentAttr); }); // Check invalid values. invalidValues.forEach(function (v) { element.setAttribute(contentAttr, v); is(element[idlAttr], defaultValueInvalid, "When the content attribute is set to an invalid value, the default value should be returned."); is(element.getAttribute(contentAttr), v, "Content attribute should not have been changed."); element.removeAttribute(contentAttr); element[idlAttr] = v; is(element[idlAttr], defaultValueInvalid, "When the value is set to an invalid value, the default value should be returned."); is(element.getAttribute(contentAttr), v, "Content attribute should not have been changed."); element.removeAttribute(contentAttr); }); // Check valid values we currently do not support. // Basically, it's like the checks for the valid values but with some todo's. unsupportedValues.forEach(function (v) { element.setAttribute(contentAttr, v); todo_is(element[idlAttr], v, "'" + v + "' should be accepted as a valid value for " + idlAttr); is(element.getAttribute(contentAttr), v, "Content attribute should return the value it has been set to."); element.removeAttribute(contentAttr); element.setAttribute(contentAttr, v.toUpperCase()); todo_is(element[idlAttr], v, "Enumerated attributes should be case-insensitive."); is(element.getAttribute(contentAttr), v.toUpperCase(), "Content attribute should not be lower-cased."); element.removeAttribute(contentAttr); element[idlAttr] = v; todo_is(element[idlAttr], v, "'" + v + "' should be accepted as a valid value for " + idlAttr); is(element.getAttribute(contentAttr), v, "Content attribute should return the value it has been set to."); element.removeAttribute(contentAttr); element[idlAttr] = v.toUpperCase(); todo_is(element[idlAttr], v, "Enumerated attributes should be case-insensitive."); is(element.getAttribute(contentAttr), v.toUpperCase(), "Content attribute should not be lower-cased."); element.removeAttribute(contentAttr); }); if (nullable) { is(defaultValueMissing, null, "Missing default value should be null for nullable attributes"); ok(validValues.length > 0, "We better have at least one valid value"); element.setAttribute(contentAttr, validValues[0]); ok(element.hasAttribute(contentAttr), "Should have content attribute: we just set it"); element[idlAttr] = null; ok(!element.hasAttribute(contentAttr), "Should have removed content attribute"); } } /** * Checks that a given attribute is correctly reflected as a boolean. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test on * - attribute String name of the attribute * OR * attribute Object object containing two attributes, 'content' and 'idl' */ function reflectBoolean(aParameters) { var element = aParameters.element; var contentAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.content; var idlAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.idl; ok(idlAttr in element, idlAttr + " should be an IDL attribute of this element"); is(typeof element[idlAttr], "boolean", idlAttr + " IDL attribute should be a boolean"); // Tests when the attribute isn't set. is(element.getAttribute(contentAttr), null, "When not set, the content attribute should be null."); is(element[idlAttr], false, "When not set, the IDL attribute should return false"); /** * Test various values. * Each value to test is actually an object containing a 'value' property * containing the value to actually test, a 'stringified' property containing * the stringified value and a 'result' property containing the expected * result when the value is set to the IDL attribute. */ var valuesToTest = [ { value: true, stringified: "true", result: true }, { value: false, stringified: "false", result: false }, { value: "true", stringified: "true", result: true }, { value: "false", stringified: "false", result: true }, { value: "foo", stringified: "foo", result: true }, { value: idlAttr, stringified: idlAttr, result: true }, { value: contentAttr, stringified: contentAttr, result: true }, { value: "null", stringified: "null", result: true }, { value: "undefined", stringified: "undefined", result: true }, { value: "", stringified: "", result: false }, { value: undefined, stringified: "undefined", result: false }, { value: null, stringified: "null", result: false }, { value: +0, stringified: "0", result: false }, { value: -0, stringified: "0", result: false }, { value: NaN, stringified: "NaN", result: false }, { value: 42, stringified: "42", result: true }, { value: Infinity, stringified: "Infinity", result: true }, { value: -Infinity, stringified: "-Infinity", result: true }, // ES5, verse 9.2. { value: { toString: function() { return "foo" } }, stringified: "foo", result: true }, { value: { valueOf: function() { return "foo" } }, stringified: "[object Object]", result: true }, { value: { valueOf: function() { return "quux" }, toString: undefined }, stringified: "quux", result: true }, { value: { valueOf: function() { return "foo" }, toString: function() { return "bar" } }, stringified: "bar", result: true }, { value: { valueOf: function() { return false } }, stringified: "[object Object]", result: true }, { value: { foo: false, bar: false }, stringified: "[object Object]", result: true }, { value: { }, stringified: "[object Object]", result: true }, ]; valuesToTest.forEach(function(v) { element.setAttribute(contentAttr, v.value); is(element[idlAttr], true, "IDL attribute should return always return 'true' if the content attribute has been set"); is(element.getAttribute(contentAttr), v.stringified, "Content attribute should return the stringified value it has been set to."); element.removeAttribute(contentAttr); element[idlAttr] = v.value; is(element[idlAttr], v.result, "IDL attribute should return " + v.result); is(element.getAttribute(contentAttr), v.result ? "" : null, v.result ? "Content attribute should return the empty string." : "Content attribute should return null."); is(element.hasAttribute(contentAttr), v.result, v.result ? contentAttr + " should not be present" : contentAttr + " should be present"); element.removeAttribute(contentAttr); }); // Tests after removeAttribute() is called. Should be equivalent with not set. is(element.getAttribute(contentAttr), null, "When not set, the content attribute should be null."); is(element[contentAttr], false, "When not set, the IDL attribute should return false"); } /** * Checks that a given attribute name for a given element is correctly reflected * as an signed integer. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test on * - attribute String name of the attribute * - nonNegative Boolean true if the attribute is limited to 'non-negative numbers', false otherwise * - defaultValue Integer [optional] default value, if one exists */ function reflectInt(aParameters) { // Expected value returned by .getAttribute() when |value| has been previously passed to .setAttribute(). function expectedGetAttributeResult(value) { return String(value); } function stringToInteger(value, nonNegative, defaultValue) { // Parse: Ignore leading whitespace, find [+/-][numbers] var result = /^[ \t\n\f\r]*([\+\-]?[0-9]+)/.exec(value); if (result) { var resultInt = parseInt(result[1], 10); if ((nonNegative ? 0 : -0x80000000) <= resultInt && resultInt <= 0x7FFFFFFF) { // If the value is within allowed value range for signed/unsigned // integer, return it -- but add 0 to it to convert a possible -0 into // +0, the only zero present in the signed integer range. return resultInt + 0; } } return defaultValue; } // Expected value returned by .getAttribute(attr) or .attr if |value| has been set via the IDL attribute. function expectedIdlAttributeResult(value) { // This returns the result of calling the ES ToInt32 algorithm on value. return value << 0; } var element = aParameters.element; var attr = aParameters.attribute; var nonNegative = aParameters.nonNegative; var defaultValue = aParameters.defaultValue !== undefined ? aParameters.defaultValue : nonNegative ? -1 : 0; ok(attr in element, attr + " should be an IDL attribute of this element"); is(typeof element[attr], "number", attr + " IDL attribute should be a number"); // Check default value. is(element[attr], defaultValue, "default value should be " + defaultValue); ok(!element.hasAttribute(attr), attr + " shouldn't be present"); /** * Test various values. * value: The test value that will be set using both setAttribute(value) and * element[attr] = value */ var valuesToTest = [ // Test numeric inputs up to max signed integer 0, 1, 55555, 2147483647, +42, // Test string inputs up to max signed integer "0", "1", "777777", "2147483647", "+42", // Test negative numeric inputs up to min signed integer -0, -1, -3333, -2147483648, // Test negative string inputs up to min signed integer "-0", "-1", "-222", "-2147483647", "-2147483648", // Test numeric inputs that are outside legal 32 bit signed values -2147483649, -3000000000, -4294967296, 2147483649, 4000000000, -4294967297, // Test string inputs with extra padding " 1111111", " 23456 ", // Test non-numeric string inputs "", " ", "+", "-", "foo", "+foo", "-foo", "+ foo", "- foo", "+-2", "-+2", "++2", "--2", "hello1234", "1234hello", "444 world 555", "why 567 what", "-3 nots", "2e5", "300e2", "42+-$", "+42foo", "-514not", "\vblah", "0x10FFFF", "-0xABCDEF", // Test decimal numbers 1.2345, 42.0, 3456789.1, -2.3456, -6789.12345, -2147483649.1234, // Test decimal strings "1.2345", "42.0", "3456789.1", "-2.3456", "-6789.12345", "-2147483649.1234", // Test special values undefined, null, NaN, Infinity, -Infinity, ]; valuesToTest.forEach(function(v) { var intValue = stringToInteger(v, nonNegative, defaultValue); element.setAttribute(attr, v); is(element.getAttribute(attr), expectedGetAttributeResult(v), element.localName + ".setAttribute(" + attr + ", " + v + "), " + element.localName + ".getAttribute(" + attr + ") "); is(element[attr], intValue, element.localName + ".setAttribute(" + attr + ", " + v + "), " + element.localName + "[" + attr + "] "); element.removeAttribute(attr); if (nonNegative && expectedIdlAttributeResult(v) < 0) { try { element[attr] = v; ok(false, element.localName + "[" + attr + "] = " + v + " should throw IndexSizeError"); } catch(e) { is(e.name, "IndexSizeError", element.localName + "[" + attr + "] = " + v + " should throw IndexSizeError"); is(e.code, DOMException.INDEX_SIZE_ERR, element.localName + "[" + attr + "] = " + v + " should throw INDEX_SIZE_ERR"); } } else { element[attr] = v; is(element[attr], expectedIdlAttributeResult(v), element.localName + "[" + attr + "] = " + v + ", " + element.localName + "[" + attr + "] "); is(element.getAttribute(attr), String(expectedIdlAttributeResult(v)), element.localName + "[" + attr + "] = " + v + ", " + element.localName + ".getAttribute(" + attr + ") "); } element.removeAttribute(attr); }); // Tests after removeAttribute() is called. Should be equivalent with not set. is(element.getAttribute(attr), null, "When not set, the content attribute should be null."); is(element[attr], defaultValue, "When not set, the IDL attribute should return default value."); } /** * Checks that a given attribute is correctly reflected as a url. * * @param aParameters Object object containing the parameters, which are: * - element Element node to test * - attribute String name of the attribute * OR * attribute Object object containing two attributes, 'content' and 'idl' */ function reflectURL(aParameters) { var element = aParameters.element; var contentAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.content; var idlAttr = typeof aParameters.attribute === "string" ? aParameters.attribute : aParameters.attribute.idl; element[idlAttr] = ""; is(element[idlAttr], document.URL, "Empty string should resolve to document URL"); }