Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

u-polish.test.tsBlame148 lines · 1 contributor
dc26881CC LABS App1/**
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 */
18import { describe, it, expect } from "bun:test";
19import app from "../app";
20
21const HOME = "/";
22
23async function fetchHomeHtml(): Promise<string> {
24 const res = await app.request(HOME);
25 expect(res.status).toBe(200);
26 return await res.text();
27}
28
29describe("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
78describe("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
120describe("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});