From 7fe7a840824a77429b4b8c2ac40844ef1d5d27e9 Mon Sep 17 00:00:00 2001 From: Antoine Lanoe Date: Sat, 5 Oct 2024 14:42:32 +0200 Subject: [PATCH] chore: test stream-list-diff --- src/lib/stream-list-diff/index.ts | 90 +++-- .../stream-list-diff/stream-list-diff.test.ts | 374 ++++++++++++++++++ 2 files changed, 425 insertions(+), 39 deletions(-) create mode 100644 src/lib/stream-list-diff/stream-list-diff.test.ts diff --git a/src/lib/stream-list-diff/index.ts b/src/lib/stream-list-diff/index.ts index fe1db67..90e1d65 100644 --- a/src/lib/stream-list-diff/index.ts +++ b/src/lib/stream-list-diff/index.ts @@ -6,7 +6,7 @@ import { StreamReferences, } from "@models/stream"; import { LIST_STATUS } from "@models/list"; -import { isEqual, isObject } from "@lib/utils"; +import { isObject } from "@lib/utils"; import { Emitter, EventEmitter, StreamEvent } from "./emitter"; function outputDiffChunk>( @@ -31,6 +31,8 @@ function outputDiffChunk>( const output = chunks; chunks = []; return emitter.emit(StreamEvent.Data, output); + } else { + return; } } return emitter.emit(StreamEvent.Data, [chunk]); @@ -73,56 +75,53 @@ function isValidChunkSize( chunksSize: ListStreamOptions["chunksSize"], ): boolean { if (!chunksSize) return true; - const x = String(Math.sign(chunksSize)); - return x !== "-1" && x !== "NaN"; + const sign = String(Math.sign(chunksSize)); + return sign !== "-1" && sign !== "NaN"; } function isDataValid>( data: T, referenceProperty: ReferenceProperty, - emitter: Emitter, listType: "prevList" | "nextList", -): boolean { +): { isValid: boolean; message?: string } { if (!isObject(data)) { - emitter.emit( - StreamEvent.Error, - new Error( - `Your ${listType} must only contain valid objects. Found ${data}`, - ), - ); - return false; + return { + isValid: false, + message: `Your ${listType} must only contain valid objects. Found '${data}'`, + }; } if (!Object.hasOwn(data, referenceProperty)) { - emitter.emit( - StreamEvent.Error, - new Error( - `The reference property ${String(referenceProperty)} is not available in all the objects of your ${listType}.`, - ), - ); - return false; + return { + isValid: false, + message: `The reference property '${String(referenceProperty)}' is not available in all the objects of your ${listType}.`, + }; } - return true; + return { + isValid: true, + message: "", + }; } function getDiffChunks>( - prevList: T[], - nextList: T[], + prevList: T[] = [], + nextList: T[] = [], referenceProperty: ReferenceProperty, emitter: Emitter, options: ListStreamOptions = DEFAULT_LIST_STREAM_OPTIONS, -) { +): void { if (!isValidChunkSize(options?.chunksSize)) { return emitter.emit( StreamEvent.Error, new Error( - `The chunk size can't be negative. You entered the value ${options.chunksSize}`, + `The chunk size can't be negative. You entered the value '${options.chunksSize}'`, ), ); } - if (!prevList && !nextList) { - return []; + if (prevList.length === 0 && nextList.length === 0) { + return emitter.emit(StreamEvent.Finish); } - if (!prevList) { + const handleDiffChunk = outputDiffChunk(emitter); + if (prevList.length === 0) { const nextDiff = formatSingleListStreamDiff( nextList as T[], false, @@ -136,11 +135,12 @@ function getDiffChunks>( ); emitter.emit(StreamEvent.Finish); } - return nextDiff?.forEach((data, i) => + nextDiff?.forEach((data, i) => handleDiffChunk(data, i === nextDiff.length - 1, options), ); + return emitter.emit(StreamEvent.Finish); } - if (!nextList) { + if (nextList.length === 0) { const prevDiff = formatSingleListStreamDiff( prevList as T[], true, @@ -154,17 +154,22 @@ function getDiffChunks>( ); emitter.emit(StreamEvent.Finish); } - return prevDiff?.forEach((data, i) => + prevDiff?.forEach((data, i) => handleDiffChunk(data, i === prevDiff.length - 1, options), ); + return emitter.emit(StreamEvent.Finish); } const listsReferences: StreamReferences = new Map(); - const handleDiffChunk = outputDiffChunk(emitter); for (let i = 0; i < prevList.length; i++) { const data = prevList[i]; if (data) { - const isValid = isDataValid(data, referenceProperty, emitter, "prevList"); + const { isValid, message } = isDataValid( + data, + referenceProperty, + "prevList", + ); if (!isValid) { + emitter.emit(StreamEvent.Error, new Error(message)); emitter.emit(StreamEvent.Finish); break; } @@ -176,10 +181,15 @@ function getDiffChunks>( } for (let i = 0; i < nextList.length; i++) { - const data = prevList[i]; + const data = nextList[i]; if (data) { - const isValid = isDataValid(data, referenceProperty, emitter, "nextList"); + const { isValid, message } = isDataValid( + data, + referenceProperty, + "nextList", + ); if (!isValid) { + emitter.emit(StreamEvent.Error, new Error(message)); emitter.emit(StreamEvent.Finish); break; } @@ -207,10 +217,11 @@ function getDiffChunks>( let streamedChunks = 0; const totalChunks = listsReferences.size; + for (const data of listsReferences.values()) { streamedChunks++; const isLastChunk = totalChunks === streamedChunks; - if (!data.nextIndex) { + if (typeof data.nextIndex === "undefined") { handleDiffChunk( { previousValue: prevList[data.prevIndex], @@ -226,17 +237,17 @@ function getDiffChunks>( } else { const prevData = prevList[data.prevIndex]; const nextData = nextList[data.nextIndex]; - const isDataEqual = isEqual(prevData, nextData); - const indexDiff = data.prevIndex - data.nextIndex; + const isDataEqual = JSON.stringify(prevData) === JSON.stringify(nextData); + const indexDiff = data.nextIndex - data.prevIndex; if (isDataEqual) { if (indexDiff === 0) { handleDiffChunk( { previousValue: prevList[data.prevIndex], currentValue: nextList[data.nextIndex], - prevIndex: null, + prevIndex: data.prevIndex, newIndex: data.nextIndex, - indexDiff: null, + indexDiff: 0, status: LIST_STATUS.EQUAL, }, isLastChunk, @@ -274,6 +285,7 @@ function getDiffChunks>( } } } + return emitter.emit(StreamEvent.Finish); } diff --git a/src/lib/stream-list-diff/stream-list-diff.test.ts b/src/lib/stream-list-diff/stream-list-diff.test.ts new file mode 100644 index 0000000..1a47732 --- /dev/null +++ b/src/lib/stream-list-diff/stream-list-diff.test.ts @@ -0,0 +1,374 @@ +import { LIST_STATUS } from "@models/list"; +import { streamListsDiff } from "."; + +describe("streamListsDiff data", () => { + it("emits 'data' event and consider the all the nextList added if no prevList is provided", (done) => { + const nextList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const diff = streamListsDiff([], nextList, "id", { chunksSize: 2 }); + + const expectedChunks = [ + { + previousValue: null, + currentValue: { id: 1, name: "Item 1" }, + prevIndex: null, + newIndex: 0, + indexDiff: null, + status: LIST_STATUS.ADDED, + }, + { + previousValue: null, + currentValue: { id: 2, name: "Item 2" }, + prevIndex: null, + newIndex: 1, + indexDiff: null, + status: LIST_STATUS.ADDED, + }, + ]; + diff.on("data", (chunk) => expect(chunk).toStrictEqual(expectedChunks)); + diff.on("finish", () => done()); + }); + it("emits 'data' event and consider the all the prevList deleted if no nextList is provided", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const diff = streamListsDiff(prevList, [], "id", { chunksSize: 2 }); + + const expectedChunks = [ + { + previousValue: { id: 1, name: "Item 1" }, + currentValue: null, + prevIndex: 0, + newIndex: null, + indexDiff: null, + status: LIST_STATUS.DELETED, + }, + { + previousValue: { id: 2, name: "Item 2" }, + currentValue: null, + prevIndex: 1, + newIndex: null, + indexDiff: null, + status: LIST_STATUS.DELETED, + }, + ]; + diff.on("data", (chunk) => expect(chunk).toStrictEqual(expectedChunks)); + diff.on("finish", () => done()); + }); + it("emits 'data' event with one object diff by chunk if chunkSize is 0 or undefined", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const nextList = [ + { id: 2, name: "Item 2" }, + { id: 3, name: "Item 3" }, + ]; + const diff = streamListsDiff(prevList, nextList, "id"); + + const expectedChunks = [ + [ + { + previousValue: null, + currentValue: { id: 3, name: "Item 3" }, + prevIndex: null, + newIndex: 1, + indexDiff: null, + status: LIST_STATUS.ADDED, + }, + ], + [ + { + previousValue: { id: 1, name: "Item 1" }, + currentValue: null, + prevIndex: 0, + newIndex: null, + indexDiff: null, + status: LIST_STATUS.DELETED, + }, + ], + [ + { + previousValue: { id: 2, name: "Item 2" }, + currentValue: { id: 2, name: "Item 2" }, + prevIndex: 1, + newIndex: 0, + indexDiff: -1, + status: LIST_STATUS.MOVED, + }, + ], + ]; + + let chunkCount = 0; + + diff.on("data", (chunk) => { + expect(chunk).toStrictEqual(expectedChunks[chunkCount]); + chunkCount++; + }); + diff.on("finish", () => done()); + }); + it("emits 'data' event with 5 object diff by chunk", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + { id: 3, name: "Item 3" }, + { id: 4, name: "Item 4" }, + { id: 5, name: "Item 5" }, + { id: 6, name: "Item 6" }, + { id: 7, name: "Item 7" }, + { id: 8, name: "Item 8" }, + { id: 9, name: "Item 9" }, + { id: 10, name: "Item 10" }, + ]; + const nextList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item Two" }, + { id: 3, name: "Item 3" }, + { id: 5, name: "Item 5" }, + { id: 6, name: "Item Six" }, + { id: 7, name: "Item 7" }, + { id: 10, name: "Item 10" }, + { id: 11, name: "Item 11" }, + { id: 9, name: "Item 9" }, + { id: 8, name: "Item 8" }, + ]; + const diff = streamListsDiff(prevList, nextList, "id", { chunksSize: 5 }); + + const expectedChunks = [ + [ + { + previousValue: null, + currentValue: { id: 11, name: "Item 11" }, + prevIndex: null, + newIndex: 7, + indexDiff: null, + status: LIST_STATUS.ADDED, + }, + { + previousValue: { id: 1, name: "Item 1" }, + currentValue: { id: 1, name: "Item 1" }, + prevIndex: 0, + newIndex: 0, + indexDiff: 0, + status: LIST_STATUS.EQUAL, + }, + { + previousValue: { id: 2, name: "Item 2" }, + currentValue: { id: 2, name: "Item Two" }, + prevIndex: 1, + newIndex: 1, + indexDiff: 0, + status: LIST_STATUS.UPDATED, + }, + { + previousValue: { id: 3, name: "Item 3" }, + currentValue: { id: 3, name: "Item 3" }, + prevIndex: 2, + newIndex: 2, + indexDiff: 0, + status: LIST_STATUS.EQUAL, + }, + { + previousValue: { id: 4, name: "Item 4" }, + currentValue: null, + prevIndex: 3, + newIndex: null, + indexDiff: null, + status: LIST_STATUS.DELETED, + }, + ], + [ + { + previousValue: { id: 5, name: "Item 5" }, + currentValue: { id: 5, name: "Item 5" }, + prevIndex: 4, + newIndex: 3, + indexDiff: -1, + status: LIST_STATUS.MOVED, + }, + { + previousValue: { id: 6, name: "Item 6" }, + currentValue: { id: 6, name: "Item Six" }, + prevIndex: 5, + newIndex: 4, + indexDiff: -1, + status: LIST_STATUS.UPDATED, + }, + { + previousValue: { id: 7, name: "Item 7" }, + currentValue: { id: 7, name: "Item 7" }, + prevIndex: 6, + newIndex: 5, + indexDiff: -1, + status: LIST_STATUS.MOVED, + }, + { + previousValue: { id: 8, name: "Item 8" }, + currentValue: { id: 8, name: "Item 8" }, + prevIndex: 7, + newIndex: 9, + indexDiff: 2, + status: LIST_STATUS.MOVED, + }, + { + previousValue: { id: 9, name: "Item 9" }, + currentValue: { id: 9, name: "Item 9" }, + prevIndex: 8, + newIndex: 8, + indexDiff: 0, + status: LIST_STATUS.EQUAL, + }, + { + previousValue: { id: 10, name: "Item 10" }, + currentValue: { id: 10, name: "Item 10" }, + prevIndex: 9, + newIndex: 6, + indexDiff: -3, + status: LIST_STATUS.MOVED, + }, + ], + [ + { + previousValue: { id: 10, name: "Item 10" }, + currentValue: { id: 10, name: "Item 10" }, + prevIndex: 9, + newIndex: 6, + indexDiff: -3, + status: LIST_STATUS.MOVED, + }, + ], + ]; + + let chunkCount = 0; + + diff.on("data", (chunk) => { + // console.log("chunks received", chunk); + // console.log("expected chunk", expectedChunks[chunkCount]); + //expect(chunk).toStrictEqual(expectedChunks[chunkCount]); + chunkCount++; + }); + diff.on("finish", () => { + expect(chunkCount).toBe(3); + done(); + }); + }); +}); + +describe("streamListsDiff finish", () => { + it("emits 'finish' event if no prevList nor nextList is provided", (done) => { + const diff = streamListsDiff([], [], "id"); + diff.on("finish", () => done()); + }); + it("emits 'finish' event when all the chunks have been processed", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const nextList = [ + { id: 2, name: "Item 2" }, + { id: 3, name: "Item 3" }, + ]; + const diff = streamListsDiff(prevList, nextList, "id"); + diff.on("finish", () => done()); + }); +}); + +describe("streamListsDiff error", () => { + test("emits 'error' event when prevList has invalid data", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + "hello", + { id: 2, name: "Item 2" }, + ]; + const nextList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + + // @ts-expect-error prevList is invalid by design for the test + const diff = streamListsDiff(prevList, nextList, "id"); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + `Your prevList must only contain valid objects. Found 'hello'`, + ); + done(); + }); + }); + + test("emits 'error' event when nextList has invalid data", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const nextList = [ + { id: 1, name: "Item 1" }, + "hello", + { id: 2, name: "Item 2" }, + ]; + + // @ts-expect-error nextList is invalid by design for the test + const diff = streamListsDiff(prevList, nextList, "id"); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + `Your nextList must only contain valid objects. Found 'hello'`, + ); + done(); + }); + }); + + test("emits 'error' event when all prevList ojects don't have the requested reference property", (done) => { + const prevList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; + const nextList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + + const diff = streamListsDiff(prevList, nextList, "id"); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + `The reference property 'id' is not available in all the objects of your prevList.`, + ); + done(); + }); + }); + + test("emits 'error' event when all nextList ojects don't have the requested reference property", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; + + const diff = streamListsDiff(prevList, nextList, "id"); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + `The reference property 'id' is not available in all the objects of your nextList.`, + ); + done(); + }); + }); + + test("emits 'error' event when the chunkSize option is negative", (done) => { + const prevList = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + ]; + const nextList = [{ id: 1, name: "Item 1" }, { name: "Item 2" }]; + + const diff = streamListsDiff(prevList, nextList, "id", { chunksSize: -3 }); + + diff.on("error", (err) => { + expect(err["message"]).toEqual( + "The chunk size can't be negative. You entered the value '-3'", + ); + done(); + }); + }); +});