[Closes #1] [Closes !5] Migrated from PrimeVue to shadcn
All checks were successful
Production Build and Deploy / Build (push) Successful in 52s
Production Build and Deploy / Deploy (push) Successful in 19s

This commit is contained in:
Liviu Burcusel 2025-12-22 16:20:35 +01:00
parent 7b34c27290
commit 0cec9f5afd
Signed by: liviu
GPG key ID: 6CDB37A4AD2C610C
123 changed files with 4685 additions and 3607 deletions

View file

@ -1,11 +1,38 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import { afterEach, describe, expect, it, vi } from "vitest";
import { mount } from "@vue/test-utils";
import DefaultLayout from "~/layouts/Default.vue";
describe("Default.vue", () => {
const wrapper: VueWrapper = mount(DefaultLayout, {});
afterEach(() => {
vi.useRealTimers();
});
it("loads without crashing", () => {
const wrapper = mount(DefaultLayout);
expect(wrapper.exists()).toBe(true);
});
describe("Footer", () => {
it("footer is displayed", () => {
const wrapper = mount(DefaultLayout);
const footer = wrapper.find("[data-testid='footer']");
expect(footer.exists()).toBe(true);
});
it("footer shows only 2025 when current year is 2025", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2025, 0, 1));
const wrapper = mount(DefaultLayout);
const footer = wrapper.find("[data-testid='footer']");
expect(footer.text()).toBe("Glowing Fiesta 2025");
});
it("footer shows range when current year is not 2025", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2069, 0, 1));
const wrapper = mount(DefaultLayout);
const footer = wrapper.find("[data-testid='footer']");
expect(footer.text()).toBe("Glowing Fiesta 2025 - 2069");
});
});
});

View file

@ -1,54 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Footer from "~/layouts/default/Footer.vue";
describe("Footer.vue", () => {
const RealDate = Date;
const mockDate = (year: number) => {
globalThis.Date = class extends RealDate {
constructor(...args: any[]) {
super(args.length === 0 ? `${year}-01-01` : args[0]);
}
static now() {
return new RealDate(`${year}-01-01`).getTime();
}
} as any as DateConstructor;
};
afterEach(() => {
globalThis.Date = RealDate;
});
it("loads without crashing", () => {
const wrapper: VueWrapper = mount(Footer, {});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-footer").exists()).toBe(true);
expect(wrapper.find(".layout-footer").classes()).toContain("font-bold");
});
it("displays only 'Glowing Fiesta 2025' when current year is 2025", () => {
mockDate(2025);
const wrapper = mount(Footer);
expect(wrapper.text()).toBe("Glowing Fiesta 2025(with auto-deploy)");
expect(wrapper.text()).not.toContain(" - ");
});
it("displays 'Glowing Fiesta 2025 - 2034' when current year is 2034", () => {
mockDate(2034);
const wrapper = mount(Footer);
expect(wrapper.text()).toBe("Glowing Fiesta 2025 - 2034(with auto-deploy)");
});
it("has proper structure / content", () => {
const wrapper = mount(Footer);
const footer = wrapper.find("footer");
expect(footer.exists()).toBe(true);
expect(footer.element.tagName).toBe("FOOTER");
expect(wrapper.findAll("div")).toHaveLength(2);
});
});

View file

