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
before:  after:  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>
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
/** @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();
|
||
});
|
||
});
|