vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
512 lines (393 loc) • 14.6 kB
text/typescript
import { mount } from "@vue/test-utils";
import { describe, it, expect, vi } from "vitest";
import { nextTick } from "vue";
import UInputFile from "../UInputFile.vue";
import ULabel from "../../ui.form-label/ULabel.vue";
import UFiles from "../../ui.text-files/UFiles.vue";
import type { Props } from "../types.ts";
global.URL.createObjectURL = vi.fn(() => "mock-url");
describe("UInputFile.vue", () => {
const createFile = (name: string, size: number, type: string) => {
const blobContent = new Uint8Array(size); //
return new File([blobContent], name, { type });
};
describe("Props", () => {
it("Model Value – sets initial value correctly for single file", () => {
const file = createFile("test.txt", 1024, "text/plain");
const component = mount(UInputFile, {
props: {
modelValue: file,
},
});
const filesComponent = component.findComponent(UFiles);
expect(filesComponent.exists()).toBe(true);
expect(filesComponent.props("fileList")).toEqual([file]);
});
it("Model Value – sets initial value correctly for multiple files", () => {
const files = [
createFile("file1.txt", 1024, "text/plain"),
createFile("file2.pdf", 2048, "application/pdf"),
];
const component = mount(UInputFile, {
props: {
modelValue: files,
multiple: true,
},
});
const filesComponent = component.findComponent(UFiles);
expect(filesComponent.exists()).toBe(true);
expect(filesComponent.props("fileList")).toEqual(files);
});
it("Label – passes label to ULabel component", () => {
const labelText = "Upload Files";
const component = mount(UInputFile, {
props: {
label: labelText,
},
});
const labelComponent = component.getComponent(ULabel);
expect(labelComponent.props("label")).toBe(labelText);
});
it("Label – renders file input element with correct id", () => {
const customId = "custom-file-input";
const component = mount(UInputFile, {
props: {
id: customId,
label: "Upload Files",
},
});
const input = component.find("input[type='file']");
const labelComponent = component.getComponent(ULabel);
expect(input.attributes("id")).toBe(customId);
expect(labelComponent.props("for")).toBe(customId);
});
it("LabelAlign – passes labelAlign prop to ULabel component", () => {
const labelAlign = "topInside";
const component = mount(UInputFile, {
props: {
label: "Upload Files",
labelAlign,
},
});
const labelComponent = component.getComponent(ULabel);
expect(labelComponent.props("align")).toBe(labelAlign);
});
it("Description – passes description to ULabel component", () => {
const descriptionText = "Select files to upload";
const component = mount(UInputFile, {
props: {
description: descriptionText,
},
});
const labelComponent = component.getComponent(ULabel);
expect(labelComponent.props("description")).toBe(descriptionText);
});
it("Error – passes error message to ULabel component", () => {
const errorText = "File upload failed";
const component = mount(UInputFile, {
props: {
error: errorText,
},
});
const labelComponent = component.getComponent(ULabel);
expect(labelComponent.props("error")).toBe(errorText);
});
it("Error – applies error styles component", () => {
const dropzoneErrorClasses = "border-error";
const contentErrorClasses = "bg-error/5";
const component = mount(UInputFile, {
props: {
error: "File upload failed",
},
});
const dropzone = component.find('[vl-key="dropzone"]');
const content = component.find('[vl-key="content"]');
expect(dropzone.attributes("class")).toContain(dropzoneErrorClasses);
expect(content.attributes("class")).toContain(contentErrorClasses);
});
it("Size – passes size prop to ULabel and UFiles components", () => {
const size = "lg";
const file = createFile("test.txt", 1024, "text/plain");
const component = mount(UInputFile, {
props: {
size,
modelValue: file,
},
});
const labelComponent = component.getComponent(ULabel);
const filesComponent = component.getComponent(UFiles);
expect(labelComponent.props("size")).toBe(size);
expect(filesComponent.props("size")).toBe(size);
});
it("Size – applies correct classes based on size prop", () => {
const placeholderSizeClasses = {
sm: "text-small",
md: "text-medium",
lg: "text-large",
};
Object.entries(placeholderSizeClasses).forEach(([size, className]) => {
const component = mount(UInputFile, {
props: {
size: size as Props["size"],
},
});
const placeholder = component.find('[vl-key="placeholder"]');
expect(placeholder.classes()).toContain(className);
});
});
it("Disabled – sets disabled attribute on input and buttons", () => {
const component = mount(UInputFile, {
props: {
disabled: true,
},
});
const input = component.get("input[type='file']");
const buttons = component.findAllComponents({ name: "UButton" });
expect(input.attributes("disabled")).toBeDefined();
buttons.forEach((button) => {
expect(button.props("disabled")).toBe(true);
});
});
it("Disabled – passes disabled prop to ULabel component", () => {
const component = mount(UInputFile, {
props: {
disabled: true,
label: "Upload Files",
},
});
const labelComponent = component.getComponent(ULabel);
expect(labelComponent.props("disabled")).toBe(true);
});
it("Disabled – applies disabled styles to dropzone and content", () => {
const dropzoneDisabledClass = "bg-lifted";
const contentDisabledClass = "bg-accented";
const component = mount(UInputFile, {
props: {
disabled: true,
},
});
const dropzone = component.find('[vl-key="dropzone"]');
const content = component.find('[vl-key="content"]');
expect(dropzone.classes()).toContain(dropzoneDisabledClass);
expect(content.classes()).toContain(contentDisabledClass);
});
it("Data Test – sets data-test attributes", () => {
const testCases = [
{
key: "clear",
modelValue: createFile("test.txt", 1024, "text/plain"),
},
{
key: "upload",
modelValue: undefined,
},
];
const dataTestValue = "file-input-test";
testCases.forEach(({ key, modelValue }) => {
const component = mount(UInputFile, {
props: {
modelValue: modelValue,
dataTest: dataTestValue,
},
});
component.get(`[data-test="${dataTestValue}-${key}"]`);
});
});
});
describe("Slots", () => {
it("Label – renders custom content from label slot", () => {
const customLabelContent = "Custom File Label";
const component = mount(UInputFile, {
props: {
label: "Default Label",
},
slots: {
label: customLabelContent,
},
});
const labelComponent = component.getComponent(ULabel);
const labelElement = labelComponent.find("label");
expect(labelElement.text()).toBe(customLabelContent);
});
it("Label – exposes label prop to slot", () => {
const defaultLabel = "Upload Files";
const component = mount(UInputFile, {
props: {
label: defaultLabel,
},
slots: {
label: "Custom {{ params.label }}",
},
});
const labelComponent = component.getComponent(ULabel);
const labelElement = labelComponent.find("label");
expect(labelElement.text()).toBe(`Custom ${defaultLabel}`);
});
it("Top – renders custom content from top slot", () => {
const testClass = "custom-top";
const component = mount(UInputFile, {
slots: {
top: `<div class="${testClass}">Upload Instructions</div>`,
},
});
const topSlotElement = component.find(`.${testClass}`);
expect(topSlotElement.exists()).toBe(true);
expect(topSlotElement.text()).toBe("Upload Instructions");
});
it("Left – renders custom content from left slot", () => {
const testClass = "custom-left";
const component = mount(UInputFile, {
slots: {
left: `<span class="${testClass}">📁</span>`,
},
});
const leftSlotElement = component.find(`.${testClass}`);
expect(leftSlotElement.exists()).toBe(true);
expect(leftSlotElement.text()).toBe("📁");
});
it("Left – positions left slot content before placeholder when no files", () => {
const leftContent = "File Icon";
const testClass = "left-content";
const component = mount(UInputFile, {
slots: {
left: `<span class="${testClass}">${leftContent}</span>`,
},
});
const contentDiv = component.find('[vl-key="content"]');
const leftSlot = contentDiv.find(`.${testClass}`);
const placeholder = contentDiv.find('[vl-key="placeholder"]');
expect(leftSlot.exists()).toBe(true);
expect(placeholder.exists()).toBe(true);
// Check that left slot comes before placeholder in DOM order
const leftIndex = Array.from(contentDiv.element.children).indexOf(leftSlot.element);
const placeholderIndex = Array.from(contentDiv.element.children).indexOf(placeholder.element);
expect(leftIndex).toBeLessThan(placeholderIndex);
});
it("Bottom – renders custom content from bottom slot", () => {
const testClass = "custom-bottom";
const component = mount(UInputFile, {
slots: {
bottom: `<div class="${testClass}">Max file size: 5MB</div>`,
},
});
const bottomSlotElement = component.find(`.${testClass}`);
expect(bottomSlotElement.exists()).toBe(true);
expect(bottomSlotElement.text()).toBe("Max file size: 5MB");
});
it("File – renders custom file content when files are present", () => {
const file = createFile("test.txt", 1024, "text/plain");
const testClass = "custom-file";
const component = mount(UInputFile, {
props: {
modelValue: file,
},
slots: {
default: `<div class="${testClass}">Custom file: {{ params.label }}</div>`,
},
});
const customFileElement = component.find(`.${testClass}`);
expect(customFileElement.exists()).toBe(true);
expect(customFileElement.text()).toContain("Custom file: test.txt");
});
it("File – exposes file properties to slot", () => {
const file = createFile("document.pdf", 2048, "application/pdf");
const testClass = "file-info";
const component = mount(UInputFile, {
props: {
modelValue: file,
},
slots: {
default: `
<div class="${testClass}">
<span>{{ params.id }}</span>
<span>{{ params.label }}</span>
<span>{{ params.index }}</span>
</div>
`,
},
});
const fileInfo = component.find(".file-info");
const [fileId, fileLabel, fileIndex] = fileInfo.findAll("span");
expect(fileInfo.exists()).toBe(true);
expect(fileId.exists()).toBe(true);
expect(fileLabel.text()).toBe("document.pdf");
expect(fileIndex.text()).toBe("0");
});
it("File – renders multiple custom file items for multiple files", () => {
const testClass = "custom-file-item";
const files = [
createFile("file1.txt", 1024, "text/plain"),
createFile("file2.pdf", 2048, "application/pdf"),
];
const component = mount(UInputFile, {
props: {
modelValue: files,
multiple: true,
},
slots: {
default: `<div class="${testClass}">{{ params.label }}</div>`,
},
});
const customFileItems = component.findAll(".custom-file-item");
expect(customFileItems).toHaveLength(2);
expect(customFileItems[0].text()).toBe("file1.txt");
expect(customFileItems[1].text()).toBe("file2.pdf");
});
});
describe("Functionality", () => {
it("Validation – validates file size", async () => {
const maxFileSize = 1;
const component = mount(UInputFile, {
props: {
maxFileSize,
},
});
const file = createFile("test.txt", 1024 * 1024 * 3, "text/plain"); // 3 MB file
const input = component.find("input[type='file']");
Object.defineProperty(input.element, "files", {
value: [file],
writable: false,
});
await input.trigger("change");
expect(component.emitted("error")).toBeTruthy();
});
it("Validation – validates file type", async () => {
const allowedFileTypes = ["image/jpeg"];
const component = mount(UInputFile, {
props: {
allowedFileTypes,
},
});
const file = createFile("test.txt", 1024, "text/plain");
const input = component.find("input[type='file']");
Object.defineProperty(input.element, "files", {
value: [file],
writable: false,
});
await input.trigger("change");
expect(component.vm.error).toBeTruthy();
});
it("Clear Button – clears files when clear button is clicked", async () => {
const file = createFile("test.txt", 1024, "text/plain");
const component = mount(UInputFile, {
props: {
modelValue: file,
},
});
await component.get("[vl-key='clearButton']").trigger("click");
await nextTick();
expect(component.emitted("update:modelValue")![0][0]).toBeNull();
});
});
describe("Exposed Refs", () => {
it("Error – exposes error ref", async () => {
const errorText = "Test error";
const component = mount(UInputFile, {
props: {
error: errorText,
},
});
expect(component.vm.error).toBe(errorText);
});
});
});