fix(conversion): 轉檔完成頁「已加入」按鈕不再重複 promote

SuccessView「加到模型庫」按鈕文字依 importedModelId 切換(已加入/加到模型庫),
但 onClick 永遠是 handleImportClick(開 PromoteDialog)→ 已 promote 後再點又
開 dialog 又 promote。

- 按鈕 onClick 改 handleImportButtonClick 分流:importedModelId 有值→
  router.push(/models/{id}) 導航(不再 promote);空→開 PromoteDialog(現狀)
- aria-label 同步切換(import.aria.done)對齊實際行為
- 後端 PromoteToModels 本有冪等(同 jobID 不重複建),前端 onClick 為防禦層

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-07-01 00:30:18 +08:00
parent 7d8ad4857e
commit ab50691f8a
4 changed files with 100 additions and 2 deletions

View File

@ -447,6 +447,83 @@ describe("<SuccessView /> — 加到模型庫", () => {
const text = btn.textContent ?? "";
expect(/已加入|Added/.test(text)).toBe(true);
});
it("尚未 promote 時點按鈕 → 開 PromoteDialog、不導航", () => {
setSucceeded();
renderView();
// 初始無 dialog、無導航
expect(screen.queryByTestId("promote-dialog")).toBeNull();
fireEvent.click(screen.getByTestId("success-import-button"));
// 開 dialog
expect(screen.getByTestId("promote-dialog")).toBeTruthy();
// 不導航
expect(mockRouterPush).not.toHaveBeenCalled();
});
it("已 promote 後再點按鈕 → 導向 /models/{id}、不再開 dialog、不再 promote", async () => {
setSucceeded();
const promoteSpy = vi
.spyOn(useConversionStore.getState(), "promoteToModels")
.mockResolvedValue({ model_id: "m-dup-guard" });
renderView();
// 第一次:開 dialog → 確認 → promote 成功
fireEvent.click(screen.getByTestId("success-import-button"));
fireEvent.click(screen.getByTestId("promote-dialog-confirm"));
await waitFor(() => {
expect(screen.queryByTestId("promote-dialog")).toBeNull();
});
await waitFor(() => {
expect(promoteSpy).toHaveBeenCalledTimes(1);
});
// 按鈕已切「已加入」狀態
const btn = screen.getByTestId("success-import-button");
expect(/已加入|Added/.test(btn.textContent ?? "")).toBe(true);
// 第二次點同一顆按鈕 → 應只導航、不再開 dialog、不再 promote
fireEvent.click(btn);
// 不再開 PromoteDialog
expect(screen.queryByTestId("promote-dialog")).toBeNull();
// promote 仍然只被呼叫過一次(沒有重複 promote
expect(promoteSpy).toHaveBeenCalledTimes(1);
// 導向模型詳細頁
expect(mockRouterPush).toHaveBeenCalledWith("/models/m-dup-guard");
});
it("已 promote 後按鈕 aria-label 切成 done 文案(語意為前往查看)", async () => {
setSucceeded();
vi.spyOn(useConversionStore.getState(), "promoteToModels").mockResolvedValue(
{ model_id: "m-aria" },
);
renderView();
// promote 前aria-label 為 cta含「對話框 / dialog」語意
const btnBefore = screen.getByTestId("success-import-button");
const ariaBefore = btnBefore.getAttribute("aria-label") ?? "";
expect(/對話框|dialog/i.test(ariaBefore)).toBe(true);
// 執行 promote
fireEvent.click(btnBefore);
fireEvent.click(screen.getByTestId("promote-dialog-confirm"));
await waitFor(() => {
expect(screen.queryByTestId("promote-dialog")).toBeNull();
});
// promote 後aria-label 改為 done語意為前往查看、不再是開對話框
const ariaAfter =
screen.getByTestId("success-import-button").getAttribute("aria-label") ??
"";
expect(/前往查看|open the model|detail/i.test(ariaAfter)).toBe(true);
expect(/對話框|dialog/i.test(ariaAfter)).toBe(false);
});
});
/* -------------------------------------------------------------------------- */

View File

@ -292,6 +292,20 @@ function SuccessViewInner({
setPromoteOpen(true);
};
/**
* importedModelId
* - promoteimportedModelId PromoteDialoghandleImportClick
* - promoteimportedModelId
* /models/{id}** dialog promote** promote bug
*/
const handleImportButtonClick = () => {
if (importedModelId) {
router.push(`/models/${importedModelId}`);
return;
}
handleImportClick();
};
const handlePromoteSuccess = (modelId: string) => {
setImportedModelId(modelId);
// 對齊 wireframe §7.1toast「已加入模型庫」+「前往模型庫」連結
@ -419,9 +433,13 @@ function SuccessViewInner({
type="button"
variant="default"
size="lg"
onClick={handleImportClick}
onClick={handleImportButtonClick}
disabled={isExpired}
aria-label={t("conversion.success.import.aria.cta")}
aria-label={
importedModelId
? t("conversion.success.import.aria.done")
: t("conversion.success.import.aria.cta")
}
data-testid="success-import-button"
className="h-auto justify-start gap-3 py-4"
>

View File

@ -579,6 +579,8 @@ export const en: Dictionary = {
"Couldn't add to model library; please try again.",
"conversion.success.import.aria.cta":
"Add to model library (opens a confirmation dialog)",
"conversion.success.import.aria.done":
"Added to model library; open the model detail page",
"conversion.success.download.title": "Download .nef",
"conversion.success.description":
"{source} has been converted into a .nef compatible with {chip}.",

View File

@ -548,6 +548,7 @@ export const zhHant: Dictionary = {
"加到模型庫失敗,請稍後再試",
"conversion.success.import.aria.cta":
"加到模型庫(會開啟確認對話框)",
"conversion.success.import.aria.done": "已加入模型庫,前往查看模型詳細頁",
"conversion.success.download.title": "下載 .nef",
"conversion.success.description":
"{source} 已成功轉成 {chip} 可用的 .nef 檔",