fix(canvas): 画布媒体 - 修复预览首帧音量和创作状态展示
This commit is contained in:
1 parent
7737047a8d
commit
12796b048c
30 files changed
+689
-182
No files matched your search
@@ -5,7 +5,7 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { canvasNodeRenderPadding, resolveActiveCanvasMediaNodeId } from "../src/lib/canvas/canvas-performance-mode";
|
||||
import { videoMetadata } from "../src/lib/canvas/canvas-generation-task-sync";
|
||||
import { canvasNodeVideoPreviewUrl, canvasVideoAssetPreviewUrl } from "../src/lib/canvas/canvas-media-preview";
|
||||
import { canvasNodeVideoPreviewReference, canvasNodeVideoPreviewUrl, canvasVideoAssetPreviewUrl } from "../src/lib/canvas/canvas-media-preview";
|
||||
import { collectImageStorageKeys } from "../src/services/image-storage";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "../src/types/canvas";
|
||||
import { detectVideoAudioTrack, detectVideoAudioTrackFromBlob, detectVideoAudioTrackFromUrl } from "../src/lib/video-poster";
|
||||
@@ -15,6 +15,11 @@ const canvasAudioPlayerSource = readFileSync(resolve(import.meta.dir, "../src/co
|
||||
const canvasMentionSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-resource-mention-textarea.tsx"), "utf8");
|
||||
const canvasNodeSource = readFileSync(resolve(import.meta.dir, "../src/components/canvas/canvas-node.tsx"), "utf8");
|
||||
const canvasVideoPreviewSource = readFileSync(resolve(import.meta.dir, "../src/services/canvas-video-preview.ts"), "utf8");
|
||||
const browserDownloadSource = readFileSync(resolve(import.meta.dir, "../src/services/browser-download.ts"), "utf8");
|
||||
const canvasNodeEditorSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-node-editor.ts"), "utf8");
|
||||
const assetLibrarySource = readFileSync(resolve(import.meta.dir, "../src/pages/assets/index.tsx"), "utf8");
|
||||
const projectAssetsSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/assets.tsx"), "utf8");
|
||||
const workflowProductionSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/workflow-production-workbench.tsx"), "utf8");
|
||||
const videoPlayerSource = readFileSync(resolve(import.meta.dir, "../src/components/video-player.tsx"), "utf8");
|
||||
const canvasProjectSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
|
||||
const globalStylesSource = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
@@ -71,9 +76,15 @@ describe("large canvas media rendering", () => {
|
||||
|
||||
test("keeps inactive video nodes on a viewport-gated static first frame", () => {
|
||||
const inactivePreviewSource = canvasNodeContentSource.match(/function InactiveVideoPreview[\s\S]*?\n}\n\nfunction VideoPreviewPlayButton/)?.[0] || "";
|
||||
expect(canvasNodeContentSource).toContain("if (previewUrl || !nearViewport || !node.metadata?.content || !updateMetadataRef.current)");
|
||||
expect(canvasNodeContentSource).toContain("if (hasPersistedPreview || !nearViewport || (!node.metadata?.content && !node.metadata?.storageKey) || !updateMetadataRef.current)");
|
||||
expect(canvasNodeContentSource).not.toContain("hydrateMediaPreview");
|
||||
expect(inactivePreviewSource).not.toContain("<video");
|
||||
expect(inactivePreviewSource).toContain("hasPersistedPreview || localPreviewUrl || !nearViewport");
|
||||
expect(inactivePreviewSource).toContain("<video");
|
||||
expect(inactivePreviewSource).toContain("muted");
|
||||
expect(inactivePreviewSource).toContain('preload="auto"');
|
||||
expect(inactivePreviewSource).toContain("video.currentTime = Math.min(0.001, video.duration / 2)");
|
||||
expect(inactivePreviewSource).toContain("onCanPlay={() => setPassiveVideoReady(true)}");
|
||||
expect(inactivePreviewSource).not.toContain("autoPlay");
|
||||
expect(inactivePreviewSource).toContain("<VideoPreviewPlayButton");
|
||||
expect(canvasNodeContentSource).toContain("useVideoPlaybackUrl(node, mediaActive)");
|
||||
expect(canvasNodeContentSource).toContain("onMediaPlayRequest?.(node.id)");
|
||||
@@ -81,9 +92,23 @@ describe("large canvas media rendering", () => {
|
||||
expect(canvasNodeContentSource).toContain("scheduleResourceBlobCache(node.metadata?.storageKey || \"\")");
|
||||
});
|
||||
|
||||
test("allows failed or empty first-frame requests to retry", () => {
|
||||
expect(canvasVideoPreviewSource).toContain("if (!preview) previewRequests.delete(requestKey)");
|
||||
expect(canvasVideoPreviewSource).toContain("previewRequests.delete(requestKey);");
|
||||
test("downloads OSS media by browser navigation without fetching it into a Blob", () => {
|
||||
expect(browserDownloadSource).toContain('getResourceAccess(normalizedStorageKey, "download"');
|
||||
expect(browserDownloadSource).toContain('document.createElement("a")');
|
||||
expect(browserDownloadSource).not.toContain("fetch(");
|
||||
expect(browserDownloadSource).not.toContain("response.blob()");
|
||||
expect(browserDownloadSource).not.toContain("new Blob(");
|
||||
expect(canvasNodeEditorSource).toContain("downloadBrowserMedia({ storageKey, url: content, fileName: downloadName })");
|
||||
expect(assetLibrarySource).toContain("downloadBrowserMedia({ storageKey: asset.data.storageKey");
|
||||
expect(projectAssetsSource).toContain("downloadBrowserMedia({ storageKey: personal.data.storageKey");
|
||||
expect(workflowProductionSource).toContain("storageKey: resourceStorageKey(artifact.resourceId)");
|
||||
expect(workflowProductionSource).not.toContain("await response.blob()");
|
||||
});
|
||||
|
||||
test("shows a captured first frame before its OSS poster upload finishes and allows later retries", () => {
|
||||
expect(canvasVideoPreviewSource).toContain("const localUrl = URL.createObjectURL(captured.poster)");
|
||||
expect(canvasVideoPreviewSource).toContain("return { localUrl, persisted }");
|
||||
expect(canvasVideoPreviewSource).not.toContain("previewRequests");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,7 +267,7 @@ describe("video canvas controls", () => {
|
||||
|
||||
test("allows a runtime-positive probe to correct stale silent metadata", () => {
|
||||
expect(videoPlayerSource).toContain("hasAudio === true || detectedHasAudio === true ? true");
|
||||
expect(videoPlayerSource).toContain("hasAudio === false ? false : undefined");
|
||||
expect(videoPlayerSource).toContain("detectedHasAudio === false ? false");
|
||||
expect(videoPlayerSource).toContain("detectedHasAudio === false ? false");
|
||||
expect(videoPlayerSource).toContain("detectVideoAudioTrackFromUrl(src)");
|
||||
});
|
||||
@@ -309,6 +334,26 @@ describe("passive video previews", () => {
|
||||
expect(metadata.hasAudio).toBe(false);
|
||||
expect(collectImageStorageKeys(metadata)).toContain("image:user:1");
|
||||
expect(canvasNodeVideoPreviewUrl(video)).toBe("blob:poster");
|
||||
expect(canvasNodeVideoPreviewReference(video)).toEqual({ src: "blob:poster", storageKey: "image:user:1" });
|
||||
});
|
||||
|
||||
test("keeps the poster storage key so passive previews resolve a fresh OSS URL", () => {
|
||||
const video = node("video", CanvasNodeType.Video);
|
||||
video.metadata = {
|
||||
content: "/api/resources/video-1/file",
|
||||
storageKey: "resource:video-1",
|
||||
videoPreview: {
|
||||
content: "/api/resources/poster-1/file",
|
||||
storageKey: "resource:poster-1",
|
||||
width: 400,
|
||||
height: 225,
|
||||
},
|
||||
};
|
||||
|
||||
expect(canvasNodeVideoPreviewReference(video)).toEqual({
|
||||
src: "/api/resources/poster-1/file",
|
||||
storageKey: "resource:poster-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses explicit posters without ever returning the original video URL", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { generationTaskShowsProgress, generationTaskStatusLabel, mediaDeliverySummary } from "@/lib/generation-task-display";
|
||||
import { canCancelGenerationTask, generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel, mediaDeliverySummary } from "@/lib/generation-task-display";
|
||||
import { resetGenerationTaskMetadata } from "@/lib/canvas/canvas-task-state";
|
||||
|
||||
describe("作品保存阶段", () => {
|
||||
@@ -22,3 +22,35 @@ describe("作品保存阶段", () => {
|
||||
expect(metadata.taskCanRecoverMedia).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("生成任务取消边界", () => {
|
||||
test("排队和本地准备阶段仍允许取消", () => {
|
||||
expect(canCancelGenerationTask({ status: "queued", stage: "等待队列调度" })).toBe(true);
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "后端接管任务" })).toBe(true);
|
||||
});
|
||||
|
||||
test("进入上游提交阶段后立即隐藏取消入口", () => {
|
||||
for (const stage of ["调用生成模型", "正在连接上游", "上游生成中", "后台仍在生成", "等待上游任务同步", "generating", "submitting", "submitted", "submission_unknown"]) {
|
||||
expect(canCancelGenerationTask({ status: "running", stage })).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("已有上游请求或取消协调状态时不可取消", () => {
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "后端接管任务", providerRequestId: "provider-1" })).toBe(false);
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "后端接管任务", providerCancelStatus: "requested" })).toBe(false);
|
||||
});
|
||||
|
||||
test("作品已生成进入保存阶段或上游返回未知阶段时也不暴露取消入口", () => {
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "作品已生成,正在保存", mediaStage: "upload" })).toBe(false);
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "provider_processing" })).toBe(false);
|
||||
expect(canCancelGenerationTask({ status: "running", stage: "" })).toBe(false);
|
||||
expect(canCancelGenerationTask({ status: "queued", stage: "" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("生成任务用户可见阶段", () => {
|
||||
test("不向用户暴露参考素材准备和上游连接等技术阶段", () => {
|
||||
expect(generationTaskStageLabel({ status: "running", stage: "正在准备参考素材" })).toBe("作品创作中");
|
||||
expect(generationTaskStageLabel({ status: "running", stage: "正在连接上游" })).toBe("作品创作中");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user