@ -1,11 +1,186 @@
import { describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Sidebar from "~/layouts/default/Sidebar.vue";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import SidebarLayout from "~/layouts/default/Sidebar.vue";
import { ref } from "vue";
import type * as SidebarUI from "~/components/ui/sidebar";
describe("Sidebar.vue", () => {
it("loads without crashing", () => {
const wrapper: VueWrapper = mount(Sidebar, {});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-sidebar").exists()).toBe(true);
const { useSidebarMock } = vi.hoisted(() => ({
useSidebarMock: vi.fn(),
}));
// Mock the UI components and hook
vi.mock("~/components/ui/sidebar", async (importOriginal) => {
const actual = await importOriginal<typeof SidebarUI>();
const { ref } = await import("vue");
const MockComponent = {
template: "<div><slot /></div>",
inheritAttrs: false,
};
// Default implementation
useSidebarMock.mockReturnValue({
isMobile: ref(false),
state: ref("expanded"),
openMobile: ref(false),
setOpenMobile: vi.fn(),
});
return {
...actual,
Sidebar: MockComponent,
SidebarContent: MockComponent,
SidebarFooter: MockComponent,
SidebarGroup: MockComponent,
SidebarHeader: MockComponent,
SidebarMenu: MockComponent,
SidebarMenuItem: MockComponent,
SidebarMenuButton: MockComponent,
SidebarRail: MockComponent,
SidebarMenuSub: MockComponent,
SidebarMenuSubButton: MockComponent,
SidebarMenuSubItem: MockComponent,
useSidebar: useSidebarMock,
};
});
// Mock other UI components to avoid rendering complexity
vi.mock("~/components/ui/dropdown-menu", () => {
const MockComponent = { template: "<div><slot /></div>" };
return {
DropdownMenu: MockComponent,
DropdownMenuContent: MockComponent,
DropdownMenuGroup: MockComponent,
DropdownMenuItem: MockComponent,
DropdownMenuLabel: MockComponent,
DropdownMenuSeparator: MockComponent,
DropdownMenuTrigger: MockComponent,
};
});
vi.mock("~/components/ui/collapsible", () => {
const MockComponent = { template: "<div><slot /></div>" };
return {
Collapsible: MockComponent,
CollapsibleContent: MockComponent,
CollapsibleTrigger: MockComponent,
};
});
vi.mock("~/components/ui/avatar", () => {
const MockComponent = { template: "<div><slot /></div>" };
return {
Avatar: MockComponent,
AvatarFallback: MockComponent,
AvatarImage: MockComponent,
};
});
vi.mock("@/components/ui/sheet", () => {
const MockComponent = { template: "<div><slot /></div>" };
return {
Sheet: MockComponent,
SheetContent: MockComponent,
};
});
vi.mock("@/components/ui/sheet/SheetDescription.vue", () => ({
default: { template: "<div><slot /></div>" },
}));
vi.mock("@/components/ui/sheet/SheetHeader.vue", () => ({
default: { template: "<div><slot /></div>" },
}));
vi.mock("lucide-vue-next", () => {
const MockIcon = { template: '<svg class="lucide-mock" />' };
return {
BadgeCheck: MockIcon,
Bell: MockIcon,
ChevronRight: MockIcon,
ChevronsUpDown: MockIcon,
CreditCard: MockIcon,
BookOpen: MockIcon,
HandCoins: MockIcon,
LogOut: MockIcon,
Settings2: MockIcon,
Sparkles: MockIcon,
SquareTerminal: MockIcon,
};
});
describe("SidebarLayout", () => {
it("renders the header correctly", () => {
useSidebarMock.mockReturnValue({
isMobile: ref(false),
state: ref("expanded"),
openMobile: ref(false),
setOpenMobile: vi.fn(),
});
const wrapper = mount(SidebarLayout, {
props: {
collapsible: "icon",
},
});
expect(wrapper.text()).toContain("Glowing Fiesta");
expect(wrapper.text()).toContain("v1.0.0");
});
it("renders sidebar content correctly", () => {
const wrapper = mount(SidebarLayout);
const text = wrapper.text();
// Navigation groups
expect(text).toContain("Playground");
expect(text).toContain("Documentation");
expect(text).toContain("Settings");
// User information
expect(text).toContain("Liviu");
expect(text).toContain("x.liviu@gmail.com");
// Sub-items
expect(text).toContain("History");
expect(text).toContain("Starred");
expect(text).toContain("Introduction");
expect(text).toContain("Get Started");
});
it("does not render icon if item.icon is missing", () => {
const wrapper = mount(SidebarLayout, {
props: {
navItems: [
{
title: "No Icon Item",
url: "#",
items: [],
},
],
},
});
expect(wrapper.text()).toContain("No Icon Item");
expect(wrapper.find('[data-testid="sidebar-icon"]').exists()).toBe(false);
});
it("renders correctly in mobile view", () => {
useSidebarMock.mockReturnValue({
isMobile: ref(true),
state: ref("expanded"),
openMobile: ref(true),
setOpenMobile: vi.fn(),
});
const wrapper = mount(SidebarLayout);
// When isMobile is true, it renders a Sheet.
// Since we mocked Sheet components as simple divs with slots, the content should still be present.
// However, the structure is different.
// We can verify that the content is still rendered (passed through the slot).
const text = wrapper.text();
expect(text).toContain("Glowing Fiesta");
expect(text).toContain("Liviu");
// Check specific specific mock interaction if needed, or just that it doesn't crash
// and renders the menu items.
expect(text).toContain("Playground");
});
});

View file

@ -1,159 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import Topbar from "~/layouts/default/Topbar.vue";
describe("Topbar.vue", () => {
let wrapper: VueWrapper;
beforeEach(() => {
document.documentElement.className = "";
document.body.innerHTML = '<div class="layout-wrapper"></div>';
wrapper = mount(Topbar);
});
afterEach(() => {
if (wrapper) {
wrapper.unmount();
}
});
describe("Component Rendering", () => {
it("loads without crashing", () => {
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".layout-topbar").exists()).toBe(true);
});
it("renders the logo image and text", () => {
const logo = wrapper.find(".layout-topbar-logo");
expect(logo.exists()).toBe(true);
expect(logo.find("img").attributes("alt")).toBe("Git like tree using red, white-ish and blue colours.");
expect(logo.text()).toContain("Glowing Fiesta");
});
it("renders the menu toggle button", () => {
const menuButton = wrapper.find(".layout-menu-button");
expect(menuButton.exists()).toBe(true);
expect(menuButton.find(".pi-bars").exists()).toBe(true);
});
it("renders the dark mode toggle button", () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
expect(darkModeButton.exists()).toBe(true);
});
it("renders the topbar menu items", () => {
const menuButtons = wrapper.findAll(".layout-topbar-menu button");
expect(menuButtons).toHaveLength(3);
expect(menuButtons[0].text()).toContain("Calendar");
expect(menuButtons[1].text()).toContain("Messages");
expect(menuButtons[2].text()).toContain("Profile");
});
});
describe("Dark Mode Toggle", () => {
it("starts with sun icon (light mode)", () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
expect(darkModeButton.find(".pi-sun").exists()).toBe(true);
expect(darkModeButton.find(".pi-moon").exists()).toBe(false);
});
it("toggles to moon icon when clicked", async () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
await darkModeButton.trigger("click");
expect(darkModeButton.find(".pi-moon").exists()).toBe(true);
expect(darkModeButton.find(".pi-sun").exists()).toBe(false);
});
it("removes app-dark class when toggled off", async () => {
expect(document.documentElement.classList.contains("app-dark")).toBe(false);
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
// Toggle on
await darkModeButton.trigger("click");
expect(document.documentElement.classList.contains("app-dark")).toBe(true);
// Toggle off
await darkModeButton.trigger("click");
expect(document.documentElement.classList.contains("app-dark")).toBe(false);
});
it("updates isDarkTheme ref when toggled", async () => {
const darkModeButton = wrapper.find(".layout-topbar-action-highlight");
// @ts-expect-error: it's a test accessing vm property
expect(wrapper.vm.isDarkTheme).toBe(false);
await darkModeButton.trigger("click");
// @ts-expect-error: it's a test accessing vm property
expect(wrapper.vm.isDarkTheme).toBe(true);
await darkModeButton.trigger("click");
// @ts-expect-error: it's a test accessing vm property
expect(wrapper.vm.isDarkTheme).toBe(false);
});
});
describe("Menu Toggle", () => {
it("toggles layout-static-inactive class on layout wrapper", async () => {
const menuButton = wrapper.find(".layout-menu-button");
const layoutWrapper = document.querySelector(".layout-wrapper");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false);
await menuButton.trigger("click");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(true);
await menuButton.trigger("click");
expect(layoutWrapper?.classList.contains("layout-static-inactive")).toBe(false);
});
it("handles missing layout wrapper gracefully", async () => {
document.body.innerHTML = "";
const menuButton = wrapper.find(".layout-menu-button");
// Should not throw error
expect(() => menuButton.trigger("click")).not.toThrow();
});
});
describe("Logo Link", () => {
it("links to home page", () => {
const logoLink = wrapper.find(".layout-topbar-logo");
expect(logoLink.exists()).toBe(true);
});
it("has correct image source", () => {
const img = wrapper.find(".layout-topbar-logo img");
expect(img.attributes("src")).toContain("data:image/svg+xml");
});
});
describe("Topbar Actions", () => {
it("renders Calendar action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const calendarButton = buttons[0];
expect(calendarButton.find(".pi-calendar").exists()).toBe(true);
expect(calendarButton.text()).toContain("Calendar");
});
it("renders Messages action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const messagesButton = buttons[1];
expect(messagesButton.find(".pi-inbox").exists()).toBe(true);
expect(messagesButton.text()).toContain("Messages");
});
it("renders Profile action button with icon", () => {
const buttons = wrapper.findAll(".layout-topbar-menu button");
const profileButton = buttons[2];
expect(profileButton.find(".pi-user").exists()).toBe(true);
expect(profileButton.text()).toContain("Profile");
});
});
});