Newer
Older
gnexus-ui-kit / tests / unit / gn-file-upload.spec.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 2 KB Add Vue adapter behavior contract tests
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();
	});
});