From 610de9c78891eab1eb4b899735a71d65b2316759 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Tue, 3 Oct 2023 07:15:46 +0100 Subject: [PATCH] More tests for image window loader --- .../core/src/common/image-window-loader.ts | 4 +- .../core/test/image-window-loader.test.ts | 209 ++++++++++++++++++ 2 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 packages/core/test/image-window-loader.test.ts diff --git a/packages/core/src/common/image-window-loader.ts b/packages/core/src/common/image-window-loader.ts index 784fbc838..1c8e4fa74 100644 --- a/packages/core/src/common/image-window-loader.ts +++ b/packages/core/src/common/image-window-loader.ts @@ -34,14 +34,14 @@ class ImageWindowLoaderImpl implements ImageWindowLoader { private imageLoaded: (locations: readonly Item[]) => void = () => undefined; private loadedLocations: [number, number][] = []; - private visibleWindow: Rectangle = { + public visibleWindow: Rectangle = { x: 0, y: 0, width: 0, height: 0, }; - private freezeCols: number = 0; + public freezeCols: number = 0; private isInWindow = (packed: number) => { const col = unpackCol(packed); diff --git a/packages/core/test/image-window-loader.test.ts b/packages/core/test/image-window-loader.test.ts new file mode 100644 index 000000000..7dd6c176d --- /dev/null +++ b/packages/core/test/image-window-loader.test.ts @@ -0,0 +1,209 @@ +import ImageWindowLoaderImpl from "../src/common/image-window-loader"; + +describe("ImageWindowLoaderImpl", () => { + let loader: ImageWindowLoaderImpl; + + beforeEach(() => { + loader = new ImageWindowLoaderImpl(); + }); + + describe("setWindow()", () => { + it("should set the new visible window and freezeCols", () => { + const newWindow = { + x: 10, + y: 10, + width: 100, + height: 100, + }; + const freezeCols = 5; + + loader.setWindow(newWindow, freezeCols); + + // Assuming you modify your class to expose `visibleWindow` and `freezeCols` for testing + expect(loader.visibleWindow).toEqual(newWindow); + expect(loader.freezeCols).toBe(freezeCols); + }); + + it("should call clearOutOfWindow() if the window or freezeCols changes", () => { + const spyClearOutOfWindow = jest.spyOn(loader, "clearOutOfWindow" as any); // Private method, so using 'as any' + + const window1 = { + x: 10, + y: 10, + width: 100, + height: 100, + }; + const window2 = { + x: 15, + y: 15, + width: 100, + height: 100, + }; + const freezeCols1 = 5; + const freezeCols2 = 10; + + loader.setWindow(window1, freezeCols1); + expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1); + + loader.setWindow(window2, freezeCols1); + expect(spyClearOutOfWindow).toHaveBeenCalledTimes(2); + + loader.setWindow(window2, freezeCols2); + expect(spyClearOutOfWindow).toHaveBeenCalledTimes(3); + + // Cleanup + spyClearOutOfWindow.mockRestore(); + }); + + it("should not call clearOutOfWindow() if the window and freezeCols stay the same", () => { + const spyClearOutOfWindow = jest.spyOn(loader, "clearOutOfWindow" as any); + + const newWindow = { + x: 10, + y: 10, + width: 100, + height: 100, + }; + const freezeCols = 5; + + loader.setWindow(newWindow, freezeCols); + loader.setWindow(newWindow, freezeCols); + + expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1); + + // Cleanup + spyClearOutOfWindow.mockRestore(); + }); + }); + + describe("loadOrGetImage()", () => { + // Define image data that might be used across tests + const url = "http://example.com/image.png"; + const col = 1; + const row = 1; + + it("should return the image if it is in the cache", () => { + // Setup: Mock the cache to contain the image + const img = new Image(); + const key = url; + (loader as any).cache[key] = { + img, + url, + cells: [], + cancel: jest.fn(), + }; + + // Act: Retrieve the image + const result = loader.loadOrGetImage(url, col, row); + + // Assert: Ensure the returned image is what we expect + expect(result).toBe(img); + }); + + it("should trigger image loading if the image is not in the cache", () => { + // Setup: Spy on `loadImage` method and clear the cache + const spyLoadImage = jest.spyOn(loader, "loadImage" as any); + (loader as any).cache = {}; + + // Act: Try to retrieve an image that's not in the cache + loader.loadOrGetImage(url, col, row); + + // Assert: Ensure `loadImage` was called to fetch the image + expect(spyLoadImage).toHaveBeenCalledWith(url, col, row, url); + + // Cleanup: Restore the spy + spyLoadImage.mockRestore(); + }); + + it("should not trigger image loading if the image is in the cache but not yet loaded", () => { + // Setup: Mock the cache with a non-loaded image and spy on `loadImage` method + const key = url; + (loader as any).cache[key] = { + img: undefined, + url, + cells: [], + cancel: jest.fn(), + }; + const spyLoadImage = jest.spyOn(loader, "loadImage" as any); + + // Act: Try to retrieve an image that's in the cache but not loaded + const result = loader.loadOrGetImage(url, col, row); + + // Assert: Ensure the image isn't returned and `loadImage` wasn't called + expect(result).toBeUndefined(); + expect(spyLoadImage).not.toHaveBeenCalled(); + + // Cleanup: Restore the spy + spyLoadImage.mockRestore(); + }); + }); + + describe("loadImage()", () => { + // Define image data that might be used across tests + const url = "http://example.com/image.png"; + const col = 1; + const row = 1; + const key = url; + + it("should add a loading image to the cache", () => { + // Act: Load an image + (loader as any).loadImage(url, col, row, key); + + // Assert: Ensure the cache was updated with an entry for the loading image + expect((loader as any).cache[key]).toBeDefined(); + expect((loader as any).cache[key].url).toBe(url); + expect((loader as any).cache[key].img).toBeUndefined(); + expect((loader as any).cache[key].cells).toEqual([row * (1 << 16) + col]); + }); + + it("should handle image loading and decoding successfully", async () => { + // Setup: Spy on `Image` constructor and `decode` method + const img = new Image(); + img.addEventListener = jest.fn((_kind: string, cb: any) => cb()); + jest.useFakeTimers(); + + const spyConstructor = jest.spyOn(window, "Image").mockImplementation(() => img); + img.decode = jest.fn().mockResolvedValue(undefined); + + // Act: Load an image and simulate its successful loading + (loader as any).loadImage(url, col, row, key); + + jest.runAllTimers(); + + jest.useRealTimers(); + await new Promise(r => setTimeout(r, 100)); + + // Assert: Ensure `decode` was called and the image data is updated in the cache + expect(img.decode).toHaveBeenCalled(); + expect((loader as any).cache[key].img).toBe(img); + + // Cleanup: Restore the spies + spyConstructor.mockRestore(); + }); + + it("should handle image loading failure gracefully", async () => { + // Setup: Spy on `Image` constructor and `decode` method + const img = new Image(); + img.addEventListener = jest.fn((_kind: string, cb: any) => cb()); + jest.useFakeTimers(); + + const spyConstructor = jest.spyOn(window, "Image").mockImplementation(() => img); + img.decode = jest.fn().mockRejectedValue(new Error("Decoding failed")); + + // Act: Load an image and simulate its successful loading but decoding failure + (loader as any).loadImage(url, col, row, key); + + jest.runAllTimers(); + + jest.useRealTimers(); + await new Promise(r => setTimeout(r, 100)); + + // Assert: Ensure `decode` was called and the image data remains undefined in the cache + expect(img.decode).toHaveBeenCalled(); + expect((loader as any).cache[key].img).toBeUndefined(); + + // Cleanup: Restore the spies + spyConstructor.mockRestore(); + }); + }); +});