| 1 | package fiesta |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // This file is rastrillo's cssfloor_test.go aimed at Fiesta's two |
| 11 | // documents, plus the two checks that hold the README and the |
| 12 | // changelog to the release they describe. |
| 13 | |
| 14 | // cssFloor is the engine floor the stylesheets impose on every page |
| 15 | // that links them. It is derived, not chosen: take the features |
| 16 | // tokens.css and the three themes use, and keep the highest engine |
| 17 | // version among them. light-dark() sets it everywhere except Firefox, |
| 18 | // where :has() does. Both documents state the result, because a |
| 19 | // reader who cannot find the floor invents one: an agent building on |
| 20 | // rastrillo once refused oklch() as "a 2023-era browser feature" and |
| 21 | // reached for hex twins, which protect nobody, since an engine too old |
| 22 | // for oklch() dropped the whole light-dark() palette several |
| 23 | // declarations earlier. |
| 24 | // |
| 25 | // Two files stating one fact can drift into two facts. |
| 26 | // TestCSSFloorIsStatedTheSameInBothFiles holds them equal. Both |
| 27 | // hard-wrap prose, so the comparison normalises whitespace: the string |
| 28 | // may break across a line, it may not disagree. |
| 29 | const cssFloor = "Chrome 123, Safari 17.5, Firefox 121" |
| 30 | |
| 31 | // The adoption policy is a rolling one: a feature is fair game once it |
| 32 | // has been in all three engines about a year. A rolling policy with no |
| 33 | // clock is a lapsed one nobody has noticed yet, and these two constants |
| 34 | // are the clock. |
| 35 | // |
| 36 | // What does not move when the policy rolls is the floor. It rises only |
| 37 | // when someone writes a newer feature into the CSS, so "anything a year |
| 38 | // old may be used" and "today we bottom out at Chrome 123" stay true |
| 39 | // together. That is also why the review is by hand: the computable |
| 40 | // half, what is now a year old, was never the hard half. The judgement |
| 41 | // is which of those to want, and every yes raises the floor for every |
| 42 | // page that links the CSS. |
| 43 | // |
| 44 | // Nine months rather than twelve, so a review that slips a release |
| 45 | // still lands inside the year the policy names. |
| 46 | // |
| 47 | // Answering the failure means re-deriving, not bumping the date: list |
| 48 | // the features css/tokens.css and css/themes/*.css use, take the |
| 49 | // highest engine version among them, and if it differs from cssFloor |
| 50 | // change it here, in README.md and in DESIGN.md together. Then set |
| 51 | // floorReviewed to the day you did it. |
| 52 | const ( |
| 53 | floorReviewed = "2026-09-01" |
| 54 | floorReviewMonths = 9 |
| 55 | ) |
| 56 | |
| 57 | // floorFiles are the documents that state the floor: the README, for |
| 58 | // someone deciding whether to link the CSS, and DESIGN.md, for someone |
| 59 | // writing against it. |
| 60 | var floorFiles = []string{"README.md", "DESIGN.md"} |
| 61 | |
| 62 | // TestCSSFloorIsStatedTheSameInBothFiles fails when the README and |
| 63 | // DESIGN.md disagree about the floor, or either leaves it out. They |
| 64 | // are written for different readers, and a reader who checks one |
| 65 | // against the other and finds two numbers trusts neither. |
| 66 | func TestCSSFloorIsStatedTheSameInBothFiles(t *testing.T) { |
| 67 | for _, path := range floorFiles { |
| 68 | b, err := os.ReadFile(path) |
| 69 | if err != nil { |
| 70 | t.Errorf("%s must exist and state the floor %q: %v", path, cssFloor, err) |
| 71 | continue |
| 72 | } |
| 73 | if !strings.Contains(strings.Join(strings.Fields(string(b)), " "), cssFloor) { |
| 74 | t.Errorf("%s does not state the floor %q.\n"+ |
| 75 | "Either it drifted, or the floor moved and this file was not brought along: "+ |
| 76 | "cssFloor, README.md and DESIGN.md change together or not at all.", |
| 77 | path, cssFloor) |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // TestCSSFloorHasBeenReviewedRecently is the tripwire. It fails on a |
| 83 | // date, which is unusual and intended: the alternative is a warning, |
| 84 | // and a warning on a release evening is a thing you scroll past. A |
| 85 | // build that stops for ten minutes while somebody re-reads a list of |
| 86 | // CSS features is cheap; a floor that quietly describes 2024 while |
| 87 | // the policy promises "about a year" is the silence the policy exists |
| 88 | // to end. |
| 89 | func TestCSSFloorHasBeenReviewedRecently(t *testing.T) { |
| 90 | reviewed, err := time.Parse(time.DateOnly, floorReviewed) |
| 91 | if err != nil { |
| 92 | t.Fatalf("floorReviewed must be a %s date: %v", time.DateOnly, err) |
| 93 | } |
| 94 | due := reviewed.AddDate(0, floorReviewMonths, 0) |
| 95 | if time.Now().After(due) { |
| 96 | t.Fatalf("the CSS floor was last reviewed %s and was due %s.\n"+ |
| 97 | "Re-derive it, the features css/tokens.css and css/themes/*.css use and the highest engine "+ |
| 98 | "version among them, then update cssFloor, README.md and DESIGN.md if it moved, and set "+ |
| 99 | "floorReviewed to today. Do not bump the date alone; the date is the record that somebody looked.", |
| 100 | floorReviewed, due.Format(time.DateOnly)) |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // changelogName is the file whose newest release must be Version. |
| 105 | const changelogName = "CHANGELOG.md" |
| 106 | |
| 107 | // TestVersionIsTheChangelogsNewestRelease holds the changelog and the |
| 108 | // tree to one version: the first second-level heading is Unreleased, |
| 109 | // and the one after it names Version, so a release is cut only once |
| 110 | // its entry is written, and the tree is never a version the changelog |
| 111 | // has not heard of. TestTheVersionIsSemver pins Version itself. |
| 112 | func TestVersionIsTheChangelogsNewestRelease(t *testing.T) { |
| 113 | b, err := os.ReadFile(changelogName) |
| 114 | if err != nil { |
| 115 | t.Fatalf("%s must exist, with ## Unreleased and then ## %s: %v", changelogName, Version, err) |
| 116 | } |
| 117 | var headings []string |
| 118 | for _, line := range strings.Split(string(b), "\n") { |
| 119 | if strings.HasPrefix(line, "## ") { |
| 120 | headings = append(headings, strings.TrimSpace(line)) |
| 121 | } |
| 122 | } |
| 123 | if len(headings) < 2 { |
| 124 | t.Fatalf("%s has %d second-level headings %q; want ## Unreleased and then ## %s", changelogName, len(headings), headings, Version) |
| 125 | } |
| 126 | if headings[0] != "## Unreleased" { |
| 127 | t.Errorf("%s's first heading is %q, want %q", changelogName, headings[0], "## Unreleased") |
| 128 | } |
| 129 | if want := "## " + Version; headings[1] != want { |
| 130 | t.Errorf("%s's newest release is %q, but the tree is %q: the changelog entry and fiesta.Version move together", changelogName, headings[1], want) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | // oneLink is the tag the README exists to show: a page links this and |
| 135 | // has the design system. /1/ is rewritten to the newest 1.y.z release. |
| 136 | const oneLink = `<link rel="stylesheet" href="https://fiesta.rastrillo.org/1/fiesta.css">` |
| 137 | |
| 138 | // TestTheReadmeShowsTheOneLink: the README carries the link tag |
| 139 | // exactly as a page should paste it. |
| 140 | func TestTheReadmeShowsTheOneLink(t *testing.T) { |
| 141 | b, err := os.ReadFile("README.md") |
| 142 | if err != nil { |
| 143 | t.Fatalf("README.md must exist and show the one link: %v", err) |
| 144 | } |
| 145 | if !strings.Contains(string(b), oneLink) { |
| 146 | t.Errorf("README.md does not show the one link, verbatim: %s", oneLink) |
| 147 | } |
| 148 | } |
| 149 | |