commit e8421a7b1d1415fe2a147f5a021b9d4e9e210c5c
parent 1cec7f3186be84cbf11de7d9e942386003c71e22
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 18:23:00 -0400
export(e2e): every build carries the Ko-fi mark, last in the social row
site-branding's "a site build carries no Ko-fi link" becomes "every build
carries the Ko-fi mark, last in the social row", and the hub's "bare Ko-fi
link" test in official-instances asserts the same mark. Both call one
helper, expectKofiMarkLast (e2e/helpers.ts): exactly one Ko-fi link on the
page (the hub's text link is gone), href KOFI_URL, a new tab, no visible
text, an img of /kofi-symbol.svg with alt "" that is drawn, the LAST <li>
of the footer's social list, and the served file 200 image/svg+xml with
the official file's sha256. The site test also pins the fixture's GitHub
link before it in the same row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 80 insertions(+), 18 deletions(-)
diff --git a/export/e2e-hub/official-instances.spec.ts b/export/e2e-hub/official-instances.spec.ts
@@ -1,5 +1,5 @@
import { expect, test, type Page, type Route } from "@playwright/test";
-import { KOFI_URL } from "../../common/lib/project";
+import { expectKofiMarkLast } from "../e2e/helpers";
// The hub's "Official Instances": one card per built-in member
// (hub-sites.json), carrying the homepage's own figures for it when the hub was
@@ -117,20 +117,17 @@ test.describe("hub official instances", () => {
).toHaveCount(0);
});
- test("the hub footer carries a bare Ko-fi link", async ({ page }) => {
+ test("the hub footer carries the Ko-fi mark, once, last in the social row", async ({
+ page,
+ }) => {
await stubMember(page);
await page.route("**/hub-summary.json", (r) =>
r.fulfill({ status: 404, body: "" }),
);
await page.goto("/");
- const kofi = page
- .locator("footer")
- .getByRole("link", { name: "Ko-fi", exact: true });
- await expect(kofi).toHaveCount(1);
- await expect(kofi).toHaveAttribute("href", KOFI_URL);
- await expect(kofi).toHaveAttribute("target", "_blank");
- await expect(kofi).toHaveAttribute("rel", "noopener noreferrer");
- await expect(kofi).toHaveText("Ko-fi");
+ // The same mark as every site (the operator's 2026-09-26 ruling). The
+ // hub's own "Ko-fi" text link is gone: one Ko-fi per footer.
+ await expectKofiMarkLast(page);
});
test("without hub-summary.json, the card renders with no figures", async ({
diff --git a/export/e2e/helpers.ts b/export/e2e/helpers.ts
@@ -1,4 +1,6 @@
-import type { Page, Route } from "@playwright/test";
+import { createHash } from "node:crypto";
+import { expect, type Page, type Route } from "@playwright/test";
+import { KOFI_URL } from "../../common/lib/project";
import {
channelSubsManifest,
channelTranscriptsManifest,
@@ -173,3 +175,57 @@ export async function closeModal(page: Page) {
.getByRole("button", { name: "Mark clip start at current time" })
.waitFor({ state: "detached" });
}
+
+// sha256 of Ko-fi's official `kofi_symbol.svg` (their brand-asset zip), which
+// the export vendors byte-for-byte as public/kofi-symbol.svg: a brand asset is
+// used as shipped, never recoloured or re-exported.
+export const KOFI_SYMBOL_SHA256 =
+ "f5a2d5f1abb5c7f13392ecf6e7d5dffc8bef77917ab59c28a6080deab52f5078";
+
+// The footer's Ko-fi mark, on every build (site and hub): exactly one Ko-fi
+// link on the page, the LAST item of the footer's social row, opening Ko-fi in
+// a new tab, with no visible text — only the official symbol, which is drawn
+// (not a broken image) and served unmodified.
+export async function expectKofiMarkLast(page: Page) {
+ const footer = page.locator("footer");
+ const kofi = footer.getByRole("link", { name: "Ko-fi", exact: true });
+ await expect(kofi).toHaveCount(1);
+ // One per page: no second Ko-fi link anywhere (the hub's old text link).
+ await expect(page.getByRole("link", { name: /ko-?fi/i })).toHaveCount(1);
+ await expect(page.locator(`a[href="${KOFI_URL}"]`)).toHaveCount(1);
+ await expect(kofi).toBeVisible();
+ await expect(kofi).toHaveAttribute("href", KOFI_URL);
+ await expect(kofi).toHaveAttribute("target", "_blank");
+ await expect(kofi).toHaveAttribute("rel", "noopener noreferrer");
+ // No copy: the mark is the whole link.
+ expect((await kofi.innerText()).trim()).toBe("");
+
+ const img = kofi.locator('img[src="/kofi-symbol.svg"]');
+ await expect(img).toHaveCount(1);
+ await expect(img).toHaveAttribute("alt", "");
+ await expect
+ .poll(() =>
+ img.evaluate((el) => {
+ const i = el as HTMLImageElement;
+ return i.complete && i.naturalWidth > 0;
+ }),
+ )
+ .toBe(true);
+
+ // `has` is queried from each <ul>, so it takes a page-rooted locator: `kofi`
+ // itself starts at <footer> and would look for a footer inside the list.
+ const row = footer
+ .locator("ul")
+ .filter({ has: page.getByRole("link", { name: "Ko-fi", exact: true }) });
+ await expect(row).toHaveCount(1);
+ await expect(
+ row.locator(":scope > li").last().getByRole("link"),
+ ).toHaveAccessibleName("Ko-fi");
+
+ const res = await page.request.get("/kofi-symbol.svg");
+ expect(res.status()).toBe(200);
+ expect(res.headers()["content-type"]).toMatch(/^image\/svg\+xml/);
+ expect(createHash("sha256").update(await res.body()).digest("hex")).toBe(
+ KOFI_SYMBOL_SHA256,
+ );
+}
diff --git a/export/e2e/site-branding.spec.ts b/export/e2e/site-branding.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
-import { KOFI_URL, PROJECT_URL } from "../../common/lib/project";
-import { installRoutes } from "./helpers";
+import { PROJECT_URL } from "../../common/lib/project";
+import { expectKofiMarkLast, installRoutes } from "./helpers";
// The export app is built per-site; its SSR branding (document title, header,
// home tagline, footer social links) comes from currentSite() reading the
@@ -45,13 +45,22 @@ test("footer credits the project, ungated and in the same tab", async ({
await expect(footer).toContainText("Built with Archilyzer");
});
-test("a site build carries no Ko-fi link", async ({ page }) => {
+test("every build carries the Ko-fi mark, last in the social row", async ({
+ page,
+}) => {
await installRoutes(page);
await page.goto("/");
- // The project's Ko-fi is on the hub and the homepage only: an archive
- // someone else hosts must not carry this project's tip jar.
- await expect(page.getByRole("link", { name: "Ko-fi" })).toHaveCount(0);
- await expect(page.locator(`a[href="${KOFI_URL}"]`)).toHaveCount(0);
+ // The operator's 2026-09-26 ruling replaced "hub and homepage only": the
+ // project's Ko-fi is on every site, as Ko-fi's official symbol after the
+ // site's own social links, with no copy around it.
+ await expectKofiMarkLast(page);
+ // The site's own links come first, in the same row.
+ const row = page
+ .locator("footer ul")
+ .filter({ has: page.getByRole("link", { name: "Ko-fi", exact: true }) });
+ const items = row.locator(":scope > li");
+ await expect(items).toHaveCount(2);
+ await expect(items.first().getByRole("link")).toHaveAccessibleName("GitHub");
});
test("the Downloads eyebrow appears only when something is behind it", async ({