151 lines
7.7 KiB
C++
151 lines
7.7 KiB
C++
|
|
/*
|
||
|
|
catcrafts.net
|
||
|
|
Copyright (C) 2026 Catcrafts
|
||
|
|
|
||
|
|
The source code of this website is made available for viewing purposes only.
|
||
|
|
No permission is granted to copy, modify, distribute, or create derivative works.
|
||
|
|
*/
|
||
|
|
|
||
|
|
// The no-JavaScript guarantee. The site must be complete without the wasm
|
||
|
|
// module — if these fail, the SSR work has regressed and crawlers see an
|
||
|
|
// empty page again. Shop pages carry exactly ONE pinned-down inline script
|
||
|
|
// (the timezone price hint); everything else ships none at all.
|
||
|
|
|
||
|
|
import std;
|
||
|
|
import Catcrafts.E2eHarness;
|
||
|
|
|
||
|
|
using namespace Catcrafts::E2e;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
// Visible prose only: strip tags, so an instance name inside an href (a
|
||
|
|
// post's own permalink necessarily contains one) does not trip the check.
|
||
|
|
std::string StripTags(std::string_view html) {
|
||
|
|
std::string out;
|
||
|
|
out.reserve(html.size());
|
||
|
|
bool inTag = false;
|
||
|
|
for (char c : html) {
|
||
|
|
if (c == '<') inTag = true;
|
||
|
|
else if (c == '>') { inTag = false; out.push_back(' '); }
|
||
|
|
else if (!inTag) out.push_back(c);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Lower(std::string s) {
|
||
|
|
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main(int argc, char** argv) {
|
||
|
|
TestServer srv(argv[1], 8211);
|
||
|
|
|
||
|
|
// ── the no-JavaScript guarantee ───────────────────────────────────
|
||
|
|
srv.BodyHas("/projects", "imsd", "/projects has content in the HTML");
|
||
|
|
srv.BodyHas("/projects", "<title>Projects", "/projects has a real title");
|
||
|
|
srv.BodyLacks("/projects", "<script", "/projects ships no script at all");
|
||
|
|
srv.BodyLacks("/legal/privacy", "<script", "/legal/privacy ships no script");
|
||
|
|
srv.BodyHas("/financials", "<title>Financials", "/financials has a real title");
|
||
|
|
srv.BodyLacks("/financials", "<script", "/financials ships no script");
|
||
|
|
|
||
|
|
// Placeholders are dev-only markers; one reaching production is a content
|
||
|
|
// bug (an imprint that says PLACEHOLDER once shipped exactly that way).
|
||
|
|
for (std::string_view pg : { "/legal/privacy", "/legal/terms", "/legal/imprint",
|
||
|
|
"/shop/fp6-pmos", "/financials" }) {
|
||
|
|
srv.BodyLacks(std::string(pg), "PLACEHOLDER",
|
||
|
|
std::format("{} ships no placeholder markers", pg));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Shop pages are the one exception to script-free: they carry exactly ONE
|
||
|
|
// EXECUTABLE inline script — the timezone price hint, whose tag is the
|
||
|
|
// bare <script>. JSON-LD blocks (<script type="application/ld+json">) are
|
||
|
|
// inert data the browser never executes, so they don't count against the
|
||
|
|
// rule. Pin the shape hard: inline only (no src=, so nothing external can
|
||
|
|
// ever ride in under this exception), no network APIs, and the page must
|
||
|
|
// remain complete without it — both prices in the markup regardless.
|
||
|
|
for (std::string_view pg : { "/shop", "/shop/fp6-pmos" }) {
|
||
|
|
const std::string body = srv.Body(std::string(pg));
|
||
|
|
const std::size_t n = CountOccurrences(body, "<script>");
|
||
|
|
Check(n == 1,
|
||
|
|
std::format("{} carries exactly one executable script (the price hint)", pg),
|
||
|
|
std::format("expected 1 bare <script>, got {}", n));
|
||
|
|
const std::regex srcTag(R"(<script[^>]*src=)");
|
||
|
|
Check(!std::regex_search(body, srcTag),
|
||
|
|
std::format("{} script is inline, not external", pg));
|
||
|
|
const std::size_t open = body.find("<script>");
|
||
|
|
const std::size_t close = body.find("</script>", open);
|
||
|
|
std::string_view inlineScript;
|
||
|
|
if (open != std::string::npos && close != std::string::npos) {
|
||
|
|
inlineScript = std::string_view(body).substr(open, close - open);
|
||
|
|
}
|
||
|
|
bool network = false;
|
||
|
|
for (std::string_view api : { "fetch", "XMLHttpRequest", "WebSocket",
|
||
|
|
"navigator.sendBeacon" }) {
|
||
|
|
network = network || inlineScript.find(api) != std::string_view::npos;
|
||
|
|
}
|
||
|
|
Check(!network, std::format("{} script makes no network calls", pg));
|
||
|
|
}
|
||
|
|
srv.BodyHas("/shop/fp6-pmos", "cc-noneu", "price hint tags the non-EU outcome");
|
||
|
|
srv.BodyHas("/shop/fp6-pmos", "cc-eu", "price hint tags the confirmed-EU outcome too");
|
||
|
|
|
||
|
|
// The renderer loads only where a demo entry declares needsWasm — the
|
||
|
|
// demo LIST is a content page and must stay free of it.
|
||
|
|
srv.BodyLacks("/demos", "<script", "/demos itself ships no script");
|
||
|
|
srv.BodyHas("/demos/raytracer", "id=\"webgpu-demo\"", "raytracer page has the mount element");
|
||
|
|
// Exactly one chrome root: the wasm adopts the server's, never builds a second.
|
||
|
|
{
|
||
|
|
const std::size_t roots =
|
||
|
|
CountOccurrences(srv.Body("/demos/raytracer"), "id=\"catcrafts-root\"");
|
||
|
|
Check(roots == 1, "raytracer page has exactly one chrome root",
|
||
|
|
std::format("expected 1, got {}", roots));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── SSR / wasm head interaction ───────────────────────────────────
|
||
|
|
// catcrafts-head.js used to set document.title unconditionally, which
|
||
|
|
// replaced the server's per-route title with the generic site name and
|
||
|
|
// appended a second stylesheet, favicon and viewport tag. The
|
||
|
|
// <meta name="cc-ssr"> marker is what it now checks; if that marker stops
|
||
|
|
// being emitted the guard silently stops working, so assert it is present
|
||
|
|
// and that the head is not duplicated.
|
||
|
|
srv.BodyHas("/demos/raytracer", "name=\"cc-ssr\"", "SSR marker present for head.js to detect");
|
||
|
|
srv.BodyHas("/demos/raytracer", "<title>Real-time ray tracer",
|
||
|
|
"demo page keeps its route-specific title");
|
||
|
|
{
|
||
|
|
const std::string body = srv.Body("/demos/raytracer");
|
||
|
|
for (std::string_view probe : { "rel=\"stylesheet\"", "rel=\"icon\"",
|
||
|
|
"name=\"viewport\"" }) {
|
||
|
|
const std::size_t n = CountOccurrences(body, probe);
|
||
|
|
Check(n == 1, std::format("demo page has exactly one {}", probe),
|
||
|
|
std::format("expected 1, got {}", n));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The base tag belongs only where the runtime needs it. On a content page
|
||
|
|
// it is dead weight and one more thing that could retarget a future
|
||
|
|
// relative link.
|
||
|
|
srv.BodyLacks("/posts", "<base", "/posts has no base tag");
|
||
|
|
srv.BodyLacks("/shop/fp6-pmos", "<base", "/shop/<slug> has no base tag");
|
||
|
|
|
||
|
|
// ── home page actions ─────────────────────────────────────────────
|
||
|
|
srv.BodyHas("/", "Browse projects", "home links to projects");
|
||
|
|
srv.BodyHas("/", "Browse shop", "home links to the shop");
|
||
|
|
srv.BodyLacks("/", "ray tracer", "home no longer pushes the ray tracer");
|
||
|
|
|
||
|
|
// ── instance-agnostic copy ────────────────────────────────────────
|
||
|
|
// The account lives on one instance but posts go into communities on
|
||
|
|
// others, so no page should name a specific instance as though it were
|
||
|
|
// the home of the discussion. In visible text, not in href values.
|
||
|
|
for (std::string_view pg : { "/", "/posts", "/shop" }) {
|
||
|
|
const std::string prose = Lower(StripTags(srv.Body(std::string(pg))));
|
||
|
|
Check(prose.find("ani.social") == std::string::npos,
|
||
|
|
std::format("{} names no specific instance in visible text", pg));
|
||
|
|
}
|
||
|
|
srv.BodyHas("/posts", "fediverse", "/posts refers to the fediverse generally");
|
||
|
|
// The fediverse account is not advertised at all — only individual posts are.
|
||
|
|
srv.BodyLacks("/", "/u/", "footer does not link a fediverse profile");
|
||
|
|
srv.BodyLacks("/posts", "/u/", "/posts links no account profile, only threads");
|
||
|
|
|
||
|
|
return Finish();
|
||
|
|
}
|