diff --git a/app/components/LoginForm.vue b/app/components/LoginForm.vue
index 1906254..8dd34a3 100644
--- a/app/components/LoginForm.vue
+++ b/app/components/LoginForm.vue
@@ -43,7 +43,7 @@ const doLogin = async () => {
Don't have an account?
-
+
diff --git a/app/components/SignupForm.vue b/app/components/SignupForm.vue
index 134d456..9bfa07a 100644
--- a/app/components/SignupForm.vue
+++ b/app/components/SignupForm.vue
@@ -63,7 +63,7 @@ const createAccount = async () => {
Already have an account?
-
+
diff --git a/app/layouts/Default.vue b/app/layouts/Default.vue
index a8a9b30..f82fff1 100644
--- a/app/layouts/Default.vue
+++ b/app/layouts/Default.vue
@@ -1,18 +1,11 @@
+
+
+
+
+
+
+
+
+ {{ breadcrumbStore.items[i - 1]?.label }}
+
+
+
+
+
+
+ {{ breadcrumbStore.items[breadcrumbStore.items.length - 1]?.label }}
+
+
+
+
+
+
diff --git a/app/layouts/default/SidebarFooter.vue b/app/layouts/default/SidebarFooter.vue
index 818dd1b..3fcd70e 100644
--- a/app/layouts/default/SidebarFooter.vue
+++ b/app/layouts/default/SidebarFooter.vue
@@ -29,11 +29,11 @@ const userInititials = computed(() => {
});
const handleLogout = () => {
- navigateTo("/member/auth/logout");
+ navigateTo("/auth/logout");
};
const handleLogin = () => {
- navigateTo("/member/auth/login");
+ navigateTo("/auth/login");
};
diff --git a/app/pages/member/auth/create-account.vue b/app/pages/auth/create-account.vue
similarity index 59%
rename from app/pages/member/auth/create-account.vue
rename to app/pages/auth/create-account.vue
index 96858b8..cecf990 100644
--- a/app/pages/member/auth/create-account.vue
+++ b/app/pages/auth/create-account.vue
@@ -1,5 +1,9 @@
diff --git a/app/pages/member/auth/login.vue b/app/pages/auth/login.vue
similarity index 51%
rename from app/pages/member/auth/login.vue
rename to app/pages/auth/login.vue
index c8c0dc3..fb41d63 100644
--- a/app/pages/member/auth/login.vue
+++ b/app/pages/auth/login.vue
@@ -1,5 +1,9 @@
diff --git a/app/pages/member/auth/logout.vue b/app/pages/auth/logout.vue
similarity index 87%
rename from app/pages/member/auth/logout.vue
rename to app/pages/auth/logout.vue
index fbce4dc..8227e51 100644
--- a/app/pages/member/auth/logout.vue
+++ b/app/pages/auth/logout.vue
@@ -4,9 +4,13 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Field, FieldDescription } from "@/components/ui/field";
import { useAuthStore } from "~/stores/auth";
+import { useBreadcrumbStore } from "~/stores/breadcrumbs";
const authStore = useAuthStore();
await authStore.init();
+
+const breadcrumbStore = useBreadcrumbStore();
+breadcrumbStore.setBreadcrumbs([{ label: "Auth" }, { label: "Logout", to: "/auth/logout" }]);
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 2764077..65c5c37 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,5 +1,6 @@
diff --git a/app/stores/breadcrumbs.ts b/app/stores/breadcrumbs.ts
new file mode 100644
index 0000000..1d929bd
--- /dev/null
+++ b/app/stores/breadcrumbs.ts
@@ -0,0 +1,24 @@
+import { defineStore } from "pinia";
+
+export interface BreadcrumbItem {
+ label: string;
+ to?: string;
+}
+
+export const useBreadcrumbStore = defineStore("breadcrumb", {
+ state: () => ({
+ items: [] as BreadcrumbItem[],
+ }),
+
+ actions: {
+ setBreadcrumbs(items: BreadcrumbItem[]) {
+ this.items = items;
+ },
+ addBreadcrumb(item: BreadcrumbItem) {
+ this.items.push(item);
+ },
+ clear() {
+ this.items = [];
+ },
+ },
+});
diff --git a/tests/layouts/default/SidebarFooter.test.ts b/tests/layouts/default/SidebarFooter.test.ts
index ffb7f07..4668e9f 100644
--- a/tests/layouts/default/SidebarFooter.test.ts
+++ b/tests/layouts/default/SidebarFooter.test.ts
@@ -166,7 +166,7 @@ describe("SidebarFooter.vue", () => {
expect(wrapper.text()).not.toContain("Log out");
});
- it("calls navigateTo('/member/auth/logout') when Log out is clicked", async () => {
+ it("calls navigateTo('/auth/logout') when Log out is clicked", async () => {
const wrapper = mount(SidebarFooterComponent, {
props: { user },
global: {
@@ -181,10 +181,10 @@ describe("SidebarFooter.vue", () => {
expect(logoutItem).toBeDefined();
await logoutItem?.trigger("click");
- expect(navigateToMock).toHaveBeenCalledWith("/member/auth/logout");
+ expect(navigateToMock).toHaveBeenCalledWith("/auth/logout");
});
- it("calls navigateTo('/member/auth/login') when Log in is clicked", async () => {
+ it("calls navigateTo('/auth/login') when Log in is clicked", async () => {
const wrapper = mount(SidebarFooterComponent, {
props: { user: null },
global: {
@@ -199,7 +199,7 @@ describe("SidebarFooter.vue", () => {
expect(loginItem).toBeDefined();
await loginItem?.trigger("click");
- expect(navigateToMock).toHaveBeenCalledWith("/member/auth/login");
+ expect(navigateToMock).toHaveBeenCalledWith("/auth/login");
});
it("computes initials correctly for single name", () => {
diff --git a/tests/pages/member/auth/create-account.test.ts b/tests/pages/auth/create-account.test.ts
similarity index 97%
rename from tests/pages/member/auth/create-account.test.ts
rename to tests/pages/auth/create-account.test.ts
index 3a38b63..80adc36 100644
--- a/tests/pages/member/auth/create-account.test.ts
+++ b/tests/pages/auth/create-account.test.ts
@@ -1,6 +1,6 @@
import { mount, flushPromises } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach } from "vitest";
-import CreateAccountPage from "~/pages/member/auth/create-account.vue";
+import CreateAccountPage from "~/pages/auth/create-account.vue";
// Mock auth client
const authMocks = vi.hoisted(() => ({
diff --git a/tests/pages/member/auth/login.test.ts b/tests/pages/auth/login.test.ts
similarity index 98%
rename from tests/pages/member/auth/login.test.ts
rename to tests/pages/auth/login.test.ts
index 2f7a65a..65b0359 100644
--- a/tests/pages/member/auth/login.test.ts
+++ b/tests/pages/auth/login.test.ts
@@ -1,6 +1,6 @@
import { mount, flushPromises } from "@vue/test-utils";
import { describe, expect, it, vi, beforeEach, beforeAll, afterAll } from "vitest";
-import LoginPage from "~/pages/member/auth/login.vue";
+import LoginPage from "~/pages/auth/login.vue";
// Mock the auth store
const authStoreMocks = vi.hoisted(() => ({
diff --git a/tests/pages/member/auth/logout.test.ts b/tests/pages/auth/logout.test.ts
similarity index 97%
rename from tests/pages/member/auth/logout.test.ts
rename to tests/pages/auth/logout.test.ts
index 316b55b..400c533 100644
--- a/tests/pages/member/auth/logout.test.ts
+++ b/tests/pages/auth/logout.test.ts
@@ -1,6 +1,6 @@
import { mount, flushPromises } from "@vue/test-utils";
import { describe, expect, it, vi, beforeEach, beforeAll, afterAll } from "vitest";
-import LogoutPage from "~/pages/member/auth/logout.vue";
+import LogoutPage from "~/pages/auth/logout.vue";
// Mock the auth store
const mocks = vi.hoisted(() => ({
diff --git a/tests/pages/index.test.ts b/tests/pages/index.test.ts
index f88fbd7..e57e306 100644
--- a/tests/pages/index.test.ts
+++ b/tests/pages/index.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, beforeEach } from "vitest";
+import { beforeEach, describe, expect, it } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import IndexPage from "~/pages/index.vue";
diff --git a/tests/setup.ts b/tests/setup.ts
index 860c10b..7cffe5c 100644
--- a/tests/setup.ts
+++ b/tests/setup.ts
@@ -18,6 +18,16 @@ Object.defineProperty(global, "import", {
writable: true,
});
+const breadcrumbStoreMocks = vi.hoisted(() => ({
+ setBreadcrumbs: vi.fn(),
+ addBreadcrumb: vi.fn(),
+ clear: vi.fn(),
+ items: [{ label: "Auth" }, { label: "Create Account", to: "/auth/create-account" }],
+}));
+vi.mock("~/stores/breadcrumbs", () => ({
+ useBreadcrumbStore: () => breadcrumbStoreMocks,
+}));
+
config.global.stubs = {
NuxtLayout: true,
NuxtPage: true,
diff --git a/tests/shared/utils/auth-client.test.ts b/tests/shared/utils/auth-client.test.ts
new file mode 100644
index 0000000..2678c11
--- /dev/null
+++ b/tests/shared/utils/auth-client.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { authClient } from "#shared/utils/auth-client";
+import { createAuthClient } from "better-auth/vue";
+
+const vars = vi.hoisted(() => ({
+ mockAuthClient: {
+ signIn: vi.fn(),
+ signUp: vi.fn(),
+ signOut: vi.fn(),
+ },
+}));
+
+vi.mock("better-auth/vue", () => ({
+ createAuthClient: vi.fn(() => vars.mockAuthClient),
+}));
+
+describe("auth-client utility", () => {
+ it("should create and export the auth client", () => {
+ expect(createAuthClient).toHaveBeenCalled();
+ expect(authClient).toBe(vars.mockAuthClient);
+ });
+});
diff --git a/tests/shared/utils/env.test.ts b/tests/shared/utils/env.test.ts
new file mode 100644
index 0000000..a7fcced
--- /dev/null
+++ b/tests/shared/utils/env.test.ts
@@ -0,0 +1,36 @@
+/* eslint-disable node/no-process-env */
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { z } from "zod";
+
+describe("shared/utils/env", () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ vi.resetModules();
+ process.env = { ...originalEnv };
+ process.env.NODE_ENV = "test";
+ process.env.DATABASE_URL = "postgres://localhost:5432/db";
+ process.env.BETTER_AUTH_SECRET = "secret";
+ process.env.BETTER_AUTH_URL = "http://localhost:3000";
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ it("should validate and export variables when all required variables are present", async () => {
+ const env = (await import("#shared/utils/env")).default;
+
+ expect(env).toEqual({
+ NODE_ENV: "test",
+ DATABASE_URL: "postgres://localhost:5432/db",
+ BETTER_AUTH_SECRET: "secret",
+ BETTER_AUTH_URL: "http://localhost:3000",
+ });
+ });
+
+ it("should throw an error if NODE_ENV is missing", async () => {
+ delete process.env.NODE_ENV;
+ await expect(import("#shared/utils/env")).rejects.toThrow(z.ZodError);
+ });
+});
diff --git a/tests/stores/breadcrumbs.test.ts b/tests/stores/breadcrumbs.test.ts
new file mode 100644
index 0000000..506fb0b
--- /dev/null
+++ b/tests/stores/breadcrumbs.test.ts
@@ -0,0 +1,40 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createPinia, setActivePinia } from "pinia";
+import { useBreadcrumbStore } from "~/stores/breadcrumbs";
+
+vi.unmock("~/stores/breadcrumbs");
+
+describe("useBreadcrumbStore", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ });
+
+ describe("init", () => {
+ it("should initialize", () => {
+ const store = useBreadcrumbStore();
+ expect(store.items.length).toEqual(0);
+ });
+
+ it("clear should remove all breadcrumbs", () => {
+ const store = useBreadcrumbStore();
+ store.addBreadcrumb({ label: "Test", to: "/test" });
+ store.clear();
+ expect(store.items.length).toEqual(0);
+ });
+
+ it("addBreadcrumb should add a breadcrumb", () => {
+ const store = useBreadcrumbStore();
+ store.addBreadcrumb({ label: "Test", to: "/test" });
+ expect(store.items.length).toEqual(1);
+ });
+
+ it("setBreadcrumbs should set breadcrumbs", () => {
+ const store = useBreadcrumbStore();
+ store.setBreadcrumbs([
+ { label: "Test", to: "/test" },
+ { label: "Test 2", to: "/test2" },
+ ]);
+ expect(store.items.length).toEqual(2);
+ });
+ });
+});