import { act, renderHook } from '@testing-library/react' import { AssetRecordType, Editor, EditorProvider, TLAssetId } from '@tldraw/editor' import { ReactNode } from 'react' import { vi } from 'vitest' import { renderTldrawComponentWithEditor } from '../../../test/testutils/renderTldrawComponent' import { Tldraw } from '../../Tldraw' import { useImageOrVideoAsset } from './useImageOrVideoAsset' let editor: Editor const assetId = AssetRecordType.createId('image') const otherAssetId = AssetRecordType.createId('image2') function imageAsset(id: TLAssetId, w: number, src = 'http://localhost/image.png') { return { id, type: 'image' as const, typeName: 'asset' as const, props: { w, h: 100, name: 'image.png', isAnimated: false, mimeType: 'image/png', src, }, meta: {}, } } beforeEach(async () => { const result = await renderTldrawComponentWithEditor((onMount) => , { waitForPatterns: false, }) editor = result.editor editor.createAssets([imageAsset(assetId, 100)]) }) async function flush() { await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)) }) } function renderAssetHook(initialAssetId: TLAssetId | null) { const wrapper = ({ children }: { children: ReactNode }) => ( {children} ) return renderHook( ({ assetId }: { assetId: TLAssetId | null }) => useImageOrVideoAsset({ assetId, width: 100 }), { wrapper, initialProps: { assetId: initialAssetId } } ) } it('clears the resolved asset when the assetId is removed, and resolves it again when re-attached', async () => { const { result, rerender } = renderAssetHook(assetId) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) rerender({ assetId: null }) await flush() expect(result.current).toMatchObject({ url: null, asset: null }) rerender({ assetId }) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) }) it('resolves the asset again when its record is deleted and recreated', async () => { const { result } = renderAssetHook(assetId) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) await act(async () => editor.deleteAssets([assetId])) await flush() expect(result.current).toMatchObject({ url: null, asset: null }) await act(async () => editor.createAssets([imageAsset(assetId, 100)])) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) }) it('returns the new asset when switching to one that resolves to the same url', async () => { editor.createAssets([imageAsset(otherAssetId, 200)]) const { result, rerender } = renderAssetHook(assetId) await flush() expect(result.current.asset).toMatchObject({ id: assetId, props: { w: 100 } }) rerender({ assetId: otherAssetId }) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: otherAssetId, props: { w: 200 } }, }) }) it('shows the new asset, not the old one, when switching to an asset that has no url yet', async () => { // Like the dotcom asset stores, resolve an asset without a src to null rather than '' const resolve = editor.resolveAssetUrl.bind(editor) vi.spyOn(editor, 'resolveAssetUrl').mockImplementation( async (id, ctx) => (await resolve(id, ctx)) || null ) editor.createAssets([imageAsset(otherAssetId, 200, '')]) const { result, rerender } = renderAssetHook(assetId) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) rerender({ assetId: otherAssetId }) await flush() expect(result.current).toMatchObject({ url: null, asset: { id: otherAssetId } }) }) it('ignores a resolution that was still in flight when the asset record was deleted', async () => { const { result } = renderAssetHook(assetId) await flush() expect(result.current).toMatchObject({ url: 'http://localhost/image.png', asset: { id: assetId }, }) let finishResolving!: (url: string) => void vi.spyOn(editor, 'resolveAssetUrl').mockImplementation( () => new Promise((resolve) => (finishResolving = resolve)) ) // Zoom triggers a debounced re-resolution; drive the debounce with ticks until it fires await act(async () => { editor.setCamera({ x: 0, y: 0, z: 2 }) for (let i = 0; i < 40; i++) editor.emit('tick', 16) }) expect(editor.resolveAssetUrl).toHaveBeenCalled() await act(async () => editor.deleteAssets([assetId])) await flush() expect(result.current).toMatchObject({ url: null, asset: null }) await act(async () => finishResolving('http://localhost/image.png')) await flush() expect(result.current).toMatchObject({ url: null, asset: null }) })