Files
jhonny-editor/bm/premiere-pro-mcp-main/tests/uxp/adobe-26-3-uxp-commands.test.ts
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

151 lines
9.1 KiB
TypeScript
Executable File

import { createRequire } from "node:module";
import { describe, expect, it, vi } from "vitest";
const require = createRequire(import.meta.url);
const Commands = require("../../uxp-plugin/commands.cjs");
const Protocol = require("../../uxp-plugin/protocol.cjs");
function adobe263Host() {
const audioTrack = { name: "A1", createSetNameAction: vi.fn((name: string) => ({ apply: () => { audioTrack.name = name; } })) };
const videoTrack = { name: "V1", createSetNameAction: vi.fn((name: string) => ({ apply: () => { videoTrack.name = name; } })) };
const captionTrack = { name: "Captions", createSetNameAction: vi.fn((name: string) => ({ apply: () => { captionTrack.name = name; } })) };
const marker = {
guid: { toString: () => "marker-guid-1" },
getName: vi.fn(() => "Review"), getComments: vi.fn(() => "Check this cut"), getType: vi.fn(() => "Comment"),
getColorIndex: vi.fn(() => 6), getStart: vi.fn(() => ({ seconds: 12.5 })), getDuration: vi.fn(() => ({ seconds: 1.25 })),
getColor: vi.fn(() => ({ red: 0.2, green: 0.4, blue: 0.8, alpha: 1 })),
getUrl: vi.fn(() => "https://review.example.test/cut?token=opaque"), getTarget: vi.fn(() => "frame-41"),
};
const source = {
id: "source-1", name: "Interview", isClip: true,
getId: vi.fn(() => "source-1"),
createSubClipAction: vi.fn((name: string) => ({ apply: () => {
rootItems.push({ id: "subclip-1", name, isClip: true, getId: () => "subclip-1" });
} })),
};
const rootItems: Array<Record<string, unknown>> = [source];
const root = { isFolder: true, getItems: vi.fn(async () => rootItems) };
const sequence = {
guid: "sequence-1",
getAudioTrackCount: vi.fn(async () => 1), getAudioTrack: vi.fn(async () => audioTrack),
getVideoTrackCount: vi.fn(async () => 1), getVideoTrack: vi.fn(async () => videoTrack),
getCaptionTrackCount: vi.fn(async () => 1), getCaptionTrack: vi.fn(async () => captionTrack),
};
const addAction = vi.fn((action: { apply?: () => void }) => { action.apply?.(); return true; });
const project = {
getActiveSequence: vi.fn(async () => sequence), getRootItem: vi.fn(async () => root),
lockedAccess: vi.fn((callback: () => void) => callback()),
executeTransaction: vi.fn((callback: (compound: { addAction: typeof addAction }) => void) => { callback({ addAction }); return true; }),
};
let sourcePosition = 0;
const aafOptionValues: Record<string, unknown> = {};
const aafOptions = {
setMixdownVideo: vi.fn((value: boolean) => { aafOptionValues.mixdownVideo = value; }),
setSampleRate: vi.fn((value: number) => { aafOptionValues.sampleRate = value; }),
setAudioFileFormat: vi.fn((value: number) => { aafOptionValues.audioFileFormat = value; }),
setVideoMixdownPresetPath: vi.fn((value: string) => { aafOptionValues.videoMixdownPresetPath = value; }),
};
const ppro = {
Project: { getActiveProject: vi.fn(async () => project) },
TickTime: { createWithSeconds: vi.fn((seconds: number) => ({ seconds })) },
ClipProjectItem: { cast: vi.fn((item: { isClip?: boolean }) => { if (!item.isClip) throw new Error("not a clip"); return item; }) },
ProjectItem: { cast: vi.fn((item: unknown) => item) },
FolderItem: { cast: vi.fn((item: { isFolder?: boolean }) => { if (!item.isFolder) throw new Error("not a folder"); return item; }) },
Markers: { getMarkers: vi.fn(async () => ({ getMarkers: vi.fn(async () => [marker]) })) },
SourceMonitor: {
setPosition: vi.fn(async (position: { seconds: number }) => { sourcePosition = position.seconds; return true; }),
getPosition: vi.fn(async () => ({ seconds: sourcePosition })),
},
AAFExportOptions: vi.fn(() => aafOptions),
Constants: { AAFExportAudioFormat: { AIFF: 0, WAV: 1 } },
ProjectConverter: { exportAAF: vi.fn(async () => true) },
};
const workspace = {
status: vi.fn(() => ({ configured: true })),
assertPathAllowed: vi.fn((path: string) => path),
};
return {
registry: Commands.createCommandRegistry({ ppro, Protocol, workspace }), ppro, project, source, audioTrack, marker, aafOptionValues, workspace,
};
}
describe("Adobe Premiere Pro 26.3 UXP commands", () => {
it("uses locked undoable action transactions for track renames and subclip creation, then reads both back", async () => {
const value = adobe263Host();
await expect(value.registry.dispatch("track.rename", {
trackType: "audio", trackIndex: 0, name: "Dialogue", operationId: "track-1",
})).resolves.toMatchObject({ renamed: true, name: "Dialogue", outcome: "verified", operationId: "track-1" });
await expect(value.registry.dispatch("subclip.create", {
projectItemId: "source-1", name: "Interview Select", startSeconds: 3, endSeconds: 9,
})).resolves.toMatchObject({
created: true, outcome: "verified", subclip: { projectItemId: "subclip-1", name: "Interview Select" },
});
expect(value.project.lockedAccess).toHaveBeenCalledTimes(2);
expect(value.audioTrack.createSetNameAction).toHaveBeenCalledWith("Dialogue");
expect(value.source.createSubClipAction).toHaveBeenCalledWith(
"Interview Select", { seconds: 3 }, { seconds: 9 }, false, { takeVideo: true, takeAudio: true },
);
});
it("returns stable marker GUIDs, redacts optional values by default, and opts in to documented link and raw color readback", async () => {
const value = adobe263Host();
await expect(value.registry.dispatch("marker.list", {})).resolves.toMatchObject({
scope: "sequence", count: 1, markers: [{ guid: "marker-guid-1", startSeconds: 12.5, durationSeconds: 1.25 }],
});
expect(value.marker.getUrl).not.toHaveBeenCalled();
expect(value.marker.getTarget).not.toHaveBeenCalled();
expect(value.marker.getColor).not.toHaveBeenCalled();
await expect(value.registry.dispatch("marker.list", { includeWebLinks: true })).resolves.toMatchObject({
scope: "sequence", markers: [{ url: "https://review.example.test/cut?token=opaque", target: "frame-41" }],
});
expect(value.marker.getUrl).toHaveBeenCalledTimes(1);
expect(value.marker.getTarget).toHaveBeenCalledTimes(1);
await expect(value.registry.dispatch("marker.list", { includeColorValues: true })).resolves.toMatchObject({
scope: "sequence", markers: [{ color: { red: 0.2, green: 0.4, blue: 0.8, alpha: 1 } }],
});
expect(value.marker.getColor).toHaveBeenCalledTimes(1);
const partialHost = adobe263Host();
delete (partialHost.marker as { getUrl?: unknown }).getUrl;
delete (partialHost.marker as { getTarget?: unknown }).getTarget;
await expect(partialHost.registry.dispatch("marker.list", { includeWebLinks: true })).resolves.toMatchObject({
markers: [{ url: null, target: null }],
});
delete (partialHost.marker as { getColor?: unknown }).getColor;
await expect(partialHost.registry.dispatch("marker.list", { includeColorValues: true })).resolves.toMatchObject({
markers: [{ color: null }],
});
const malformedHost = adobe263Host();
malformedHost.marker.getColor.mockReturnValueOnce({ red: Number.NaN, green: 0.4, blue: 0.8, alpha: 1 });
await expect(malformedHost.registry.dispatch("marker.list", { includeColorValues: true }))
.rejects.toMatchObject({ code: "UXP_VERIFICATION_FAILED" });
});
it("verifies Source Monitor positioning and applies bounded AAF options", async () => {
const value = adobe263Host();
await expect(value.registry.dispatch("sourceMonitor.position.set", { seconds: 4.5 })).resolves.toMatchObject({
positioned: true, seconds: 4.5, outcome: "verified",
});
await expect(value.registry.dispatch("interchange.aaf.export", {
outputFilePath: "/exports/edit.aaf",
options: { mixdownVideo: true, sampleRate: 48000, audioFileFormat: "wav", videoMixdownPresetPath: "/presets/prores.epr" },
})).resolves.toMatchObject({ exported: true, format: "aaf", outcome: "committed_unverified", outputVerified: false });
expect(value.aafOptionValues).toEqual({ mixdownVideo: true, sampleRate: 48000, audioFileFormat: 1, videoMixdownPresetPath: "/presets/prores.epr" });
expect(value.workspace.assertPathAllowed).toHaveBeenNthCalledWith(1, "/exports/edit.aaf", { label: "outputFilePath", kind: "file" });
expect(value.workspace.assertPathAllowed).toHaveBeenNthCalledWith(2, "/presets/prores.epr", { label: "videoMixdownPresetPath", kind: "file" });
});
it("rejects unsafe subclip ranges and unbounded AAF options before invoking the host", async () => {
const value = adobe263Host();
await expect(value.registry.dispatch("subclip.create", {
projectItemId: "source-1", name: "Invalid", startSeconds: 9, endSeconds: 3,
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(value.registry.dispatch("interchange.aaf.export", {
outputFilePath: "/exports/edit.aaf", options: { sampleRate: 12345 },
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(value.registry.dispatch("marker.list", { includeWebLinks: "true" })).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(value.registry.dispatch("marker.list", { includeColorValues: "true" })).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
});
});