blob: 89dbea664773522ecd145dc0f81eab1a9c60e153 [file] [log] [blame] [edit]
function shouldBe(a, b) {
if (a !== b)
throw new Error(`Expected ${b} but got ${a}`);
}
function shouldBeArray(a, b) {
shouldBe(a.length, b.length);
for (let i = 0; i < a.length; i++) {
if (Array.isArray(b[i])) {
shouldBeArray(a[i], b[i]);
} else {
shouldBe(a[i], b[i]);
}
}
}
function testKeys(map) {
return Array.from(map.keys());
}
noInline(testKeys);
function testValues(map) {
return Array.from(map.values());
}
noInline(testValues);
function testEntries(map) {
return Array.from(map.entries());
}
noInline(testEntries);
// ========== Basic tests for keys() ==========
// int32 keys
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const array = testKeys(map);
shouldBe(array.length, 3);
shouldBe(array[0], 1);
shouldBe(array[1], 2);
shouldBe(array[2], 3);
}
// double keys
{
const map = new Map([[1.1, 'a'], [2.2, 'b'], [3.3, 'c']]);
const array = testKeys(map);
shouldBe(array.length, 3);
shouldBe(array[0], 1.1);
shouldBe(array[1], 2.2);
shouldBe(array[2], 3.3);
}
// object keys (contiguous)
{
const key1 = { k: 1 };
const key2 = { k: 2 };
const key3 = { k: 3 };
const map = new Map([[key1, 'a'], [key2, 'b'], [key3, 'c']]);
const array = testKeys(map);
shouldBe(array.length, 3);
shouldBe(array[0], key1);
shouldBe(array[1], key2);
shouldBe(array[2], key3);
}
// ========== Basic tests for values() ==========
// int32 values
{
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const array = testValues(map);
shouldBe(array.length, 3);
shouldBe(array[0], 1);
shouldBe(array[1], 2);
shouldBe(array[2], 3);
}
// double values
{
const map = new Map([['a', 1.1], ['b', 2.2], ['c', 3.3]]);
const array = testValues(map);
shouldBe(array.length, 3);
shouldBe(array[0], 1.1);
shouldBe(array[1], 2.2);
shouldBe(array[2], 3.3);
}
// object values (contiguous)
{
const val1 = { v: 1 };
const val2 = { v: 2 };
const val3 = { v: 3 };
const map = new Map([['a', val1], ['b', val2], ['c', val3]]);
const array = testValues(map);
shouldBe(array.length, 3);
shouldBe(array[0], val1);
shouldBe(array[1], val2);
shouldBe(array[2], val3);
}
// ========== Basic tests for entries() ==========
// entries always returns arrays (contiguous)
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const array = testEntries(map);
shouldBe(array.length, 3);
shouldBeArray(array[0], [1, 'a']);
shouldBeArray(array[1], [2, 'b']);
shouldBeArray(array[2], [3, 'c']);
}
{
const map = new Map([['x', 100], ['y', 200]]);
const array = testEntries(map);
shouldBe(array.length, 2);
shouldBeArray(array[0], ['x', 100]);
shouldBeArray(array[1], ['y', 200]);
}
// ========== Empty map ==========
{
const map = new Map();
shouldBe(testKeys(map).length, 0);
shouldBe(testValues(map).length, 0);
shouldBe(testEntries(map).length, 0);
}
// ========== Large map ==========
{
const map = new Map();
for (let i = 0; i < 1000; i++) {
map.set(i, i * 2);
}
const keys = testKeys(map);
shouldBe(keys.length, 1000);
for (let i = 0; i < 1000; i++) {
shouldBe(keys[i], i);
}
const values = testValues(map);
shouldBe(values.length, 1000);
for (let i = 0; i < 1000; i++) {
shouldBe(values[i], i * 2);
}
const entries = testEntries(map);
shouldBe(entries.length, 1000);
for (let i = 0; i < 1000; i++) {
shouldBeArray(entries[i], [i, i * 2]);
}
}
// ========== Map with deleted elements ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c'], [4, 'd'], [5, 'e']]);
map.delete(2);
map.delete(4);
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], 1);
shouldBe(keys[1], 3);
shouldBe(keys[2], 5);
const values = testValues(map);
shouldBe(values.length, 3);
shouldBe(values[0], 'a');
shouldBe(values[1], 'c');
shouldBe(values[2], 'e');
}
// ========== Iteration order is preserved ==========
{
const map = new Map();
map.set(3, 'three');
map.set(1, 'one');
map.set(4, 'four');
map.set(1, 'ONE'); // update, should not change order
map.set(5, 'five');
const keys = testKeys(map);
shouldBe(keys.length, 4);
shouldBe(keys[0], 3);
shouldBe(keys[1], 1);
shouldBe(keys[2], 4);
shouldBe(keys[3], 5);
}
// ========== Special values ==========
{
const map = new Map([
[NaN, 'nan'],
[Infinity, 'inf'],
[-Infinity, '-inf'],
[0, 'zero'],
[null, 'null'],
[undefined, 'undef']
]);
const keys = testKeys(map);
shouldBe(keys.length, 6);
shouldBe(Number.isNaN(keys[0]), true);
shouldBe(keys[1], Infinity);
shouldBe(keys[2], -Infinity);
shouldBe(keys[3], 0);
shouldBe(keys[4], null);
shouldBe(keys[5], undefined);
}
// ========== String keys ==========
{
const map = new Map([['hello', 1], ['world', 2], ['', 3]]);
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], 'hello');
shouldBe(keys[1], 'world');
shouldBe(keys[2], '');
}
// ========== Symbol keys ==========
{
const sym1 = Symbol('a');
const sym2 = Symbol('b');
const sym3 = Symbol.for('c');
const map = new Map([[sym1, 1], [sym2, 2], [sym3, 3]]);
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], sym1);
shouldBe(keys[1], sym2);
shouldBe(keys[2], sym3);
}
// ========== BigInt ==========
{
const map = new Map([[1n, 'one'], [2n, 'two'], [9007199254740993n, 'big']]);
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], 1n);
shouldBe(keys[1], 2n);
shouldBe(keys[2], 9007199254740993n);
}
// ========== Single element ==========
{
const map = new Map([[42, 'answer']]);
shouldBe(testKeys(map).length, 1);
shouldBe(testKeys(map)[0], 42);
shouldBe(testValues(map).length, 1);
shouldBe(testValues(map)[0], 'answer');
}
// ========== Re-adding deleted element changes order ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
map.delete(2);
map.set(2, 'B');
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], 1);
shouldBe(keys[1], 3);
shouldBe(keys[2], 2);
}
// ========== Modified Map.prototype[Symbol.iterator] should use slow path ==========
{
const originalIterator = Map.prototype[Symbol.iterator];
Map.prototype[Symbol.iterator] = function* () {
yield [100, 'hundred'];
yield [200, 'two hundred'];
};
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
// Note: keys() uses its own iterator, not [Symbol.iterator]
// So this test is about entries behavior via Symbol.iterator
Map.prototype[Symbol.iterator] = originalIterator;
}
// ========== Modified %MapIteratorPrototype%.next should use slow path ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const mapIteratorPrototype = Object.getPrototypeOf(map.keys());
const originalNext = mapIteratorPrototype.next;
let callCount = 0;
mapIteratorPrototype.next = function() {
callCount++;
return originalNext.call(this);
};
const array = Array.from(map.keys());
shouldBe(array.length, 3);
shouldBe(array[0], 1);
shouldBe(array[1], 2);
shouldBe(array[2], 3);
// If slow path was used, next() should have been called
shouldBe(callCount > 0, true);
mapIteratorPrototype.next = originalNext;
}
// ========== Iterator already started should use slow path ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const iter = map.keys();
iter.next(); // Consume one element
const result = Array.from(iter);
shouldBe(result.length, 2);
shouldBe(result[0], 2);
shouldBe(result[1], 3);
}
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const iter = map.values();
iter.next(); // Consume one element
const result = Array.from(iter);
shouldBe(result.length, 2);
shouldBe(result[0], 'b');
shouldBe(result[1], 'c');
}
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const iter = map.entries();
iter.next(); // Consume one element
const result = Array.from(iter);
shouldBe(result.length, 2);
shouldBeArray(result[0], [2, 'b']);
shouldBeArray(result[1], [3, 'c']);
}
// ========== Map subclass ==========
{
class MyMap extends Map {
constructor(iterable) {
super(iterable);
}
}
const map = new MyMap([[1, 'a'], [2, 'b'], [3, 'c']]);
const keys = Array.from(map.keys());
shouldBe(keys.length, 3);
shouldBe(keys[0], 1);
shouldBe(keys[1], 2);
shouldBe(keys[2], 3);
}
// ========== Array.from with mapFn ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const array = Array.from(map.keys(), x => x * 2);
shouldBe(array.length, 3);
shouldBe(array[0], 2);
shouldBe(array[1], 4);
shouldBe(array[2], 6);
}
// ========== Array.from with mapFn and thisArg ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
const obj = { multiplier: 10 };
const array = Array.from(map.keys(), function(x) { return x * this.multiplier; }, obj);
shouldBe(array.length, 3);
shouldBe(array[0], 10);
shouldBe(array[1], 20);
shouldBe(array[2], 30);
}
// ========== Very large map ==========
{
const map = new Map();
for (let i = 0; i < 10000; i++) {
map.set(i, `value${i}`);
}
const keys = testKeys(map);
shouldBe(keys.length, 10000);
shouldBe(keys[0], 0);
shouldBe(keys[9999], 9999);
const values = testValues(map);
shouldBe(values.length, 10000);
shouldBe(values[0], 'value0');
shouldBe(values[9999], 'value9999');
}
// ========== Map with many deletions ==========
{
const map = new Map();
for (let i = 0; i < 100; i++) {
map.set(i, `val${i}`);
}
for (let i = 0; i < 100; i += 2) {
map.delete(i);
}
const keys = testKeys(map);
shouldBe(keys.length, 50);
for (let i = 0; i < 50; i++) {
shouldBe(keys[i], i * 2 + 1);
}
}
// ========== Type promotions ==========
// int32 to double promotion for keys
{
const map = new Map([[1, 'a'], [2, 'b'], [3.5, 'c'], [4, 'd']]);
const keys = testKeys(map);
shouldBe(keys.length, 4);
shouldBe(keys[0], 1);
shouldBe(keys[1], 2);
shouldBe(keys[2], 3.5);
shouldBe(keys[3], 4);
}
// int32 to contiguous promotion for keys
{
const obj = {};
const map = new Map([[1, 'a'], [2, 'b'], [obj, 'c'], [4, 'd']]);
const keys = testKeys(map);
shouldBe(keys.length, 4);
shouldBe(keys[0], 1);
shouldBe(keys[1], 2);
shouldBe(keys[2], obj);
shouldBe(keys[3], 4);
}
// int32 to double promotion for values
{
const map = new Map([['a', 1], ['b', 2], ['c', 3.5], ['d', 4]]);
const values = testValues(map);
shouldBe(values.length, 4);
shouldBe(values[0], 1);
shouldBe(values[1], 2);
shouldBe(values[2], 3.5);
shouldBe(values[3], 4);
}
// int32 to contiguous promotion for values
{
const obj = {};
const map = new Map([['a', 1], ['b', 2], ['c', obj], ['d', 4]]);
const values = testValues(map);
shouldBe(values.length, 4);
shouldBe(values[0], 1);
shouldBe(values[1], 2);
shouldBe(values[2], obj);
shouldBe(values[3], 4);
}
// ========== Repeated calls with same map ==========
{
const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']]);
for (let i = 0; i < 1000; i++) {
const keys = testKeys(map);
shouldBe(keys.length, 3);
shouldBe(keys[0], 1);
shouldBe(keys[1], 2);
shouldBe(keys[2], 3);
}
}
// ========== Verify returned array is a proper Array ==========
{
const map = new Map([[1, 'a'], [2, 'b']]);
const keys = testKeys(map);
shouldBe(Array.isArray(keys), true);
shouldBe(keys.constructor, Array);
shouldBe(Object.getPrototypeOf(keys), Array.prototype);
}
// ========== Verify returned array is mutable ==========
{
const map = new Map([[1, 'a'], [2, 'b']]);
const keys = testKeys(map);
keys.push(99);
shouldBe(keys.length, 3);
shouldBe(keys[2], 99);
keys[0] = 100;
shouldBe(keys[0], 100);
}
// ========== Map containing functions ==========
{
const fn1 = () => 1;
const fn2 = function() { return 2; };
const map = new Map([[fn1, 'fn1'], [fn2, 'fn2']]);
const keys = testKeys(map);
shouldBe(keys.length, 2);
shouldBe(keys[0], fn1);
shouldBe(keys[1], fn2);
}
// ========== Map containing arrays ==========
{
const arr1 = [1, 2];
const arr2 = [3, 4];
const map = new Map([[arr1, 'arr1'], [arr2, 'arr2']]);
const keys = testKeys(map);
shouldBe(keys.length, 2);
shouldBe(keys[0], arr1);
shouldBe(keys[1], arr2);
}
// ========== Mixed key and value types in entries ==========
{
const obj = { x: 1 };
const map = new Map([
[1, 'one'],
['two', 2],
[obj, [3, 4]],
[null, undefined]
]);
const entries = testEntries(map);
shouldBe(entries.length, 4);
shouldBeArray(entries[0], [1, 'one']);
shouldBeArray(entries[1], ['two', 2]);
shouldBe(entries[2][0], obj);
shouldBeArray(entries[2][1], [3, 4]);
shouldBe(entries[3][0], null);
shouldBe(entries[3][1], undefined);
}