mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Future-proofs marketing text so it doesn't need updating every time a tool is added.
264 lines
9.3 KiB
TypeScript
264 lines
9.3 KiB
TypeScript
// @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<Record<string, unknown>>) => (
|
|
<div {...props}>{children}</div>
|
|
),
|
|
span: ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => (
|
|
<span {...props}>{children}</span>
|
|
),
|
|
},
|
|
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(<Hero />);
|
|
expect(screen.getByText("Your images. Stay yours.")).toBeDefined();
|
|
});
|
|
|
|
it("renders the subheadline", () => {
|
|
render(<Hero />);
|
|
expect(screen.getByText(/free, open-source image toolkit/)).toBeDefined();
|
|
});
|
|
|
|
it("renders the CTA button linking to GitHub", () => {
|
|
render(<Hero />);
|
|
const cta = screen.getByText("Get it for free");
|
|
const link = cta.closest("a");
|
|
expect(link?.getAttribute("href")).toBe("https://github.com/snapotter-hq/snapotter");
|
|
expect(link?.getAttribute("target")).toBe("_blank");
|
|
});
|
|
|
|
it("renders the word cloud (hidden on mobile)", () => {
|
|
render(<Hero />);
|
|
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(<TypingCursor />);
|
|
expect(screen.getByText("No signups. No accounts.")).toBeDefined();
|
|
});
|
|
|
|
it("uses a 3-second interval for rotation", () => {
|
|
vi.useFakeTimers();
|
|
render(<TypingCursor />);
|
|
const first = screen.getByText("No signups. No accounts.");
|
|
expect(first).toBeDefined();
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
// ── WhyChoose ─────────────────────────────────────────────────────────
|
|
|
|
describe("WhyChoose", () => {
|
|
it("renders the section heading", () => {
|
|
render(<WhyChoose />);
|
|
expect(screen.getByText("Built different. On purpose.")).toBeDefined();
|
|
});
|
|
|
|
it("renders all 9 benefit cards", () => {
|
|
render(<WhyChoose />);
|
|
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(<WhyChoose />);
|
|
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(<Enterprise />);
|
|
expect(screen.getByText("Your data never leaves your network.")).toBeDefined();
|
|
});
|
|
|
|
it("renders all 3 enterprise feature cards", () => {
|
|
render(<Enterprise />);
|
|
expect(screen.getByText("Data Sovereignty")).toBeDefined();
|
|
expect(screen.getByText("Enterprise Controls")).toBeDefined();
|
|
expect(screen.getByText("Deploy Anywhere")).toBeDefined();
|
|
});
|
|
|
|
it("mentions compliance standards", () => {
|
|
render(<Enterprise />);
|
|
expect(screen.getByText(/GDPR, HIPAA, CCPA/)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ── Pricing ───────────────────────────────────────────────────────────
|
|
|
|
describe("Pricing", () => {
|
|
it("renders the section heading", () => {
|
|
render(<Pricing />);
|
|
expect(screen.getByText(/Free for everyone/)).toBeDefined();
|
|
});
|
|
|
|
it("renders both plan names", () => {
|
|
render(<Pricing />);
|
|
expect(screen.getByText("Open Source")).toBeDefined();
|
|
expect(screen.getByText("Enterprise")).toBeDefined();
|
|
});
|
|
|
|
it("renders Free price", () => {
|
|
render(<Pricing />);
|
|
expect(screen.getByText("Free")).toBeDefined();
|
|
});
|
|
|
|
it("renders Custom price for enterprise", () => {
|
|
render(<Pricing />);
|
|
expect(screen.getByText("Custom")).toBeDefined();
|
|
});
|
|
|
|
it("renders free plan features", () => {
|
|
render(<Pricing />);
|
|
expect(screen.getByText("All 50+ image processing tools")).toBeDefined();
|
|
expect(screen.getByText("Unlimited usage, no hidden caps")).toBeDefined();
|
|
expect(screen.getByText("15 local AI models included")).toBeDefined();
|
|
expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined();
|
|
});
|
|
|
|
it("renders enterprise features", () => {
|
|
render(<Pricing />);
|
|
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(<Pricing />);
|
|
const freeButton = screen.getByText(/Get Started Free/);
|
|
expect(freeButton.closest("a")?.getAttribute("href")).toBe(
|
|
"https://github.com/snapotter-hq/snapotter",
|
|
);
|
|
});
|
|
|
|
it("links enterprise CTA to contact page", () => {
|
|
render(<Pricing />);
|
|
const contactButton = screen.getByText(/Contact Sales/);
|
|
expect(contactButton.closest("a")?.getAttribute("href")).toBe("/contact");
|
|
});
|
|
});
|
|
|
|
// ── OpenSource ────────────────────────────────────────────────────────
|
|
|
|
describe("OpenSource", () => {
|
|
it("renders the heading", () => {
|
|
render(<OpenSource />);
|
|
expect(screen.getByText("Open source. Always.")).toBeDefined();
|
|
});
|
|
|
|
it("renders the description mentioning AGPL-3.0", () => {
|
|
render(<OpenSource />);
|
|
expect(screen.getByText(/AGPL-3.0 licensed/)).toBeDefined();
|
|
});
|
|
|
|
it("links to GitHub", () => {
|
|
render(<OpenSource />);
|
|
const link = screen.getByText("Star on GitHub").closest("a");
|
|
expect(link?.getAttribute("href")).toBe("https://github.com/snapotter-hq/snapotter");
|
|
});
|
|
});
|
|
|
|
// ── Footer ────────────────────────────────────────────────────────────
|
|
|
|
describe("Footer", () => {
|
|
it("renders the brand", () => {
|
|
render(<Footer />);
|
|
expect(screen.getByText("SnapOtter")).toBeDefined();
|
|
});
|
|
|
|
it("renders all four column titles", () => {
|
|
render(<Footer />);
|
|
expect(screen.getByText("Product")).toBeDefined();
|
|
expect(screen.getByText("Resources")).toBeDefined();
|
|
expect(screen.getByText("Community")).toBeDefined();
|
|
expect(screen.getByText("Legal")).toBeDefined();
|
|
});
|
|
|
|
it("renders product links", () => {
|
|
render(<Footer />);
|
|
expect(screen.getByText("Features")).toBeDefined();
|
|
expect(screen.getByText("Pricing")).toBeDefined();
|
|
});
|
|
|
|
it("renders community links to GitHub", () => {
|
|
render(<Footer />);
|
|
const ghLink = screen.getByText("GitHub");
|
|
expect(ghLink.closest("a")?.getAttribute("href")).toBe(
|
|
"https://github.com/snapotter-hq/snapotter",
|
|
);
|
|
});
|
|
|
|
it("renders legal links", () => {
|
|
render(<Footer />);
|
|
expect(screen.getByText("Terms and Conditions")).toBeDefined();
|
|
expect(screen.getByText("Privacy Policy")).toBeDefined();
|
|
expect(screen.getByText("FAQ")).toBeDefined();
|
|
});
|
|
|
|
it("renders the copyright notice with current year", () => {
|
|
render(<Footer />);
|
|
const year = new Date().getFullYear();
|
|
expect(screen.getByText(new RegExp(`${year}`))).toBeDefined();
|
|
});
|
|
|
|
it("opens external links in new tab", () => {
|
|
render(<Footer />);
|
|
const docsLink = screen.getByText("Documentation").closest("a");
|
|
expect(docsLink?.getAttribute("target")).toBe("_blank");
|
|
expect(docsLink?.getAttribute("rel")).toBe("noopener noreferrer");
|
|
});
|
|
|
|
it("keeps internal links without target=_blank", () => {
|
|
render(<Footer />);
|
|
const faqLink = screen.getByText("FAQ").closest("a");
|
|
expect(faqLink?.getAttribute("target")).toBeNull();
|
|
});
|
|
});
|