/** @vitest-environment jsdom */ import { cleanup, fireEvent, render, screen, within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { ImageAssetSelector } from '../src/view/ui-editor/components/Inspector/Components/ImageAssetSelector'; const sprites = { hero: { asset_id: 'hero', metadata: { name: '主角', asset_type: 'sprite' }, path: 'assets/hero.png', pixel_size: [32, 32] as [number, number], pixels_per_unit: 1, border: { left: 0, right: 0, top: 0, bottom: 0 }, }, enemy: { asset_id: 'enemy', metadata: { name: '敌人', asset_type: 'sprite' }, path: 'assets/enemy.png', pixel_size: [32, 32] as [number, number], pixels_per_unit: 1, border: { left: 0, right: 0, top: 0, bottom: 0 }, }, }; function renderSelector( value: string | null = null, onChange = vi.fn(), previewUrls: Record = {}, selectorSprites: typeof sprites = sprites, ) { return render( , ); } describe('ImageAssetSelector', () => { afterEach(() => cleanup()); it('selects a sprite and clears the selection from the modal', async () => { const user = userEvent.setup(); const onChange = vi.fn(); renderSelector(null, onChange); await user.click(screen.getByRole('button', { name: '选择素材' })); expect(screen.getByRole('dialog', { name: '选择图片素材' })).toBeTruthy(); await user.click(screen.getByRole('button', { name: /主角/ })); expect(onChange).toHaveBeenCalledWith('hero'); expect(screen.queryByRole('dialog')).toBeNull(); await user.click(screen.getByRole('button', { name: '选择素材' })); await user.click(screen.getByRole('button', { name: /清除选择/ })); expect(onChange).toHaveBeenLastCalledWith(null); }); it('shows preview images, placeholders, and invalid references', async () => { const user = userEvent.setup(); renderSelector('missing', vi.fn(), { hero: 'data:image/png;base64,abc' }); expect(screen.getByText('素材不存在(missing)')).toBeTruthy(); await user.click(screen.getByRole('button', { name: '更换素材' })); const dialog = screen.getByRole('dialog', { name: '选择图片素材' }); expect(within(dialog).getByAltText('主角')).toBeTruthy(); expect(within(dialog).getByText('素材不存在')).toBeTruthy(); expect( within(dialog).getByRole('button', { name: /清除选择/ }), ).toBeTruthy(); }); it('cannot open while read only', () => { render( , ); expect(screen.getByRole('button', { name: '更换素材' })).toHaveProperty( 'disabled', true, ); }); it('retries a preview when its URL is regenerated', async () => { const user = userEvent.setup(); const view = renderSelector(null, vi.fn(), { hero: 'data:image/png;base64,first', }); await user.click(screen.getByRole('button', { name: '选择素材' })); const dialog = screen.getByRole('dialog', { name: '选择图片素材' }); const image = within(dialog).getByAltText('主角'); fireEvent.error(image); expect(within(dialog).queryByAltText('主角')).toBeNull(); view.rerender( , ); expect( within(screen.getByRole('dialog', { name: '选择图片素材' })).getByAltText( '主角', ), ).toBeTruthy(); }); it('supports keyboard activation for sprite cards', async () => { const user = userEvent.setup(); const onChange = vi.fn(); renderSelector(null, onChange); await user.click(screen.getByRole('button', { name: '选择素材' })); const hero = screen.getByRole('button', { name: /主角/ }); hero.focus(); await user.keyboard('{Enter}'); expect(onChange).toHaveBeenCalledWith('hero'); }); it('closes when the modal backdrop is clicked', async () => { const user = userEvent.setup(); renderSelector(); await user.click(screen.getByRole('button', { name: '选择素材' })); const dialog = screen.getByRole('dialog', { name: '选择图片素材' }); const overlay = dialog.parentElement; expect(overlay).toBeTruthy(); await user.click(overlay!); expect(screen.queryByRole('dialog', { name: '选择图片素材' })).toBeNull(); }); it('shows an empty state when no sprites are available', async () => { const user = userEvent.setup(); renderSelector(null, vi.fn(), {}, {}); await user.click(screen.getByRole('button', { name: '选择素材' })); const dialog = screen.getByRole('dialog', { name: '选择图片素材' }); expect(within(dialog).getByText('暂无可用素材')).toBeTruthy(); expect( within(dialog).getByRole('button', { name: /清除选择/ }), ).toBeTruthy(); }); });