修复资源增量导入失败后的状态同步
本地与远端导入失败后重新读取项目 manifest 保留原导入错误提示并覆盖已提交资源恢复场景 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -208,6 +208,12 @@ export function AssetImporter({
|
||||
void refresh(ROOT_PATH);
|
||||
}, [open, refresh, settings]);
|
||||
|
||||
const refreshProjectAfterImportFailure = async (cause: unknown) => {
|
||||
const importError = cause instanceof Error ? cause.message : String(cause);
|
||||
await refresh(PROJECT_ASSETS_PATH);
|
||||
setError(importError);
|
||||
};
|
||||
|
||||
const pickLocal = async () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
@@ -247,7 +253,7 @@ export function AssetImporter({
|
||||
);
|
||||
onClose();
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
await refreshProjectAfterImportFailure(cause);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -299,7 +305,7 @@ export function AssetImporter({
|
||||
onImport(imported);
|
||||
onClose();
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
await refreshProjectAfterImportFailure(cause);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,127 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { FONT_IMPORTER_SETTINGS } from '../src/components/AssetImporter/settings';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { createElement } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
loadEditorAssetLibrary: vi.fn(),
|
||||
openNativeFileDialog: vi.fn(),
|
||||
resolveClientAssetReadUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: mocks.invoke }));
|
||||
vi.mock('@tauri-apps/plugin-dialog', () => ({
|
||||
open: mocks.openNativeFileDialog,
|
||||
}));
|
||||
vi.mock('../src/services/clientApi', () => ({
|
||||
loadEditorAssetLibrary: mocks.loadEditorAssetLibrary,
|
||||
resolveClientAssetReadUrl: mocks.resolveClientAssetReadUrl,
|
||||
}));
|
||||
vi.mock('@cubone/react-file-manager', () => ({
|
||||
FileManager: ({
|
||||
files,
|
||||
onFolderChange,
|
||||
onSelectionChange,
|
||||
}: {
|
||||
files: Array<{
|
||||
asset?: { id: string };
|
||||
isDirectory: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
}>;
|
||||
onFolderChange: (path: string) => void;
|
||||
onSelectionChange: (files: unknown[]) => void;
|
||||
}) =>
|
||||
createElement(
|
||||
'div',
|
||||
null,
|
||||
createElement(
|
||||
'button',
|
||||
{
|
||||
onClick: () => onFolderChange('/云端素材库'),
|
||||
type: 'button',
|
||||
},
|
||||
'打开云端素材库',
|
||||
),
|
||||
...files
|
||||
.filter((file) => !file.isDirectory && file.asset)
|
||||
.map((file) =>
|
||||
createElement(
|
||||
'button',
|
||||
{
|
||||
key: file.path,
|
||||
onClick: () => onSelectionChange([file]),
|
||||
type: 'button',
|
||||
},
|
||||
`选择 ${file.name}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
import { AssetImporter } from '../src/components/AssetImporter';
|
||||
import {
|
||||
FONT_IMPORTER_SETTINGS,
|
||||
SPRITE_IMPORTER_SETTINGS,
|
||||
} from '../src/components/AssetImporter/settings';
|
||||
import {
|
||||
buildProjectFiles,
|
||||
buildRootFiles,
|
||||
} from '../src/components/AssetImporter/utils';
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.invoke.mockReset();
|
||||
mocks.loadEditorAssetLibrary.mockReset();
|
||||
mocks.openNativeFileDialog.mockReset();
|
||||
mocks.resolveClientAssetReadUrl.mockReset();
|
||||
let manifestReadCount = 0;
|
||||
mocks.invoke.mockImplementation(async (command: string) => {
|
||||
if (command === 'get_local_game_manifest') {
|
||||
manifestReadCount += 1;
|
||||
return {
|
||||
assets:
|
||||
manifestReadCount === 1
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: 'committed-1',
|
||||
localPath: 'assets/uploads/committed.png',
|
||||
mediaType: 'image/png',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (command === 'list_local_project_files') {
|
||||
return { files: [{ path: 'assets/uploads/committed.png' }] };
|
||||
}
|
||||
if (command === 'import_ui_editor_assets') {
|
||||
throw new Error('第二个素材登记失败');
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
mocks.loadEditorAssetLibrary.mockResolvedValue({
|
||||
folders: [{ folderId: 'folder-1', label: '角色' }],
|
||||
assets: [
|
||||
{
|
||||
folderId: 'folder-1',
|
||||
assetId: 'remote-1',
|
||||
objectKey: 'remote/hero.png',
|
||||
label: '英雄',
|
||||
size: 1024,
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.openNativeFileDialog.mockResolvedValue([
|
||||
'/tmp/first.png',
|
||||
'/tmp/second.png',
|
||||
]);
|
||||
mocks.resolveClientAssetReadUrl.mockResolvedValue(
|
||||
'https://assets.example/hero.png',
|
||||
);
|
||||
});
|
||||
|
||||
describe('AssetImporter font mode', () => {
|
||||
it('discovers only registered font candidates and hides the remote root', () => {
|
||||
const assets = [
|
||||
@@ -39,3 +155,77 @@ describe('AssetImporter font mode', () => {
|
||||
).toEqual(['本地项目']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AssetImporter incremental import recovery', () => {
|
||||
it('reloads the committed manifest after a local import partially fails', async () => {
|
||||
render(
|
||||
createElement(AssetImporter, {
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
onImport: vi.fn(),
|
||||
projectPath: '/tmp/ui-editor',
|
||||
settings: SPRITE_IMPORTER_SETTINGS,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '从电脑导入' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
),
|
||||
).toHaveLength(2),
|
||||
);
|
||||
expect(screen.getByRole('alert').textContent).toContain(
|
||||
'第二个素材登记失败',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '选择 committed.png' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('reloads the committed manifest after a remote import partially fails', async () => {
|
||||
render(
|
||||
createElement(AssetImporter, {
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
onImport: vi.fn(),
|
||||
projectPath: '/tmp/ui-editor',
|
||||
settings: SPRITE_IMPORTER_SETTINGS,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
),
|
||||
).toHaveLength(1),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开云端素材库' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '选择 英雄' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '导入' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
mocks.invoke.mock.calls.filter(
|
||||
([command]) => command === 'get_local_game_manifest',
|
||||
),
|
||||
).toHaveLength(2),
|
||||
);
|
||||
expect(screen.getByRole('alert').textContent).toContain(
|
||||
'第二个素材登记失败',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '选择 committed.png' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user