Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,17 +1467,39 @@ describe('v2 layout invariants (CSS rule presence)', () => {
test('the zh hero title suffix cannot be split across lines', () => {
// TASK-211. The zh hero reads 「与你的___对话」: the rotator swaps the object
// and 对话 trails it. At 390px with the "Claude Code" term the suffix
// straddled the wrap — 对 ended line 2 at x=348.5 in a 342px column and 话
// sat alone on line 3 (measured live; at 320 the 整个团队 term did the same).
// `white-space: nowrap` is what produces the measured fix, so that is what
// is pinned here — jsdom has no line boxes, and the browser is the gate.
// straddled the wrap — the h1's content box ran 24 → 366, 对 fitted
// (348.5), and 话 is 44.3px wide and would have ended 26.8px past the edge,
// so 话 sat alone on line 3 (measured live; at 320 the 整个团队 term did the
// same). `white-space: nowrap` is what produces the measured fix, so that is
// what is pinned here — jsdom has no line boxes, and the browser is the gate.
// The companion assertion that the suffix is rendered as its own element
// (so this rule has something to bind to) lives in
// landingHeroContent.test.tsx; a declaration pin alone would survive the
// suffix being inlined as bare text.
expect(ruleBody(landing, '.v2-landing__title-suffix')).toContain('white-space: nowrap');
});

test('the zh hero suffix takes its own line on a phone', () => {
// TASK-213. Even with the suffix unbreakable (the test above), the zh h1
// gains a line whenever a term is long enough to push 对话 onto a third
// line: at 320–414 it measures 94.6px under three terms and 140.6px under
// the fourth, so the lede, the CTAs and everything below move 46px once per
// rotation cycle. lily-shen ruled option 1 (2026-09-30 00:23Z): the suffix
// is a block at ≤680, which makes the height the same under every term.
// Scoped with `mediaAt` rather than a whole-sheet read or an `indexOf`: the
// sheet has one (max-width: 680px) block today, and the helper is what makes
// a later one unable to satisfy this assertion by accident — `mediaAt`
// returns '' when the at-rule is absent, so a dropped block reds rather than
// silently matching some other rule. The declaration pin binds to the
// browser gate in ux-lead's sweep; `margin-left: 0` is asserted separately
// from `display: block` because it is what puts 对 on the column's left
// edge (the base rule's .18em indent would otherwise move with it).
const phone = mediaAt(landing, '@media (max-width: 680px) {');
expect(phone).not.toBe('');
expect(selectorRuleBody(phone, '.v2-landing__title-suffix')).toContain('display: block');
expect(selectorRuleBody(phone, '.v2-landing__title-suffix')).toContain('margin-left: 0');
});

test('reaction chips baseline-align emoji ink with the count (not box-centering)', () => {
// align-items: center centers the spans' layout boxes, but Apple Color
// Emoji ink extends below the baseline while digit ink does not, so the
Expand Down
37 changes: 23 additions & 14 deletions frontend/src/v2/landing/__tests__/landingHeroContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,28 @@ describe('V2LandingPage hero content (TASK-152)', () => {
expect(zhSuffix.length).toBeGreaterThan(0);

await act(async () => { await i18n.changeLanguage('zh-CN'); });
const zh = renderLanding();
const node = zh.container.querySelector('.v2-landing__title-suffix');
expect(node).not.toBeNull();
expect(node?.textContent).toBe(zhSuffix);
expect(node?.closest('h1.v2-landing__title')).not.toBeNull();
zh.unmount();

// en ships an empty suffix, so no span is rendered there and the rule is
// zh-only by construction — this is the control that keeps the pin from
// quietly becoming an English-hero assertion.
await act(async () => { await i18n.changeLanguage('en'); });
const en = renderLanding();
expect(en.container.querySelector('.v2-landing__title-suffix')).toBeNull();
en.unmount();
try {
const zh = renderLanding();
const node = zh.container.querySelector('.v2-landing__title-suffix');
expect(node).not.toBeNull();
expect(node?.textContent).toBe(zhSuffix);
expect(node?.closest('h1.v2-landing__title')).not.toBeNull();
zh.unmount();

// en ships an empty suffix, so no span is rendered there and the rule is
// zh-only by construction — this is the control that keeps the pin from
// quietly becoming an English-hero assertion.
await act(async () => { await i18n.changeLanguage('en'); });
const en = renderLanding();
expect(en.container.querySelector('.v2-landing__title-suffix')).toBeNull();
en.unmount();
} finally {
// TASK-213: the language is the suite's ambient state, so the restore has
// to survive a failure above it. Unguarded, a red assertion in here left
// every later test rendering zh, and their English misses read as defects
// in the page rather than in this test (found by sprint-review on #2033,
// reproduced with a forced-failure probe).
await act(async () => { await i18n.changeLanguage('en'); });
}
});
});
18 changes: 16 additions & 2 deletions frontend/src/v2/landing/v2-landing.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@
margin-left: 0.18em;
/* TASK-211: the zh hero is 「与你的___对话」, so this suffix is a word the
reader has to see whole. At 390 with the "Claude Code" term the two
characters straddled the wrap — 对 ended line 2 at x=348.5 in a 342px
column and 话 sat alone on line 3. nowrap makes the suffix one unbreakable
characters straddled the wrap: the h1's content box ran 24 → 366, so 对
fitted (348.5, i.e. 17.5px inside the right edge) and it was 话 — 44.3px
wide, which would have ended at 392.8, 26.8px past it — that sat alone on
line 3. nowrap makes the suffix one unbreakable
unit: it moves to the next line as a pair instead of splitting. Measured
live at 390 (44px/46px) and again at 320, where the 整个团队 term split it
the same way. en ships an empty suffix and renders no span, so this is
Expand Down Expand Up @@ -1250,6 +1252,18 @@
.v2-landing__footer { padding-top: 32px; }
.v2-landing__footer-legal { padding: 16px 0 20px; }
.v2-landing__title { font-size: 44px; line-height: 46px; }
/* TASK-213 (lily-shen's ruling, option 1): on a phone the suffix takes its own
line, so the title is 「与你的 / <term> / 对话」 at every width and only the
term rotates. Without this the h1 is 94.6px under three of the four terms
and 140.6px under the fourth at 320–414, so the lede, the CTAs and every
section below jumped 46px once per cycle (measured on main abe19fe0 and
again on #2033). `margin-left: 0` is part of the rule rather than tidying:
the base rule's .18em is the inline separator, and on its own line it would
indent 对 by 7.9px. Specificity is the base rule's (0,1,0) and this block is
later in the sheet, so it wins by order — the same mechanism the TASK-205
scroll-padding-top above documents. TASK-211's nowrap still applies here;
this rule changes `display`, so the two compose. */
.v2-landing__title-suffix { display: block; margin-left: 0; }
.v2-landing__lede { font-size: 16px; line-height: 26px; }
.v2-landing__nav { gap: 14px; }
.v2-landing__nav .v2-landing__navlink { display: none; }
Expand Down
Loading