mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(landing): migrate from Next.js to Astro 5
Zero client JS, 165 static pages, self-hosted fonts, Otter Orange design system. Includes tool SEO data for all 157 tools, enterprise page, and updated e2e landing tests.
This commit is contained in:
@@ -7,6 +7,7 @@ test.describe("Heading Hierarchy", () => {
|
||||
{ path: "/faq", name: "FAQ" },
|
||||
{ path: "/privacy", name: "Privacy" },
|
||||
{ path: "/terms", name: "Terms" },
|
||||
{ path: "/enterprise", name: "Enterprise" },
|
||||
];
|
||||
|
||||
for (const { path, name } of pages) {
|
||||
@@ -48,6 +49,15 @@ test.describe("Landmark Elements", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Skip Link", () => {
|
||||
test("skip link targets main content", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const skipLink = page.locator('a.skip-link[href="#main"]');
|
||||
await expect(skipLink).toHaveAttribute("href", "#main");
|
||||
await expect(page.locator("main#main")).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Meta Tags", () => {
|
||||
test("homepage has meta description", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -12,6 +12,7 @@ test.describe("Landing Homepage", () => {
|
||||
test("navbar renders brand and navigation links", async ({ page }) => {
|
||||
await expect(page.getByText("SnapOtter").first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Features" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Enterprise" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Pricing" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Docs" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Contact" }).first()).toBeVisible();
|
||||
@@ -22,55 +23,74 @@ test.describe("Landing Homepage", () => {
|
||||
});
|
||||
|
||||
test("hero section renders headline and subheadline", async ({ page }) => {
|
||||
await expect(page.getByText("Your images. Stay yours.")).toBeVisible();
|
||||
await expect(page.getByText("Every image tool you need.")).toBeVisible();
|
||||
await expect(page.getByText("Your files never leave")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("self-hosted tools for images, video, audio, documents, and data"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("hero CTA links to GitHub", async ({ page }) => {
|
||||
const cta = page.getByRole("link", { name: "Get it for free" });
|
||||
test("hero CTA links to getting started docs", async ({ page }) => {
|
||||
const cta = page.getByRole("link", { name: "Deploy Free" });
|
||||
await expect(cta).toBeVisible();
|
||||
await expect(cta).toHaveAttribute("href", "https://github.com/snapotter-hq/snapotter");
|
||||
await expect(cta).toHaveAttribute("href", "https://docs.snapotter.com/guide/getting-started");
|
||||
await expect(cta).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
||||
test("how-it-works section renders Docker command", async ({ page }) => {
|
||||
await expect(page.getByText("Get started in seconds")).toBeVisible();
|
||||
await expect(page.getByText("docker run -d --name SnapOtter", { exact: false })).toBeVisible();
|
||||
test("hero section renders Docker command", async ({ page }) => {
|
||||
await expect(
|
||||
page.getByText("docker run -d -p 1349:1349 snapotter/snapotter", { exact: false }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("why-choose section renders all 9 benefit cards", async ({ page }) => {
|
||||
await expect(page.getByText("Built different. On purpose.")).toBeVisible();
|
||||
const cards = [
|
||||
"No Signup",
|
||||
"No Uploads",
|
||||
"Forever Free",
|
||||
"No Limits",
|
||||
"Batch Processing",
|
||||
"Lightning Fast",
|
||||
"Open Source",
|
||||
"REST API",
|
||||
"Pipeline Automation",
|
||||
];
|
||||
for (const card of cards) {
|
||||
await expect(page.getByText(card, { exact: true }).first()).toBeVisible();
|
||||
test("trust signals show stats", async ({ page }) => {
|
||||
await expect(page.getByText("Processing Tools")).toBeVisible();
|
||||
await expect(page.getByText("GitHub Stars")).toBeVisible();
|
||||
await expect(page.getByText("Docker Pulls")).toBeVisible();
|
||||
await expect(page.getByText("Languages")).toBeVisible();
|
||||
});
|
||||
|
||||
test("modality explorer renders section heading and cards", async ({ page }) => {
|
||||
await expect(page.getByText("Every file format. One platform.")).toBeVisible();
|
||||
const modalities = ["Image", "Video", "Audio", "Documents", "Data"];
|
||||
for (const mod of modalities) {
|
||||
await expect(page.getByText(mod, { exact: true }).first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("bento grid renders with search and tool count", async ({ page }) => {
|
||||
await expect(page.getByText("50+ tools. Zero cloud dependency.")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("Search tools...")).toBeVisible();
|
||||
await expect(page.getByText(/Showing 53 of 53 tools/)).toBeVisible();
|
||||
test("feature highlights section renders heading and features", async ({ page }) => {
|
||||
await expect(page.getByText("Built for serious infrastructure.")).toBeVisible();
|
||||
await expect(page.getByText("Your files stay on your servers")).toBeVisible();
|
||||
await expect(page.getByText("Full REST API for every tool")).toBeVisible();
|
||||
await expect(page.getByText("15 AI models, all running locally")).toBeVisible();
|
||||
await expect(page.getByText("Deploy once, use everywhere")).toBeVisible();
|
||||
});
|
||||
|
||||
test("enterprise section renders feature cards", async ({ page }) => {
|
||||
await expect(page.getByText("Your data never leaves your network.")).toBeVisible();
|
||||
await expect(page.getByText("Data Sovereignty")).toBeVisible();
|
||||
await expect(page.getByText("Enterprise Controls")).toBeVisible();
|
||||
await expect(page.getByText("Deploy Anywhere")).toBeVisible();
|
||||
await expect(page.getByText("Built for enterprise deployment.")).toBeVisible();
|
||||
const features = [
|
||||
"SAML SSO",
|
||||
"SCIM Provisioning",
|
||||
"Multi-Factor Auth",
|
||||
"Multi-Tenancy",
|
||||
"Per-Tool Permissions",
|
||||
"Audit Export",
|
||||
"S3-Compatible Storage",
|
||||
"Webhooks",
|
||||
];
|
||||
for (const feature of features) {
|
||||
await expect(page.getByText(feature, { exact: true }).first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("how-it-works section renders steps", async ({ page }) => {
|
||||
await expect(page.getByText("Up and running in 60 seconds.")).toBeVisible();
|
||||
await expect(page.getByText("Run a single Docker command", { exact: false })).toBeVisible();
|
||||
await expect(page.getByText("Navigate to localhost:1349", { exact: false })).toBeVisible();
|
||||
await expect(page.getByText("Upload files, pick a tool", { exact: false })).toBeVisible();
|
||||
});
|
||||
|
||||
test("pricing section renders both plans", async ({ page }) => {
|
||||
await expect(page.getByText(/Free for everyone/)).toBeVisible();
|
||||
await expect(page.getByText("Free for everyone. Enterprise when you need it.")).toBeVisible();
|
||||
const openSource = page.getByText("Open Source", { exact: true });
|
||||
await expect(openSource.first()).toBeVisible();
|
||||
await expect(page.getByText("Enterprise", { exact: true }).first()).toBeVisible();
|
||||
@@ -84,6 +104,7 @@ test.describe("Landing Homepage", () => {
|
||||
|
||||
test("footer renders all column titles", async ({ page }) => {
|
||||
await expect(page.getByText("Product", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Solutions", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Resources", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Community", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Legal", { exact: true })).toBeVisible();
|
||||
@@ -91,6 +112,6 @@ test.describe("Landing Homepage", () => {
|
||||
|
||||
test("footer renders copyright with current year", async ({ page }) => {
|
||||
const year = new Date().getFullYear();
|
||||
await expect(page.getByText(new RegExp(String(year)))).toBeVisible();
|
||||
await expect(page.getByText(new RegExp(`${year}.*Chocolate Wafers`))).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,50 +1,9 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("BentoGrid Interactions", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("search filters tools by name", async ({ page }) => {
|
||||
const input = page.getByPlaceholder("Search tools...");
|
||||
await input.fill("resize");
|
||||
await expect(page.getByText("Resize", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText(/Showing \d+ of 53 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("category pill filters tools", async ({ page }) => {
|
||||
await page.getByText(/AI Tools/).click();
|
||||
await expect(page.getByText(/Showing 16 of 53 tools/)).toBeVisible();
|
||||
await expect(page.getByText("Remove Background")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking All resets category filter", async ({ page }) => {
|
||||
await page.getByText(/AI Tools/).click();
|
||||
await expect(page.getByText(/Showing 16 of 53 tools/)).toBeVisible();
|
||||
|
||||
await page.getByText(/All \(53\)/).click();
|
||||
await expect(page.getByText(/Showing 53 of 53 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("search with no results shows empty state", async ({ page }) => {
|
||||
const input = page.getByPlaceholder("Search tools...");
|
||||
await input.fill("xyznonexistent");
|
||||
await expect(page.getByText("No tools found. Try a different search.")).toBeVisible();
|
||||
await expect(page.getByText(/Showing 0 of 53 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("combined search and category filter works", async ({ page }) => {
|
||||
await page.getByText(/AI Tools/).click();
|
||||
const input = page.getByPlaceholder("Search tools...");
|
||||
await input.fill("background");
|
||||
await expect(page.getByText("Remove Background")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("HowItWorks Copy Button", () => {
|
||||
test.describe("Docker Command Copy Button", () => {
|
||||
test("copy button shows Copied! feedback", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const copyButton = page.locator("button").filter({ hasText: "docker run" });
|
||||
const copyButton = page.locator("button[data-copy]");
|
||||
await copyButton.click();
|
||||
await expect(page.getByText("Copied!")).toBeVisible();
|
||||
});
|
||||
@@ -94,6 +53,7 @@ test.describe("Mobile Navigation", () => {
|
||||
const toggle = page.getByLabel("Toggle menu");
|
||||
await toggle.click();
|
||||
await expect(page.getByRole("link", { name: "Features" }).last()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Enterprise" }).last()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Pricing" }).last()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Docs" }).last()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Contact" }).last()).toBeVisible();
|
||||
|
||||
@@ -41,6 +41,7 @@ test.describe("Privacy Page", () => {
|
||||
|
||||
test("renders all section headings", async ({ page }) => {
|
||||
const headings = [
|
||||
"Data Controller",
|
||||
"Overview",
|
||||
"Website (snapotter.com)",
|
||||
"Self-Hosted Software",
|
||||
@@ -72,6 +73,7 @@ test.describe("Terms Page", () => {
|
||||
|
||||
test("renders all section headings", async ({ page }) => {
|
||||
const headings = [
|
||||
"Who We Are",
|
||||
"Overview",
|
||||
"Software License",
|
||||
"Website Use",
|
||||
@@ -92,16 +94,16 @@ test.describe("Terms Page", () => {
|
||||
});
|
||||
|
||||
test.describe("Cross-Page Navigation", () => {
|
||||
test("footer Privacy Policy link navigates to /privacy", async ({ page }) => {
|
||||
test("footer Privacy link navigates to /privacy", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Privacy Policy" }).click();
|
||||
await page.locator("footer").getByRole("link", { name: "Privacy" }).click();
|
||||
await expect(page).toHaveURL("/privacy");
|
||||
await expect(page.getByText("Privacy Policy").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("footer Terms link navigates to /terms", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Terms and Conditions" }).click();
|
||||
await page.locator("footer").getByRole("link", { name: "Terms" }).click();
|
||||
await expect(page).toHaveURL("/terms");
|
||||
await expect(page.getByText("Terms and Conditions").first()).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user