brackt/app/models/__tests__/ncaa-68-bracket.test.ts
Chris Parsons 4bffa40606
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations

Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.

no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).

consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-non-null-assertion lint violations and promote to error

Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.

Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers

Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.

- prefer-add-event-listener: converted onchange/onclick/onload
  assignments to addEventListener in useDraftNotifications.ts and
  admin.data-sync.tsx; stored changeHandler ref for proper cleanup
  with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
  side-effect imports (*.css, @testing-library/jest-dom,
  @testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
  cypress/support/e2e.ts (file already has an import)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TypeScript errors from no-non-null-assertion fixes

Two fixes introduced by the non-null assertion cleanup produced type
errors:

- scoring-event.ts: `?? ""` was wrong type for a participant object map;
  restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
  truthy guarantee, causing TS18047 on the write-back block; added
  `participant &&` guard before accessing its properties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add npm run typecheck as Stop hook in Claude settings

Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00

387 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from "vitest";
import {
NCAA_68,
getScoringRoundType,
buildNCAA68SlotMap,
matchIndexForSeedSlot,
STANDARD_BRACKET_SEEDING,
} from "~/lib/bracket-templates";
/**
* NCAA 68 Bracket Unit Tests - Phase 2.8
*
* Tests the NCAA 68 tournament template structure and scoring logic:
* - 68 teams total (60 direct seeds + 8 in First Four)
* - First Four: 4 play-in games (8 teams)
* - Round of 64: 32 games
* - Only Elite Eight and beyond score fantasy points
*/
describe("NCAA 68 Bracket Template - Phase 2.8", () => {
describe("Template Structure", () => {
it("has correct total teams", () => {
expect(NCAA_68.totalTeams).toBe(68);
});
it("has all 7 rounds", () => {
expect(NCAA_68.rounds).toHaveLength(7);
expect(NCAA_68.rounds[0].name).toBe("First Four");
expect(NCAA_68.rounds[1].name).toBe("Round of 64");
expect(NCAA_68.rounds[2].name).toBe("Round of 32");
expect(NCAA_68.rounds[3].name).toBe("Sweet Sixteen");
expect(NCAA_68.rounds[4].name).toBe("Elite Eight");
expect(NCAA_68.rounds[5].name).toBe("Final Four");
expect(NCAA_68.rounds[6].name).toBe("Championship");
});
it("has correct match counts per round", () => {
expect(NCAA_68.rounds[0].matchCount).toBe(4); // First Four
expect(NCAA_68.rounds[1].matchCount).toBe(32); // Round of 64
expect(NCAA_68.rounds[2].matchCount).toBe(16); // Round of 32
expect(NCAA_68.rounds[3].matchCount).toBe(8); // Sweet Sixteen
expect(NCAA_68.rounds[4].matchCount).toBe(4); // Elite Eight (Quarterfinals)
expect(NCAA_68.rounds[5].matchCount).toBe(2); // Final Four (Semifinals)
expect(NCAA_68.rounds[6].matchCount).toBe(1); // Championship (Finals)
});
it("has correct round feeding structure", () => {
expect(NCAA_68.rounds[0].feedsInto).toBe("Round of 64");
expect(NCAA_68.rounds[1].feedsInto).toBe("Round of 32");
expect(NCAA_68.rounds[2].feedsInto).toBe("Sweet Sixteen");
expect(NCAA_68.rounds[3].feedsInto).toBe("Elite Eight");
expect(NCAA_68.rounds[4].feedsInto).toBe("Final Four");
expect(NCAA_68.rounds[5].feedsInto).toBe("Championship");
expect(NCAA_68.rounds[6].feedsInto).toBeNull(); // Championship has no next round
});
});
describe("Scoring Configuration", () => {
it("marks scoring to start at Elite Eight", () => {
expect(NCAA_68.scoringStartsAtRound).toBe("Elite Eight");
});
it("marks First Four as non-scoring", () => {
const firstFour = NCAA_68.rounds[0];
expect(firstFour.name).toBe("First Four");
expect(firstFour.isScoring).toBe(false);
});
it("marks Round of 64 as non-scoring", () => {
const roundOf64 = NCAA_68.rounds[1];
expect(roundOf64.name).toBe("Round of 64");
expect(roundOf64.isScoring).toBe(false);
});
it("marks Round of 32 as non-scoring", () => {
const roundOf32 = NCAA_68.rounds[2];
expect(roundOf32.name).toBe("Round of 32");
expect(roundOf32.isScoring).toBe(false);
});
it("marks Sweet Sixteen as non-scoring", () => {
const sweetSixteen = NCAA_68.rounds[3];
expect(sweetSixteen.name).toBe("Sweet Sixteen");
expect(sweetSixteen.isScoring).toBe(false);
});
it("marks Elite Eight as scoring (Quarterfinals)", () => {
const eliteEight = NCAA_68.rounds[4];
expect(eliteEight.name).toBe("Elite Eight");
expect(eliteEight.isScoring).toBe(true);
});
it("marks Final Four as scoring (Semifinals)", () => {
const finalFour = NCAA_68.rounds[5];
expect(finalFour.name).toBe("Final Four");
expect(finalFour.isScoring).toBe(true);
});
it("marks Championship as scoring (Finals)", () => {
const championship = NCAA_68.rounds[6];
expect(championship.name).toBe("Championship");
expect(championship.isScoring).toBe(true);
});
it("has exactly 3 scoring rounds (Elite Eight, Final Four, Championship)", () => {
const scoringRounds = NCAA_68.rounds.filter((r) => r.isScoring);
expect(scoringRounds).toHaveLength(3);
expect(scoringRounds[0].name).toBe("Elite Eight");
expect(scoringRounds[1].name).toBe("Final Four");
expect(scoringRounds[2].name).toBe("Championship");
});
it("has exactly 4 non-scoring rounds (FF, R64, R32, S16)", () => {
const nonScoringRounds = NCAA_68.rounds.filter((r) => !r.isScoring);
expect(nonScoringRounds).toHaveLength(4);
});
});
describe("Scoring Round Types", () => {
it("identifies Elite Eight as quarterfinals", () => {
const type = getScoringRoundType("Elite Eight", NCAA_68);
expect(type).toBe("quarterfinals");
});
it("identifies Final Four as semifinals", () => {
const type = getScoringRoundType("Final Four", NCAA_68);
expect(type).toBe("semifinals");
});
it("identifies Championship as finals", () => {
const type = getScoringRoundType("Championship", NCAA_68);
expect(type).toBe("finals");
});
it("returns null for non-scoring rounds", () => {
expect(getScoringRoundType("First Four", NCAA_68)).toBeNull();
expect(getScoringRoundType("Round of 64", NCAA_68)).toBeNull();
expect(getScoringRoundType("Round of 32", NCAA_68)).toBeNull();
expect(getScoringRoundType("Sweet Sixteen", NCAA_68)).toBeNull();
});
});
describe("Tournament Math", () => {
it("has correct number of total matches", () => {
// 4 + 32 + 16 + 8 + 4 + 2 + 1 = 67 total matches
const totalMatches = NCAA_68.rounds.reduce((sum, round) => sum + round.matchCount, 0);
expect(totalMatches).toBe(67);
});
it("eliminates 60 teams before Elite Eight (4+32+16+8)", () => {
const nonScoringRounds = NCAA_68.rounds.filter((r) => !r.isScoring);
const teamsEliminatedBeforeEliteEight = nonScoringRounds.reduce(
(sum, round) => sum + round.matchCount,
0
);
expect(teamsEliminatedBeforeEliteEight).toBe(60); // 4+32+16+8
});
it("has 8 teams in Elite Eight (top 8 scoring)", () => {
const eliteEightRound = NCAA_68.rounds.find((r) => r.name === "Elite Eight");
const teamsInEliteEight = (eliteEightRound?.matchCount ?? 0) * 2;
expect(teamsInEliteEight).toBe(8);
});
it("verifies only 8 teams score fantasy points (Q18)", () => {
// Elite Eight has 4 matches = 8 teams
// These are the ONLY teams that can score fantasy points
const scoringRounds = NCAA_68.rounds.filter((r) => r.isScoring);
const eliteEightMatches = scoringRounds[0].matchCount;
const scoringTeamsCount = eliteEightMatches * 2;
expect(scoringTeamsCount).toBe(8);
});
});
describe("Phase 2.8 Requirements", () => {
it("meets requirement: 68 total teams", () => {
expect(NCAA_68.totalTeams).toBe(68);
});
it("meets requirement: 4 First Four play-in games", () => {
const firstFour = NCAA_68.rounds[0];
expect(firstFour.matchCount).toBe(4);
});
it("meets requirement: Only Elite Eight and beyond score points", () => {
const scoringRoundNames = NCAA_68.rounds
.filter((r) => r.isScoring)
.map((r) => r.name);
expect(scoringRoundNames).toEqual(["Elite Eight", "Final Four", "Championship"]);
});
it("meets requirement: Early elimination = 0 points (per Q20)", () => {
const firstFour = NCAA_68.rounds[0];
const roundOf64 = NCAA_68.rounds[1];
const roundOf32 = NCAA_68.rounds[2];
const sweetSixteen = NCAA_68.rounds[3];
expect(firstFour.isScoring).toBe(false); // 0 points
expect(roundOf64.isScoring).toBe(false); // 0 points
expect(roundOf32.isScoring).toBe(false); // 0 points
expect(sweetSixteen.isScoring).toBe(false); // 0 points
});
it("verifies total participants: 60 direct + 8 First Four", () => {
const firstFourTeams = 4 * 2; // 4 games × 2 teams = 8 teams
const directSeeds = 68 - firstFourTeams; // 60 teams directly seeded
expect(directSeeds).toBe(60);
expect(firstFourTeams).toBe(8);
expect(directSeeds + firstFourTeams).toBe(68);
});
});
describe("Region Config (2026)", () => {
it("has 4 named regions", () => {
expect(NCAA_68.regions).toBeDefined();
expect(NCAA_68.regions).toHaveLength(4);
});
it("has regions in order: East, South, West, Midwest", () => {
const names = (NCAA_68.regions ?? []).map((r) => r.name);
expect(names).toEqual(["East", "South", "West", "Midwest"]);
});
it("East has 16 direct seeds and no play-ins", () => {
const east = (NCAA_68.regions ?? [])[0];
expect(east.directSeeds).toHaveLength(16);
expect(east.playIns).toHaveLength(0);
});
it("South has 15 direct seeds and one 16-seed play-in", () => {
const south = (NCAA_68.regions ?? [])[1];
expect(south.directSeeds).toHaveLength(15);
expect(south.playIns).toHaveLength(1);
expect(south.playIns[0].seedSlot).toBe(16);
});
it("West has 15 direct seeds and one 11-seed play-in", () => {
const west = (NCAA_68.regions ?? [])[2];
expect(west.directSeeds).toHaveLength(15);
expect(west.playIns).toHaveLength(1);
expect(west.playIns[0].seedSlot).toBe(11);
});
it("Midwest has 14 direct seeds and two play-ins (11 and 16)", () => {
const midwest = (NCAA_68.regions ?? [])[3];
expect(midwest.directSeeds).toHaveLength(14);
expect(midwest.playIns).toHaveLength(2);
expect(midwest.playIns[0].seedSlot).toBe(11);
expect(midwest.playIns[1].seedSlot).toBe(16);
});
it("total direct seeds across all regions equals 60", () => {
const total = (NCAA_68.regions ?? []).reduce((sum, r) => sum + r.directSeeds.length, 0);
expect(total).toBe(60);
});
it("total play-in teams across all regions equals 8", () => {
const total = (NCAA_68.regions ?? []).reduce(
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
0
);
expect(total).toBe(8);
});
it("direct + play-in teams sum to 68", () => {
const direct = (NCAA_68.regions ?? []).reduce((sum, r) => sum + r.directSeeds.length, 0);
const playIn = (NCAA_68.regions ?? []).reduce(
(sum, r) => sum + r.playIns.reduce((s, pi) => s + pi.teams, 0),
0
);
expect(direct + playIn).toBe(68);
});
});
describe("buildNCAA68SlotMap", () => {
const slotMap = buildNCAA68SlotMap(NCAA_68.regions ?? []);
it("East starts at index 0", () => {
expect(slotMap.directOffsets[0]).toBe(0);
});
it("South starts at index 16 (after East's 16 direct seeds)", () => {
expect(slotMap.directOffsets[1]).toBe(16);
});
it("West starts at index 31 (after East 16 + South 15)", () => {
expect(slotMap.directOffsets[2]).toBe(31);
});
it("Midwest starts at index 46 (after East 16 + South 15 + West 15)", () => {
expect(slotMap.directOffsets[3]).toBe(46);
});
it("totalDirect is 60", () => {
expect(slotMap.totalDirect).toBe(60);
});
it("totalPlayIn is 8", () => {
expect(slotMap.totalPlayIn).toBe(8);
});
it("has 4 play-in offsets", () => {
expect(slotMap.playInOffsets).toHaveLength(4);
});
it("FF1 (South 16-seed) starts at index 60", () => {
const ff1 = slotMap.playInOffsets[0];
expect(ff1.regionIndex).toBe(1); // South
expect(ff1.seedSlot).toBe(16);
expect(ff1.startIndex).toBe(60);
});
it("FF2 (West 11-seed) starts at index 62", () => {
const ff2 = slotMap.playInOffsets[1];
expect(ff2.regionIndex).toBe(2); // West
expect(ff2.seedSlot).toBe(11);
expect(ff2.startIndex).toBe(62);
});
it("FF3 (Midwest 11-seed) starts at index 64", () => {
const ff3 = slotMap.playInOffsets[2];
expect(ff3.regionIndex).toBe(3); // Midwest
expect(ff3.seedSlot).toBe(11);
expect(ff3.startIndex).toBe(64);
});
it("FF4 (Midwest 16-seed) starts at index 66", () => {
const ff4 = slotMap.playInOffsets[3];
expect(ff4.regionIndex).toBe(3); // Midwest
expect(ff4.seedSlot).toBe(16);
expect(ff4.startIndex).toBe(66);
});
});
describe("matchIndexForSeedSlot", () => {
it("seed 16 is at match index 0 (1v16)", () => {
expect(matchIndexForSeedSlot(16)).toBe(0);
});
it("seed 1 is at match index 0 (1v16)", () => {
expect(matchIndexForSeedSlot(1)).toBe(0);
});
it("seed 11 is at match index 4 (6v11)", () => {
expect(matchIndexForSeedSlot(11)).toBe(4);
});
it("seed 6 is at match index 4 (6v11)", () => {
expect(matchIndexForSeedSlot(6)).toBe(4);
});
it("all 8 standard seeding pairs are covered", () => {
for (const [hi, lo] of STANDARD_BRACKET_SEEDING) {
expect(() => matchIndexForSeedSlot(hi)).not.toThrow();
expect(() => matchIndexForSeedSlot(lo)).not.toThrow();
}
});
});
describe("First Four → Round of 64 match number derivation", () => {
const slotMap = buildNCAA68SlotMap(NCAA_68.regions ?? []);
// r64MatchNumber = regionIndex * 8 + matchIndexForSeedSlot(seedSlot) + 1
it("FF1 (South region index 1, seed 16) → R64 match 9", () => {
const ff1 = slotMap.playInOffsets[0]; // South 16-seed
const r64 = ff1.regionIndex * 8 + matchIndexForSeedSlot(ff1.seedSlot) + 1;
expect(r64).toBe(9); // East occupies matches 1-8; South match index 0 → 9
});
it("FF2 (West region index 2, seed 11) → R64 match 21", () => {
const ff2 = slotMap.playInOffsets[1]; // West 11-seed
const r64 = ff2.regionIndex * 8 + matchIndexForSeedSlot(ff2.seedSlot) + 1;
expect(r64).toBe(21); // West starts at 17; match index 4 → 17+4=21
});
it("FF3 (Midwest region index 3, seed 11) → R64 match 29", () => {
const ff3 = slotMap.playInOffsets[2]; // Midwest 11-seed
const r64 = ff3.regionIndex * 8 + matchIndexForSeedSlot(ff3.seedSlot) + 1;
expect(r64).toBe(29); // Midwest starts at 25; match index 4 → 25+4=29
});
it("FF4 (Midwest region index 3, seed 16) → R64 match 25", () => {
const ff4 = slotMap.playInOffsets[3]; // Midwest 16-seed
const r64 = ff4.regionIndex * 8 + matchIndexForSeedSlot(ff4.seedSlot) + 1;
expect(r64).toBe(25); // Midwest starts at 25; match index 0 → 25
});
});
});