From 6af315ac44edae2fe1b98bd96454cda4b7865258 Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Fri, 24 Apr 2026 13:15:15 +0800 Subject: [PATCH] test: add comprehensive landing page unit tests (85 tests) 6 test files covering all landing page components: - BentoGrid: search filtering, category pills, tool count, empty state - Navbar: links, mobile menu, GitHub star count formatting, fetch - HowItWorks: Docker command display, clipboard copy, terminal chrome - FAQ: accordion expand/collapse, all questions rendered - Contact: form fields, validation, Formspree submission, error states - Components: Hero, TypingCursor, WhyChoose, Enterprise, Pricing, OpenSource, Footer (rendering, links, content verification) Also updates vitest.config.ts with landing page aliases and automatic JSX transform for Next.js components. --- tests/unit/landing/bento-grid.test.tsx | 119 +++++++++++ tests/unit/landing/components.test.tsx | 261 +++++++++++++++++++++++ tests/unit/landing/contact.test.tsx | 184 ++++++++++++++++ tests/unit/landing/faq.test.tsx | 98 +++++++++ tests/unit/landing/how-it-works.test.tsx | 98 +++++++++ tests/unit/landing/navbar.test.tsx | 127 +++++++++++ vitest.config.ts | 16 +- 7 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 tests/unit/landing/bento-grid.test.tsx create mode 100644 tests/unit/landing/components.test.tsx create mode 100644 tests/unit/landing/contact.test.tsx create mode 100644 tests/unit/landing/faq.test.tsx create mode 100644 tests/unit/landing/how-it-works.test.tsx create mode 100644 tests/unit/landing/navbar.test.tsx diff --git a/tests/unit/landing/bento-grid.test.tsx b/tests/unit/landing/bento-grid.test.tsx new file mode 100644 index 00000000..a460068d --- /dev/null +++ b/tests/unit/landing/bento-grid.test.tsx @@ -0,0 +1,119 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import type React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: React.PropsWithChildren>) => ( +
{children}
+ ), + }, + AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}, +})); + +import { BentoGrid } from "@landing/components/bento-grid"; + +afterEach(cleanup); + +function getCountText(): string { + const el = screen.getByText(/^Showing/); + return el.textContent ?? ""; +} + +describe("BentoGrid", () => { + it("renders the section heading", () => { + render(); + expect(screen.getByText("40+ tools. Zero cloud dependency.")).toBeDefined(); + }); + + it("renders the search input", () => { + render(); + expect(screen.getByPlaceholderText("Search tools...")).toBeDefined(); + }); + + it("shows all tools by default", () => { + render(); + const text = getCountText(); + expect(text).toMatch(/Showing 46 of 46 tools/); + }); + + it("renders category filter pills including All", () => { + render(); + expect(screen.getByText((_, el) => el?.textContent === "All (46)")).toBeDefined(); + expect(screen.getByText(/Essentials/)).toBeDefined(); + expect(screen.getByText(/AI Tools/)).toBeDefined(); + expect(screen.getByText(/Optimization/)).toBeDefined(); + }); + + it("filters tools by search query", () => { + render(); + const input = screen.getByPlaceholderText("Search tools..."); + fireEvent.change(input, { target: { value: "resize" } }); + expect(screen.getByText("Resize")).toBeDefined(); + const text = getCountText(); + expect(text).toMatch(/Showing \d+ of 46 tools/); + expect(screen.queryByText("OCR / Text Extraction")).toBeNull(); + }); + + it("filters tools by category", () => { + render(); + const aiButton = screen.getByText(/AI Tools/); + fireEvent.click(aiButton); + const text = getCountText(); + expect(text).toMatch(/Showing 13 of 46 tools/); + expect(screen.getByText("Remove Background")).toBeDefined(); + expect(screen.queryByText("Resize")).toBeNull(); + }); + + it("shows empty state when search matches nothing", () => { + render(); + const input = screen.getByPlaceholderText("Search tools..."); + fireEvent.change(input, { target: { value: "xyznonexistent" } }); + expect(screen.getByText("No tools found. Try a different search.")).toBeDefined(); + const text = getCountText(); + expect(text).toMatch(/Showing 0 of 46 tools/); + }); + + it("combines search and category filter", () => { + render(); + const aiButton = screen.getByText(/AI Tools/); + fireEvent.click(aiButton); + const input = screen.getByPlaceholderText("Search tools..."); + fireEvent.change(input, { target: { value: "background" } }); + expect(screen.getByText("Remove Background")).toBeDefined(); + expect(screen.queryByText("Image Upscaling")).toBeNull(); + }); + + it("clicking All resets category filter", () => { + render(); + fireEvent.click(screen.getByText(/AI Tools/)); + expect(getCountText()).toMatch(/Showing 13 of 46 tools/); + fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (46)")); + expect(getCountText()).toMatch(/Showing 46 of 46 tools/); + }); + + it("renders each tool with name and description", () => { + render(); + expect(screen.getByText("Crop")).toBeDefined(); + expect(screen.getByText("Freeform crop, aspect ratio presets, shape crop")).toBeDefined(); + }); + + it("renders all 8 category pills", () => { + render(); + const categoryNames = [ + "Essentials", + "Optimization", + "Adjustments", + "AI Tools", + "Watermark & Overlay", + "Utilities", + "Layout & Composition", + "Format & Conversion", + ]; + for (const name of categoryNames) { + expect(screen.getByText(new RegExp(name))).toBeDefined(); + } + }); +}); diff --git a/tests/unit/landing/components.test.tsx b/tests/unit/landing/components.test.tsx new file mode 100644 index 00000000..0aab5bb8 --- /dev/null +++ b/tests/unit/landing/components.test.tsx @@ -0,0 +1,261 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import type React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("framer-motion", () => ({ + motion: { + div: ({ children, ...props }: React.PropsWithChildren>) => ( +
{children}
+ ), + span: ({ children, ...props }: React.PropsWithChildren>) => ( + {children} + ), + }, + AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}, +})); + +const fetchMock = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ stargazers_count: 500 }), +}); +vi.stubGlobal("fetch", fetchMock); + +import { Enterprise } from "@landing/components/enterprise"; +import { Footer } from "@landing/components/footer"; +import { Hero } from "@landing/components/hero"; +import { OpenSource } from "@landing/components/open-source"; +import { Pricing } from "@landing/components/pricing"; +import { TypingCursor } from "@landing/components/typing-cursor"; +import { WhyChoose } from "@landing/components/why-choose"; + +afterEach(cleanup); + +// ── Hero ────────────────────────────────────────────────────────────── + +describe("Hero", () => { + it("renders the headline", () => { + render(); + expect(screen.getByText("Your images. Stay yours.")).toBeDefined(); + }); + + it("renders the subheadline", () => { + render(); + expect( + screen.getByText("The open-source, self-hosted image processing platform."), + ).toBeDefined(); + }); + + it("renders the CTA button linking to GitHub", () => { + render(); + const cta = screen.getByText("Get it for free"); + const link = cta.closest("a"); + expect(link?.getAttribute("href")).toBe("https://github.com/ashim-hq/ashim"); + expect(link?.getAttribute("target")).toBe("_blank"); + }); + + it("renders the word cloud (hidden on mobile)", () => { + render(); + expect(screen.getByText("Resize")).toBeDefined(); + expect(screen.getByText("OCR")).toBeDefined(); + expect(screen.getByText("Remove BG")).toBeDefined(); + }); +}); + +// ── TypingCursor ────────────────────────────────────────────────────── + +describe("TypingCursor", () => { + it("renders the first phrase", () => { + render(); + expect(screen.getByText("No signups. No accounts.")).toBeDefined(); + }); + + it("uses a 3-second interval for rotation", () => { + vi.useFakeTimers(); + render(); + const first = screen.getByText("No signups. No accounts."); + expect(first).toBeDefined(); + vi.useRealTimers(); + }); +}); + +// ── WhyChoose ───────────────────────────────────────────────────────── + +describe("WhyChoose", () => { + it("renders the section heading", () => { + render(); + expect(screen.getByText("Built different. On purpose.")).toBeDefined(); + }); + + it("renders all 9 benefit cards", () => { + render(); + const titles = [ + "No Signup", + "No Uploads", + "Forever Free", + "No Limits", + "Batch Processing", + "Lightning Fast", + "Open Source", + "REST API", + "Pipeline Automation", + ]; + for (const title of titles) { + expect(screen.getByText(title)).toBeDefined(); + } + }); + + it("renders descriptions for each card", () => { + render(); + expect(screen.getByText("Start instantly, no accounts or emails required.")).toBeDefined(); + expect(screen.getByText("Chain tools together. Automate your workflows.")).toBeDefined(); + }); +}); + +// ── Enterprise ──────────────────────────────────────────────────────── + +describe("Enterprise", () => { + it("renders the section heading", () => { + render(); + expect(screen.getByText("Your data never leaves your network.")).toBeDefined(); + }); + + it("renders all 3 enterprise feature cards", () => { + render(); + expect(screen.getByText("Data Sovereignty")).toBeDefined(); + expect(screen.getByText("Enterprise Controls")).toBeDefined(); + expect(screen.getByText("Deploy Anywhere")).toBeDefined(); + }); + + it("mentions compliance standards", () => { + render(); + expect(screen.getByText(/GDPR, HIPAA, CCPA/)).toBeDefined(); + }); +}); + +// ── Pricing ─────────────────────────────────────────────────────────── + +describe("Pricing", () => { + it("renders the section heading", () => { + render(); + expect(screen.getByText(/Free for everyone/)).toBeDefined(); + }); + + it("renders both plan names", () => { + render(); + expect(screen.getByText("Open Source")).toBeDefined(); + expect(screen.getByText("Enterprise")).toBeDefined(); + }); + + it("renders Free price", () => { + render(); + expect(screen.getByText("Free")).toBeDefined(); + }); + + it("renders Custom price for enterprise", () => { + render(); + expect(screen.getByText("Custom")).toBeDefined(); + }); + + it("renders free plan features", () => { + render(); + expect(screen.getByText("All 40+ image processing tools")).toBeDefined(); + expect(screen.getByText("Unlimited usage, no hidden caps")).toBeDefined(); + expect(screen.getByText("14 local AI models included")).toBeDefined(); + expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined(); + }); + + it("renders enterprise features", () => { + render(); + expect(screen.getByText("Everything in Open Source")).toBeDefined(); + expect(screen.getByText("Priority email and video support")).toBeDefined(); + expect(screen.getByText("OEM and white-label options")).toBeDefined(); + }); + + it("links free plan CTA to GitHub", () => { + render(); + const freeButton = screen.getByText(/Get Started Free/); + expect(freeButton.closest("a")?.getAttribute("href")).toBe("https://github.com/ashim-hq/ashim"); + }); + + it("links enterprise CTA to contact page", () => { + render(); + const contactButton = screen.getByText(/Contact Sales/); + expect(contactButton.closest("a")?.getAttribute("href")).toBe("/contact"); + }); +}); + +// ── OpenSource ──────────────────────────────────────────────────────── + +describe("OpenSource", () => { + it("renders the heading", () => { + render(); + expect(screen.getByText("Open source. Always.")).toBeDefined(); + }); + + it("renders the description mentioning AGPL-3.0", () => { + render(); + expect(screen.getByText(/AGPL-3.0 licensed/)).toBeDefined(); + }); + + it("links to GitHub", () => { + render(); + const link = screen.getByText("Star on GitHub").closest("a"); + expect(link?.getAttribute("href")).toBe("https://github.com/ashim-hq/ashim"); + }); +}); + +// ── Footer ──────────────────────────────────────────────────────────── + +describe("Footer", () => { + it("renders the brand", () => { + render(