From 82677fff881446f4d24a24be09e174278ea6a231 Mon Sep 17 00:00:00 2001 From: Andrei Fangli Date: Mon, 24 Jun 2024 21:54:14 +0300 Subject: [PATCH] Added forEach implementation --- .../observableMap/ReadOnlyObservableMap.ts | 12 +- .../tests/ObservableMap.forEach.tests.ts | 117 ++++++++++++++++++ .../tests/ObservableMap.get.tests.ts | 12 +- .../tests/ObservableMap.has.tests.ts | 18 +-- 4 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src/collections/observableMap/tests/ObservableMap.forEach.tests.ts diff --git a/src/collections/observableMap/ReadOnlyObservableMap.ts b/src/collections/observableMap/ReadOnlyObservableMap.ts index c31e91d..21b3e9b 100644 --- a/src/collections/observableMap/ReadOnlyObservableMap.ts +++ b/src/collections/observableMap/ReadOnlyObservableMap.ts @@ -127,7 +127,14 @@ export class ReadOnlyObservableMap extends ViewModel implements IRe * @see [Map.forEach](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach) */ public forEach(callback: (this: TContext, item: TItem, key: TKey, map: this) => void, thisArg?: TContext): void { - throw new Error("Method not implemented."); + const changeTokenCopy = this._changeToken; + + for (const [key, item] of this) { + callback.call(thisArg, item, key, this); + + if (changeTokenCopy !== this._changeToken) + throw new Error('Map has changed while being iterated.'); + } } /** @@ -164,7 +171,8 @@ export class ReadOnlyObservableMap extends ViewModel implements IRe * @see [Map.clear](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) */ protected clear(): void { - throw new Error("Method not implemented."); + this._changeToken = 1; + // throw new Error("Method not implemented."); } } diff --git a/src/collections/observableMap/tests/ObservableMap.forEach.tests.ts b/src/collections/observableMap/tests/ObservableMap.forEach.tests.ts new file mode 100644 index 0000000..fd740cb --- /dev/null +++ b/src/collections/observableMap/tests/ObservableMap.forEach.tests.ts @@ -0,0 +1,117 @@ +import { ObservableMap } from '../ObservableMap'; +import { testBlankMutatingOperation } from './common'; + +describe('ObservableMap.forEach', (): void => { + it('iterating over an empty map does not invoke the callback', (): void => { + let mapInvocationCount = 0; + let observableMapInvocationCount = 0; + + testBlankMutatingOperation({ + initialState: [], + + applyOperation: { + applyMapOperation: map => map.forEach(_ => mapInvocationCount++), + applyObservableMapOperation: observableMap => observableMap.forEach(_ => observableMapInvocationCount++) + }, + + expectedResult: undefined + }); + + expect(mapInvocationCount).toBe(0); + expect(observableMapInvocationCount).toBe(0); + }); + + it('iterating over a map invokes the callback for each item', (): void => { + const mapItems: (readonly [string, number])[] = []; + const observableMapItems: (readonly [string, number])[] = []; + + testBlankMutatingOperation({ + initialState: [ + ['a', 1], + ['b', 2], + ['c', 3] + ], + + applyOperation: { + applyMapOperation: map => map.forEach((item, key) => mapItems.push([key, item])), + applyObservableMapOperation: observableMap => observableMap.forEach((item, key) => observableMapItems.push([key, item])) + }, + + expectedResult: undefined + }); + + expect(observableMapItems).toEqual(mapItems); + }); + + it('calling forEach passes arguments to each parameter accordingly', (): void => { + let invocationCount = 0; + const observableMap = new ObservableMap([ + [1, 'a'] + ]); + observableMap.forEach((item, key, map) => { + invocationCount++; + + expect(key).toBe(1); + expect(item).toBe('a'); + expect(map).toStrictEqual(observableMap); + + return true; + }); + + expect(invocationCount).toBe(1); + }); + + it('calling forEach with context passes it to the callback', (): void => { + let invocationCount = 0; + const context = {}; + const observableMap = new ObservableMap([ + [1, 'a'] + ]); + observableMap.forEach( + function (item, key, map) { + invocationCount++; + + expect(this).toStrictEqual(context); + expect(key).toBe(1); + expect(item).toBe('a'); + expect(map).toStrictEqual(observableMap); + + return true; + }, + context + ); + + expect(invocationCount).toBe(1); + }); + + it('modifying the map while executing forEach throws exception', (): void => { + expect( + () => { + const observableMap = new ObservableMap([ + [1, 'a'], + [2, 'b'], + [3, 'c'] + ]); + observableMap.forEach(_ => { + observableMap.clear(); + }); + }) + .toThrow(new Error('Map has changed while being iterated.')); + }); + + it('calling forEach while iterating will not break iterators', (): void => { + expect( + () => { + const observableMap = new ObservableMap([ + [1, 'a'], + [2, 'b'], + [3, 'c'] + ]); + + for (const _ of observableMap) + observableMap.forEach(_ => { }); + }) + .not + .toThrow(); + }); +}); \ No newline at end of file diff --git a/src/collections/observableMap/tests/ObservableMap.get.tests.ts b/src/collections/observableMap/tests/ObservableMap.get.tests.ts index 8e383de..86f106c 100644 --- a/src/collections/observableMap/tests/ObservableMap.get.tests.ts +++ b/src/collections/observableMap/tests/ObservableMap.get.tests.ts @@ -31,9 +31,9 @@ describe('ObserableMap.get', (): void => { it('looking up item by non-existing key returns undefined', (): void => { testBlankMutatingOperation({ initialState: [ - [1, '1'], - [2, '2'], - [3, '3'] + [1, 'a'], + [2, 'b'], + [3, 'c'] ], applyOperation: map => map.get(4), @@ -46,9 +46,9 @@ describe('ObserableMap.get', (): void => { expect( () => { const observableMap = new ObservableMap([ - [1, '1'], - [2, '2'], - [3, '3'] + [1, 'a'], + [2, 'b'], + [3, 'c'] ]); let valueToCheck = 2; diff --git a/src/collections/observableMap/tests/ObservableMap.has.tests.ts b/src/collections/observableMap/tests/ObservableMap.has.tests.ts index 3c71c8c..33a635e 100644 --- a/src/collections/observableMap/tests/ObservableMap.has.tests.ts +++ b/src/collections/observableMap/tests/ObservableMap.has.tests.ts @@ -15,9 +15,9 @@ describe('ObserableMap.has', (): void => { it('checking if existing item is part of map returns true', (): void => { testBlankMutatingOperation({ initialState: [ - [1, '1'], - [2, '2'], - [3, '3'] + [1, 'a'], + [2, 'b'], + [3, 'c'] ], applyOperation: map => map.has(2), @@ -29,9 +29,9 @@ describe('ObserableMap.has', (): void => { it('checking if non-existing item is part of map returns false', (): void => { testBlankMutatingOperation({ initialState: [ - [1, '1'], - [2, '2'], - [3, '3'] + [1, 'a'], + [2, 'b'], + [3, 'c'] ], applyOperation: map => map.has(4), @@ -44,9 +44,9 @@ describe('ObserableMap.has', (): void => { expect( () => { const observableMap = new ObservableMap([ - [1, '1'], - [2, '2'], - [3, '3'] + [1, 'a'], + [2, 'b'], + [3, 'c'] ]); let valueToCheck = 2;