修复素材预览失败后的重试逻辑
按预览 URL 记录失败状态,允许资源重新生成地址后再次加载 补充预览重试回归测试并同步 UI 编辑会话边界文档
This commit is contained in:
+9
-11
@@ -18,9 +18,9 @@ export function ImageAssetSelector({
|
||||
onChange: (value: string | null) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [failedPreviewIds, setFailedPreviewIds] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const [failedPreviewUrls, setFailedPreviewUrls] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const selectedSprite = value ? sprites[value] : undefined;
|
||||
const selectedLabel = selectedSprite
|
||||
? selectedSprite.metadata.name || value || '未绑定'
|
||||
@@ -38,12 +38,10 @@ export function ImageAssetSelector({
|
||||
close();
|
||||
}
|
||||
|
||||
function markPreviewFailed(id: string) {
|
||||
setFailedPreviewIds((current) => {
|
||||
if (current.has(id)) return current;
|
||||
const next = new Set(current);
|
||||
next.add(id);
|
||||
return next;
|
||||
function markPreviewFailed(id: string, url: string) {
|
||||
setFailedPreviewUrls((current) => {
|
||||
if (current[id] === url) return current;
|
||||
return { ...current, [id]: url };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,7 +109,7 @@ export function ImageAssetSelector({
|
||||
{Object.entries(sprites).map(([id, sprite]) => {
|
||||
const previewUrl = previewUrls[id];
|
||||
const hasPreview =
|
||||
Boolean(previewUrl) && !failedPreviewIds.has(id);
|
||||
Boolean(previewUrl) && failedPreviewUrls[id] !== previewUrl;
|
||||
const selected = value === id;
|
||||
return (
|
||||
<button
|
||||
@@ -127,7 +125,7 @@ export function ImageAssetSelector({
|
||||
src={previewUrl}
|
||||
alt={sprite.metadata.name || id}
|
||||
className="size-full object-contain"
|
||||
onError={() => markPreviewFailed(id)}
|
||||
onError={() => markPreviewFailed(id, previewUrl)}
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/** @vitest-environment jsdom */
|
||||
import { cleanup, render, screen, within } from '@testing-library/react';
|
||||
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';
|
||||
|
||||
@@ -90,6 +96,35 @@ describe('ImageAssetSelector', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
预览树在会话边界归一化为 `UITree | null`:尚未选择界面图、正在加载或尚未识别树时均以 `null` 表示,不能把 `Array.find` 的 `undefined` 传播到视图接口。
|
||||
|
||||
素材缩略图的加载失败状态按“素材 ID + 预览 URL”记录;同一 URL 失败后显示占位图,资源重新加载或重新导入产生新 URL 时必须允许再次尝试,不能把失败永久绑定到素材 ID。
|
||||
|
||||
保存与代码生成共享同一份持久化 State/revision。会话层在保存或生成进行期间互斥拦截,且代码生成必须基于已加载的持久化 revision;视图层的保存按钮和“保存并返回”按钮同步遵守该互斥状态。
|
||||
|
||||
`UiDesignStateStore` 的 `generateCode(assetId)` 是必需能力,返回成功结果时不得为 nullable;所有注入的 adapter 与测试替身都必须实现该方法。
|
||||
|
||||
Reference in New Issue
Block a user