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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
u-polish.test.ts5.7 KB · 142 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
 * Block U — Senior polish pass smoke tests.
 *
 * 2026-06-10 update: `GET /` now serves the self-contained
 * `Landing2030Page` (src/views/landing-2030.tsx) — the legacy
 * `LandingPage` + master Layout CSS no longer render on the home route.
 * U1 assertions therefore target the 2030 hero contract; U2/U4 master-CSS
 * assertions target /help, a public Layout-rendered page that needs no DB.
 *
 * U1: hero — exactly two CTAs in the hero actions row, trust line
 *     beneath, product-card mock below the CTAs.
 * U2: button CSS — hover lifts every .btn by translateY(-1px) with a
 *     soft drop shadow; focus-visible uses box-shadow (not outline);
 *     primary CTA shimmers via background-position; disabled buttons
 *     never lift.
 * U4: master CSS contains the @view-transition block and the
 *     prefers-reduced-motion guard.
 *
 * No DB stubs, no mock pollution — pure rendering checks against the
 * already-mounted Hono app.
 */
import { describe, it, expect } from "bun:test";
import app from "../app";

const HOME = "/";
// Public, Layout-rendered, DB-free page that carries the master CSS.
const LAYOUT_PAGE = "/help";

async function fetchHomeHtml(): Promise<string> {
  const res = await app.request(HOME);
  expect(res.status).toBe(200);
  return await res.text();
}

async function fetchLayoutHtml(): Promise<string> {
  const res = await app.request(LAYOUT_PAGE);
  expect(res.status).toBe(200);
  return await res.text();
}

describe("Block U1 — landing hero (2030 reboot)", () => {
  it("renders exactly two CTAs in the hero actions row", async () => {
    const body = await fetchHomeHtml();
    const start = body.indexOf('class="hero-actions');
    expect(start).toBeGreaterThan(-1);
    // The actions row is a tight window of <a … class="btn …"> anchors.
    const tail = body.slice(start, start + 600);
    const anchorMatches = tail.match(/<a[^>]*class="btn[^"]*"/g) || [];
    expect(anchorMatches.length).toBe(2);
    expect(tail).toContain('href="/register"');
    expect(tail).toContain('href="#loop"');
  });

  it("renders the trust line beneath the CTAs", async () => {
    const body = await fetchHomeHtml();
    const ctas = body.indexOf('class="hero-actions');
    const trust = body.indexOf('class="hero-trust');
    expect(ctas).toBeGreaterThan(-1);
    expect(trust).toBeGreaterThan(ctas);
    expect(body).toContain("Self-hosted · Git-native · Claude-first");
  });

  it("places the product-card mock below the CTAs", async () => {
    const body = await fetchHomeHtml();
    const ctas = body.indexOf('class="hero-actions');
    const card = body.indexOf('class="hero-card');
    expect(ctas).toBeGreaterThan(-1);
    expect(card).toBeGreaterThan(ctas);
  });
});

describe("Block U2 — button polish", () => {
  it("includes the universal hover-lift transform on .btn", async () => {
    const body = await fetchLayoutHtml();
    // The master CSS is inlined in <style> by Layout. We assert on the
    // hover-rule shape: `.btn:hover { … transform: translateY(-1px); … }`.
    expect(body).toMatch(/\.btn:hover\b[\s\S]*?transform:\s*translateY\(-1px\)/);
  });

  it("uses a soft drop shadow on .btn:hover (not just border)", async () => {
    const body = await fetchLayoutHtml();
    expect(body).toMatch(/\.btn:hover\b[\s\S]*?box-shadow:\s*0\s+4px\s+12px/);
  });

  it("uses box-shadow (not outline) for the .btn focus ring", async () => {
    const body = await fetchLayoutHtml();
    // Focus-visible rule must set box-shadow with the soft accent rgba.
    expect(body).toMatch(
      /\.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\)/
    );
    // And must NOT fall back to outline:2px on .btn anywhere.
    expect(body).not.toMatch(/\.btn:focus-visible[^{]*\{\s*outline:\s*2px/);
  });

  it("disables hover-lift on disabled buttons", async () => {
    const body = await fetchLayoutHtml();
    // The disabled rule sets transform:none AND opacity:0.5.
    expect(body).toMatch(/\.btn:disabled[\s\S]*?transform:\s*none/);
    expect(body).toMatch(/\.btn:disabled[\s\S]*?opacity:\s*0\.5/);
  });

  it("shimmers the primary CTA via background-position transition", async () => {
    const body = await fetchLayoutHtml();
    // Primary button declares background-size 200% so the position
    // animation has somewhere to travel.
    expect(body).toMatch(/\.btn-primary\b[\s\S]*?background-size:\s*200%/);
    // The transition lists background-position with the 600ms duration.
    expect(body).toMatch(
      /\.btn-primary\b[\s\S]*?transition:[\s\S]*?background-position\s+600ms/
    );
  });
});

describe("Block U4 — view transitions", () => {
  it("includes the @view-transition opt-in", async () => {
    const body = await fetchLayoutHtml();
    expect(body).toContain("@view-transition");
    expect(body).toMatch(/@view-transition\s*\{\s*navigation:\s*auto;?\s*\}/);
  });

  it("declares both fade-out and fade-in keyframes for ::view-transition-*", async () => {
    const body = await fetchLayoutHtml();
    expect(body).toContain("::view-transition-old(root)");
    expect(body).toContain("::view-transition-new(root)");
    expect(body).toContain("@keyframes vt-fade-out");
    expect(body).toContain("@keyframes vt-fade-in");
  });

  it("disables the transition under prefers-reduced-motion", async () => {
    const body = await fetchLayoutHtml();
    // The reduced-motion block must mention the view-transition
    // pseudos and set animation-duration: 0s.
    expect(body).toMatch(
      /@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{[\s\S]*?::view-transition-(old|new)\(root\)[\s\S]*?animation-duration:\s*0s/
    );
  });

  it("attaches view-transition-name: root to body", async () => {
    const body = await fetchLayoutHtml();
    expect(body).toMatch(/\bbody\b\s*\{[^}]*view-transition-name:\s*root/);
  });
});