下载失败改为弹窗交代并提供在文件管理器中打开
src-tauri/src/project/asset_export.rs:新增按保存同一条路径门禁解析产物位置的能力,供文件管理器定位复用。 src-tauri/src/commands/desktop.rs:新增 reveal_local_project_asset_file 命令,用系统文件管理器定位项目内产物。 src-tauri/src/desktop.rs:注册新命令。 view/project-development/export/state/xhsMinitoolDownload.ts:保存结果改为带失败原因,新增在文件管理器里定位原件的兜底调用。 view/project-development/export/tabs/xiaohongshu/DownloadFailureDialog.tsx:新增下载失败弹窗,给「关闭」与「在文件管理器中打开」两条路。 view/project-development/export/tabs/xiaohongshu/ArtifactsPane.tsx:下载失败改用弹窗与兜底出口,去掉会被滚出视野的一行小字。 tests/xhsMinitoolExport.test.tsx:覆盖失败弹窗、定位命令参数与定位失败留在弹窗。
This commit is contained in:
@@ -830,6 +830,22 @@ pub(crate) fn save_local_project_asset_file(
|
||||
save_local_project_asset_file_at(input)
|
||||
}
|
||||
|
||||
/// 在系统文件管理器里定位一份项目内产物。
|
||||
///
|
||||
/// 保存到用户选定位置失败时的兜底出口:原件还在项目里,把它的位置打开,用户自己复制。
|
||||
/// 路径由 [`reveal_local_project_asset_file_at`] 按「能保存就能定位」的同一条门禁复核。
|
||||
#[tauri::command]
|
||||
pub(crate) fn reveal_local_project_asset_file(
|
||||
app: tauri::AppHandle,
|
||||
project_path: String,
|
||||
relative_path: String,
|
||||
) -> Result<(), String> {
|
||||
let path = reveal_local_project_asset_file_at(project_path.trim(), relative_path.trim())?;
|
||||
app.opener()
|
||||
.reveal_item_in_dir(path)
|
||||
.map_err(|error| format!("打开文件管理器失败:{error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn read_local_project_image_preview(
|
||||
preview_manager: tauri::State<'_, ProjectResourcePreviewReadManager>,
|
||||
|
||||
@@ -668,6 +668,7 @@ pub(super) fn run() {
|
||||
read_local_project_file,
|
||||
read_local_project_image_preview,
|
||||
save_local_project_asset_file,
|
||||
reveal_local_project_asset_file,
|
||||
read_local_project_text_preview,
|
||||
read_local_project_structured_preview,
|
||||
read_local_project_media_preview,
|
||||
|
||||
@@ -210,6 +210,21 @@ pub(crate) fn save_local_project_asset_file_at(
|
||||
})
|
||||
}
|
||||
|
||||
/// 解析要在系统文件管理器里定位的那份导出产物。
|
||||
///
|
||||
/// 与保存共用 [`resolve_export_source_file`]:只有项目根内真实存在的普通文件才会被打开,路径
|
||||
/// 门禁、敏感文件与控制面拒绝口径完全一致。它是"保存失败后的兜底出口"——不替用户挑落点,只把
|
||||
/// 原件所在位置打开,让他自己复制。
|
||||
pub(crate) fn reveal_local_project_asset_file_at(
|
||||
project_path: &str,
|
||||
relative_path: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
let root = Path::new(project_path);
|
||||
validate_project_root(root)?;
|
||||
enforce_project_auto_permission_policy(root, "file.read")?;
|
||||
resolve_export_source_file(root, relative_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -285,6 +300,23 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&destination_directory);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_resolves_existing_project_file_and_rejects_missing() {
|
||||
let (root, assets) = create_export_project();
|
||||
std::fs::write(assets.join("hero.png"), b"payload").expect("write source asset");
|
||||
|
||||
let path = reveal_local_project_asset_file_at(&root.to_string_lossy(), "assets/hero.png")
|
||||
.expect("reveal existing project file");
|
||||
assert_eq!(path, assets.join("hero.png"));
|
||||
|
||||
let error =
|
||||
reveal_local_project_asset_file_at(&root.to_string_lossy(), "assets/missing.png")
|
||||
.expect_err("missing file must fail");
|
||||
assert!(error.contains("素材文件不存在或不可读"), "{error}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_directory_as_source_asset() {
|
||||
let (root, assets) = create_export_project();
|
||||
|
||||
+66
-10
@@ -4,17 +4,29 @@
|
||||
* 复用素材保存的既有链路 `save_local_project_asset_file`:路径门禁、敏感文件拒绝、自我覆盖保护
|
||||
* 都在宿主那一侧,前端不自己读文件、不拼绝对路径、也不写第二份保存实现。
|
||||
*
|
||||
* 返回值说明这次到底存没存下来:`saved`(宿主写完)、`cancelled`(用户关掉对话框)、`failed`
|
||||
* (对话框打不开 / 宿主拒绝写)。成功不必在面板上复述——落点是用户亲手选的,原生对话框就是回执;
|
||||
* 失败则由调用方决定怎么告知,不能像以前那样只留一条控制台痕迹。
|
||||
* 返回值不只说明这次存没存下来,失败还带一句原因:`saved`(宿主写完)、`cancelled`(用户关掉
|
||||
* 对话框)、`failed`(对话框打不开 / 宿主拒绝写)。成功不必在面板上复述——落点是用户亲手选的,
|
||||
* 原生对话框就是回执;失败则由调用方决定怎么告知,不能像以前那样只留一条控制台痕迹,也不能只给
|
||||
* 一句没有下文的「请重试」。
|
||||
*
|
||||
* 同一条保存链路上还有 [`revealXhsMinitoolFileInManager`]:保存失败时用系统文件管理器打开原件
|
||||
* 所在位置,作为用户自己把文件拿出去的兜底出口。
|
||||
*/
|
||||
import { save as saveNativeFileDialog } from '@tauri-apps/plugin-dialog';
|
||||
|
||||
import { resolveTauriInvoke } from '../../../../app/tauri';
|
||||
import { resourceFileNameFromPath } from '../../../../features/resource-canvas/resourceCanvasAssetTransferModel';
|
||||
|
||||
/** 一次导出产物保存的结局。 */
|
||||
export type XhsMinitoolDownloadResult = 'saved' | 'cancelled' | 'failed';
|
||||
/** 一次导出产物保存的结局。失败带一句可展示的原因(宿主给的是中文人话)。 */
|
||||
export type XhsMinitoolDownloadResult =
|
||||
| { status: 'saved' }
|
||||
| { status: 'cancelled' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
/** 在系统文件管理器里定位一份导出产物的结局。 */
|
||||
export type XhsMinitoolRevealResult =
|
||||
| { status: 'revealed' }
|
||||
| { status: 'failed'; reason: string };
|
||||
|
||||
export async function saveXhsMinitoolFileToDisk(input: {
|
||||
projectPath: string;
|
||||
@@ -25,7 +37,9 @@ export async function saveXhsMinitoolFileToDisk(input: {
|
||||
title: string;
|
||||
}): Promise<XhsMinitoolDownloadResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) return 'failed';
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能保存文件' };
|
||||
}
|
||||
let destination: string | null = null;
|
||||
try {
|
||||
destination = await saveNativeFileDialog({
|
||||
@@ -37,10 +51,10 @@ export async function saveXhsMinitoolFileToDisk(input: {
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('导出产物保存对话框没能打开', error);
|
||||
return 'failed';
|
||||
return { status: 'failed', reason: '保存对话框没能打开' };
|
||||
}
|
||||
// 用户取消对话框:什么都没发生,也没有话要说。
|
||||
if (!destination) return 'cancelled';
|
||||
if (!destination) return { status: 'cancelled' };
|
||||
try {
|
||||
await invoke('save_local_project_asset_file', {
|
||||
input: {
|
||||
@@ -49,9 +63,51 @@ export async function saveXhsMinitoolFileToDisk(input: {
|
||||
destinationPath: destination,
|
||||
},
|
||||
});
|
||||
return 'saved';
|
||||
return { status: 'saved' };
|
||||
} catch (error) {
|
||||
console.warn('导出产物没能写到你选的位置', error);
|
||||
return 'failed';
|
||||
return { status: 'failed', reason: exportFailureReason(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在系统文件管理器里定位这份导出产物。
|
||||
*
|
||||
* 保存失败后的兜底出口:不去替用户挑落点,只把原件所在位置打开,让他自己复制。与保存共用同一条
|
||||
* 宿主路径门禁——能保存的原件才能被定位,打开的一定是项目里真实存在的那份文件。
|
||||
*/
|
||||
export async function revealXhsMinitoolFileInManager(input: {
|
||||
projectPath: string;
|
||||
relativePath: string;
|
||||
}): Promise<XhsMinitoolRevealResult> {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
return { status: 'failed', reason: '需要在应用内运行才能打开文件管理器' };
|
||||
}
|
||||
try {
|
||||
await invoke('reveal_local_project_asset_file', {
|
||||
projectPath: input.projectPath,
|
||||
relativePath: input.relativePath,
|
||||
});
|
||||
return { status: 'revealed' };
|
||||
} catch (error) {
|
||||
console.warn('导出产物没能在文件管理器里打开', error);
|
||||
return { status: 'failed', reason: exportFailureReason(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把宿主或对话框抛出的原因收成一句能给用户看的话。
|
||||
*
|
||||
* 宿主这条链路的拒绝都是中文人话(`Result<_, String>`),原样带出去;认不出形状时退回一句通用
|
||||
* 说明,不把 `[object Object]` 之类的内部形状糊到界面上。
|
||||
*/
|
||||
function exportFailureReason(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error.trim() || '写入所选位置失败';
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message.trim() || '写入所选位置失败';
|
||||
}
|
||||
return '写入所选位置失败';
|
||||
}
|
||||
|
||||
+64
-22
@@ -2,11 +2,18 @@ import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import type { XhsMinitoolExportState } from '../../state/useXhsMinitoolExport';
|
||||
import { saveXhsMinitoolFileToDisk } from '../../state/xhsMinitoolDownload';
|
||||
import {
|
||||
revealXhsMinitoolFileInManager,
|
||||
saveXhsMinitoolFileToDisk,
|
||||
} from '../../state/xhsMinitoolDownload';
|
||||
import { XHS_MINITOOL_ARTIFACT_RELATIVE_PATH } from '../../state/xhsMinitoolInstruction';
|
||||
import { PaneButton } from '../common/PaneButton';
|
||||
import { AdaptPromptDialog } from './AdaptPromptDialog';
|
||||
import { ConflictCard } from './ConflictCard';
|
||||
import {
|
||||
DownloadFailureDialog,
|
||||
type XhsMinitoolDownloadFailure,
|
||||
} from './DownloadFailureDialog';
|
||||
import { FailureDialog } from './FailureDialog';
|
||||
import { FormCard } from './FormCard';
|
||||
|
||||
@@ -23,9 +30,10 @@ const XHS_PUBLISH_URL = 'https://creator.xiaohongshu.com/new/red-app';
|
||||
* 内容与脚本判据按固定间隔重读(所以没有刷新按钮)。
|
||||
*
|
||||
* 「还没配置好导出」不再是一张常驻卡片,而是阻断弹窗:重读每 2 秒一次,卡片会跟着重读闪,弹窗则
|
||||
* 只在用户真的需要做选择时出现一次。失败现场同理走阻断弹窗(`FailureDialog`):左半本来就矮,
|
||||
* 卡片会被滚出视野,而失败是用户必须看见的东西。可见文案说用户的话,不出现脚本名、注册表、zip
|
||||
* 这些实现词。
|
||||
* 只在用户真的需要做选择时出现一次。失败现场同理走阻断弹窗:构建 / 读取失败走 `FailureDialog`,
|
||||
* 下载失败走 `DownloadFailureDialog`(后者多给一条「在文件管理器中打开原件」的自助出口)。左半
|
||||
* 本来就矮,卡片会被滚出视野,而失败是用户必须看见的东西。可见文案说用户的话,不出现脚本名、
|
||||
* 注册表、zip 这些实现词。
|
||||
*/
|
||||
export function ArtifactsPane({
|
||||
minitool,
|
||||
@@ -35,8 +43,14 @@ export function ArtifactsPane({
|
||||
projectPath: string | null;
|
||||
}) {
|
||||
const { form, conflict, failure } = minitool;
|
||||
/** 下载失败的一句话现场:成功 / 用户取消都不说,只有宿主真的没写下来才占这一行。 */
|
||||
const [downloadNotice, setDownloadNotice] = useState('');
|
||||
/**
|
||||
* 下载失败现场:宿主拒绝写盘或对话框打不开时,用弹窗把「哪一份、为什么、下一步」一次说清,
|
||||
* 并给出「在文件管理器中打开原件」这条兜底出口。
|
||||
*/
|
||||
const [downloadFailure, setDownloadFailure] =
|
||||
useState<XhsMinitoolDownloadFailure | null>(null);
|
||||
/** 「在文件管理器中打开」自身失败时的一句话;没失败时为空。 */
|
||||
const [revealFailure, setRevealFailure] = useState('');
|
||||
/**
|
||||
* 未适配:`hasScript` 的初值是 `false`,注册表还没读回来(或读失败)时我们不能替项目下
|
||||
* 「还没适配」的结论——那种情况只该显示读取中或失败现场。
|
||||
@@ -55,37 +69,63 @@ export function ArtifactsPane({
|
||||
const showAdaptPrompt = needsAdaptation && !minitool.adaptPromptDismissed;
|
||||
|
||||
// 成功不打回执:用户刚在原生对话框里选完落点,磁盘上有没有那个文件是他自己看得见的事实。
|
||||
// 失败则必须说一句话——宿主拒绝落盘不能只留一条控制台痕迹。
|
||||
// 失败则必须让用户看见——宿主拒绝落盘不能只留一条控制台痕迹,也不能只给一句「请重试」。
|
||||
const downloadIcon = (): void => {
|
||||
if (!projectPath || !form) return;
|
||||
setDownloadNotice('');
|
||||
const relativePath = form.iconPath.trim();
|
||||
void saveXhsMinitoolFileToDisk({
|
||||
projectPath,
|
||||
relativePath: form.iconPath.trim(),
|
||||
relativePath,
|
||||
fallbackFileName: 'icon.png',
|
||||
title: '保存图标文件',
|
||||
}).then((result) => {
|
||||
if (result === 'failed') {
|
||||
setDownloadNotice('没能把图标保存到所选位置,请重试');
|
||||
}
|
||||
if (result.status !== 'failed') return;
|
||||
setRevealFailure('');
|
||||
setDownloadFailure({ kind: 'icon', relativePath, reason: result.reason });
|
||||
});
|
||||
};
|
||||
|
||||
const downloadArtifact = (): void => {
|
||||
if (!projectPath) return;
|
||||
setDownloadNotice('');
|
||||
const relativePath = XHS_MINITOOL_ARTIFACT_RELATIVE_PATH;
|
||||
void saveXhsMinitoolFileToDisk({
|
||||
projectPath,
|
||||
relativePath: XHS_MINITOOL_ARTIFACT_RELATIVE_PATH,
|
||||
relativePath,
|
||||
fallbackFileName: 'xhs-minitool.zip',
|
||||
title: '保存代码包',
|
||||
}).then((result) => {
|
||||
if (result === 'failed') {
|
||||
setDownloadNotice('没能把代码包保存到所选位置,请重试');
|
||||
}
|
||||
if (result.status !== 'failed') return;
|
||||
setRevealFailure('');
|
||||
setDownloadFailure({
|
||||
kind: 'artifact',
|
||||
relativePath,
|
||||
reason: result.reason,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** 兜底出口:让系统文件管理器定位原件;打不开就把原因留在弹窗里。 */
|
||||
const revealDownloadedFile = (): void => {
|
||||
if (!projectPath || !downloadFailure) return;
|
||||
void revealXhsMinitoolFileInManager({
|
||||
projectPath,
|
||||
relativePath: downloadFailure.relativePath,
|
||||
}).then((result) => {
|
||||
if (result.status === 'failed') {
|
||||
setRevealFailure(result.reason);
|
||||
return;
|
||||
}
|
||||
// 文件管理器已经弹出:收起弹窗,别挡着用户去看。
|
||||
setDownloadFailure(null);
|
||||
setRevealFailure('');
|
||||
});
|
||||
};
|
||||
|
||||
const dismissDownloadFailure = (): void => {
|
||||
setDownloadFailure(null);
|
||||
setRevealFailure('');
|
||||
};
|
||||
|
||||
/**
|
||||
* 左半顶部槽位:整张表单 / 一行读取中 / 什么都不放(读失败由弹窗拿着现场)/ 一行自动重试兜底。
|
||||
* 四种形状差得远,用 if/else 摊开算好,JSX 里只留一个 `{formSlot}`,别把控制流埋进链式三元。
|
||||
@@ -117,11 +157,6 @@ export function ArtifactsPane({
|
||||
aria-label="导出内容区"
|
||||
>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-3">
|
||||
{downloadNotice ? (
|
||||
<p role="status" className="text-xs text-[#b4562f]">
|
||||
{downloadNotice}
|
||||
</p>
|
||||
) : null}
|
||||
{formSlot}
|
||||
|
||||
<section
|
||||
@@ -200,6 +235,13 @@ export function ArtifactsPane({
|
||||
onRepair={minitool.repair}
|
||||
onDismiss={minitool.dismissFailure}
|
||||
/>
|
||||
|
||||
<DownloadFailureDialog
|
||||
failure={downloadFailure}
|
||||
revealFailure={revealFailure}
|
||||
onReveal={revealDownloadedFile}
|
||||
onDismiss={dismissDownloadFailure}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { ThemedModal } from '../../../../../components/modal/ThemedModal';
|
||||
import { PaneButton } from '../common/PaneButton';
|
||||
|
||||
/** 哪一份产物没存下来:文案与「在文件管理器中打开」的目标都跟着它走。 */
|
||||
export type XhsMinitoolDownloadFailure = {
|
||||
kind: 'icon' | 'artifact';
|
||||
/** 项目内相对路径:用户要在文件管理器里打开的就是它。 */
|
||||
relativePath: string;
|
||||
/** 保存失败的原因(宿主给的中文说明,或一句通用说明)。 */
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载失败弹窗:一句「哪一份没存下来」+ 原因 + 兜底出口。
|
||||
*
|
||||
* 保存失败不再只留一行会被滚出视野的小字:左半本来就矮,而这是用户点过下载之后必须看见的结果。
|
||||
* 弹窗给两条路:关掉,或者让系统文件管理器打开原件所在位置自己复制——宿主拒绝写盘时(权限、
|
||||
* 目标不存在、路径被门禁拦下),用户至少还能把项目里那份原件拿出去。
|
||||
*
|
||||
* 与失败弹窗(`FailureDialog`)同一种阻断层:背景点击与 Esc 不生效。宿主的原始错误不在这里展开,
|
||||
* `reason` 只用调用方收好的那一句人话。
|
||||
*/
|
||||
export function DownloadFailureDialog({
|
||||
failure,
|
||||
revealFailure,
|
||||
onReveal,
|
||||
onDismiss,
|
||||
}: {
|
||||
/** 没有下载失败现场时为 `null`,弹窗关着。 */
|
||||
failure: XhsMinitoolDownloadFailure | null;
|
||||
/** 「在文件管理器中打开」自身失败时的原因;空串表示没有。 */
|
||||
revealFailure: string;
|
||||
onReveal: () => void;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ThemedModal
|
||||
open={failure !== null}
|
||||
ariaLabel="下载失败"
|
||||
onClose={onDismiss}
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
panelClassName="w-full max-w-md rounded-2xl border border-[#e2cbb8] p-4"
|
||||
>
|
||||
<p className="text-sm leading-relaxed text-[#3d1f10]">
|
||||
{failure
|
||||
? `没能把${failure.kind === 'icon' ? '图标' : '代码包'}保存到你选的位置。`
|
||||
: null}
|
||||
</p>
|
||||
{failure ? (
|
||||
<p className="mt-2 text-xs leading-relaxed text-[#6f5848]">
|
||||
{failure.reason}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-xs leading-relaxed text-[#6f5848]">
|
||||
也可以让系统在文件管理器里打开它原来的位置,自己复制过去。
|
||||
</p>
|
||||
{revealFailure ? (
|
||||
<p role="status" className="mt-2 text-xs text-[#b4562f]">
|
||||
{revealFailure}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mt-4 flex flex-wrap justify-end gap-2">
|
||||
<PaneButton onClick={onDismiss}>关闭</PaneButton>
|
||||
<PaneButton variant="primary" onClick={onReveal}>
|
||||
在文件管理器中打开
|
||||
</PaneButton>
|
||||
</div>
|
||||
</ThemedModal>
|
||||
);
|
||||
}
|
||||
@@ -298,13 +298,13 @@ describe('面板左半', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('宿主拒绝落盘时不静默:面板给出重试提示', async () => {
|
||||
it('宿主拒绝落盘时弹出失败面板,并给出「在文件管理器中打开」的兜底出口', async () => {
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'read_local_project_image_preview') {
|
||||
return { dataUrl: 'data:image/png;base64,icon' };
|
||||
}
|
||||
if (command === 'save_local_project_asset_file') {
|
||||
throw new Error('目标位置不可写');
|
||||
throw '目标位置不可写';
|
||||
}
|
||||
return {};
|
||||
});
|
||||
@@ -313,13 +313,46 @@ describe('面板左半', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载代码包' }));
|
||||
|
||||
// 弹窗:哪一份没存下来、为什么,以及用户自己把原件拿出去的出口。
|
||||
await screen.findByText('没能把代码包保存到你选的位置。');
|
||||
screen.getByText('目标位置不可写');
|
||||
screen.getByText(/在文件管理器里打开它原来的位置/u);
|
||||
|
||||
// 兜底出口走宿主与保存同一条门禁的定位命令。
|
||||
fireEvent.click(screen.getByRole('button', { name: '在文件管理器中打开' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText('没能把代码包保存到所选位置,请重试'),
|
||||
).toBeTruthy(),
|
||||
expect(invoke).toHaveBeenCalledWith('reveal_local_project_asset_file', {
|
||||
projectPath: PROJECT_PATH,
|
||||
relativePath: XHS_MINITOOL_ARTIFACT_RELATIVE_PATH,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('文件管理器也打不开时,弹窗留在原地并说清原因', async () => {
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'read_local_project_image_preview') {
|
||||
return { dataUrl: 'data:image/png;base64,icon' };
|
||||
}
|
||||
if (command === 'save_local_project_asset_file') {
|
||||
throw '目标位置不可写';
|
||||
}
|
||||
if (command === 'reveal_local_project_asset_file') {
|
||||
throw '系统没有可用的文件管理器';
|
||||
}
|
||||
return {};
|
||||
});
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderPane(fakeMinitool({ artifactReady: true }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载代码包' }));
|
||||
await screen.findByText('没能把代码包保存到你选的位置。');
|
||||
fireEvent.click(screen.getByRole('button', { name: '在文件管理器中打开' }));
|
||||
|
||||
await screen.findByText('系统没有可用的文件管理器');
|
||||
// 弹窗还在:用户可以改关弹窗、或在文件管理器恢复后重试。
|
||||
screen.getByRole('button', { name: '在文件管理器中打开' });
|
||||
});
|
||||
|
||||
it('图标先出缩略图,选择图标走公共素材导入器并把路径写回表单', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
|
||||
Reference in New Issue
Block a user