This commit is contained in:
parent
a8bc400371
commit
ed61fb5ca5
12 changed files with 254 additions and 38 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue