// Wave 4B — LIVE GRADE-SHIFT timeline helper + component (source-asserted). // Locks the color law (green toward / amber against / dim flat), the revision // strike-through, and the <3-point self-hide. const fs = require('fs'); const path = require('path'); const { buildGradeTimeline, classifyMove, cleanHistory, TOWARD_COLOR, AGAINST_COLOR, FLAT_COLOR, MIN_POINTS, } = require('../../web/src/lib/gradeShift'); const read = (rel) => fs.readFileSync(path.join(__dirname, '..', '..', 'web', 'src', rel), 'utf8'); describe('gradeShift helper — color law', () => { it('color tokens are green / amber / dim (never red)', () => { expect(TOWARD_COLOR).toBe('var(--g-a)'); expect(AGAINST_COLOR).toBe('var(--amber)'); expect(FLAT_COLOR).toBe('var(--text-1)'); for (const c of [TOWARD_COLOR, AGAINST_COLOR, FLAT_COLOR]) { expect(c).not.toBe('var(--miss)'); } }); it('OVER: line up = TOWARD (green), line down = AGAINST (amber), no move = FLAT (dim)', () => { expect(classifyMove(0.5, 'Over')).toEqual({ dir: 'toward', color: TOWARD_COLOR }); expect(classifyMove(-0.5, 'Over')).toEqual({ dir: 'against', color: AGAINST_COLOR }); expect(classifyMove(0, 'Over')).toEqual({ dir: 'flat', color: FLAT_COLOR }); }); it('UNDER inverts the sign: line down = TOWARD (green), line up = AGAINST (amber)', () => { expect(classifyMove(-0.5, 'Under')).toEqual({ dir: 'toward', color: TOWARD_COLOR }); expect(classifyMove(0.5, 'Under')).toEqual({ dir: 'against', color: AGAINST_COLOR }); }); }); describe('gradeShift helper — timeline assembly', () => { it('self-hides below 3 real captured points', () => { expect(buildGradeTimeline({ history: null }).show).toBe(false); expect(buildGradeTimeline({ history: [{ t: '1', line: 5.5 }] }).show).toBe(false); expect(buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: 5.5 }] }).show).toBe(false); expect(buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: 6 }, { t: '3', line: 6.5 }] }).show).toBe(true); }); it('drops non-numeric points (Number(null)===0 guard)', () => { const tl = buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: null }, { t: '3', line: 6.5 }] }); expect(tl.points.length).toBe(2); // the null point is dropped, not coerced to 0 expect(cleanHistory([{ line: null }, { line: 3 }])).toHaveLength(1); }); it('net move on an OVER that rose reads TOWARD (green)', () => { const tl = buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: 6 }, { t: '3', line: 6.5 }], side: 'Over' }); expect(tl.net.dir).toBe('toward'); expect(tl.net.color).toBe(TOWARD_COLOR); expect(tl.firstLine).toBe(5.5); expect(tl.lastLine).toBe(6.5); }); it('net move on an OVER that fell reads AGAINST (amber)', () => { const tl = buildGradeTimeline({ history: [{ t: '1', line: 6.5 }, { t: '2', line: 6 }, { t: '3', line: 5.5 }], side: 'Over' }); expect(tl.net.dir).toBe('against'); expect(tl.net.color).toBe(AGAINST_COLOR); }); it('a revision surfaces the ORIGINAL grade struck (from) alongside the new (to)', () => { const tl = buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: 6 }, { t: '3', line: 6.5 }], grade: 'B', revisedFrom: 'A', }); expect(tl.revision).toEqual({ from: 'A', to: 'B' }); }); it('no revision when there was no prior grade or it is unchanged', () => { const base = [{ t: '1', line: 5.5 }, { t: '2', line: 6 }, { t: '3', line: 6.5 }]; expect(buildGradeTimeline({ history: base, grade: 'A' }).revision).toBeNull(); expect(buildGradeTimeline({ history: base, grade: 'A', revisedFrom: 'A' }).revision).toBeNull(); }); it('accepts the snapshot field name revised_from_grade too', () => { const tl = buildGradeTimeline({ history: [{ t: '1', line: 5.5 }, { t: '2', line: 6 }, { t: '3', line: 6.5 }], grade: 'C', revised_from_grade: 'B', }); expect(tl.revision).toEqual({ from: 'B', to: 'C' }); }); it('MIN_POINTS matches the LineSparkline floor of 3', () => { expect(MIN_POINTS).toBe(3); }); }); describe('GradeShift component', () => { const src = read('components/vyndr/GradeShift.tsx'); it('self-hides on !tl.show (returns null)', () => { expect(src).toContain('if (!tl.show) return null'); }); it('renders the original grade struck-through when revised', () => { expect(src).toContain('tl.revision'); expect(src).toContain('line-through'); expect(src).toContain('tl.revision.from'); expect(src).toContain('tl.revision.to'); }); it('is a data surface — mono, no glitch classes', () => { expect(src).toContain('mono'); expect(src).not.toMatch(/wm-tear|glitch-shift|head-tear|glitch-hover/); }); }); describe('GradeResultCard mounts the timeline', () => { const src = read('components/vyndr/GradeResultCard.tsx'); it('imports and renders GradeShift with the optional history fields', () => { expect(src).toContain("import GradeShift from '@/components/vyndr/GradeShift'"); expect(src).toContain(' | null'); expect(src).toContain('revisedFrom'); }); });