diff --git a/app/components/ui/avatar/Avatar.vue b/app/components/ui/avatar/Avatar.vue new file mode 100644 index 0000000..9fb53e3 --- /dev/null +++ b/app/components/ui/avatar/Avatar.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/avatar/AvatarFallback.vue b/app/components/ui/avatar/AvatarFallback.vue new file mode 100644 index 0000000..1642b89 --- /dev/null +++ b/app/components/ui/avatar/AvatarFallback.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/avatar/AvatarImage.vue b/app/components/ui/avatar/AvatarImage.vue new file mode 100644 index 0000000..5df7505 --- /dev/null +++ b/app/components/ui/avatar/AvatarImage.vue @@ -0,0 +1,12 @@ + + + diff --git a/app/components/ui/avatar/index.ts b/app/components/ui/avatar/index.ts new file mode 100644 index 0000000..036eee9 --- /dev/null +++ b/app/components/ui/avatar/index.ts @@ -0,0 +1,3 @@ +export { default as Avatar } from "./Avatar.vue"; +export { default as AvatarFallback } from "./AvatarFallback.vue"; +export { default as AvatarImage } from "./AvatarImage.vue"; diff --git a/app/components/ui/breadcrumb/Breadcrumb.vue b/app/components/ui/breadcrumb/Breadcrumb.vue new file mode 100644 index 0000000..bbcb100 --- /dev/null +++ b/app/components/ui/breadcrumb/Breadcrumb.vue @@ -0,0 +1,13 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue b/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue new file mode 100644 index 0000000..f53b4df --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbItem.vue b/app/components/ui/breadcrumb/BreadcrumbItem.vue new file mode 100644 index 0000000..63b3d42 --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbItem.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbLink.vue b/app/components/ui/breadcrumb/BreadcrumbLink.vue new file mode 100644 index 0000000..e825394 --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbLink.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbList.vue b/app/components/ui/breadcrumb/BreadcrumbList.vue new file mode 100644 index 0000000..2c47d75 --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbList.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbPage.vue b/app/components/ui/breadcrumb/BreadcrumbPage.vue new file mode 100644 index 0000000..833a514 --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbPage.vue @@ -0,0 +1,20 @@ + + + diff --git a/app/components/ui/breadcrumb/BreadcrumbSeparator.vue b/app/components/ui/breadcrumb/BreadcrumbSeparator.vue new file mode 100644 index 0000000..c3e132f --- /dev/null +++ b/app/components/ui/breadcrumb/BreadcrumbSeparator.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/breadcrumb/index.ts b/app/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..e51a7ed --- /dev/null +++ b/app/components/ui/breadcrumb/index.ts @@ -0,0 +1,7 @@ +export { default as Breadcrumb } from "./Breadcrumb.vue"; +export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue"; +export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"; +export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"; +export { default as BreadcrumbList } from "./BreadcrumbList.vue"; +export { default as BreadcrumbPage } from "./BreadcrumbPage.vue"; +export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue"; diff --git a/app/components/ui/collapsible/Collapsible.vue b/app/components/ui/collapsible/Collapsible.vue new file mode 100644 index 0000000..cf82a6c --- /dev/null +++ b/app/components/ui/collapsible/Collapsible.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/collapsible/CollapsibleContent.vue b/app/components/ui/collapsible/CollapsibleContent.vue new file mode 100644 index 0000000..49a402f --- /dev/null +++ b/app/components/ui/collapsible/CollapsibleContent.vue @@ -0,0 +1,12 @@ + + + diff --git a/app/components/ui/collapsible/CollapsibleTrigger.vue b/app/components/ui/collapsible/CollapsibleTrigger.vue new file mode 100644 index 0000000..b071d07 --- /dev/null +++ b/app/components/ui/collapsible/CollapsibleTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/app/components/ui/collapsible/index.ts b/app/components/ui/collapsible/index.ts new file mode 100644 index 0000000..289bdbb --- /dev/null +++ b/app/components/ui/collapsible/index.ts @@ -0,0 +1,3 @@ +export { default as Collapsible } from "./Collapsible.vue"; +export { default as CollapsibleContent } from "./CollapsibleContent.vue"; +export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue"; diff --git a/app/components/ui/dropdown-menu/DropdownMenu.vue b/app/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..3d5e334 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..ba9fc8c --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuContent.vue b/app/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..43d4847 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuGroup.vue b/app/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..d22b024 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,12 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuItem.vue b/app/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..457c4a8 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuLabel.vue b/app/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..a69a7cc --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..32313a3 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..0aa597b --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,38 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..6f3568a --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..3bcb2c9 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSub.vue b/app/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..22624e8 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..bc3cf82 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..b878bcc --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..d52d0b4 --- /dev/null +++ b/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/components/ui/dropdown-menu/index.ts b/app/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..9e57848 --- /dev/null +++ b/app/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from "./DropdownMenu.vue"; + +export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"; +export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"; +export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"; +export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"; +export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"; +export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"; +export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"; +export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"; +export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"; +export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"; +export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"; +export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"; +export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"; +export { DropdownMenuPortal } from "reka-ui"; diff --git a/app/layouts/Default.vue b/app/layouts/Default.vue index 2d676cd..a22ffae 100644 --- a/app/layouts/Default.vue +++ b/app/layouts/Default.vue @@ -1,14 +1,48 @@ diff --git a/app/layouts/default/Sidebar.vue b/app/layouts/default/Sidebar.vue index 0f0f43d..98beae4 100644 --- a/app/layouts/default/Sidebar.vue +++ b/app/layouts/default/Sidebar.vue @@ -1,20 +1,113 @@ diff --git a/app/pages/index.vue b/app/pages/index.vue index 2282efc..2764077 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,10 +1,29 @@ diff --git a/tests/layouts/Default.test.ts b/tests/layouts/Default.test.ts index c2b5893..b3257b9 100644 --- a/tests/layouts/Default.test.ts +++ b/tests/layouts/Default.test.ts @@ -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"); + }); + }); }); diff --git a/tests/layouts/default/Sidebar.test.ts b/tests/layouts/default/Sidebar.test.ts new file mode 100644 index 0000000..ba04808 --- /dev/null +++ b/tests/layouts/default/Sidebar.test.ts @@ -0,0 +1,191 @@ +import { mount } from "@vue/test-utils"; +import { describe, it, expect, vi } from "vitest"; +import SidebarLayout from "~/layouts/default/Sidebar.vue"; +import { ref } from "vue"; +import type * as SidebarUI from "~/components/ui/sidebar"; + +const { useSidebarMock } = vi.hoisted(() => ({ + useSidebarMock: vi.fn(), +})); + +// Mock the UI components and hook +vi.mock("~/components/ui/sidebar", async (importOriginal) => { + const actual = await importOriginal(); + const { ref } = await import("vue"); + const MockComponent = { + template: "
", + 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: "
" }; + return { + DropdownMenu: MockComponent, + DropdownMenuContent: MockComponent, + DropdownMenuGroup: MockComponent, + DropdownMenuItem: MockComponent, + DropdownMenuLabel: MockComponent, + DropdownMenuSeparator: MockComponent, + DropdownMenuTrigger: MockComponent, + }; +}); + +vi.mock("~/components/ui/collapsible", () => { + const MockComponent = { template: "
" }; + return { + Collapsible: MockComponent, + CollapsibleContent: MockComponent, + CollapsibleTrigger: MockComponent, + }; +}); + +vi.mock("~/components/ui/avatar", () => { + const MockComponent = { template: "
" }; + return { + Avatar: MockComponent, + AvatarFallback: MockComponent, + AvatarImage: MockComponent, + }; +}); + +vi.mock("@/components/ui/sheet", () => { + const MockComponent = { template: "
" }; + return { + Sheet: MockComponent, + SheetContent: MockComponent, + }; +}); + +vi.mock("@/components/ui/sheet/SheetDescription.vue", () => ({ + default: { template: "
" }, +})); +vi.mock("@/components/ui/sheet/SheetHeader.vue", () => ({ + default: { template: "
" }, +})); +vi.mock("lucide-vue-next", () => { + const MockIcon = { template: '' }; + 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 navigation groups", () => { + const wrapper = mount(SidebarLayout); + const text = wrapper.text(); + expect(text).toContain("Playground"); + expect(text).toContain("Documentation"); + expect(text).toContain("Settings"); + }); + + it("renders user information", () => { + const wrapper = mount(SidebarLayout); + expect(wrapper.text()).toContain("Liviu"); + expect(wrapper.text()).toContain("x.liviu@gmail.com"); + }); + + it("renders sub-items in navigation", () => { + const wrapper = mount(SidebarLayout); + const text = wrapper.text(); + // Checking sub-items of Playground + expect(text).toContain("History"); + expect(text).toContain("Starred"); + // Checking sub-items of Documentation + 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"); + }); +}); diff --git a/tests/pages/index.test.ts b/tests/pages/index.test.ts index 55dccf9..f88fbd7 100644 --- a/tests/pages/index.test.ts +++ b/tests/pages/index.test.ts @@ -1,11 +1,52 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, beforeEach } from "vitest"; import { mount, type VueWrapper } from "@vue/test-utils"; import IndexPage from "~/pages/index.vue"; describe("pages/index.vue", () => { - const wrapper: VueWrapper = mount(IndexPage); + let wrapper: VueWrapper; + + beforeEach(() => { + wrapper = mount(IndexPage); + }); it("loads without crashing", () => { expect(wrapper.exists()).toBe(true); }); + + it("displays initial state correctly", () => { + const displayElement = wrapper.find(".text-lime-500"); + expect(displayElement.exists()).toBe(true); + expect(displayElement.text()).toBe("None"); + }); + + it("updates text when Default button is clicked", async () => { + // Find button method 1: by text content inside button elements + const buttons = wrapper.findAll("button"); + const defaultBtn = buttons.find((b) => b.text() === "Default"); + + expect(defaultBtn?.exists()).toBe(true); + + await defaultBtn?.trigger("click"); + expect(wrapper.find(".text-lime-500").text()).toBe("default"); + }); + + it("updates text when other buttons are clicked", async () => { + const testCases = [ + { label: "Outline", expected: "outline" }, + { label: "Ghost", expected: "ghost" }, + { label: "Link", expected: "link" }, + { label: "Secondary", expected: "secondary" }, + { label: "Destructive", expected: "destructive" }, + ]; + + for (const { label, expected } of testCases) { + const buttons = wrapper.findAll("button"); + const btn = buttons.find((b) => b.text() === label); + + expect(btn?.exists(), `Button with label ${label} should exist`).toBe(true); + await btn?.trigger("click"); + + expect(wrapper.find(".text-lime-500").text()).toBe(expected); + } + }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 07f384e..4bdb98f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -32,6 +32,9 @@ export default defineConfig({ // Exclude config files "*.config.*", "assets/icons/**", + + // Exclude UI components + "app/components/ui/**", ], }, name: "GFiesta",