diff --git a/CHANGELOG b/CHANGELOG index 407b883..13f894e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ *SVN* +* Make Hash.toQueryString serialize undefined values. Ensure consistency with String.prototype.toQueryParams. Closes #7806. [Mislav Marohnić] + Examples: + $H({a:'b',c:undefined}).toQueryString() -> 'a=b&c' + $H({a:'b',c:null}).toQueryString() -> 'a=b&c=' + $H('a=b&c'.toQueryParams()).toQueryString() -> 'a=b&c' + $H('a=b&c='.toQueryParams()).toQueryString() -> 'a=b&c=' + * Fix issues with Selector an+b logic, :not support, attribute selector double quotes, plus performance improvements. Closes #7873, #7901. [Andrew Dupont] * Fix an issue with Element.getDimensions with some element types on non-IE browsers. Closes #7683. [Andrew Dupont] diff --git a/src/hash.js b/src/hash.js index 9d0e0b1..8bd09fe 100644 --- a/src/hash.js +++ b/src/hash.js @@ -35,9 +35,9 @@ Object.extend(Hash, { }); Hash.toQueryString.addPair = function(key, value, prefix) { - if (value == null) return; key = encodeURIComponent(key); - this.push(key + '=' + (value == null ? '' : encodeURIComponent(value))); + if (value === undefined) this.push(key); + else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value))); } Object.extend(Hash.prototype, Enumerable); diff --git a/src/string.js b/src/string.js index df20371..fa3ebd0 100644 --- a/src/string.js +++ b/src/string.js @@ -95,15 +95,15 @@ Object.extend(String.prototype, { return match[1].split(separator || '&').inject({}, function(hash, pair) { if ((pair = pair.split('='))[0]) { - var name = decodeURIComponent(pair[0]); - var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; - - if (hash[name] !== undefined) { - if (hash[name].constructor != Array) - hash[name] = [hash[name]]; - if (value) hash[name].push(value); + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (hash[key].constructor != Array) hash[key] = [hash[key]]; + hash[key].push(value); } - else hash[name] = value; + else hash[key] = value; } return hash; }); diff --git a/test/lib/unittest.js b/test/lib/unittest.js index 5238964..75b7dc4 100644 --- a/test/lib/unittest.js +++ b/test/lib/unittest.js @@ -303,6 +303,12 @@ Test.Unit.Assertions.prototype = { '", actual "' + Test.Unit.inspect(actual) + '"'); } catch(e) { this.error(e); } }, + assertNotEqual: function(expected, actual) { + var message = arguments[2] || "assertNotEqual"; + try { (expected != actual) ? this.pass() : + this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, assertEnumEqual: function(expected, actual) { var message = arguments[2] || "assertEnumEqual"; expected = $A(expected); @@ -313,10 +319,44 @@ Test.Unit.Assertions.prototype = { ', actual ' + Test.Unit.inspect(actual)); } catch(e) { this.error(e); } }, - assertNotEqual: function(expected, actual) { - var message = arguments[2] || "assertNotEqual"; - try { (expected != actual) ? this.pass() : - this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } + assertEnumNotEqual: function(expected, actual) { + var message = arguments[2] || "assertEnumEqual"; + expected = $A(expected); + actual = $A(actual); + try { expected.length != actual.length || + expected.zip(actual).any(function(pair) { return pair[0] != pair[1] }) ? + this.pass() : this.fail(message + ': ' + Test.Unit.inspect(expected) + + ' was the same as ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, + assertHashEqual: function(expected, actual) { + var message = arguments[2] || "assertHashEqual"; + expected = $H(expected); + actual = $H(actual); + var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort(); + // from now we recursively zip & compare nested arrays + try { expected_array.length == actual_array.length && + expected_array.zip(actual_array).all(function(pair) { + return pair.all(function(i){ return i && i.constructor == Array }) ? + pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1]; + }) ? + this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + + ', actual ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, + assertHashNotEqual: function(expected, actual) { + var message = arguments[2] || "assertHashEqual"; + expected = $H(expected); + actual = $H(actual); + var expected_array = expected.toArray().sort(), actual_array = actual.toArray().sort(); + // from now we recursively zip & compare nested arrays + try { !(expected_array.length == actual_array.length && + expected_array.zip(actual_array).all(function(pair) { + return pair.all(function(i){ return i && i.constructor == Array }) ? + pair[0].zip(pair[1]).all(arguments.callee) : pair[0] == pair[1]; + })) ? + this.pass() : this.fail(message + ': ' + Test.Unit.inspect(expected) + + ' was the same as ' + Test.Unit.inspect(actual)); } catch(e) { this.error(e); } }, assertIdentical: function(expected, actual) { @@ -372,18 +412,21 @@ Test.Unit.Assertions.prototype = { assertMatch: function(expected, actual) { var message = arguments[2] || 'assertMatch'; var regex = new RegExp(expected); - try { (regex.exec(actual)) ? this.pass() : + try { regex.exec(actual) ? this.pass() : this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); } catch(e) { this.error(e); } }, + assertNoMatch: function(expected, actual) { + var message = arguments[2] || 'assertMatch'; + var regex = new RegExp(expected); + try { !regex.exec(actual) ? this.pass() : + this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' matched: ' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, assertHidden: function(element) { var message = arguments[1] || 'assertHidden'; this.assertEqual("none", element.style.display, message); }, - assertNotNull: function(object) { - var message = arguments[1] || 'assertNotNull'; - this.assert(object != null, message); - }, assertInstanceOf: function(expected, actual) { var message = arguments[2] || 'assertInstanceOf'; try { diff --git a/test/unit/hash.html b/test/unit/hash.html index 1671632..de83338 100644 --- a/test/unit/hash.html +++ b/test/unit/hash.html @@ -38,8 +38,8 @@ }, functions: { - alpha: 'foo', - beta: function(n) { return n+1; } + quad: function(n) { return n*n }, + plus: function(n) { return n+n } }, multiple: { color: $w('r g b') }, @@ -65,61 +65,65 @@ new Test.Unit.Runner({ testConstruct: function(){ with(this) { - var h = $H(Fixtures.one) - assertNotIdentical(Fixtures.one, h) - assertIdentical(h, $H(h)) + var h = $H(Fixtures.one); + assertNotIdentical(Fixtures.one, h); + assertIdentical(h, $H(h)); - var h2 = new Hash(h) - assertNotIdentical(h, h2) - assertEqual(h.inspect(), h2.inspect()) + var h2 = new Hash(h); + assertNotIdentical(h, h2); + assertHashEqual(h, h2); }}, testKeys: function(){ with(this) { assertEnumEqual([], $H({}).keys()); assertEnumEqual(['a'], $H(Fixtures.one).keys()); - assertEnumEqual($w('a b c d'), $H(Fixtures.many).keys()); - assertEnumEqual($w('alpha beta'), $H(Fixtures.functions).keys()); + assertEnumEqual($w('a b c d'), $H(Fixtures.many).keys().sort()); + assertEnumEqual($w('plus quad'), $H(Fixtures.functions).keys().sort()); }}, testValues: function(){ with(this) { assertEnumEqual([], $H({}).values()); assertEnumEqual(['A#'], $H(Fixtures.one).values()); - assertEnumEqual($w('A B C D#'), $H(Fixtures.many).values()); - assertEqual('function', typeof $H(Fixtures.functions).values()[1]); - assertEqual(2, $H(Fixtures.functions).beta(1)); + assertEnumEqual($w('A B C D#'), $H(Fixtures.many).values().sort()); + assertEnumEqual($w('function function'), + $H(Fixtures.functions).values().map(function(i){ return typeof i })); + assertEqual(9, $H(Fixtures.functions).quad(3)); + assertEqual(6, $H(Fixtures.functions).plus(3)); }}, testMerge: function(){ with(this) { - assertEqual($H(Fixtures.many).inspect(), $H(Fixtures.many).merge().inspect()); - assertEqual($H(Fixtures.many).inspect(), $H(Fixtures.many).merge({}).inspect()); - assertEqual("#", $H(Fixtures.many).merge({aaa: 'AAA'}).inspect()); - assertEqual("#", $H(Fixtures.many).merge(Fixtures.one).inspect()); + assertHashEqual(Fixtures.many, $H(Fixtures.many).merge()); + assertHashEqual(Fixtures.many, $H(Fixtures.many).merge({})); + assertHashEqual(Fixtures.many, $H(Fixtures.many).merge($H())); + assertHashEqual({a:'A', b:'B', c:'C', d:'D#', aaa:'AAA' }, $H(Fixtures.many).merge({aaa: 'AAA'})); + assertHashEqual({a:'A#', b:'B', c:'C', d:'D#' }, $H(Fixtures.many).merge(Fixtures.one)); }}, testRemove: function(){ with(this) { var hash = $H(Fixtures.many); var values = hash.remove('b', 'c'); - assertEnumEqual($w('a d'), hash.keys()); + assertHashEqual({a:'A', d:'D#'}, hash); assertEnumEqual($w('B C'), values); }}, testToQueryString: function(){ with(this) { - assertEqual('', $H({}).toQueryString()); - assertEqual('a=A%23', $H(Fixtures.one).toQueryString()); - assertEqual('a=A&b=B&c=C&d=D%23', $H(Fixtures.many).toQueryString()); - assertEqual("a=b", $H(Fixtures.value_undefined).toQueryString()); - assertEqual("a=b", $H(Fixtures.value_null).toQueryString()); - assertEqual("a=b&c=0", $H(Fixtures.value_zero).toQueryString()); + assertEqual('', $H({}).toQueryString()); + assertEqual('a=A%23', $H(Fixtures.one).toQueryString()); + assertEqual('a=A&b=B&c=C&d=D%23', $H(Fixtures.many).toQueryString()); + assertEqual("a=b&c", $H(Fixtures.value_undefined).toQueryString()); + assertEqual("a=b&c", $H("a=b&c".toQueryParams()).toQueryString()); + assertEqual("a=b&c=", $H(Fixtures.value_null).toQueryString()); + assertEqual("a=b&c=0", $H(Fixtures.value_zero).toQueryString()); assertEqual("color=r&color=g&color=b", $H(Fixtures.multiple).toQueryString()); - assertEqual("color=r&color=g&color=0", $H(Fixtures.multiple_nil).toQueryString()); - assertEqual("", $H(Fixtures.multiple_all_nil).toQueryString()); - assertEqual("", $H(Fixtures.multiple_empty).toQueryString()); + assertEqual("color=r&color=&color=g&color&color=0", $H(Fixtures.multiple_nil).toQueryString()); + assertEqual("color=&color", $H(Fixtures.multiple_all_nil).toQueryString()); + assertEqual("", $H(Fixtures.multiple_empty).toQueryString()); assertEqual("stuff%5B%5D=%24&stuff%5B%5D=a&stuff%5B%5D=%3B", $H(Fixtures.multiple_special).toQueryString()); - - assertEnumEqual($w("_each=E map=M keys=K values=V collect=C inject=I").sort(), - Hash.toQueryString(Fixtures.dangerous).split('&').sort()); - assertEnumEqual($w('_each=E map=M keys=K values=V collect=C inject=I').sort(), - $H(Fixtures.dangerous).toQueryString().split('&').sort()); + assertHashEqual(Fixtures.multiple_special, $H(Fixtures.multiple_special).toQueryString().toQueryParams()); + + var danger = $w("_each=E collect=C inject=I keys=K map=M values=V"); + assertEnumEqual(danger, Hash.toQueryString(Fixtures.dangerous).split('&').sort()); + assertEnumEqual(danger, $H(Fixtures.dangerous).toQueryString().split('&').sort()); }}, testInspect: function(){ with(this) { diff --git a/test/unit/string.html b/test/unit/string.html index 90bacdd..63ca6b7 100644 --- a/test/unit/string.html +++ b/test/unit/string.html @@ -289,32 +289,29 @@ }}, testToQueryParams: function() {with(this) { - assertEnumEqual([], Object.keys(''.toQueryParams())); - assertEnumEqual([], Object.keys('foo?'.toQueryParams())); + // only the query part + var result = {a:undefined, b:'c'}; + assertHashEqual({}, ''.toQueryParams(), 'empty query'); + assertHashEqual({}, 'foo?'.toQueryParams(), 'empty query with URL'); + assertHashEqual(result, 'foo?a&b=c'.toQueryParams(), 'query with URL'); + assertHashEqual(result, 'foo?a&b=c#fragment'.toQueryParams(), 'query with URL and fragment'); + assertHashEqual(result, 'a;b=c'.toQueryParams(';'), 'custom delimiter'); + + assertHashEqual({a:undefined}, 'a'.toQueryParams(), 'key without value'); + assertHashEqual({a:'b'}, 'a=b&=c'.toQueryParams(), 'empty key'); + assertHashEqual({a:'b', c:''}, 'a=b&c='.toQueryParams(), 'empty value'); - assertEnumEqual(['a', 'b'], Object.keys('foo?a&b'.toQueryParams())); - assertEnumEqual(['a', 'b'], Object.keys('foo?a&b#fragment'.toQueryParams())); - assertEnumEqual(['a', 'b'], Object.keys('a;b'.toQueryParams(';'))); + assertHashEqual({'a b':'c', d:'e f', g:'h'}, + 'a%20b=c&d=e%20f&g=h'.toQueryParams(), 'proper decoding'); + assertHashEqual({a:'b=c=d'}, 'a=b=c=d'.toQueryParams(), 'multiple equal signs'); + assertHashEqual({a:'b', c:'d'}, '&a=b&&&c=d'.toQueryParams(), 'proper splitting'); - var result = 'a'.toQueryParams(); - assertEqual(undefined, result['a']); - assert(result.hasOwnProperty('a')); - - result = 'a&b=c'.toQueryParams(); - assertEqual(undefined, result['a']); - assert(result.hasOwnProperty('a')); - assertEqual('c', result['b']); - - result = 'a%20b=c&d=e%20f&g=h'.toQueryParams(); - assertEqual('c', result['a b']); - assertEqual('e f', result['d']); - assertEqual('h', result['g']); - - result = 'color=r&color=g&color=b'.toQueryParams(); - assertEnumEqual(['r', 'g', 'b'], result['color']); - assertEnumEqual(['r', 'b'], 'c=r&c=&c=b'.toQueryParams()['c']); - assertEqual('blue', 'c=&c=blue'.toQueryParams()['c']); - assertEqual('blue', 'c=blue&c='.toQueryParams()['c']); + assertEnumEqual($w('r g b'), 'col=r&col=g&col=b'.toQueryParams()['col'], + 'collection without square brackets'); + var msg = 'empty values inside collection'; + assertEnumEqual(['r', '', 'b'], 'c=r&c=&c=b'.toQueryParams()['c'], msg); + assertEnumEqual(['', 'blue'], 'c=&c=blue'.toQueryParams()['c'], msg); + assertEnumEqual(['blue', ''], 'c=blue&c='.toQueryParams()['c'], msg); }}, testInspect: function() {with(this) { diff --git a/test/unit/unit_tests.html b/test/unit/unit_tests.html new file mode 100644 index 0000000..fb8c85a --- /dev/null +++ b/test/unit/unit_tests.html @@ -0,0 +1,170 @@ + + + + Prototype Unit test file + + + + + + + +

Prototype Unit test file

+

+ Test the unit testing library (unittest.js) +

+ + +
+ + +
+ +
+ + + +
+
+ + + + +
XXXX
+ +
testcss1
testcss1
+ + + + +