[Closes #8] Added authentication
This commit is contained in:
parent
6eefa137bb
commit
6d3cdb560d
65 changed files with 5834 additions and 440 deletions
|
|
@ -1,38 +1,89 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { afterEach, describe, expect, it, vi, beforeAll, afterAll } from "vitest";
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import DefaultLayout from "~/layouts/Default.vue";
|
||||
|
||||
vi.mock("#app", () => ({
|
||||
useRuntimeConfig: () => ({
|
||||
public: {
|
||||
appVersion: "1.0.1",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("~/stores/auth", () => ({
|
||||
useAuthStore: () => ({
|
||||
init: vi.fn(),
|
||||
user: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Default.vue", () => {
|
||||
beforeAll(() => {
|
||||
const shouldSuppress = (args: string[]) => {
|
||||
const msg = args.join(" ");
|
||||
return msg.includes("<Suspense> is an experimental feature");
|
||||
};
|
||||
|
||||
const spyMethods = ["warn", "error", "log", "info"] as const;
|
||||
for (const method of spyMethods) {
|
||||
const original = console[method];
|
||||
vi.spyOn(console, method).mockImplementation((...args) => {
|
||||
if (shouldSuppress(args)) return;
|
||||
original(...args);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("loads without crashing", () => {
|
||||
const wrapper = mount(DefaultLayout);
|
||||
it("loads without crashing", async () => {
|
||||
const wrapper = mount({
|
||||
components: { DefaultLayout },
|
||||
template: "<Suspense><DefaultLayout /></Suspense>",
|
||||
});
|
||||
await flushPromises(); // Wait for async setup
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe("Footer", () => {
|
||||
it("footer is displayed", () => {
|
||||
const wrapper = mount(DefaultLayout);
|
||||
it("footer is displayed", async () => {
|
||||
const wrapper = mount({
|
||||
components: { DefaultLayout },
|
||||
template: "<Suspense><DefaultLayout /></Suspense>",
|
||||
});
|
||||
await flushPromises();
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("footer shows only 2025 when current year is 2025", () => {
|
||||
it("footer shows only 2025 when current year is 2025", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2025, 0, 1));
|
||||
const wrapper = mount(DefaultLayout);
|
||||
const wrapper = mount({
|
||||
components: { DefaultLayout },
|
||||
template: "<Suspense><DefaultLayout /></Suspense>",
|
||||
});
|
||||
await flushPromises();
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025 (1.0.1)");
|
||||
});
|
||||
|
||||
it("footer shows range when current year is not 2025", () => {
|
||||
it("footer shows range when current year is not 2025", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2069, 0, 1));
|
||||
const wrapper = mount(DefaultLayout);
|
||||
const wrapper = mount({
|
||||
components: { DefaultLayout },
|
||||
template: "<Suspense><DefaultLayout /></Suspense>",
|
||||
});
|
||||
await flushPromises();
|
||||
const footer = wrapper.find("[data-testid='footer']");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025 - 2069");
|
||||
expect(footer.text()).toBe("Glowing Fiesta 2025 - 2069 (1.0.1)");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi, beforeAll, afterAll } from "vitest";
|
||||
import SidebarLayout from "~/layouts/default/Sidebar.vue";
|
||||
import { ref } from "vue";
|
||||
import type * as SidebarUI from "~/components/ui/sidebar";
|
||||
|
|
@ -8,6 +8,20 @@ const { useSidebarMock } = vi.hoisted(() => ({
|
|||
useSidebarMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// ... (existing mocks)
|
||||
|
||||
vi.mock("~/stores/auth", () => ({
|
||||
useAuthStore: () => ({
|
||||
user: {
|
||||
name: "Liviu",
|
||||
email: "x.liviu@gmail.com",
|
||||
image: "avatar.png",
|
||||
},
|
||||
signOut: vi.fn(),
|
||||
init: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the UI components and hook
|
||||
vi.mock("~/components/ui/sidebar", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SidebarUI>();
|
||||
|
|
@ -99,6 +113,7 @@ vi.mock("lucide-vue-next", () => {
|
|||
CreditCard: MockIcon,
|
||||
BookOpen: MockIcon,
|
||||
HandCoins: MockIcon,
|
||||
LogIn: MockIcon,
|
||||
LogOut: MockIcon,
|
||||
Settings2: MockIcon,
|
||||
Sparkles: MockIcon,
|
||||
|
|
@ -107,7 +122,26 @@ vi.mock("lucide-vue-next", () => {
|
|||
});
|
||||
|
||||
describe("SidebarLayout", () => {
|
||||
it("renders the header correctly", () => {
|
||||
beforeAll(() => {
|
||||
const shouldSuppress = (args: string[]) => {
|
||||
const msg = args.join(" ");
|
||||
return msg.includes("<Suspense> is an experimental feature");
|
||||
};
|
||||
|
||||
const spyMethods = ["warn", "error", "log", "info"] as const;
|
||||
for (const method of spyMethods) {
|
||||
const original = console[method];
|
||||
vi.spyOn(console, method).mockImplementation((...args) => {
|
||||
if (shouldSuppress(args)) return;
|
||||
original(...args);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
it("renders the header correctly", async () => {
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false),
|
||||
state: ref("expanded"),
|
||||
|
|
@ -115,17 +149,41 @@ describe("SidebarLayout", () => {
|
|||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarLayout, {
|
||||
props: {
|
||||
collapsible: "icon",
|
||||
const wrapper = mount({
|
||||
components: { SidebarLayout },
|
||||
template: "<Suspense><SidebarLayout v-bind='props' /></Suspense>",
|
||||
setup() {
|
||||
return {
|
||||
props: {
|
||||
collapsible: "icon",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain("Glowing Fiesta");
|
||||
expect(wrapper.text()).toContain("v1.0.0");
|
||||
});
|
||||
|
||||
it("renders sidebar content correctly", () => {
|
||||
const wrapper = mount(SidebarLayout);
|
||||
it("renders sidebar content correctly", async () => {
|
||||
const user = {
|
||||
name: "Liviu",
|
||||
email: "x.liviu@gmail.com",
|
||||
image: "avatar.png",
|
||||
id: "123",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const wrapper = mount({
|
||||
components: { SidebarLayout },
|
||||
template: "<Suspense><SidebarLayout :user='user' /></Suspense>",
|
||||
setup() {
|
||||
return { user };
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
const text = wrapper.text();
|
||||
|
||||
// Navigation groups
|
||||
|
|
@ -144,24 +202,31 @@ describe("SidebarLayout", () => {
|
|||
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: [],
|
||||
it("does not render icon if item.icon is missing", async () => {
|
||||
const wrapper = mount({
|
||||
components: { SidebarLayout },
|
||||
template: "<Suspense><SidebarLayout v-bind='props' /></Suspense>",
|
||||
setup() {
|
||||
return {
|
||||
props: {
|
||||
navItems: [
|
||||
{
|
||||
title: "No Icon Item",
|
||||
url: "#",
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain("No Icon Item");
|
||||
expect(wrapper.find('[data-testid="sidebar-icon"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("renders correctly in mobile view", () => {
|
||||
it("renders correctly in mobile view", async () => {
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(true),
|
||||
state: ref("expanded"),
|
||||
|
|
@ -169,7 +234,24 @@ describe("SidebarLayout", () => {
|
|||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarLayout);
|
||||
const user = {
|
||||
name: "Liviu",
|
||||
email: "x.liviu@gmail.com",
|
||||
image: "avatar.png",
|
||||
id: "123",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const wrapper = mount({
|
||||
components: { SidebarLayout },
|
||||
template: "<Suspense><SidebarLayout :user='user' /></Suspense>",
|
||||
setup() {
|
||||
return { user };
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
// When isMobile is true, it renders a Sheet.
|
||||
// Since we mocked Sheet components as simple divs with slots, the content should still be present.
|
||||
|
|
|
|||
336
tests/layouts/default/SidebarFooter.test.ts
Normal file
336
tests/layouts/default/SidebarFooter.test.ts
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import SidebarFooterComponent from "~/layouts/default/SidebarFooter.vue";
|
||||
|
||||
import type * as SidebarUI from "~/components/ui/sidebar";
|
||||
|
||||
const { useSidebarMock } = vi.hoisted(() => ({
|
||||
useSidebarMock: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock navigateTo
|
||||
const navigateToMock = vi.fn();
|
||||
vi.stubGlobal("navigateTo", navigateToMock);
|
||||
|
||||
// Mock UI components
|
||||
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,
|
||||
};
|
||||
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false), // Ensure isMobile is a ref
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
SidebarFooter: MockComponent,
|
||||
SidebarMenu: MockComponent,
|
||||
SidebarMenuItem: MockComponent,
|
||||
SidebarMenuButton: MockComponent,
|
||||
useSidebar: useSidebarMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/components/ui/avatar", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
return {
|
||||
Avatar: MockComponent,
|
||||
AvatarFallback: MockComponent,
|
||||
AvatarImage: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/components/ui/dropdown-menu", () => {
|
||||
const MockComponent = { template: "<div><slot /></div>" };
|
||||
const MockContent = {
|
||||
template: '<div data-testid="dropdown-content"><slot /></div>',
|
||||
name: "DropdownMenuContent",
|
||||
};
|
||||
const DropdownMenuItem = {
|
||||
name: "DropdownMenuItem",
|
||||
template: '<div data-testid="dropdown-item" @click="$emit(\'click\')"><slot /></div>',
|
||||
emits: ["click"],
|
||||
};
|
||||
return {
|
||||
DropdownMenu: MockComponent,
|
||||
DropdownMenuTrigger: MockComponent,
|
||||
DropdownMenuContent: MockContent,
|
||||
DropdownMenuGroup: MockComponent,
|
||||
DropdownMenuItem: DropdownMenuItem,
|
||||
DropdownMenuLabel: MockComponent,
|
||||
DropdownMenuSeparator: MockComponent,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("lucide-vue-next", () => {
|
||||
const MockIcon = { template: '<svg class="lucide-mock" />' };
|
||||
return {
|
||||
BadgeCheck: MockIcon,
|
||||
Bell: MockIcon,
|
||||
ChevronsUpDown: MockIcon,
|
||||
CreditCard: MockIcon,
|
||||
LogIn: MockIcon,
|
||||
LogOut: MockIcon,
|
||||
};
|
||||
});
|
||||
|
||||
describe("SidebarFooter.vue", () => {
|
||||
const user = {
|
||||
name: "Liviu Test",
|
||||
email: "test@example.com",
|
||||
image: "avatar.png",
|
||||
id: "123",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
it("renders user information correctly when user is provided", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Liviu Test");
|
||||
expect(wrapper.text()).toContain("test@example.com");
|
||||
// Initials "Liviu Test" -> "LT"
|
||||
expect(wrapper.text()).toContain("LT");
|
||||
});
|
||||
|
||||
it("renders anonymous view correctly when user is not provided (null)", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: null },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Anonymous");
|
||||
expect(wrapper.text()).toContain("No email");
|
||||
expect(wrapper.text()).toContain("Anon");
|
||||
});
|
||||
|
||||
it("renders anonymous view correctly when user is undefined", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: undefined },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Anonymous");
|
||||
expect(wrapper.text()).toContain("No email");
|
||||
expect(wrapper.text()).toContain("Anon");
|
||||
});
|
||||
|
||||
it("renders 'Log out' option when user is logged in", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Log out");
|
||||
expect(wrapper.text()).not.toContain("Log in");
|
||||
});
|
||||
|
||||
it("renders 'Log in' option when user is anonymous", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: null },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Log in");
|
||||
expect(wrapper.text()).not.toContain("Log out");
|
||||
});
|
||||
|
||||
it("calls navigateTo('/member/auth/logout') when Log out is clicked", async () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const logoutItem = wrapper.findAll('[data-testid="dropdown-item"]').find((item) => item.text().includes("Log out"));
|
||||
|
||||
expect(logoutItem).toBeDefined();
|
||||
await logoutItem?.trigger("click");
|
||||
|
||||
expect(navigateToMock).toHaveBeenCalledWith("/member/auth/logout");
|
||||
});
|
||||
|
||||
it("calls navigateTo('/member/auth/login') when Log in is clicked", async () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: null },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginItem = wrapper.findAll('[data-testid="dropdown-item"]').find((item) => item.text().includes("Log in"));
|
||||
|
||||
expect(loginItem).toBeDefined();
|
||||
await loginItem?.trigger("click");
|
||||
|
||||
expect(navigateToMock).toHaveBeenCalledWith("/member/auth/login");
|
||||
});
|
||||
|
||||
it("computes initials correctly for single name", () => {
|
||||
const singleNameUser = { ...user, name: "Liviu" };
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: singleNameUser },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
// "Liviu" -> "L"
|
||||
expect(wrapper.text()).toContain("L");
|
||||
});
|
||||
|
||||
it("renders correctly when user has no image", () => {
|
||||
const userNoImage = { ...user, image: null as unknown as string };
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: userNoImage },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Liviu Test");
|
||||
expect(wrapper.text()).toContain("test@example.com");
|
||||
expect(wrapper.text()).toContain("LT");
|
||||
});
|
||||
|
||||
it("sets side to 'bottom' when isMobile is true", () => {
|
||||
const { ref } = require("vue");
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(true),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownContent = wrapper.findComponent({ name: "DropdownMenuContent" });
|
||||
expect(dropdownContent.attributes("side")).toBe("bottom");
|
||||
});
|
||||
|
||||
it("sets side to 'right' when isMobile is false", () => {
|
||||
const { ref } = require("vue");
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownContent = wrapper.findComponent({ name: "DropdownMenuContent" });
|
||||
expect(dropdownContent.attributes("side")).toBe("right");
|
||||
});
|
||||
|
||||
it("sets side to 'bottom' when isMobile is true and user is anonymous", () => {
|
||||
const { ref } = require("vue");
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(true),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: null },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownContent = wrapper.findComponent({ name: "DropdownMenuContent" });
|
||||
expect(dropdownContent.attributes("side")).toBe("bottom");
|
||||
});
|
||||
|
||||
it("sets side to 'right' when isMobile is false and user is anonymous", () => {
|
||||
const { ref } = require("vue");
|
||||
useSidebarMock.mockReturnValue({
|
||||
isMobile: ref(false),
|
||||
state: ref("expanded"),
|
||||
openMobile: ref(false),
|
||||
setOpenMobile: vi.fn(),
|
||||
});
|
||||
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: null },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dropdownContent = wrapper.findComponent({ name: "DropdownMenuContent" });
|
||||
expect(dropdownContent.attributes("side")).toBe("right");
|
||||
});
|
||||
|
||||
it("returns empty string for userInititials when user is undefined (covering line 24)", () => {
|
||||
const wrapper = mount(SidebarFooterComponent, {
|
||||
props: { user: undefined },
|
||||
global: {
|
||||
stubs: {
|
||||
ClientOnly: { template: "<div><slot /></div>" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Access the component's internal state/computed properties
|
||||
expect((wrapper.vm as any).userInititials).toBe("");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue