/** * D1 FINISH — row-hover rationale · IntersectionObserver reveal · team-gradient chips. * Ported to the exact System-artboard anatomy. The honesty properties are the point. */ const { rationaleFor, hasRationale } = require('../../web/src/lib/rowRationale'); const chips = require('../../web/src/lib/teamChips'); const reveal = require('../../web/src/lib/reveal'); const { bootDelayMs } = require('../../web/src/lib/reactions'); describe('row-hover rationale — real grade factors or NOTHING', () => { const real = { reasoning: { summary: 'Averaging 5.8 assists over the last 5. L20 4.5. 0.7 below the line of 6.5.' }, kill_conditions_triggered: [{ code: 'TOP_DEFENSE', reason: 'Opponent ranks top-five defending this stat.' }], }; test('surfaces the grade OWN reasoning + real kill conditions', () => { const out = rationaleFor(real); expect(out.summary).toMatch(/5\.8 assists/); expect(out.kills).toEqual([{ code: 'TOP_DEFENSE', reason: 'Opponent ranks top-five defending this stat.' }]); }); test('a row with NO reasoning is honest-absent — null, never a generic why', () => { expect(rationaleFor({ grade: 'B' })).toBeNull(); expect(rationaleFor({ reasoning: {} })).toBeNull(); expect(rationaleFor({ reasoning: { summary: ' ' } })).toBeNull(); expect(rationaleFor(null)).toBeNull(); expect(hasRationale({})).toBe(false); }); test('a LOCKED/gated reasoning is treated as absent — never paraphrased or leaked', () => { expect(rationaleFor({ reasoning: { summary: 'secret', locked: true } })).toBeNull(); expect(rationaleFor({ reasoning: { summary: 'secret' }, tier_gated: true })).toBeNull(); }); test('a kill condition with no reason explains nothing and is dropped', () => { expect(rationaleFor({ kill_conditions_triggered: [{ code: 'X' }] })).toBeNull(); }); test('kills alone (no summary) still count as a real why', () => { const out = rationaleFor({ kill_conditions_triggered: [{ code: 'A', reason: 'Real reason.' }] }); expect(out.summary).toBeNull(); expect(out.kills).toHaveLength(1); }); }); describe('team-gradient chips — partial coverage, honest-neutral elsewhere', () => { test('known teams use the artboard pair VERBATIM', () => { const bos = chips.chipStyle('BOS'); expect(bos.known).toBe(true); expect(bos.style.background).toBe('linear-gradient(135deg,#BD3039,#6b1c22)'); }); test('an UNKNOWN team renders honest-neutral — no guessed colour, no blank gap', () => { const x = chips.chipStyle('XYZ'); expect(x.known).toBe(false); expect(x.style.background).toBe('transparent'); expect(x.style.border).toMatch(/var\(--border-hi\)/); expect(x.style.width).toBe(10); // still occupies the slot }); test('coverage is PARTIAL and reported, not hidden', () => { const c = chips.coverage(80); expect(c.known).toBe(10); expect(c.total).toBe(80); expect(c.known).toBeLessThan(c.total); // we do not invent the rest }); test('chips carry the Rev-3 geometry (10px, inside-row placement)', () => { const s = chips.chipStyle('BOS').style; expect(s.width).toBe(10); expect(s.height).toBe(10); expect(s.borderRadius).toBe(3); expect(s.marginRight).toBe(3); }); test('the ranked opacity ramp dims chips with their row (1/.86/.64/.48)', () => { expect(chips.rankOpacity(0)).toBe(1); expect(chips.rankOpacity(1)).toBe(0.86); expect(chips.rankOpacity(2)).toBe(0.64); expect(chips.rankOpacity(3)).toBe(0.48); expect(chips.rankOpacity(99)).toBe(0.48); // clamps to the tail }); test('handles junk input without throwing or claiming a colour', () => { for (const v of [null, undefined, '', 123]) expect(chips.chipStyle(v).known).toBe(false); }); }); describe('reveal — once on view, reusing D1-A motion discipline', () => { test('with no IntersectionObserver it reveals IMMEDIATELY (never hides content)', () => { const node = { dataset: {}, style: {}, classList: { add(c) { this._c = c; } } }; const stop = reveal.observeRows([node], null); expect(node.classList._c).toBe('vy-rowin'); expect(node.dataset.revealed).toBe('1'); expect(typeof stop).toBe('function'); }); test('stagger reuses D1-A bootDelayMs — ONE source of truth, 60ms steps', () => { const nodes = [0, 1, 2].map(() => ({ dataset: {}, style: {}, classList: { add() {} } })); reveal.observeRows(nodes, null); expect(nodes[0].style.animationDelay).toBe(`${bootDelayMs(0)}ms`); expect(nodes[1].style.animationDelay).toBe('60ms'); expect(nodes[2].style.animationDelay).toBe('120ms'); }); test('a row already revealed is NOT re-fired (react then rest, never a loop)', () => { let adds = 0; const node = { dataset: { revealed: '1' }, style: {}, classList: { add() { adds += 1; } } }; reveal.observeRows([node], null); expect(adds).toBe(0); }); test('reduced motion is handled in CSS, so the row is visible either way', () => { const css = require('fs').readFileSync( require('path').join(__dirname, '../../web/src/app/globals.css'), 'utf8'); expect(css).toMatch(/prefers-reduced-motion: reduce/); expect(css).toMatch(/\.vy-rowin/); }); });