This commit is contained in:
parent
eb655b24b6
commit
25805a093a
20 changed files with 788 additions and 805 deletions
|
|
@ -10,61 +10,106 @@ No permission is granted to copy, modify, distribute, or create derivative works
|
|||
// charges, the currency conversions the shop card carries, and the copy
|
||||
// around them. Runs in both shop states — coming-soon asserts the closed
|
||||
// shape, launch re-arms the open one with no edit here.
|
||||
//
|
||||
// The goods half addresses whatever priced product the compiled catalogue
|
||||
// lists first — its slug, its variants and its photo are read off the record,
|
||||
// and every expected figure is formatted with the same Money routines the
|
||||
// renderer uses. While the catalogue lists no goods (as since 2026-09-05) that
|
||||
// half prints a note and is skipped; the donation checks run regardless.
|
||||
|
||||
import std;
|
||||
import Catcrafts.Shared;
|
||||
import Catcrafts.E2eHarness;
|
||||
|
||||
using namespace Catcrafts;
|
||||
using namespace Catcrafts::E2e;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestServer srv(argv[1], 8216);
|
||||
namespace {
|
||||
|
||||
void PricedProduct(TestServer& srv, const Product& pr) {
|
||||
const std::string shop = "/shop/" + pr.slug;
|
||||
const std::string incl = Money::FormatEuro(pr.priceInclMinor);
|
||||
const std::string net = Money::FormatEuro(Money::NetFromGross(pr.priceInclMinor));
|
||||
|
||||
// The price is rendered from the same integers the checkout charges, with
|
||||
// the derived ex-VAT twin alongside — asserting both pins the arithmetic.
|
||||
srv.BodyHas("/shop/fp6-pmos", "€573.80",
|
||||
"product page shows the from-price (green supplier + €60.50 gross markup)");
|
||||
srv.BodyHas("/shop/fp6-pmos", "€474.21", "product page shows the derived ex-VAT price");
|
||||
srv.BodyHas("/shop/fp6-pmos", ">from<", "product page marks the price as a from-price");
|
||||
srv.BodyHas("/shop", "€573.80", "shop card shows the from-price");
|
||||
// Every colour is priced in the selector, and the form carries the exact
|
||||
// data blob the preview computes from.
|
||||
srv.BodyHas("/shop/fp6-pmos", "Black €579.80", "colour selector prices black");
|
||||
srv.BodyHas("/shop/fp6-pmos", "White €665.38", "colour selector prices white");
|
||||
if (srv.ShopOpen()) {
|
||||
srv.BodyHas("/shop/fp6-pmos", "data-cc=", "form embeds the pricing blob");
|
||||
srv.BodyHas("/shop/fp6-pmos", "id=\"cc-total\"", "live total element present");
|
||||
srv.BodyHas(shop, incl, "product page shows the (from-)price");
|
||||
srv.BodyHas(shop, net, "product page shows the derived ex-VAT price");
|
||||
srv.BodyHas("/shop", incl, "shop card shows the (from-)price");
|
||||
// "from" is a claim that other colours cost more, so it appears exactly
|
||||
// when there is more than one colour to choose from.
|
||||
if (pr.variants.size() > 1) {
|
||||
srv.BodyHas(shop, ">from<", "product page marks the price as a from-price");
|
||||
Check(pr.CheapestVariant() && pr.CheapestVariant()->priceInclMinor == pr.priceInclMinor,
|
||||
"the advertised from-price is the cheapest colour's");
|
||||
} else {
|
||||
srv.BodyHas("/shop/fp6-pmos", "Coming soon", "coming-soon notice on the buy panel");
|
||||
srv.BodyHas("/shop", "coming soon", "shop card carries the coming-soon badge");
|
||||
srv.BodyLacks("/shop/fp6-pmos", "<form", "no order form while coming soon");
|
||||
srv.BodyLacks(shop, ">from<", "a single-price product is not a from-price");
|
||||
}
|
||||
// Every colour is priced in the selector, label and euro amount together,
|
||||
// and the form carries the exact data blob the preview computes from.
|
||||
if (srv.ShopOpen(pr.slug)) {
|
||||
for (const Variant& v : pr.variants) {
|
||||
srv.BodyHas(shop, v.label + " " + Money::FormatEuro(v.priceInclMinor),
|
||||
"colour selector prices " + v.slug);
|
||||
}
|
||||
srv.BodyHas(shop, "data-cc=", "form embeds the pricing blob");
|
||||
srv.BodyHas(shop, "id=\"cc-total\"", "live total element present");
|
||||
} else {
|
||||
srv.BodyHas(shop, "Coming soon", "coming-soon notice on the buy panel");
|
||||
srv.BodyHas("/shop", "coming soon", "shop card carries the coming-soon badge");
|
||||
srv.BodyLacks(shop, "<form", "no order form while coming soon");
|
||||
}
|
||||
// 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.
|
||||
// The photo: on the page, on the card, in the link preview, and actually
|
||||
// present in the repo — the file itself is Caddy's to serve (static
|
||||
// asset), so its presence is asserted against the tree, not this server.
|
||||
if (!pr.image.empty()) {
|
||||
const std::string src = "src=\"" + pr.image + "\"";
|
||||
srv.BodyHas(shop, src, "product page embeds the photo");
|
||||
srv.BodyHas("/shop", src, "shop card embeds the thumbnail");
|
||||
Check(std::filesystem::exists("images" + pr.image),
|
||||
"product photo exists in the repo", pr.image);
|
||||
}
|
||||
// The spec lede is generated from the brand, so it introduces THIS sheet
|
||||
// and no other device's.
|
||||
if (!pr.specs.empty()) {
|
||||
srv.BodyHas(shop, "a stock " + pr.brand + ", unmodified",
|
||||
"the spec lede names the brand, not a model");
|
||||
}
|
||||
srv.BodyLacks(shop, "not yet verified", "no unverified-emergency-calling caveat on the page");
|
||||
srv.BodyLacks(shop, "Reserve one", "no reservation form survives");
|
||||
|
||||
// The shop card: one euro number as the crawler/no-JS text, every
|
||||
// supported currency pre-formatted server-side as a data attribute for
|
||||
// the script to pick from. Converted amounts carry "~". GBP converts the
|
||||
// ex-VAT price; SEK (an EU member's currency) converts the VAT-inclusive
|
||||
// price. USD and CAD are absent on purpose — the shop refuses those
|
||||
// destinations, so it does not quote a local price to them either.
|
||||
srv.BodyHas("/shop", "class=\"price__single\"", "shop card renders the single-number price");
|
||||
srv.BodyHas("/shop", "data-gbp=\"~£", "shop card carries a GBP conversion");
|
||||
srv.BodyHas("/shop", "data-sek=\"~kr ", "shop card carries an SEK conversion");
|
||||
srv.BodyHas("/shop", "data-world=\"" + net + "\"", "shop card carries the euro export fallback");
|
||||
srv.BodyLacks("/shop", "data-usd=", "no USD price for a country the shop refuses");
|
||||
srv.BodyLacks("/shop", "data-cad=", "no CAD price for a country the shop refuses");
|
||||
// The product page gets the same headline element, so a British visitor
|
||||
// sees ~£ at the top there too, and the buy card states the customs
|
||||
// position plainly.
|
||||
srv.BodyHas(shop, "data-gbp=\"~£", "product page headline carries the conversion");
|
||||
srv.BodyHas(shop, "indicative only", "buy card says converted prices are indicative");
|
||||
srv.BodyHas(shop, "customs authority", "buy card names whose problem import charges are");
|
||||
srv.BodyLacks(shop, "collected on arrival", "the vague customs phrasing is gone");
|
||||
// The label must not claim the Dutch rate is an EU-wide one.
|
||||
srv.BodyLacks(shop, "EU VAT", "price label does not call 21% an EU-wide rate");
|
||||
}
|
||||
|
||||
void Donation(TestServer& srv) {
|
||||
// The donation item: open whatever the goods are doing — 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");
|
||||
srv.BodyHas("/shop", "any amount", "donation card quotes no price");
|
||||
srv.BodyHas("/shop/donation", "name=\"amount\"", "donation form asks for an amount");
|
||||
srv.BodyHas("/shop/donation", ">Donate</button>",
|
||||
"donation form submits to payment");
|
||||
srv.BodyHas("/shop/donation", ">Donate</button>", "donation form submits to payment");
|
||||
srv.BodyLacks("/shop/donation", "name=\"street\"",
|
||||
"donation form asks no address — nothing ships");
|
||||
srv.BodyLacks("/shop/donation", "name=\"quantity\"",
|
||||
|
|
@ -79,44 +124,21 @@ int main(int argc, char** argv) {
|
|||
// No price, no conversions, no preview: the donation page ships NO
|
||||
// executable script at all — stricter than the shop pages' one-script rule.
|
||||
srv.BodyLacks("/shop/donation", "<script>", "donation page ships no executable script");
|
||||
|
||||
srv.BodyHas("/shop/fp6-pmos", "src=\"/fp6-pmos.jpg\"", "product page embeds the photo");
|
||||
srv.BodyHas("/shop", "src=\"/fp6-pmos.jpg\"", "shop card embeds the thumbnail");
|
||||
// The image file itself is Caddy's to serve (static asset), so its
|
||||
// presence is asserted against the repo, not this server.
|
||||
Check(std::filesystem::exists("images/fp6-pmos.jpg"), "product photo exists in the repo");
|
||||
// 112 was verified against a live network on 2026-08-18, so the caveat
|
||||
// that used to sit above the spec sheet is gone from the served page and
|
||||
// must not creep back in.
|
||||
srv.BodyLacks("/shop/fp6-pmos", "not yet verified",
|
||||
"no unverified-emergency-calling caveat on the page");
|
||||
srv.BodyLacks("/shop", "reservation", "no reservation copy survives on /shop");
|
||||
srv.BodyLacks("/shop/fp6-pmos", "Reserve one", "no reservation form survives");
|
||||
}
|
||||
|
||||
// The shop card: one euro number as the crawler/no-JS text, every
|
||||
// supported currency pre-formatted server-side as a data attribute for
|
||||
// the script to pick from. Converted amounts carry "~". GBP converts the
|
||||
// ex-VAT price; SEK (an EU member's currency) converts the VAT-inclusive
|
||||
// price. USD and CAD are absent on purpose — the shop refuses those
|
||||
// destinations, so it does not quote a local price to them either.
|
||||
srv.BodyHas("/shop", "class=\"price__single\"", "shop card renders the single-number price");
|
||||
srv.BodyHas("/shop", "data-gbp=\"~£", "shop card carries a GBP conversion");
|
||||
srv.BodyHas("/shop", "data-sek=\"~kr ", "shop card carries an SEK conversion");
|
||||
srv.BodyHas("/shop", "data-world=\"€474.21\"", "shop card carries the euro export fallback");
|
||||
srv.BodyLacks("/shop", "data-usd=", "no USD price for a country the shop refuses");
|
||||
srv.BodyLacks("/shop", "data-cad=", "no CAD price for a country the shop refuses");
|
||||
// The product page gets the same headline element, so a British visitor
|
||||
// sees ~£ at the top there too, and the buy card states the customs
|
||||
// position plainly.
|
||||
srv.BodyHas("/shop/fp6-pmos", "data-gbp=\"~£", "product page headline carries the conversion");
|
||||
srv.BodyHas("/shop/fp6-pmos", "indicative only",
|
||||
"buy card says converted prices are indicative");
|
||||
srv.BodyHas("/shop/fp6-pmos", "customs authority",
|
||||
"buy card names whose problem import charges are");
|
||||
srv.BodyLacks("/shop/fp6-pmos", "collected on arrival",
|
||||
"the vague customs phrasing is gone");
|
||||
// The label must not claim the Dutch rate is an EU-wide one.
|
||||
srv.BodyLacks("/shop/fp6-pmos", "EU VAT", "price label does not call 21% an EU-wide rate");
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestServer srv(argv[1], 8216);
|
||||
|
||||
if (const Product* pr = FirstPricedProduct()) {
|
||||
PricedProduct(srv, *pr);
|
||||
} else {
|
||||
std::println("note: the catalogue lists no priced product; the goods checks "
|
||||
"did not run and re-arm when one is listed");
|
||||
}
|
||||
Donation(srv);
|
||||
|
||||
return Finish();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue