This commit is contained in:
parent
33c68c2f44
commit
749f525f83
44 changed files with 5380 additions and 3532 deletions
151
tests/ShouldStayScriptFree/main.cpp
Normal file
151
tests/ShouldStayScriptFree/main.cpp
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
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();
|
||||
}
|
||||
Loading…
Reference in a new issue