From ca094dc8c44da7c821ba2b9a8705cea6a900a5c0 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Fri, 30 Aug 2024 15:24:05 -0400 Subject: [PATCH 1/5] chore(vscode): debugging support for tests --- .vscode/launch.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c0fa924f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Current Test File", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/tinyspy/**", "**/node_modules/@vitest/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal" + } + ] +} \ No newline at end of file From b70cce39c035e27041e7c342c1c3e176a8bf7297 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Fri, 30 Aug 2024 15:24:54 -0400 Subject: [PATCH 2/5] refactor: lower level mock to capture bug #531 --- test/unit/hooks/useAssets.test.tsx | 108 +++++++++++++++++------------ 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/test/unit/hooks/useAssets.test.tsx b/test/unit/hooks/useAssets.test.tsx index 0f4143be..3b5bea0f 100644 --- a/test/unit/hooks/useAssets.test.tsx +++ b/test/unit/hooks/useAssets.test.tsx @@ -1,4 +1,4 @@ -import { Assets, Cache, Sprite, Texture, type UnresolvedAsset } from 'pixi.js'; +import { Assets, loadTextures, Sprite, Texture, type UnresolvedAsset } from 'pixi.js'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { extend, useAssets } from '../../../src'; import { cleanup, renderHook, waitFor } from '@testing-library/react'; @@ -7,76 +7,58 @@ extend({ Sprite }); describe('useAssets', async () => { - const assets: UnresolvedAsset[] = [{ src: 'test.png' }, { src: 'test2.png' }]; + loadTextures.config = { + preferWorkers: false, + preferCreateImageBitmap: true, + crossOrigin: 'anonymous', + }; - // Store the loaded assets and data state to verify the hook results - let loaded: Record = {}; - let data: Record = {}; + const mockFetch = vi.spyOn(global, 'fetch'); - // Mock the Assets.load, Assets.get & Cache.has method - const load = vi.spyOn(Assets, 'load'); - const get = vi.spyOn(Assets, 'get'); - const has = vi.spyOn(Cache, 'has'); - - // Mock the Assets.load to populate the loaded record, and resolve after 1ms - load.mockImplementation((urls) => + global.createImageBitmap = vi.fn().mockImplementation(() => new Promise((resolve) => { - const assets = urls as UnresolvedAsset[]; - - return new Promise((resolve) => - { - setTimeout(() => - { - loaded = { ...loaded, ...assets.reduce((acc, val) => ({ ...acc, [val.src!.toString()]: Texture.EMPTY }), {}) }; - data = { ...data, ...assets.reduce((acc, val) => ({ ...acc, [val.src!.toString()]: val.data }), {}) }; - resolve(loaded); - }, 1); - }); - }); - - // Mock the Assets.get to return the loaded record - get.mockImplementation((keys) => - keys.reduce>((acc, key, idx) => ({ ...acc, [idx]: loaded[key] }), {})); + setTimeout(() => resolve({ width: 100, height: 100, close: () => { /* noop */ } }), 1); + })); - // Mock the Cache.has to check if the key is in the loaded record - has.mockImplementation((key) => key in loaded); - - // Load the default results using Assets.load to compare against the results from the useAssets hook - const defaultResults = await Assets.load(assets); + mockFetch.mockImplementation(() => new Promise((resolve) => + { + setTimeout(() => resolve(new Response(new Blob([new Uint8Array([137, 80, 78, 89, 13, 11, 8, 2, 255, 0, 1, 0, 5, 49, 137, 80, 78, 89, 13, 11, 8, 73, 69, 82, 1, 0, 255, 255, 255])], { type: 'image/png' }))), 1); + })); beforeEach(() => { - loaded = {}; - data = {}; + Assets.init({ + skipDetections: true, + }); }); afterEach(() => { + Assets.reset(); cleanup(); }); afterAll(() => { - load.mockRestore(); - get.mockRestore(); + mockFetch.mockRestore(); }); it('loads assets', async () => { + const assets: UnresolvedAsset[] = [{ src: 'http://localhost/bc3d2999.png' }, { src: 'http://localhost/1a5c1ce4.png' }]; + const { result } = renderHook(() => useAssets(assets)); expect(result.current.isPending).toBe(true); await waitFor(() => expect(result.current.isSuccess).toBe(true)); - - expect(result.current.assets).toEqual(assets.map(({ src }) => defaultResults[src!.toString()])); }); it('accepts data', async () => { // Explicitly type the T in the useAssets hook const { result } = renderHook(() => useAssets([ - { src: 'test.png', data: { test: '7a1c8bee' } }, - { src: 'test2.png', data: { test: '230a3f41' } }, + { src: 'http://localhost/7a1c8bee.png', data: { test: '7a1c8bee' } }, + { src: 'http://localhost/230a3f41.png', data: { test: '230a3f41' } }, ])); expect(result.current.isPending).toBe(true); @@ -85,8 +67,6 @@ describe('useAssets', async () => const { assets: [texture], isSuccess } = result.current; expect(isSuccess).toBe(true); - expect(data['test.png'].test).toBe('7a1c8bee'); - expect(data['test2.png'].test).toBe('230a3f41'); const isTexture = (texture?: Texture) => texture && texture instanceof Texture; @@ -97,7 +77,7 @@ describe('useAssets', async () => { // Do not provide a type for T in the useAssets hook const { result } = renderHook(() => useAssets([ - { src: 'test.png', data: { test: 'd460dbdd' } }, + { src: 'http://localhost/d460dbdd.png', data: { test: 'd460dbdd' } }, ])); expect(result.current.isPending).toBe(true); @@ -111,4 +91,44 @@ describe('useAssets', async () => expect(isTexture(texture)).toBe(true); }); + + it('handles subsequent loads', async () => + { + const assets: UnresolvedAsset[] = [{ src: 'http://localhost/c13a19b0.png' }, { src: 'http://localhost/ba91270b.png' }]; + + const { result, rerender } = renderHook(() => useAssets(assets)); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const { result: result2, rerender: rerender2 } = renderHook(() => useAssets(assets)); + + await waitFor(() => expect(result2.current.isSuccess).toBe(true)); + + expect(result.current.isSuccess).toBe(true); + expect(result2.current.isSuccess).toBe(true); + + const isTexture = (texture?: Texture) => texture && texture instanceof Texture; + + const { assets: [texture1, texture2] } = result.current; + const { assets: [texture3, texture4] } = result2.current; + + expect(isTexture(texture1)).toBe(true); + expect(isTexture(texture2)).toBe(true); + expect(isTexture(texture3)).toBe(true); + expect(isTexture(texture4)).toBe(true); + + rerender(); + + expect(isTexture(texture1)).toBe(true); + expect(isTexture(texture2)).toBe(true); + expect(isTexture(texture3)).toBe(true); + expect(isTexture(texture4)).toBe(true); + + rerender2(); + + expect(isTexture(texture1)).toBe(true); + expect(isTexture(texture2)).toBe(true); + expect(isTexture(texture3)).toBe(true); + expect(isTexture(texture4)).toBe(true); + }); }); From 8a07f1da486b9f8eb86a21a96cd4d84fadaebdcb Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sun, 1 Sep 2024 09:38:43 -0400 Subject: [PATCH 3/5] fix: potential fix for #531 --- src/hooks/useAssets.ts | 47 +++++++++++++++++------------- test/unit/hooks/useAssets.test.tsx | 35 ++++++++++++---------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/hooks/useAssets.ts b/src/hooks/useAssets.ts index 273336cc..f5391af8 100644 --- a/src/hooks/useAssets.ts +++ b/src/hooks/useAssets.ts @@ -18,6 +18,20 @@ function assetsLoadedTest(asset: UnresolvedAsset) return Cache.has(getAssetKey(asset)); } +function resolveAssets(assets: UnresolvedAsset[]) +{ + const assetKeys = assets.map((asset: UnresolvedAsset) => getAssetKey(asset)); + const resolvedAssetsDictionary = Assets.get(assetKeys) as Record; + + return { + assets: assets.map((_asset: UnresolvedAsset, index: number) => resolvedAssetsDictionary[index]), + isError: false, + isPending: false, + isSuccess: true, + status: UseAssetsStatus.SUCCESS, + }; +} + /** Loads assets, returning a hash of assets once they're loaded. */ export function useAssets( /** @description Assets to be loaded. */ @@ -27,18 +41,18 @@ export function useAssets( options: UseAssetsOptions = {}, ): UseAssetsResult { - const [state, setState] = useState>({ - assets: Array(assets.length).fill(undefined), - isError: false, - isPending: true, - isSuccess: false, - status: UseAssetsStatus.PENDING, - }); + const allAssetsAreLoaded = assets.reduce((acc, cur) => acc && assetsLoadedTest(cur), true); - if (typeof window === 'undefined') - { - return state; - } + const [state, setState] = useState>( + typeof window !== 'undefined' && allAssetsAreLoaded + ? resolveAssets(assets) + : { + assets: Array(assets.length).fill(undefined), + isError: false, + isPending: true, + isSuccess: false, + status: UseAssetsStatus.PENDING, + }); const { maxRetries = 3, @@ -47,8 +61,6 @@ export function useAssets( retryOnFailure = true, } = options; - const allAssetsAreLoaded = assets.some(assetsLoadedTest); - if (!allAssetsAreLoaded) { let cachedState = errorCache.get(assets); @@ -80,16 +92,9 @@ export function useAssets( }) .then(() => { - const assetKeys = assets.map((asset: UnresolvedAsset) => getAssetKey(asset)); - const resolvedAssetsDictionary = Assets.get(assetKeys) as Record; - setState((previousState) => ({ ...previousState, - assets: assets.map((_asset: UnresolvedAsset, index: number) => resolvedAssetsDictionary[index]), - isError: false, - isPending: false, - isSuccess: true, - status: UseAssetsStatus.SUCCESS, + ...resolveAssets(assets), })); }) .catch((error) => diff --git a/test/unit/hooks/useAssets.test.tsx b/test/unit/hooks/useAssets.test.tsx index 3b5bea0f..4785aea5 100644 --- a/test/unit/hooks/useAssets.test.tsx +++ b/test/unit/hooks/useAssets.test.tsx @@ -22,7 +22,7 @@ describe('useAssets', async () => mockFetch.mockImplementation(() => new Promise((resolve) => { - setTimeout(() => resolve(new Response(new Blob([new Uint8Array([137, 80, 78, 89, 13, 11, 8, 2, 255, 0, 1, 0, 5, 49, 137, 80, 78, 89, 13, 11, 8, 73, 69, 82, 1, 0, 255, 255, 255])], { type: 'image/png' }))), 1); + setTimeout(() => resolve(new Response()), 1); })); beforeEach(() => @@ -109,26 +109,29 @@ describe('useAssets', async () => const isTexture = (texture?: Texture) => texture && texture instanceof Texture; - const { assets: [texture1, texture2] } = result.current; - const { assets: [texture3, texture4] } = result2.current; - - expect(isTexture(texture1)).toBe(true); - expect(isTexture(texture2)).toBe(true); - expect(isTexture(texture3)).toBe(true); - expect(isTexture(texture4)).toBe(true); + expect(result.current.isSuccess).toBe(true); + expect(result2.current.isSuccess).toBe(true); + expect(isTexture(result.current.assets[0])).toBe(true); + expect(isTexture(result.current.assets[1])).toBe(true); + expect(isTexture(result2.current.assets[0])).toBe(true); + expect(isTexture(result2.current.assets[1])).toBe(true); rerender(); - expect(isTexture(texture1)).toBe(true); - expect(isTexture(texture2)).toBe(true); - expect(isTexture(texture3)).toBe(true); - expect(isTexture(texture4)).toBe(true); + expect(result.current.isSuccess).toBe(true); + expect(result2.current.isSuccess).toBe(true); + expect(isTexture(result.current.assets[0])).toBe(true); + expect(isTexture(result.current.assets[1])).toBe(true); + expect(isTexture(result2.current.assets[0])).toBe(true); + expect(isTexture(result2.current.assets[1])).toBe(true); rerender2(); - expect(isTexture(texture1)).toBe(true); - expect(isTexture(texture2)).toBe(true); - expect(isTexture(texture3)).toBe(true); - expect(isTexture(texture4)).toBe(true); + expect(result.current.isSuccess).toBe(true); + expect(result2.current.isSuccess).toBe(true); + expect(isTexture(result.current.assets[0])).toBe(true); + expect(isTexture(result.current.assets[1])).toBe(true); + expect(isTexture(result2.current.assets[0])).toBe(true); + expect(isTexture(result2.current.assets[1])).toBe(true); }); }); From 59b4ed115ac0aa07050c9314399d7dbb34184ebe Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sun, 1 Sep 2024 09:51:52 -0400 Subject: [PATCH 4/5] test: remove redundant expect --- test/unit/hooks/useAssets.test.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/unit/hooks/useAssets.test.tsx b/test/unit/hooks/useAssets.test.tsx index 4785aea5..e3258969 100644 --- a/test/unit/hooks/useAssets.test.tsx +++ b/test/unit/hooks/useAssets.test.tsx @@ -104,9 +104,6 @@ describe('useAssets', async () => await waitFor(() => expect(result2.current.isSuccess).toBe(true)); - expect(result.current.isSuccess).toBe(true); - expect(result2.current.isSuccess).toBe(true); - const isTexture = (texture?: Texture) => texture && texture instanceof Texture; expect(result.current.isSuccess).toBe(true); From e651448b1523db4a584dfe021601752a68b34e2d Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Fri, 6 Sep 2024 01:45:35 -0400 Subject: [PATCH 5/5] refactor: every over reduce --- src/hooks/useAssets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAssets.ts b/src/hooks/useAssets.ts index f5391af8..b88a37cd 100644 --- a/src/hooks/useAssets.ts +++ b/src/hooks/useAssets.ts @@ -41,7 +41,7 @@ export function useAssets( options: UseAssetsOptions = {}, ): UseAssetsResult { - const allAssetsAreLoaded = assets.reduce((acc, cur) => acc && assetsLoadedTest(cur), true); + const allAssetsAreLoaded = assets.every((asset) => assetsLoadedTest(asset)); const [state, setState] = useState>( typeof window !== 'undefined' && allAssetsAreLoaded