diff --git a/frontend/src/components/HomeContactFooter.vue b/frontend/src/components/HomeContactFooter.vue new file mode 100644 index 0000000..78eff7e --- /dev/null +++ b/frontend/src/components/HomeContactFooter.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/frontend/src/components/HomeHeroStatus.vue b/frontend/src/components/HomeHeroStatus.vue new file mode 100644 index 0000000..6e4328f --- /dev/null +++ b/frontend/src/components/HomeHeroStatus.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/frontend/src/components/HomeMemberAccess.vue b/frontend/src/components/HomeMemberAccess.vue new file mode 100644 index 0000000..72b4a74 --- /dev/null +++ b/frontend/src/components/HomeMemberAccess.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/frontend/src/components/HomeProfessionalSections.vue b/frontend/src/components/HomeProfessionalSections.vue new file mode 100644 index 0000000..2f9b321 --- /dev/null +++ b/frontend/src/components/HomeProfessionalSections.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/frontend/src/components/TopBar.vue b/frontend/src/components/TopBar.vue index a88e904..1edc3b8 100644 --- a/frontend/src/components/TopBar.vue +++ b/frontend/src/components/TopBar.vue @@ -103,6 +103,10 @@ function onWindowKeydown(event) { menuButton.value?.focus(); } +/** + * WHY: Keep the opened mobile menu keyboard-contained and return Escape focus to the menu toggle. + * @param {KeyboardEvent} event Navigation key event from the primary nav. + */ function onNavKeydown(event) { if (event.key === "Escape") { closeMenu(); diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index d918688..c3151a7 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,132 +1,16 @@ diff --git a/testing/frontend/unit/home.spec.js b/testing/frontend/unit/home.spec.js index 31f25dc..0228f01 100644 --- a/testing/frontend/unit/home.spec.js +++ b/testing/frontend/unit/home.spec.js @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals"; -import { RouterLinkStub, flushPromises, shallowMount } from "@vue/test-utils"; +import { RouterLinkStub, flushPromises, mount, shallowMount } from "@vue/test-utils"; import PlatformSection from "../../../frontend/src/components/PlatformSection.vue"; import { auth } from "../../../frontend/src/auth.js"; @@ -42,7 +42,7 @@ describe("HomeView", () => { it("renders the professional homepage while logged out", () => { resetAuth(); - const wrapper = shallowMount(HomeView, { + const wrapper = mount(HomeView, { global: { stubs: { RouterLink: RouterLinkStub, @@ -106,7 +106,7 @@ describe("HomeView", () => { it("shows a calm fallback when public status is unavailable", () => { resetAuth(); - const wrapper = shallowMount(HomeView, { + const wrapper = mount(HomeView, { global: { stubs: { RouterLink: RouterLinkStub, @@ -130,7 +130,7 @@ describe("HomeView", () => { auth.username = "ada"; auth.email = "ada@example.dev"; - const wrapper = shallowMount(HomeView, { + const wrapper = mount(HomeView, { global: { stubs: { RouterLink: RouterLinkStub, diff --git a/testing/frontend/unit/static-views.spec.js b/testing/frontend/unit/static-views.spec.js index e73e1ba..a813878 100644 --- a/testing/frontend/unit/static-views.spec.js +++ b/testing/frontend/unit/static-views.spec.js @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, jest } from "@jest/globals"; import { flushPromises, mount, shallowMount } from "@vue/test-utils"; +import { nextTick } from "vue"; import axios from "axios"; @@ -123,24 +124,56 @@ describe("static shell views and components", () => { const logout = jest.spyOn(authModule, "logout").mockImplementation(() => {}); jest.spyOn(authModule, "login").mockImplementation(() => {}); - const wrapper = mount(TopBar); - expect(wrapper.text()).toContain("Contact"); - expect(wrapper.text()).toContain("Login"); - expect(wrapper.text()).toContain("Register"); - expect(wrapper.text()).not.toContain("Reset Password"); - expect(wrapper.find("a[href='/#contact']").exists()).toBe(true); + const target = document.createElement("div"); + document.body.appendChild(target); + const wrapper = mount(TopBar, { attachTo: target }); - await wrapper.find("button.menu-toggle").trigger("click"); - expect(wrapper.find("nav").classes()).toContain("open"); - await wrapper.find("a[href='/#contact']").trigger("click"); - expect(wrapper.find("nav").classes()).not.toContain("open"); + try { + expect(wrapper.text()).toContain("Contact"); + expect(wrapper.text()).toContain("Login"); + expect(wrapper.text()).toContain("Register"); + expect(wrapper.text()).not.toContain("Reset Password"); + expect(wrapper.find("a[href='/#contact']").exists()).toBe(true); - auth.authenticated = true; - await flushPromises(); - expect(wrapper.text()).toContain("Account"); - expect(wrapper.text()).toContain("Open Services"); - await wrapper.findAll("button.button").at(-1).trigger("click"); - expect(logout).toHaveBeenCalled(); + await wrapper.find("button.menu-toggle").trigger("click"); + const nav = wrapper.find("nav"); + expect(nav.classes()).toContain("open"); + const navItems = () => Array.from(nav.element.querySelectorAll("a[href], button:not([disabled])")); + navItems()[0].focus(); + nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true })); + await nextTick(); + expect(document.activeElement).toBe(navItems().at(-1)); + + navItems().at(-1).focus(); + nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true })); + await nextTick(); + expect(document.activeElement).toBe(navItems()[0]); + + nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); + await nextTick(); + expect(nav.classes()).not.toContain("open"); + expect(document.activeElement).toBe(wrapper.find("button.menu-toggle").element); + + await wrapper.find("button.menu-toggle").trigger("click"); + expect(nav.classes()).toContain("open"); + window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); + await nextTick(); + expect(nav.classes()).not.toContain("open"); + + await wrapper.find("button.menu-toggle").trigger("click"); + await wrapper.find("a[href='/#contact']").trigger("click"); + expect(nav.classes()).not.toContain("open"); + + auth.authenticated = true; + await flushPromises(); + expect(wrapper.text()).toContain("Account"); + expect(wrapper.text()).toContain("Open Services"); + await wrapper.findAll("button.button").at(-1).trigger("click"); + expect(logout).toHaveBeenCalled(); + } finally { + wrapper.unmount(); + target.remove(); + } }); it("loads Monero status and handles API failures", async () => {