Files
Genarrative/apps/ai-game-creator-shell/tests/imageAssetSelector.test.tsx
T
k88936 f5f94111ca
Project CI / Native shell tests (push) Successful in 20m12s
Project CI / Repository checks (push) Successful in 2m42s
Project CI / Frontend tests (push) Successful in 3m39s
Project CI / Backend tests (push) Successful in 7m18s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Native shell tests (pull_request) Failing after 32m52s
Project CI / Frontend tests (pull_request) Failing after 44m27s
Feat/图片绑定选择器改用带预览的模态框 (#269)
before:
![image.png](/attachments/5ae7df1f-cf57-4712-8038-f65ae83e2ffa)

after:
![shotmd-1788417438.jpg](/attachments/676e653d-2561-4eca-b28f-9725d3b8af02)

close #268

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/269
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-09-03 20:02:48 +08:00

162 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @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<string, string> = {},
selectorSprites: typeof sprites = sprites,
) {
return render(
<ImageAssetSelector
value={value}
sprites={selectorSprites}
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('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(
<ImageAssetSelector
value={null}
sprites={sprites}
previewUrls={{ hero: 'data:image/png;base64,regenerated' }}
readOnly={false}
onChange={vi.fn()}
/>,
);
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();
});
});