es-map
Version:
An ES-spec-compliant Map shim/polyfill/replacement that works as far down as ES3
664 lines (534 loc) • 22.1 kB
JavaScript
var HasOwnProperty = require('es-abstract/2024/HasOwnProperty');
var isEnumerable = Object.prototype.propertyIsEnumerable;
var functionsHaveNames = require('functions-have-names')();
var define = require('define-properties');
var hasSymbols = require('has-symbols')();
var ArrayFrom = require('array.from');
var testMapping = function testMapping(t, map, key, value, desc) {
if (!desc) { desc = ''; } // eslint-disable-line no-param-reassign
t.equal(map.has(key), false, desc + ' - .has(key) returns false');
t.equal(map.get(key), undefined, desc + ' - .get(key) returns undefined');
t.equal(map.set(key, value), map, desc + ' - .set(key) returns the map');
t.equal(map.get(key), value, desc + ' - .get(key) returns the value');
t.equal(map.has(key), true, desc + ' - .has(key) returns true');
};
var entriesArray = function (map) {
var arr = [];
map.forEach(function (value, key) {
arr.push([key, value]);
});
return arr;
};
var range = function (from, to) {
var result = [];
for (var value = from; value < to; value++) {
result.push(value);
}
return result;
};
var prototypePropIsEnumerable = Object.prototype.propertyIsEnumerable.call(function () {}, 'prototype');
var expectNotEnumerable = function (t, object, desc) {
if (prototypePropIsEnumerable && typeof object === 'function') {
t.deepEqual(Object.keys(object), ['prototype'], desc);
} else {
t.deepEqual(Object.keys(object), [], desc);
}
};
module.exports = function runTests(Map, t) {
t.test('should has valid getter and setter calls', function (st) {
var map = new Map();
st.doesNotThrow(function () { map.get({}); });
st.doesNotThrow(function () { map.set({}); });
st.doesNotThrow(function () { map.has({}); });
st.doesNotThrow(function () { map['delete']({}); });
st.end();
});
t.test('throws when `.call`ed with an existing instance', function (st) {
var map = new Map();
st['throws'](function () { Map.call(map); }, 'Map can not be .called on an existing map');
st.end();
});
t.test('should accept an iterable as argument', function (st) {
var map = new Map();
testMapping(st, map, 'a', 'b', 'add "a"->"b" to map');
testMapping(st, map, 'c', 'd', 'add "c"->"d" to map');
var map2;
st.doesNotThrow(function () { map2 = new Map(map); });
st.ok(map2 instanceof Map, 'map2 is an instance of Map');
st.equal(map2.has('a'), true, 'map2 has "a"');
st.equal(map2.has('c'), true, 'map2 has "c"');
st.deepEqual(entriesArray(map2), [['a', 'b'], ['c', 'd']], 'entries are correct');
st.end();
});
t.test('should throw with iterables that return primitives', function (st) {
st['throws'](function () { return new Map('123'); }, TypeError, 'string');
st['throws'](function () { return new Map([1, 2, 3]); }, TypeError, 'array of numbers');
st['throws'](function () { return new Map(['1', '2', '3']); }, TypeError, 'array of strings');
st['throws'](function () { return new Map([true]); }, TypeError, 'array of booleans');
st.end();
});
t.test('should not be callable without "new"', function (st) {
st['throws'](Map, TypeError, 'call without new');
st.end();
});
t.test('should be subclassable', { skip: !Object.setPrototypeOf }, function (st) {
var MyMap = function MyMap() {
var testMap = new Map([['a', 'b']]);
Object.setPrototypeOf(testMap, MyMap.prototype);
return testMap;
};
Object.setPrototypeOf(MyMap, Map);
MyMap.prototype = Object.create(Map.prototype, {
constructor: { value: MyMap }
});
var myMap = new MyMap();
testMapping(st, myMap, 'c', 'd', 'add "c"->"d" to myMap');
st.deepEqual(entriesArray(myMap), [['a', 'b'], ['c', 'd']], 'entries are correct');
st.end();
});
t.test('uses SameValueZero even on a Map of size > 4', function (st) {
// Chrome 38-42, node 0.11/0.12, iojs 1/2 have a bug when the Map has a size > 4
var firstFour = [[1, 0], [2, 0], [3, 0], [4, 0]];
var fourMap = new Map(firstFour);
st.equal(fourMap.size, 4, 'fourMap contains 4 elements');
st.equal(fourMap.has(-0), false, 'fourMap doesn\'t contain -0');
st.equal(fourMap.has(0), false, 'fourMap doesn\'t contain 0');
fourMap.set(-0, fourMap);
st.equal(fourMap.has(0), true, 'fourMap contains 0');
st.equal(fourMap.has(-0), true, 'fourMap contains -0');
st.end();
});
t.test('treats positive and negative zero the same', function (st) {
var map = new Map();
var value1 = {};
var value2 = {};
testMapping(st, map, +0, value1, 'add +0->value1 to map');
st.equal(map.has(-0), true, 'map has -0');
st.equal(map.get(-0), value1, '-0 corresponds to value1');
st.equal(map.set(-0, value2), map, 'add -0->value2 to map');
st.equal(map.get(-0), value2, '-0 corresponds to value2');
st.equal(map.get(+0), value2, '+0 corresponds to value2');
st.end();
});
t.test('should map values correctly', function (st) {
/*
* Run this test twice, one with the "fast" implementation (which only
* allows string and numeric keys) and once with the "slow" impl.
*/
[true, false].forEach(function (slowkeys) {
var map = new Map();
range(1, 20).forEach(function (number) {
if (slowkeys) { testMapping(st, map, number, {}, 'an integer key to the map - slowkeys:' + slowkeys); }
testMapping(st, map, number / 100, {}, 'a rational key to map - slowkeys:' + slowkeys);
testMapping(st, map, 'key-' + number, {}, 'a string key to map - slowkeys:' + slowkeys);
testMapping(st, map, String(number), {}, 'a string key to map (2) - slowkeys:' + slowkeys);
if (slowkeys) { testMapping(st, map, Object(String(number)), {}, 'an object key to map - slowkeys:' + slowkeys); }
});
var testkeys = [Infinity, -Infinity, NaN];
if (slowkeys) {
testkeys.push(true, false, null, undefined);
}
testkeys.forEach(function (key) {
testMapping(st, map, key, {}, 'a ' + key + ' key to map - slowkeys:' + slowkeys);
testMapping(st, map, String(key), {}, 'a String(' + key + ') key to map - slowkeys:' + slowkeys);
});
testMapping(st, map, '', {}, 'an empty string key to map - slowkeys:' + slowkeys);
// verify that properties of Object don't peek through.
[
'hasOwnProperty',
'constructor',
'toString',
'isPrototypeOf',
'__proto__',
'__parent__',
'__count__'
].forEach(function (key) {
testMapping(st, map, key, {}, '"' + key + '" as a key in map');
});
});
st.end();
});
t.test('should map empty values correctly', function (st) {
var map = new Map();
testMapping(st, map, {}, true, 'test {} key');
testMapping(st, map, null, true, 'test null key');
testMapping(st, map, undefined, true, 'test undefined key');
testMapping(st, map, '', true, 'test "" key');
testMapping(st, map, NaN, true, 'test NaN key');
testMapping(st, map, 0, true, 'test 0 key');
st.end();
});
t.test('should has correct querying behavior', function (st) {
var map = new Map();
var key = {};
testMapping(st, map, key, 'to-be-present', 'add key to map');
st.equal(map.has(key), true, 'has that key');
st.equal(map.has({}), false, 'doesn\'t have a shallowly equal key');
st.equal(map.set(key, undefined), map, 'set that key to undefined');
st.equal(map.get(key), undefined, 'the corresponding value is undefined');
st.equal(map.has(key), true, 'the key is still present');
st.equal(map.has({}), false, 'the shallowly equal key is still not present');
st.end();
});
t.test('should allow NaN values as keys', function (st) {
var map = new Map();
st.equal(map.has(NaN), false, 'NaN not present');
st.equal(map.has(NaN + 1), false, 'different NaN not present');
st.equal(map.has(23), false, 'different number not present');
st.equal(map.set(NaN, 'value'), map, 'add a NaN key to the map');
st.equal(map.has(NaN), true, 'NaN is present');
st.equal(map.has(NaN + 1), true, 'a different NaN is present');
st.equal(map.has(23), false, 'different number still not present');
st.end();
});
t.test('should not have [[Enumerable]] props', function (st) {
expectNotEnumerable(st, Map, 'Map doesn\'t have enumerable props');
expectNotEnumerable(st, Map.prototype, 'Map.prototype doesn\'t have enumerable props');
expectNotEnumerable(st, new Map(), 'new Map() doesn\'t have enumerable props');
st.end();
});
t.test('should not have an own constructor', function (st) {
var map = new Map();
st.equal(HasOwnProperty(map, 'constructor'), false);
st.equal(map.constructor, Map);
st.end();
});
t.test('should allow common ecmascript idioms', function (st) {
var map = new Map();
st.ok(map instanceof Map);
st.equal(typeof Map.prototype.get, 'function');
st.equal(typeof Map.prototype.set, 'function');
st.equal(typeof Map.prototype.has, 'function');
st.equal(typeof Map.prototype['delete'], 'function');
st.end();
});
t.test('should have a unique constructor', function (st) {
st.notEqual(Map.prototype, Object.prototype);
st.end();
});
t.test('#clear()', function (st) {
st.ok(HasOwnProperty(Map.prototype, 'clear'), 'Map.prototype.clear exists');
st.equal(Map.prototype.clear.length, 0, 'Map.prototype.clear has length of 0');
st.test('function name', { skip: !functionsHaveNames }, function (sst) {
sst.equal(Map.prototype.clear.name, 'clear', 'Map.prototype.clear has name "clear"');
sst.end();
});
st.test('enumerability', { skip: !define.supportsDescriptors }, function (sst) {
sst.equal(false, isEnumerable.call(Map.prototype, 'clear'), 'Map.prototype.clear is not enumerable');
sst.end();
});
st.test('behavior', function (sst) {
var map = new Map();
sst.equal(map.set(1, 2), map);
sst.equal(map.set(5, 2), map);
sst.equal(map.size, 2);
sst.equal(map.has(5), true);
map.clear();
sst.equal(map.size, 0);
sst.equal(map.has(5), false);
sst.end();
});
st.end();
});
t.test('#keys()', function (st) {
st.ok(HasOwnProperty(Map.prototype, 'keys'), 'Map.prototype.keys exists');
st.equal(Map.prototype.keys.length, 0, 'Map.prototype.keys has length of 0');
st.test('function name', { skip: !functionsHaveNames }, function (sst) {
sst.equal(Map.prototype.keys.name, 'keys', 'Map.prototype.keys has name "keys"');
sst.end();
});
st.test('enumerability', { skip: !define.supportsDescriptors }, function (sst) {
sst.equal(false, isEnumerable.call(Map.prototype, 'keys'), 'Map.prototype.keys is not enumerable');
sst.end();
});
st.end();
});
t.test('#values()', function (st) {
st.ok(HasOwnProperty(Map.prototype, 'values'), 'Map.prototype.values exists');
st.equal(Map.prototype.values.length, 0, 'Map.prototype.values has length of 0');
st.test('function name', { skip: !functionsHaveNames }, function (sst) {
sst.equal(Map.prototype.values.name, 'values', 'Map.prototype.values has name "values"');
sst.end();
});
st.test('enumerability', { skip: !define.supportsDescriptors }, function (sst) {
sst.equal(false, isEnumerable.call(Map.prototype, 'values'), 'Map.prototype.values is not enumerable');
sst.end();
});
st.end();
});
t.test('#entries()', function (st) {
st.ok(HasOwnProperty(Map.prototype, 'entries'), 'Map.prototype.entries exists');
st.equal(Map.prototype.entries.length, 0, 'Map.prototype.entties has length of 0');
st.test('function name', { skip: !functionsHaveNames }, function (sst) {
sst.equal(Map.prototype.entries.name, 'entries', 'Map.prototype.entties has name "entries"');
sst.end();
});
st.test('enumerability', { skip: !define.supportsDescriptors }, function (sst) {
sst.equal(false, isEnumerable.call(Map.prototype, 'entries'), 'Map.prototype.entries is not enumerable');
sst.end();
});
st.end();
});
t.test('#size', { skip: !define.supportsDescriptors }, function (st) {
st.test('throws TypeError when accessed directly', function (sst) {
// see https://github.com/paulmillr/es6-shim/issues/176
sst['throws'](function () { return Map.prototype.size; }, TypeError, 'Map.prototype.set throws (1)');
sst['throws'](function () { return Map.prototype.size; }, TypeError, 'Map.prototype.set throws (2)');
sst.end();
});
st.test('is an accessor function on the prototype', function (sst) {
var desc = Object.getOwnPropertyDescriptor(Map.prototype, 'size');
sst.ok(HasOwnProperty(desc, 'get'), '#size descriptor has get');
sst.equal(typeof desc.get, 'function', '#size descriptor has a get function');
sst.ok(!HasOwnProperty(new Map(), 'size'), '.size is not an own property of new Map(');
sst.end();
});
st.end();
});
t.test('should return false when deleting a nonexistent key', function (st) {
var map = new Map();
st.equal(map.has('a'), false, 'map does not contain "a"');
st.equal(map['delete']('a'), false, 'deleting "a" returns false');
st.end();
});
t.test('should have keys, values and size props', function (st) {
var map = new Map();
st.equal(map.set('a', 1), map, 'add "a"->1 into the mapp');
st.equal(map.set('b', 2), map, 'add "b"->2 into the mapp');
st.equal(map.set('c', 3), map, 'add "c"->3 into the mapp');
st.equal(typeof map.keys, 'function', 'map.keys is a function');
st.equal(typeof map.values, 'function', 'map.values is a function');
st.equal(map.size, 3, 'map.size is a 3');
st.equal(map['delete']('a'), true, 'deleting "a" returns true');
st.equal(map.size, 2, 'map.size is 2');
st.end();
});
t.test('should have an iterator that works with native Array.from', { skip: !Array.from }, function (st) {
var map = new Map();
st.equal(map.set('a', 1), map, 'add "a"->1 to map');
st.equal(map.set('b', NaN), map, 'add "b"->NaN to map');
st.equal(map.set('c', false), map, 'add "c"->false to map');
st.deepEqual(Array.from(map), [['a', 1], ['b', NaN], ['c', false]], 'Array.from(map) returns the entries');
st.deepEqual(Array.from(map.keys()), ['a', 'b', 'c'], 'Array.from(map.keys()) returns the keys');
st.deepEqual(Array.from(map.values()), [1, NaN, false], 'Array.from(map.values()) returns the values');
st.deepEqual(Array.from(map.entries()), entriesArray(map), 'Array.from(map.entries()) returns the entries');
st.end();
});
t.test('should have an iterator that works with polyfilled Array.from', { skip: !hasSymbols }, function (st) {
var map = new Map();
st.equal(map.set('a', 1), map, 'add "a"->1 to map');
st.equal(map.set('b', NaN), map, 'add "b"->NaN to map');
st.equal(map.set('c', false), map, 'add "c"->false to map');
st.deepEqual(ArrayFrom(map), [['a', 1], ['b', NaN], ['c', false]], 'Array.from(map) returns the entries');
st.deepEqual(ArrayFrom(map.keys()), ['a', 'b', 'c'], 'Array.from(map.keys()) returns the keys');
st.deepEqual(ArrayFrom(map.values()), [1, NaN, false], 'Array.from(map.values()) returns the values');
st.deepEqual(ArrayFrom(map.entries()), [['a', 1], ['b', NaN], ['c', false]], 'Array.from(map.entries()) returns the entries');
st.end();
});
t.test('has the right default iteration function', { skip: !hasSymbols }, function (st) {
// fixed in Webkit https://bugs.webkit.org/show_bug.cgi?id=143838
st.ok(HasOwnProperty(Map.prototype, Symbol.iterator), 'Map.prototype as a [Symbol.iterator] property');
st.equal(Map.prototype[Symbol.iterator], Map.prototype.entries, 'Map.prototype[Symbol.iterator] is Map.prototype.entries');
st.end();
});
t.test('#forEach', function (st) {
var init = function () {
var mapToIterate = new Map();
mapToIterate.set('a', 1);
mapToIterate.set('b', 2);
mapToIterate.set('c', 3);
return mapToIterate;
};
st.ok(HasOwnProperty(Map.prototype, 'forEach'), 'Map.prototype.forEach exists');
st.equal(Map.prototype.forEach.length, 1, 'Map.prototype.forEach has length of 0');
st.test('function name', { skip: !functionsHaveNames }, function (sst) {
sst.equal(Map.prototype.forEach.name, 'forEach', 'Map.prototype.forEach has name "forEach"');
sst.end();
});
st.test('enumerability', { skip: !define.supportsDescriptors }, function (sst) {
sst.equal(false, isEnumerable.call(Map.prototype, 'forEach'), 'Map.prototype.forEach is not enumerable');
sst.end();
});
st.test('should be iterable via forEach', function (sst) {
var expectedMap = {
a: 1,
b: 2,
c: 3
};
var foundMap = {};
var map = init();
map.forEach(function (value, key, entireMap) {
sst.equal(entireMap, map, 'the third argument to the .forEach callback is the map');
foundMap[key] = value;
});
sst.deepEqual(foundMap, expectedMap, '.forEach visited all the entries');
sst.end();
});
st.test('should iterate over empty keys', function (sst) {
var mapWithEmptyKeys = new Map();
var expectedKeys = [{}, null, undefined, '', NaN, 0];
expectedKeys.forEach(function (key) {
mapWithEmptyKeys.set(key, true);
});
var foundKeys = [];
mapWithEmptyKeys.forEach(function (value, key, entireMap) {
sst.equal(entireMap.get(key), value, 'the value is correct');
foundKeys.push(key);
});
sst.deepEqual(foundKeys, expectedKeys, '.forEach visits empty keys');
sst.end();
});
st.test('should support the thisArg', function (sst) {
var context = {};
init().forEach(function () {
sst.equal(this, context, 'this === context');
}, context);
sst.end();
});
st.test('should not revisit modified keys', function (sst) {
var mapToIterate = init();
var hasModifiedA = false;
mapToIterate.forEach(function (value, key) {
if (!hasModifiedA && key === 'a') {
mapToIterate.set('a', 4);
hasModifiedA = true;
} else {
sst.notEqual(key, 'a', 'does not visit "a" again');
}
});
sst.end();
});
st.test('visits keys added in the iterator', function (sst) {
var mapToIterate = init();
var hasAdded = false;
var hasFoundD = false;
mapToIterate.forEach(function (value, key) {
if (!hasAdded) {
mapToIterate.set('d', 5);
hasAdded = true;
} else if (key === 'd') {
hasFoundD = true;
}
});
sst.ok(hasFoundD, 'has visited d after adding it');
sst.end();
});
st.test('visits keys added in the iterator when there is a deletion', function (sst) {
var hasSeenFour = false;
var mapToMutate = new Map();
mapToMutate.set('0', 42);
mapToMutate.forEach(function (value, key) {
if (key === '0') {
sst.ok(mapToMutate['delete']('0'), 'deletes "0"');
mapToMutate.set('4', 'a value');
} else if (key === '4') {
hasSeenFour = true;
}
});
sst.ok(hasSeenFour, 'has seen "4"');
sst.end();
});
st.test('does not visit keys deleted before a visit', function (sst) {
var mapToIterate = init();
var hasVisitedC = false;
var hasDeletedC = false;
mapToIterate.forEach(function (value, key) {
if (key === 'c') {
hasVisitedC = true;
}
if (!hasVisitedC && !hasDeletedC) {
hasDeletedC = mapToIterate['delete']('c');
sst.ok(hasDeletedC, 'has deleted "c"');
}
});
sst.notOk(hasVisitedC, 'has visited "c"');
sst.end();
});
st.test('should work after deletion of the current key', function (sst) {
var mapToIterate = init();
var expectedMap = {
a: 1,
b: 2,
c: 3
};
var foundMap = {};
mapToIterate.forEach(function (value, key) {
foundMap[key] = value;
sst.ok(mapToIterate['delete'](key), 'delete the current entry');
});
sst.deepEqual(foundMap, expectedMap, 'visits the correct entries');
sst.end();
});
st.test('should convert key -0 to +0', function (sst) {
var zeroMap = new Map();
var result = [];
zeroMap.set(-0, 'a');
zeroMap.forEach(function (value, key) {
result.push(String(1 / key) + ' ' + value);
});
zeroMap.set(1, 'b');
zeroMap.set(0, 'c'); // shouldn't cause reordering
zeroMap.forEach(function (value, key) {
result.push(String(1 / key) + ' ' + value);
});
sst.deepEqual(result, ['Infinity a', 'Infinity c', '1 b'], 'visits the entries in the correct order');
sst.end();
});
});
t.test('should preserve insertion order', function (st) {
var convertToPairs = function (item) { return [item, true]; };
var arr1 = ['d', 'a', 'b'];
var arr2 = [3, 2, 'z', 'a', 1];
var arr3 = [3, 2, 'z', {}, 'a', 1];
[arr1, arr2, arr3].forEach(function (array, i) {
var entries = array.map(convertToPairs);
st.deepEqual(entriesArray(new Map(entries)), entries, 'map ' + i + ' has the correct entries');
});
st.end();
});
t.test('map iteration', function (st) {
var map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
map.set('d', 4);
var keys = [];
var iterator = map.keys();
keys.push(iterator.next().value);
st.equal(map['delete']('a'), true, 'delete a');
st.equal(map['delete']('b'), true, 'delete b');
st.equal(map['delete']('c'), true, 'delete c');
map.set('e');
keys.push(iterator.next().value);
keys.push(iterator.next().value);
st.equal(iterator.next().done, true, 'iterator is exhausted');
map.set('f');
st.equal(iterator.next().done, true, 'iterator is still exhausted after adding an entry');
st.deepEqual(keys, ['a', 'd', 'e'], 'the iterator visited the correct keys');
st.end();
});
/*
* Disabled since we don't have Set here
* t.test('MapIterator identification test prototype inequality', { skip: !Object.getPrototypeOf }, function (st) {
* var mapEntriesProto = Object.getPrototypeOf(new Map().entries());
* var setEntriesProto = Object.getPrototypeOf(new Set().entries());
* st.notEqual(mapEntriesProto, setEntriesProto);
* });
*/
t.test('MapIterator identification', function (st) {
var fnMapValues = Map.prototype.values;
var mapSentinel = new Map([[1, 'MapSentinel']]);
var testMap = new Map();
var testMapValues = testMap.values();
st.equal(testMapValues.next.call(fnMapValues.call(mapSentinel)).value, 'MapSentinel', 'extracts value from a different map');
/*
* var testSet = new Set();
* var testSetValues = testSet.values();
* st.throws(function () {
* return testSetValues.next.call(fnMapValues.call(mapSentinel)).value;
* }, TypeError);
*/
st.end();
});
};
;