import { beforeEach, describe, expect, it, vi } from "vitest";
import { mount } from "@vue/test-utils";
import { nextTick } from "vue";
import GnFileUpload from "../../src/vue/components/GnFileUpload.js";
const pngFile = () => new File(["x"], "photo.png", { type: "image/png" });
const txtFile = () => new File(["x"], "notes.txt", { type: "text/plain" });
let urlCounter = 0;
const created = [];
const revoked = [];
beforeEach(() => {
urlCounter = 0;
created.length = 0;
revoked.length = 0;
vi.stubGlobal("URL", Object.assign(URL, {
createObjectURL: vi.fn(file => {
const url = `blob:url-${++urlCounter}`;
created.push({ file, url });
return url;
}),
revokeObjectURL: vi.fn(url => revoked.push(url))
}));
document.body.innerHTML = "";
});
const mountUpload = (modelValue = []) =>
mount(GnFileUpload, {
props: { modelValue },
attachTo: document.body
});
describe("GnFileUpload", () => {
it("shows an image preview for images and a type label for other files", async () => {
const png = pngFile();
const txt = txtFile();
const wrapper = mountUpload([png, txt]);
await nextTick();
const img = document.querySelector(".file-upload-preview img");
expect(img?.src).toBe("blob:url-1");
expect(document.querySelectorAll(".file-upload-preview-item")).toHaveLength(2);
expect(document.querySelector(".file-upload-preview-type")?.textContent).toBe("TXT");
wrapper.unmount();
});
it("revokes the object URL when a file is removed", async () => {
const png = pngFile();
const txt = txtFile();
const wrapper = mountUpload([png, txt]);
await nextTick();
const removePng = [...document.querySelectorAll(".file-upload-preview-remove")]
.find(node => node.getAttribute("aria-label") === "Remove photo.png");
removePng.click();
await nextTick();
expect(revoked).toContain("blob:url-1");
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual([txt]);
wrapper.unmount();
});
it("revokes URLs of files removed via the modelValue watcher", async () => {
const png = pngFile();
const wrapper = mountUpload([png]);
await nextTick();
await wrapper.setProps({ modelValue: [] });
expect(revoked).toContain("blob:url-1");
wrapper.unmount();
});
it("revokes every URL on unmount", async () => {
const png = pngFile();
const txt = txtFile();
const wrapper = mountUpload([png, txt]);
await nextTick();
wrapper.unmount();
// only the image ever got an object URL; the .txt shows a type label
expect(created).toHaveLength(1);
expect(revoked).toEqual(["blob:url-1"]);
});
it("Reset button clears files and revokes all URLs", async () => {
const png = pngFile();
const wrapper = mountUpload([png]);
await nextTick();
const reset = [...document.querySelectorAll("button")].find(node => node.textContent === "Reset");
reset.click();
await nextTick();
expect(revoked).toContain("blob:url-1");
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual([]);
wrapper.unmount();
});
});