vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
647 lines (512 loc) • 18 kB
text/typescript
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import { createRouter, createWebHistory } from "vue-router";
import UModal from "../UModal.vue";
import UHeader from "../../ui.text-header/UHeader.vue";
import UButton from "../../ui.button/UButton.vue";
import ULink from "../../ui.button-link/ULink.vue";
import UIcon from "../../ui.image-icon/UIcon.vue";
import type { Props } from "../types.ts";
import type { UnknownObject } from "../../types.ts";
// Create a mock router for testing router-link functionality
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", name: "home", component: { template: "<div>Home</div>" } },
{ path: "/back", name: "back", component: { template: "<div>Back</div>" } },
],
});
// Helper function to mount component with router
const mountWithRouter = (component: unknown, options: UnknownObject) => {
return mount(component, {
...options,
global: {
plugins: [router],
...(options.global || {}),
},
});
};
describe("UModal", () => {
const modelValue = true;
// Wait for an async component to load
function sleep(ms: number = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Props tests
describe("Props", () => {
// ModelValue prop
it("renders when modelValue is true", () => {
const component = mount(UModal, {
props: {
modelValue,
},
});
expect(component.isVisible()).toBe(modelValue);
});
it("does not render when modelValue is false", () => {
const modelValue = false;
const component = mount(UModal, {
props: {
modelValue,
},
});
expect(component.find("[vl-key='overlay']").exists()).toBe(modelValue);
});
// Title prop
it("renders with title prop", () => {
const title = "Modal Title";
const component = mount(UModal, {
props: {
modelValue,
title,
},
});
const header = component.findComponent(UHeader);
expect(header.exists()).toBe(true);
expect(header.props("label")).toBe(title);
});
// Description prop
it("renders with description prop", () => {
const title = "Modal Title";
const description = "Modal Description";
const component = mount(UModal, {
props: {
modelValue,
title,
description,
},
});
expect(component.text()).toContain(description);
});
// Variant prop
it("applies correct variant classes", () => {
const variants = {
solid: "bg-default border-transparent",
outlined: "bg-default border-muted",
subtle: "bg-muted border-default/50",
soft: "bg-muted border-transparent",
};
Object.entries(variants).forEach(([variant, expectedClasses]) => {
const component = mount(UModal, {
props: {
modelValue,
variant: variant as Props["variant"],
},
});
expect(component.find("[vl-key='modal']").attributes("class")).toContain(expectedClasses);
});
});
// Size prop
it("applies correct size classes", () => {
const sizes = {
xs: "md:w-[25rem]",
sm: "md:w-[31.25rem]",
md: "md:w-[37.5rem]",
lg: "md:w-[43.75rem]",
xl: "md:w-[50rem]",
"2xl": "md:w-[56.25rem]",
"3xl": "md:w-[62.5rem]",
"4xl": "md:w-[68.75rem]",
"5xl": "md:w-[75rem]",
};
Object.entries(sizes).forEach(([size, expectedClass]) => {
const component = mount(UModal, {
props: {
modelValue,
size: size as Props["size"],
},
});
expect(component.find("[vl-key='modal']").attributes("class")).toContain(expectedClass);
});
});
// BackTo and BackLabel props
it("renders back link when backTo and backLabel are provided", () => {
const title = "Modal Title";
const backTo = "/back";
const backLabel = "Back to previous page";
const component = mountWithRouter(UModal, {
props: {
modelValue,
title,
backTo,
backLabel,
},
});
const backLink = component.findComponent(ULink);
const backIcon = component.findComponent(UIcon);
expect(backLink.exists()).toBe(true);
expect(backIcon.exists()).toBe(true);
expect(backLink.props("to")).toBe(backTo);
expect(backLink.props("label")).toBe(backLabel);
});
// CloseOnCross prop
it("renders close button when closeOnCross is true", () => {
const title = "Modal Title";
const closeOnCross = [true, false];
closeOnCross.forEach((value) => {
const component = mount(UModal, {
props: {
modelValue,
title,
closeOnCross: value,
},
});
const closeButton = component.findComponent(UButton);
expect(closeButton.exists()).toBe(value);
});
});
// CloseOnOverlay prop
it("renders with closeOnOverlay prop", () => {
const closeOnOverlay = [true, false];
closeOnOverlay.forEach(async (value) => {
const component = mount(UModal, {
props: {
modelValue,
closeOnOverlay: value,
},
});
const overlay = component.find('[vl-key="overlay"]');
expect(overlay.exists()).toBe(true);
await overlay.trigger("click");
await sleep(500);
const modal = component.find('[vl-key="modal"]');
expect(modal.exists()).toBe(!value);
});
});
// CloseOnEsc prop
it("renders with closeOnEsc prop", () => {
const closeOnEsc = [true, false];
closeOnEsc.forEach(async (value) => {
const component = mount(UModal, {
props: {
modelValue,
closeOnEsc: value,
},
});
const wrapper = component.find("[vl-key='wrapper']");
await wrapper.trigger("keydown", { key: "Escape" });
await sleep(500);
const modal = component.find('[vl-key="modal"]');
expect(modal.exists()).toBe(!value);
});
});
// Inner prop
it("applies inner class when inner prop is true", () => {
const inner = true;
const expectedClass = "mt-4";
const component = mount(UModal, {
props: {
modelValue,
inner,
},
});
const modal = component.find("[vl-key='modal']");
expect(modal.attributes("class")).toContain(expectedClass);
});
// Divided prop
it("applies divided class when divided prop is true", () => {
const divided = true;
const footerLeftContent = "Footer Left";
const footerRightContent = "Footer Right";
const expectedClass = "border-t";
const component = mount(UModal, {
props: {
modelValue,
divided,
},
slots: {
"footer-left": footerLeftContent,
"footer-right": footerRightContent,
},
});
const footer = component.find("[vl-key='footer']");
expect(footer.attributes("class")).toContain(expectedClass);
});
// DataTest prop
it("applies the correct data-test attribute", () => {
const dataTest = "modal-test";
const component = mount(UModal, {
props: {
modelValue,
dataTest,
},
});
const modalWrapper = component.find("[vl-key='wrapper']");
expect(modalWrapper.attributes("data-test")).toBe(dataTest);
});
});
// Slots tests
describe("Slots", () => {
// Default slot
it("renders content in default slot", () => {
const slotClass = "default-content";
const slotContent = "Default Content";
const component = mount(UModal, {
props: { modelValue: true },
slots: {
default: `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
});
// Before-title slot
it("renders content in before-title slot and shows header", () => {
const slotClass = "before-title";
const slotContent = "Before Title";
const component = mount(UModal, {
props: {
modelValue: true,
title: "Modal Title",
},
slots: {
"before-title": `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
// Check that header is shown when before-title slot is provided
const header = component.find("[vl-key='header']");
expect(header.exists()).toBe(true);
expect(header.text()).toContain(slotContent);
});
// Title slot
it("renders custom content in title slot and shows header", () => {
const slotClass = "custom-title";
const slotContent = "Custom Title";
const component = mount(UModal, {
props: {
modelValue: true,
title: "Modal Title",
},
slots: {
title: `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
expect(component.findComponent(UHeader).exists()).toBe(false);
// Check that header is shown when title slot is provided
const header = component.find("[vl-key='header']");
expect(header.exists()).toBe(true);
expect(header.text()).toContain(slotContent);
});
// After-title slot
it("renders content in after-title slot and shows header", () => {
const slotClass = "after-title";
const slotContent = "After Title";
const component = mount(UModal, {
props: {
modelValue: true,
title: "Modal Title",
},
slots: {
"after-title": `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
// Check that header is shown when after-title slot is provided
const header = component.find("[vl-key='header']");
expect(header.exists()).toBe(true);
expect(header.text()).toContain(slotContent);
});
// Actions slot
it("renders custom content in actions slot and shows header", () => {
const slotClass = "actions";
const slotContent = "Actions";
const component = mount(UModal, {
props: {
modelValue: true,
title: "Modal Title",
},
slots: {
actions: `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
expect(component.findComponent(UButton).exists()).toBe(false);
// Check that header is shown when actions slot is provided
const header = component.find("[vl-key='header']");
expect(header.exists()).toBe(true);
expect(header.text()).toContain(slotContent);
});
it("does not show header when no title or slots are provided", () => {
const component = mount(UModal, {
props: { modelValue },
});
const header = component.find("[vl-key='header']");
expect(header.exists()).toBe(false);
});
it("provides icon-name and close bindings to actions slot", () => {
const component = mount(UModal, {
props: {
modelValue: true,
title: "Modal Title",
},
slots: {
actions: `
<template #default="{ iconName, close }">
<button class="custom-close" :data-icon="iconName" @click="close">Close</button>
</template>
`,
},
});
const closeButton = component.find(".custom-close");
expect(closeButton.exists()).toBe(true);
expect(closeButton.attributes("data-icon")).toBe("close");
// Click the close button
closeButton.trigger("click");
// Check if the modal emitted the update:modelValue event with false
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")?.[0]).toEqual([false]);
});
// Footer-left slot
it("renders content in footer-left slot and shows footer", () => {
const slotClass = "footer-left";
const slotContent = "Footer Left";
const component = mount(UModal, {
props: { modelValue: true },
slots: {
"footer-left": `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
// Check that footer is shown when footer-left slot is provided
const footer = component.find("[vl-key='footer']");
expect(footer.exists()).toBe(true);
expect(footer.text()).toContain(slotContent);
});
// Footer-right slot
it("renders content in footer-right slot and shows footer", () => {
const slotClass = "footer-right";
const slotContent = "Footer Right";
const component = mount(UModal, {
props: { modelValue: true },
slots: {
"footer-right": `<div class="${slotClass}">${slotContent}</div>`,
},
});
expect(component.find(`.${slotClass}`).exists()).toBe(true);
expect(component.text()).toContain(slotContent);
// Check that footer is shown when footer-right slot is provided
const footer = component.find("[vl-key='footer']");
expect(footer.exists()).toBe(true);
expect(footer.text()).toContain(slotContent);
});
it("does not show footer when no footer slots are provided", () => {
const component = mount(UModal, {
props: { modelValue },
});
const footer = component.find("[vl-key='footer']");
expect(footer.exists()).toBe(false);
});
});
// Events tests
describe("Events", () => {
// Update:modelValue event
it("emits update:modelValue event when modal is closed", async () => {
const title = "Modal Title";
const closeOnCross = true;
const component = mount(UModal, {
props: {
modelValue,
title,
closeOnCross,
},
});
const closeButton = component.findComponent(UButton);
await closeButton.trigger("click");
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")?.[0]).toEqual([false]);
});
it("emits back event when back link is clicked", async () => {
const title = "Modal Title";
const backTo = "/back";
const backLabel = "Back to previous page";
const component = mountWithRouter(UModal, {
props: {
modelValue,
title,
backTo,
backLabel,
},
});
const backLink = component.findComponent(ULink);
await backLink.trigger("click");
expect(component.emitted("back")).toBeTruthy();
});
// Close event
it("emits close event when modal is closed", async () => {
const title = "Modal Title";
const closeOnCross = true;
const component = mount(UModal, {
props: {
modelValue,
title,
closeOnCross,
},
});
const closeButton = component.findComponent(UButton);
await closeButton.trigger("click");
expect(component.emitted("close")).toBeTruthy();
});
// CloseOnOverlay events
it("emits events when overlay is clicked based on closeOnOverlay prop", () => {
const closeOnOverlay = [true, false];
closeOnOverlay.forEach(async (value) => {
const component = mount(UModal, {
props: {
modelValue,
closeOnOverlay: value,
},
});
const innerWrapper = component.find("[vl-key='innerWrapper']");
await innerWrapper.trigger("click");
if (value) {
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")?.[0]).toEqual([false]);
expect(component.emitted("close")).toBeTruthy();
} else {
expect(component.emitted("update:modelValue")).toBeFalsy();
expect(component.emitted("close")).toBeFalsy();
}
});
});
// CloseOnEsc events
it("emits events when escape key is pressed based on closeOnEsc prop", () => {
const closeOnEsc = [true, false];
closeOnEsc.forEach(async (value) => {
const component = mount(UModal, {
props: {
modelValue,
closeOnEsc: value,
},
});
const wrapper = component.find("[vl-key='wrapper']");
await wrapper.trigger("keydown", { key: "Escape" });
if (value) {
expect(component.emitted("update:modelValue")).toBeTruthy();
expect(component.emitted("update:modelValue")?.[0]).toEqual([false]);
expect(component.emitted("close")).toBeTruthy();
} else {
expect(component.emitted("update:modelValue")).toBeFalsy();
expect(component.emitted("close")).toBeFalsy();
}
});
});
});
// Exposed refs tests
describe("Exposed refs", () => {
// wrapperRef
it("exposes wrapperRef", () => {
const component = mount(UModal, {
props: { modelValue },
});
expect(component.vm.wrapperRef).toBeDefined();
expect(component.vm.wrapperRef instanceof HTMLDivElement).toBe(true);
});
});
});