Skip to content

Commit ade982d

Browse files
authored
Merge pull request #528 from thejustinwalsh/use-assets-types-bug
fix: ensure that useAssets does not infer type from data
2 parents 1c4e6c3 + 1427db4 commit ade982d

File tree

7 files changed

+339
-90
lines changed

7 files changed

+339
-90
lines changed

package-lock.json

+214-83
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@rollup/plugin-commonjs": "^25.0.8",
6767
"@rollup/plugin-json": "^6.1.0",
6868
"@rollup/plugin-node-resolve": "^15.2.3",
69+
"@testing-library/react": "^16.0.0",
6970
"@types/eslint": "^8.56.10",
7071
"@types/react": "^18.3.2",
7172
"@types/react-reconciler": "0.28.8",
@@ -90,6 +91,9 @@
9091
"optional": true
9192
}
9293
},
94+
"overrides": {
95+
"rollup": "^4.18.0"
96+
},
9397
"publishConfig": {
9498
"access": "public"
9599
},

src/hooks/useAsset.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import type { ErrorCallback } from '../typedefs/ErrorCallback';
1515
const errorCache: Map<UnresolvedAsset | string, AssetRetryState> = new Map();
1616

1717
/** @deprecated Use `useAssets` instead. */
18-
export function useAsset<T>(
18+
export function useAsset<T = any>(
1919
/** @description Asset options. */
20-
options: (UnresolvedAsset<T> & AssetRetryOptions) | string,
20+
options: (UnresolvedAsset & AssetRetryOptions) | string,
2121
/** @description A function to be called when the asset loader reports loading progress. */
2222
onProgress?: ProgressCallback,
2323
/** @description A function to be called when the asset loader reports loading progress. */

src/hooks/useAssets.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ function assetsLoadedTest<T>(asset: UnresolvedAsset<T>)
2121
/** Loads assets, returning a hash of assets once they're loaded. */
2222
export function useAssets<T = any>(
2323
/** @description Assets to be loaded. */
24-
assets: UnresolvedAsset<T>[],
24+
assets: UnresolvedAsset[],
2525

2626
/** @description Asset options. */
2727
options: UseAssetsOptions = {},
2828
): UseAssetsResult<T>
2929
{
3030
const [state, setState] = useState<UseAssetsResult<T>>({
31-
assets: Array(assets.length).fill(null),
31+
assets: Array(assets.length).fill(undefined),
3232
isError: false,
3333
isPending: true,
3434
isSuccess: false,

src/hooks/useSuspenseAssets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function assetsLoadedTest<T>(asset: UnresolvedAsset<T>)
1818
/** Loads assets, returning a hash of assets once they're loaded. Must be inside of a `<Suspense>` component. */
1919
export function useSuspenseAssets<T = any>(
2020
/** @description Assets to be loaded. */
21-
assets: UnresolvedAsset<T>[],
21+
assets: UnresolvedAsset[],
2222
/** @description Asset options. */
2323
options: UseAssetsOptions = {},
2424
): T[]

src/typedefs/UseAssetsResult.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { UseAssetsStatus } from '../constants/UseAssetsStatus';
22

33
export interface UseAssetsResult<T>
44
{
5-
/** @description An array of resolved assets, or `null` for assets that are still loading. */
6-
assets: (T | null)[];
5+
/** @description An array of resolved assets, or `undefined` for assets that are still loading. */
6+
assets: (T | undefined)[];
77

88
/** @description The error that was encountered. */
99
error?: Error;

test/unit/hooks/useAssets.test.tsx

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Assets, Cache, Sprite, Texture, type UnresolvedAsset } from 'pixi.js';
2+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { extend, useAssets } from '../../../src';
4+
import { cleanup, renderHook, waitFor } from '@testing-library/react';
5+
6+
extend({ Sprite });
7+
8+
describe('useAssets', async () =>
9+
{
10+
const assets: UnresolvedAsset[] = [{ src: 'test.png' }, { src: 'test2.png' }];
11+
12+
// Store the loaded assets and data state to verify the hook results
13+
let loaded: Record<string, unknown> = {};
14+
let data: Record<string, any> = {};
15+
16+
// Mock the Assets.load, Assets.get & Cache.has method
17+
const load = vi.spyOn(Assets, 'load');
18+
const get = vi.spyOn(Assets, 'get');
19+
const has = vi.spyOn(Cache, 'has');
20+
21+
// Mock the Assets.load to populate the loaded record, and resolve after 1ms
22+
load.mockImplementation((urls) =>
23+
{
24+
const assets = urls as UnresolvedAsset[];
25+
26+
return new Promise((resolve) =>
27+
{
28+
setTimeout(() =>
29+
{
30+
loaded = { ...loaded, ...assets.reduce((acc, val) => ({ ...acc, [val.src!.toString()]: Texture.EMPTY }), {}) };
31+
data = { ...data, ...assets.reduce((acc, val) => ({ ...acc, [val.src!.toString()]: val.data }), {}) };
32+
resolve(loaded);
33+
}, 1);
34+
});
35+
});
36+
37+
// Mock the Assets.get to return the loaded record
38+
get.mockImplementation((keys) =>
39+
keys.reduce<Record<string, unknown>>((acc, key, idx) => ({ ...acc, [idx]: loaded[key] }), {}));
40+
41+
// Mock the Cache.has to check if the key is in the loaded record
42+
has.mockImplementation((key) => key in loaded);
43+
44+
// Load the default results using Assets.load to compare against the results from the useAssets hook
45+
const defaultResults = await Assets.load<Texture>(assets);
46+
47+
beforeEach(() =>
48+
{
49+
loaded = {};
50+
data = {};
51+
});
52+
53+
afterEach(() =>
54+
{
55+
cleanup();
56+
});
57+
58+
afterAll(() =>
59+
{
60+
load.mockRestore();
61+
get.mockRestore();
62+
});
63+
64+
it('loads assets', async () =>
65+
{
66+
const { result } = renderHook(() => useAssets<Texture>(assets));
67+
68+
expect(result.current.isPending).toBe(true);
69+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
70+
71+
expect(result.current.assets).toEqual(assets.map(({ src }) => defaultResults[src!.toString()]));
72+
});
73+
74+
it('accepts data', async () =>
75+
{
76+
// Explicitly type the T in the useAssets hook
77+
const { result } = renderHook(() => useAssets<Texture>([
78+
{ src: 'test.png', data: { test: '7a1c8bee' } },
79+
{ src: 'test2.png', data: { test: '230a3f41' } },
80+
]));
81+
82+
expect(result.current.isPending).toBe(true);
83+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
84+
85+
const { assets: [texture], isSuccess } = result.current;
86+
87+
expect(isSuccess).toBe(true);
88+
expect(data['test.png'].test).toBe('7a1c8bee');
89+
expect(data['test2.png'].test).toBe('230a3f41');
90+
91+
const isTexture = (texture?: Texture) => texture && texture instanceof Texture;
92+
93+
expect(isTexture(texture)).toBe(true);
94+
});
95+
96+
it('is properly typed with data', async () =>
97+
{
98+
// Do not provide a type for T in the useAssets hook
99+
const { result } = renderHook(() => useAssets([
100+
{ src: 'test.png', data: { test: 'd460dbdd' } },
101+
]));
102+
103+
expect(result.current.isPending).toBe(true);
104+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
105+
106+
const { assets: [texture], isSuccess } = result.current;
107+
108+
expect(isSuccess).toBe(true);
109+
110+
const isTexture = (texture?: Texture) => texture && texture instanceof Texture;
111+
112+
expect(isTexture(texture)).toBe(true);
113+
});
114+
});

0 commit comments

Comments
 (0)