diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 0dcc273a3a4..b2eb7b97196 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -60,6 +60,7 @@ return [ 'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js', 'tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js', 'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js', + 'tests/qunit/suites/resources/startup/mw.Map.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js', diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js index 8989905bfab..614ad735ead 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.test.js @@ -73,86 +73,6 @@ ); } ); - QUnit.test( 'mw.Map', function ( assert ) { - var arry, conf, funky, nummy, someValues; - - conf = new mw.Map(); - - // Dummy variables - funky = function () {}; - arry = []; - nummy = 7; - - // Single get and set - - assert.strictEqual( conf.set( 'foo', 'Bar' ), true, 'Map.set returns boolean true if a value was set for a valid key string' ); - assert.strictEqual( conf.get( 'foo' ), 'Bar', 'Map.get returns a single value value correctly' ); - - assert.strictEqual( conf.get( 'example' ), null, 'Map.get returns null if selection was a string and the key was not found' ); - assert.strictEqual( conf.get( 'example', arry ), arry, 'Map.get returns fallback by reference if the key was not found' ); - assert.strictEqual( conf.get( 'example', undefined ), undefined, 'Map.get supports `undefined` as fallback instead of `null`' ); - - assert.strictEqual( conf.get( 'constructor' ), null, 'Map.get does not look at Object.prototype of internal storage (constructor)' ); - assert.strictEqual( conf.get( 'hasOwnProperty' ), null, 'Map.get does not look at Object.prototype of internal storage (hasOwnProperty)' ); - - conf.set( 'hasOwnProperty', function () { - return true; - } ); - assert.strictEqual( conf.get( 'example', 'missing' ), 'missing', 'Map.get uses neutral hasOwnProperty method (positive)' ); - - conf.set( 'example', 'Foo' ); - conf.set( 'hasOwnProperty', function () { - return false; - } ); - assert.strictEqual( conf.get( 'example' ), 'Foo', 'Map.get uses neutral hasOwnProperty method (negative)' ); - - assert.strictEqual( conf.set( 'constructor', 42 ), true, 'Map.set for key "constructor"' ); - assert.strictEqual( conf.get( 'constructor' ), 42, 'Map.get for key "constructor"' ); - - assert.strictEqual( conf.set( 'undef' ), false, 'Map.set requires explicit value (no undefined default)' ); - - assert.strictEqual( conf.set( 'undef', undefined ), true, 'Map.set allows setting value to `undefined`' ); - assert.strictEqual( conf.get( 'undef', 'fallback' ), undefined, 'Map.get supports retrieving value of `undefined`' ); - - assert.strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' ); - assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' ); - assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' ); - assert.strictEqual( conf.set( null, 'Null' ), false, 'Map.set returns false if key is invalid (null)' ); - assert.strictEqual( conf.set( {}, 'Object' ), false, 'Map.set returns false if key is invalid (plain object)' ); - - conf.set( String( nummy ), 'I used to be a number' ); - - assert.strictEqual( conf.get( funky ), null, 'Map.get returns null if selection was invalid (Function)' ); - assert.strictEqual( conf.get( nummy ), null, 'Map.get returns null if selection was invalid (Number)' ); - assert.propEqual( conf.get( [ nummy ] ), {}, 'Map.get returns null if selection was invalid (multiple)' ); - assert.strictEqual( conf.get( nummy, false ), false, 'Map.get returns custom fallback for invalid selection' ); - - assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' ); - assert.strictEqual( conf.exists( 'undef' ), true, 'Map.exists where value is `undefined`' ); - assert.strictEqual( conf.exists( nummy ), false, 'Map.exists with invalid key that looks like an existing key' ); - - // Multiple values at once - conf = new mw.Map(); - someValues = { - foo: 'bar', - lorem: 'ipsum', - MediaWiki: true - }; - assert.strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' ); - assert.deepEqual( conf.get( [ 'foo', 'lorem' ] ), { - foo: 'bar', - lorem: 'ipsum' - }, 'Map.get returns multiple values correctly as an object' ); - - assert.deepEqual( conf.get( [ 'foo', 'notExist' ] ), { - foo: 'bar', - notExist: null - }, 'Map.get return includes keys that were not found as null values' ); - - assert.propEqual( conf.values, someValues, 'Map.values is an internal object with all values (exposed for convenience)' ); - assert.propEqual( conf.get(), someValues, 'Map.get() returns an object with all values' ); - } ); - QUnit.test( 'mw.message & mw.messages', function ( assert ) { var goodbye, hello; diff --git a/tests/qunit/suites/resources/startup/mw.Map.test.js b/tests/qunit/suites/resources/startup/mw.Map.test.js new file mode 100644 index 00000000000..bdee9140bd2 --- /dev/null +++ b/tests/qunit/suites/resources/startup/mw.Map.test.js @@ -0,0 +1,130 @@ +( function () { + + // Dummy variables + var funky = function () {}; + var arry = []; + + QUnit.module( 'mw.Map' ); + + QUnit.test( 'Store simple string key', function ( assert ) { + var conf = new mw.Map(); + + assert.true( conf.set( 'foo', 'Bar' ), 'set' ); + assert.strictEqual( conf.get( 'foo' ), 'Bar', 'get' ); + } ); + + QUnit.test( 'Store number-like key', function ( assert ) { + var conf = new mw.Map(); + + assert.true( conf.set( '42', 'X' ), 'set' ); + assert.strictEqual( conf.get( '42' ), 'X', 'get' ); + } ); + + QUnit.test( 'get()', function ( assert ) { + var conf = new mw.Map(); + + assert.strictEqual( conf.get( 'example' ), null, 'default fallback' ); + assert.strictEqual( conf.get( 'example', arry ), arry, 'array fallback' ); + assert.strictEqual( conf.get( 'example', funky ), funky, 'function fallback' ); + assert.strictEqual( conf.get( 'example', undefined ), undefined, 'undefined fallback' ); + + // Numbers are not valid keys. Ignore any stored keys that could match after casting. + assert.true( conf.set( '7', 'I used to be a number' ) ); + assert.strictEqual( conf.get( 7 ), null, 'ignore number key (single)' ); + assert.deepEqual( conf.get( [ 7 ] ), {}, 'ignore number key (multiple)' ); + assert.strictEqual( conf.get( 7, 42 ), 42, 'ignore number key (fallback)' ); + + // Functions are not valid keys. + assert.true( conf.set( String( funky ), 'I used to be a function' ) ); + assert.strictEqual( conf.get( funky ), null, 'ignore function key' ); + + conf = new mw.Map(); + conf.set( 'foo', 'bar' ); + conf.set( 'x', [ 'y', 'z' ] ); + conf.set( 'num', 7 ); + conf.set( 'num2', 42 ); + assert.deepEqual( + conf.get( [ 'foo', 'x', 'num' ] ), + { foo: 'bar', x: [ 'y', 'z' ], num: 7 }, + 'get multiple' + ); + assert.deepEqual( + conf.get( [ 'foo', 'bar', 'x', 'y' ] ), + { foo: 'bar', bar: null, x: [ 'y', 'z' ], y: null }, + 'get multiple with some unknown' + ); + + assert.propEqual( + conf.get(), + { foo: 'bar', x: [ 'y', 'z' ], num: 7, num2: 42 }, + 'get all values' + ); + } ); + + // Expose 'values' getter with all values, for developer convenience on the console + QUnit.test( 'values', function ( assert ) { + var conf = new mw.Map(); + conf.set( { num: 7, num2: 42 } ); + conf.set( 'foo', 'bar' ); + + assert.propEqual( conf.values, { num: 7, num2: 42, foo: 'bar' } ); + } ); + + QUnit.test( 'set()', function ( assert ) { + var conf = new mw.Map(); + + // There should not be an implied default value + assert.false( conf.set( 'no-value' ), 'reject without value argument' ); + + assert.false( conf.set( funky, 'Funky' ), 'reject Function key' ); + assert.false( conf.set( arry, 'Arry' ), 'reject Array key' ); + assert.false( conf.set( 7, 'Nummy' ), 'reject number key' ); + assert.false( conf.set( null, 'Null' ), 'reject null key' ); + assert.false( conf.set( {}, 'Object' ), 'reject plain object as key' ); + + // Support storing `undefined`, get() will not return the default. + assert.true( conf.set( 'example', undefined ), 'Store the undefined value' ); + assert.strictEqual( conf.get( 'example' ), undefined, 'Get the undefined value' ); + assert.strictEqual( conf.get( 'example', 42 ), undefined, 'Get the undefined value (ignore default)' ); + + assert.true( conf.set( { key1: { x: 'x' }, key2: [ 'y' ] } ), 'set multiple' ); + assert.deepEqual( conf.get( 'key1' ), { x: 'x' } ); + assert.deepEqual( conf.get( 'key2' ), [ 'y' ] ); + } ); + + QUnit.test( 'exists()', function ( assert ) { + var conf = new mw.Map(); + + assert.false( conf.exists( 'doesNotExist' ), 'unknown' ); + + assert.true( conf.set( 'undef', undefined ) ); + assert.true( conf.exists( 'undef' ), 'known with undefined value' ); + + assert.true( conf.set( '7', 42 ) ); + assert.false( conf.exists( 7 ), 'unknown, with known number-like key' ); + } ); + + // Confirm protection against Object.prototype inheritance + QUnit.test( 'Avoid prototype pollution', function ( assert ) { + var conf = new mw.Map(); + + assert.strictEqual( conf.get( 'constructor' ), null, 'Get unknown "constructor"' ); + assert.strictEqual( conf.get( 'hasOwnProperty' ), null, 'Get unkonwn "hasOwnProperty"' ); + + conf.set( + 'hasOwnProperty', + function () { return true; } + ); + assert.strictEqual( conf.get( 'example', 'missing' ), 'missing', 'Use original hasOwnProperty method (positive)' ); + + conf.set( 'example', 'Foo' ); + conf.set( + 'hasOwnProperty', + function () { return false; } + ); + assert.strictEqual( conf.get( 'example' ), 'Foo', 'Use original hasOwnProperty method (negative)' ); + + assert.strictEqual( conf.set( 'constructor', 42 ), true, 'Set "constructor"' ); + assert.strictEqual( conf.get( 'constructor' ), 42, 'Get "constructor"' ); + } ); +}() );