diff --git a/packages/core/src/collection/collection.persistent.ts b/packages/core/src/collection/collection.persistent.ts index f7425ff1..86b049a3 100644 --- a/packages/core/src/collection/collection.persistent.ts +++ b/packages/core/src/collection/collection.persistent.ts @@ -156,7 +156,7 @@ export class CollectionPersistent< // If successfully loaded Item value, assign Item to Collection if (loadedPersistedValueIntoItem) - this.collection().assignItem(dummyItem); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method) + this.collection().assignItem(dummyItem, { overwrite: false }); // TODO SECOND GROUP REBUILD (by calling rebuildGroupThatInclude() in the assignItem() method) } } } diff --git a/packages/core/src/collection/index.ts b/packages/core/src/collection/index.ts index edf12d81..e4aec4b3 100644 --- a/packages/core/src/collection/index.ts +++ b/packages/core/src/collection/index.ts @@ -193,7 +193,7 @@ export class Collection { * @param config - Configuration object */ public Selector( - initialKey: ItemKey, + initialKey: ItemKey | null, config: SelectorConfigInterface = {} ): Selector { if (this.isInstantiated) { @@ -489,7 +489,7 @@ export class Collection { * @param config - Configuration object */ public getGroup( - groupKey: GroupKey | undefined, + groupKey: GroupKey | undefined | null, config: HasConfigInterface = {} ): Group | undefined { config = defineConfig(config, { @@ -589,7 +589,7 @@ export class Collection { */ public createSelector( selectorKey: SelectorKey, - itemKey: ItemKey + itemKey: ItemKey | null ): Selector { let selector = this.getSelector(selectorKey, { notExisting: true }); if (!this.isInstantiated) LogCodeManager.log('1B:02:04'); @@ -662,7 +662,7 @@ export class Collection { * @param config - Configuration object */ public getSelector( - selectorKey: SelectorKey | undefined, + selectorKey: SelectorKey | undefined | null, config: HasConfigInterface = {} ): Selector | undefined { config = defineConfig(config, { @@ -699,14 +699,10 @@ export class Collection { // Create dummy Selector to hold reference if (selector == null) { - selector = new Selector( - this, - Selector.unknownItemPlaceholderKey, - { - key: selectorKey, - isPlaceholder: true, - } - ); + selector = new Selector(this, null, { + key: selectorKey, + isPlaceholder: true, + }); this.selectors[selectorKey] = selector; } @@ -773,7 +769,7 @@ export class Collection { * @param config - Configuration object */ public getItem( - itemKey: ItemKey | undefined, + itemKey: ItemKey | undefined | null, config: HasConfigInterface = {} ): Item | undefined { config = defineConfig(config, { @@ -1425,9 +1421,11 @@ export class Collection { } // Check if Item already exists - if (this.getItem(itemKey) != null) { - if (!config.overwrite) return true; - else increaseCollectionSize = false; + if (this.getItem(itemKey, { notExisting: true }) != null) { + if (!config.overwrite) { + this.assignData(item._value); + return true; + } else increaseCollectionSize = false; } // Assign/add Item to Collection diff --git a/packages/core/src/collection/selector.ts b/packages/core/src/collection/selector.ts index 1366f24d..5e484e9d 100644 --- a/packages/core/src/collection/selector.ts +++ b/packages/core/src/collection/selector.ts @@ -8,20 +8,19 @@ import { StateRuntimeJobConfigInterface, } from '../internal'; -export class Selector extends State< - DataType | undefined -> { +export class Selector< + DataType extends Object = DefaultItem +> extends State { // Collection the Selector belongs to public collection: () => Collection; - static unknownItemPlaceholderKey = '__UNKNOWN__ITEM__KEY__'; static rebuildSelectorSideEffectKey = 'rebuildSelector'; static rebuildItemSideEffectKey = 'rebuildItem'; // Item the Selector represents - public _item: Item | undefined; + public _item: Item | null; // Key/Name identifier of the Item the Selector represents - public _itemKey: ItemKey; + public _itemKey: ItemKey | null; /** * A Selector represents an Item from a Collection in the long term. @@ -39,23 +38,21 @@ export class Selector extends State< */ constructor( collection: Collection, - itemKey: ItemKey, + itemKey: ItemKey | null, config: SelectorConfigInterface = {} ) { config = defineConfig(config, { isPlaceholder: false, }); - super(collection.agileInstance(), undefined, config); + super(collection.agileInstance(), null, config); this.collection = () => collection; - this._item = undefined; - this._itemKey = !config.isPlaceholder - ? itemKey - : Selector.unknownItemPlaceholderKey; + this._item = null; + this._itemKey = !config.isPlaceholder && itemKey != null ? itemKey : null; this._key = config?.key; this.isPlaceholder = true; // Because hasn't selected any Item yet // Initial select of the Item - if (!config.isPlaceholder) this.select(itemKey, { overwrite: true }); + if (this._itemKey != null) this.select(itemKey, { overwrite: true }); } /** @@ -65,7 +62,7 @@ export class Selector extends State< * * @public */ - public get itemKey(): ItemKey { + public get itemKey(): ItemKey | null { return this._itemKey; } @@ -78,7 +75,7 @@ export class Selector extends State< * @public * @param value - New key/name identifier of the Item to be represented by the Selector. */ - public set itemKey(value: ItemKey) { + public set itemKey(value: ItemKey | null) { this.select(value); } @@ -89,7 +86,7 @@ export class Selector extends State< * * @public */ - public get item(): Item | undefined { + public get item(): Item | null { return this._item; } @@ -102,7 +99,7 @@ export class Selector extends State< * @public * @param value - New Item to be represented by the Selector. */ - public set item(value: Item | undefined) { + public set item(value: Item | null) { if (value?._key) this.select(value._key); } @@ -116,7 +113,7 @@ export class Selector extends State< * @param config - Configuration object */ public select( - itemKey: ItemKey, + itemKey: ItemKey | null, config: StateRuntimeJobConfigInterface = {} ): this { config = defineConfig(config, { @@ -140,7 +137,9 @@ export class Selector extends State< return this; // Unselect old Item - this.unselect({ background: true }); + this.unselect({ background: itemKey != null }); + + if (itemKey == null) return this; // Retrieve new Item from Collection const newItem = this.collection().getItemWithReference(itemKey); @@ -224,16 +223,20 @@ export class Selector extends State< }); // Unselect Item - if (item) { + if (item != null) { item.selectedBy.delete(this._key as any); item.removeSideEffect(Selector.rebuildSelectorSideEffectKey); item.removeSideEffect(Selector.rebuildItemSideEffectKey); - if (item.isPlaceholder) delete this.collection().data[this._itemKey]; + if ( + item.isPlaceholder && + this._itemKey != null + ) + delete this.collection().data[this._itemKey]; } // Reset Selector - this._item = undefined; - this._itemKey = Selector.unknownItemPlaceholderKey; + this._item = null; + this._itemKey = null; this.rebuildSelector(config); this.isPlaceholder = true; @@ -250,7 +253,10 @@ export class Selector extends State< * @param itemKey - Key/Name identifier of the Item. * @param correctlySelected - Whether the Item has to be selected correctly. */ - public hasSelected(itemKey: ItemKey, correctlySelected = true): boolean { + public hasSelected( + itemKey: ItemKey | null, + correctlySelected = true + ): boolean { if (correctlySelected) { return ( this._itemKey === itemKey && @@ -273,7 +279,7 @@ export class Selector extends State< public rebuildSelector(config: StateRuntimeJobConfigInterface = {}): this { // Assign 'undefined' to the Selector value if no Item is set if (this._item == null || this._item.isPlaceholder) { - this.set(undefined, config); + this.set(null, config); return this; } diff --git a/packages/core/tests/unit/collection/collection.persistent.test.ts b/packages/core/tests/unit/collection/collection.persistent.test.ts index 764b482c..3ebbc97c 100644 --- a/packages/core/tests/unit/collection/collection.persistent.test.ts +++ b/packages/core/tests/unit/collection/collection.persistent.test.ts @@ -414,9 +414,9 @@ describe('CollectionPersistent Tests', () => { followCollectionPersistKeyPattern: false, } ); - expect(dummyCollection.assignItem).toHaveBeenCalledWith( - placeholderItem1 - ); + expect( + dummyCollection.assignItem + ).toHaveBeenCalledWith(placeholderItem1, { overwrite: false }); expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem2 ); // Because Item persistent isn't ready @@ -507,9 +507,9 @@ describe('CollectionPersistent Tests', () => { followCollectionPersistKeyPattern: false, } ); - expect(dummyCollection.assignItem).toHaveBeenCalledWith( - placeholderItem1 - ); + expect( + dummyCollection.assignItem + ).toHaveBeenCalledWith(placeholderItem1, { overwrite: false }); expect(dummyCollection.assignItem).not.toHaveBeenCalledWith( placeholderItem3 ); // Because Item 3 is already present in the Collection diff --git a/packages/core/tests/unit/collection/collection.test.ts b/packages/core/tests/unit/collection/collection.test.ts index 3ad578f8..8d709e8d 100644 --- a/packages/core/tests/unit/collection/collection.test.ts +++ b/packages/core/tests/unit/collection/collection.test.ts @@ -1414,7 +1414,10 @@ describe('Collection Tests', () => { expect(response).toBeInstanceOf(Selector); expect(response.isPlaceholder).toBeTruthy(); + expect(response._item).toBeNull(); + expect(response._itemKey).toBeNull(); expect(response._key).toBe('notExistingSelector'); + expect(collection.selectors['notExistingSelector']).toBe(response); expect(ComputedTracker.tracked).toHaveBeenCalledWith(response.observer); }); @@ -2832,6 +2835,7 @@ describe('Collection Tests', () => { dummyItem1.patch = jest.fn(); toAddDummyItem2.patch = jest.fn(); collection.rebuildGroupsThatIncludeItemKey = jest.fn(); + collection.assignData = jest.fn(); }); it('should assign valid Item to Collection (default config)', () => { @@ -2847,6 +2851,7 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); expect(toAddDummyItem2._key).toBe('dummyItem2'); @@ -2870,6 +2875,7 @@ describe('Collection Tests', () => { background: true, } ); + expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); expect(toAddDummyItem2._key).toBe('dummyItem2'); @@ -2895,6 +2901,7 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).toHaveBeenCalledWith( { id: 'randomDummyId' }, @@ -2923,6 +2930,7 @@ describe('Collection Tests', () => { expect( collection.rebuildGroupsThatIncludeItemKey ).not.toHaveBeenCalled(); + expect(collection.assignData).not.toHaveBeenCalled(); expect(toAddDummyItem2.patch).not.toHaveBeenCalled(); expect(toAddDummyItem2._key).toBe('dummyItem2'); @@ -2944,6 +2952,7 @@ describe('Collection Tests', () => { expect( collection.rebuildGroupsThatIncludeItemKey ).not.toHaveBeenCalled(); + expect(collection.assignData).toHaveBeenCalledWith(dummyItem1._value); expect(dummyItem1.patch).not.toHaveBeenCalled(); expect(dummyItem1._key).toBe('dummyItem1'); @@ -2965,6 +2974,7 @@ describe('Collection Tests', () => { background: false, } ); + expect(collection.assignData).not.toHaveBeenCalled(); expect(dummyItem1.patch).not.toHaveBeenCalled(); expect(dummyItem1._key).toBe('dummyItem1'); diff --git a/packages/core/tests/unit/collection/selector.test.ts b/packages/core/tests/unit/collection/selector.test.ts index cb4b2f87..96e6170b 100644 --- a/packages/core/tests/unit/collection/selector.test.ts +++ b/packages/core/tests/unit/collection/selector.test.ts @@ -29,20 +29,21 @@ describe('Selector Tests', () => { const selector = new Selector(dummyCollection, 'dummyItemKey'); expect(selector.collection()).toBe(dummyCollection); - expect(selector._item).toBeUndefined(); + expect(selector._item).toBeNull(); // Because 'select()' is mocked expect(selector._itemKey).toBe('dummyItemKey'); expect(selector.select).toHaveBeenCalledWith('dummyItemKey', { overwrite: true, }); + // Check if State was called with correct parameters expect(selector._key).toBeUndefined(); expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); - expect(selector.initialStateValue).toBeUndefined(); - expect(selector._value).toBeUndefined(); - expect(selector.previousStateValue).toBeUndefined(); - expect(selector.nextStateValue).toBeUndefined(); + expect(selector.initialStateValue).toBeNull(); + expect(selector._value).toBeNull(); + expect(selector.previousStateValue).toBeNull(); + expect(selector.nextStateValue).toBeNull(); expect(selector.observer).toBeInstanceOf(StateObserver); expect(selector.observer.dependents.size).toBe(0); expect(selector.observer._key).toBeUndefined(); @@ -65,20 +66,21 @@ describe('Selector Tests', () => { }); expect(selector.collection()).toBe(dummyCollection); - expect(selector._item).toBeUndefined(); + expect(selector._item).toBeNull(); // Because 'select()' is mocked expect(selector._itemKey).toBe('dummyItemKey'); expect(selector.select).toHaveBeenCalledWith('dummyItemKey', { overwrite: true, }); + // Check if State was called with correct parameters expect(selector._key).toBe('dummyKey'); expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); - expect(selector.initialStateValue).toBeUndefined(); - expect(selector._value).toBeUndefined(); - expect(selector.previousStateValue).toBeUndefined(); - expect(selector.nextStateValue).toBeUndefined(); + expect(selector.initialStateValue).toBeNull(); + expect(selector._value).toBeNull(); + expect(selector.previousStateValue).toBeNull(); + expect(selector.nextStateValue).toBeNull(); expect(selector.observer).toBeInstanceOf(StateObserver); expect(selector.observer.dependents.size).toBe(0); expect(selector.observer._key).toBe('dummyKey'); @@ -101,18 +103,52 @@ describe('Selector Tests', () => { }); expect(selector.collection()).toBe(dummyCollection); - expect(selector._item).toBeUndefined(); - expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); + expect(selector._item).toBeNull(); + expect(selector._itemKey).toBeNull(); expect(selector.select).not.toHaveBeenCalled(); + // Check if State was called with correct parameters expect(selector._key).toBeUndefined(); expect(selector.valueType).toBeUndefined(); expect(selector.isSet).toBeFalsy(); expect(selector.isPlaceholder).toBeTruthy(); - expect(selector.initialStateValue).toBeUndefined(); - expect(selector._value).toBeUndefined(); - expect(selector.previousStateValue).toBeUndefined(); - expect(selector.nextStateValue).toBeUndefined(); + expect(selector.initialStateValue).toBeNull(); + expect(selector._value).toBeNull(); + expect(selector.previousStateValue).toBeNull(); + expect(selector.nextStateValue).toBeNull(); + expect(selector.observer).toBeInstanceOf(StateObserver); + expect(selector.observer.dependents.size).toBe(0); + expect(selector.observer._key).toBeUndefined(); + expect(selector.sideEffects).toStrictEqual({}); + expect(selector.computeValueMethod).toBeUndefined(); + expect(selector.computeExistsMethod).toBeInstanceOf(Function); + expect(selector.isPersisted).toBeFalsy(); + expect(selector.persistent).toBeUndefined(); + expect(selector.watchers).toStrictEqual({}); + }); + + it("should create Selector and shouldn't call initial select if specified selector key is null (default config)", () => { + // Overwrite select once to not call it + jest + .spyOn(Selector.prototype, 'select') + .mockReturnValueOnce(undefined as any); + + const selector = new Selector(dummyCollection, null); + + expect(selector.collection()).toBe(dummyCollection); + expect(selector._item).toBeNull(); + expect(selector._itemKey).toBeNull(); + expect(selector.select).not.toHaveBeenCalled(); + + // Check if State was called with correct parameters + expect(selector._key).toBeUndefined(); + expect(selector.valueType).toBeUndefined(); + expect(selector.isSet).toBeFalsy(); + expect(selector.isPlaceholder).toBeTruthy(); + expect(selector.initialStateValue).toBeNull(); + expect(selector._value).toBeNull(); + expect(selector.previousStateValue).toBeNull(); + expect(selector.nextStateValue).toBeNull(); expect(selector.observer).toBeInstanceOf(StateObserver); expect(selector.observer.dependents.size).toBe(0); expect(selector.observer._key).toBeUndefined(); @@ -236,8 +272,9 @@ describe('Selector Tests', () => { expect.any(Function), { weight: 100 } ); - expect(dummyItem2.selectedBy.size).toBe(1); - expect(dummyItem2.selectedBy.has(selector._key as any)); + expect(Array.from(dummyItem2.selectedBy)).toStrictEqual([ + selector._key, + ]); }); it('should unselect old selected Item and select new Item (specific config)', () => { @@ -300,8 +337,9 @@ describe('Selector Tests', () => { expect(selector.addSideEffect).not.toHaveBeenCalled(); expect(dummyItem1.addSideEffect).not.toHaveBeenCalled(); - expect(dummyItem1.selectedBy.size).toBe(1); - expect(dummyItem1.selectedBy.has(selector._key as any)); + expect(Array.from(dummyItem1.selectedBy)).toStrictEqual([ + selector._key, + ]); }); it('should select selected Item again (config.force = true)', () => { @@ -361,7 +399,7 @@ describe('Selector Tests', () => { expect(selector.addSideEffect).not.toHaveBeenCalled(); expect(dummyItem2.addSideEffect).not.toHaveBeenCalled(); - expect(dummyItem2.selectedBy.size).toBe(0); + expect(Array.from(dummyItem2.selectedBy)).toStrictEqual([]); }); it("should unselect old selected Item and select new Item although Collection isn't instantiated (config.force = true)", () => { @@ -404,8 +442,9 @@ describe('Selector Tests', () => { expect.any(Function), { weight: 100 } ); - expect(dummyItem2.selectedBy.size).toBe(1); - expect(dummyItem2.selectedBy.has(selector._key as any)); + expect(Array.from(dummyItem2.selectedBy)).toStrictEqual([ + selector._key, + ]); }); it('should remove old selected Item, select new Item and overwrite Selector if old Item is placeholder (default config)', async () => { @@ -445,8 +484,9 @@ describe('Selector Tests', () => { expect.any(Function), { weight: 100 } ); - expect(dummyItem2.selectedBy.size).toBe(1); - expect(dummyItem2.selectedBy.has(selector._key as any)); + expect(Array.from(dummyItem2.selectedBy)).toStrictEqual([ + selector._key, + ]); }); it("should remove old selected Item, select new Item and shouldn't overwrite Selector if old Item is placeholder (config.overwrite = false)", async () => { @@ -486,8 +526,22 @@ describe('Selector Tests', () => { expect.any(Function), { weight: 100 } ); - expect(dummyItem2.selectedBy.size).toBe(1); - expect(dummyItem2.selectedBy.has(selector._key as any)); + expect(Array.from(dummyItem2.selectedBy)).toStrictEqual([ + selector._key, + ]); + }); + + it("shouldn't select null and unselect old Item (default config)", () => { + dummyCollection.getItemWithReference = jest.fn(() => dummyItem2); + + selector.select(null); + + expect(dummyCollection.getItemWithReference).not.toHaveBeenCalled(); + // expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); // Because 'unselect()' is mocked + // expect(selector._item).toBeNull(); // Because 'unselect()' is mocked + expect(selector.unselect).toHaveBeenCalledWith({ background: false }); + expect(selector.rebuildSelector).not.toHaveBeenCalled(); + expect(selector.addSideEffect).not.toHaveBeenCalled(); }); describe('test added sideEffect called Selector.rebuildSelectorSideEffectKey', () => { @@ -613,8 +667,8 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector._item).toBeUndefined(); - expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); + expect(selector._item).toBeNull(); + expect(selector._itemKey).toBeNull(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({}); @@ -633,8 +687,8 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector._item).toBeUndefined(); - expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); + expect(selector._item).toBeNull(); + expect(selector._itemKey).toBeNull(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({ background: true, @@ -655,8 +709,8 @@ describe('Selector Tests', () => { Selector.rebuildSelectorSideEffectKey ); - expect(selector._item).toBeUndefined(); - expect(selector._itemKey).toBe(Selector.unknownItemPlaceholderKey); + expect(selector._item).toBeNull(); + expect(selector._itemKey).toBeNull(); expect(selector.isPlaceholder).toBeTruthy(); expect(selector.rebuildSelector).toHaveBeenCalledWith({}); @@ -684,13 +738,13 @@ describe('Selector Tests', () => { }); it("should return false if Selector hasn't selected itemKey correctly (item = undefined)", () => { - selector._item = undefined; + selector._item = null; expect(selector.hasSelected('dummyItemKey')).toBeFalsy(); }); it("should return true if Selector hasn't selected itemKey correctly (item = undefined, correctlySelected = false)", () => { - selector._item = undefined; + selector._item = null; expect(selector.hasSelected('dummyItemKey', false)).toBeTruthy(); }); @@ -742,11 +796,11 @@ describe('Selector Tests', () => { }); it('should set selector value to undefined if Item is undefined (default config)', () => { - selector._item = undefined; + selector._item = null; selector.rebuildSelector(); - expect(selector.set).toHaveBeenCalledWith(undefined, {}); + expect(selector.set).toHaveBeenCalledWith(null, {}); }); }); }); diff --git a/packages/core/tests/unit/state/state.observer.test.ts b/packages/core/tests/unit/state/state.observer.test.ts index 7a248cb8..ac95ca58 100644 --- a/packages/core/tests/unit/state/state.observer.test.ts +++ b/packages/core/tests/unit/state/state.observer.test.ts @@ -133,7 +133,7 @@ describe('StateObserver Tests', () => { it( "should call 'ingestValue' with computed value " + 'if Observer belongs to a Computed State (default config)', - () => { + async () => { dummyComputed.compute = jest.fn(() => Promise.resolve('computedValue') ); @@ -141,7 +141,7 @@ describe('StateObserver Tests', () => { computedObserver.ingest(); expect(dummyComputed.compute).toHaveBeenCalled(); - waitForExpect(() => { + await waitForExpect(() => { expect(computedObserver.ingestValue).toHaveBeenCalledWith( 'computedValue', {}