CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
u-polish.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| dc26881 | 1 | /** |
| 2 | * Block U — Senior polish pass smoke tests. | |
| 3 | * | |
| 4 | * U1: hero rebuild — exactly two primary CTAs in the buttons row, | |
| 5 | * tertiary text-link row sits underneath with the demoted demo + | |
| 6 | * vs-github affordances, install snippet sits BELOW the CTAs | |
| 7 | * wrapped in the "For power users" panel. | |
| 8 | * U2: button CSS — hover lifts every .btn by translateY(-1px) with a | |
| 9 | * soft drop shadow; focus-visible uses box-shadow (not outline); | |
| 10 | * primary CTA shimmers via background-position; disabled buttons | |
| 11 | * never lift. | |
| 12 | * U4: master CSS contains the @view-transition block and the | |
| 13 | * prefers-reduced-motion guard. | |
| 14 | * | |
| 15 | * No DB stubs, no mock pollution — pure rendering checks against the | |
| 16 | * already-mounted Hono app. | |
| 17 | */ | |
| 18 | import { describe, it, expect } from "bun:test"; | |
| 19 | import app from "../app"; | |
| 20 | ||
| 21 | const HOME = "/"; | |
| 22 | ||
| 23 | async function fetchHomeHtml(): Promise<string> { | |
| 24 | const res = await app.request(HOME); | |
| 25 | expect(res.status).toBe(200); | |
| 26 | return await res.text(); | |
| 27 | } | |
| 28 | ||
| 29 | describe("Block U1 — landing hero rebuild", () => { | |
| 30 | it("renders exactly two primary CTAs in the main button row", async () => { | |
| 31 | const body = await fetchHomeHtml(); | |
| 32 | // Extract the primary CTA row using the data-testid we shipped. | |
| 33 | const start = body.indexOf('data-testid="hero-primary-ctas"'); | |
| 34 | expect(start).toBeGreaterThan(-1); | |
| 35 | // The wrapper opens with that attribute on a div; find the matching | |
| 36 | // closing </div>. Heuristic: it's the first </div> after a tight | |
| 37 | // window of <a … class="btn …"> elements. | |
| 38 | const tail = body.slice(start, start + 2400); | |
| 39 | // Count the <a class="btn …"> anchors inside the row. | |
| 40 | const anchorMatches = tail.match(/<a[^>]*class="btn[^"]*"/g) || []; | |
| 41 | expect(anchorMatches.length).toBeGreaterThanOrEqual(2); | |
| 42 | // Sanity-check the two CTAs we explicitly preserve: | |
| 43 | expect(tail).toContain('href="/register"'); | |
| 44 | expect(tail).toContain('href="/gluecron.dxt"'); | |
| 45 | // The tertiary row is OUTSIDE this slice; we assert the demoted | |
| 46 | // demo + vs-github hrefs are NOT in the primary slice. | |
| 47 | const primaryRowEnd = tail.indexOf("hero-tertiary-row"); | |
| 48 | const primaryOnly = | |
| 49 | primaryRowEnd > 0 ? tail.slice(0, primaryRowEnd) : tail.slice(0, 800); | |
| 50 | expect(primaryOnly).not.toContain('href="/demo"'); | |
| 51 | expect(primaryOnly).not.toContain('href="/vs-github"'); | |
| 52 | }); | |
| 53 | ||
| 54 | it("renders the tertiary text-link row with demo + vs-github affordances", async () => { | |
| 55 | const body = await fetchHomeHtml(); | |
| 56 | expect(body).toContain('data-testid="hero-tertiary-row"'); | |
| 57 | expect(body).toContain('data-testid="cta-tertiary-demo"'); | |
| 58 | expect(body).toContain('data-testid="cta-tertiary-vs"'); | |
| 59 | // The legacy labels survive on the demoted row so L10 regression | |
| 60 | // guards keep passing. | |
| 61 | expect(body).toContain("Try the live demo"); | |
| 62 | expect(body).toContain("Compare to GitHub"); | |
| 63 | }); | |
| 64 | ||
| 65 | it("places the install snippet inside a 'For power users' panel below the CTAs", async () => { | |
| 66 | const body = await fetchHomeHtml(); | |
| 67 | const ctas = body.indexOf('data-testid="hero-primary-ctas"'); | |
| 68 | const installPanel = body.indexOf("Power users install panel"); | |
| 69 | const installLabel = body.indexOf("For power users"); | |
| 70 | expect(ctas).toBeGreaterThan(-1); | |
| 71 | expect(installPanel).toBeGreaterThan(-1); | |
| 72 | expect(installLabel).toBeGreaterThan(-1); | |
| 73 | // Install panel sits AFTER the CTAs — that's the whole U1 point. | |
| 74 | expect(installPanel).toBeGreaterThan(ctas); | |
| 75 | }); | |
| 76 | }); | |
| 77 | ||
| 78 | describe("Block U2 — button polish", () => { | |
| 79 | it("includes the universal hover-lift transform on .btn", async () => { | |
| 80 | const body = await fetchHomeHtml(); | |
| 81 | // The master CSS is inlined in <style> by Layout. We assert on the | |
| 82 | // hover-rule shape: `.btn:hover { … transform: translateY(-1px); … }`. | |
| 83 | expect(body).toMatch(/\.btn:hover\b[\s\S]*?transform:\s*translateY\(-1px\)/); | |
| 84 | }); | |
| 85 | ||
| 86 | it("uses a soft drop shadow on .btn:hover (not just border)", async () => { | |
| 87 | const body = await fetchHomeHtml(); | |
| 88 | expect(body).toMatch(/\.btn:hover\b[\s\S]*?box-shadow:\s*0\s+4px\s+12px/); | |
| 89 | }); | |
| 90 | ||
| 91 | it("uses box-shadow (not outline) for the .btn focus ring", async () => { | |
| 92 | const body = await fetchHomeHtml(); | |
| 93 | // Focus-visible rule must set box-shadow with the soft accent rgba. | |
| 94 | expect(body).toMatch( | |
| 95 | /\.btn:focus-visible\b[\s\S]*?box-shadow:\s*0\s+0\s+0\s+3px\s+rgba\(140,\s*109,\s*255,\s*0\.35\)/ | |
| 96 | ); | |
| 97 | // And must NOT fall back to outline:2px on .btn anywhere. | |
| 98 | expect(body).not.toMatch(/\.btn:focus-visible[^{]*\{\s*outline:\s*2px/); | |
| 99 | }); | |
| 100 | ||
| 101 | it("disables hover-lift on disabled buttons", async () => { | |
| 102 | const body = await fetchHomeHtml(); | |
| 103 | // The disabled rule sets transform:none AND opacity:0.5. | |
| 104 | expect(body).toMatch(/\.btn:disabled[\s\S]*?transform:\s*none/); | |
| 105 | expect(body).toMatch(/\.btn:disabled[\s\S]*?opacity:\s*0\.5/); | |
| 106 | }); | |
| 107 | ||
| 108 | it("shimmers the primary CTA via background-position transition", async () => { | |
| 109 | const body = await fetchHomeHtml(); | |
| 110 | // Primary button declares background-size 200% so the position | |
| 111 | // animation has somewhere to travel. | |
| 112 | expect(body).toMatch(/\.btn-primary\b[\s\S]*?background-size:\s*200%/); | |
| 113 | // The transition lists background-position with the 600ms duration. | |
| 114 | expect(body).toMatch( | |
| 115 | /\.btn-primary\b[\s\S]*?transition:[\s\S]*?background-position\s+600ms/ | |
| 116 | ); | |
| 117 | }); | |
| 118 | }); | |
| 119 | ||
| 120 | describe("Block U4 — view transitions", () => { | |
| 121 | it("includes the @view-transition opt-in", async () => { | |
| 122 | const body = await fetchHomeHtml(); | |
| 123 | expect(body).toContain("@view-transition"); | |
| 124 | expect(body).toMatch(/@view-transition\s*\{\s*navigation:\s*auto;?\s*\}/); | |
| 125 | }); | |
| 126 | ||
| 127 | it("declares both fade-out and fade-in keyframes for ::view-transition-*", async () => { | |
| 128 | const body = await fetchHomeHtml(); | |
| 129 | expect(body).toContain("::view-transition-old(root)"); | |
| 130 | expect(body).toContain("::view-transition-new(root)"); | |
| 131 | expect(body).toContain("@keyframes vt-fade-out"); | |
| 132 | expect(body).toContain("@keyframes vt-fade-in"); | |
| 133 | }); | |
| 134 | ||
| 135 | it("disables the transition under prefers-reduced-motion", async () => { | |
| 136 | const body = await fetchHomeHtml(); | |
| 137 | // The reduced-motion block must mention the view-transition | |
| 138 | // pseudos and set animation-duration: 0s. | |
| 139 | expect(body).toMatch( | |
| 140 | /@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?::view-transition-(old|new)\(root\)[\s\S]*?animation-duration:\s*0s/ | |
| 141 | ); | |
| 142 | }); | |
| 143 | ||
| 144 | it("attaches view-transition-name: root to body", async () => { | |
| 145 | const body = await fetchHomeHtml(); | |
| 146 | expect(body).toMatch(/\bbody\b\s*\{[^}]*view-transition-name:\s*root/); | |
| 147 | }); | |
| 148 | }); |