-
- Copyright © Jeel Dobariya 2025โ2026. All rights
- reserved.
-
+
+
+ Copyright © Jeel Dobariya 2025โ2026. All rights
+ reserved.
+
+
+ Design by{" "}
+
+ @harsha-vardhan-burra
+
+
+
Crafted for privacy and local control.
diff --git a/src/components/layout/Logo.tsx b/src/components/layout/Logo.tsx
index ef1c164..acaf4a8 100644
--- a/src/components/layout/Logo.tsx
+++ b/src/components/layout/Logo.tsx
@@ -2,10 +2,10 @@
import { useState } from "react";
import { Lock } from "lucide-react";
-import { LOGO_SRC } from "@/lib/constants";
+import { LOGO_SRC, LOGO_WEBP } from "@/lib/constants";
import { cn } from "@/lib/utils";
-/** Real logo with a graceful icon fallback (never shows a broken image). */
+/** Real logo with modern WebP negotiation and graceful icon fallback. */
export function Logo({
className = "h-10 w-10 rounded-xl",
}: {
@@ -28,12 +28,19 @@ export function Logo({
}
return (
- // eslint-disable-next-line @next/next/no-img-element
-
setFailed(true)}
- />
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ setFailed(true)}
+ />
+
);
}
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
index a99c23d..eab446c 100644
--- a/src/components/layout/Navbar.tsx
+++ b/src/components/layout/Navbar.tsx
@@ -80,7 +80,7 @@ export function Navbar() {
Get App
diff --git a/src/data/normalized-releases.json b/src/data/normalized-releases.json
index 86d0557..3a8a49f 100644
--- a/src/data/normalized-releases.json
+++ b/src/data/normalized-releases.json
@@ -48,8 +48,9 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v3.2.0...v3.2.1",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": false,
"assetsCount": 5,
- "downloadCount": 24
+ "downloadCount": 31
},
{
"slug": "v3-2-0-stable",
@@ -95,6 +96,7 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v3.1.0...v3.2.0",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": false,
"assetsCount": 5,
"downloadCount": 69
},
@@ -151,6 +153,7 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v3.0.0...v3.1.0",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": false,
"assetsCount": 5,
"downloadCount": 67
},
@@ -189,6 +192,7 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v2.1.1...v3.0.0",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": false,
"assetsCount": 5,
"downloadCount": 65
},
@@ -259,6 +263,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v2.1.0...v2.1.1",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 37
},
@@ -320,6 +326,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v2.0.0...v2.1.0",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 40
},
@@ -387,6 +395,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v1.2.1...v2.0.0",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 42
},
@@ -422,6 +432,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v1.2.0...v1.2.1",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 48
},
@@ -465,6 +477,8 @@
"isMajor": false,
"isYanked": true,
"yankedReason": "Yanked release: version name was mistakenly kept as v1.1.2-Alpha during release packaging. Superseded immediately by v1.2.1.",
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 13
},
@@ -519,6 +533,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v1.1.1...v1.1.2",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 49
},
@@ -554,6 +570,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v1.1.0...v1.1.1",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 56
},
@@ -608,6 +626,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v1.0.0...v1.1.0",
"isMajor": false,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 31
},
@@ -657,6 +677,8 @@
"compareUrl": "https://github.com/PasscodesApp/Passcodes/compare/v0.1.0...v1.0.0",
"isMajor": true,
"isYanked": false,
+ "isDeprecated": true,
+ "deprecatedReason": "Superseded by the newer database architecture introduced in later releases.",
"assetsCount": 6,
"downloadCount": 66
},
@@ -706,6 +728,7 @@
"isMajor": true,
"isYanked": true,
"yankedReason": "Yanked prototype release: very early premature prototype published under com.passwordmanager. Kept strictly for documentation and project history.",
+ "isDeprecated": false,
"assetsCount": 6,
"downloadCount": 98
}
diff --git a/src/data/raw-releases.json b/src/data/raw-releases.json
index 22f7acc..c590da0 100644
--- a/src/data/raw-releases.json
+++ b/src/data/raw-releases.json
@@ -68,7 +68,7 @@
"state": "uploaded",
"size": 35922426,
"digest": "sha256:b8d20f36f9b87dcc31e5ca10ab4b0d1db7f485cf4907bfc39f53e811daa997b8",
- "download_count": 0,
+ "download_count": 1,
"created_at": "2026-08-02T14:54:35Z",
"updated_at": "2026-08-02T15:02:05Z",
"browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-x86_64-app.apk"
@@ -140,7 +140,7 @@
"state": "uploaded",
"size": 97593756,
"digest": "sha256:bf839ba66057169219d13c9c9fc883548f4eda26075a0df9311f6840f870d6fc",
- "download_count": 0,
+ "download_count": 2,
"created_at": "2026-08-02T14:57:32Z",
"updated_at": "2026-08-02T15:02:05Z",
"browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-universal-app.apk"
@@ -176,7 +176,7 @@
"state": "uploaded",
"size": 28447625,
"digest": "sha256:110e35124036cd19002f3d838f0b559e3840c4b5fd8ec4a087e9fd1862174411",
- "download_count": 0,
+ "download_count": 1,
"created_at": "2026-08-02T15:00:28Z",
"updated_at": "2026-08-02T15:02:05Z",
"browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-armeabi_v7a-app.apk"
@@ -212,7 +212,7 @@
"state": "uploaded",
"size": 35428199,
"digest": "sha256:1db1e993f9421007bab6601e16453d90883384608345be3bff351aefc2db7f96",
- "download_count": 24,
+ "download_count": 27,
"created_at": "2026-08-02T15:00:58Z",
"updated_at": "2026-08-02T15:02:05Z",
"browser_download_url": "https://github.com/PasscodesApp/Passcodes/releases/download/v3.2.1/v3.2.1-passcodes-arm64_v8a-app.apk"
diff --git a/src/hooks/useGithubRelease.ts b/src/hooks/useGithubRelease.ts
index b2fb359..685aff4 100644
--- a/src/hooks/useGithubRelease.ts
+++ b/src/hooks/useGithubRelease.ts
@@ -7,6 +7,7 @@ import {
getLatestStableRelease,
classifyRelease,
isYankedRelease,
+ isDeprecatedRelease,
type ReleaseChannel,
GithubRateLimitError,
} from "@/lib/github";
@@ -47,4 +48,4 @@ export function isRateLimitError(error: Error | null): boolean {
return error instanceof GithubRateLimitError;
}
-export { getLatestStableRelease, classifyRelease, isYankedRelease, type ReleaseChannel };
+export { getLatestStableRelease, classifyRelease, isYankedRelease, isDeprecatedRelease, type ReleaseChannel };
diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts
index 215d781..e8b484b 100644
--- a/src/lib/changelog.ts
+++ b/src/lib/changelog.ts
@@ -39,6 +39,8 @@ export interface ChangelogEntry {
isMajor?: boolean;
isYanked?: boolean;
yankedReason?: string;
+ isDeprecated?: boolean;
+ deprecatedReason?: string;
isMilestone?: boolean;
assetsCount?: number;
downloadCount?: number;
@@ -90,3 +92,17 @@ export function getAdjacentEntries(currentSlug: string): {
next: index > 0 ? CHANGELOG_ENTRIES[index - 1] : null,
};
}
+
+/**
+ * Check if a changelog entry is deprecated.
+ * v1.x and v2.x releases are deprecated (superseded by newer database architecture).
+ */
+export function isDeprecatedEntry(entry: ChangelogEntry): boolean {
+ if (typeof entry.isDeprecated === "boolean") {
+ return entry.isDeprecated;
+ }
+ const match = entry.version.match(/^v?(\d+)\./i);
+ if (!match) return false;
+ const major = parseInt(match[1], 10);
+ return major === 1 || major === 2;
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 192edea..94f953b 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -46,7 +46,9 @@ export const DOCS_RELEASE_NOTES_URL =
"https://passcodesapp.github.io/Passcodes-Docs/user-docs/release-notes/";
export const CONTACT_EMAIL = "jeeldobariya38@gmail.com";
export const LOGO_SRC = `${process.env.BASE_PATH || ""}/logo.png`;
+export const LOGO_WEBP = `${process.env.BASE_PATH || ""}/logo.webp`;
export const KOMI_BADGE_SRC = `${process.env.BASE_PATH || ""}/komi-store-badge.png`;
+export const KOMI_BADGE_WEBP = `${process.env.BASE_PATH || ""}/komi-store-badge.webp`;
export const DISCORD_URL = "https://discord.gg/kSSkYq7KAQ";
export const TELEGRAM_URL = "https://t.me/passcodescommunity";
export const KOMI_STORE_URL =
diff --git a/src/lib/contributors.ts b/src/lib/contributors.ts
index dee056d..7db3c5e 100644
--- a/src/lib/contributors.ts
+++ b/src/lib/contributors.ts
@@ -42,6 +42,13 @@ export const FEATURED_CONTRIBUTORS: ContributorProfile[] = [
github: "https://github.com/kudanilll",
avatar: "https://github.com/kudanilll.png",
},
+ {
+ name: "A Guy",
+ role: "Community Manager",
+ login: "I-A-GUY",
+ github: "https://github.com/I-A-GUY",
+ avatar: "https://github.com/I-A-GUY.png",
+ },
{
name: "hexCode",
role: "Contributor",
diff --git a/src/lib/github.ts b/src/lib/github.ts
index f6fb979..248ef9f 100644
--- a/src/lib/github.ts
+++ b/src/lib/github.ts
@@ -88,6 +88,18 @@ export function isYankedRelease(release: GithubRelease): boolean {
return text.includes("yanked");
}
+/**
+ * Check if a release is deprecated.
+ * v1.x and v2.x releases are deprecated (superseded by newer database architecture).
+ */
+export function isDeprecatedRelease(release: GithubRelease): boolean {
+ const text = release.tag_name || release.name || "";
+ const match = text.match(/v?(\d+)\./i);
+ if (!match) return false;
+ const major = parseInt(match[1], 10);
+ return major === 1 || major === 2;
+}
+
/**
* Classify a release into Stable, Beta, or Alpha channels based on metadata and naming conventions.
* Primary classification:
@@ -127,7 +139,7 @@ export function classifyRelease(release: GithubRelease): ReleaseChannel {
* Find the latest stable release from a list of releases.
* Follows GitHub's /releases/latest semantics:
* - not a draft
- * - classified as stable (not prerelease, not alpha/beta, not yanked)
+ * - classified as stable (not prerelease, not alpha/beta, not yanked, not deprecated)
* - newest according to created_at
* Pure function that does not mutate the source array.
*/
@@ -135,7 +147,7 @@ export function getLatestStableRelease(
releases: GithubRelease[]
): GithubRelease | undefined {
return releases.reduce
((latest, release) => {
- if (release.draft || isYankedRelease(release)) return latest;
+ if (release.draft || isYankedRelease(release) || isDeprecatedRelease(release)) return latest;
if (classifyRelease(release) !== "stable") return latest;
if (!latest) return release;
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 080493e..f63c23a 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -20,7 +20,7 @@
/* Typography Colors (Slate Precision) */
--text: #f8fafc;
--text-muted: #94a3b8;
- --text-dim: #64748b;
+ --text-dim: #8295ab;
/* Refined Sapphire / Azure Accent (Dark Mode) */
--primary: #3b82f6;
@@ -85,7 +85,7 @@ html.light {
--text: #0f172a;
--text-muted: #475569;
- --text-dim: #94a3b8;
+ --text-dim: #54667a;
/* Refined Sapphire / Azure Accent (Light Mode) */
--primary: #2563eb;
@@ -178,18 +178,16 @@ html.light {
line-height: 1.6;
}
-/* Route transition (Tier 1/2: fast, subtle 4px) */
+/* Route transition (Restrained, opacity-only to prevent layout shifts) */
.page-enter {
animation: pageEnter 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes pageEnter {
from {
opacity: 0.96;
- transform: translate3d(0, 4px, 0);
}
to {
opacity: 1;
- transform: translate3d(0, 0, 0);
}
}
@@ -482,6 +480,8 @@ html.light .active-nav {
/* ============================= */
.store-section {
max-width: 680px;
+ min-height: 180px;
+ contain: layout;
margin: 0 auto 2rem;
padding: 1.75rem;
text-align: center;
@@ -516,7 +516,9 @@ html.light .active-nav {
}
.store-badge img {
max-width: 190px;
+ width: 100%;
height: auto;
+ aspect-ratio: 574 / 287;
display: block;
}
@@ -604,6 +606,31 @@ html.light .active-nav {
var(--card-bg) 100%
);
}
+.release-card.deprecated {
+ border: 1px solid rgba(239, 68, 68, 0.42);
+ background: linear-gradient(
+ 180deg,
+ rgba(239, 68, 68, 0.045) 0%,
+ var(--card-bg) 100%
+ );
+ opacity: 0.94;
+}
+.release-card.deprecated:hover {
+ border-color: rgba(239, 68, 68, 0.65);
+ box-shadow: 0 4px 16px rgba(239, 68, 68, 0.12);
+}
+html.light .release-card.deprecated {
+ border: 1px solid rgba(220, 38, 38, 0.38);
+ background: linear-gradient(
+ 180deg,
+ rgba(220, 38, 38, 0.035) 0%,
+ var(--card-bg) 100%
+ );
+}
+html.light .release-card.deprecated:hover {
+ border-color: rgba(220, 38, 38, 0.58);
+ box-shadow: 0 4px 16px rgba(220, 38, 38, 0.1);
+}
.release-top {
display: flex;
@@ -663,6 +690,33 @@ html.light .active-nav {
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
+.tag.deprecated {
+ background: rgba(239, 68, 68, 0.14);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ color: #f87171;
+}
+
+/* Light mode high-contrast tag tuning */
+html.light .tag.stable {
+ background: rgba(22, 163, 74, 0.1);
+ border-color: rgba(22, 163, 74, 0.28);
+ color: #15803d;
+}
+html.light .tag.beta {
+ background: rgba(217, 119, 6, 0.1);
+ border-color: rgba(217, 119, 6, 0.28);
+ color: #b45309;
+}
+html.light .tag.alpha {
+ background: rgba(220, 38, 38, 0.1);
+ border-color: rgba(220, 38, 38, 0.28);
+ color: #b91c1c;
+}
+html.light .tag.deprecated {
+ background: rgba(220, 38, 38, 0.1);
+ border-color: rgba(220, 38, 38, 0.32);
+ color: #b91c1c;
+}
/* Light mode high-contrast tag tuning */
html.light .tag.stable {
diff --git a/tests/fixtures/base-test.ts b/tests/fixtures/base-test.ts
new file mode 100644
index 0000000..0226abd
--- /dev/null
+++ b/tests/fixtures/base-test.ts
@@ -0,0 +1,75 @@
+import { test as base, expect } from "@playwright/test";
+import rawReleases from "../../src/data/raw-releases.json";
+
+/**
+ * Custom test fixture that:
+ * 1. Intercepts external GitHub API requests and responds with local snapshots (deterministic, offline).
+ * 2. Monitors uncaught page errors and console.error events, failing tests if fatal JS crashes occur.
+ */
+export const test = base.extend({
+ page: async ({ page }, use) => {
+ const pageErrors: Error[] = [];
+ const consoleErrors: string[] = [];
+
+ page.on("pageerror", (err) => {
+ pageErrors.push(err);
+ });
+
+ page.on("console", (msg) => {
+ if (msg.type() === "error") {
+ const text = msg.text();
+ // Filter out non-actionable external icon 404 noise if any
+ if (!text.includes("favicon.ico")) {
+ consoleErrors.push(text);
+ }
+ }
+ });
+
+ // Deterministic GitHub API routing with local repository snapshots
+ await page.route("https://api.github.com/**", async (route) => {
+ const url = route.request().url();
+ if (url.includes("/releases")) {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(rawReleases),
+ });
+ } else if (url.includes("/contributors")) {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify([]),
+ });
+ } else if (url.includes("/repos/PasscodesApp/Passcodes")) {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ stargazers_count: 150,
+ forks_count: 20,
+ }),
+ });
+ } else {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({}),
+ });
+ }
+ });
+
+ await use(page);
+
+ expect(
+ pageErrors,
+ `Uncaught page errors detected: ${pageErrors.map((e) => e.message).join("; ")}`
+ ).toHaveLength(0);
+
+ expect(
+ consoleErrors,
+ `Console errors detected: ${consoleErrors.join("; ")}`
+ ).toHaveLength(0);
+ },
+});
+
+export { expect };
diff --git a/tests/functional/changelog-search.spec.ts b/tests/functional/changelog-search.spec.ts
new file mode 100644
index 0000000..e9ca501
--- /dev/null
+++ b/tests/functional/changelog-search.spec.ts
@@ -0,0 +1,62 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Changelog Search Functional Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/changelog/");
+ });
+
+ test("searching by version filters changelog entries", async ({ page }) => {
+ const searchInput = page.getByLabel("Search changelog entries");
+ await expect(searchInput).toBeVisible();
+
+ // Search for specific version
+ await searchInput.fill("v3.2.1");
+
+ // The matching article should be visible
+ const matchingArticles = page.locator("article").filter({ hasText: /v3\.2\.1/i });
+ await expect(matchingArticles.first()).toBeVisible();
+
+ // Articles should be filtered down
+ const totalVisible = await page.locator("article").count();
+ expect(totalVisible).toBeLessThanOrEqual(2);
+ });
+
+ test("searching by keyword filters results and clear button restores list", async ({ page }) => {
+ const searchInput = page.getByLabel("Search changelog entries");
+
+ // Search for a keyword like "Biometric" or "Material"
+ await searchInput.fill("Material");
+
+ const matchingArticles = page.locator("article").filter({ hasText: /Material/i });
+ await expect(matchingArticles.first()).toBeVisible();
+
+ // Exactly one clear button appears
+ const clearBtn = page.getByRole("button", { name: "Clear search" });
+ await expect(clearBtn).toBeVisible();
+ await expect(clearBtn).toHaveCount(1);
+
+ // Click clear and ensure search is reset
+ await clearBtn.click();
+ await expect(searchInput).toHaveValue("");
+
+ // All entries restored
+ const restoredCount = await page.locator("article").count();
+ expect(restoredCount).toBeGreaterThan(5);
+ });
+
+ test("no results message appears on unmatched query and reset works", async ({ page }) => {
+ const searchInput = page.getByLabel("Search changelog entries");
+ await searchInput.fill("nonexistent-release-query-xyz");
+
+ // "No updates match your filter"
+ await expect(page.getByText(/No updates match your filter/i)).toBeVisible();
+
+ // Reset Filters button restores list
+ const resetBtn = page.getByRole("button", { name: /Reset Filters/i });
+ await expect(resetBtn).toBeVisible();
+ await resetBtn.click();
+
+ await expect(searchInput).toHaveValue("");
+ await expect(page.locator("article").first()).toBeVisible();
+ });
+});
diff --git a/tests/functional/release-filters.spec.ts b/tests/functional/release-filters.spec.ts
new file mode 100644
index 0000000..02904fd
--- /dev/null
+++ b/tests/functional/release-filters.spec.ts
@@ -0,0 +1,70 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Downloads Channel Filter & Search Functional Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/downloads/");
+ // Wait for release cards to be rendered
+ await expect(page.locator(".release-card").first()).toBeVisible();
+ });
+
+ test("release channel filter buttons update the visible release list", async ({ page }) => {
+ const stableFilterBtn = page.getByRole("button", { name: "Stable", exact: true });
+ const betaFilterBtn = page.getByRole("button", { name: "Beta", exact: true });
+ const alphaFilterBtn = page.getByRole("button", { name: "Alpha", exact: true });
+ const allFilterBtn = page.getByRole("button", { name: "All Releases", exact: true });
+
+ const archiveCards = page.locator(".release-card:not(.latest)");
+
+ // Filter by Stable
+ await stableFilterBtn.click();
+ await expect(stableFilterBtn).toHaveClass(/active/);
+ await expect(archiveCards.first()).toBeVisible();
+
+ // Stable releases should include v3.2.1
+ await expect(archiveCards.filter({ hasText: /v3\.2\.1/i })).toBeVisible();
+ // Should not show Beta releases in stable filter
+ await expect(archiveCards.locator(".tag.beta")).toHaveCount(0);
+
+ // Filter by Beta
+ await betaFilterBtn.click();
+ await expect(betaFilterBtn).toHaveClass(/active/);
+ await expect(archiveCards.first()).toBeVisible();
+ await expect(archiveCards.locator(".tag.beta").first()).toBeVisible();
+ await expect(archiveCards.locator(".tag.stable")).toHaveCount(0);
+
+ // Filter by Alpha
+ await alphaFilterBtn.click();
+ await expect(alphaFilterBtn).toHaveClass(/active/);
+ await expect(archiveCards.first()).toBeVisible();
+ await expect(archiveCards.locator(".tag.alpha").first()).toBeVisible();
+ await expect(archiveCards.locator(".tag.stable")).toHaveCount(0);
+
+ // Reset to All
+ await allFilterBtn.click();
+ await expect(allFilterBtn).toHaveClass(/active/);
+ await expect(archiveCards.count()).resolves.toBeGreaterThan(5);
+ });
+
+ test("downloads search filters releases and clear button restores list", async ({ page }) => {
+ const searchInput = page.getByLabel("Search releases");
+ await expect(searchInput).toBeVisible();
+
+ // Enter search term
+ await searchInput.fill("v3.2.1");
+
+ // Release list should filter to v3.2.1
+ const archiveCards = page.locator(".release-card:not(.latest)");
+ await expect(archiveCards).toHaveCount(1);
+ await expect(archiveCards.first()).toContainText("v3.2.1");
+
+ // Clear button should be visible (only one clear button inside search input container)
+ const clearBtn = page.getByRole("button", { name: "Clear search" });
+ await expect(clearBtn).toBeVisible();
+ await expect(clearBtn).toHaveCount(1);
+
+ // Click clear and verify list is restored
+ await clearBtn.click();
+ await expect(searchInput).toHaveValue("");
+ await expect(archiveCards.count()).resolves.toBeGreaterThan(1);
+ });
+});
diff --git a/tests/functional/theme.spec.ts b/tests/functional/theme.spec.ts
new file mode 100644
index 0000000..4cb5ff1
--- /dev/null
+++ b/tests/functional/theme.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Theme Toggle Functional Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/");
+ });
+
+ test("theme toggle switches between dark and light modes and updates DOM", async ({ page }) => {
+ // Visible theme button (handles desktop vs mobile header)
+ const themeBtn = page.getByRole("button", { name: /Switch to (light|dark) mode/i }).and(page.locator(":visible"));
+ await expect(themeBtn).toBeVisible();
+
+ const initialLabel = await themeBtn.getAttribute("aria-label");
+ const isInitialDark = initialLabel?.includes("light");
+
+ // Toggle theme
+ await themeBtn.click();
+
+ // Verify HTML element received appropriate class
+ const html = page.locator("html");
+ if (isInitialDark) {
+ await expect(html).toHaveClass(/light/);
+ await expect(themeBtn).toHaveAttribute("aria-label", /Switch to dark mode/i);
+ } else {
+ await expect(html).toHaveClass(/dark/);
+ await expect(themeBtn).toHaveAttribute("aria-label", /Switch to light mode/i);
+ }
+
+ // Toggle back
+ await themeBtn.click();
+ if (isInitialDark) {
+ await expect(html).toHaveClass(/dark/);
+ } else {
+ await expect(html).toHaveClass(/light/);
+ }
+ });
+
+ test("theme preference persists across reload", async ({ page }) => {
+ const themeBtn = page.getByRole("button", { name: /Switch to (light|dark) mode/i }).and(page.locator(":visible"));
+ await themeBtn.click();
+
+ // Reload page
+ await page.reload();
+
+ // Ensure page is still functional
+ await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
+ });
+});
diff --git a/tests/smoke/changelog.spec.ts b/tests/smoke/changelog.spec.ts
new file mode 100644
index 0000000..f3085b7
--- /dev/null
+++ b/tests/smoke/changelog.spec.ts
@@ -0,0 +1,60 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Changelog Smoke & Detail Page Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/changelog/");
+ });
+
+ test("changelog index loads and displays timeline stream", async ({ page }) => {
+ await expect(page).toHaveTitle(/Passcodes/i);
+ await expect(page.getByRole("heading", { name: "Release Highlights & Milestones", level: 1 })).toBeVisible();
+
+ // Articles/timeline entries render
+ const articles = page.locator("article");
+ await expect(articles.first()).toBeVisible();
+
+ // Latest release (v3.2.1) is present
+ await expect(page.getByText("v3.2.1").first()).toBeVisible();
+ });
+
+ test("historical releases display deprecated and yanked indicators appropriately", async ({ page }) => {
+ // v2.1.1 should have Deprecated tag
+ const v2Entry = page.locator("article").filter({ hasText: /v2\.1\.1/i }).first();
+ await expect(v2Entry).toBeVisible();
+ await expect(v2Entry.locator(".tag.deprecated")).toBeVisible();
+
+ // v1.2.0 should display Yanked tag and Deprecated tag
+ const v120Entry = page.locator("article").filter({ has: page.getByRole("heading", { name: /v1\.2\.0/i }) }).first();
+ await expect(v120Entry).toBeVisible();
+ await expect(v120Entry.locator(".tag.deprecated")).toBeVisible();
+ await expect(v120Entry.locator(".tag").filter({ hasText: /yanked/i })).toBeVisible();
+ });
+
+ test("release detail page loads with metadata and navigates back", async ({ page }) => {
+ // Navigate to v3.2.1 detail page
+ await page.goto("/changelog/v3-2-1-stable/");
+
+ await expect(page).toHaveTitle(/v3\.2\.1.*Passcodes Changelog/i);
+ const titleHeading = page.getByRole("heading", { level: 1 });
+ await expect(titleHeading).toBeVisible();
+ await expect(titleHeading).toContainText("v3.2.1");
+
+ // Release Highlights or sections are visible
+ await expect(page.getByText(/Release Highlights/i)).toBeVisible();
+
+ // Back link works
+ const backLink = page.getByRole("link", { name: /Back to all updates/i });
+ await expect(backLink).toBeVisible();
+ await backLink.click();
+ await expect(page).toHaveURL(/\/changelog\/?$/);
+ });
+
+ test("deprecated release detail page displays deprecation notice banner", async ({ page }) => {
+ // Navigate to deprecated release detail page
+ await page.goto("/changelog/v2-1-1-beta/");
+
+ await expect(page).toHaveTitle(/v2\.1\.1.*Passcodes Changelog/i);
+ await expect(page.locator(".tag.deprecated")).toContainText(/Deprecated Release/i);
+ await expect(page.getByText(/Deprecated Release Notice/i)).toBeVisible();
+ });
+});
diff --git a/tests/smoke/community.spec.ts b/tests/smoke/community.spec.ts
new file mode 100644
index 0000000..7242a5e
--- /dev/null
+++ b/tests/smoke/community.spec.ts
@@ -0,0 +1,39 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Community Page Smoke Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/community/");
+ });
+
+ test("community page loads and displays contributors", async ({ page }) => {
+ await expect(page).toHaveTitle(/Community/i);
+ await expect(page.getByRole("heading", { name: "Community", level: 1 })).toBeVisible();
+
+ // Established contributors appear
+ await expect(page.getByRole("heading", { name: "Jeel Dobariya" })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Harsha Vardhan Burra" })).toBeVisible();
+ });
+
+ test("A Guy community manager card renders with appropriate GitHub link", async ({ page }) => {
+ // Find "A Guy" contributor card
+ const guyCard = page.locator(".card").filter({ hasText: "A Guy" });
+ await expect(guyCard).toBeVisible();
+ await expect(guyCard).toHaveCount(1);
+
+ // Role should be Community Manager
+ await expect(guyCard.locator(".role")).toHaveText("Community Manager");
+
+ // Verify GitHub link points to exact profile
+ const githubLink = guyCard.locator("a[href='https://github.com/I-A-GUY']");
+ await expect(githubLink).toBeVisible();
+ });
+
+ test("established contributors have appropriate social/GitHub links", async ({ page }) => {
+ const harshaCard = page.locator(".card").filter({ hasText: "Harsha Vardhan Burra" });
+ await expect(harshaCard).toBeVisible();
+
+ // Verify GitHub link
+ const githubLink = harshaCard.locator("a[href*='github.com/harsha-vardhan-burra']");
+ await expect(githubLink).toBeVisible();
+ });
+});
diff --git a/tests/smoke/downloads.spec.ts b/tests/smoke/downloads.spec.ts
new file mode 100644
index 0000000..ec41e53
--- /dev/null
+++ b/tests/smoke/downloads.spec.ts
@@ -0,0 +1,52 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Downloads Page Smoke & Regression Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/downloads/");
+ });
+
+ test("downloads page loads and displays release archive", async ({ page }) => {
+ await expect(page).toHaveTitle(/Downloads/i);
+ await expect(page.getByRole("heading", { name: "Downloads & Releases", level: 1 })).toBeVisible();
+
+ // Release list container
+ const releaseCards = page.locator(".release-card");
+ await expect(releaseCards.first()).toBeVisible();
+ });
+
+ test("recommended production build displays architecture download options", async ({ page }) => {
+ // Recommended production build section
+ await expect(page.getByText("Recommended Production Build")).toBeVisible();
+
+ // Download controls are rendered
+ const downloadOptions = page.locator(".arch-download-btn, .arch-dropdown, a[href*='.apk']");
+ await expect(downloadOptions.first()).toBeVisible();
+ });
+
+ test("deprecated v1.x and v2.x releases display deprecation indicators while v3.x does not", async ({ page }) => {
+ // v3.2.1 is current/stable and must NOT be marked deprecated
+ const v3Card = page.locator(".release-card").filter({ hasText: /v3\.2\.1/i }).first();
+ await expect(v3Card).toBeVisible();
+ await expect(v3Card.locator(".tag.deprecated")).toHaveCount(0);
+
+ // v2.1.1 is an older major version and MUST display deprecated badge
+ const v2Card = page.locator(".release-card").filter({ hasText: /v2\.1\.1/i }).first();
+ await expect(v2Card).toBeVisible();
+ await expect(v2Card.locator(".tag.deprecated")).toBeVisible();
+ await expect(v2Card.getByText(/Deprecated Release/i)).toBeVisible();
+
+ // v1.0.0 is an older major version and MUST display deprecated badge
+ const v1Card = page.locator(".release-card").filter({ hasText: /v1\.0\.0/i }).first();
+ await expect(v1Card).toBeVisible();
+ await expect(v1Card.locator(".tag.deprecated")).toBeVisible();
+ });
+
+ test("yanked release v1.2.0 displays both yanked and deprecated statuses cleanly", async ({ page }) => {
+ const v120Card = page.locator(".release-card").filter({ hasText: /v1\.2\.0/i }).first();
+ await expect(v120Card).toBeVisible();
+
+ // Both distinct statuses coexist
+ await expect(v120Card.locator(".tag.deprecated")).toBeVisible();
+ await expect(v120Card.locator(".tag").filter({ hasText: /yanked/i })).toBeVisible();
+ });
+});
diff --git a/tests/smoke/footer.spec.ts b/tests/smoke/footer.spec.ts
new file mode 100644
index 0000000..11ecf28
--- /dev/null
+++ b/tests/smoke/footer.spec.ts
@@ -0,0 +1,42 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Footer Contributor Credit & Integrity Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/");
+ });
+
+ test("footer contains Design by @harsha-vardhan-burra credit pointing to GitHub", async ({ page }) => {
+ const footer = page.locator("footer.site-footer");
+ await expect(footer).toBeVisible();
+
+ // The attribution text "Design by" is visible in footer
+ await expect(footer.getByText(/Design by/i)).toBeVisible();
+
+ // The link with handle @harsha-vardhan-burra
+ const creditLink = footer.getByRole("link", { name: /@harsha-vardhan-burra/i });
+ await expect(creditLink).toBeVisible();
+ await expect(creditLink).toHaveCount(1);
+ await expect(creditLink).toHaveAttribute("href", "https://github.com/harsha-vardhan-burra");
+ await expect(creditLink).toHaveAttribute("target", "_blank");
+ await expect(creditLink).toHaveAttribute("rel", /noopener/);
+
+ // Accessible label / title includes full name
+ const accessibleName = await creditLink.getAttribute("aria-label");
+ const titleAttr = await creditLink.getAttribute("title");
+ const hasAccessibleIdentity =
+ (accessibleName && accessibleName.includes("Harsha Vardhan Burra")) ||
+ (titleAttr && titleAttr.includes("Harsha Vardhan Burra"));
+ expect(hasAccessibleIdentity).toBe(true);
+ });
+
+ test("footer preserves privacy/security and copyright messaging", async ({ page }) => {
+ const footer = page.locator("footer.site-footer");
+
+ // Copyright message
+ await expect(footer.getByText(/Copyright ยฉ Jeel Dobariya/i)).toBeVisible();
+
+ // Privacy messaging
+ await expect(footer.getByText(/Crafted for privacy and local control/i)).toBeVisible();
+ await expect(footer.getByText(/zero cloud, zero trackers/i)).toBeVisible();
+ });
+});
diff --git a/tests/smoke/homepage.spec.ts b/tests/smoke/homepage.spec.ts
new file mode 100644
index 0000000..734128d
--- /dev/null
+++ b/tests/smoke/homepage.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Homepage Smoke Tests", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/");
+ });
+
+ test("homepage loads successfully and displays core branding", async ({ page }) => {
+ await expect(page).toHaveTitle(/Passcodes/i);
+ const heading = page.getByRole("heading", { level: 1 });
+ await expect(heading).toBeVisible();
+ await expect(heading).toContainText("passwords");
+ });
+
+ test("hero section contains primary CTAs and release info", async ({ page }) => {
+ // Primary Call to Action
+ const downloadCTA = page.getByRole("link", { name: /Download for Android/i });
+ await expect(downloadCTA).toBeVisible();
+ await expect(downloadCTA).toHaveAttribute("href", "/downloads/");
+
+ // GitHub Repository Link
+ const githubCTA = page.getByRole("link", { name: /View on GitHub/i });
+ await expect(githubCTA).toBeVisible();
+ await expect(githubCTA).toHaveAttribute("href", /github\.com\/PasscodesApp\/Passcodes/);
+
+ // Current release announcement badge
+ const releasePill = page.getByRole("link", { name: /v3\.\d+\.\d+.*is live/i });
+ await expect(releasePill).toBeVisible();
+ });
+
+ test("primary navigation links are functional", async ({ page }) => {
+ const nav = page.getByRole("navigation", { name: "Main navigation" });
+
+ // Downloads navigation
+ const downloadsLink = nav.getByRole("link", { name: "Downloads" });
+ if (await downloadsLink.isVisible()) {
+ await downloadsLink.click();
+ await expect(page).toHaveURL(/\/downloads\/?$/);
+ await expect(page.getByRole("heading", { name: /Downloads/i, level: 1 })).toBeVisible();
+ }
+ });
+
+ test("core trust features are presented", async ({ page }) => {
+ await expect(page.getByRole("heading", { name: "On-Device Storage" })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Designed for Offline Privacy" })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "100% Open Source" })).toBeVisible();
+ });
+});
diff --git a/tests/smoke/mobile.spec.ts b/tests/smoke/mobile.spec.ts
new file mode 100644
index 0000000..8ea8988
--- /dev/null
+++ b/tests/smoke/mobile.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from "../fixtures/base-test";
+
+test.describe("Mobile Responsive Usability Tests", () => {
+ test("mobile navigation drawer opens and navigates", async ({ page, isMobile }) => {
+ // Run mobile menu tests on mobile projects
+ test.skip(!isMobile, "Mobile-specific drawer test only applies to mobile viewports");
+
+ await page.goto("/");
+
+ // Open menu button
+ const menuBtn = page.getByRole("button", { name: "Open menu" });
+ await expect(menuBtn).toBeVisible();
+ await menuBtn.click();
+
+ // Drawer should be open
+ const mobileDrawer = page.locator("#mobile-menu");
+ await expect(mobileDrawer).toBeVisible();
+
+ // Navigation links visible inside drawer
+ const downloadsLink = mobileDrawer.getByRole("link", { name: "Downloads" });
+ await expect(downloadsLink).toBeVisible();
+ await downloadsLink.click();
+
+ await expect(page).toHaveURL(/\/downloads\/?$/);
+ });
+
+ test("downloads page does not horizontally overflow viewport on mobile", async ({ page, isMobile }) => {
+ test.skip(!isMobile, "Horizontal overflow check targeted for mobile viewports");
+
+ await page.goto("/downloads/");
+
+ const hasHorizontalOverflow = await page.evaluate(() => {
+ return document.documentElement.scrollWidth > window.innerWidth;
+ });
+
+ expect(hasHorizontalOverflow, "Page has unexpected horizontal overflow").toBe(false);
+ });
+
+ test("changelog page does not horizontally overflow viewport on mobile", async ({ page, isMobile }) => {
+ test.skip(!isMobile, "Horizontal overflow check targeted for mobile viewports");
+
+ await page.goto("/changelog/");
+
+ const hasHorizontalOverflow = await page.evaluate(() => {
+ return document.documentElement.scrollWidth > window.innerWidth;
+ });
+
+ expect(hasHorizontalOverflow, "Changelog has unexpected horizontal overflow").toBe(false);
+ });
+});