补充图片素材选择器交互测试

覆盖选择、清除、预览占位、失效引用、只读和键盘操作。
This commit is contained in:
2026-09-03 13:52:29 +08:00
parent ed4e4af646
commit 9165418c48
2 changed files with 106 additions and 3 deletions
@@ -3,7 +3,6 @@ import { useState } from 'react';
import { ThemedModal } from '../../../../../components/modal/ThemedModal';
import type { SpriteAsset } from '../../../../../features/ui-editor/types/SpriteAsset';
import { ComponentField } from './ComponentField';
export function ImageAssetSelector({
value,
@@ -50,7 +49,8 @@ export function ImageAssetSelector({
return (
<>
<ComponentField label="目标素材">
<div className="block text-[11px] font-semibold text-(--platform-text-soft)">
<div className="mt-1 flex items-center gap-2">
<div
className={`min-w-0 flex-1 truncate rounded-lg border border-(--platform-subpanel-border) bg-white/65 px-2 py-2 text-xs ${value && !selectedSprite ? 'text-red-600' : 'text-(--platform-text-strong)'}`}
@@ -67,7 +67,7 @@ export function ImageAssetSelector({
{buttonLabel}
</button>
</div>
</ComponentField>
</div>
<ThemedModal
open={open}
@@ -0,0 +1,103 @@
/** @vitest-environment jsdom */
import { cleanup, 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<string, string> = {},
) {
return render(
<ImageAssetSelector
value={value}
sprites={sprites}
previewUrls={previewUrls}
readOnly={false}
onChange={onChange}
/>,
);
}
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(
<ImageAssetSelector
value="hero"
sprites={sprites}
previewUrls={{}}
readOnly
onChange={vi.fn()}
/>,
);
expect(screen.getByRole('button', { name: '更换素材' })).toHaveProperty(
'disabled',
true,
);
});
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');
});
});