Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ jobs:
__tests__/unit/homepage-demo-accessibility.test.ts
__tests__/unit/linkified-text.test.ts
__tests__/unit/loom-import-ui.test.ts
__tests__/unit/media-format-converter-selection.test.ts
__tests__/unit/media-format-converter.test.ts
__tests__/unit/media-player-store-refs.test.ts
__tests__/unit/playback-store.test.ts
Expand Down
144 changes: 144 additions & 0 deletions apps/web/__tests__/unit/media-format-converter-selection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// @vitest-environment jsdom

import { act, createElement } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { MediaFormatConverter } from "@/components/tools/MediaFormatConverter";

vi.mock("@cap/ui", () => ({
Button: ({ children, ...props }: React.ComponentProps<"button">) =>
createElement("button", { type: "button", ...props }, children),
}));
vi.mock("next/link", () => ({
default: ({ children, ...props }: React.ComponentProps<"a">) =>
createElement("a", props, children),
}));
vi.mock("next/navigation", () => ({
usePathname: () => "/tools/convert/mp4-to-webm",
useRouter: () => ({ push: vi.fn() }),
}));
vi.mock("@/app/utils/analytics", () => ({ trackEvent: vi.fn() }));
vi.mock("@remotion/media-parser", async (importOriginal) => ({
...(await importOriginal<typeof import("@remotion/media-parser")>()),
mediaParserController: () => ({ abort: vi.fn() }),
parseMedia: () => Promise.resolve({ durationInSeconds: 1 }),
}));
vi.mock("@remotion/webcodecs", async (importOriginal) => ({
...(await importOriginal<typeof import("@remotion/webcodecs")>()),
convertMedia: vi.fn(),
}));

describe("media converter file selection", () => {
let container: HTMLDivElement;
let root: Root;

beforeEach(async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("MediaRecorder", { isTypeSupported: () => true });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
await act(async () =>
root.render(
createElement(MediaFormatConverter, {
initialConversionPath: "mp4-to-webm",
}),
),
);
});

afterEach(async () => {
await act(async () => root.unmount());
container.remove();
delete (ArrayBuffer.prototype as { resize?: () => void }).resize;
vi.unstubAllGlobals();
});

const selectFile = async (container: HTMLElement, file: File) => {
const input = container.querySelector("input[type=file]");
if (!(input instanceof HTMLInputElement)) throw new Error("Missing input");
Object.defineProperty(input, "files", {
configurable: true,
value: [file],
});
await act(async () =>
input.dispatchEvent(new Event("change", { bubbles: true })),
);
};

it.each([
["invalid type", new File(["wrong"], "notes.txt", { type: "text/plain" })],
["oversized", new File(["x"], "huge.mp4", { type: "video/mp4" })],
])(
"clears the previous video after an %s replacement",
async (reason, replacement) => {
if (reason === "oversized") {
Object.defineProperty(replacement, "size", {
value: 500 * 1024 * 1024 + 1,
});
}
await selectFile(
container,
new File(["video"], "first.mp4", { type: "video/mp4" }),
);
expect(container.textContent).toContain("first.mp4");
await selectFile(container, replacement);
expect(container.textContent).not.toContain("first.mp4");
expect(container.textContent).not.toContain("Convert to WEBM");
},
);

it("keeps the active file when an invalid replacement is dropped during conversion", async () => {
const { convertMedia } = await import("@remotion/webcodecs");
let finishConversion: (blob: Blob) => void = () => {};
vi.mocked(convertMedia).mockResolvedValueOnce({
save: () =>
new Promise<Blob>((resolve) => {
finishConversion = resolve;
}),
} as Awaited<ReturnType<typeof convertMedia>>);
vi.stubGlobal("VideoDecoder", class {});
vi.stubGlobal("AudioDecoder", class {});
Object.defineProperty(ArrayBuffer.prototype, "resize", {
configurable: true,
value: vi.fn(),
});
vi.stubGlobal(
"URL",
class extends URL {
static createObjectURL = vi.fn(() => "blob:converted");
static revokeObjectURL = vi.fn();
},
);

await selectFile(
container,
new File(["video"], "first.mp4", { type: "video/mp4" }),
);
const convertButton = [...container.querySelectorAll("button")].find(
(button) => button.textContent?.includes("Convert to WEBM"),
);
await act(async () => {
convertButton?.click();
});
expect(container.textContent).toContain("Converting...");

const dropZone = container.querySelector("[class*='border-dashed']");
if (!dropZone) throw new Error("Missing drop zone");
const drop = new Event("drop", { bubbles: true });
Object.defineProperty(drop, "dataTransfer", {
value: {
files: [new File(["wrong"], "notes.txt", { type: "text/plain" })],
},
});
await act(async () => dropZone.dispatchEvent(drop));
await act(async () =>
finishConversion(new Blob(["converted"], { type: "video/webm" })),
);

expect(container.textContent).toContain("first.mp4");
expect(container.textContent).toContain("Conversion complete!");
expect(container.textContent).toContain("Download WEBM");
expect(container.textContent).not.toContain("Please select a MP4 file.");
});
});
8 changes: 8 additions & 0 deletions apps/web/components/tools/MediaFormatConverter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,15 @@ export const MediaFormatConverter = ({
};

const validateAndSetFile = (selectedFile: File) => {
if (isConverting) return;

setError(null);
setFile(null);
Comment thread
yashranaway marked this conversation as resolved.
if (outputUrl) {
URL.revokeObjectURL(outputUrl);
}
setOutputUrl(null);
recordedChunksRef.current = [];

const expectedMimeType = getMimeType(currentSourceFormat);

Expand Down Expand Up @@ -974,6 +981,7 @@ export const MediaFormatConverter = ({
<input
type="file"
accept={getAcceptAttribute(currentSourceFormat)}
disabled={isConverting}
className="hidden"
onChange={handleFileChange}
ref={fileInputRef}
Expand Down
Loading