This commit is contained in:
parent
a8bc400371
commit
ed61fb5ca5
12 changed files with 254 additions and 38 deletions
|
|
@ -165,6 +165,11 @@ int main(int argc, char** argv) {
|
|||
srv.BodyHas("/", "property=\"og:title\"", "home page has og:title");
|
||||
srv.BodyHas("/shop/fp6-pmos", "og:image\" content=\"https://catcrafts.net/fp6-pmos.jpg",
|
||||
"product og:image is the absolute photo URL");
|
||||
// Per product, not per site: two phones that share a link-preview image
|
||||
// are two Mastodon posts that look like the same phone.
|
||||
srv.BodyHas("/shop/fp6plus-pmos",
|
||||
"og:image\" content=\"https://catcrafts.net/fp6plus-pmos.jpg",
|
||||
"the refresh previews with its own photo, not the Gen. 6's");
|
||||
|
||||
// ── the person-company weld ───────────────────────────────────────
|
||||
// The about page: the founder must be named in the HTML, the Person
|
||||
|
|
|
|||
|
|
@ -38,7 +38,26 @@ int main(int argc, char** argv) {
|
|||
srv.BodyHas("/shop", "coming soon", "shop card carries the coming-soon badge");
|
||||
srv.BodyLacks("/shop/fp6-pmos", "<form", "no order form while coming soon");
|
||||
}
|
||||
// The donation item: open while the phone above may not be — the shop's
|
||||
// The Gen. 6+ beside it: one launch price in all three colours, so the
|
||||
// "from" price and every selector row are the same number — and the
|
||||
// ex-VAT twin is still derived from it rather than typed in.
|
||||
srv.BodyHas("/shop/fp6plus-pmos", "€709.50",
|
||||
"refresh page shows the from-price (€649.00 supplier + €60.50 gross markup)");
|
||||
srv.BodyHas("/shop/fp6plus-pmos", "€586.36", "refresh page shows the derived ex-VAT price");
|
||||
srv.BodyHas("/shop", "€709.50", "shop card shows the refresh from-price");
|
||||
srv.BodyHas("/shop/fp6plus-pmos", "Cobalt Blue €709.50",
|
||||
"colour selector prices the colour only this model ships in");
|
||||
srv.BodyHas("/shop/fp6plus-pmos", "12 GB RAM",
|
||||
"refresh page states the spec that makes it a different phone");
|
||||
srv.BodyHas("/shop/fp6plus-pmos", "src=\"/fp6plus-pmos.jpg\"",
|
||||
"refresh page embeds its own photo");
|
||||
srv.BodyHas("/shop", "src=\"/fp6plus-pmos.jpg\"", "shop card embeds the refresh thumbnail");
|
||||
Check(std::filesystem::exists("images/fp6plus-pmos.jpg"),
|
||||
"refresh photo exists in the repo");
|
||||
srv.BodyLacks("/shop/fp6plus-pmos", "stock Fairphone (Gen. 6),",
|
||||
"refresh page does not caption its specs as the Gen. 6's");
|
||||
|
||||
// The donation item: open while the phones above may not be — the shop's
|
||||
// soft opening. Its card quotes no price (there is none), its page is a
|
||||
// form asking an amount and, optionally, an email — never an address.
|
||||
srv.BodyHas("/shop", "/shop/donation", "shop grid lists the donation item");
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ int main(int argc, char** argv) {
|
|||
TestServer srv(argv[1], 8210);
|
||||
|
||||
// ── status codes ──────────────────────────────────────────────────
|
||||
for (std::string_view p : { "/", "/about", "/shop", "/shop/fp6-pmos", "/projects",
|
||||
for (std::string_view p : { "/", "/about", "/shop", "/shop/fp6-pmos",
|
||||
"/shop/fp6plus-pmos", "/shop/donation", "/projects",
|
||||
"/posts", "/demos", "/demos/raytracer", "/financials",
|
||||
"/legal/privacy", "/legal/terms", "/legal/imprint",
|
||||
"/feed.xml", "/sitemap.xml", "/api/healthz" }) {
|
||||
|
|
@ -72,6 +73,8 @@ int main(int argc, char** argv) {
|
|||
|
||||
// ── sitemap and feed content ──────────────────────────────────────
|
||||
srv.BodyHas("/sitemap.xml", "/shop/fp6-pmos", "sitemap lists the product");
|
||||
srv.BodyHas("/sitemap.xml", "/shop/fp6plus-pmos",
|
||||
"sitemap lists the refresh — it comes off the catalogue, not a second list");
|
||||
srv.BodyHas("/sitemap.xml", "/about", "sitemap lists the about page");
|
||||
srv.BodyHas("/sitemap.xml", "/legal/privacy", "sitemap lists the privacy page");
|
||||
srv.BodyHas("/sitemap.xml", "/demos", "sitemap lists the demos page");
|
||||
|
|
|
|||
|
|
@ -31,11 +31,12 @@ void CatalogueContract() {
|
|||
using namespace Catcrafts::Money;
|
||||
|
||||
const auto& products = Content::Products();
|
||||
// Two entries: the phone, and the donation item that soft-opens the shop.
|
||||
// The phone stays FIRST — it is the headline, and the suites below
|
||||
// address Products()[0] as the priced product.
|
||||
Check(products.size() == 2, "content: two products");
|
||||
if (products.size() == 2) {
|
||||
// Three entries: the two phones, and the donation item that soft-opens
|
||||
// the shop. The Gen. 6 stays FIRST — it is the headline, and the suites
|
||||
// below address Products()[0] as the priced product — and the donation
|
||||
// stays LAST.
|
||||
Check(products.size() == 3, "content: three products");
|
||||
if (products.size() == 3) {
|
||||
const Product& pr = products[0];
|
||||
Check(pr.slug == "fp6-pmos", "content: product slug");
|
||||
// Coming-soon is the pre-launch state; launch flips it to
|
||||
|
|
@ -167,15 +168,91 @@ void CatalogueContract() {
|
|||
"schema: and the rest of the record still parses");
|
||||
}
|
||||
|
||||
// The Gen. 6+: the same contract as the phone above, asserted against
|
||||
// its own numbers. That the two SHARE the pricing rule is the point —
|
||||
// a second device must not quietly become a second pricing policy.
|
||||
const Product& plus = products[1];
|
||||
Check(plus.slug == "fp6plus-pmos", "content: the refresh is the second listing");
|
||||
Check(plus.brand == "Fairphone", "content: the refresh names its manufacturer");
|
||||
Check(plus.Buyable() || plus.ComingSoon(),
|
||||
"content: the refresh is buyable or deliberately coming soon");
|
||||
Check(plus.variants.size() == 3, "content: three colours on the refresh");
|
||||
// Launch price in every colour, so all three stickers are €709.50 —
|
||||
// and each still nets exactly €50 after VAT, by the same arithmetic
|
||||
// the invoice and the checkout use.
|
||||
for (std::string_view slug : { "green", "black", "blue" }) {
|
||||
const Variant* v = plus.FindVariant(slug);
|
||||
Check(v && v->priceInclMinor == 70950,
|
||||
"content: refresh colour = 649.00 supplier + 60.50 gross markup", slug);
|
||||
Check(v && Money::NetFromGross(v->priceInclMinor)
|
||||
- Money::NetFromGross(64900) == 5000,
|
||||
"content: refresh colour nets the supplier price plus exactly €50", slug);
|
||||
}
|
||||
Check(plus.FindVariant("white") == nullptr,
|
||||
"content: a colour this model doesn't ship in is null");
|
||||
Check(plus.CheapestVariant()
|
||||
&& plus.priceInclMinor == plus.CheapestVariant()->priceInclMinor,
|
||||
"content: refresh from-price is the cheapest variant");
|
||||
// Same box, same 193 g device, so the same bracket and the same
|
||||
// per-parcel ceiling the phone above is checked against.
|
||||
Check(plus.shipWeightGrams == 700,
|
||||
"content: the refresh leaves in the same 700 g parcel");
|
||||
// One warranty text for both phones, not two that can drift — the
|
||||
// reason it is a shared constant rather than a second paste.
|
||||
Check(plus.warranty == pr.warranty,
|
||||
"content: both phones carry the identical warranty text");
|
||||
Check(plus.specs.size() == pr.specs.size(),
|
||||
"content: the refresh states every spec row the Gen. 6 does");
|
||||
// The refresh IS the faster silicon — if these two rows ever match the
|
||||
// Gen. 6's, the listing is selling the wrong phone.
|
||||
{
|
||||
bool soc = false, ram = false;
|
||||
for (const Spec& s : plus.specs) {
|
||||
soc = soc || (s.label == "Processor" && s.value.find("7s Gen 4") != std::string::npos);
|
||||
ram = ram || (s.label == "Memory" && s.value.find("12 GB") != std::string::npos);
|
||||
}
|
||||
Check(soc && ram, "content: the refresh lists its own SoC and 12 GB of RAM");
|
||||
}
|
||||
// Its own photo, not the Gen. 6's — the two phones ship in different
|
||||
// colours, so sharing one render would picture a phone this listing
|
||||
// cannot sell. Published, not merely committed: a file that the page,
|
||||
// the link preview and the schema record all omit is a photo nobody
|
||||
// ever sees, which is the failure the checks below actually catch.
|
||||
Check(plus.image == "/fp6plus-pmos.jpg", "content: the refresh names its own photo");
|
||||
Check(plus.image != pr.image, "content: and not the Gen. 6's");
|
||||
// The pictured colour is the pre-selected one. With a single price
|
||||
// across the range that is purely a question of list order, so it is
|
||||
// worth pinning: a reorder would leave the page showing blue and
|
||||
// selling green by default.
|
||||
Check(plus.CheapestVariant() && plus.CheapestVariant()->slug == "blue",
|
||||
"content: the default colour is the one the photo shows");
|
||||
{
|
||||
const auto pp = Views::RenderProduct(plus, Rates{});
|
||||
// The spec lede is generated from the brand, so it must introduce
|
||||
// THIS sheet — the old markup hardcoded "Fairphone (Gen. 6)" and
|
||||
// would now caption the refresh's specs with the other phone's name.
|
||||
Check(pp.main.View().find("a stock Fairphone, unmodified") != std::string_view::npos,
|
||||
"refresh page: the spec lede names the brand, not a model");
|
||||
Check(pp.main.View().find("stock Fairphone (Gen. 6)") == std::string_view::npos,
|
||||
"refresh page: and does not caption these specs as the Gen. 6's");
|
||||
Check(pp.main.View().find("product__photo") != std::string_view::npos,
|
||||
"refresh page: the photo renders");
|
||||
Check(pp.meta.ogImage == "/fp6plus-pmos.jpg",
|
||||
"refresh page: the link preview names its own photo");
|
||||
Check(pp.meta.jsonLd.find("\"image\":\"https://catcrafts.net/fp6plus-pmos.jpg\"")
|
||||
!= std::string::npos,
|
||||
"refresh page: and the schema record carries it absolute");
|
||||
}
|
||||
|
||||
// The donation item: the shop's soft opening. Available (it is what
|
||||
// the shop is open FOR) while the phone stays coming-soon; buyer
|
||||
// the shop is open FOR) while both phones stay coming-soon; buyer
|
||||
// names the amount, so no price, no variants, no weight — and the
|
||||
// Buyable() price check is waived for exactly this shape.
|
||||
const Product& don = products[1];
|
||||
const Product& don = products[2];
|
||||
Check(don.slug == "donation" && don.donation,
|
||||
"content: the second product is the donation item");
|
||||
"content: the donation item comes last");
|
||||
Check(don.Buyable() && !don.ComingSoon(),
|
||||
"content: the donation item is on sale while the phone is not");
|
||||
"content: the donation item is on sale while the phones are not");
|
||||
Check(don.priceInclMinor == 0 && don.variants.empty()
|
||||
&& don.shipWeightGrams == 0,
|
||||
"content: a donation has no price, no colours and no parcel");
|
||||
|
|
@ -206,6 +283,13 @@ void CatalogueContract() {
|
|||
"donation page: no price-hint script — nothing to convert");
|
||||
}
|
||||
}
|
||||
// The spec lede is "a stock <brand>, unmodified", so a spec sheet with no
|
||||
// brand renders a sentence with a hole in it. Cheaper to assert than to
|
||||
// branch around in the renderer.
|
||||
for (const Product& any : products) {
|
||||
Check(any.specs.empty() || !any.brand.empty(),
|
||||
"content: a product with a spec sheet names its manufacturer", any.slug);
|
||||
}
|
||||
Check(!Content::Projects().empty(), "content: projects present");
|
||||
Check(Content::LegalPages().size() == 3, "content: three legal pages");
|
||||
Check(Content::AboutPage().sections.size() >= 3
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ int main(int argc, char** argv) {
|
|||
// 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" }) {
|
||||
"/shop/fp6-pmos", "/shop/fp6plus-pmos", "/financials" }) {
|
||||
srv.BodyLacks(std::string(pg), "PLACEHOLDER",
|
||||
std::format("{} ships no placeholder markers", pg));
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ int main(int argc, char** argv) {
|
|||
// 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" }) {
|
||||
for (std::string_view pg : { "/shop", "/shop/fp6-pmos", "/shop/fp6plus-pmos" }) {
|
||||
const std::string body = srv.Body(std::string(pg));
|
||||
const std::size_t n = CountOccurrences(body, "<script>");
|
||||
Check(n == 1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue