e453c24d2c
Final phase of the VYNDR 2.0 conversion (Sessions 33-39). Verify -> fix -> lock. Frontend-only; zero backend changes. §13 automated checklist: all PASS or FIXED. - QA.1 token resolution FIXED: ProcessingGrade #00ffb8 -> var(--g-ap); game/[id] sport literals -> var(--s-*). Remaining hex documented as intentional (var-with-fallback, Next metadata, bespoke intel-surface shades). - QA.6 glitch discipline: ZERO glitch on data components. - QA.4/5/8/11/16/18 verified; QA.9 (cmd palette) documented deferred; QA.17 (AI slop) flagged for Kev's manual browser review. De-flake: soccerFeatureExtractorCascade hit Jest's 5s default under full-suite load (falls through to live adapters on cache miss) -> jest.setTimeout(20000), same family as the S32 pipeline test. Verified stable across 3 full-suite runs. New: tests/unit/vyndrParityQA.test.js (17 tests locking the parity invariants). Backend 1890 -> 1907, 146 suites, zero regressions (stable x3). Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.3 KiB
JavaScript
93 lines
3.3 KiB
JavaScript
// VYNDR 2.0 — Phase H parity QA (Session 39). Locks the automated §13
|
|
// checklist invariants so the design conversion can't silently regress.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
|
|
|
describe('QA.6 — glitch discipline (data NEVER glitches)', () => {
|
|
const GLITCH = /wm-tear|glitch-shift|head-tear|glitch-hover/;
|
|
it.each([
|
|
'components/vyndr/GradeResultCard.tsx',
|
|
'components/vyndr/GameCard.tsx',
|
|
'components/GameCard.tsx',
|
|
'components/vyndr/ProcessingGrade.tsx',
|
|
])('%s contains no glitch classes', (rel) => {
|
|
expect(GLITCH.test(read(rel))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('QA.1 — token resolution (no literals that duplicate a token)', () => {
|
|
it('ProcessingGrade uses the A+ token, not the raw #00ffb8 literal', () => {
|
|
const src = read('components/vyndr/ProcessingGrade.tsx');
|
|
expect(src).not.toContain('#00ffb8');
|
|
expect(src).toContain('var(--g-ap)');
|
|
});
|
|
it('game detail uses sport tokens, not duplicated sport hex', () => {
|
|
const src = read('app/game/[id]/page.tsx');
|
|
expect(src).toContain('var(--s-nba)');
|
|
expect(src).not.toMatch(/#E94B3C/);
|
|
});
|
|
});
|
|
|
|
describe('QA.4 — Bloomberg best/worst line pattern', () => {
|
|
it('legacy GameCard (live slate) tints best green / worst red', () => {
|
|
const src = read('components/GameCard.tsx');
|
|
expect(src).toContain('rgba(0,212,160,.13)');
|
|
expect(src).toContain('rgba(255,82,82,.07)');
|
|
expect(src).toMatch(/bestAway|bestHome/);
|
|
});
|
|
});
|
|
|
|
describe('QA.5 — wordmark everywhere', () => {
|
|
it.each([
|
|
'components/Nav.tsx',
|
|
'components/Footer.tsx',
|
|
'app/login/page.tsx',
|
|
'app/about/page.tsx',
|
|
'app/not-found.tsx',
|
|
])('%s renders the Wordmark', (rel) => {
|
|
expect(read(rel)).toContain('Wordmark');
|
|
});
|
|
});
|
|
|
|
describe('QA.11 — mobile parity (5-tab bar)', () => {
|
|
it('BottomTabBar declares Slate/Terminal/Scan/Ledger/More', () => {
|
|
const src = read('components/BottomTabBar.tsx');
|
|
['Slate', 'Terminal', 'Scan', 'Ledger', 'More'].forEach((t) => expect(src).toContain(`'${t}'`));
|
|
});
|
|
});
|
|
|
|
describe('QA.16 — no dead buttons', () => {
|
|
it.each(['components', 'app'])('no empty onClick handlers under web/src/%s', (dir) => {
|
|
const root = path.join(WEB, dir);
|
|
const offenders = [];
|
|
const walk = (d) => {
|
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
const fp = path.join(d, e.name);
|
|
if (e.isDirectory()) walk(fp);
|
|
else if (e.name.endsWith('.tsx') && /onClick=\{\}/.test(fs.readFileSync(fp, 'utf8'))) offenders.push(fp);
|
|
}
|
|
};
|
|
walk(root);
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('QA.18 — auth gate integration', () => {
|
|
it('AuthGate gates via lib/routes and bounces to /login?next=', () => {
|
|
const src = read('components/AuthGate.tsx');
|
|
expect(src).toContain('isGatedRoute');
|
|
expect(src).toContain('/login?next=');
|
|
});
|
|
it('gated route list covers the personal surfaces', () => {
|
|
const routes = require('../../web/src/lib/routes');
|
|
['/ledger', '/tracker', '/account', '/notifications'].forEach((r) =>
|
|
expect(routes.isGatedRoute(r)).toBe(true),
|
|
);
|
|
expect(routes.isGatedRoute('/dashboard')).toBe(false); // free funnel stays open
|
|
});
|
|
});
|