From 67ca6ff0381afe221fa4245e41ec5757268a25d4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:42:50 +0300 Subject: [PATCH] Refactor tests for consistency --- AGENTS.md | 6 + app/components/Pagination.test.ts | 30 +- .../layout/global-search-persisted.test.ts | 12 +- .../global-search-search-params.test.ts | 6 +- .../match-page/ingested-scoreboard.test.ts | 12 +- .../match-page-search-params.test.ts | 6 +- app/components/match-page/utils.test.ts | 16 +- .../objective-timeline-utils.test.ts | 26 +- app/db/json-columns.test.ts | 4 +- app/db/json-selections.test.ts | 12 +- .../admin/admin-search-params.test.ts | 6 +- app/features/admin/routes/admin.test.ts | 14 +- app/features/art/art-search-params.test.ts | 14 +- .../associations-search-params.test.ts | 6 +- .../associations/core/Association.test.ts | 28 +- app/features/ban/core/banned.test.ts | 20 +- .../analyzer-search-params.test.ts | 6 +- .../builds/BuildRepository.server.test.ts | 33 +- .../builds/builds-search-params.test.ts | 6 +- .../calendar-progression-form.test.ts | 32 +- .../calendar/calendar-search-params.test.ts | 14 +- app/features/calendar/calendar-utils.test.ts | 12 +- .../calendar/core/CalendarEvent.test.ts | 32 +- app/features/chat/chat-constants.test.ts | 88 +- app/features/chat/chat-last-read.test.ts | 8 +- app/features/chat/chat-search-params.test.ts | 8 +- .../comp-analyzer-search-params.test.ts | 8 +- .../friends/friends-search-params.test.ts | 6 +- app/features/img-export/core/RunComps.test.ts | 20 +- .../img-export/core/SeasonSummary.test.ts | 66 +- .../img-upload/ImageRepository.server.test.ts | 105 +- .../LeaderboardRepository.server.test.ts | 53 +- .../leaderboards-search-params.test.ts | 6 +- app/features/lfg/lfg-search-params.test.ts | 10 +- .../map-list-generator/core/MapList.test.ts | 121 +- .../map-list-generator-search-params.test.ts | 6 +- .../MatchProfileRepository.server.test.ts | 7 +- app/features/mmr/core/Seasons.test.ts | 14 +- .../calculator-search-params.test.ts | 8 +- app/features/params/core/WeaponParams.test.ts | 46 +- .../params/params-search-params.test.ts | 8 +- .../plus-suggestions-search-params.test.ts | 6 +- .../ScannerIngestRepository.server.test.ts | 42 +- .../scanner-ingest/core/Matches.test.ts | 229 ++-- .../scanner-ingest/core/Scoreboards.test.ts | 105 +- .../scanner-ingest/core/VodMatches.test.ts | 10 +- .../scanner-ingest/core/tests/fixtures.ts | 75 + app/features/scanner/README.md | 18 +- .../scanner/scanner-search-params.test.ts | 6 +- .../tests/{ => logic}/dedupe-events.test.ts | 12 +- .../tests/{ => logic}/match-builder.test.ts | 26 +- .../tests/{ => logic}/match-sets.test.ts | 9 +- .../tests/{ => logic}/scheduler.test.ts | 4 +- .../actions/scrims.request.server.test.ts | 4 +- app/features/scrims/core/Scrim.test.ts | 84 +- .../scrims/core/ScrimMapByMap.test.ts | 34 +- app/features/scrims/routes/scrims.new.test.ts | 8 +- app/features/scrims/scrims-schemas.test.ts | 8 +- .../scrims/scrims-search-params.test.ts | 6 +- app/features/scrims/scrims-utils.test.ts | 97 +- .../search/search-search-params.test.ts | 6 +- app/features/sendouq/q-constants.test.ts | 27 +- app/features/sendouq/q-search-params.test.ts | 12 +- .../settings/match-profile-schemas.test.ts | 8 +- .../settings/settings-search-params.test.ts | 6 +- .../sidebar/core/StreamRanking.test.ts | 20 +- .../actions/t.$customUrl.edit.server.test.ts | 14 +- .../team/actions/t.new.server.test.ts | 6 +- .../team/routes/t.$customUrl.edit.test.ts | 22 +- app/features/team/routes/t.$customUrl.test.ts | 42 +- app/features/team/team-search-params.test.ts | 4 +- app/features/team/team-utils.test.ts | 14 +- app/features/team/tests/fixtures.ts | 20 + .../tier-list-maker-search-params.test.ts | 10 +- .../tier-list-maker-utils.test.ts | 26 +- .../top-search-search-params.test.ts | 6 +- .../components/ExportDialog.test.ts | 12 +- .../tournament-admin-search-params.test.ts | 10 +- .../Bracket/useBracketSpoilerCensor.test.ts | 16 +- .../core/AbDivisions.test.ts | 24 +- .../tournament-bracket/core/Bracket.test.ts | 80 +- .../tournament-bracket/core/Deadline.test.ts | 28 +- .../tournament-bracket/core/PickBan.test.ts | 331 ++--- .../core/Progression.test.ts | 1202 +++++------------ .../core/RunningTournaments.test.ts | 12 +- .../tournament-bracket/core/Seeding.test.ts | 32 +- .../tournament-bracket/core/Swiss.test.ts | 68 +- .../core/Tournament.test.ts | 42 +- .../core/Tournament.timezone.test.ts | 6 +- .../engine/create/double-elimination.test.ts | 6 +- .../core/engine/create/helpers.test.ts | 20 +- .../core/engine/create/round-robin.test.ts | 14 +- .../engine/create/single-elimination.test.ts | 12 +- .../core/engine/general.test.ts | 18 +- .../propagation/double-elimination.test.ts | 12 +- .../engine/propagation/round-robin.test.ts | 14 +- .../core/engine/propagation/set.test.ts | 14 +- .../propagation/single-elimination.test.ts | 4 +- .../core/engine/propagation/update.test.ts | 20 +- .../core/summarizer.test.ts | 29 +- .../tournament-bracket-search-params.test.ts | 6 +- .../routes/to.$id.matches.$mid.test.ts | 48 +- ...rnament-organization-search-params.test.ts | 6 +- ...ournamentAuditLogRepository.server.test.ts | 53 +- .../TournamentRepository.finalize.test.ts | 26 +- .../TournamentRepository.searchByName.test.ts | 6 +- ...ournamentRepository.showcaseCounts.test.ts | 31 +- .../TournamentTeamRepository.server.test.ts | 103 +- .../tournament/core/Standings.test.ts | 36 +- .../tournament/core/sets.server.test.ts | 4 +- app/features/tournament/core/tiering.test.ts | 64 +- .../tournament-search-params.test.ts | 12 +- .../tournament/tournament-utils.test.ts | 499 +++---- .../UserCardRepository.server.test.ts | 155 ++- .../user-card/user-card-search-params.test.ts | 10 +- .../user-page/bio-json.server.test.ts | 4 +- .../core/badge-sorting.server.test.ts | 12 +- .../core/build-sorting.server.test.ts | 34 +- .../core/widgets/leaderboard-cache.test.ts | 4 +- .../custom-theme-json.server.test.ts | 6 +- app/features/user-page/in-game-name.test.ts | 8 +- .../routes/u.$identifier.edit.test.ts | 4 +- .../user-page/user-page-search-params.test.ts | 24 +- app/features/vods/vods-search-params.test.ts | 16 +- app/features/vods/vods-utils.test.ts | 50 +- app/form/SendouForm.browser.test.tsx | 394 ++---- app/hooks/useRecentlyReportedWeapons.test.ts | 6 +- app/hooks/useSpoilerFree.test.ts | 6 +- app/hooks/useUnseenFriendRequests.test.ts | 6 +- app/modules/cache/index.test.ts | 16 +- app/modules/csv.test.ts | 20 +- .../persisted-state/persisted-state.test.ts | 14 +- .../search-params/search-params.test.ts | 62 +- app/modules/twitch/vods.test.ts | 20 +- .../closeExpiredContinueVotes.test.ts | 22 +- app/routines/notifyCheckInStart.test.ts | 17 +- app/routines/resolveStaleSQMatches.test.ts | 17 +- app/utils/arrays.test.ts | 168 +-- app/utils/compression.test.ts | 16 +- app/utils/dates.test.ts | 16 +- app/utils/i18n.test.ts | 10 +- app/utils/numbers.test.ts | 168 +-- app/utils/random.test.ts | 32 +- app/utils/remix.server.test.ts | 16 +- app/utils/remix.test.ts | 10 +- app/utils/urls.test.ts | 8 +- app/utils/zod.test.ts | 66 +- vitest.scanner.config.ts | 5 +- vitest.unit.config.ts | 6 +- 149 files changed, 2656 insertions(+), 3679 deletions(-) create mode 100644 app/features/scanner-ingest/core/tests/fixtures.ts rename app/features/scanner/tests/{ => logic}/dedupe-events.test.ts (88%) rename app/features/scanner/tests/{ => logic}/match-builder.test.ts (97%) rename app/features/scanner/tests/{ => logic}/match-sets.test.ts (95%) rename app/features/scanner/tests/{ => logic}/scheduler.test.ts (99%) create mode 100644 app/features/team/tests/fixtures.ts diff --git a/AGENTS.md b/AGENTS.md index bf000a8a0..e7549dcbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,12 @@ - library used for unit testing is Vitest - Vitest browser mode can be used to write tests for components +- use `test`, not `it` +- name a test after the behaviour it establishes, with no `"should "` prefix (`test("returns null for an unknown id")`) +- `describe` takes the bare function name, except for files consumed through a `* as Module` import, where it takes `Module.fn` — the way callers write it +- when a test is `input -> expected output` with no setup, make it a `test.each` table rather than a run of near-identical `test` blocks; give every row a short label (`$why`, `%s`) so a failure names the case +- users come from `UserFactory.pool()` declared at module scope and filled in `beforeEach` — never a module-level `let` reassigned per test. Where positions carry meaning, name them with accessors next to the pool (`const actorId = () => users.id(1)`) +- fixture builders shared by more than one test file live in a `tests/` folder of the feature they belong to (`app/features//**/tests/fixtures.ts`); don't copy a builder into a second file ## i18n diff --git a/app/components/Pagination.test.ts b/app/components/Pagination.test.ts index fe2e84664..2c0bf695d 100644 --- a/app/components/Pagination.test.ts +++ b/app/components/Pagination.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { getPageNumbers } from "./Pagination"; /** What is rendered on a narrow (mobile) viewport — `desktopOnly` items are hidden. */ @@ -16,35 +16,35 @@ function desktopView(currentPage: number, pagesCount: number) { } describe("getPageNumbers", () => { - it("shows every page without ellipsis when there are 5 or fewer", () => { + test("shows every page without ellipsis when there are 5 or fewer", () => { expect(mobileView(2, 5)).toEqual([1, 2, 3, 4, 5]); expect(desktopView(2, 5)).toEqual([1, 2, 3, 4, 5]); }); - it("shows every page on desktop when there are 9 or fewer", () => { + test("shows every page on desktop when there are 9 or fewer", () => { expect(desktopView(2, 9)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); expect(desktopView(5, 9)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); }); - it("inserts a mobile ellipsis instead of silently hiding middle pages", () => { + test("inserts a mobile ellipsis instead of silently hiding middle pages", () => { // Regression: page=2 of 9 used to render "1 2 8 9" with no ellipsis expect(mobileView(2, 9)).toEqual([1, 2, 3, "...", 9]); }); - it("shows a bridging number on the first page instead of a lonely jump", () => { + test("shows a bridging number on the first page instead of a lonely jump", () => { // Regression: page=1 of 8 used to render "1 2 ... 8" with nothing in between expect(mobileView(1, 8)).toEqual([1, 2, 3, "...", 8]); expect(mobileView(1, 20)).toEqual([1, 2, 3, "...", 20]); expect(desktopView(1, 20)).toEqual([1, 2, 3, 4, "...", 20]); }); - it("shows a bridging number on the last page instead of a lonely jump", () => { + test("shows a bridging number on the last page instead of a lonely jump", () => { expect(mobileView(8, 8)).toEqual([1, "...", 6, 7, 8]); expect(mobileView(20, 20)).toEqual([1, "...", 18, 19, 20]); expect(desktopView(20, 20)).toEqual([1, "...", 17, 18, 19, 20]); }); - it("keeps the current page and its neighbours visible on mobile", () => { + test("keeps the current page and its neighbours visible on mobile", () => { const view = mobileView(5, 9); expect(view).toContain(4); expect(view).toContain(5); @@ -53,7 +53,7 @@ describe("getPageNumbers", () => { expect(view).toEqual([1, "...", 4, 5, 6, "...", 9]); }); - it("always keeps the first and last page", () => { + test("always keeps the first and last page", () => { for (const currentPage of [1, 7, 20]) { const view = mobileView(currentPage, 20); expect(view[0]).toBe(1); @@ -61,7 +61,7 @@ describe("getPageNumbers", () => { } }); - it("windows around the current page with ellipses on both sides for many pages", () => { + test("windows around the current page with ellipses on both sides for many pages", () => { expect(mobileView(10, 20)).toEqual([1, "...", 9, 10, 11, "...", 20]); expect(desktopView(10, 20)).toEqual([ 1, @@ -76,17 +76,17 @@ describe("getPageNumbers", () => { ]); }); - it("omits the leading ellipsis when the current page is near the start", () => { + test("omits the leading ellipsis when the current page is near the start", () => { expect(desktopView(2, 20)).toEqual([1, 2, 3, 4, "...", 20]); expect(mobileView(2, 20)).toEqual([1, 2, 3, "...", 20]); }); - it("omits the trailing ellipsis when the current page is near the end", () => { + test("omits the trailing ellipsis when the current page is near the end", () => { expect(desktopView(19, 20)).toEqual([1, "...", 17, 18, 19, 20]); expect(mobileView(19, 20)).toEqual([1, "...", 18, 19, 20]); }); - it("shows a bridging number instead of an ellipsis that hides a single page", () => { + test("shows a bridging number instead of an ellipsis that hides a single page", () => { // An ellipsis takes the same space as one page number, so replacing a // lone hidden page with "..." is never an improvement (same intent as the // edge "lonely jump" fix, but for windows one step inward). @@ -98,7 +98,7 @@ describe("getPageNumbers", () => { expect(mobileView(3, 6)).toEqual([1, 2, 3, 4, 5, 6]); }); - it("never renders an ellipsis in place of a single hidden page", () => { + test("never renders an ellipsis in place of a single hidden page", () => { for (let pagesCount = 1; pagesCount <= 25; pagesCount++) { for (let currentPage = 1; currentPage <= pagesCount; currentPage++) { for (const view of [ @@ -118,7 +118,7 @@ describe("getPageNumbers", () => { } }); - it("never produces duplicate page numbers", () => { + test("never produces duplicate page numbers", () => { for (let pagesCount = 1; pagesCount <= 25; pagesCount++) { for (let currentPage = 1; currentPage <= pagesCount; currentPage++) { const values = getPageNumbers(currentPage, pagesCount) @@ -129,7 +129,7 @@ describe("getPageNumbers", () => { } }); - it("always includes the current page in both views", () => { + test("always includes the current page in both views", () => { for (let pagesCount = 1; pagesCount <= 25; pagesCount++) { for (let currentPage = 1; currentPage <= pagesCount; currentPage++) { expect(mobileView(currentPage, pagesCount)).toContain(currentPage); diff --git a/app/components/layout/global-search-persisted.test.ts b/app/components/layout/global-search-persisted.test.ts index bad6c803e..b6d48d262 100644 --- a/app/components/layout/global-search-persisted.test.ts +++ b/app/components/layout/global-search-persisted.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -9,7 +9,7 @@ import { } from "./global-search-persisted"; describe("searchTypePersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(searchTypePersisted, [ "weapons", "users", @@ -19,21 +19,21 @@ describe("searchTypePersisted", () => { ]); }); - it("decodes legacy plain-string values", () => { + test("decodes legacy plain-string values", () => { expect(searchTypePersisted.decode("users")).toBe("users"); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(searchTypePersisted, ["USER", "[1]"]); }); }); describe("recentWeaponsPersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(recentWeaponsPersisted, [[], [0, 10, 8000]]); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(recentWeaponsPersisted, ["not json", "[99999]"]); }); }); diff --git a/app/components/layout/global-search-search-params.test.ts b/app/components/layout/global-search-search-params.test.ts index c17ef64a9..44a7c833b 100644 --- a/app/components/layout/global-search-search-params.test.ts +++ b/app/components/layout/global-search-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { globalSearchSearchParams } from "./global-search-search-params"; describe("globalSearchSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(globalSearchSearchParams, { search: [null, "open"], type: [null, "weapons", "users", "teams", "organizations", "tournaments"], @@ -14,7 +14,7 @@ describe("globalSearchSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(globalSearchSearchParams, "search", [["closed"]]); assertDecodesToDefault(globalSearchSearchParams, "type", [["USER"]]); assertDecodesToDefault(globalSearchSearchParams, "weapon", [ diff --git a/app/components/match-page/ingested-scoreboard.test.ts b/app/components/match-page/ingested-scoreboard.test.ts index a3a51998a..58518e158 100644 --- a/app/components/match-page/ingested-scoreboard.test.ts +++ b/app/components/match-page/ingested-scoreboard.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { IngestedScoreboardPlayer } from "~/features/scanner-ingest/core/Scoreboards"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { resolveTimelineWeapons } from "./ingested-scoreboard"; @@ -22,7 +22,7 @@ function ingestedPlayer( } describe("resolveTimelineWeapons()", () => { - it("passes reported weapons through and leaves gaps null without ingested rows", () => { + test("passes reported weapons through and leaves gaps null without ingested rows", () => { expect( resolveTimelineWeapons({ linkedWeapons: [10, null, 20, null], @@ -32,7 +32,7 @@ describe("resolveTimelineWeapons()", () => { ).toEqual([10, null, 20, null]); }); - it("fills gaps from unaccounted ingested rows, marked unverified", () => { + test("fills gaps from unaccounted ingested rows, marked unverified", () => { expect( resolveTimelineWeapons({ linkedWeapons: [10, null, null, null], @@ -50,7 +50,7 @@ describe("resolveTimelineWeapons()", () => { ]); }); - it("does not reuse an ingested row whose weapon a roster member already reported", () => { + test("does not reuse an ingested row whose weapon a roster member already reported", () => { expect( resolveTimelineWeapons({ linkedWeapons: [10, null, null, null], @@ -60,7 +60,7 @@ describe("resolveTimelineWeapons()", () => { ).toEqual([10, null, null, null]); }); - it("keeps the extra ingested row of a weapon two players ran when only one reported it", () => { + test("keeps the extra ingested row of a weapon two players ran when only one reported it", () => { expect( resolveTimelineWeapons({ linkedWeapons: [10, null, null, null], @@ -73,7 +73,7 @@ describe("resolveTimelineWeapons()", () => { ).toEqual([10, { weaponSplId: 10, unverified: true }, null, null]); }); - it("skips ingested rows already attributed to a user, from the other team or without a weapon", () => { + test("skips ingested rows already attributed to a user, from the other team or without a weapon", () => { expect( resolveTimelineWeapons({ linkedWeapons: [null, null, null, null], diff --git a/app/components/match-page/match-page-search-params.test.ts b/app/components/match-page/match-page-search-params.test.ts index 27dba5b45..297691cd0 100644 --- a/app/components/match-page/match-page-search-params.test.ts +++ b/app/components/match-page/match-page-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,13 +6,13 @@ import { import { matchPageSearchParams } from "./match-page-search-params"; describe("matchPageSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(matchPageSearchParams, { tab: [null, "rosters", "action", "result", "stats", "admin"], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(matchPageSearchParams, "tab", [["garbage"]]); }); }); diff --git a/app/components/match-page/utils.test.ts b/app/components/match-page/utils.test.ts index e5e3a98b2..90b7c4c5c 100644 --- a/app/components/match-page/utils.test.ts +++ b/app/components/match-page/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, test } from "vitest"; +import { describe, expect, test } from "vitest"; import type { CommonUser } from "~/utils/kysely.server"; import { inferSubstitutions, resolveRoomPass } from "./utils"; @@ -14,7 +14,7 @@ function user(id: number): CommonUser { } describe("inferSubstitutions", () => { - it("returns an empty array when rosters are unchanged", () => { + test("returns an empty array when rosters are unchanged", () => { const rosters = { alpha: [user(1), user(2), user(3), user(4)], bravo: [user(5), user(6), user(7), user(8)], @@ -23,7 +23,7 @@ describe("inferSubstitutions", () => { expect(inferSubstitutions(rosters, rosters)).toEqual([]); }); - it("detects a single substitution on alpha", () => { + test("detects a single substitution on alpha", () => { const previous = { alpha: [user(1), user(2), user(3), user(4)], bravo: [user(5), user(6), user(7), user(8)], @@ -38,7 +38,7 @@ describe("inferSubstitutions", () => { ]); }); - it("detects substitutions on both sides in the same map transition", () => { + test("detects substitutions on both sides in the same map transition", () => { const previous = { alpha: [user(1), user(2)], bravo: [user(3), user(4)], @@ -54,7 +54,7 @@ describe("inferSubstitutions", () => { ]); }); - it("pairs multiple substitutions on the same side by roster order", () => { + test("pairs multiple substitutions on the same side by roster order", () => { const previous = { alpha: [user(1), user(2), user(3), user(4)], bravo: [user(5), user(6)], @@ -70,7 +70,7 @@ describe("inferSubstitutions", () => { ]); }); - it("ignores unpaired leavers when no new player joined", () => { + test("ignores unpaired leavers when no new player joined", () => { const previous = { alpha: [user(1), user(2), user(3), user(4)], bravo: [user(5), user(6)], @@ -83,7 +83,7 @@ describe("inferSubstitutions", () => { expect(inferSubstitutions(previous, current)).toEqual([]); }); - it("ignores unpaired joiners when no player left", () => { + test("ignores unpaired joiners when no player left", () => { const previous = { alpha: [user(1), user(2), user(3)], bravo: [user(5), user(6)], @@ -96,7 +96,7 @@ describe("inferSubstitutions", () => { expect(inferSubstitutions(previous, current)).toEqual([]); }); - it("treats players switching sides as separate substitutions on each side", () => { + test("treats players switching sides as separate substitutions on each side", () => { const previous = { alpha: [user(1), user(2)], bravo: [user(3), user(4)], diff --git a/app/components/objective-timeline-utils.test.ts b/app/components/objective-timeline-utils.test.ts index 03226fdcf..cbeb1e7c7 100644 --- a/app/components/objective-timeline-utils.test.ts +++ b/app/components/objective-timeline-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { matchScoresFromObjective, type ObjectiveScoreRead, @@ -19,31 +19,31 @@ function counterReads( } describe("smoothPenalties", () => { - it("passes steady reads through", () => { + test("passes steady reads through", () => { expect(smoothPenalties(reads([0, 10], [2, 10], [4, 10]))).toEqual([ 10, 10, 10, ]); }); - it("median-filters an isolated dropped-digit misread", () => { + test("median-filters an isolated dropped-digit misread", () => { expect(smoothPenalties(reads([0, 36], [2, 6], [4, 36]))).toEqual([ 36, 36, 36, ]); }); - it("bridges a short null gap with the previous value", () => { + test("bridges a short null gap with the previous value", () => { expect(smoothPenalties(reads([0, 12], [2, null], [4, 12]))).toEqual([ 12, 12, 12, ]); }); - it("does not bridge a gap longer than the bridge window", () => { + test("does not bridge a gap longer than the bridge window", () => { expect( smoothPenalties(reads([0, 12], [1, 12], [20, null], [40, 8], [41, 8])), ).toEqual([12, 12, null, 8, 8]); }); - it("drops one-off reads with no nearby confirmation", () => { + test("drops one-off reads with no nearby confirmation", () => { expect(smoothPenalties(reads([0, 5], [30, 12], [60, 7]))).toEqual([ null, null, @@ -51,7 +51,7 @@ describe("smoothPenalties", () => { ]); }); - it("does not extend past the last read", () => { + test("does not extend past the last read", () => { expect(smoothPenalties(reads([0, 10], [2, 10], [4, null]))).toEqual([ 10, 10, @@ -59,13 +59,13 @@ describe("smoothPenalties", () => { ]); }); - it("keeps all-null reads null", () => { + test("keeps all-null reads null", () => { expect(smoothPenalties(reads([0, null], [2, null]))).toEqual([null, null]); }); }); describe("matchScoresFromObjective", () => { - it("inverts the last counter read of each team", () => { + test("inverts the last counter read of each team", () => { expect( matchScoresFromObjective( counterReads([0, 100, 100], [60, 80, 92], [120, 55, 0]), @@ -73,7 +73,7 @@ describe("matchScoresFromObjective", () => { ).toEqual([45, 100]); }); - it("falls back to the latest readable count", () => { + test("falls back to the latest readable count", () => { expect( matchScoresFromObjective( counterReads([0, 100, 100], [60, 55, 40], [120, null, null]), @@ -81,13 +81,13 @@ describe("matchScoresFromObjective", () => { ).toEqual([45, 60]); }); - it("ignores counts outside the counter's range", () => { + test("ignores counts outside the counter's range", () => { expect( matchScoresFromObjective(counterReads([0, 100, 100], [60, 155, 40])), ).toEqual([0, 60]); }); - it("reads the last count regardless of the order given", () => { + test("reads the last count regardless of the order given", () => { expect( matchScoresFromObjective( counterReads([120, 55, 0], [0, 100, 100], [60, 80, 92]), @@ -95,7 +95,7 @@ describe("matchScoresFromObjective", () => { ).toEqual([45, 100]); }); - it("reports nothing when no count was read", () => { + test("reports nothing when no count was read", () => { expect(matchScoresFromObjective(counterReads([0, null, null]))).toEqual([ null, null, diff --git a/app/db/json-columns.test.ts b/app/db/json-columns.test.ts index cf6fc3f83..d37ac76b8 100644 --- a/app/db/json-columns.test.ts +++ b/app/db/json-columns.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { JSON_COLUMNS } from "./json-columns"; describe("JSON_COLUMNS", () => { - it("matches the JSONColumnType declarations in tables.ts", () => { + test("matches the JSONColumnType declarations in tables.ts", () => { expect([...JSON_COLUMNS].sort()).toEqual(jsonColumnsFromTablesSource()); }); }); diff --git a/app/db/json-selections.test.ts b/app/db/json-selections.test.ts index bf7ef8949..a7e26d6e3 100644 --- a/app/db/json-selections.test.ts +++ b/app/db/json-selections.test.ts @@ -1,5 +1,5 @@ import { sql } from "kysely"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; @@ -12,7 +12,7 @@ import type { Tables } from "./tables"; const JSON_SHAPED_TEXT = '{"note":"gg"}'; describe("computedJsonColumns", () => { - it("recognizes a json helper selection but not a coalesce over user text", () => { + test("recognizes a json helper selection but not a coalesce over user text", () => { const query = db .selectFrom("User") .select((eb) => [ @@ -32,7 +32,7 @@ describe("computedJsonColumns", () => { ); }); - it("recognizes a json column contributed by another branch of a compound select", () => { + test("recognizes a json column contributed by another branch of a compound select", () => { const query = db .selectFrom("CalendarEventResultTeam") .select([ @@ -47,7 +47,7 @@ describe("computedJsonColumns", () => { ); }); - it("recognizes a json selection passed through a derived table", () => { + test("recognizes a json selection passed through a derived table", () => { const query = db .selectFrom((eb) => eb @@ -72,7 +72,7 @@ describe("computedJsonColumns", () => { }); describe("reading rows", () => { - it("keeps a JSON-object-shaped in-tournament name as text", async () => { + test("keeps a JSON-object-shaped in-tournament name as text", async () => { const [organizer, member] = await UserFactory.createMany(2); const tournament = await TournamentFactory.create({ authorId: organizer.id, @@ -106,7 +106,7 @@ describe("reading rows", () => { expect(row.username).toBe(JSON_SHAPED_TEXT); }); - it("parses json columns and json helper selections", async () => { + test("parses json columns and json helper selections", async () => { const user = await UserFactory.create(undefined, { matchProfile: { languages: ["en", "ja"] }, }); diff --git a/app/features/admin/admin-search-params.test.ts b/app/features/admin/admin-search-params.test.ts index e653c102a..98d65a986 100644 --- a/app/features/admin/admin-search-params.test.ts +++ b/app/features/admin/admin-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,13 +6,13 @@ import { import { adminSearchParams } from "./admin-search-params"; describe("adminSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(adminSearchParams, { friendCode: ["1234-5678-9012", "SW-1234-5678-9012", "123456789012"], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(adminSearchParams, "friendCode", [ ["not-a-friend-code"], ["1234-5678"], diff --git a/app/features/admin/routes/admin.test.ts b/app/features/admin/routes/admin.test.ts index c5eba109b..52cd561e0 100644 --- a/app/features/admin/routes/admin.test.ts +++ b/app/features/admin/routes/admin.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import * as BuildFactory from "~/db/seed/factories/BuildFactory"; import * as PlusVoteFactory from "~/db/seed/factories/PlusVoteFactory"; import * as SkillFactory from "~/db/seed/factories/SkillFactory"; @@ -274,7 +274,7 @@ describe("Account migration", () => { await createUsers(2); }); - it("migrates a blank account", async () => { + test("migrates a blank account", async () => { expect(await UserRepository.findProfileByIdentifier("0")).toBeDefined(); expect(await UserRepository.findProfileByIdentifier("1")).toBeDefined(); @@ -287,7 +287,7 @@ describe("Account migration", () => { expect(newUser?.id).toBe(users.id(1)); // took the old user's id }); - it("two accounts with teams results in an error", async () => { + test("two accounts with teams results in an error", async () => { await TeamFactory.create({ memberUserIds: [users.id(1)] }); await TeamFactory.create({ memberUserIds: [users.id(2)] }); @@ -303,7 +303,7 @@ describe("Account migration", () => { .where("userId", "=", userId) .executeTakeFirst(); - it("deletes past team membership status of the new user", async () => { + test("deletes past team membership status of the new user", async () => { const team = await TeamFactory.create({ memberUserIds: [users.id(2)] }); await TeamRepository.deleteById(team.id); @@ -317,7 +317,7 @@ describe("Account migration", () => { expect(membershipAfterMigration).toBeUndefined(); }); - it("handles old user member of the same team as new user (old user has left the team, new user current)", async () => { + test("handles old user member of the same team as new user (old user has left the team, new user current)", async () => { const team = await TeamFactory.create({ memberUserIds: [users.id(2), users.id(1)], }); @@ -340,7 +340,7 @@ describe("Account migration", () => { expect(membershipNewUser).toBeUndefined(); }); - it("deletes weapon pool from the new user when migrating (takes weapon pool from the old user)", async () => { + test("deletes weapon pool from the new user when migrating (takes weapon pool from the old user)", async () => { await UserFactory.grant(users.id(1), { weapons: [{ weaponSplId: 1, isFavorite: 1 }], }); @@ -357,7 +357,7 @@ describe("Account migration", () => { ]); }); - it("deletes builds from the new user when migrating", async () => { + test("deletes builds from the new user when migrating", async () => { await BuildFactory.create({ ownerId: users.id(2) }); const buildsBefore = await BuildRepository.findAllByUserId(users.id(2)); diff --git a/app/features/art/art-search-params.test.ts b/app/features/art/art-search-params.test.ts index ea4753719..d779f9ea0 100644 --- a/app/features/art/art-search-params.test.ts +++ b/app/features/art/art-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -10,7 +10,7 @@ import { } from "./art-search-params"; describe("artSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(artSearchParams, { tag: ["cat", "some tag"], tab: ["recently-uploaded", "showcase"], @@ -18,20 +18,20 @@ describe("artSearchParams", () => { }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(artSearchParams, "tab", [["not-a-tab"]]); assertDecodesToDefault(artSearchParams, "open", [["yes"], ["1"]]); }); }); describe("artGridSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(artGridSearchParams, { big: [1, 42], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(artGridSearchParams, "big", [ ["abc"], ["-1"], @@ -41,13 +41,13 @@ describe("artGridSearchParams", () => { }); describe("artNewSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(artNewSearchParams, { art: [1, 999], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(artNewSearchParams, "art", [["abc"], ["0"]]); }); }); diff --git a/app/features/associations/associations-search-params.test.ts b/app/features/associations/associations-search-params.test.ts index 511d4890a..e3cd06e00 100644 --- a/app/features/associations/associations-search-params.test.ts +++ b/app/features/associations/associations-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,13 +6,13 @@ import { import { associationsSearchParams } from "./associations-search-params"; describe("associationsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(associationsSearchParams, { inviteCode: ["abcdefghij", "A1b2C3d4E5"], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(associationsSearchParams, "inviteCode", [ ["short"], ["waytoolonginvitecode"], diff --git a/app/features/associations/core/Association.test.ts b/app/features/associations/core/Association.test.ts index 0a466a5b0..9ade813c4 100644 --- a/app/features/associations/core/Association.test.ts +++ b/app/features/associations/core/Association.test.ts @@ -1,10 +1,10 @@ import { add } from "date-fns"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import * as Association from "./Association"; describe("isVisible", () => { - it("should return true if visibility is null", () => { + test("returns true if visibility is null", () => { const args: Association.IsVisibleArgs = { visibility: null, associations: null, @@ -12,7 +12,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return false if not member of the association", () => { + test("returns false if not member of the association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: 1 }, associations: null, @@ -20,7 +20,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(false); }); - it("should return true if member of the association", () => { + test("returns true if member of the association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: 1 }, associations: { @@ -31,7 +31,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return true if member of the virtual association", () => { + test("returns true if member of the virtual association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "+1" }, associations: { @@ -42,7 +42,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return false if not yet visible", () => { + test("returns false if not yet visible", () => { const visibleAt = add(new Date(), { days: 1 }); const args: Association.IsVisibleArgs = { @@ -60,7 +60,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(false); }); - it("should return true if has become visible", () => { + test("returns true if has become visible", () => { const visibleAt = add(new Date(), { days: 1 }); const args: Association.IsVisibleArgs = { @@ -79,7 +79,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return true if has become public", () => { + test("returns true if has become public", () => { const visibleAt = add(new Date(), { days: 1 }); const args: Association.IsVisibleArgs = { @@ -98,7 +98,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return true if viewer is a friend of the content owner", () => { + test("returns true if viewer is a friend of the content owner", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, associations: { @@ -111,7 +111,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return false if viewer is not a friend of the content owner", () => { + test("returns false if viewer is not a friend of the content owner", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, associations: { @@ -124,7 +124,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(false); }); - it("should return false for a non-friend when the viewer has the FRIENDS virtual association every user gets", () => { + test("returns false for a non-friend when the viewer has the FRIENDS virtual association every user gets", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, associations: { @@ -137,7 +137,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(false); }); - it("should return false for FRIENDS visibility when not logged in", () => { + test("returns false for FRIENDS visibility when not logged in", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, associations: null, @@ -145,7 +145,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(false); }); - it("should return true when FRIENDS visibility becomes public via notFoundInstructions", () => { + test("returns true when FRIENDS visibility becomes public via notFoundInstructions", () => { const visibleAt = add(new Date(), { days: 1 }); const args: Association.IsVisibleArgs = { @@ -165,7 +165,7 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); - it("should return true if has become public (no associations)", () => { + test("returns true if has become public (no associations)", () => { const visibleAt = add(new Date(), { days: 1 }); const args: Association.IsVisibleArgs = { diff --git a/app/features/ban/core/banned.test.ts b/app/features/ban/core/banned.test.ts index b501496af..80d42a884 100644 --- a/app/features/ban/core/banned.test.ts +++ b/app/features/ban/core/banned.test.ts @@ -1,24 +1,24 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { checkBanStatus } from "./banned.server"; describe("checkBanStatus", () => { - it("returns false when banned is null", () => { + test("returns false when banned is null", () => { expect(checkBanStatus(null)).toBe(false); }); - it("returns false when banned is undefined", () => { + test("returns false when banned is undefined", () => { expect(checkBanStatus(undefined)).toBe(false); }); - it("returns false when banned is 0", () => { + test("returns false when banned is 0", () => { expect(checkBanStatus(0)).toBe(false); }); - it("returns true when banned is 1 (permanent ban)", () => { + test("returns true when banned is 1 (permanent ban)", () => { expect(checkBanStatus(1)).toBe(true); }); - it("returns true when ban expires in the future", () => { + test("returns true when ban expires in the future", () => { const now = new Date("2025-01-01T12:00:00Z"); const futureTimestamp = Math.floor( new Date("2025-01-01T13:00:00Z").getTime() / 1000, @@ -27,7 +27,7 @@ describe("checkBanStatus", () => { expect(checkBanStatus(futureTimestamp, now)).toBe(true); }); - it("returns false when ban has expired", () => { + test("returns false when ban has expired", () => { const now = new Date("2025-01-01T12:00:00Z"); const pastTimestamp = Math.floor( new Date("2025-01-01T11:00:00Z").getTime() / 1000, @@ -36,21 +36,21 @@ describe("checkBanStatus", () => { expect(checkBanStatus(pastTimestamp, now)).toBe(false); }); - it("returns false when ban expires exactly at current time", () => { + test("returns false when ban expires exactly at current time", () => { const now = new Date("2025-01-01T12:00:00Z"); const exactTimestamp = Math.floor(now.getTime() / 1000); expect(checkBanStatus(exactTimestamp, now)).toBe(false); }); - it("returns true when ban expires 1 second in the future", () => { + test("returns true when ban expires 1 second in the future", () => { const now = new Date("2025-01-01T12:00:00Z"); const oneSecondLater = Math.floor(now.getTime() / 1000) + 1; expect(checkBanStatus(oneSecondLater, now)).toBe(true); }); - it("returns false when ban expired 1 second ago", () => { + test("returns false when ban expired 1 second ago", () => { const now = new Date("2025-01-01T12:00:00Z"); const oneSecondEarlier = Math.floor(now.getTime() / 1000) - 1; diff --git a/app/features/build-analyzer/analyzer-search-params.test.ts b/app/features/build-analyzer/analyzer-search-params.test.ts index ee4b1de99..ca9e55a1a 100644 --- a/app/features/build-analyzer/analyzer-search-params.test.ts +++ b/app/features/build-analyzer/analyzer-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { EMPTY_BUILD } from "~/features/builds/builds-constants"; import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists/types"; import { @@ -20,7 +20,7 @@ const PARTIAL_BUILD: BuildAbilitiesTupleWithUnknown = [ ]; describe("analyzerSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(analyzerSearchParams, { weapon: [0, 10, 8000], build: [EMPTY_BUILD, FULL_BUILD, PARTIAL_BUILD], @@ -31,7 +31,7 @@ describe("analyzerSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(analyzerSearchParams, "weapon", [ [""], ["9999999"], diff --git a/app/features/builds/BuildRepository.server.test.ts b/app/features/builds/BuildRepository.server.test.ts index c1f7b564e..6796fc522 100644 --- a/app/features/builds/BuildRepository.server.test.ts +++ b/app/features/builds/BuildRepository.server.test.ts @@ -10,8 +10,7 @@ import type { } from "~/modules/in-game-lists/types"; import * as BuildRepository from "./BuildRepository.server"; -let owner: { id: number }; -let otherOwner: { id: number }; +const users = UserFactory.pool(); // Splattershot (40) is the canonical base, Hero Shot Replica (45) is an alt skin // that should be folded to 40 by the canonical id mapping. @@ -33,7 +32,7 @@ const EXPECTED_SIGNATURE = "ISM_38,ISS_19"; const baseArgs = ( overrides: Partial[0]> = {}, ): Parameters[0] => ({ - ownerId: owner.id, + ownerId: users.id(1), title: "Test Build", description: null, modes: null, @@ -93,7 +92,7 @@ const buildWeaponAbilitiesByBuildId = (buildId: number) => describe("BuildRepository.insert — computeBuildData", () => { beforeEach(async () => { - [owner, otherOwner] = await UserFactory.createMany(2); + await users.create(2); }); describe("abilitiesSignature & ability sums", () => { @@ -231,7 +230,7 @@ describe("BuildRepository.insert — computeBuildData", () => { }); test("subtracts 1 when the weapon is top500 for the owner", async () => { - await makeTop500(owner.id, SPLATTERSHOT); + await makeTop500(users.id(1), SPLATTERSHOT); const { id } = await BuildRepository.insert( baseArgs({ weaponSplIds: [SPLATTERSHOT, SPLATTERSHOT_NOUVEAU] }), @@ -261,10 +260,10 @@ describe("BuildRepository.insert — computeBuildData", () => { }); test("findAllByWeaponId.weapons[].isTop500 matches the sortValue formula", async () => { - await makeTop500(owner.id, SPLATTERSHOT); + await makeTop500(users.id(1), SPLATTERSHOT); await createBuild({ - ownerId: owner.id, + ownerId: users.id(1), weaponSplIds: [SPLATTERSHOT, SPLATTERSHOT_NOUVEAU], }); @@ -285,7 +284,7 @@ describe("BuildRepository.insert — computeBuildData", () => { test("a multi-weapon build is returned by findAllByWeaponId for each of its weapons", async () => { await createBuild({ - ownerId: owner.id, + ownerId: users.id(1), title: "Multi-weapon Build", weaponSplIds: [SPLATTERSHOT, SPLATTERSHOT_NOUVEAU], }); @@ -315,17 +314,17 @@ describe("BuildRepository.findAllPopularAbilitiesByWeaponId", () => { ]; beforeEach(async () => { - [owner, otherOwner] = await UserFactory.createMany(2); + await users.create(2); }); test("counts each user at most once across signature buckets", async () => { // Each user has two Splattershot builds with different signatures. // Without per-user dedup, both users would inflate both buckets and // the total count across rows would be 4 instead of <=2. - await createBuild({ ownerId: owner.id }); - await createBuild({ ownerId: owner.id, abilities: SS_ABILITIES }); - await createBuild({ ownerId: otherOwner.id }); - await createBuild({ ownerId: otherOwner.id, abilities: SS_ABILITIES }); + await createBuild({ ownerId: users.id(1) }); + await createBuild({ ownerId: users.id(1), abilities: SS_ABILITIES }); + await createBuild({ ownerId: users.id(2) }); + await createBuild({ ownerId: users.id(2), abilities: SS_ABILITIES }); const rows = await BuildRepository.findAllPopularAbilitiesByWeaponId(SPLATTERSHOT); @@ -336,8 +335,8 @@ describe("BuildRepository.findAllPopularAbilitiesByWeaponId", () => { }); test("only counts public builds", async () => { - await createBuild({ ownerId: owner.id }); - await createBuild({ ownerId: otherOwner.id, isPrivate: 1 }); + await createBuild({ ownerId: users.id(1) }); + await createBuild({ ownerId: users.id(2), isPrivate: 1 }); const rows = await BuildRepository.findAllPopularAbilitiesByWeaponId(SPLATTERSHOT); @@ -347,9 +346,9 @@ describe("BuildRepository.findAllPopularAbilitiesByWeaponId", () => { }); test("folds alt skins via canonicalWeaponSplId", async () => { - await createBuild({ ownerId: owner.id }); + await createBuild({ ownerId: users.id(1) }); await createBuild({ - ownerId: otherOwner.id, + ownerId: users.id(2), weaponSplIds: [HERO_SHOT_REPLICA], }); diff --git a/app/features/builds/builds-search-params.test.ts b/app/features/builds/builds-search-params.test.ts index 48a3c111e..4117c6390 100644 --- a/app/features/builds/builds-search-params.test.ts +++ b/app/features/builds/builds-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { buildsSearchParams } from "./builds-search-params"; describe("buildsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(buildsSearchParams, { limit: [24, 48, 1, 240], abilities: [ @@ -23,7 +23,7 @@ describe("buildsSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(buildsSearchParams, "limit", [ [""], ["0"], diff --git a/app/features/calendar/calendar-progression-form.test.ts b/app/features/calendar/calendar-progression-form.test.ts index 59f483124..42b4611a4 100644 --- a/app/features/calendar/calendar-progression-form.test.ts +++ b/app/features/calendar/calendar-progression-form.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { z } from "zod"; import * as Progression from "~/features/tournament-bracket/core/Progression"; import { @@ -83,23 +83,23 @@ function validationIssues(formValues: { } describe("progressionToFormValues + formValuesToInputBrackets", () => { - it("round-trips a single double elimination bracket", () => { + test("round-trips a single double elimination bracket", () => { expect(roundTrip(DOUBLE_ELIMINATION)).toEqual(DOUBLE_ELIMINATION); }); - it("round-trips round robin to single elimination with an underground bracket", () => { + test("round-trips round robin to single elimination with an underground bracket", () => { expect(roundTrip(RR_TO_SE_WITH_UNDERGROUND)).toEqual( RR_TO_SE_WITH_UNDERGROUND, ); }); - it("round-trips swiss with early advance (empty placements)", () => { + test("round-trips swiss with early advance (empty placements)", () => { expect(roundTrip(SWISS_EARLY_ADVANCE_TO_TOP_CUT)).toEqual( SWISS_EARLY_ADVANCE_TO_TOP_CUT, ); }); - it("round-trips the N+ rest placements syntax", () => { + test("round-trips the N+ rest placements syntax", () => { const progression: Progression.ParsedBracket[] = [ RR_TO_SE_WITH_UNDERGROUND[0], RR_TO_SE_WITH_UNDERGROUND[1], @@ -112,7 +112,7 @@ describe("progressionToFormValues + formValuesToInputBrackets", () => { expect(roundTrip(progression)).toEqual(progression); }); - it("round-trips a bracket sourcing teams from two brackets", () => { + test("round-trips a bracket sourcing teams from two brackets", () => { const progression: Progression.ParsedBracket[] = [ RR_TO_SE_WITH_UNDERGROUND[0], RR_TO_SE_WITH_UNDERGROUND[2], @@ -128,7 +128,7 @@ describe("progressionToFormValues + formValuesToInputBrackets", () => { expect(roundTrip(progression)).toEqual(progression); }); - it("round-trips bracket start time", () => { + test("round-trips bracket start time", () => { const progression: Progression.ParsedBracket[] = [ RR_TO_SE_WITH_UNDERGROUND[0], { ...RR_TO_SE_WITH_UNDERGROUND[1], startTime: 1735689600 }, @@ -138,7 +138,7 @@ describe("progressionToFormValues + formValuesToInputBrackets", () => { expect(roundTrip(progression)).toEqual(progression); }); - it("ignores stale settings of other format types", () => { + test("ignores stale settings of other format types", () => { const { brackets, progression } = defaultBracketsFormValues(); const withStaleSettings = [ { ...brackets[0], hasAbDivisions: true, earlyAdvance: true }, @@ -158,7 +158,7 @@ describe("progressionToFormValues + formValuesToInputBrackets", () => { ]); }); - it("ignores placements and check-in of a bracket sourcing from sign-up", () => { + test("ignores placements and check-in of a bracket sourcing from sign-up", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[2] = { ...formValues.progression[2], @@ -178,11 +178,11 @@ describe("progressionToFormValues + formValuesToInputBrackets", () => { }); describe("validateBracketProgressionFormValues", () => { - it("accepts the default form values", () => { + test("accepts the default form values", () => { expect(validationIssues(defaultBracketsFormValues())).toHaveLength(0); }); - it("attaches unparseable placements to the progression entry", () => { + test("attaches unparseable placements to the progression entry", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[1] = { ...formValues.progression[1], @@ -198,7 +198,7 @@ describe("validateBracketProgressionFormValues", () => { ); }); - it("attaches a duplicate bracket name to both name fields", () => { + test("attaches a duplicate bracket name to both name fields", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.brackets[2] = { ...formValues.brackets[2], name: "Top cut" }; @@ -210,7 +210,7 @@ describe("validateBracketProgressionFormValues", () => { ]); }); - it("rejects an out of range source bracket", () => { + test("rejects an out of range source bracket", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[1] = { ...formValues.progression[1], @@ -229,7 +229,7 @@ describe("validateBracketProgressionFormValues", () => { ]); }); - it("rejects a non-canonical source bracket idx string", () => { + test("rejects a non-canonical source bracket idx string", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[1] = { ...formValues.progression[1], @@ -248,7 +248,7 @@ describe("validateBracketProgressionFormValues", () => { ]); }); - it("rejects a bracket sourcing itself", () => { + test("rejects a bracket sourcing itself", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[1] = { ...formValues.progression[1], @@ -267,7 +267,7 @@ describe("validateBracketProgressionFormValues", () => { ]); }); - it("rejects the same source bracket twice for one bracket", () => { + test("rejects the same source bracket twice for one bracket", () => { const formValues = progressionToFormValues(RR_TO_SE_WITH_UNDERGROUND); formValues.progression[1] = { ...formValues.progression[1], diff --git a/app/features/calendar/calendar-search-params.test.ts b/app/features/calendar/calendar-search-params.test.ts index f5be37c71..689184b2f 100644 --- a/app/features/calendar/calendar-search-params.test.ts +++ b/app/features/calendar/calendar-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -11,7 +11,7 @@ import { import * as CalendarEvent from "./core/CalendarEvent"; describe("calendarSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(calendarSearchParams, { modes: [CalendarEvent.defaultFilters().modes, ["SZ", "TC"], ["TB"]], modesExact: [false, true], @@ -39,7 +39,7 @@ describe("calendarSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(calendarSearchParams, "preferredStartTime", [ ["XX"], ["eu"], @@ -76,13 +76,13 @@ describe("calendarSearchParams", () => { }); describe("calendarEventsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(calendarEventsSearchParams, { view: [null, "registered", "hosting", "scrims", "saved", "organization"], }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(calendarEventsSearchParams, "view", [ ["invalid"], ["Registered"], @@ -92,7 +92,7 @@ describe("calendarEventsSearchParams", () => { }); describe("calendarNewSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(calendarNewSearchParams, { eventId: [null, 1, 12345], copyEventId: [null, 99], @@ -100,7 +100,7 @@ describe("calendarNewSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(calendarNewSearchParams, "eventId", [ ["0"], ["-1"], diff --git a/app/features/calendar/calendar-utils.test.ts b/app/features/calendar/calendar-utils.test.ts index d9e03ce40..853b18c4f 100644 --- a/app/features/calendar/calendar-utils.test.ts +++ b/app/features/calendar/calendar-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { calendarEventMaxDate, calendarEventMinDate, @@ -8,18 +8,18 @@ import { } from "./calendar-utils"; describe("calendar-utils", () => { - it("calendarEventMinDate should return a fixed date", () => { + test("calendarEventMinDate should return a fixed date", () => { expect(calendarEventMinDate()).toEqual(new Date(Date.UTC(2015, 4, 28))); }); - it("calendarEventMaxDate should return a date one year from now", () => { + test("calendarEventMaxDate should return a date one year from now", () => { const result = calendarEventMaxDate(); const expected = new Date(); expected.setFullYear(expected.getFullYear() + 1); expect(result.getFullYear()).toBe(expected.getFullYear()); }); - it("regClosesAtDate should return correct date based on closesAt option", () => { + test("regClosesAtDate should return correct date based on closesAt option", () => { const startTime = new Date(); expect(regClosesAtDate({ startTime, closesAt: "5min" })).toEqual( new Date(startTime.getTime() - 5 * 60 * 1000), @@ -29,12 +29,12 @@ describe("calendar-utils", () => { ); }); - it("regClosesAtToDisplayName should return correct display name", () => { + test("regClosesAtToDisplayName should return correct display name", () => { expect(regClosesAtToDisplayName("5min")).toBe("5 minutes"); expect(regClosesAtToDisplayName("1h")).toBe("1 hour"); }); - it("datesToRegClosesAt should return correct closesAt option based on date difference", () => { + test("datesToRegClosesAt should return correct closesAt option based on date difference", () => { const startTime = new Date(); expect( datesToRegClosesAt({ diff --git a/app/features/calendar/core/CalendarEvent.test.ts b/app/features/calendar/core/CalendarEvent.test.ts index 0a9d14080..2f149dc3d 100644 --- a/app/features/calendar/core/CalendarEvent.test.ts +++ b/app/features/calendar/core/CalendarEvent.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { CalendarEvent as CalendarEventType, CalendarFilters, @@ -33,7 +33,7 @@ function makeEvent( } describe("CalendarEvent.applyFilters", () => { - it("returns all events as shown with default filters", () => { + test("returns all events as shown with default filters", () => { const events = [ { at: 123, @@ -48,7 +48,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.hidden).toHaveLength(0); }); - it("filters by isRanked", () => { + test("filters by isRanked", () => { const events = [ { at: 123, @@ -69,7 +69,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.hidden).toHaveLength(2); }); - it("filters by tagsIncluded", () => { + test("filters by tagsIncluded", () => { const events = [ { at: 123, @@ -88,7 +88,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown[0].id).toBe(1); }); - it("filters by tagsExcluded", () => { + test("filters by tagsExcluded", () => { const events = [ { at: 123, @@ -107,7 +107,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown[0].id).toBe(2); }); - it("filters by games", () => { + test("filters by games", () => { const events = [ { at: 123, @@ -127,7 +127,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown[0].id).toBe(1); }); - it("filters by preferredVersus", () => { + test("filters by preferredVersus", () => { const events = [ { at: 123, @@ -147,7 +147,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([1, 2]); }); - it("filters by modes (not exact)", () => { + test("filters by modes (not exact)", () => { const events = [ { at: 123, @@ -165,7 +165,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([1]); }); - it("filters by modes (exact)", () => { + test("filters by modes (exact)", () => { const events = [ { at: 123, @@ -184,7 +184,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([2]); }); - it("filters by minTeamCount", () => { + test("filters by minTeamCount", () => { const events = [ { at: 123, @@ -202,7 +202,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([2]); }); - it("filters by tier range, taking the tentative tier into account", () => { + test("filters by tier range, taking the tentative tier into account", () => { const events = [ { at: 123, @@ -225,7 +225,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([2, 3, 4]); }); - it("shows untiered events when the tier range is at its default", () => { + test("shows untiered events when the tier range is at its default", () => { const events = [ { at: 123, @@ -239,7 +239,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([1, 2]); }); - it("filters by orgsIncluded", () => { + test("filters by orgsIncluded", () => { const events = [ { at: 123, @@ -258,7 +258,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([1]); }); - it("filters by orgsExcluded", () => { + test("filters by orgsExcluded", () => { const events = [ { at: 123, @@ -277,7 +277,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([2, 3]); }); - it("filters by authorIdsExcluded", () => { + test("filters by authorIdsExcluded", () => { const events = [ { at: 123, @@ -295,7 +295,7 @@ describe("CalendarEvent.applyFilters", () => { expect(result[0].events.shown.map((e) => e.id)).toEqual([2]); }); - it("filters by combining two different filters", () => { + test("filters by combining two different filters", () => { const events = [ { at: 123, diff --git a/app/features/chat/chat-constants.test.ts b/app/features/chat/chat-constants.test.ts index 4a621cf0e..a7bf33b78 100644 --- a/app/features/chat/chat-constants.test.ts +++ b/app/features/chat/chat-constants.test.ts @@ -2,76 +2,28 @@ import { describe, expect, test } from "vitest"; import { findRoomLinks, isSplatnetRoomUrl } from "./chat-constants"; describe("isSplatnetRoomUrl", () => { - test("accepts canonical SplatNet share path", () => { - expect( - isSplatnetRoomUrl( - "https://s.nintendo.com/av5ja-lp1/znca/game/4834290508791808?p=%2Froom_creator%2Finvitation%2F1f14e24b-3c9e-6352-8a80-b7993ffad0d0", - ), - ).toBe(true); + test.each([ + "https://s.nintendo.com/av5ja-lp1/znca/game/4834290508791808?p=%2Froom_creator%2Finvitation%2F1f14e24b-3c9e-6352-8a80-b7993ffad0d0", + "https://s.nintendo.com/av5ja-lp1/abc123", + "https://s.nintendo.com/abcdef", + ])("accepts %s", (url) => { + expect(isSplatnetRoomUrl(url)).toBe(true); }); - test("accepts canonical SplatNet share path (no query params)", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com/av5ja-lp1/abc123")).toBe( - true, - ); - }); - - test("accepts a simple alphanumeric path", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com/abcdef")).toBe(true); - }); - - test("rejects http (non-https)", () => { - expect(isSplatnetRoomUrl("http://s.nintendo.com/abc")).toBe(false); - }); - - test("rejects unescaped-dot lookalike host (sanintendoacom.evil.tld)", () => { - expect(isSplatnetRoomUrl("https://sanintendoacom.evil.tld/lobby")).toBe( - false, - ); - }); - - test("rejects dash variant host (s-nintendo-com.evil.tld)", () => { - expect(isSplatnetRoomUrl("https://s-nintendo-com.evil.tld/lobby")).toBe( - false, - ); - }); - - test("rejects userinfo in URL (s.nintendo.com@evil.com)", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com@evil.com/abc")).toBe( - false, - ); - }); - - test("rejects custom port", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com:8080/abc")).toBe(false); - }); - - test("rejects query string", () => { - expect( - isSplatnetRoomUrl("https://s.nintendo.com/abc?redirect=evil.com"), - ).toBe(false); - }); - - test("rejects fragment", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com/abc#@evil.com")).toBe( - false, - ); - }); - - test("rejects trailing dot in hostname", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com./abc")).toBe(false); - }); - - test("rejects empty path", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com/")).toBe(false); - }); - - test("rejects path with disallowed characters", () => { - expect(isSplatnetRoomUrl("https://s.nintendo.com/abc!def")).toBe(false); - }); - - test("rejects malformed URL", () => { - expect(isSplatnetRoomUrl("not a url")).toBe(false); + test.each([ + ["http://s.nintendo.com/abc", "http, not https"], + ["https://sanintendoacom.evil.tld/lobby", "unescaped-dot lookalike host"], + ["https://s-nintendo-com.evil.tld/lobby", "dash variant host"], + ["https://s.nintendo.com@evil.com/abc", "userinfo in the URL"], + ["https://s.nintendo.com:8080/abc", "custom port"], + ["https://s.nintendo.com/abc?redirect=evil.com", "query string"], + ["https://s.nintendo.com/abc#@evil.com", "fragment"], + ["https://s.nintendo.com./abc", "trailing dot in the hostname"], + ["https://s.nintendo.com/", "empty path"], + ["https://s.nintendo.com/abc!def", "disallowed characters in the path"], + ["not a url", "malformed URL"], + ])("rejects %s (%s)", (url) => { + expect(isSplatnetRoomUrl(url)).toBe(false); }); }); diff --git a/app/features/chat/chat-last-read.test.ts b/app/features/chat/chat-last-read.test.ts index d13f0611e..0ab703d65 100644 --- a/app/features/chat/chat-last-read.test.ts +++ b/app/features/chat/chat-last-read.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,15 +6,15 @@ import { import { lastReadCountsPersisted } from "./chat-last-read"; describe("lastReadCountsPersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(lastReadCountsPersisted, [0, 42]); }); - it("decodes legacy raw number strings", () => { + test("decodes legacy raw number strings", () => { expect(lastReadCountsPersisted.decode("7")).toBe(7); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(lastReadCountsPersisted, ["abc", "", "Infinity"]); }); }); diff --git a/app/features/chat/chat-search-params.test.ts b/app/features/chat/chat-search-params.test.ts index 750700526..3e3fee696 100644 --- a/app/features/chat/chat-search-params.test.ts +++ b/app/features/chat/chat-search-params.test.ts @@ -1,22 +1,22 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { assertRoundTrips } from "~/modules/search-params/search-params-test-utils"; import { chatUsersSearchParams } from "./chat-search-params"; describe("chatUsersSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(chatUsersSearchParams, { ids: [[], [1], [1, 2, 3]], }); }); - it("accepts the legacy comma-joined form", () => { + test("accepts the legacy comma-joined form", () => { const { ids } = chatUsersSearchParams.parse( new URL("http://localhost/api/chat-users?ids=1,2,3"), ); expect(ids).toEqual([1, 2, 3]); }); - it("drops invalid members instead of the whole array", () => { + test("drops invalid members instead of the whole array", () => { const { ids } = chatUsersSearchParams.parse( new URL("http://localhost/api/chat-users?ids=1&ids=abc&ids=-5&ids=3"), ); diff --git a/app/features/comp-analyzer/comp-analyzer-search-params.test.ts b/app/features/comp-analyzer/comp-analyzer-search-params.test.ts index 23efd13d9..3bbbe7d15 100644 --- a/app/features/comp-analyzer/comp-analyzer-search-params.test.ts +++ b/app/features/comp-analyzer/comp-analyzer-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SearchParams from "~/modules/search-params/search-params"; import { assertDecodesToDefault, @@ -7,7 +7,7 @@ import { import { compAnalyzerSearchParams } from "./comp-analyzer-search-params"; describe("compAnalyzerSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(compAnalyzerSearchParams, { categorization: ["category", "sub", "special"], weapons: [[], [0], [10, 20, 30, 40]], @@ -17,7 +17,7 @@ describe("compAnalyzerSearchParams", () => { }); }); - it("accepts legacy comma-joined weapon ids", () => { + test("accepts legacy comma-joined weapon ids", () => { expect( SearchParams.decodeParam(compAnalyzerSearchParams.shape.weapons, [ "10,20", @@ -25,7 +25,7 @@ describe("compAnalyzerSearchParams", () => { ).toEqual([10, 20]); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(compAnalyzerSearchParams, "categorization", [ ["kit"], ]); diff --git a/app/features/friends/friends-search-params.test.ts b/app/features/friends/friends-search-params.test.ts index 46fca58be..d216b287a 100644 --- a/app/features/friends/friends-search-params.test.ts +++ b/app/features/friends/friends-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,13 +6,13 @@ import { import { friendsSearchParams } from "./friends-search-params"; describe("friendsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(friendsSearchParams, { view: [null, "friends", "team", "all"], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(friendsSearchParams, "view", [["garbage"]]); }); }); diff --git a/app/features/img-export/core/RunComps.test.ts b/app/features/img-export/core/RunComps.test.ts index 423e30922..d8ae327d2 100644 --- a/app/features/img-export/core/RunComps.test.ts +++ b/app/features/img-export/core/RunComps.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import * as RunComps from "./RunComps"; @@ -15,11 +15,11 @@ const observation = ( ): RunComps.CompObservation => ({ playerKey, weaponSplId, mapOrder }); describe("buildComp", () => { - it("returns an empty comp for no observations", () => { + test("returns an empty comp for no observations", () => { expect(RunComps.buildComp([])).toEqual([]); }); - it("picks each player's most played weapon", () => { + test("picks each player's most played weapon", () => { expect( RunComps.buildComp([ observation("a", SHOOTER, 0), @@ -29,7 +29,7 @@ describe("buildComp", () => { ).toEqual([SHOOTER]); }); - it("breaks a most played tie by the most recently played weapon", () => { + test("breaks a most played tie by the most recently played weapon", () => { expect( RunComps.buildComp([ observation("a", CHARGER, 0), @@ -38,7 +38,7 @@ describe("buildComp", () => { ).toEqual([SHOOTER]); }); - it("sorts the comp by weapon id with Tacticooler weapons last", () => { + test("sorts the comp by weapon id with Tacticooler weapons last", () => { expect( RunComps.buildComp([ observation("a", ROLLER, 0), @@ -48,7 +48,7 @@ describe("buildComp", () => { ).toEqual([SHOOTER, ROLLER, TACTICOOLER_WEAPON]); }); - it("keeps the players that played the most maps when there are more than four", () => { + test("keeps the players that played the most maps when there are more than four", () => { const fullSet = (playerKey: string, weaponSplId: MainWeaponId) => [ observation(playerKey, weaponSplId, 0), observation(playerKey, weaponSplId, 1), @@ -67,7 +67,7 @@ describe("buildComp", () => { }); describe("mapObservations", () => { - it("keeps reported weapons and ingested rows of other players", () => { + test("keeps reported weapons and ingested rows of other players", () => { expect( RunComps.mapObservations({ mapOrder: 3, @@ -80,7 +80,7 @@ describe("mapObservations", () => { ]); }); - it("drops an ingested row linked to a user that already reported", () => { + test("drops an ingested row linked to a user that already reported", () => { expect( RunComps.mapObservations({ mapOrder: 0, @@ -90,7 +90,7 @@ describe("mapObservations", () => { ).toEqual([observation("user-1", SHOOTER, 0)]); }); - it("drops an unlinked ingested row whose weapon a report accounts for, counting duplicates as a multiset", () => { + test("drops an unlinked ingested row whose weapon a report accounts for, counting duplicates as a multiset", () => { expect( RunComps.mapObservations({ mapOrder: 0, @@ -106,7 +106,7 @@ describe("mapObservations", () => { ]); }); - it("skips ingested rows without a weapon", () => { + test("skips ingested rows without a weapon", () => { expect( RunComps.mapObservations({ mapOrder: 0, diff --git a/app/features/img-export/core/SeasonSummary.test.ts b/app/features/img-export/core/SeasonSummary.test.ts index dd48ace9c..3bc1eb5be 100644 --- a/app/features/img-export/core/SeasonSummary.test.ts +++ b/app/features/img-export/core/SeasonSummary.test.ts @@ -1,35 +1,35 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SeasonSummary from "./SeasonSummary"; const win = { ownScore: 4, opponentScore: 2 }; const loss = { ownScore: 1, opponentScore: 4 }; describe("longestWinStreak", () => { - it("returns 0 for no sets", () => { + test("returns 0 for no sets", () => { expect(SeasonSummary.longestWinStreak([])).toBe(0); }); - it("returns 0 when every set was lost", () => { + test("returns 0 when every set was lost", () => { expect(SeasonSummary.longestWinStreak([loss, loss])).toBe(0); }); - it("counts consecutive wins only", () => { + test("counts consecutive wins only", () => { expect( SeasonSummary.longestWinStreak([win, win, loss, win, win, win, loss]), ).toBe(3); }); - it("counts a streak lasting until the end", () => { + test("counts a streak lasting until the end", () => { expect(SeasonSummary.longestWinStreak([loss, win, win])).toBe(2); }); }); describe("clutchRecord", () => { - it("returns zeros for no sets", () => { + test("returns zeros for no sets", () => { expect(SeasonSummary.clutchRecord([])).toEqual({ won: 0, total: 0 }); }); - it("only counts sets decided by one map", () => { + test("only counts sets decided by one map", () => { expect( SeasonSummary.clutchRecord([ { ownScore: 4, opponentScore: 3 }, @@ -43,7 +43,7 @@ describe("clutchRecord", () => { }); describe("bestStage", () => { - it("returns undefined when no stage has enough maps played", () => { + test("returns undefined when no stage has enough maps played", () => { expect( SeasonSummary.bestStage({ 1: { SZ: { wins: 2, losses: 0 } }, @@ -51,7 +51,7 @@ describe("bestStage", () => { ).toBeUndefined(); }); - it("aggregates winrate across modes", () => { + test("aggregates winrate across modes", () => { expect( SeasonSummary.bestStage({ 1: { SZ: { wins: 4, losses: 2 }, TC: { wins: 2, losses: 2 } }, @@ -59,7 +59,7 @@ describe("bestStage", () => { ).toEqual({ stageId: 1, winratePercentage: 60 }); }); - it("picks the stage with the highest winrate among qualified ones", () => { + test("picks the stage with the highest winrate among qualified ones", () => { expect( SeasonSummary.bestStage({ 1: { SZ: { wins: 9, losses: 1 } }, @@ -69,7 +69,7 @@ describe("bestStage", () => { ).toEqual({ stageId: 1, winratePercentage: 90 }); }); - it("does not let a low sample size stage win over a qualified one", () => { + test("does not let a low sample size stage win over a qualified one", () => { expect( SeasonSummary.bestStage({ 1: { SZ: { wins: 3, losses: 0 } }, @@ -80,7 +80,7 @@ describe("bestStage", () => { }); describe("tournamentRunScore", () => { - it("lets tier dominate over placement quality", () => { + test("lets tier dominate over placement quality", () => { const higherTierRun = SeasonSummary.tournamentRunScore({ tier: 2, placement: 2, @@ -97,7 +97,7 @@ describe("tournamentRunScore", () => { expect(higherTierRun).toBeGreaterThan(lowerTierWin); }); - it("rewards better placement within the same tier", () => { + test("rewards better placement within the same tier", () => { const winner = SeasonSummary.tournamentRunScore({ tier: 5, placement: 1, @@ -114,7 +114,7 @@ describe("tournamentRunScore", () => { expect(winner).toBeGreaterThan(runnerUp); }); - it("scores an untiered tournament below a tiered one with a similar run", () => { + test("scores an untiered tournament below a tiered one with a similar run", () => { const untiered = SeasonSummary.tournamentRunScore({ tier: null, placement: 1, @@ -131,7 +131,7 @@ describe("tournamentRunScore", () => { expect(untiered).toBeLessThan(tiered); }); - it("breaks a tie between identical runs of the same tier by field strength", () => { + test("breaks a tie between identical runs of the same tier by field strength", () => { const strongField = SeasonSummary.tournamentRunScore({ tier: 4, placement: 3, @@ -148,7 +148,7 @@ describe("tournamentRunScore", () => { expect(strongField).toBeGreaterThan(weakField); }); - it("does not let field strength outweigh a tier step", () => { + test("does not let field strength outweigh a tier step", () => { const strongerField = SeasonSummary.tournamentRunScore({ tier: 4, placement: 3, @@ -167,11 +167,11 @@ describe("tournamentRunScore", () => { }); describe("bestTournamentRun", () => { - it("returns undefined for no runs", () => { + test("returns undefined for no runs", () => { expect(SeasonSummary.bestTournamentRun([])).toBeUndefined(); }); - it("picks the run with the highest score", () => { + test("picks the run with the highest score", () => { const runs = [ { tier: 6, placement: 1, teamsCount: 32, topEightAvgSp: 2400 }, { tier: 2, placement: 10, teamsCount: 32, topEightAvgSp: 1800 }, @@ -181,7 +181,7 @@ describe("bestTournamentRun", () => { expect(SeasonSummary.bestTournamentRun(runs)).toBe(runs[1]); }); - it("picks the stronger field among runs tied by tier and placement", () => { + test("picks the stronger field among runs tied by tier and placement", () => { const runs = [ { tier: 3, placement: 5, teamsCount: 32, topEightAvgSp: 1900 }, { tier: 3, placement: 5, teamsCount: 32, topEightAvgSp: 2500 }, @@ -193,11 +193,11 @@ describe("bestTournamentRun", () => { }); describe("topWeaponUsages", () => { - it("returns empty array for no reported weapons", () => { + test("returns empty array for no reported weapons", () => { expect(SeasonSummary.topWeaponUsages([])).toEqual([]); }); - it("returns the most used weapons with their usage share", () => { + test("returns the most used weapons with their usage share", () => { expect( SeasonSummary.topWeaponUsages([ { weaponSplId: 40, count: 10 }, @@ -218,19 +218,19 @@ const OFF_SEASON_DATE = new Date("2026-05-20T12:00:00Z"); const MID_SEASON_12_DATE = new Date("2026-06-10T12:00:00Z"); describe("isSeasonExportableByAll", () => { - it("latest finished season is exportable during off-season", () => { + test("latest finished season is exportable during off-season", () => { expect(SeasonSummary.isSeasonExportableByAll(11, OFF_SEASON_DATE)).toBe( true, ); }); - it("older seasons are not exportable during off-season", () => { + test("older seasons are not exportable during off-season", () => { expect(SeasonSummary.isSeasonExportableByAll(10, OFF_SEASON_DATE)).toBe( false, ); }); - it("nothing is exportable while a season is in progress", () => { + test("nothing is exportable while a season is in progress", () => { expect(SeasonSummary.isSeasonExportableByAll(11, MID_SEASON_12_DATE)).toBe( false, ); @@ -247,17 +247,17 @@ describe("canExportSeasonSummary", () => { date: OFF_SEASON_DATE, }; - it("allows the profile owner to export the latest finished season during off-season", () => { + test("allows the profile owner to export the latest finished season during off-season", () => { expect(SeasonSummary.canExportSeasonSummary(baseArgs)).toBe(true); }); - it("disallows exporting someone else's profile", () => { + test("disallows exporting someone else's profile", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, profileUserId: 2 }), ).toBe(false); }); - it("disallows without being logged in", () => { + test("disallows without being logged in", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, @@ -266,7 +266,7 @@ describe("canExportSeasonSummary", () => { ).toBe(false); }); - it("disallows a season not participated in", () => { + test("disallows a season not participated in", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, @@ -275,7 +275,7 @@ describe("canExportSeasonSummary", () => { ).toBe(false); }); - it("disallows without a calculated skill", () => { + test("disallows without a calculated skill", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, @@ -284,13 +284,13 @@ describe("canExportSeasonSummary", () => { ).toBe(false); }); - it("disallows a non-supporter exporting an older season", () => { + test("disallows a non-supporter exporting an older season", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, season: 10 }), ).toBe(false); }); - it("allows a supporter to export any finished participated season, also mid-season", () => { + test("allows a supporter to export any finished participated season, also mid-season", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, @@ -301,7 +301,7 @@ describe("canExportSeasonSummary", () => { ).toBe(true); }); - it("disallows exporting an ongoing season", () => { + test("disallows exporting an ongoing season", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, @@ -312,7 +312,7 @@ describe("canExportSeasonSummary", () => { ).toBe(false); }); - it("disallows a supporter exporting an ongoing season", () => { + test("disallows a supporter exporting an ongoing season", () => { expect( SeasonSummary.canExportSeasonSummary({ ...baseArgs, diff --git a/app/features/img-upload/ImageRepository.server.test.ts b/app/features/img-upload/ImageRepository.server.test.ts index 625c70dcf..a3b15f84b 100644 --- a/app/features/img-upload/ImageRepository.server.test.ts +++ b/app/features/img-upload/ImageRepository.server.test.ts @@ -6,20 +6,18 @@ import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as ArtRepository from "../art/ArtRepository.server"; import * as ImageRepository from "./ImageRepository.server"; -let submitter: { id: number }; -let otherSubmitter: { id: number }; -let thirdSubmitter: { id: number }; +const users = UserFactory.pool(); const createUnvalidatedArt = (authorId: number) => ArtFactory.create({ authorId, validatedAt: null }); describe("findById", () => { beforeEach(async () => { - submitter = await UserFactory.create(); + await users.create(1); }); test("finds image by id", async () => { - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); const result = await ImageRepository.findById(img.id); @@ -28,9 +26,9 @@ describe("findById", () => { }); test("finds image with calendar event data", async () => { - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); await CalendarEventFactory.create({ - authorId: submitter.id, + authorId: users.id(1), avatarImgId: img.id, }); @@ -49,11 +47,11 @@ describe("findById", () => { describe("deleteById", () => { beforeEach(async () => { - submitter = await UserFactory.create(); + await users.create(1); }); test("deletes image by id", async () => { - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); await ImageRepository.deleteById(img.id); @@ -62,9 +60,9 @@ describe("deleteById", () => { }); test("deletes associated art when deleting image", async () => { - const art = await ArtFactory.create({ authorId: submitter.id }); + const art = await ArtFactory.create({ authorId: users.id(1) }); - const artsBefore = await ArtRepository.findArtsByUserId(submitter.id); + const artsBefore = await ArtRepository.findArtsByUserId(users.id(1)); expect(artsBefore).toHaveLength(1); expect(artsBefore[0].id).toBe(art.id); @@ -73,36 +71,36 @@ describe("deleteById", () => { const result = await ImageRepository.findById(art.imgId); expect(result).toBeUndefined(); - const artsAfter = await ArtRepository.findArtsByUserId(submitter.id); + const artsAfter = await ArtRepository.findArtsByUserId(users.id(1)); expect(artsAfter).toHaveLength(0); }); }); describe("countUnvalidatedArt", () => { beforeEach(async () => { - submitter = await UserFactory.create(); + await users.create(1); }); test("counts unvalidated art by author", async () => { - await createUnvalidatedArt(submitter.id); - await createUnvalidatedArt(submitter.id); + await createUnvalidatedArt(users.id(1)); + await createUnvalidatedArt(users.id(1)); - const count = await ImageRepository.countUnvalidatedArt(submitter.id); + const count = await ImageRepository.countUnvalidatedArt(users.id(1)); expect(count).toBe(2); }); test("does not count validated art", async () => { - await createUnvalidatedArt(submitter.id); - await ArtFactory.create({ authorId: submitter.id }); + await createUnvalidatedArt(users.id(1)); + await ArtFactory.create({ authorId: users.id(1) }); - const count = await ImageRepository.countUnvalidatedArt(submitter.id); + const count = await ImageRepository.countUnvalidatedArt(users.id(1)); expect(count).toBe(1); }); test("returns 0 when author has no unvalidated art", async () => { - const count = await ImageRepository.countUnvalidatedArt(submitter.id); + const count = await ImageRepository.countUnvalidatedArt(users.id(1)); expect(count).toBe(0); }); @@ -110,11 +108,11 @@ describe("countUnvalidatedArt", () => { describe("countAllUnvalidated", () => { beforeEach(async () => { - submitter = await UserFactory.create(); + await users.create(1); }); test("counts unvalidated images used in art", async () => { - await createUnvalidatedArt(submitter.id); + await createUnvalidatedArt(users.id(1)); const count = await ImageRepository.countAllUnvalidated(); @@ -122,9 +120,9 @@ describe("countAllUnvalidated", () => { }); test("counts unvalidated images used in calendar events", async () => { - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); await CalendarEventFactory.create({ - authorId: submitter.id, + authorId: users.id(1), avatarImgId: img.id, }); @@ -134,7 +132,7 @@ describe("countAllUnvalidated", () => { }); test("does not count validated images", async () => { - await ArtFactory.create({ authorId: submitter.id }); + await ArtFactory.create({ authorId: users.id(1) }); const count = await ImageRepository.countAllUnvalidated(); @@ -142,11 +140,11 @@ describe("countAllUnvalidated", () => { }); test("counts multiple unvalidated images across different types", async () => { - await createUnvalidatedArt(submitter.id); + await createUnvalidatedArt(users.id(1)); - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); await CalendarEventFactory.create({ - authorId: submitter.id, + authorId: users.id(1), avatarImgId: img.id, }); @@ -164,46 +162,46 @@ describe("countAllUnvalidated", () => { describe("countUnvalidatedBySubmitterUserId", () => { beforeEach(async () => { - [submitter, otherSubmitter] = await UserFactory.createMany(2); + await users.create(2); }); test("counts unvalidated images connected to art by submitter", async () => { - await createUnvalidatedArt(submitter.id); - await createUnvalidatedArt(submitter.id); + await createUnvalidatedArt(users.id(1)); + await createUnvalidatedArt(users.id(1)); const count = await ImageRepository.countUnvalidatedBySubmitterUserId( - submitter.id, + users.id(1), ); expect(count).toBe(2); }); test("does not count orphan images not connected to anything", async () => { - await ImageFactory.create({ submitterUserId: submitter.id }); + await ImageFactory.create({ submitterUserId: users.id(1) }); const count = await ImageRepository.countUnvalidatedBySubmitterUserId( - submitter.id, + users.id(1), ); expect(count).toBe(0); }); test("does not count validated images", async () => { - await ArtFactory.create({ authorId: submitter.id }); + await ArtFactory.create({ authorId: users.id(1) }); const count = await ImageRepository.countUnvalidatedBySubmitterUserId( - submitter.id, + users.id(1), ); expect(count).toBe(0); }); test("does not count images from other submitters", async () => { - await createUnvalidatedArt(submitter.id); - await createUnvalidatedArt(otherSubmitter.id); + await createUnvalidatedArt(users.id(1)); + await createUnvalidatedArt(users.id(2)); const count = await ImageRepository.countUnvalidatedBySubmitterUserId( - submitter.id, + users.id(1), ); expect(count).toBe(1); @@ -211,7 +209,7 @@ describe("countUnvalidatedBySubmitterUserId", () => { test("returns 0 when user has no unvalidated images", async () => { const count = await ImageRepository.countUnvalidatedBySubmitterUserId( - submitter.id, + users.id(1), ); expect(count).toBe(0); @@ -220,11 +218,11 @@ describe("countUnvalidatedBySubmitterUserId", () => { describe("validateById", () => { beforeEach(async () => { - submitter = await UserFactory.create(); + await users.create(1); }); test("marks image as validated", async () => { - const img = await ImageFactory.create({ submitterUserId: submitter.id }); + const img = await ImageFactory.create({ submitterUserId: users.id(1) }); await ImageRepository.validateById(img.id); @@ -233,7 +231,7 @@ describe("validateById", () => { }); test("validated image is not included in unvalidated count", async () => { - const art = await createUnvalidatedArt(submitter.id); + const art = await createUnvalidatedArt(users.id(1)); const countBefore = await ImageRepository.countAllUnvalidated(); expect(countBefore).toBe(1); @@ -247,16 +245,13 @@ describe("validateById", () => { describe("findAllUnvalidated", () => { beforeEach(async () => { - [submitter, otherSubmitter, thirdSubmitter] = await UserFactory.createMany( - 3, - (index) => ({ discordName: `user${index + 1}` }), - ); + await users.create(3, (index) => ({ discordName: `user${index + 1}` })); }); test("fetches unvalidated images with submitter info", async () => { const filename = "unvalidated-art.png"; await ArtFactory.create({ - authorId: submitter.id, + authorId: users.id(1), url: filename, validatedAt: null, }); @@ -264,13 +259,13 @@ describe("findAllUnvalidated", () => { const result = await ImageRepository.findAllUnvalidated(); expect(result).toHaveLength(1); - expect(result[0].submitterUserId).toBe(submitter.id); + expect(result[0].submitterUserId).toBe(users.id(1)); expect(result[0].username).toBe("user1"); expect(result[0].url).toBe(`http://127.0.0.1:9000/sendou/${filename}`); }); test("does not fetch validated images", async () => { - await ArtFactory.create({ authorId: submitter.id }); + await ArtFactory.create({ authorId: users.id(1) }); const result = await ImageRepository.findAllUnvalidated(); @@ -278,14 +273,14 @@ describe("findAllUnvalidated", () => { }); test("fetches images from art and calendar events", async () => { - await createUnvalidatedArt(submitter.id); - await createUnvalidatedArt(otherSubmitter.id); + await createUnvalidatedArt(users.id(1)); + await createUnvalidatedArt(users.id(2)); const img = await ImageFactory.create({ - submitterUserId: thirdSubmitter.id, + submitterUserId: users.id(3), }); await CalendarEventFactory.create({ - authorId: thirdSubmitter.id, + authorId: users.id(3), avatarImgId: img.id, }); @@ -296,7 +291,7 @@ describe("findAllUnvalidated", () => { test("respects the max unvalidated images to show at once for approval limit constant", async () => { for (let i = 0; i < 10; i++) { - await createUnvalidatedArt(submitter.id); + await createUnvalidatedArt(users.id(1)); } const result = await ImageRepository.findAllUnvalidated(); diff --git a/app/features/leaderboards/LeaderboardRepository.server.test.ts b/app/features/leaderboards/LeaderboardRepository.server.test.ts index 2c19f2aaf..b9bb52d1c 100644 --- a/app/features/leaderboards/LeaderboardRepository.server.test.ts +++ b/app/features/leaderboards/LeaderboardRepository.server.test.ts @@ -25,19 +25,15 @@ const OVER_THRESHOLD = MATCHES_COUNT_NEEDED_FOR_LEADERBOARD + 1; const IN_SEASON = SEASON_RANGE.starts; const OUT_OF_SEASON = new Date(SEASON_RANGE.starts.getTime() - 60 * 1000); -let player: { id: number }; -let otherPlayer: { id: number }; -/** The other players of the SendouQ groups the two report their weapons in. */ -let groupFillers: Array<{ id: number }>; +/** The first two report their weapons; the rest fill out their SendouQ groups. */ +const users = UserFactory.pool(); const createSendouqMatch = (createdAt: Date) => // played out so that the groups go inactive and the same users can queue again SQMatchFactory.create( { - alphaUserIds: [player, otherPlayer, ...groupFillers.slice(0, 2)].map( - (user) => user.id, - ), - bravoUserIds: groupFillers.slice(2).map((user) => user.id), + alphaUserIds: users.ids(4), + bravoUserIds: users.ids().slice(4), }, { isConcluded: true, createdAt }, ); @@ -54,7 +50,7 @@ const createTournamentMatch = async ({ { authorId, minMembersPerTeam: 1 }, { playedOut: isFinalized ? "all" : 0, - teamRosters: [[authorId], [groupFillers[1].id]], + teamRosters: [[authorId], [users.id(4)]], }, ); @@ -100,13 +96,12 @@ const reportTournamentWeapons = async (args: { describe("findSeasonPopularUsersWeapon", () => { beforeEach(async () => { - const users = await UserFactory.createMany(FULL_GROUP_SIZE * 2); - [player, otherPlayer, ...groupFillers] = users; + await users.create(FULL_GROUP_SIZE * 2); }); test("returns user's most reported SendouQ weapon", async () => { await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD, }); @@ -114,12 +109,12 @@ describe("findSeasonPopularUsersWeapon", () => { const result = await LeaderboardRepository.findSeasonPopularUsersWeapon(SEASON); - expect(result).toEqual({ [player.id]: 10 }); + expect(result).toEqual({ [users.id(1)]: 10 }); }); test("requires more reports than the threshold", async () => { await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: MATCHES_COUNT_NEEDED_FOR_LEADERBOARD, }); @@ -132,7 +127,7 @@ describe("findSeasonPopularUsersWeapon", () => { test("counts weapons reported in finalized tournaments", async () => { await reportTournamentWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 1000, count: OVER_THRESHOLD, }); @@ -140,12 +135,12 @@ describe("findSeasonPopularUsersWeapon", () => { const result = await LeaderboardRepository.findSeasonPopularUsersWeapon(SEASON); - expect(result).toEqual({ [player.id]: 1000 }); + expect(result).toEqual({ [users.id(1)]: 1000 }); }); test("ignores weapons reported in unfinalized tournaments", async () => { await reportTournamentWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 1000, count: OVER_THRESHOLD, isFinalized: false, @@ -161,12 +156,12 @@ describe("findSeasonPopularUsersWeapon", () => { const half = Math.ceil(OVER_THRESHOLD / 2); await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: half, }); await reportTournamentWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD - half, }); @@ -174,22 +169,22 @@ describe("findSeasonPopularUsersWeapon", () => { const result = await LeaderboardRepository.findSeasonPopularUsersWeapon(SEASON); - expect(result).toEqual({ [player.id]: 10 }); + expect(result).toEqual({ [users.id(1)]: 10 }); }); test("picks the most reported weapon across both sources", async () => { await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 0, count: OVER_THRESHOLD + 1, }); await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD - 3, }); await reportTournamentWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD - 3, }); @@ -197,17 +192,17 @@ describe("findSeasonPopularUsersWeapon", () => { const result = await LeaderboardRepository.findSeasonPopularUsersWeapon(SEASON); - expect(result).toEqual({ [player.id]: 10 }); + expect(result).toEqual({ [users.id(1)]: 10 }); }); test("returns weapons of multiple users", async () => { await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD, }); await reportTournamentWeapons({ - userId: otherPlayer.id, + userId: users.id(2), weaponSplId: 1000, count: OVER_THRESHOLD, }); @@ -215,18 +210,18 @@ describe("findSeasonPopularUsersWeapon", () => { const result = await LeaderboardRepository.findSeasonPopularUsersWeapon(SEASON); - expect(result).toEqual({ [player.id]: 10, [otherPlayer.id]: 1000 }); + expect(result).toEqual({ [users.id(1)]: 10, [users.id(2)]: 1000 }); }); test("ignores reports outside the season", async () => { await reportSendouqWeapons({ - userId: player.id, + userId: users.id(1), weaponSplId: 10, count: OVER_THRESHOLD, matchCreatedAt: OUT_OF_SEASON, }); await reportTournamentWeapons({ - userId: otherPlayer.id, + userId: users.id(2), weaponSplId: 1000, count: OVER_THRESHOLD, createdAt: OUT_OF_SEASON, diff --git a/app/features/leaderboards/leaderboards-search-params.test.ts b/app/features/leaderboards/leaderboards-search-params.test.ts index 14d2c845a..dcbde1403 100644 --- a/app/features/leaderboards/leaderboards-search-params.test.ts +++ b/app/features/leaderboards/leaderboards-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { leaderboardsSearchParams } from "./leaderboards-search-params"; describe("leaderboardsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(leaderboardsSearchParams, { type: [ "USER", @@ -21,7 +21,7 @@ describe("leaderboardsSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(leaderboardsSearchParams, "type", [ ["garbage"], ["XP-WEAPON-99999"], diff --git a/app/features/lfg/lfg-search-params.test.ts b/app/features/lfg/lfg-search-params.test.ts index d609a0864..bf810eb10 100644 --- a/app/features/lfg/lfg-search-params.test.ts +++ b/app/features/lfg/lfg-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { lfgNewSearchParams, lfgSearchParams } from "./lfg-search-params"; describe("lfgSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(lfgSearchParams, { weapons: [[], [0], [0, 10, 4001]], type: [null, "PLAYER_FOR_TEAM", "COACH_FOR_TEAM"], @@ -18,7 +18,7 @@ describe("lfgSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(lfgSearchParams, "type", [["NOT_A_TYPE"], [""]]); assertDecodesToDefault(lfgSearchParams, "timezone", [ ["13"], @@ -32,13 +32,13 @@ describe("lfgSearchParams", () => { }); describe("lfgNewSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(lfgNewSearchParams, { postId: [1, 123], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(lfgNewSearchParams, "postId", [["abc"], ["0"]]); }); }); diff --git a/app/features/map-list-generator/core/MapList.test.ts b/app/features/map-list-generator/core/MapList.test.ts index cca50e4db..85818db5f 100644 --- a/app/features/map-list-generator/core/MapList.test.ts +++ b/app/features/map-list-generator/core/MapList.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { StageId } from "~/modules/in-game-lists/types"; import { unwrap } from "~/utils/result"; @@ -29,17 +29,12 @@ describe("MapList.generate()", () => { } describe("singular map list", () => { - it("returns an array with given amount of items", () => { + test.each([1, 3, 5])("returns an array of %d items", (amount) => { const gen = initGenerator(); - expect(gen.next({ amount: 3 }).value).toHaveLength(3); + expect(gen.next({ amount }).value).toHaveLength(amount); }); - it("returns an array with only one item", () => { - const gen = initGenerator(); - expect(gen.next({ amount: 1 }).value).toHaveLength(1); - }); - - it("includes only maps from the given map pool", () => { + test("includes only maps from the given map pool", () => { const gen = initGenerator(); const maps = gen.next({ amount: 3 }).value; @@ -48,7 +43,7 @@ describe("MapList.generate()", () => { } }); - it("contains only unique maps, when possible", () => { + test("contains only unique maps, when possible", () => { const gen = initGenerator(); const maps = gen.next({ amount: 3 }).value; @@ -57,7 +52,7 @@ describe("MapList.generate()", () => { ); }); - it("repeats maps when amount is larger than pool size", () => { + test("repeats maps when amount is larger than pool size", () => { const gen = initGenerator( new MapPool({ TW: [1], @@ -75,7 +70,7 @@ describe("MapList.generate()", () => { } }); - it("contains every mode once before repeating", () => { + test("contains every mode once before repeating", () => { const gen = initGenerator(); const maps = gen.next({ amount: 5 }).value; const modes = maps.map((m) => m.mode); @@ -85,21 +80,21 @@ describe("MapList.generate()", () => { } }); - it("repeats a mode following the pattern when amount bigger than mode count", () => { + test("repeats a mode following the pattern when amount bigger than mode count", () => { const gen = initGenerator(); const maps = gen.next({ amount: 6 }).value; expect(maps[0].mode).toBe(maps[5].mode); }); - it("handles empty map pool", () => { + test("handles empty map pool", () => { const gen = initGenerator(MapPool.EMPTY); const maps = gen.next({ amount: 3 }).value; expect(maps).toHaveLength(0); }); - it("follows a pattern", () => { + test("follows a pattern", () => { for (let i = 0; i < 10; i++) { const gen = initGenerator(); const maps = gen.next({ amount: 3, pattern: "*SZ*" }).value; @@ -109,7 +104,7 @@ describe("MapList.generate()", () => { } }); - it("follows and repeats a pattern", () => { + test("follows and repeats a pattern", () => { const gen = initGenerator(); const maps = gen.next({ amount: 5, pattern: "*SZ*" }).value; @@ -118,7 +113,7 @@ describe("MapList.generate()", () => { expect(maps[3].mode).toBe("SZ"); }); - it("guarantees a must-include even when the pattern's only ANY slot is in the back half", () => { + test("guarantees a must-include even when the pattern's only ANY slot is in the back half", () => { for (let i = 0; i < 10; i++) { const gen = initGenerator(); const maps = gen.next({ amount: 3, pattern: "[RM!]SZTC*" }).value; @@ -128,7 +123,7 @@ describe("MapList.generate()", () => { } }); - it("follows a one mode only pattern", () => { + test("follows a one mode only pattern", () => { const gen = initGenerator(); const maps = gen.next({ amount: 3, pattern: "SZ" }).value; @@ -137,7 +132,7 @@ describe("MapList.generate()", () => { expect(maps[2].mode).toBe("SZ"); }); - it("follows a pattern where starting and ending mode is the same", () => { + test("follows a pattern where starting and ending mode is the same", () => { const gen = initGenerator( new MapPool({ ...ALL_MODES_TEST_MAP_POOL.getClonedObject(), @@ -151,7 +146,7 @@ describe("MapList.generate()", () => { expect(maps[4].mode, "Map 5 is not SZ").toBe("SZ"); }); - it("follows a one mode only pattern (Bo9)", () => { + test("follows a one mode only pattern (Bo9)", () => { const gen = initGenerator(); const maps = gen.next({ amount: 9, pattern: "SZ" }).value; @@ -160,7 +155,7 @@ describe("MapList.generate()", () => { } }); - it("includes a mustInclude mode", () => { + test("includes a mustInclude mode", () => { for (let i = 0; i < 10; i++) { const gen = initGenerator(); const maps = gen.next({ amount: 1, pattern: "[SZ]" }).value; @@ -169,7 +164,7 @@ describe("MapList.generate()", () => { } }); - it("includes a mustInclude mode (guaranteed)", () => { + test("includes a mustInclude mode (guaranteed)", () => { const gen = initGenerator(); for (let i = 0; i < 50; i++) { const maps = gen.next({ amount: 5, pattern: "[SZ!]" }).value; @@ -178,7 +173,7 @@ describe("MapList.generate()", () => { } }); - it("includes a mustInclude mode with pattern", () => { + test("includes a mustInclude mode with pattern", () => { const gen = initGenerator(); for (let i = 0; i < 50; i++) { const maps = gen.next({ amount: 3, pattern: "[SZ]*TC*" }).value; @@ -187,7 +182,7 @@ describe("MapList.generate()", () => { } }); - it("follows a pattern with multiple specific modes", () => { + test("follows a pattern with multiple specific modes", () => { const gen = initGenerator(); const maps = gen.next({ amount: 5, pattern: "SZ*TC" }).value; @@ -196,14 +191,14 @@ describe("MapList.generate()", () => { expect(maps[2].mode, "missign TC (required by pattern)").toBe("TC"); }); - it("places a non-guaranteed must-include when the pattern has no flexible slots but more maps than pattern parts", () => { + test("places a non-guaranteed must-include when the pattern has no flexible slots but more maps than pattern parts", () => { const gen = initGenerator(); const maps = gen.next({ amount: 5, pattern: "[RM]SZTC" }).value; expect(maps.map((map) => map.mode)).toContain("RM"); }); - it("handles a conflict between pattern and must include", () => { + test("handles a conflict between pattern and must include", () => { const gen = initGenerator(); const maps = gen.next({ amount: 1, pattern: "TW[SZ]" }).value; @@ -211,7 +206,7 @@ describe("MapList.generate()", () => { expect(maps[0].mode).toBe("TW"); // pattern has priority }); - it("handles more must include modes than amount", () => { + test("handles more must include modes than amount", () => { const gen = initGenerator(); const maps = gen.next({ amount: 1, pattern: "[TW][SZ]" }).value; @@ -219,7 +214,7 @@ describe("MapList.generate()", () => { expect(["TW", "SZ"]).toContain(maps[0].mode); }); - it("ignores a mode in the pattern not in the map pool", () => { + test("ignores a mode in the pattern not in the map pool", () => { const gen = initGenerator( new MapPool({ TW: [1, 2, 3], @@ -237,7 +232,7 @@ describe("MapList.generate()", () => { expect(maps[2].mode).toBe("TW"); }); - it("ignores a must include mode not in the map pool", () => { + test("ignores a must include mode not in the map pool", () => { const gen = initGenerator( new MapPool({ TW: [1, 2, 3], @@ -255,7 +250,7 @@ describe("MapList.generate()", () => { }); describe("many map lists", () => { - it("generates many map lists", () => { + test("generates many map lists", () => { const gen = initGenerator(); const first = gen.next({ amount: 3 }).value; const second = gen.next({ amount: 3 }).value; @@ -264,7 +259,7 @@ describe("MapList.generate()", () => { expect(second).toBeInstanceOf(Array); }); - it("has different maps in each list", () => { + test("has different maps in each list", () => { // TW, SZ & TC with 3 maps each const mapPool = new MapPool({ TW: [1, 2, 3], @@ -291,7 +286,7 @@ describe("MapList.generate()", () => { expect(all).toContainEqual({ mode: "TC", stageId: 9 }); }); - it("randomizes the stage order", () => { + test("randomizes the stage order", () => { const stagesSeen = new Set(); for (let i = 0; i < 10; i++) { const gen = initGenerator(ALL_MAPS_TEST_MAP_POOL); @@ -303,7 +298,7 @@ describe("MapList.generate()", () => { expect(stagesSeen.size).toBeGreaterThan(1); }); - it("cycles a single mode order continuously across sets", () => { + test("cycles a single mode order continuously across sets", () => { // 5 modes, Bo3 sets -> the order keeps rolling without resetting const gen = initGenerator(); const first = gen.next({ amount: 3 }).value!.map((m) => m.mode); @@ -313,7 +308,7 @@ describe("MapList.generate()", () => { expect(second[2]).toBe(first[0]); }); - it("uses the same mode order when a set spans the whole rotation", () => { + test("uses the same mode order when a set spans the whole rotation", () => { // 5 modes, Bo5 sets -> each set is exactly one full rotation const gen = initGenerator(); const first = gen.next({ amount: 5 }).value!.map((m) => m.mode); @@ -322,7 +317,7 @@ describe("MapList.generate()", () => { expect(second).toEqual(first); }); - it("keeps cycling other modes across sets when a must-include pattern is set", () => { + test("keeps cycling other modes across sets when a must-include pattern is set", () => { // A single generator drives every bracket round. With a `[SZ]` // must-include pattern the non-SZ slots should keep advancing through // the mode order across rounds instead of replaying the order's prefix @@ -342,7 +337,7 @@ describe("MapList.generate()", () => { expect(modesSeen).toContain("RM"); }); - it("keeps cycling other modes across sets when a positional pattern is set", () => { + test("keeps cycling other modes across sets when a positional pattern is set", () => { // With a `*SZ*` pattern and Bo3 sets the two ANY slots are filled from // the non-SZ modes (TC, RM, CB). Because the cycle offset advances by the // full set size (3) instead of the slots actually consumed, every round @@ -371,7 +366,7 @@ describe("MapList.generate()", () => { expect([...modesSeen].sort()).toEqual(["CB", "RM", "SZ", "TC"]); }); - it("advances the cycle by the ANY slots consumed when a positional pattern is set", () => { + test("advances the cycle by the ANY slots consumed when a positional pattern is set", () => { const gen = MapList.generate({ mapPool: new MapPool({ TW: [], @@ -392,7 +387,7 @@ describe("MapList.generate()", () => { expect(nextModes()).toEqual(["RM", "SZ", "CB"]); }); - it("replenishes the stage id pool with different order", () => { + test("replenishes the stage id pool with different order", () => { const gen = initGenerator( new MapPool({ TW: [], @@ -417,7 +412,7 @@ describe("MapList.generate()", () => { expect(someDifferent).toBe(true); }); - it("should find unique maps when possible (All 4 One #50 bug)", () => { + test("finds unique maps when possible (All 4 One #50 bug)", () => { const mapPool = new MapPool({ TW: [], SZ: [1, 2, 3, 4, 5, 6, 7], @@ -444,7 +439,7 @@ describe("MapList.generate()", () => { } }); - it("should find unique maps when possible (All 4 One #51 bug)", () => { + test("finds unique maps when possible (All 4 One #51 bug)", () => { const mapPool = new MapPool({ TW: [], SZ: [1, 2, 3, 4, 5, 6, 7], @@ -468,7 +463,7 @@ describe("MapList.generate()", () => { } }); - it("applies different weight penalties based on guaranteed positions when considerGuaranteed is true", () => { + test("applies different weight penalties based on guaranteed positions when considerGuaranteed is true", () => { const mapPool = new MapPool({ TW: [], SZ: [1, 2, 3, 4, 5], @@ -517,49 +512,49 @@ describe("MapList.generate()", () => { }); describe("MapList.parsePattern()", () => { - it("parses a simple pattern", () => { + test("parses a simple pattern", () => { expect(unwrap(MapList.parsePattern("SZ*TC"))).toEqual({ pattern: ["SZ", "ANY", "TC"], }); }); - it("handles extra spaces", () => { + test("handles extra spaces", () => { expect(unwrap(MapList.parsePattern(" * SZ "))).toEqual({ pattern: ["ANY", "SZ"], }); }); - it("handles the same mode twice in pattern", () => { + test("handles the same mode twice in pattern", () => { expect(unwrap(MapList.parsePattern("SZ*SZ"))).toEqual({ pattern: ["SZ", "ANY", "SZ"], }); }); - it("returns error on invalid mode", () => { + test("returns error on invalid mode", () => { expect(MapList.parsePattern("*INVALID*").ok).toBe(false); }); - it("if starts and ends with ANY, the ending ANY is dropped", () => { + test("if starts and ends with ANY, the ending ANY is dropped", () => { expect(unwrap(MapList.parsePattern("*SZ*"))).toEqual({ pattern: ["ANY", "SZ"], }); }); - it("parses a mustInclude mode", () => { + test("parses a mustInclude mode", () => { expect(unwrap(MapList.parsePattern("[SZ]"))).toEqual({ mustInclude: [{ mode: "SZ", isGuaranteed: false }], pattern: [], }); }); - it("parses a guaranteed mustInclude mode", () => { + test("parses a guaranteed mustInclude mode", () => { expect(unwrap(MapList.parsePattern("[SZ!]"))).toEqual({ mustInclude: [{ mode: "SZ", isGuaranteed: true }], pattern: [], }); }); - it("parses a complex pattern", () => { + test("parses a complex pattern", () => { expect(unwrap(MapList.parsePattern(" * [SZ] * TC [TW]"))).toEqual({ mustInclude: [ { mode: "TW", isGuaranteed: false }, @@ -569,20 +564,20 @@ describe("MapList.parsePattern()", () => { }); }); - it("ignores repeated must include mode", () => { + test("ignores repeated must include mode", () => { expect(unwrap(MapList.parsePattern("[SZ][SZ]"))).toEqual({ mustInclude: [{ mode: "SZ", isGuaranteed: false }], pattern: [], }); }); - it("parses an empty pattern", () => { + test("parses an empty pattern", () => { expect(unwrap(MapList.parsePattern(""))).toEqual({ pattern: [], }); }); - it("returns error when pattern is too long", () => { + test("returns error when pattern is too long", () => { const longPattern = "a".repeat(51); const result = MapList.parsePattern(longPattern); expect(result.ok).toBe(false); @@ -591,7 +586,7 @@ describe("MapList.parsePattern()", () => { } }); - it("return error on lorem ipsum", () => { + test("return error on lorem ipsum", () => { expect( MapList.parsePattern( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut varius velit. Ut egestas lacus dolor, sit amet iaculis justo dictum sed. Fusce aliquet sed nunc sit amet ullamcorper. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer leo ex, congue eu porta nec, imperdiet sed neque.", @@ -601,7 +596,7 @@ describe("MapList.parsePattern()", () => { }); describe("MapList.generate() with initialWeights", () => { - it("accepts initialWeights parameter without errors", () => { + test("accepts initialWeights parameter without errors", () => { const mapPool = new MapPool({ SZ: [1, 2, 3], TC: [4, 5], @@ -623,7 +618,7 @@ describe("MapList.generate() with initialWeights", () => { expect(maps.every((m) => mapPool.has(m))).toBe(true); }); - it("handles empty initialWeights", () => { + test("handles empty initialWeights", () => { const mapPool = new MapPool({ SZ: [1, 2], TC: [], @@ -640,7 +635,7 @@ describe("MapList.generate() with initialWeights", () => { expect(maps).toHaveLength(2); }); - it("handles undefined initialWeights", () => { + test("handles undefined initialWeights", () => { const mapPool = new MapPool({ SZ: [1, 2], TC: [], @@ -657,7 +652,7 @@ describe("MapList.generate() with initialWeights", () => { expect(maps).toHaveLength(2); }); - it("initialWeights affect stage selection", () => { + test("initialWeights affect stage selection", () => { const mapPool = new MapPool({ SZ: [1, 2, 3, 4, 5], TC: [], @@ -700,13 +695,13 @@ describe("MapList.resume()", () => { return result![0]; } - it("starts with the pool's first mode when history is empty", () => { + test("starts with the pool's first mode when history is empty", () => { for (let i = 0; i < 20; i++) { expect(nextMap([]).mode).toBe("SZ"); } }); - it("rotates through modes in pool order across history length", () => { + test("rotates through modes in pool order across history length", () => { expect(nextMap([{ mode: "SZ", stageId: 1 }]).mode).toBe("TC"); expect( nextMap([ @@ -723,7 +718,7 @@ describe("MapList.resume()", () => { ).toBe("CB"); }); - it("wraps the mode order back to the start after a full rotation", () => { + test("wraps the mode order back to the start after a full rotation", () => { const history = [ { mode: "SZ", stageId: 1 }, { mode: "TC", stageId: 4 }, @@ -733,7 +728,7 @@ describe("MapList.resume()", () => { expect(nextMap([...history]).mode).toBe("SZ"); }); - it("avoids already-played (mode, stage) pairs", () => { + test("avoids already-played (mode, stage) pairs", () => { const history = [ { mode: "SZ", stageId: 1 }, { mode: "TC", stageId: 4 }, @@ -748,7 +743,7 @@ describe("MapList.resume()", () => { } }); - it("rotates only through modes present in the pool", () => { + test("rotates only through modes present in the pool", () => { const threeModePool = new MapPool({ TW: [], SZ: [1, 2, 3], @@ -782,7 +777,7 @@ describe("MapList.resume()", () => { ).toBe("SZ"); }); - it("avoids the just-played stage when alternatives exist, even across modes", () => { + test("avoids the just-played stage when alternatives exist, even across modes", () => { const sharedPool = new MapPool({ TW: [], SZ: [1, 2, 3, 4, 5], diff --git a/app/features/map-list-generator/map-list-generator-search-params.test.ts b/app/features/map-list-generator/map-list-generator-search-params.test.ts index b449c3484..31eee4a6c 100644 --- a/app/features/map-list-generator/map-list-generator-search-params.test.ts +++ b/app/features/map-list-generator/map-list-generator-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -7,7 +7,7 @@ import { MapPool } from "./core/map-pool"; import { mapListGeneratorSearchParams } from "./map-list-generator-search-params"; describe("mapListGeneratorSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(mapListGeneratorSearchParams, { pool: [ MapPool.ANARCHY.serialized, @@ -18,7 +18,7 @@ describe("mapListGeneratorSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(mapListGeneratorSearchParams, "eventId", [ ["abc"], ["-1"], diff --git a/app/features/match-profile/MatchProfileRepository.server.test.ts b/app/features/match-profile/MatchProfileRepository.server.test.ts index 97e15e3d5..eecad1511 100644 --- a/app/features/match-profile/MatchProfileRepository.server.test.ts +++ b/app/features/match-profile/MatchProfileRepository.server.test.ts @@ -4,7 +4,7 @@ import type { UserMapModePreferences } from "~/db/tables-json"; import { withUserId } from "~/utils/Test"; import * as MatchProfileRepository from "./MatchProfileRepository.server"; -let userId: number; +const users = UserFactory.pool(); const PREFERENCES: UserMapModePreferences = { modes: [{ mode: "SZ", preference: "PREFER" }], @@ -21,7 +21,7 @@ const updateProfile = ( Parameters[0] > = {}, ) => - withUserId(userId, () => + withUserId(users.id(1), () => MatchProfileRepository.updateOwnMatchProfile({ mapModePreferences: PREFERENCES, vc: "NO", @@ -34,10 +34,9 @@ const updateProfile = ( describe("updateOwnMatchProfile", () => { beforeEach(async () => { - const user = await UserFactory.create(null, { + await users.create(1, null, { matchProfile: { mapModePreferences: PREFERENCES, noScreen: 0 }, }); - userId = user.id; }); test("reports no change when nothing matchmaking-relevant changed", async () => { diff --git a/app/features/mmr/core/Seasons.test.ts b/app/features/mmr/core/Seasons.test.ts index e62cbf025..5445dd3c7 100644 --- a/app/features/mmr/core/Seasons.test.ts +++ b/app/features/mmr/core/Seasons.test.ts @@ -1,35 +1,35 @@ import { addHours } from "date-fns"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { list, nthToDateRange, nthToReportingDateRange } from "./Seasons"; describe("nthToDateRange()", () => { - it("returns the date range for an existing season", () => { + test("returns the date range for an existing season", () => { const { starts, ends } = nthToDateRange(0); expect(starts).toEqual(list[0].starts); expect(ends).toEqual(list[0].ends); }); - it("throws for a season number past the end of the list", () => { + test("throws for a season number past the end of the list", () => { expect(() => nthToDateRange(list.length)).toThrow(); }); - it("throws for a negative season number", () => { + test("throws for a negative season number", () => { expect(() => nthToDateRange(-1)).toThrow(); }); }); describe("nthToReportingDateRange()", () => { - it("starts when the season starts", () => { + test("starts when the season starts", () => { const { starts } = nthToReportingDateRange(0); expect(starts).toEqual(list[0].starts); }); - it("ends 25 hours after the season ends, covering matches made at the buzzer", () => { + test("ends 25 hours after the season ends, covering matches made at the buzzer", () => { const { ends } = nthToReportingDateRange(0); expect(ends).toEqual(addHours(list[0].ends, 25)); }); - it("throws for a season number past the end of the list", () => { + test("throws for a season number past the end of the list", () => { expect(() => nthToReportingDateRange(list.length)).toThrow(); }); }); diff --git a/app/features/object-damage-calculator/calculator-search-params.test.ts b/app/features/object-damage-calculator/calculator-search-params.test.ts index 6c462dc65..562f2b794 100644 --- a/app/features/object-damage-calculator/calculator-search-params.test.ts +++ b/app/features/object-damage-calculator/calculator-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SearchParams from "~/modules/search-params/search-params"; import { assertDecodesToDefault, @@ -7,7 +7,7 @@ import { import { calculatorSearchParams } from "./calculator-search-params"; describe("calculatorSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(calculatorSearchParams, { weapon: [ { type: "MAIN", id: 0 }, @@ -21,13 +21,13 @@ describe("calculatorSearchParams", () => { }); }); - it("decodes the legacy bare numeric weapon format", () => { + test("decodes the legacy bare numeric weapon format", () => { expect( SearchParams.decodeParam(calculatorSearchParams.shape.weapon, ["1000"]), ).toEqual({ type: "MAIN", id: 1000 }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(calculatorSearchParams, "weapon", [ [""], ["MAIN_999999"], diff --git a/app/features/params/core/WeaponParams.test.ts b/app/features/params/core/WeaponParams.test.ts index 9d0b26f3c..b057ebbb5 100644 --- a/app/features/params/core/WeaponParams.test.ts +++ b/app/features/params/core/WeaponParams.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { DAMAGE_MULTIPLIER_PARAM_KEY, INCOMING_DAMAGE_MULTIPLIER_PARAM_KEY, @@ -31,7 +31,7 @@ const row = (overrides: { }); describe("damageMultipliersForWeapon", () => { - it("collects only rows applying to the weapon for the given kind", () => { + test("collects only rows applying to the weapon for the given kind", () => { const rows = { a: row({ specialWeaponIds: [11], @@ -48,7 +48,7 @@ describe("damageMultipliersForWeapon", () => { expect(result.map((m) => m.target)).toEqual(["Chariot"]); }); - it("de-duplicates identical target histories shared across rows", () => { + test("de-duplicates identical target histories shared across rows", () => { const sharedTarget: DamageMultiplierWithHistory = { target: "GreatBarrier_Barrier", current: 1.4, @@ -67,7 +67,7 @@ describe("damageMultipliersForWeapon", () => { expect(result).toHaveLength(1); }); - it("merges several rows of the same target into the most informative entry", () => { + test("merges several rows of the same target into the most informative entry", () => { const rows = { swing: row({ specialWeaponIds: [11], @@ -95,7 +95,7 @@ describe("damageMultipliersForWeapon", () => { expect(result[0].current).toBe(3.273); }); - it("orders entries like DAMAGE_RECEIVERS", () => { + test("orders entries like DAMAGE_RECEIVERS", () => { const rows = { a: row({ specialWeaponIds: [11], @@ -117,7 +117,7 @@ describe("patchHistory damage multipliers", () => { const buildWith = (multiplier: DamageMultiplierWithHistory) => WeaponParams.patchHistory(emptyParsed(11), VERSIONS, [], [multiplier]); - it("attributes a change to the version after the recorded one and flags a higher rate as a buff", () => { + test("attributes a change to the version after the recorded one and flags a higher rate as a buff", () => { const patches = buildWith({ target: "Wsb_Shield", current: 2.2, @@ -137,7 +137,7 @@ describe("patchHistory damage multipliers", () => { ]); }); - it("flags a lower rate as a nerf", () => { + test("flags a lower rate as a nerf", () => { const patches = buildWith({ target: "NiceBall_Armor", current: 1.82, @@ -151,7 +151,7 @@ describe("patchHistory damage multipliers", () => { }); describe("incomingDamageMultipliersForWeapon", () => { - it("collects other weapons' rates against the weapon's receiver targets", () => { + test("collects other weapons' rates against the weapon's receiver targets", () => { const rows = { fromSpecial: row({ specialWeaponIds: [10], @@ -193,7 +193,7 @@ describe("incomingDamageMultipliersForWeapon", () => { expect(result[1].attackers.mainWeaponIds).toEqual([200, 201]); }); - it("de-duplicates the same attacker group and target across rows", () => { + test("de-duplicates the same attacker group and target across rows", () => { const target = { target: "GreatBarrier_Barrier", current: 1.4, @@ -213,7 +213,7 @@ describe("incomingDamageMultipliersForWeapon", () => { expect(result).toHaveLength(1); }); - it("returns nothing for a weapon that is not a damageable object", () => { + test("returns nothing for a weapon that is not a damageable object", () => { const rows = { a: row({ specialWeaponIds: [10], @@ -235,7 +235,7 @@ describe("incomingDamageMultipliersForWeapon", () => { }); describe("patchHistory incoming damage multipliers", () => { - it("flags a higher incoming rate as a nerf to the defending weapon and carries the attackers", () => { + test("flags a higher incoming rate as a nerf to the defending weapon and carries the attackers", () => { const patches = WeaponParams.patchHistory( emptyParsed(2), VERSIONS, @@ -267,7 +267,7 @@ describe("patchHistory incoming damage multipliers", () => { }); }); - it("flags a lower incoming rate as a buff to the defending weapon", () => { + test("flags a lower incoming rate as a buff to the defending weapon", () => { const patches = WeaponParams.patchHistory( emptyParsed(2), VERSIONS, @@ -292,7 +292,7 @@ describe("patchHistory incoming damage multipliers", () => { }); describe("parse damage falloff curves", () => { - it("serializes a DistanceDamage array into a scaled damage @ distance string", () => { + test("serializes a DistanceDamage array into a scaled damage @ distance string", () => { const parsed = WeaponParams.parse( 0, { @@ -311,7 +311,7 @@ describe("parse damage falloff curves", () => { ); }); - it("flattens nested breakpoint arrays", () => { + test("flattens nested breakpoint arrays", () => { const parsed = WeaponParams.parse( 0, { @@ -330,7 +330,7 @@ describe("parse damage falloff curves", () => { ); }); - it("tracks per-version history of a damage falloff curve", () => { + test("tracks per-version history of a damage falloff curve", () => { const parsed = WeaponParams.parse( 0, { @@ -349,19 +349,19 @@ describe("parse damage falloff curves", () => { }); describe("classifyParamChange damage falloff curves", () => { - it("flags higher damage as a buff", () => { + test("flags higher damage as a buff", () => { expect( classifyParamChange("BlastParam", "DistanceDamage", "40 @ 4", "60 @ 4"), ).toBe("buff"); }); - it("flags lower damage as a nerf", () => { + test("flags lower damage as a nerf", () => { expect( classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "40 @ 4"), ).toBe("nerf"); }); - it("flags longer reach at the same damage as a buff", () => { + test("flags longer reach at the same damage as a buff", () => { expect( classifyParamChange( "BlastParam", @@ -372,7 +372,7 @@ describe("classifyParamChange damage falloff curves", () => { ).toBe("buff"); }); - it("flags shorter reach at the same damage as a nerf", () => { + test("flags shorter reach at the same damage as a nerf", () => { expect( classifyParamChange( "BlastParam", @@ -383,13 +383,13 @@ describe("classifyParamChange damage falloff curves", () => { ).toBe("nerf"); }); - it("is neutral when damage rises but reach shrinks", () => { + test("is neutral when damage rises but reach shrinks", () => { expect( classifyParamChange("BlastParam", "DistanceDamage", "60 @ 4", "70 @ 3.5"), ).toBe("neutral"); }); - it("is neutral when the curve gains or loses a breakpoint", () => { + test("is neutral when the curve gains or loses a breakpoint", () => { expect( classifyParamChange( "BlastParam", @@ -445,7 +445,7 @@ describe("kitPatchHistories", () => { specialIncomingDamageMultipliers: {}, }); - it("folds the kit's main, sub and special weapon changes into one descending history", () => { + test("folds the kit's main, sub and special weapon changes into one descending history", () => { const [history] = kitHistory(); expect(history.weaponId).toBe(11); @@ -455,7 +455,7 @@ describe("kitPatchHistories", () => { ]); }); - it("tags each change with its source and groups main before sub before special", () => { + test("tags each change with its source and groups main before sub before special", () => { const [history] = kitHistory(); const v2 = history.patches.find((patch) => patch.version === "2.0.0")!; diff --git a/app/features/params/params-search-params.test.ts b/app/features/params/params-search-params.test.ts index a331abd76..83cc274b5 100644 --- a/app/features/params/params-search-params.test.ts +++ b/app/features/params/params-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SearchParams from "~/modules/search-params/search-params"; import { assertDecodesToDefault, @@ -7,7 +7,7 @@ import { import { weaponParamsSearchParams } from "./params-search-params"; describe("weaponParamsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(weaponParamsSearchParams, { tab: ["params", "patches"], hidden: [[], [10], [0, 50, 1000]], @@ -16,7 +16,7 @@ describe("weaponParamsSearchParams", () => { }); }); - it("accepts legacy comma-joined hidden ids", () => { + test("accepts legacy comma-joined hidden ids", () => { expect( SearchParams.decodeParam(weaponParamsSearchParams.shape.hidden, [ "10,20,30", @@ -24,7 +24,7 @@ describe("weaponParamsSearchParams", () => { ).toEqual([10, 20, 30]); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(weaponParamsSearchParams, "tab", [["bogus"]]); assertDecodesToDefault(weaponParamsSearchParams, "hidden", [ ["abc"], diff --git a/app/features/plus-suggestions/plus-suggestions-search-params.test.ts b/app/features/plus-suggestions/plus-suggestions-search-params.test.ts index 73e6ec246..48bc8bbdc 100644 --- a/app/features/plus-suggestions/plus-suggestions-search-params.test.ts +++ b/app/features/plus-suggestions/plus-suggestions-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { plusSuggestionsSearchParams } from "./plus-suggestions-search-params"; describe("plusSuggestionsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(plusSuggestionsSearchParams, { tier: ["1", "2", "3"], alert: [false, true], @@ -14,7 +14,7 @@ describe("plusSuggestionsSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(plusSuggestionsSearchParams, "tier", [ ["0"], ["4"], diff --git a/app/features/scanner-ingest/ScannerIngestRepository.server.test.ts b/app/features/scanner-ingest/ScannerIngestRepository.server.test.ts index aedafd9d1..921f6f108 100644 --- a/app/features/scanner-ingest/ScannerIngestRepository.server.test.ts +++ b/app/features/scanner-ingest/ScannerIngestRepository.server.test.ts @@ -3,18 +3,13 @@ import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; -import type { - ScannerMatch, - ScannerMatchPlayer, -} from "~/features/scanner/core/scanner-match"; +import type { ScannerMatch } from "~/features/scanner/core/scanner-match"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; -import type { MainWeaponId } from "~/modules/in-game-lists/types"; import * as Matches from "./core/Matches"; import type { IngestableGame } from "./core/Scoreboards"; +import { NAMES, scannerMatch, WEAPONS } from "./core/tests/fixtures"; import * as ScannerIngestRepository from "./ScannerIngestRepository.server"; -const NAMES = ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"]; -const WEAPONS: MainWeaponId[] = [10, 20, 30, 40, 50, 60, 70, 80]; const PLAYED_AT = Date.UTC(2026, 7, 1, 18, 0, 0); /** enough teams for the bracket winner to play more than one match */ const TOURNAMENT_TEAM_COUNT = 4; @@ -328,38 +323,9 @@ describe("gamesInTournamentMatch", () => { }); }); -function player(name: string, weaponId: MainWeaponId): ScannerMatchPlayer { - return { - name, - weaponId, - paint: 1000, - ka: 10, - d: 5, - s: 2, - }; -} - +/** The default roster's match, stamped with the fixed PLAYED_AT this suite asserts on. */ function testMatch(partial: Partial = {}): ScannerMatch { - return { - startsAt: 100, - endsAt: 400, - playedAt: PLAYED_AT, - lobby: "PRIVATE", - mode: "SZ", - stage: 0, - matchScores: [100, 52], - replayCode: null, - cast: false, - objective: null, - playerStatus: null, - teams: [ - { players: NAMES.slice(0, 4).map((n, i) => player(n, WEAPONS[i]!)) }, - { players: NAMES.slice(4).map((n, i) => player(n, WEAPONS[4 + i]!)) }, - ], - winner: 0, - pov: null, - ...partial, - }; + return scannerMatch({ playedAt: PLAYED_AT, ...partial }); } async function setupSendouqMatch(options: { isConcluded?: boolean } = {}) { diff --git a/app/features/scanner-ingest/core/Matches.test.ts b/app/features/scanner-ingest/core/Matches.test.ts index 6dc2380d0..341f852af 100644 --- a/app/features/scanner-ingest/core/Matches.test.ts +++ b/app/features/scanner-ingest/core/Matches.test.ts @@ -1,69 +1,18 @@ -import { describe, expect, it } from "vitest"; -import type { - ScannerMatch, - ScannerMatchPlayer, -} from "~/features/scanner/core/scanner-match"; +import { describe, expect, test } from "vitest"; +import type { ScannerMatch } from "~/features/scanner/core/scanner-match"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import * as Matches from "./Matches"; - -const NAMES = ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"]; -const WEAPONS: MainWeaponId[] = [10, 20, 30, 40, 50, 60, 70, 80]; - -function player( - name: string | null, - weaponId: MainWeaponId | null, - partial: Partial = {}, -): ScannerMatchPlayer { - return { - name, - weaponId, - paint: null, - ka: null, - d: null, - s: null, - ...partial, - }; -} - -function testMatch(partial: Partial = {}): ScannerMatch { - return { - startsAt: 100, - endsAt: 400, - playedAt: null, - lobby: "PRIVATE", - mode: "SZ", - stage: 0, - matchScores: [100, 52], - replayCode: null, - cast: false, - objective: null, - playerStatus: null, - teams: [ - { players: NAMES.slice(0, 4).map((n, i) => player(n, WEAPONS[i]!)) }, - { players: NAMES.slice(4).map((n, i) => player(n, WEAPONS[4 + i]!)) }, - ], - winner: 0, - pov: null, - ...partial, - }; -} - -/** The same rosters seen from the other side (e.g. a minimap alpha/bravo view). */ -function sideSwapped(match: ScannerMatch): ScannerMatch { - return { - ...match, - teams: [match.teams[1], match.teams[0]], - winner: match.winner === null ? null : match.winner === 0 ? 1 : 0, - matchScores: - match.matchScores === null - ? null - : [match.matchScores[1], match.matchScores[0]], - }; -} +import { + NAMES, + scannerMatch, + scannerMatchPlayer, + sideSwapped, + WEAPONS, +} from "./tests/fixtures"; describe("canonicalMatch", () => { - it("serializes identically regardless of input key order", () => { - const match = testMatch({ + test("serializes identically regardless of input key order", () => { + const match = scannerMatch({ objective: { mode: "SZ", samples: [ @@ -102,16 +51,16 @@ describe("canonicalMatch", () => { }); describe("isSameMatch", () => { - it("recognizes an identical match", () => { - expect(Matches.isSameMatch(testMatch(), testMatch())).toBe(true); + test("recognizes an identical match", () => { + expect(Matches.isSameMatch(scannerMatch(), scannerMatch())).toBe(true); }); - it("matching replay codes are a strong key", () => { - const a = testMatch({ + test("matching replay codes are a strong key", () => { + const a = scannerMatch({ replayCode: "RABC-DEFG-HIJK-LMNO", - teams: testMatch().teams, + teams: scannerMatch().teams, }); - const b = testMatch({ + const b = scannerMatch({ replayCode: "RABC-DEFG-HIJK-LMNO", matchScores: null, teams: [{ players: [] }, { players: [] }], @@ -120,21 +69,21 @@ describe("isSameMatch", () => { expect(Matches.isSameMatch(a, b)).toBe(true); }); - it("tolerates OCR jitter in the replay code", () => { - const a = testMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); - const b = testMatch({ replayCode: "RA8C-DEFG-HIJK-LMN0" }); + test("tolerates OCR jitter in the replay code", () => { + const a = scannerMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); + const b = scannerMatch({ replayCode: "RA8C-DEFG-HIJK-LMN0" }); expect(Matches.isSameMatch(a, b)).toBe(true); }); - it("clearly different replay codes contradict identity", () => { - const a = testMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); - const b = testMatch({ replayCode: "RZYX-WVUT-SRQP-ONML" }); + test("clearly different replay codes contradict identity", () => { + const a = scannerMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); + const b = scannerMatch({ replayCode: "RZYX-WVUT-SRQP-ONML" }); expect(Matches.isSameMatch(a, b)).toBe(false); }); - it("close play times identify a match", () => { - const a = testMatch({ playedAt: 1_700_000_000_000 }); - const b = testMatch({ + test("close play times identify a match", () => { + const a = scannerMatch({ playedAt: 1_700_000_000_000 }); + const b = scannerMatch({ playedAt: 1_700_000_000_000 + 5 * 60 * 1000, matchScores: null, teams: [{ players: [] }, { players: [] }], @@ -143,77 +92,88 @@ describe("isSameMatch", () => { expect(Matches.isSameMatch(a, b)).toBe(true); }); - it("far-apart play times contradict identity even with equal rosters", () => { - const a = testMatch({ playedAt: 1_700_000_000_000 }); - const b = testMatch({ playedAt: 1_700_000_000_000 + 60 * 60 * 1000 }); + test("far-apart play times contradict identity even with equal rosters", () => { + const a = scannerMatch({ playedAt: 1_700_000_000_000 }); + const b = scannerMatch({ playedAt: 1_700_000_000_000 + 60 * 60 * 1000 }); expect(Matches.isSameMatch(a, b)).toBe(false); }); - it("differing modes or stages contradict identity", () => { + test("differing modes or stages contradict identity", () => { expect( - Matches.isSameMatch(testMatch({ mode: "SZ" }), testMatch({ mode: "TC" })), + Matches.isSameMatch( + scannerMatch({ mode: "SZ" }), + scannerMatch({ mode: "TC" }), + ), ).toBe(false); expect( - Matches.isSameMatch(testMatch({ stage: 0 }), testMatch({ stage: 1 })), + Matches.isSameMatch( + scannerMatch({ stage: 0 }), + scannerMatch({ stage: 1 }), + ), ).toBe(false); }); - it("a null mode does not contradict a read one", () => { + test("a null mode does not contradict a read one", () => { expect( - Matches.isSameMatch(testMatch({ mode: null }), testMatch({ mode: "TC" })), + Matches.isSameMatch( + scannerMatch({ mode: null }), + scannerMatch({ mode: "TC" }), + ), ).toBe(true); }); - it("roster overlap identifies a match even side-swapped", () => { - expect(Matches.isSameMatch(testMatch(), sideSwapped(testMatch()))).toBe( - true, - ); + test("roster overlap identifies a match even side-swapped", () => { + expect( + Matches.isSameMatch(scannerMatch(), sideSwapped(scannerMatch())), + ).toBe(true); }); - it("roster overlap survives a couple of misread names", () => { - const b = testMatch(); - b.teams[0].players[0] = player("misread", WEAPONS[0]!); - b.teams[1].players[3] = player(null, WEAPONS[7]!); - expect(Matches.isSameMatch(testMatch(), b)).toBe(true); + test("roster overlap survives a couple of misread names", () => { + const b = scannerMatch(); + b.teams[0].players[0] = scannerMatchPlayer("misread", WEAPONS[0]!); + b.teams[1].players[3] = scannerMatchPlayer(null, WEAPONS[7]!); + expect(Matches.isSameMatch(scannerMatch(), b)).toBe(true); }); - it("weapons alone identify a match when names are unread (minimap vs scoreboard)", () => { - const minimap = testMatch({ + test("weapons alone identify a match when names are unread (minimap vs scoreboard)", () => { + const minimap = scannerMatch({ winner: null, lobby: null, matchScores: null, teams: [ - { players: WEAPONS.slice(0, 4).map((w) => player(null, w)) }, - { players: WEAPONS.slice(4).map((w) => player(null, w)) }, + { + players: WEAPONS.slice(0, 4).map((w) => scannerMatchPlayer(null, w)), + }, + { players: WEAPONS.slice(4).map((w) => scannerMatchPlayer(null, w)) }, ], }); - expect(Matches.isSameMatch(testMatch(), minimap)).toBe(true); + expect(Matches.isSameMatch(scannerMatch(), minimap)).toBe(true); }); - it("unrelated matches are not the same", () => { - const other = testMatch({ + test("unrelated matches are not the same", () => { + const other = scannerMatch({ matchScores: [88, 12], teams: [ { players: ["a", "b", "c", "d"].map((n, i) => - player(n, (100 + 10 * i) as MainWeaponId), + scannerMatchPlayer(n, (100 + 10 * i) as MainWeaponId), ), }, { players: ["e", "f", "g", "h"].map((n, i) => - player(n, (200 + 10 * i) as MainWeaponId), + scannerMatchPlayer(n, (200 + 10 * i) as MainWeaponId), ), }, ], }); - expect(Matches.isSameMatch(testMatch(), other)).toBe(false); + expect(Matches.isSameMatch(scannerMatch(), other)).toBe(false); }); }); describe("mergeMatches", () => { - it("fills stored nulls and reports no change when nothing was added", () => { - const existing = testMatch({ mode: null, playedAt: null }); - const incoming = testMatch({ mode: "SZ", playedAt: 1_700_000_000_000 }); + test("fills stored nulls and reports no change when nothing was added", () => { + const existing = scannerMatch({ mode: null, playedAt: null }); + const incoming = scannerMatch({ mode: "SZ", playedAt: 1_700_000_000_000 }); const first = Matches.mergeMatches(existing, incoming); expect(first.changed).toBe(true); @@ -224,20 +184,23 @@ describe("mergeMatches", () => { expect(second.changed).toBe(false); }); - it("stored values win on conflict", () => { - const existing = testMatch({ stage: 0 }); - const incoming = testMatch({ stage: null }); - incoming.teams[0].players[0] = player("other", 999 as MainWeaponId); + test("stored values win on conflict", () => { + const existing = scannerMatch({ stage: 0 }); + const incoming = scannerMatch({ stage: null }); + incoming.teams[0].players[0] = scannerMatchPlayer( + "other", + 999 as MainWeaponId, + ); const { merged } = Matches.mergeMatches(existing, incoming); expect(merged.stage).toBe(0); expect(merged.teams[0].players[0]!.name).toBe("w1"); }); - it("aligns a side-swapped incoming match before merging", () => { - const existing = testMatch({ winner: null, matchScores: null }); + test("aligns a side-swapped incoming match before merging", () => { + const existing = scannerMatch({ winner: null, matchScores: null }); const incoming = sideSwapped( - testMatch({ matchScores: [84, 71], playedAt: 1_700_000_000_000 }), + scannerMatch({ matchScores: [84, 71], playedAt: 1_700_000_000_000 }), ); const { merged } = Matches.mergeMatches(existing, incoming); @@ -248,15 +211,15 @@ describe("mergeMatches", () => { ); }); - it("merges player rows by name, keeping stored stats and adding missing ones", () => { - const existing = testMatch(); - existing.teams[1].players[1] = player("l2", null); - const incoming = testMatch(); + test("merges player rows by name, keeping stored stats and adding missing ones", () => { + const existing = scannerMatch(); + existing.teams[1].players[1] = scannerMatchPlayer("l2", null); + const incoming = scannerMatch(); incoming.teams[1].players = [ - player("l2", WEAPONS[5]!, { ka: 12, abilities: [["ISM"]] }), - player("l1", WEAPONS[4]!), - player("l3", WEAPONS[6]!), - player("l4", WEAPONS[7]!), + scannerMatchPlayer("l2", WEAPONS[5]!, { ka: 12, abilities: [["ISM"]] }), + scannerMatchPlayer("l1", WEAPONS[4]!), + scannerMatchPlayer("l3", WEAPONS[6]!), + scannerMatchPlayer("l4", WEAPONS[7]!), ]; const { merged } = Matches.mergeMatches(existing, incoming); @@ -266,14 +229,14 @@ describe("mergeMatches", () => { expect(l2.abilities).toEqual([["ISM"]]); }); - it("fills empty teams from the incoming match", () => { - const existing = testMatch({ + test("fills empty teams from the incoming match", () => { + const existing = scannerMatch({ winner: null, matchScores: null, teams: [{ players: [] }, { players: [] }], replayCode: "RABC-DEFG-HIJK-LMNO", }); - const incoming = testMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); + const incoming = scannerMatch({ replayCode: "RABC-DEFG-HIJK-LMNO" }); const { merged, changed } = Matches.mergeMatches(existing, incoming); expect(changed).toBe(true); @@ -303,25 +266,25 @@ describe("playerStatus", () => { ], }; - it("merges whole-series first-ingest-wins", () => { + test("merges whole-series first-ingest-wins", () => { const filled = Matches.mergeMatches( - testMatch(), - testMatch({ playerStatus: STATUS }), + scannerMatch(), + scannerMatch({ playerStatus: STATUS }), ); expect(filled.merged.playerStatus).toEqual(STATUS); expect(filled.changed).toBe(true); const kept = Matches.mergeMatches( - testMatch({ playerStatus: STATUS }), - testMatch({ playerStatus: { samples: [] } }), + scannerMatch({ playerStatus: STATUS }), + scannerMatch({ playerStatus: { samples: [] } }), ); expect(kept.merged.playerStatus).toEqual(STATUS); }); - it("side-aligning an incoming match swaps its status samples too", () => { - const incoming = sideSwapped(testMatch({ playerStatus: STATUS })); + test("side-aligning an incoming match swaps its status samples too", () => { + const incoming = sideSwapped(scannerMatch({ playerStatus: STATUS })); const { merged } = Matches.mergeMatches( - testMatch({ winner: null, matchScores: null, playerStatus: null }), + scannerMatch({ winner: null, matchScores: null, playerStatus: null }), incoming, ); expect(merged.playerStatus!.samples[0]!.dead).toEqual([ diff --git a/app/features/scanner-ingest/core/Scoreboards.test.ts b/app/features/scanner-ingest/core/Scoreboards.test.ts index 44dbe7ed8..3b74b81bf 100644 --- a/app/features/scanner-ingest/core/Scoreboards.test.ts +++ b/app/features/scanner-ingest/core/Scoreboards.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { ScannerMatch, ScannerMatchObjective, @@ -13,6 +13,7 @@ import type { StageId, } from "~/modules/in-game-lists/types"; import * as Scoreboards from "./Scoreboards"; +import { NAMES } from "./tests/fixtures"; const WINNER_TEAM_ID = 100; const LOSER_TEAM_ID = 200; @@ -54,7 +55,7 @@ function testMatch({ mode = "SZ", stage = 0, lobby = "PRIVATE", - names = ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"], + names = NAMES, weapons = [10, 10, 10, 10, 20, 20, 20, 20] as (MainWeaponId | null)[], abilities = {}, povIndex = null, @@ -185,7 +186,7 @@ function swapSides(match: ScannerMatch): ScannerMatch { } describe("matchedGames", () => { - it("matches a game's match and reports its index", () => { + test("matches a game's match and reports its index", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch()], games: [testGame()], @@ -196,7 +197,7 @@ describe("matchedGames", () => { expect(gameResultId(matched[0]!)).toBe(11); }); - it("skips matches without a known winner", () => { + test("skips matches without a known winner", () => { const matched = Scoreboards.matchedGames({ matches: [{ ...testMatch(), winner: null }], games: [testGame()], @@ -205,7 +206,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(0); }); - it("skips matches whose teams were not fully seen", () => { + test("skips matches whose teams were not fully seen", () => { const partial = testMatch(); partial.teams[1].players.pop(); const matched = Scoreboards.matchedGames({ @@ -216,7 +217,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(0); }); - it("skips a game whose linked scoreboard has different players", () => { + test("skips a game whose linked scoreboard has different players", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch()], games: [ @@ -231,7 +232,7 @@ describe("matchedGames", () => { expect(matched.map(gameResultId)).toEqual([12]); }); - it("matches a re-detection of a linked scoreboard to the same game despite misread names", () => { + test("matches a re-detection of a linked scoreboard to the same game despite misread names", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch()], games: [ @@ -246,7 +247,7 @@ describe("matchedGames", () => { expect(matched.map(gameResultId)).toEqual([11]); }); - it("does not count unreadable names towards linked scoreboard re-detection", () => { + test("does not count unreadable names towards linked scoreboard re-detection", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch({ names: ["", "", "", "", "l1", "l2", "l3", "l4"] })], games: [ @@ -261,7 +262,7 @@ describe("matchedGames", () => { expect(matched.map(gameResultId)).toEqual([12]); }); - it("matches matches to games by mode and stage", () => { + test("matches matches to games by mode and stage", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch({ mode: "RM", stage: 1, t: 60 })], games: [ @@ -273,7 +274,7 @@ describe("matchedGames", () => { expect(matched.map((m) => m.game.mapIndex)).toEqual([1]); }); - it("assigns two games on the same mode and stage in chronological order", () => { + test("assigns two games on the same mode and stage in chronological order", () => { const matched = Scoreboards.matchedGames({ matches: [ testMatch({ @@ -297,7 +298,7 @@ describe("matchedGames", () => { ]); }); - it("skips duplicate detections of the same game", () => { + test("skips duplicate detections of the same game", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch({ t: 60 }), testMatch({ t: 65 })], games: [ @@ -310,7 +311,7 @@ describe("matchedGames", () => { expect(tournamentMatchIdOf(matched[0]!)).toBe(1); }); - it("skips a duplicate detection despite a couple of OCR-misread names", () => { + test("skips a duplicate detection despite a couple of OCR-misread names", () => { const matched = Scoreboards.matchedGames({ matches: [ testMatch({ t: 60 }), @@ -329,7 +330,7 @@ describe("matchedGames", () => { expect(tournamentMatchIdOf(matched[0]!)).toBe(1); }); - it("skips matches from other lobbies", () => { + test("skips matches from other lobbies", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch({ lobby: "X" })], games: [testGame()], @@ -338,7 +339,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(0); }); - it("skips matches with unreadable mode or stage", () => { + test("skips matches with unreadable mode or stage", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch({ mode: null }), testMatch({ stage: null })], games: [testGame()], @@ -347,7 +348,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(0); }); - it("skips matches that have no matching game left", () => { + test("skips matches that have no matching game left", () => { const matched = Scoreboards.matchedGames({ matches: [ testMatch({ t: 60 }), @@ -362,7 +363,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(1); }); - it("skips a game whose known rosters contradict the match sides", () => { + test("skips a game whose known rosters contradict the match sides", () => { const matched = Scoreboards.matchedGames({ matches: [testMatch()], games: [ @@ -385,7 +386,7 @@ describe("matchedGames", () => { expect(matched.map(tournamentMatchIdOf)).toEqual([2]); }); - it("matches known in-game names ignoring discriminator, case and unicode width", () => { + test("matches known in-game names ignoring discriminator, case and unicode width", () => { const matched = Scoreboards.matchedGames({ matches: [ testMatch({ @@ -405,7 +406,7 @@ describe("matchedGames", () => { expect(matched).toHaveLength(1); }); - it("does not assign a game played before the previously assigned one", () => { + test("does not assign a game played before the previously assigned one", () => { const matched = Scoreboards.matchedGames({ matches: [ testMatch({ t: 60, mode: "RM", stage: 1 }), @@ -442,40 +443,38 @@ describe("deriveScoreboardData", () => { }); } - it("projects a match winner-first into scoreboard data", () => { + test("projects a match winner-first into scoreboard data", () => { const data = derive([{ data: testMatch(), povUserId: null }]); expect(data).toEqual({ scores: [100, 52], - players: ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"].map( - (name, i) => ({ - name, - tournamentTeamId: i < 4 ? WINNER_TEAM_ID : LOSER_TEAM_ID, - weaponSplId: i < 4 ? 10 : 20, - ka: 10, - d: 5, - s: 2, - paint: 1000, - }), - ), + players: NAMES.map((name, i) => ({ + name, + tournamentTeamId: i < 4 ? WINNER_TEAM_ID : LOSER_TEAM_ID, + weaponSplId: i < 4 ? 10 : 20, + ka: 10, + d: 5, + s: 2, + paint: 1000, + })), }); }); - it("a winner-1 match derives identically to its winner-0 mirror", () => { + test("a winner-1 match derives identically to its winner-0 mirror", () => { const straight = derive([{ data: testMatch(), povUserId: null }]); const swapped = derive([{ data: swapSides(testMatch()), povUserId: null }]); expect(swapped).toEqual(straight); }); - it("returns null for a match that cannot form a scoreboard", () => { + test("returns null for a match that cannot form a scoreboard", () => { expect(derive([])).toBe(null); expect( derive([{ data: { ...testMatch(), winner: null }, povUserId: null }]), ).toBe(null); }); - it("rebases counter samples to the game's first read", () => { + test("rebases counter samples to the game's first read", () => { const data = derive([ { data: testMatch({ objective: testObjective() }), povUserId: null }, ]); @@ -501,7 +500,7 @@ describe("deriveScoreboardData", () => { }); }); - it("derives counter samples winner-first", () => { + test("derives counter samples winner-first", () => { const straight = derive([ { data: testMatch({ objective: testObjective() }), povUserId: null }, ]); @@ -515,7 +514,7 @@ describe("deriveScoreboardData", () => { expect(swapped!.objective).toEqual(straight!.objective); }); - it("rebases status samples onto the same origin as the counter's", () => { + test("rebases status samples onto the same origin as the counter's", () => { const data = derive([ { data: testMatch({ @@ -531,7 +530,7 @@ describe("deriveScoreboardData", () => { expect(data!.objective!.samples.map((sample) => sample.t)).toEqual([5, 35]); }); - it("derives status samples winner-first", () => { + test("derives status samples winner-first", () => { const straight = derive([ { data: testMatch({ playerStatus: testPlayerStatus() }), @@ -552,13 +551,13 @@ describe("deriveScoreboardData", () => { expect(swapped!.playerStatus).toEqual(straight!.playerStatus); }); - it("leaves out the objective of a match with no counter reads", () => { + test("leaves out the objective of a match with no counter reads", () => { const data = derive([{ data: testMatch(), povUserId: null }]); expect(data!.objective).toBeUndefined(); }); - it("carries ingested player abilities through", () => { + test("carries ingested player abilities through", () => { const build: AbilityWithUnknown[][] = [ ["ISM", "ISS", "ISS", "ISS"], ["QR", "QSJ", "QSJ", "QSJ"], @@ -572,7 +571,7 @@ describe("deriveScoreboardData", () => { expect(data!.players[0]!.abilities).toBeUndefined(); }); - it("keeps players with unread weapon or empty name", () => { + test("keeps players with unread weapon or empty name", () => { const data = derive([ { data: testMatch({ @@ -590,7 +589,7 @@ describe("deriveScoreboardData", () => { expect(data!.players[2]!.ka).toBe(10); }); - it("keeps players whose name appears twice on the same side", () => { + test("keeps players whose name appears twice on the same side", () => { const data = derive([ { data: testMatch({ @@ -603,14 +602,14 @@ describe("deriveScoreboardData", () => { expect(data!.players.filter((p) => p.name === "dupe")).toHaveLength(3); }); - it("attributes the POV seat's row to the POV user", () => { + test("attributes the POV seat's row to the POV user", () => { const data = derive([{ data: testMatch({ povIndex: 2 }), povUserId: 42 }]); expect(data!.players[2]!.userId).toBe(42); expect(data!.players.filter((p) => p.userId !== undefined)).toHaveLength(1); }); - it("attributes a losing-side POV of a winner-1 match to the right row", () => { + test("attributes a losing-side POV of a winner-1 match to the right row", () => { const data = derive([ { data: swapSides(testMatch({ povIndex: 6 })), povUserId: 42 }, ]); @@ -618,7 +617,7 @@ describe("deriveScoreboardData", () => { expect(data!.players[6]!.userId).toBe(42); }); - it("attributes each linked POV onto the merged scoreboard", () => { + test("attributes each linked POV onto the merged scoreboard", () => { const data = derive([ { data: testMatch({ povIndex: 0 }), povUserId: 42 }, { data: swapSides(testMatch({ povIndex: 5 })), povUserId: 43 }, @@ -628,7 +627,7 @@ describe("deriveScoreboardData", () => { expect(data!.players[5]!.userId).toBe(43); }); - it("does not attribute the same row twice", () => { + test("does not attribute the same row twice", () => { const data = derive([ { data: testMatch({ povIndex: 2 }), povUserId: 42 }, { data: testMatch({ povIndex: 2 }), povUserId: 43 }, @@ -637,7 +636,7 @@ describe("deriveScoreboardData", () => { expect(data!.players[2]!.userId).toBe(42); }); - it("does not attribute a POV whose read name contradicts its seat's merged row", () => { + test("does not attribute a POV whose read name contradicts its seat's merged row", () => { const data = derive([ { data: testMatch(), povUserId: null }, { @@ -652,7 +651,7 @@ describe("deriveScoreboardData", () => { expect(data!.players.some((p) => p.userId === 42)).toBe(false); }); - it("merges a later partial's fields under the first link's values", () => { + test("merges a later partial's fields under the first link's values", () => { const withoutScores: ScannerMatch = { ...testMatch(), matchScores: null, @@ -667,7 +666,7 @@ describe("deriveScoreboardData", () => { }); describe("winnerFirstPlayerNames", () => { - it("returns names winner-first with unread names empty", () => { + test("returns names winner-first with unread names empty", () => { const names = Scoreboards.winnerFirstPlayerNames( swapSides( testMatch({ names: ["w1", "", "w3", "w4", "l1", "l2", "l3", "l4"] }), @@ -677,7 +676,7 @@ describe("winnerFirstPlayerNames", () => { expect(names).toEqual(["w1", "", "w3", "w4", "l1", "l2", "l3", "l4"]); }); - it("returns null for a match without a linkable scoreboard", () => { + test("returns null for a match without a linkable scoreboard", () => { expect( Scoreboards.winnerFirstPlayerNames({ ...testMatch(), winner: null }), ).toBe(null); @@ -731,7 +730,7 @@ describe("resolveContext", () => { testMatch({ t: 600, mode: "TC", stage: 1 }), ]; - it("resolves the tournament whose games match the seen sequence", () => { + test("resolves the tournament whose games match the seen sequence", () => { const context = Scoreboards.resolveContext({ matches: seenSequence, games: [ @@ -749,7 +748,7 @@ describe("resolveContext", () => { expect(context).toEqual({ type: "tournament", tournamentId: 1 }); }); - it("resolves a SendouQ match over a tournament when its games match better", () => { + test("resolves a SendouQ match over a tournament when its games match better", () => { const context = Scoreboards.resolveContext({ matches: seenSequence, games: [ @@ -767,7 +766,7 @@ describe("resolveContext", () => { expect(context).toEqual({ type: "sendouq", groupMatchId: 7 }); }); - it("does not resolve from a single matching match", () => { + test("does not resolve from a single matching match", () => { const context = Scoreboards.resolveContext({ matches: [seenSequence[0]!], games: tournamentGames(1, [ @@ -779,7 +778,7 @@ describe("resolveContext", () => { expect(context).toBe(null); }); - it("lets roster sides break a map-sequence tie", () => { + test("lets roster sides break a map-sequence tie", () => { const sharedMaplist: [ModeShort, number][] = [ ["SZ", 0], ["TC", 1], @@ -802,7 +801,7 @@ describe("resolveContext", () => { expect(context).toEqual({ type: "tournament", tournamentId: 1 }); }); - it("skips unreadable matches but resolves from the rest", () => { + test("skips unreadable matches but resolves from the rest", () => { const context = Scoreboards.resolveContext({ matches: [ seenSequence[0]!, diff --git a/app/features/scanner-ingest/core/VodMatches.test.ts b/app/features/scanner-ingest/core/VodMatches.test.ts index 9ec1e2279..160ff295f 100644 --- a/app/features/scanner-ingest/core/VodMatches.test.ts +++ b/app/features/scanner-ingest/core/VodMatches.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { vodsNewSearchParams } from "~/features/vods/vods-search-params"; import { type IngestVodMatchInput, @@ -24,7 +24,7 @@ function testMatch( } describe("prefillVodMatches", () => { - it("maps validated match rows into the form's prefill shape", () => { + test("maps validated match rows into the form's prefill shape", () => { const prefilled = prefillVodMatches([testMatch({ povWeapon: 20 })]); expect(prefilled).toHaveLength(1); @@ -37,7 +37,7 @@ describe("prefillVodMatches", () => { }); }); - it("keeps unread (null) fields for the user to fill in the form", () => { + test("keeps unread (null) fields for the user to fill in the form", () => { const prefilled = prefillVodMatches([ testMatch({ mode: null, @@ -56,14 +56,14 @@ describe("prefillVodMatches", () => { }); }); - it("rejects rows that are not sendou ids", () => { + test("rejects rows that are not sendou ids", () => { const parsed = ingestVodPrefillSchema.safeParse({ matches: [{ ...testMatch(), stage: "Scorch Gorge" }], }); expect(parsed.success).toBe(false); }); - it("accepts the `ingest` search param the scanner VoD tab sends", () => { + test("accepts the `ingest` search param the scanner VoD tab sends", () => { // what the scanner VoD tab's "Add VoD" button puts in the URL // (~/features/scanner/components/sendou-upload.ts): a { type?, matches } // payload in the compressed `ingest` param diff --git a/app/features/scanner-ingest/core/tests/fixtures.ts b/app/features/scanner-ingest/core/tests/fixtures.ts new file mode 100644 index 000000000..cdcb6132d --- /dev/null +++ b/app/features/scanner-ingest/core/tests/fixtures.ts @@ -0,0 +1,75 @@ +import type { + ScannerMatch, + ScannerMatchPlayer, +} from "~/features/scanner/core/scanner-match"; +import type { MainWeaponId } from "~/modules/in-game-lists/types"; + +/** In-game names of the default roster, winners first. */ +export const NAMES = ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"]; + +/** One distinct weapon per player of the default roster, in NAMES order. */ +export const WEAPONS: MainWeaponId[] = [10, 20, 30, 40, 50, 60, 70, 80]; + +/** A scoreboard row with every stat unread, so tests only state what they rely on. */ +export function scannerMatchPlayer( + name: string | null, + weaponId: MainWeaponId | null, + partial: Partial = {}, +): ScannerMatchPlayer { + return { + name, + weaponId, + paint: null, + ka: null, + d: null, + s: null, + ...partial, + }; +} + +/** A fully read Splat Zones match on stage 0 where the NAMES/WEAPONS roster's first team wins 100-52. */ +export function scannerMatch( + partial: Partial = {}, +): ScannerMatch { + return { + startsAt: 100, + endsAt: 400, + playedAt: null, + lobby: "PRIVATE", + mode: "SZ", + stage: 0, + matchScores: [100, 52], + replayCode: null, + cast: false, + objective: null, + playerStatus: null, + teams: [ + { + players: NAMES.slice(0, 4).map((name, i) => + scannerMatchPlayer(name, WEAPONS[i]!), + ), + }, + { + players: NAMES.slice(4).map((name, i) => + scannerMatchPlayer(name, WEAPONS[4 + i]!), + ), + }, + ], + winner: 0, + pov: null, + ...partial, + }; +} + +/** The same rosters seen from the other side (e.g. a minimap alpha/bravo view). */ +export function sideSwapped(match: ScannerMatch): ScannerMatch { + return { + ...match, + teams: [match.teams[1], match.teams[0]], + winner: match.winner === null ? null : match.winner === 0 ? 1 : 0, + matchScores: + match.matchScores === null + ? null + : [match.matchScores[1], match.matchScores[0]], + }; +} diff --git a/app/features/scanner/README.md b/app/features/scanner/README.md index 4724e7fbd..a51adfa82 100644 --- a/app/features/scanner/README.md +++ b/app/features/scanner/README.md @@ -10,13 +10,16 @@ detected game per object, every field nullable — which feed `/ingest` emberz repo; see `MIGRATION.md` there. Deliberate convention exceptions (dev tool, ported wholesale): the UI is -English-only (no i18next) and `tests/node-test-compat.ts` uses a default -export to stay a `node:test` drop-in. +English-only (no i18next), `tests/node-test-compat.ts` uses a default export +to stay a `node:test` drop-in, and the suites assert with `node:assert/strict` +rather than the repo-wide `expect`. Keep whichever file you touch on the +idiom it already uses — a half-migration would leave three idioms behind. ## Commands ```sh pnpm test:scanner # golden-file suite over tests/fixtures/ (Vitest, Node) +pnpm test:unit:browser # includes tests/logic/ — the fixture-free half, see below pnpm scanner:report # accuracy table + name character error rate across fixtures pnpm scanner:fixtures [name-substring] # run detectors over matching fixtures, verbose pnpm scanner:replay # replay ffmpeg-extracted frames through the scheduler+detectors @@ -219,6 +222,17 @@ condensed Kurokane and Rowdy (`death-weapon-ja`). Regeneration order: then the atlas rebuild; planner atlas via `scanner:build-planner-signatures` (reads the assets repo's `assets/planner-maps/`, MINI variant). +## Tests + +`tests/*.test.ts` are the golden-file suites: they read frames from +`tests/fixtures/` and need game icons from a sibling `sendou-ink/assets` +checkout, so they run in their own Vitest project (`vitest.scanner.config.ts`) +and stay out of CI. + +`tests/logic/*.test.ts` are pure logic over synthetic events — no images, no +assets checkout — so they belong to the `unit` project and do run in CI. Put +new tests there whenever they can be written without a frame. + ## Fixtures A test case is a directory `tests/fixtures///` with diff --git a/app/features/scanner/scanner-search-params.test.ts b/app/features/scanner/scanner-search-params.test.ts index 77b9f5456..a9037138e 100644 --- a/app/features/scanner/scanner-search-params.test.ts +++ b/app/features/scanner/scanner-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { scannerSearchParams } from "./scanner-search-params"; describe("scannerSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(scannerSearchParams, { tab: ["live", "screenshot", "vod"], inspect: ["1723456789012-abc123", null], @@ -14,7 +14,7 @@ describe("scannerSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(scannerSearchParams, "tab", [["garbage"], ["LIVE"]]); }); }); diff --git a/app/features/scanner/tests/dedupe-events.test.ts b/app/features/scanner/tests/logic/dedupe-events.test.ts similarity index 88% rename from app/features/scanner/tests/dedupe-events.test.ts rename to app/features/scanner/tests/logic/dedupe-events.test.ts index 5d97e84bb..1bc72fddc 100644 --- a/app/features/scanner/tests/dedupe-events.test.ts +++ b/app/features/scanner/tests/logic/dedupe-events.test.ts @@ -4,16 +4,16 @@ import type { MainWeaponId, StageId, } from "~/modules/in-game-lists/types"; -import { withoutRepeatEvents } from "../components/dedupe-events"; -import type { DeathData } from "../core/detectors/death/index"; +import { withoutRepeatEvents } from "../../components/dedupe-events"; +import type { DeathData } from "../../core/detectors/death/index"; import type { MinimapData, MinimapEnemy, MinimapTeammate, -} from "../core/detectors/minimap/index"; -import { SPECTATOR_SLOTS } from "../core/detectors/minimap/rois"; -import type { DetectedEvent } from "../core/detectors/types"; -import test from "./node-test-compat"; +} from "../../core/detectors/minimap/index"; +import { SPECTATOR_SLOTS } from "../../core/detectors/minimap/rois"; +import type { DetectedEvent } from "../../core/detectors/types"; +import test from "../node-test-compat"; const ALPHA: MainWeaponId[] = [40, 1001, 2010, 3030]; const BRAVO: MainWeaponId[] = [50, 210, 4010, 8000]; diff --git a/app/features/scanner/tests/match-builder.test.ts b/app/features/scanner/tests/logic/match-builder.test.ts similarity index 97% rename from app/features/scanner/tests/match-builder.test.ts rename to app/features/scanner/tests/logic/match-builder.test.ts index 616a0b316..cc6128a77 100644 --- a/app/features/scanner/tests/match-builder.test.ts +++ b/app/features/scanner/tests/logic/match-builder.test.ts @@ -5,27 +5,27 @@ import type { ModeShort, StageId, } from "~/modules/in-game-lists/types"; -import type { DeathData } from "../core/detectors/death/index"; +import type { DeathData } from "../../core/detectors/death/index"; import type { MinimapData, MinimapEnemy, MinimapTeammate, -} from "../core/detectors/minimap/index"; -import { SPECTATOR_SLOTS } from "../core/detectors/minimap/rois"; -import type { ObjectiveData } from "../core/detectors/objective/index"; -import type { PlayerStatusData } from "../core/detectors/objective/player-status"; -import type { StripWeaponsData } from "../core/detectors/objective/strip-weapons"; -import type { ScoreboardData } from "../core/detectors/scoreboard/index"; -import type { ScoreboardBattleLogData } from "../core/detectors/scoreboard-battle-log/index"; -import type { ScoreboardBattleLogReplayData } from "../core/detectors/scoreboard-battle-log-replay/index"; -import type { DetectedEvent } from "../core/detectors/types"; +} from "../../core/detectors/minimap/index"; +import { SPECTATOR_SLOTS } from "../../core/detectors/minimap/rois"; +import type { ObjectiveData } from "../../core/detectors/objective/index"; +import type { PlayerStatusData } from "../../core/detectors/objective/player-status"; +import type { StripWeaponsData } from "../../core/detectors/objective/strip-weapons"; +import type { ScoreboardData } from "../../core/detectors/scoreboard/index"; +import type { ScoreboardBattleLogData } from "../../core/detectors/scoreboard-battle-log/index"; +import type { ScoreboardBattleLogReplayData } from "../../core/detectors/scoreboard-battle-log-replay/index"; +import type { DetectedEvent } from "../../core/detectors/types"; import { buildScannerMatches, ingestSkipReasons, invalidObjectiveEvents, -} from "../core/match-builder"; -import type { ScannerLobby } from "../scanner-types"; -import test from "./node-test-compat"; +} from "../../core/match-builder"; +import type { ScannerLobby } from "../../scanner-types"; +import test from "../node-test-compat"; const NAMES = ["w1", "w2", "w3", "w4", "l1", "l2", "l3", "l4"]; const ALPHA: MainWeaponId[] = [40, 1001, 2010, 3030]; diff --git a/app/features/scanner/tests/match-sets.test.ts b/app/features/scanner/tests/logic/match-sets.test.ts similarity index 95% rename from app/features/scanner/tests/match-sets.test.ts rename to app/features/scanner/tests/logic/match-sets.test.ts index 6b7c5c945..bdf1e1ed6 100644 --- a/app/features/scanner/tests/match-sets.test.ts +++ b/app/features/scanner/tests/logic/match-sets.test.ts @@ -1,7 +1,10 @@ import assert from "node:assert/strict"; -import { assignMatchSets } from "../core/match-sets"; -import type { ScannerMatch, ScannerMatchPlayer } from "../core/scanner-match"; -import test from "./node-test-compat"; +import { assignMatchSets } from "../../core/match-sets"; +import type { + ScannerMatch, + ScannerMatchPlayer, +} from "../../core/scanner-match"; +import test from "../node-test-compat"; const TEAM_A = ["Sendou", "Kiver", "Brian", "Zed"]; const TEAM_B = ["Gos", "Noah", "Alice", "Bob"]; diff --git a/app/features/scanner/tests/scheduler.test.ts b/app/features/scanner/tests/logic/scheduler.test.ts similarity index 99% rename from app/features/scanner/tests/scheduler.test.ts rename to app/features/scanner/tests/logic/scheduler.test.ts index ec9044738..9c0cb2674 100644 --- a/app/features/scanner/tests/scheduler.test.ts +++ b/app/features/scanner/tests/logic/scheduler.test.ts @@ -9,8 +9,8 @@ import assert from "node:assert/strict"; import { DetectorScheduler, type SchedulingInfo, -} from "../core/detectors/scheduler"; -import test from "./node-test-compat"; +} from "../../core/detectors/scheduler"; +import test from "../node-test-compat"; const OPTS = { refineIntervalS: 0.1, diff --git a/app/features/scrims/actions/scrims.request.server.test.ts b/app/features/scrims/actions/scrims.request.server.test.ts index 45a37d849..64857887c 100644 --- a/app/features/scrims/actions/scrims.request.server.test.ts +++ b/app/features/scrims/actions/scrims.request.server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import * as ScrimPostFactory from "~/db/seed/factories/ScrimPostFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { wrappedAction } from "~/utils/Test"; @@ -20,7 +20,7 @@ const requestsForPost = async (scrimPostId: number) => (await ScrimPostRepository.findById(scrimPostId))?.requests; describe("Scrim requests: pickup roster validation", () => { - it("does not add a user who opted out of non-friend pickups (parity with post creation)", async () => { + test("does not add a user who opted out of non-friend pickups (parity with post creation)", async () => { // attacker who sends the request (the built-in "regular" test user) await UserFactory.createRegular(); diff --git a/app/features/scrims/core/Scrim.test.ts b/app/features/scrims/core/Scrim.test.ts index ac2b21517..4f81d4622 100644 --- a/app/features/scrims/core/Scrim.test.ts +++ b/app/features/scrims/core/Scrim.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; import { SCRIM_TRACKING_AUTO_LOCK_HOURS } from "../scrims-constants"; import type { ScrimFilters, ScrimPost } from "../scrims-types"; @@ -28,7 +28,7 @@ function createPost(users: MockUser[], requests: MockRequest[]): ScrimPost { } describe("participantIdsListFromAccepted", () => { - it("returns only post users if no accepted request", () => { + test("returns only post users if no accepted request", () => { const post = createPost( [{ id: 10 }, { id: 20 }], [ @@ -43,7 +43,7 @@ describe("participantIdsListFromAccepted", () => { expect(result).toEqual([10, 20]); }); - it("returns post users and accepted request users", () => { + test("returns post users and accepted request users", () => { const post = createPost( [{ id: 10 }, { id: 20 }], [ @@ -62,7 +62,7 @@ describe("participantIdsListFromAccepted", () => { expect(result).toEqual([10, 20, 40, 50]); }); - it("returns post users if accepted request has no users", () => { + test("returns post users if accepted request has no users", () => { const post = createPost( [{ id: 10 }], [ @@ -77,7 +77,7 @@ describe("participantIdsListFromAccepted", () => { expect(result).toEqual([10]); }); - it("returns empty array if no users and no accepted request", () => { + test("returns empty array if no users and no accepted request", () => { const post = createPost([], []); const result = participantIdsListFromAccepted(post); @@ -86,7 +86,7 @@ describe("participantIdsListFromAccepted", () => { }); describe("sideDisplayName", () => { - it("returns the team name when team is set", () => { + test("returns the team name when team is set", () => { const result = sideDisplayName({ team: { name: "Team Olive" }, users: [{ username: "sendou", isOwner: true }], @@ -94,7 +94,7 @@ describe("sideDisplayName", () => { expect(result).toBe("Team Olive"); }); - it("falls back to {owner}'s pickup when team is null", () => { + test("falls back to {owner}'s pickup when team is null", () => { const result = sideDisplayName({ team: null, users: [ @@ -139,7 +139,7 @@ describe("applyFilters", () => { } describe("with no filters", () => { - it("returns true when all filters are null", () => { + test("returns true when all filters are null", () => { const post = createPostForFilters(new Date("2025-01-15T14:00:00")); const filters: ScrimFilters = { divs: null, @@ -152,7 +152,7 @@ describe("applyFilters", () => { }); describe("division filters", () => { - it("returns true when post has no divs but filter has divs", () => { + test("returns true when post has no divs but filter has divs", () => { const post = createPostForFilters(new Date("2025-01-15T14:00:00")); const filters: ScrimFilters = { divs: { min: "5", max: "3" }, @@ -163,7 +163,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns true when only filter min is set and post max is at or above filter min", () => { + test("returns true when only filter min is set and post max is at or above filter min", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -178,7 +178,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when only filter min is set and post max is below filter min", () => { + test("returns false when only filter min is set and post max is below filter min", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -193,7 +193,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns true when only filter max is set and post min is at or below filter max", () => { + test("returns true when only filter max is set and post min is at or below filter max", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -208,7 +208,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when only filter max is set and post min is above filter max", () => { + test("returns false when only filter max is set and post min is above filter max", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -223,7 +223,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns true when post divs overlap with filter divs", () => { + test("returns true when post divs overlap with filter divs", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -238,7 +238,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns true when post divs exactly match filter divs", () => { + test("returns true when post divs exactly match filter divs", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -253,7 +253,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when post divs are too high for filter", () => { + test("returns false when post divs are too high for filter", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -268,7 +268,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns false when post divs are too low for filter", () => { + test("returns false when post divs are too low for filter", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -285,7 +285,7 @@ describe("applyFilters", () => { }); describe("weekday time filters", () => { - it("returns true when post time overlaps with weekday time filter", () => { + test("returns true when post time overlaps with weekday time filter", () => { const post = createPostForFilters(new Date("2025-01-15T14:00:00")); const filters: ScrimFilters = { divs: null, @@ -296,7 +296,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when post time is before weekday time filter", () => { + test("returns false when post time is before weekday time filter", () => { const post = createPostForFilters(new Date("2025-01-15T08:00:00")); const filters: ScrimFilters = { divs: null, @@ -307,7 +307,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns false when post time is after weekday time filter", () => { + test("returns false when post time is after weekday time filter", () => { const post = createPostForFilters(new Date("2025-01-15T18:00:00")); const filters: ScrimFilters = { divs: null, @@ -318,7 +318,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns true when post time range overlaps with weekday time filter", () => { + test("returns true when post time range overlaps with weekday time filter", () => { const post = createPostForFilters( new Date("2025-01-15T09:00:00"), new Date("2025-01-15T11:00:00"), @@ -332,7 +332,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when post time range does not overlap with weekday time filter", () => { + test("returns false when post time range does not overlap with weekday time filter", () => { const post = createPostForFilters( new Date("2025-01-15T06:00:00"), new Date("2025-01-15T08:00:00"), @@ -346,7 +346,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns true when post time range ends exactly at the filter start edge", () => { + test("returns true when post time range ends exactly at the filter start edge", () => { const post = createPostForFilters( new Date("2025-01-15T09:00:00"), new Date("2025-01-15T10:00:00"), @@ -360,7 +360,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns true when a post time range crossing midnight overlaps the filter", () => { + test("returns true when a post time range crossing midnight overlaps the filter", () => { const post = createPostForFilters( new Date("2025-01-15T23:00:00"), new Date("2025-01-16T01:00:00"), @@ -374,7 +374,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns true when a filter crossing midnight covers the post time", () => { + test("returns true when a filter crossing midnight covers the post time", () => { const post = createPostForFilters(new Date("2025-01-15T21:00:00")); const filters: ScrimFilters = { divs: null, @@ -387,7 +387,7 @@ describe("applyFilters", () => { }); describe("weekend time filters", () => { - it("returns true when post time overlaps with weekend time filter on Saturday", () => { + test("returns true when post time overlaps with weekend time filter on Saturday", () => { const post = createPostForFilters(new Date("2025-01-18T14:00:00")); const filters: ScrimFilters = { divs: null, @@ -398,7 +398,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns true when post time overlaps with weekend time filter on Sunday", () => { + test("returns true when post time overlaps with weekend time filter on Sunday", () => { const post = createPostForFilters(new Date("2025-01-19T14:00:00")); const filters: ScrimFilters = { divs: null, @@ -409,7 +409,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when post time is outside weekend time filter", () => { + test("returns false when post time is outside weekend time filter", () => { const post = createPostForFilters(new Date("2025-01-18T20:00:00")); const filters: ScrimFilters = { divs: null, @@ -420,7 +420,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("ignores weekday time filter on weekends", () => { + test("ignores weekday time filter on weekends", () => { const post = createPostForFilters(new Date("2025-01-18T20:00:00")); const filters: ScrimFilters = { divs: null, @@ -433,7 +433,7 @@ describe("applyFilters", () => { }); describe("combined filters", () => { - it("returns true when both div and time filters match", () => { + test("returns true when both div and time filters match", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -448,7 +448,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(true); }); - it("returns false when div filter matches but time filter does not", () => { + test("returns false when div filter matches but time filter does not", () => { const post = createPostForFilters( new Date("2025-01-15T18:00:00"), undefined, @@ -463,7 +463,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns false when time filter matches but div filter does not", () => { + test("returns false when time filter matches but div filter does not", () => { const post = createPostForFilters( new Date("2025-01-15T14:00:00"), undefined, @@ -478,7 +478,7 @@ describe("applyFilters", () => { expect(applyFilters(post, filters)).toBe(false); }); - it("returns false when neither filter matches", () => { + test("returns false when neither filter matches", () => { const post = createPostForFilters( new Date("2025-01-15T18:00:00"), undefined, @@ -496,7 +496,7 @@ describe("applyFilters", () => { }); describe("sideOfUser", () => { - it("returns ALPHA for users in the post's users list", () => { + test("returns ALPHA for users in the post's users list", () => { const post = createPost( [{ id: 1 }], [{ isAccepted: true, users: [{ id: 2 }] }], @@ -504,7 +504,7 @@ describe("sideOfUser", () => { expect(sideOfUser(post, 1)).toBe("ALPHA"); }); - it("returns BRAVO for users in the accepted request's users list", () => { + test("returns BRAVO for users in the accepted request's users list", () => { const post = createPost( [{ id: 1 }], [{ isAccepted: true, users: [{ id: 2 }] }], @@ -512,7 +512,7 @@ describe("sideOfUser", () => { expect(sideOfUser(post, 2)).toBe("BRAVO"); }); - it("returns null for non-participants", () => { + test("returns null for non-participants", () => { const post = createPost( [{ id: 1 }], [{ isAccepted: true, users: [{ id: 2 }] }], @@ -520,7 +520,7 @@ describe("sideOfUser", () => { expect(sideOfUser(post, 99)).toBeNull(); }); - it("ignores users only in non-accepted requests", () => { + test("ignores users only in non-accepted requests", () => { const post = createPost( [{ id: 1 }], [{ isAccepted: false, users: [{ id: 2 }] }], @@ -533,23 +533,23 @@ describe("isTrackingLocked", () => { const ONE_HOUR_MS = 60 * 60 * 1000; const lockWindowMs = SCRIM_TRACKING_AUTO_LOCK_HOURS * ONE_HOUR_MS; - it("returns false when no map list submitted yet", () => { + test("returns false when no map list submitted yet", () => { expect(isTrackingLocked([], [], Date.now())).toBe(false); }); - it("returns false just inside the auto-lock window from list submission", () => { + test("returns false just inside the auto-lock window from list submission", () => { const now = 1_000_000_000; const updatedAt = (now - (lockWindowMs - ONE_HOUR_MS)) / 1000; expect(isTrackingLocked([], [{ updatedAt }], now)).toBe(false); }); - it("returns true just past the auto-lock window from list submission", () => { + test("returns true just past the auto-lock window from list submission", () => { const now = 1_000_000_000; const updatedAt = (now - (lockWindowMs + ONE_HOUR_MS)) / 1000; expect(isTrackingLocked([], [{ updatedAt }], now)).toBe(true); }); - it("uses the most recent reported map as the reference point", () => { + test("uses the most recent reported map as the reference point", () => { const now = 1_000_000_000; const oldUpdatedAt = (now - lockWindowMs * 2) / 1000; const recentMapSeconds = (now - ONE_HOUR_MS) / 1000; @@ -562,7 +562,7 @@ describe("isTrackingLocked", () => { ).toBe(false); }); - it("uses the most recent list update when there are no reported maps", () => { + test("uses the most recent list update when there are no reported maps", () => { const now = 1_000_000_000; const oldUpdatedAt = (now - lockWindowMs * 2) / 1000; const recentUpdatedAt = (now - ONE_HOUR_MS) / 1000; diff --git a/app/features/scrims/core/ScrimMapByMap.test.ts b/app/features/scrims/core/ScrimMapByMap.test.ts index 11341aaed..d793b23b3 100644 --- a/app/features/scrims/core/ScrimMapByMap.test.ts +++ b/app/features/scrims/core/ScrimMapByMap.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { Tables } from "~/db/tables"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { stagesObj } from "~/modules/in-game-lists/stage-ids"; @@ -21,7 +21,7 @@ function makeMap(overrides: Partial & { index: number }): MapRow { } describe("ScrimMapByMap.unionPool", () => { - it("deduplicates stage-mode pairs across multiple lists", () => { + test("deduplicates stage-mode pairs across multiple lists", () => { const pool = unionPool([ { mapList: [ @@ -46,7 +46,7 @@ describe("ScrimMapByMap.unionPool", () => { ); }); - it("merges entries across modes", () => { + test("merges entries across modes", () => { const pool = unionPool([ { mapList: [ @@ -62,7 +62,7 @@ describe("ScrimMapByMap.unionPool", () => { }); describe("ScrimMapByMap.generateNextMap", () => { - it("avoids the just-played stage when alternatives exist", () => { + test("avoids the just-played stage when alternatives exist", () => { const pool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE, stagesObj.MAKOMART, stagesObj.WAHOO_WORLD], TC: [], @@ -80,7 +80,7 @@ describe("ScrimMapByMap.generateNextMap", () => { } }); - it("advances from the last played mode when a mode was replayed", () => { + test("advances from the last played mode when a mode was replayed", () => { const pool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE, stagesObj.MAKOMART], TC: [stagesObj.HAMMERHEAD_BRIDGE], @@ -100,7 +100,7 @@ describe("ScrimMapByMap.generateNextMap", () => { expect(next.mode).toBe("TC"); }); - it("advances mode rotation after a manual pick inside the pool", () => { + test("advances mode rotation after a manual pick inside the pool", () => { const pool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE, stagesObj.MAKOMART], TC: [stagesObj.HAMMERHEAD_BRIDGE], @@ -120,7 +120,7 @@ describe("ScrimMapByMap.generateNextMap", () => { expect(next.mode).toBe("CB"); }); - it("falls back to the pool's first mode after a manual pick outside the pool's modes", () => { + test("falls back to the pool's first mode after a manual pick outside the pool's modes", () => { const pool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE, stagesObj.MAKOMART], TC: [stagesObj.HAMMERHEAD_BRIDGE], @@ -140,7 +140,7 @@ describe("ScrimMapByMap.generateNextMap", () => { expect(next.mode).toBe("SZ"); }); - it("can still generate when only one stage is available", () => { + test("can still generate when only one stage is available", () => { const pool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE], TC: [], @@ -155,7 +155,7 @@ describe("ScrimMapByMap.generateNextMap", () => { }); describe("ScrimMapByMap.canUndo", () => { - it("returns true for the most recent reported map", () => { + test("returns true for the most recent reported map", () => { const history = [ makeMap({ index: 0, reportedAt: 100 }), makeMap({ index: 1, reportedAt: 200 }), @@ -164,12 +164,12 @@ describe("ScrimMapByMap.canUndo", () => { expect(canUndo(history[1], history)).toBe(true); }); - it("returns false for unreported maps", () => { + test("returns false for unreported maps", () => { const history = [makeMap({ index: 0, reportedAt: null })]; expect(canUndo(history[0], history)).toBe(false); }); - it("returns false for a non-latest reported map", () => { + test("returns false for a non-latest reported map", () => { const history = [ makeMap({ index: 0, reportedAt: 100 }), makeMap({ index: 1, reportedAt: 200 }), @@ -177,11 +177,11 @@ describe("ScrimMapByMap.canUndo", () => { expect(canUndo(history[0], history)).toBe(false); }); - it("returns false when given undefined", () => { + test("returns false when given undefined", () => { expect(canUndo(undefined, [])).toBe(false); }); - it("returns true when an unreported next map exists after the latest reported", () => { + test("returns true when an unreported next map exists after the latest reported", () => { const history = [ makeMap({ index: 0, reportedAt: 100 }), makeMap({ index: 1, reportedAt: 200 }), @@ -224,7 +224,7 @@ describe("ScrimMapByMap.stats", () => { }), ]; - it("aggregates wins/losses from the viewer's perspective", () => { + test("aggregates wins/losses from the viewer's perspective", () => { const result = stats(history, "ALPHA"); const szMode = result.byMode.find((r) => r.key === "SZ"); @@ -234,7 +234,7 @@ describe("ScrimMapByMap.stats", () => { expect(tcMode).toEqual({ key: "TC", wins: 1, losses: 0 }); }); - it("flips wins/losses when viewing as BRAVO", () => { + test("flips wins/losses when viewing as BRAVO", () => { const result = stats(history, "BRAVO"); const szMode = result.byMode.find((r) => r.key === "SZ"); @@ -244,14 +244,14 @@ describe("ScrimMapByMap.stats", () => { expect(tcMode).toEqual({ key: "TC", wins: 0, losses: 1 }); }); - it("filters out empty rows", () => { + test("filters out empty rows", () => { const result = stats(history, "ALPHA"); for (const row of result.byMode) { expect(row.wins + row.losses).toBeGreaterThan(0); } }); - it("respects restrictToPool", () => { + test("respects restrictToPool", () => { const restrictToPool = new MapPool({ SZ: [stagesObj.SCORCH_GORGE], TC: [], diff --git a/app/features/scrims/routes/scrims.new.test.ts b/app/features/scrims/routes/scrims.new.test.ts index a683ae7f7..ace172547 100644 --- a/app/features/scrims/routes/scrims.new.test.ts +++ b/app/features/scrims/routes/scrims.new.test.ts @@ -30,7 +30,7 @@ const defaultNewScrimPostArgs = (): Parameters[0] => ({ divs: [null, null], from: { mode: "PICKUP", - users: pickupMembers.map((user) => user.id), + users: pickupMembers.ids(), }, managedByAnyone: false, postText: "Test", @@ -41,12 +41,12 @@ const defaultNewScrimPostArgs = (): Parameters[0] => ({ mapsTournamentId: null, }); -let pickupMembers: Array<{ id: number }>; +const pickupMembers = UserFactory.pool(); describe("New scrim post action", () => { beforeEach(async () => { await UserFactory.createRegular(); - pickupMembers = await UserFactory.createMany(3); + await pickupMembers.create(3); }); test("scrim post made for now has isScheduledForFuture = false", async () => { @@ -96,7 +96,7 @@ describe("New scrim post action", () => { expect(recentPickupRosters).toHaveLength(1); expect(recentPickupRosters[0]!.users.map((user) => user.id)).toEqual( - pickupMembers.map((user) => user.id).sort((a, b) => a - b), + pickupMembers.ids().sort((a, b) => a - b), ); }); }); diff --git a/app/features/scrims/scrims-schemas.test.ts b/app/features/scrims/scrims-schemas.test.ts index f67278709..a6c49ddfd 100644 --- a/app/features/scrims/scrims-schemas.test.ts +++ b/app/features/scrims/scrims-schemas.test.ts @@ -1,20 +1,20 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { divsSchema } from "./scrims-schemas"; describe("divsSchema", () => { - it("swaps min and max when max is lower skill than min", () => { + test("swaps min and max when max is lower skill than min", () => { const result = divsSchema.parse({ min: "1", max: "10" }); expect(result).toEqual({ min: "10", max: "1" }); }); - it("keeps min and max when they are in correct order", () => { + test("keeps min and max when they are in correct order", () => { const result = divsSchema.parse({ min: "10", max: "1" }); expect(result).toEqual({ min: "10", max: "1" }); }); - it("keeps min and max when they are equal", () => { + test("keeps min and max when they are equal", () => { const result = divsSchema.parse({ min: "5", max: "5" }); expect(result).toEqual({ min: "5", max: "5" }); diff --git a/app/features/scrims/scrims-search-params.test.ts b/app/features/scrims/scrims-search-params.test.ts index c3e291945..ebea18d3c 100644 --- a/app/features/scrims/scrims-search-params.test.ts +++ b/app/features/scrims/scrims-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { scrimsSearchParams } from "./scrims-search-params"; describe("scrimsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { // divs examples are in the normalized shape the divsSchema transform // produces (max is the higher div) so decode(encode(x)) equals x assertRoundTrips(scrimsSearchParams, { @@ -28,7 +28,7 @@ describe("scrimsSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(scrimsSearchParams, "weekdayTimes", [ ["25:00-22:00"], ["18:00x22:00"], diff --git a/app/features/scrims/scrims-utils.test.ts b/app/features/scrims/scrims-utils.test.ts index eebc32475..914324c5e 100644 --- a/app/features/scrims/scrims-utils.test.ts +++ b/app/features/scrims/scrims-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { formatFlexTimeDisplay, @@ -8,29 +8,20 @@ import { } from "./scrims-utils"; describe("parseLutiDivFromName", () => { - it("parses a numeric division", () => { - expect(parseLutiDivFromName("LUTI: Season 15 - Division 2")).toBe("2"); - }); - - it("parses division X", () => { - expect(parseLutiDivFromName("LUTI Season 15 Division X")).toBe("X"); - }); - - it("parses a two-digit division without matching a single digit", () => { - expect(parseLutiDivFromName("LUTI Season 15 Div 10")).toBe("10"); - }); - - it("returns null when no division token is present", () => { - expect(parseLutiDivFromName("Leagues Under The Ink Season 15")).toBeNull(); - }); - - it("returns null for an out-of-range division", () => { - expect(parseLutiDivFromName("LUTI Division 12")).toBeNull(); + test.each([ + ["LUTI: Season 15 - Division 2", "2"], + ["LUTI Season 15 Division X", "X"], + // a two-digit division must not be read as its leading single digit + ["LUTI Season 15 Div 10", "10"], + ["Leagues Under The Ink Season 15", null], + ["LUTI Division 12", null], + ])("parses %s as %s", (name, expected) => { + expect(parseLutiDivFromName(name)).toBe(expected); }); }); describe("generateTimeOptions", () => { - it("includes both start and end times", () => { + test("includes both start and end times", () => { const start = new Date("2025-01-15T14:15:00"); const end = new Date("2025-01-15T16:45:00"); @@ -40,7 +31,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(end.getTime()); }); - it("includes all :00 and :30 times in range", () => { + test("includes all :00 and :30 times in range", () => { const start = new Date("2025-01-15T14:00:00"); const end = new Date("2025-01-15T16:00:00"); @@ -53,7 +44,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); }); - it("clears seconds and milliseconds from all times", () => { + test("clears seconds and milliseconds from all times", () => { const start = new Date("2025-01-15T14:15:23.456"); const end = new Date("2025-01-15T15:45:59.999"); @@ -66,7 +57,7 @@ describe("generateTimeOptions", () => { } }); - it("returns sorted timestamps", () => { + test("returns sorted timestamps", () => { const start = new Date("2025-01-15T14:15:00"); const end = new Date("2025-01-15T16:45:00"); @@ -77,7 +68,7 @@ describe("generateTimeOptions", () => { } }); - it("handles start time between :00 and :30", () => { + test("handles start time between :00 and :30", () => { const start = new Date("2025-01-15T14:10:00"); const end = new Date("2025-01-15T15:00:00"); @@ -88,7 +79,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); }); - it("handles start time between :30 and :00", () => { + test("handles start time between :30 and :00", () => { const start = new Date("2025-01-15T14:45:00"); const end = new Date("2025-01-15T16:00:00"); @@ -100,7 +91,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); }); - it("handles range less than 30 minutes", () => { + test("handles range less than 30 minutes", () => { const start = new Date("2025-01-15T14:15:00"); const end = new Date("2025-01-15T14:25:00"); @@ -112,7 +103,7 @@ describe("generateTimeOptions", () => { ]); }); - it("handles exact hour boundaries", () => { + test("handles exact hour boundaries", () => { const start = new Date("2025-01-15T14:00:00"); const end = new Date("2025-01-15T17:00:00"); @@ -127,7 +118,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(new Date("2025-01-15T17:00:00").getTime()); }); - it("handles exact half-hour boundaries", () => { + test("handles exact half-hour boundaries", () => { const start = new Date("2025-01-15T14:30:00"); const end = new Date("2025-01-15T16:30:00"); @@ -140,7 +131,7 @@ describe("generateTimeOptions", () => { expect(result).toContain(new Date("2025-01-15T16:30:00").getTime()); }); - it("does not include duplicate times", () => { + test("does not include duplicate times", () => { const start = new Date("2025-01-15T14:00:00"); const end = new Date("2025-01-15T15:00:00"); @@ -150,7 +141,7 @@ describe("generateTimeOptions", () => { expect(result.length).toBe(uniqueValues.size); }); - it("handles maximum 3-hour range", () => { + test("handles maximum 3-hour range", () => { const start = new Date("2025-01-15T14:00:00"); const end = new Date("2025-01-15T17:00:00"); @@ -161,7 +152,7 @@ describe("generateTimeOptions", () => { }); describe("formatFlexTimeDisplay", () => { - it("returns null when totalMinutes is 0", () => { + test("returns null when totalMinutes is 0", () => { const timestamp = Math.floor( new Date("2025-01-15T14:00:00").getTime() / 1000, ); @@ -171,7 +162,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBeNull(); }); - it("returns null when endTimestamp is before startTimestamp", () => { + test("returns null when endTimestamp is before startTimestamp", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T13:00:00").getTime() / 1000); @@ -180,7 +171,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBeNull(); }); - it("returns formatted minutes when only minutes (no hours)", () => { + test("returns formatted minutes when only minutes (no hours)", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T14:45:00").getTime() / 1000); @@ -189,7 +180,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+45m"); }); - it("returns formatted hours when exactly on the hour", () => { + test("returns formatted hours when exactly on the hour", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T16:00:00").getTime() / 1000); @@ -198,7 +189,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+2h"); }); - it("returns formatted hours and minutes when both present", () => { + test("returns formatted hours and minutes when both present", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T15:30:00").getTime() / 1000); @@ -207,7 +198,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+1h 30m"); }); - it("handles 1 minute difference", () => { + test("handles 1 minute difference", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T14:01:00").getTime() / 1000); @@ -216,7 +207,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+1m"); }); - it("handles 1 hour difference", () => { + test("handles 1 hour difference", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T15:00:00").getTime() / 1000); @@ -225,7 +216,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+1h"); }); - it("handles multiple hours and minutes", () => { + test("handles multiple hours and minutes", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T17:25:00").getTime() / 1000); @@ -234,7 +225,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+3h 25m"); }); - it("handles 59 minutes", () => { + test("handles 59 minutes", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T14:59:00").getTime() / 1000); @@ -243,7 +234,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+59m"); }); - it("handles exactly 60 minutes as 1 hour", () => { + test("handles exactly 60 minutes as 1 hour", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T15:00:00").getTime() / 1000); @@ -252,7 +243,7 @@ describe("formatFlexTimeDisplay", () => { expect(result).toBe("+1h"); }); - it("handles 61 minutes as 1 hour 1 minute", () => { + test("handles 61 minutes as 1 hour 1 minute", () => { const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); const end = Math.floor(new Date("2025-01-15T15:01:00").getTime() / 1000); @@ -265,32 +256,32 @@ describe("formatFlexTimeDisplay", () => { describe("parseMapPoolInput", () => { const VALID_POOL = "tw:3330000;sz:3a14000;tc:2c98000;rm:2bc0000;cb:39c0000"; - it("returns null for empty string", () => { + test("returns null for empty string", () => { expect(parseMapPoolInput("")).toBeNull(); }); - it("returns null for whitespace-only string", () => { + test("returns null for whitespace-only string", () => { expect(parseMapPoolInput(" \t\n ")).toBeNull(); }); - it("returns null when the parsed pool is empty", () => { + test("returns null when the parsed pool is empty", () => { expect(parseMapPoolInput("not-a-valid-pool")).toBeNull(); }); - it("returns a MapPool for a bare serialized pool", () => { + test("returns a MapPool for a bare serialized pool", () => { const result = parseMapPoolInput(VALID_POOL); expect(result).toBeInstanceOf(MapPool); expect(result?.serialized).toBe(VALID_POOL); }); - it("trims whitespace around a bare serialized pool", () => { + test("trims whitespace around a bare serialized pool", () => { const result = parseMapPoolInput(` ${VALID_POOL} `); expect(result?.serialized).toBe(VALID_POOL); }); - it("extracts the pool param from a full URL", () => { + test("extracts the pool param from a full URL", () => { const result = parseMapPoolInput( `https://sendou.ink/maps?pool=${VALID_POOL}`, ); @@ -298,11 +289,11 @@ describe("parseMapPoolInput", () => { expect(result?.serialized).toBe(VALID_POOL); }); - it("returns null for a URL without a pool param", () => { + test("returns null for a URL without a pool param", () => { expect(parseMapPoolInput("https://sendou.ink/maps?other=1")).toBeNull(); }); - it("ignores other URL params when extracting pool", () => { + test("ignores other URL params when extracting pool", () => { const result = parseMapPoolInput( `https://sendou.ink/maps?foo=bar&pool=${VALID_POOL}&baz=qux`, ); @@ -310,23 +301,23 @@ describe("parseMapPoolInput", () => { expect(result?.serialized).toBe(VALID_POOL); }); - it("returns null for a malformed URL with ://", () => { + test("returns null for a malformed URL with ://", () => { expect(parseMapPoolInput("not a url://")).toBeNull(); }); - it("parses the pool value from a query-string fragment", () => { + test("parses the pool value from a query-string fragment", () => { expect(parseMapPoolInput(`pool=${VALID_POOL}`)?.serialized).toBe( VALID_POOL, ); }); - it("stops at the next & in a query-string fragment", () => { + test("stops at the next & in a query-string fragment", () => { expect(parseMapPoolInput(`pool=${VALID_POOL}&other=1`)?.serialized).toBe( VALID_POOL, ); }); - it("preserves leading params before pool= in a query-string fragment", () => { + test("preserves leading params before pool= in a query-string fragment", () => { expect(parseMapPoolInput(`foo=bar&pool=${VALID_POOL}`)?.serialized).toBe( VALID_POOL, ); diff --git a/app/features/search/search-search-params.test.ts b/app/features/search/search-search-params.test.ts index 8db3e40a3..463601262 100644 --- a/app/features/search/search-search-params.test.ts +++ b/app/features/search/search-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { searchSearchParams } from "./search-search-params"; describe("searchSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(searchSearchParams, { q: ["", "sendou", "a".repeat(100), "with spaces & specials?"], type: ["users", "teams", "organizations", "tournaments"], @@ -14,7 +14,7 @@ describe("searchSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(searchSearchParams, "q", [["a".repeat(101)]]); assertDecodesToDefault(searchSearchParams, "type", [["weapons"]]); assertDecodesToDefault(searchSearchParams, "limit", [ diff --git a/app/features/sendouq/q-constants.test.ts b/app/features/sendouq/q-constants.test.ts index 5756b2574..2ce708108 100644 --- a/app/features/sendouq/q-constants.test.ts +++ b/app/features/sendouq/q-constants.test.ts @@ -1,23 +1,18 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { FRIEND_CODE_REGEXP } from "./q-constants"; describe("FRIEND_CODE_REGEXP", () => { - it("should match valid friend codes", () => { - const validCodes = ["SW-1234-5678-9012", "1234-5678-9012", "123456789012"]; - for (const code of validCodes) { + test.each(["SW-1234-5678-9012", "1234-5678-9012", "123456789012"])( + "matches %s", + (code) => { expect(FRIEND_CODE_REGEXP.test(code)).toBe(true); - } - }); + }, + ); - it("should not match invalid friend codes", () => { - const invalidCodes = [ - "SW-1234-5678-901", - "1234-5678-901", - "12345678901", - "hello", - ]; - for (const code of invalidCodes) { + test.each(["SW-1234-5678-901", "1234-5678-901", "12345678901", "hello"])( + "does not match %s", + (code) => { expect(FRIEND_CODE_REGEXP.test(code)).toBe(false); - } - }); + }, + ); }); diff --git a/app/features/sendouq/q-search-params.test.ts b/app/features/sendouq/q-search-params.test.ts index d4a1b38b1..cfadf5431 100644 --- a/app/features/sendouq/q-search-params.test.ts +++ b/app/features/sendouq/q-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -10,7 +10,7 @@ import { } from "./q-search-params"; describe("qSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(qSearchParams, { join: [null, "abc123", "1BFXar-zY"], }); @@ -18,21 +18,21 @@ describe("qSearchParams", () => { }); describe("qLookingSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(qLookingSearchParams, { preview: [false, true], joining: [false, true], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(qLookingSearchParams, "preview", [["1"], ["yes"]]); assertDecodesToDefault(qLookingSearchParams, "joining", [["1"], ["TRUE"]]); }); }); describe("weaponUsageSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(weaponUsageSearchParams, { userId: [null, 1, 123456], season: [null, 0, 1, 10], @@ -41,7 +41,7 @@ describe("weaponUsageSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(weaponUsageSearchParams, "userId", [ ["0"], ["-1"], diff --git a/app/features/settings/match-profile-schemas.test.ts b/app/features/settings/match-profile-schemas.test.ts index 5a6e0adec..225b95be7 100644 --- a/app/features/settings/match-profile-schemas.test.ts +++ b/app/features/settings/match-profile-schemas.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { mapModePreferencesValueSchema } from "./match-profile-schemas"; describe("mapModePreferencesValueSchema", () => { - it("strips pools for avoided modes", () => { + test("strips pools for avoided modes", () => { const result = mapModePreferencesValueSchema.parse({ modes: [ { mode: "SZ", preference: "PREFER" }, @@ -17,7 +17,7 @@ describe("mapModePreferencesValueSchema", () => { expect(result.pool).toEqual([{ mode: "SZ", stages: [1, 2] }]); }); - it("keeps pools for preferred and neutral modes", () => { + test("keeps pools for preferred and neutral modes", () => { const result = mapModePreferencesValueSchema.parse({ modes: [{ mode: "SZ", preference: "PREFER" }], pool: [ @@ -32,7 +32,7 @@ describe("mapModePreferencesValueSchema", () => { ]); }); - it("does not mutate the modes selection", () => { + test("does not mutate the modes selection", () => { const result = mapModePreferencesValueSchema.parse({ modes: [{ mode: "TC", preference: "AVOID" }], pool: [{ mode: "TC", stages: [1] }], diff --git a/app/features/settings/settings-search-params.test.ts b/app/features/settings/settings-search-params.test.ts index d54b58897..34e4149c5 100644 --- a/app/features/settings/settings-search-params.test.ts +++ b/app/features/settings/settings-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,14 +6,14 @@ import { import { settingsSearchParams } from "./settings-search-params"; describe("settingsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(settingsSearchParams, { tab: [null, "preferences", "match-profile", "locale", "theme", "sounds"], lng: [null, "en", "fr", "zh-TW"], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(settingsSearchParams, "tab", [["garbage"]]); }); }); diff --git a/app/features/sidebar/core/StreamRanking.test.ts b/app/features/sidebar/core/StreamRanking.test.ts index 63b85c761..04b75e051 100644 --- a/app/features/sidebar/core/StreamRanking.test.ts +++ b/app/features/sidebar/core/StreamRanking.test.ts @@ -1,20 +1,20 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as StreamRanking from "./StreamRanking"; describe("StreamRanking.sendouQTierToScore", () => { - it("LEVIATHAN+ scores 1", () => { + test("LEVIATHAN+ scores 1", () => { expect( StreamRanking.sendouQTierToScore({ name: "LEVIATHAN", isPlus: true }), ).toBe(1); }); - it("PLATINUM+ scores 5", () => { + test("PLATINUM+ scores 5", () => { expect( StreamRanking.sendouQTierToScore({ name: "PLATINUM", isPlus: true }), ).toBe(5); }); - it("IRON & SILVER+ scores 9 (capped)", () => { + test("IRON & SILVER+ scores 9 (capped)", () => { expect( StreamRanking.sendouQTierToScore({ name: "SILVER", isPlus: true }), ).toBe(9); @@ -26,28 +26,28 @@ describe("StreamRanking.sendouQTierToScore", () => { }); describe("StreamRanking.xpToScore", () => { - it("returns null for XP below 3000", () => { + test("returns null for XP below 3000", () => { expect(StreamRanking.xpToScore(2999)).toBeNull(); expect(StreamRanking.xpToScore(0)).toBeNull(); }); - it("3000 XP scores 9", () => { + test("3000 XP scores 9", () => { expect(StreamRanking.xpToScore(3000)).toBe(9); }); - it("3200 XP scores 8", () => { + test("3200 XP scores 8", () => { expect(StreamRanking.xpToScore(3200)).toBe(8); }); - it("3400 XP scores 7", () => { + test("3400 XP scores 7", () => { expect(StreamRanking.xpToScore(3400)).toBe(7); }); - it("3800 XP scores 5 (X rank minimum)", () => { + test("3800 XP scores 5 (X rank minimum)", () => { expect(StreamRanking.xpToScore(3800)).toBe(5); }); - it("XP above 3800 is capped at score 5", () => { + test("XP above 3800 is capped at score 5", () => { expect(StreamRanking.xpToScore(4200)).toBe(5); expect(StreamRanking.xpToScore(4600)).toBe(5); expect(StreamRanking.xpToScore(9999)).toBe(5); diff --git a/app/features/team/actions/t.$customUrl.edit.server.test.ts b/app/features/team/actions/t.$customUrl.edit.server.test.ts index 99554748b..acc141da5 100644 --- a/app/features/team/actions/t.$customUrl.edit.server.test.ts +++ b/app/features/team/actions/t.$customUrl.edit.server.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; import * as TeamFactory from "~/db/seed/factories/TeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; @@ -71,7 +71,7 @@ describe("team page editing", () => { describe("custom theme", () => { beforeEach(() => createTeam()); - it("sets a custom theme via UPDATE_CUSTOM_THEME", async () => { + test("sets a custom theme via UPDATE_CUSTOM_THEME", async () => { const response = await editTeamProfileAction( { _action: "UPDATE_CUSTOM_THEME", @@ -84,7 +84,7 @@ describe("team page editing", () => { expect((await teamRow()).customTheme).toEqual(expectedStoredTheme()); }); - it("clears a custom theme via UPDATE_CUSTOM_THEME with null", async () => { + test("clears a custom theme via UPDATE_CUSTOM_THEME with null", async () => { await editTeamProfileAction( { _action: "UPDATE_CUSTOM_THEME", @@ -105,7 +105,7 @@ describe("team page editing", () => { expect((await teamRow()).customTheme).toBeNull(); }); - it("prevents setting an invalid custom theme", async () => { + test("prevents setting an invalid custom theme", async () => { const response = await editTeamProfileAction( { _action: "UPDATE_CUSTOM_THEME", @@ -120,7 +120,7 @@ describe("team page editing", () => { expect(response.fieldErrors["newValue.baseHue"]).toBeTruthy(); }); - it("preserves an existing custom theme when editing the team profile", async () => { + test("preserves an existing custom theme when editing the team profile", async () => { await editTeamProfileAction( { _action: "UPDATE_CUSTOM_THEME", @@ -156,7 +156,7 @@ describe("team page editing", () => { imageId = avatarImgId; }); - it("deletes the submitted image row when an image is removed while editing", async () => { + test("deletes the submitted image row when an image is removed while editing", async () => { await editTeamProfileAction( { ...DEFAULT_EDIT_FIELDS }, { user: "regular", params: { customUrl } }, @@ -166,7 +166,7 @@ describe("team page editing", () => { expect(await imageExists(imageId)).toBe(false); }); - it("keeps the submitted image row when an existing image is unchanged", async () => { + test("keeps the submitted image row when an existing image is unchanged", async () => { await editTeamProfileAction( { ...DEFAULT_EDIT_FIELDS, diff --git a/app/features/team/actions/t.new.server.test.ts b/app/features/team/actions/t.new.server.test.ts index b5ecf5bc8..c9c2d149b 100644 --- a/app/features/team/actions/t.new.server.test.ts +++ b/app/features/team/actions/t.new.server.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { wrappedAction } from "~/utils/Test"; import { action as teamIndexPageAction } from "../actions/t.new.server"; @@ -14,14 +14,14 @@ describe("team creation", () => { await UserFactory.createRegular(); }); - it("prevents creating a team with a duplicate name", async () => { + test("prevents creating a team with a duplicate name", async () => { await action({ name: "Team 1" }, { user: "regular" }); const res = await action({ name: "Team 1" }, { user: "regular" }); expect(res.fieldErrors.name).toBe("forms:errors.duplicateName"); }); - it("prevents creating a team whose name is only special characters", async () => { + test("prevents creating a team whose name is only special characters", async () => { const res = await action({ name: "𝓢𝓲𝓵" }, { user: "regular" }); expect(res.fieldErrors.name).toBe("forms:errors.noOnlySpecialCharacters"); diff --git a/app/features/team/routes/t.$customUrl.edit.test.ts b/app/features/team/routes/t.$customUrl.edit.test.ts index 9ec2374bd..072475172 100644 --- a/app/features/team/routes/t.$customUrl.edit.test.ts +++ b/app/features/team/routes/t.$customUrl.edit.test.ts @@ -1,23 +1,15 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; -import * as TeamFactory from "~/db/seed/factories/TeamFactory"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { wrappedAction } from "~/utils/Test"; import { action as _editTeamAction } from "../routes/t.$customUrl.edit"; import type { editTeamFormSchema } from "../team-schemas"; +import { createTeamOwnedByRegular } from "../tests/fixtures"; const editTeamAction = wrappedAction({ action: _editTeamAction, isJsonSubmission: true, }); -const createTeam = (name: string, isMainTeam = true) => - TeamFactory.create({ - name, - isMainTeam, - memberUserIds: [REGULAR_USER_TEST_ID], - }); - const DEFAULT_FIELDS = { tag: null, bsky: null, @@ -31,9 +23,9 @@ describe("team name editing", () => { await UserFactory.createRegular(); }); - it("can't take another team's name via editing", async () => { - const team = await createTeam("Team 1"); - await createTeam("Team 2", false); + test("can't take another team's name via editing", async () => { + const team = await createTeamOwnedByRegular("Team 1"); + await createTeamOwnedByRegular("Team 2", false); const res = await editTeamAction( { @@ -47,8 +39,8 @@ describe("team name editing", () => { expect(res.fieldErrors.name).toBe("forms:errors.duplicateName"); }); - it("prevents editing team name to only special characters", async () => { - const team = await createTeam("Team 1"); + test("prevents editing team name to only special characters", async () => { + const team = await createTeamOwnedByRegular("Team 1"); const res = await editTeamAction( { diff --git a/app/features/team/routes/t.$customUrl.test.ts b/app/features/team/routes/t.$customUrl.test.ts index 40986a922..2f00889a2 100644 --- a/app/features/team/routes/t.$customUrl.test.ts +++ b/app/features/team/routes/t.$customUrl.test.ts @@ -1,8 +1,6 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; -import * as TeamFactory from "~/db/seed/factories/TeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { ADMIN_ID } from "~/features/admin/admin-constants"; import { assertResponseErrored, wrappedAction } from "~/utils/Test"; import { action as _teamPageAction } from "../actions/t.$customUrl.index.server"; import { action as teamIndexPageAction } from "../actions/t.new.server"; @@ -11,6 +9,10 @@ import type { createTeamSchema, teamProfilePageActionSchema, } from "../team-schemas"; +import { + createTeamOwnedByRegular, + createTeamWithRegularMember, +} from "../tests/fixtures"; const createTeamAction = wrappedAction({ action: teamIndexPageAction, @@ -31,29 +33,13 @@ async function loadTeams() { return { team: mainTeam, secondaryTeams }; } -/** A team the regular user is a member but not the owner of. */ -const createTeamWithRegularMember = ( - overrides: Partial[0]> = {}, -) => - TeamFactory.create({ - memberUserIds: [ADMIN_ID, REGULAR_USER_TEST_ID], - ...overrides, - }); - -const createTeamOwnedByRegular = (name: string, isMainTeam = true) => - TeamFactory.create({ - name, - isMainTeam, - memberUserIds: [REGULAR_USER_TEST_ID], - }); - describe("Secondary teams", () => { beforeEach(async () => { await UserFactory.createAdmin(); await UserFactory.createRegular(); }); - it("first team created becomes main team", async () => { + test("first team created becomes main team", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); const { team, secondaryTeams } = await loadTeams(); @@ -62,7 +48,7 @@ describe("Secondary teams", () => { expect(secondaryTeams).toHaveLength(0); }); - it("second team created becomes secondary", async () => { + test("second team created becomes secondary", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); await createTeamAction({ name: "Team 2" }, { user: "regular" }); @@ -72,7 +58,7 @@ describe("Secondary teams", () => { expect(secondaryTeams[0].name).toBe("Team 2"); }); - it("makes secondary team main team", async () => { + test("makes secondary team main team", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); await createTeamAction({ name: "Team 2" }, { user: "regular" }); @@ -82,7 +68,7 @@ describe("Secondary teams", () => { expect(secondaryTeams[0].name).toBe("Team 2"); }); - it("sets main team (2 team)", async () => { + test("sets main team (2 team)", async () => { await createTeamOwnedByRegular("Team 1"); const secondary = await createTeamOwnedByRegular("Team 2", false); @@ -96,7 +82,7 @@ describe("Secondary teams", () => { expect(team!.name).toBe("Team 2"); }); - it("when deleting the main team, the secondary team becomes main", async () => { + test("when deleting the main team, the secondary team becomes main", async () => { const main = await createTeamOwnedByRegular("Team 1"); await createTeamOwnedByRegular("Team 2", false); @@ -116,7 +102,7 @@ describe("Secondary teams", () => { expect(secondaryTeams).toHaveLength(0); }); - it("only the team owner (or admin) can delete a team", async () => { + test("only the team owner (or admin) can delete a team", async () => { const { customUrl } = await createTeamWithRegularMember({ name: "Team 1" }); const response = await teamPageAction( @@ -129,7 +115,7 @@ describe("Secondary teams", () => { expect(await TeamRepository.findByCustomUrl(customUrl)).toBeTruthy(); }); - it("when leaving the main team, the secondary team becomes main", async () => { + test("when leaving the main team, the secondary team becomes main", async () => { // owned by the admin because you can't leave a team you own const main = await createTeamWithRegularMember({ name: "Team 1" }); await createTeamWithRegularMember({ name: "Team 2", isMainTeam: false }); @@ -156,7 +142,7 @@ describe("Secondary teams", () => { expect(newSecondaryTeams).toHaveLength(0); }); - it("creates max 2 teams as non-patron", async () => { + test("creates max 2 teams as non-patron", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); await createTeamAction({ name: "Team 2" }, { user: "regular" }); @@ -174,7 +160,7 @@ describe("Secondary teams as patron", () => { await UserFactory.createRegular(null, { patronTier: 2 }); }); - it("creates more than 2 teams as patron", async () => { + test("creates more than 2 teams as patron", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); await createTeamAction({ name: "Team 2" }, { user: "regular" }); await createTeamAction({ name: "Team 3" }, { user: "regular" }); diff --git a/app/features/team/team-search-params.test.ts b/app/features/team/team-search-params.test.ts index 676b4dbc3..f192751df 100644 --- a/app/features/team/team-search-params.test.ts +++ b/app/features/team/team-search-params.test.ts @@ -1,9 +1,9 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertRoundTrips } from "~/modules/search-params/search-params-test-utils"; import { teamJoinSearchParams } from "./team-search-params"; describe("teamJoinSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(teamJoinSearchParams, { code: ["abcd1234", "F3-9_xyz"], }); diff --git a/app/features/team/team-utils.test.ts b/app/features/team/team-utils.test.ts index b1541d12e..2dc3dd905 100644 --- a/app/features/team/team-utils.test.ts +++ b/app/features/team/team-utils.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { subsOfResult } from "./team-utils"; describe("subsOfResult()", () => { - it("returns empty array if all participants are current members", () => { + test("returns empty array if all participants are current members", () => { const result = { participants: [{ id: 1 }, { id: 2 }], startsAt: 1000, @@ -15,7 +15,7 @@ describe("subsOfResult()", () => { expect(subs).toEqual([]); }); - it("returns participant not in members as sub", () => { + test("returns participant not in members as sub", () => { const result = { participants: [{ id: 1 }, { id: 2 }], startsAt: 1000, @@ -25,7 +25,7 @@ describe("subsOfResult()", () => { expect(subs).toEqual([{ id: 2 }]); }); - it("returns participant as sub if they left before result startTime", () => { + test("returns participant as sub if they left before result startTime", () => { const result = { participants: [{ id: 1 }, { id: 2 }], startsAt: 1000, @@ -38,7 +38,7 @@ describe("subsOfResult()", () => { expect(subs).toEqual([{ id: 1 }]); }); - it("does not return participant as sub if they were a member during result", () => { + test("does not return participant as sub if they were a member during result", () => { const result = { participants: [{ id: 1 }, { id: 2 }], startsAt: 1000, @@ -51,7 +51,7 @@ describe("subsOfResult()", () => { expect(subs).toEqual([]); }); - it("returns multiple subs correctly", () => { + test("returns multiple subs correctly", () => { const result = { participants: [{ id: 1 }, { id: 2 }, { id: 3 }], startsAt: 1000, @@ -64,7 +64,7 @@ describe("subsOfResult()", () => { expect(subs).toEqual([{ id: 1 }, { id: 3 }]); }); - it("returns empty array if no participants", () => { + test("returns empty array if no participants", () => { const result = { participants: [], startsAt: 1000, diff --git a/app/features/team/tests/fixtures.ts b/app/features/team/tests/fixtures.ts new file mode 100644 index 000000000..270de3b7d --- /dev/null +++ b/app/features/team/tests/fixtures.ts @@ -0,0 +1,20 @@ +import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; +import * as TeamFactory from "~/db/seed/factories/TeamFactory"; +import { ADMIN_ID } from "~/features/admin/admin-constants"; + +/** A team the regular user owns, so acting as "regular" exercises the owner paths. */ +export const createTeamOwnedByRegular = (name: string, isMainTeam = true) => + TeamFactory.create({ + name, + isMainTeam, + memberUserIds: [REGULAR_USER_TEST_ID], + }); + +/** A team the regular user is a member but not the owner of. */ +export const createTeamWithRegularMember = ( + overrides: Partial[0]> = {}, +) => + TeamFactory.create({ + memberUserIds: [ADMIN_ID, REGULAR_USER_TEST_ID], + ...overrides, + }); diff --git a/app/features/tier-list-maker/tier-list-maker-search-params.test.ts b/app/features/tier-list-maker/tier-list-maker-search-params.test.ts index 59c68ba7f..52fb11806 100644 --- a/app/features/tier-list-maker/tier-list-maker-search-params.test.ts +++ b/app/features/tier-list-maker/tier-list-maker-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SearchParams from "~/modules/search-params/search-params"; import { assertDecodesToDefault, @@ -20,7 +20,7 @@ const FILLED_STATE: TierListState = { }; describe("tierListMakerSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tierListMakerSearchParams, { state: [ { tiers: DEFAULT_TIERS, tierItems: new Map() }, @@ -37,7 +37,7 @@ describe("tierListMakerSearchParams", () => { }); }); - it("always emits the compressed form for state", () => { + test("always emits the compressed form for state", () => { const encoded = SearchParams.encodeParam( tierListMakerSearchParams.shape.state, FILLED_STATE, @@ -47,7 +47,7 @@ describe("tierListMakerSearchParams", () => { expect(encoded[0]).toMatch(/^lz~/); }); - it("decodes the legacy JSON modes format", () => { + test("decodes the legacy JSON modes format", () => { expect( SearchParams.decodeParam(tierListMakerSearchParams.shape.modes, [ '["SZ","TC"]', @@ -55,7 +55,7 @@ describe("tierListMakerSearchParams", () => { ).toEqual(["SZ", "TC"]); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tierListMakerSearchParams, "state", [ [""], ["garbage"], diff --git a/app/features/tier-list-maker/tier-list-maker-utils.test.ts b/app/features/tier-list-maker/tier-list-maker-utils.test.ts index 92af08346..b01bff6df 100644 --- a/app/features/tier-list-maker/tier-list-maker-utils.test.ts +++ b/app/features/tier-list-maker/tier-list-maker-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { TIER_LIST_SEARCH_PARAM_NAMES } from "./tier-list-maker-constants"; import type { TierListItem, TierListState } from "./tier-list-maker-schemas"; import { tierListMakerSearchParams } from "./tier-list-maker-search-params"; @@ -25,7 +25,7 @@ const splattershot: TierListItem = { type: "main-weapon", id: 40 }; const splatRoller: TierListItem = { type: "main-weapon", id: 1010 }; describe("addItemToTier", () => { - it("appends the item to the target tier", () => { + test("appends the item to the target tier", () => { const state = makeState(); const result = addItemToTier(state, "tier-a", splattershot); @@ -33,7 +33,7 @@ describe("addItemToTier", () => { expect(result.tierItems.get("tier-a")).toEqual([splattershot]); }); - it("appends to the end keeping existing items", () => { + test("appends to the end keeping existing items", () => { const state = makeState({ "tier-a": [splattershot] }); const result = addItemToTier(state, "tier-a", splatRoller); @@ -41,7 +41,7 @@ describe("addItemToTier", () => { expect(result.tierItems.get("tier-a")).toEqual([splattershot, splatRoller]); }); - it("does not mutate the original state", () => { + test("does not mutate the original state", () => { const state = makeState({ "tier-a": [splattershot] }); addItemToTier(state, "tier-a", splatRoller); @@ -49,7 +49,7 @@ describe("addItemToTier", () => { expect(state.tierItems.get("tier-a")).toEqual([splattershot]); }); - it("leaves other tiers untouched", () => { + test("leaves other tiers untouched", () => { const state = makeState({ "tier-b": [splatRoller] }); const result = addItemToTier(state, "tier-a", splattershot); @@ -57,7 +57,7 @@ describe("addItemToTier", () => { expect(result.tierItems.get("tier-b")).toEqual([splatRoller]); }); - it("returns the same state reference when the tier does not exist", () => { + test("returns the same state reference when the tier does not exist", () => { const state = makeState(); const result = addItemToTier(state, "tier-missing", splattershot); @@ -67,13 +67,13 @@ describe("addItemToTier", () => { }); describe("getNextNthForItem", () => { - it("returns 1 when the item is not yet placed", () => { + test("returns 1 when the item is not yet placed", () => { const state = makeState(); expect(getNextNthForItem(splattershot, state)).toBe(1); }); - it("returns max nth + 1 across all tiers", () => { + test("returns max nth + 1 across all tiers", () => { const state = makeState({ "tier-a": [splattershot], "tier-b": [{ ...splattershot, nth: 2 }], @@ -90,7 +90,7 @@ describe("tierListMakerPathWithState", () => { return tierListMakerSearchParams.parse(searchParams).state; } - it("round trips the tier list state", () => { + test("round trips the tier list state", () => { const state = makeState({ "tier-a": [splattershot, { ...splatRoller, nth: 2 }], "tier-b": [splatRoller], @@ -105,7 +105,7 @@ describe("tierListMakerPathWithState", () => { expect(parseStateFromPath(path)).toEqual(state); }); - it("includes the title", () => { + test("includes the title", () => { const path = tierListMakerPathWithState({ state: makeState(), title: "Weapons ranked & sorted", @@ -119,7 +119,7 @@ describe("tierListMakerPathWithState", () => { ).toBe("Weapons ranked & sorted"); }); - it("only includes tier headers param when they are hidden", () => { + test("only includes tier headers param when they are hidden", () => { const withHeaders = tierListMakerPathWithState({ state: makeState(), title: "", @@ -143,11 +143,11 @@ describe("tierListMakerPathWithState", () => { }); describe("tierListItemId", () => { - it("omits nth when it is not set", () => { + test("omits nth when it is not set", () => { expect(tierListItemId(splattershot)).toBe("main-weapon:40"); }); - it("includes nth when set", () => { + test("includes nth when set", () => { expect(tierListItemId({ ...splattershot, nth: 2 })).toBe( "main-weapon:40:2", ); diff --git a/app/features/top-search/top-search-search-params.test.ts b/app/features/top-search/top-search-search-params.test.ts index 51c8a60e1..8bcc02790 100644 --- a/app/features/top-search/top-search-search-params.test.ts +++ b/app/features/top-search/top-search-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { topSearchSearchParams } from "./top-search-search-params"; describe("topSearchSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(topSearchSearchParams, { mode: ["SZ", "TC", "RM", "CB"], region: ["WEST", "JPN"], @@ -15,7 +15,7 @@ describe("topSearchSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(topSearchSearchParams, "mode", [ ["TW"], ["garbage"], diff --git a/app/features/tournament-admin/components/ExportDialog.test.ts b/app/features/tournament-admin/components/ExportDialog.test.ts index 617d7da6d..3f3223872 100644 --- a/app/features/tournament-admin/components/ExportDialog.test.ts +++ b/app/features/tournament-admin/components/ExportDialog.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { TournamentTeamFull } from "~/features/tournament-bracket/core/Tournament.server"; import { scopedAndSortedTeams } from "./ExportDialog"; @@ -39,7 +39,7 @@ describe("scopedAndSortedTeams() check-in filtering", () => { describe("bracket without its own check-in", () => { const bracketParticipantIds = new Set([checkedInAtEventLevel.id]); - it("includes an event-level checked-in team in 'Checked in only'", () => { + test("includes an event-level checked-in team in 'Checked in only'", () => { const result = scopedAndSortedTeams({ teams: [checkedInAtEventLevel], status: "checkedIn", @@ -52,7 +52,7 @@ describe("scopedAndSortedTeams() check-in filtering", () => { expect(result.map((t) => t.id)).toEqual([checkedInAtEventLevel.id]); }); - it("excludes an event-level checked-in team from 'Not checked in'", () => { + test("excludes an event-level checked-in team from 'Not checked in'", () => { const result = scopedAndSortedTeams({ teams: [checkedInAtEventLevel], status: "notCheckedIn", @@ -79,7 +79,7 @@ describe("scopedAndSortedTeams() check-in filtering", () => { onlyEventLevel.id, ]); - it("keeps only the team checked into the bracket in 'Checked in only'", () => { + test("keeps only the team checked into the bracket in 'Checked in only'", () => { const result = scopedAndSortedTeams({ teams: [checkedIntoBracket, onlyEventLevel], status: "checkedIn", @@ -92,7 +92,7 @@ describe("scopedAndSortedTeams() check-in filtering", () => { expect(result.map((t) => t.id)).toEqual([checkedIntoBracket.id]); }); - it("lists a bracket team pending check-in in 'Not checked in'", () => { + test("lists a bracket team pending check-in in 'Not checked in'", () => { const result = scopedAndSortedTeams({ teams: [checkedIntoBracket, onlyEventLevel], status: "notCheckedIn", @@ -105,7 +105,7 @@ describe("scopedAndSortedTeams() check-in filtering", () => { expect(result.map((t) => t.id)).toEqual([onlyEventLevel.id]); }); - it("excludes a not-checked-in team that does not participate in the bracket", () => { + test("excludes a not-checked-in team that does not participate in the bracket", () => { const notInBracket = team(3, checkIns([])); const result = scopedAndSortedTeams({ diff --git a/app/features/tournament-admin/tournament-admin-search-params.test.ts b/app/features/tournament-admin/tournament-admin-search-params.test.ts index 294577712..91c10cecd 100644 --- a/app/features/tournament-admin/tournament-admin-search-params.test.ts +++ b/app/features/tournament-admin/tournament-admin-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -9,7 +9,7 @@ import { } from "./tournament-admin-search-params"; describe("tournamentAuditSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentAuditSearchParams, { page: [1, 2, 100], auditType: ["MEMBER_ADDED", "UPDATE_IN_GAME_NAME"], @@ -17,7 +17,7 @@ describe("tournamentAuditSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tournamentAuditSearchParams, "page", [ ["0"], ["-1"], @@ -35,13 +35,13 @@ describe("tournamentAuditSearchParams", () => { }); describe("tournamentImportTeamsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentImportTeamsSearchParams, { fromTournamentId: [1, 999999], }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault( tournamentImportTeamsSearchParams, "fromTournamentId", diff --git a/app/features/tournament-bracket/components/Bracket/useBracketSpoilerCensor.test.ts b/app/features/tournament-bracket/components/Bracket/useBracketSpoilerCensor.test.ts index bd553ea2f..8021e1559 100644 --- a/app/features/tournament-bracket/components/Bracket/useBracketSpoilerCensor.test.ts +++ b/app/features/tournament-bracket/components/Bracket/useBracketSpoilerCensor.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { matchCensorLevel } from "./useBracketSpoilerCensor"; const BASE_ARGS = { @@ -8,7 +8,7 @@ const BASE_ARGS = { } as const; describe("matchCensorLevel()", () => { - it("returns undefined when not censored", () => { + test("returns undefined when not censored", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -18,7 +18,7 @@ describe("matchCensorLevel()", () => { ).toBeUndefined(); }); - it("returns 'score-only' for DE winners round 1", () => { + test("returns 'score-only' for DE winners round 1", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -28,7 +28,7 @@ describe("matchCensorLevel()", () => { ).toBe("score-only"); }); - it("returns 'full' for DE winners round 2+", () => { + test("returns 'full' for DE winners round 2+", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -40,7 +40,7 @@ describe("matchCensorLevel()", () => { ).toBe("full"); }); - it("returns 'full' for DE losers round", () => { + test("returns 'full' for DE losers round", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -50,7 +50,7 @@ describe("matchCensorLevel()", () => { ).toBe("full"); }); - it("returns 'score-only' for swiss round 1", () => { + test("returns 'score-only' for swiss round 1", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -59,7 +59,7 @@ describe("matchCensorLevel()", () => { ).toBe("score-only"); }); - it("returns 'full' for swiss round 2+", () => { + test("returns 'full' for swiss round 2+", () => { expect( matchCensorLevel({ ...BASE_ARGS, @@ -70,7 +70,7 @@ describe("matchCensorLevel()", () => { ).toBe("full"); }); - it("returns 'score-only' for round robin", () => { + test("returns 'score-only' for round robin", () => { expect( matchCensorLevel({ ...BASE_ARGS, diff --git a/app/features/tournament-bracket/core/AbDivisions.test.ts b/app/features/tournament-bracket/core/AbDivisions.test.ts index 74e31655e..3a1ad4ee4 100644 --- a/app/features/tournament-bracket/core/AbDivisions.test.ts +++ b/app/features/tournament-bracket/core/AbDivisions.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { unwrap, unwrapErr } from "~/utils/result"; import * as AbDivisions from "./AbDivisions"; describe("AbDivisions.validate", () => { - it("accepts a balanced 12-team single-group configuration", () => { + test("accepts a balanced 12-team single-group configuration", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], groupCount: 1, @@ -13,7 +13,7 @@ describe("AbDivisions.validate", () => { expect(unwrap(result)).toEqual([0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]); }); - it("accepts a balanced 12-team two-group configuration", () => { + test("accepts a balanced 12-team two-group configuration", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], groupCount: 2, @@ -22,7 +22,7 @@ describe("AbDivisions.validate", () => { expect(result.ok).toBe(true); }); - it("rejects any unassigned team", () => { + test("rejects any unassigned team", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, null, 0, 1, 0, 1, 0, 1], groupCount: 1, @@ -32,7 +32,7 @@ describe("AbDivisions.validate", () => { expect(unwrapErr(result)).toMatch(/assigned/); }); - it("rejects invalid division values", () => { + test("rejects invalid division values", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 2, 0, 1, 0, 1, 0, 1], groupCount: 1, @@ -41,7 +41,7 @@ describe("AbDivisions.validate", () => { expect(result.ok).toBe(false); }); - it("rejects A/B counts differing by more than 1", () => { + test("rejects A/B counts differing by more than 1", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], groupCount: 1, @@ -51,7 +51,7 @@ describe("AbDivisions.validate", () => { expect(unwrapErr(result)).toMatch(/7 A, 5 B/); }); - it("accepts a ±1 uneven configuration with a single group", () => { + test("accepts a ±1 uneven configuration with a single group", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], groupCount: 1, @@ -60,7 +60,7 @@ describe("AbDivisions.validate", () => { expect(result.ok).toBe(true); }); - it("rejects a ±1 uneven configuration when there are multiple groups", () => { + test("rejects a ±1 uneven configuration when there are multiple groups", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], groupCount: 2, @@ -70,7 +70,7 @@ describe("AbDivisions.validate", () => { expect(unwrapErr(result)).toMatch(/single group/); }); - it("rejects team counts not divisible by group count", () => { + test("rejects team counts not divisible by group count", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], groupCount: 3, @@ -80,7 +80,7 @@ describe("AbDivisions.validate", () => { expect(unwrapErr(result)).toMatch(/10 checked-in teams into 3/); }); - it("rejects odd per-group team counts", () => { + test("rejects odd per-group team counts", () => { const result = AbDivisions.validate({ abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], groupCount: 2, @@ -90,7 +90,7 @@ describe("AbDivisions.validate", () => { expect(unwrapErr(result)).toMatch(/5 teams/); }); - it("preserves the original order of the divisions", () => { + test("preserves the original order of the divisions", () => { const divisions = [1, 0, 1, 0, 0, 1, 1, 0]; const result = AbDivisions.validate({ @@ -103,7 +103,7 @@ describe("AbDivisions.validate", () => { }); describe("AbDivisions.countByDivision", () => { - it("counts A, B, and unassigned separately", () => { + test("counts A, B, and unassigned separately", () => { const counts = AbDivisions.countByDivision([ { abDivision: 0 }, { abDivision: 0 }, diff --git a/app/features/tournament-bracket/core/Bracket.test.ts b/app/features/tournament-bracket/core/Bracket.test.ts index 5fb8a20f3..03c17db6d 100644 --- a/app/features/tournament-bracket/core/Bracket.test.ts +++ b/app/features/tournament-bracket/core/Bracket.test.ts @@ -1,5 +1,5 @@ import * as R from "remeda"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import invariant from "../../../utils/invariant"; import * as Engine from "./engine"; import { createResolved } from "./engine/create"; @@ -13,7 +13,7 @@ const TEAM_ERROR_404_ID = 17354; const TEAM_THIS_IS_FINE_ID = 17513; describe("swiss standings - losses against tied", () => { - it("should calculate losses against tied", () => { + test("calculates losses against tied", () => { const tournament = new Tournament({ ...LOW_INK_DECEMBER_2024(), }); @@ -27,7 +27,7 @@ describe("swiss standings - losses against tied", () => { expect(standing.stats?.lossesAgainstTied).toBe(1); }); - it("breaks ties on losses against tied, not wins against tied", () => { + test("breaks ties on losses against tied, not wins against tied", () => { const tournament = new Tournament({ ...LOW_INK_DECEMBER_2024(), }); @@ -54,7 +54,7 @@ describe("swiss standings - losses against tied", () => { ); }); - it("ranks fewer losses against tied above a higher opponent set win %", () => { + test("ranks fewer losses against tied above a higher opponent set win %", () => { const tournament = new Tournament({ ...LOW_INK_DECEMBER_2024(), }); @@ -78,7 +78,7 @@ describe("swiss standings - losses against tied", () => { expect(noTiedLosses.placement).toBeLessThan(oneTiedLoss.placement); }); - it("should ignore early dropped out teams for standings (losses against tied)", () => { + test("ignores early dropped out teams for standings (losses against tied)", () => { const tournament = new Tournament({ ...LOW_INK_DECEMBER_2024(), }); @@ -91,7 +91,7 @@ describe("swiss standings - losses against tied", () => { expect(standing.stats?.lossesAgainstTied).toBe(0); // they lost against "Tidy Tidings" but that team dropped out before final round }); - it("should ignore a dropped out team with an identical record (losses against tied)", () => { + test("ignores a dropped out team with an identical record (losses against tied)", () => { const data = Engine.create({ type: "swiss", seeding: [1, 2, 3, 4, 5, 6], @@ -190,7 +190,7 @@ describe("swiss standings - losses against tied", () => { }); }; - it("should handle a team with only one bye", () => { + test("handles a team with only one bye", () => { const tournament = inProgressSwissTestTournament(); const standings = tournament.bracketByIdx(0)!.liveStandings; @@ -205,7 +205,7 @@ describe("swiss standings - losses against tied", () => { expect(teamWithBye?.stats?.setLosses).toBe(0); }); - it("team with only unfinished matches should be in the current standings with blank stats", () => { + test("team with only unfinished matches should be in the current standings with blank stats", () => { const tournament = inProgressSwissTestTournament(); const standings = tournament.bracketByIdx(0)!.liveStandings; @@ -266,7 +266,7 @@ describe("swiss standings - cross group ties", () => { }); }; - it("ranks the group winner with the better effective seed first", () => { + test("ranks the group winner with the better effective seed first", () => { const standings = twoGroupSwissTournament().bracketByIdx(0)!.standings; const upsetWinnerIdx = standings.findIndex((s) => s.team.id === 6); @@ -349,7 +349,7 @@ describe("swiss standings - rematches between tied teams", () => { }); }; - it("counts every meeting between tied teams for the head-to-head tiebreaker", () => { + test("counts every meeting between tied teams for the head-to-head tiebreaker", () => { const standings = swissTournamentWithRematches().bracketByIdx(0)!.standings; const team1 = standings.find((s) => s.team.id === 1)!; @@ -362,7 +362,7 @@ describe("swiss standings - rematches between tied teams", () => { }); describe("round robin standings", () => { - it("should sort teams primarily by set wins (per group) in paddling pool 255", () => { + test("sorts teams primarily by set wins (per group) in paddling pool 255", () => { const tournamentPP255 = new Tournament(PADDLING_POOL_255()); const standings = tournamentPP255.bracketByIdx(0)!.standings; @@ -394,7 +394,7 @@ describe("round robin standings", () => { } }); - it("breaks same placement ties across groups by effective seed (own seed or best seed beaten)", () => { + test("breaks same placement ties across groups by effective seed (own seed or best seed beaten)", () => { const tournamentPP255 = new Tournament(PADDLING_POOL_255()); const bracket = tournamentPP255.bracketByIdx(0)!; @@ -538,7 +538,7 @@ describe("round robin standings - dropped out teams", () => { }); }; - it("should not credit wins against a team that dropped out before completing all of their matches", () => { + test("does not credit wins against a team that dropped out before completing all of their matches", () => { // Team 4 dropped out before playing their match against team 3. const tournament = droppedOutTournament({ skipMatchups: ["3-4"] }); const standings = tournament.bracketByIdx(0)!.liveStandings; @@ -557,7 +557,7 @@ describe("round robin standings - dropped out teams", () => { expect(team3Standing?.stats?.setLosses).toBe(2); }); - it("should still count matches against a team that dropped out only after all of their matches were reported", () => { + test("stills count matches against a team that dropped out only after all of their matches were reported", () => { const tournament = droppedOutTournament(); const standings = tournament.bracketByIdx(0)!.standings; @@ -575,7 +575,7 @@ describe("round robin standings - dropped out teams", () => { expect(team3Standing?.stats?.setLosses).toBe(2); }); - it("should not credit wins against a team that dropped out before completing all of their matches (forfeit-closed)", () => { + test("does not credit wins against a team that dropped out before completing all of their matches (forfeit-closed)", () => { // Production scenario: team 4 dropped before playing 3-4, then admin's // drop action ran endDroppedTeamMatches which closed 3-4 with a result // (team 3 marked winner) but no score on either side. Wins against team @@ -598,7 +598,7 @@ describe("round robin standings - dropped out teams", () => { expect(team3Standing?.stats?.setLosses).toBe(2); }); - it("should report relevantMatchesFinished=true when a dropped team's remaining matches were forfeited (no score)", () => { + test("reports relevantMatchesFinished=true when a dropped team's remaining matches were forfeited (no score)", () => { const tournament = droppedOutTournament({ forfeitMatchups: ["3-4"] }); const { relevantMatchesFinished } = tournament @@ -608,7 +608,7 @@ describe("round robin standings - dropped out teams", () => { expect(relevantMatchesFinished).toBe(true); }); - it("includes a fully-forfeited dropped team in standings", () => { + test("includes a fully-forfeited dropped team in standings", () => { const tournament = droppedOutTournament({ forfeitMatchups: ["3-4"] }); const standings = tournament.bracketByIdx(0)!.standings; @@ -698,14 +698,14 @@ describe("round robin standings - dropped out teams", () => { }; }; - it("includes a team whose every group opponent dropped out in standings", () => { + test("includes a team whose every group opponent dropped out in standings", () => { const { tournament, survivingTeamId } = twoTeamGroupDropoutTournament(); const standings = tournament.bracketByIdx(0)!.standings; expect(standings.map((s) => s.team.id)).toContain(survivingTeamId); }); - it("reports relevantMatchesFinished=true when a 2-team group ended via drop out", () => { + test("reports relevantMatchesFinished=true when a 2-team group ended via drop out", () => { const { tournament } = twoTeamGroupDropoutTournament(); const { relevantMatchesFinished } = tournament @@ -785,7 +785,7 @@ describe("round robin A/B divisions standings", () => { }); }; - it("filtering by abDivision preserves standard tiebreaker order within each division", () => { + test("filtering by abDivision preserves standard tiebreaker order within each division", () => { const tournament = abDivisionsTournament(); const standings = tournament.bracketByIdx(0)!.liveStandings; @@ -798,14 +798,14 @@ describe("round robin A/B divisions standings", () => { expect(divisionB.map((s) => s.team.id)).toEqual([2, 4]); }); - it("source({ placements: [1] }) returns top team from each division", () => { + test("source({ placements: [1] }) returns top team from each division", () => { const tournament = abDivisionsTournament(); const { teams } = tournament.bracketByIdx(0)!.source({ placements: [1] }); expect(teams).toEqual([1, 2]); }); - it("source({ placements: [1, 2] }) returns top two teams from each division", () => { + test("source({ placements: [1, 2] }) returns top two teams from each division", () => { const tournament = abDivisionsTournament(); const { teams } = tournament .bracketByIdx(0)! @@ -817,7 +817,7 @@ describe("round robin A/B divisions standings", () => { expect(teams.slice(2, 4)).toEqual([2, 4]); }); - it("source ignores placements beyond division size", () => { + test("source ignores placements beyond division size", () => { const tournament = abDivisionsTournament(); const { teams } = tournament .bracketByIdx(0)! @@ -893,7 +893,7 @@ describe("single elimination standings - third place match", () => { return { tournament, thirdPlaceWinnerId, thirdPlaceLoserId }; }; - it("excludes semifinal losers from standings before the third place match concludes", () => { + test("excludes semifinal losers from standings before the third place match concludes", () => { const { tournament } = singleEliminationTournament({ thirdPlaceMatchReported: false, }); @@ -903,7 +903,7 @@ describe("single elimination standings - third place match", () => { expect(standings).toHaveLength(0); }); - it("places third place match winner 3rd and loser 4th once it is played", () => { + test("places third place match winner 3rd and loser 4th once it is played", () => { const { tournament, thirdPlaceWinnerId, thirdPlaceLoserId } = singleEliminationTournament({ thirdPlaceMatchReported: true, @@ -1005,7 +1005,7 @@ describe("single elimination standings - byes in later rounds", () => { data: legacyByeBracketData(), }); - it("places every team when a match is won against a bye", () => { + test("places every team when a match is won against a bye", () => { const tournament = legacyByeTournament(); const standings = tournament.bracketByIdx(0)!.standings; @@ -1020,7 +1020,7 @@ describe("single elimination standings - byes in later rounds", () => { ]); }); - it("gives third place to the only semifinal loser when the third place match is a bye", () => { + test("gives third place to the only semifinal loser when the third place match is a bye", () => { const tournament = legacyByeTournament(); const standings = tournament.bracketByIdx(0)!.standings; @@ -1072,7 +1072,7 @@ describe("single elimination standings - projected ties", () => { return { tournament, decidedLoserId }; }; - it("projects a finished semifinal loser as tied 3rd before the other semifinal finishes", () => { + test("projects a finished semifinal loser as tied 3rd before the other semifinal finishes", () => { const { tournament, decidedLoserId } = partialSingleEliminationTournament(); const standings = tournament.bracketByIdx(0)!.standings; @@ -1160,7 +1160,7 @@ describe("double elimination standings - projected ties", () => { return { tournament, decidedLoserId, stillPlayingTeamIds }; }; - it("projects a finished losers-round-2 loser as tied 5th before the sibling match finishes", () => { + test("projects a finished losers-round-2 loser as tied 5th before the sibling match finishes", () => { const { tournament, decidedLoserId } = partialDoubleEliminationTournament(); const standings = tournament.bracketByIdx(0)!.standings; @@ -1170,7 +1170,7 @@ describe("double elimination standings - projected ties", () => { ); }); - it("does not yet place teams still playing their losers round 2 match", () => { + test("does not yet place teams still playing their losers round 2 match", () => { const { tournament, stillPlayingTeamIds } = partialDoubleEliminationTournament(); @@ -1231,7 +1231,7 @@ describe("single elimination source - underground", () => { return { tournament, firstRoundLoserIds }; }; - it("sources the first-round losers when placements are [-1]", () => { + test("sources the first-round losers when placements are [-1]", () => { const { tournament, firstRoundLoserIds } = playedSingleEliminationTournament(); @@ -1292,7 +1292,7 @@ describe("single elimination source - positive placements", () => { }); }; - it("sources the winner when placements are [1]", () => { + test("sources the winner when placements are [1]", () => { const tournament = singleEliminationTournament({ playedRounds: "all" }); const { teams, relevantMatchesFinished } = tournament @@ -1303,7 +1303,7 @@ describe("single elimination source - positive placements", () => { expect(teams).toEqual([1]); }); - it("sources the top 2 when placements are [1, 2]", () => { + test("sources the top 2 when placements are [1, 2]", () => { const tournament = singleEliminationTournament({ playedRounds: "all" }); const { teams, relevantMatchesFinished } = tournament @@ -1314,7 +1314,7 @@ describe("single elimination source - positive placements", () => { expect(teams).toEqual([1, 2]); }); - it("sources both tied semifinal losers when placements are [3]", () => { + test("sources both tied semifinal losers when placements are [3]", () => { const tournament = singleEliminationTournament({ playedRounds: "all" }); const { teams } = tournament.bracketByIdx(0)!.source({ placements: [3] }); @@ -1322,7 +1322,7 @@ describe("single elimination source - positive placements", () => { expect([...teams].sort((a, b) => a - b)).toEqual([3, 4]); }); - it("reports relevant matches unfinished while the bracket is underway", () => { + test("reports relevant matches unfinished while the bracket is underway", () => { const tournament = singleEliminationTournament({ playedRounds: "first" }); const { teams, relevantMatchesFinished } = tournament @@ -1380,7 +1380,7 @@ describe("double elimination source - positive placements", () => { }); }; - it("sources the winner when placements are [1]", () => { + test("sources the winner when placements are [1]", () => { const tournament = doubleEliminationTournament({ playedRounds: "all" }); const { teams, relevantMatchesFinished } = tournament @@ -1391,7 +1391,7 @@ describe("double elimination source - positive placements", () => { expect(teams).toEqual([1]); }); - it("sources the top 2 when placements are [1, 2]", () => { + test("sources the top 2 when placements are [1, 2]", () => { const tournament = doubleEliminationTournament({ playedRounds: "all" }); const { teams, relevantMatchesFinished } = tournament @@ -1402,7 +1402,7 @@ describe("double elimination source - positive placements", () => { expect(teams).toEqual([1, 2]); }); - it("reports relevant matches unfinished while the bracket is underway", () => { + test("reports relevant matches unfinished while the bracket is underway", () => { const tournament = doubleEliminationTournament({ playedRounds: "first" }); const { teams, relevantMatchesFinished } = tournament @@ -1444,7 +1444,7 @@ describe("swiss between rounds", () => { return data; }; - it("tournament is not over while swiss still has unpaired rounds", () => { + test("tournament is not over while swiss still has unpaired rounds", () => { const tournament = testTournament({ ctx: { settings: { bracketProgression: [SWISS_MAIN_BRACKET] }, @@ -1455,7 +1455,7 @@ describe("swiss between rounds", () => { expect(tournament.everyBracketOver).toBe(false); }); - it("can't finalize between swiss rounds when progression also has an underground bracket", () => { + test("can't finalize between swiss rounds when progression also has an underground bracket", () => { const tournament = testTournament({ ctx: { settings: { diff --git a/app/features/tournament-bracket/core/Deadline.test.ts b/app/features/tournament-bracket/core/Deadline.test.ts index f4d362031..9c7e95fc3 100644 --- a/app/features/tournament-bracket/core/Deadline.test.ts +++ b/app/features/tournament-bracket/core/Deadline.test.ts @@ -1,36 +1,36 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as Deadline from "./Deadline"; describe("totalMatchTime", () => { - it("calculates total time for best of 3", () => { + test("calculates total time for best of 3", () => { expect(Deadline.totalMatchTime(3)).toBe(26); }); - it("calculates total time for best of 5", () => { + test("calculates total time for best of 5", () => { expect(Deadline.totalMatchTime(5)).toBe(39); }); }); describe("progressPercentage", () => { - it("returns 0% when no time has elapsed", () => { + test("returns 0% when no time has elapsed", () => { expect(Deadline.progressPercentage(0, 20)).toBe(0); }); - it("returns 50% when halfway through", () => { + test("returns 50% when halfway through", () => { expect(Deadline.progressPercentage(10, 20)).toBe(50); }); - it("returns 100% when time is up", () => { + test("returns 100% when time is up", () => { expect(Deadline.progressPercentage(20, 20)).toBe(100); }); - it("returns over 100% when overtime", () => { + test("returns over 100% when overtime", () => { expect(Deadline.progressPercentage(30, 20)).toBe(150); }); }); describe("gameMarkers", () => { - it("returns correct markers for best of 3", () => { + test("returns correct markers for best of 3", () => { const markers = Deadline.gameMarkers(3); expect(markers).toHaveLength(3); expect(markers[0].gameNumber).toBe(1); @@ -42,7 +42,7 @@ describe("gameMarkers", () => { expect(markers[2].gameStartMinute).toBe(19.5); }); - it("returns correct markers for best of 5", () => { + test("returns correct markers for best of 5", () => { const markers = Deadline.gameMarkers(5); expect(markers).toHaveLength(5); expect(markers[0].gameNumber).toBe(1); @@ -55,7 +55,7 @@ describe("gameMarkers", () => { }); describe("matchStatus", () => { - it("returns normal when on schedule", () => { + test("returns normal when on schedule", () => { const status = Deadline.matchStatus({ elapsedMinutes: 10, gamesCompleted: 1, @@ -64,7 +64,7 @@ describe("matchStatus", () => { expect(status).toBe("normal"); }); - it("returns warning when behind schedule", () => { + test("returns warning when behind schedule", () => { const status = Deadline.matchStatus({ elapsedMinutes: 15, gamesCompleted: 0, @@ -73,7 +73,7 @@ describe("matchStatus", () => { expect(status).toBe("warning"); }); - it("returns error when time is up", () => { + test("returns error when time is up", () => { const status = Deadline.matchStatus({ elapsedMinutes: 30, gamesCompleted: 2, @@ -82,7 +82,7 @@ describe("matchStatus", () => { expect(status).toBe("error"); }); - it("returns normal during prep time", () => { + test("returns normal during prep time", () => { const status = Deadline.matchStatus({ elapsedMinutes: 5, gamesCompleted: 0, @@ -91,7 +91,7 @@ describe("matchStatus", () => { expect(status).toBe("normal"); }); - it("defaults to normal for zero elapsed time", () => { + test("defaults to normal for zero elapsed time", () => { const status = Deadline.matchStatus({ elapsedMinutes: 0, gamesCompleted: 0, diff --git a/app/features/tournament-bracket/core/PickBan.test.ts b/app/features/tournament-bracket/core/PickBan.test.ts index 6217d6a45..da9653338 100644 --- a/app/features/tournament-bracket/core/PickBan.test.ts +++ b/app/features/tournament-bracket/core/PickBan.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { TournamentRoundMaps } from "~/db/tables-json"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; +import type { WhoSide } from "../tournament-bracket-constants"; import { CUSTOM_FLOW_VALIDATION_ERRORS, currentTurnSessionStartedAt, @@ -17,7 +18,7 @@ import { } from "./PickBan"; describe("validateCustomFlowSection", () => { - it("returns no errors for valid preSet steps", () => { + test("returns no errors for valid preSet steps", () => { const steps = [ { action: "BAN" as const, side: "HIGHER_SEED" as const }, { action: "BAN" as const, side: "LOWER_SEED" as const }, @@ -27,7 +28,7 @@ describe("validateCustomFlowSection", () => { expect(validateCustomFlowSection(steps, "preSet")).toEqual([]); }); - it("returns no errors for valid postGame steps", () => { + test("returns no errors for valid postGame steps", () => { const steps = [ { action: "BAN" as const, side: "WINNER" as const }, { action: "PICK" as const, side: "LOSER" as const }, @@ -36,7 +37,7 @@ describe("validateCustomFlowSection", () => { expect(validateCustomFlowSection(steps, "postGame")).toEqual([]); }); - it("returns STEP_MISSING_ACTION when a step has no action", () => { + test("returns STEP_MISSING_ACTION when a step has no action", () => { const steps = [ { side: "ALPHA" as const }, { action: "PICK" as const, side: "ALPHA" as const }, @@ -47,7 +48,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns STEP_MISSING_WHO when a non-ROLL step has no side", () => { + test("returns STEP_MISSING_WHO when a non-ROLL step has no side", () => { const steps = [{ action: "BAN" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toContain( @@ -55,13 +56,13 @@ describe("validateCustomFlowSection", () => { ); }); - it("does not require side for ROLL steps", () => { + test("does not require side for ROLL steps", () => { const steps = [{ action: "ROLL" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toEqual([]); }); - it("returns LAST_STEP_MUST_BE_PICK_OR_ROLL when last step is BAN", () => { + test("returns LAST_STEP_MUST_BE_PICK_OR_ROLL when last step is BAN", () => { const steps = [ { action: "PICK" as const, side: "ALPHA" as const }, { action: "BAN" as const, side: "BRAVO" as const }, @@ -72,7 +73,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("accepts PICK_NO_MODE_REPEAT as the last (map picking) step", () => { + test("accepts PICK_NO_MODE_REPEAT as the last (map picking) step", () => { const steps = [ { action: "BAN" as const, side: "HIGHER_SEED" as const }, { action: "PICK_NO_MODE_REPEAT" as const, side: "LOWER_SEED" as const }, @@ -81,7 +82,7 @@ describe("validateCustomFlowSection", () => { expect(validateCustomFlowSection(steps, "postGame")).toEqual([]); }); - it("counts PICK_NO_MODE_REPEAT toward TOO_MANY_MAP_PICKS", () => { + test("counts PICK_NO_MODE_REPEAT toward TOO_MANY_MAP_PICKS", () => { const steps = [ { action: "PICK" as const, side: "HIGHER_SEED" as const }, { action: "PICK_NO_MODE_REPEAT" as const, side: "LOWER_SEED" as const }, @@ -92,7 +93,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns SAME_TEAM_MODE_AND_MAP_PICK for PICK_NO_MODE_REPEAT by the mode picker", () => { + test("returns SAME_TEAM_MODE_AND_MAP_PICK for PICK_NO_MODE_REPEAT by the mode picker", () => { const steps = [ { action: "MODE_PICK" as const, side: "HIGHER_SEED" as const }, { action: "PICK_NO_MODE_REPEAT" as const, side: "HIGHER_SEED" as const }, @@ -103,7 +104,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns LAST_STEP_MUST_BE_PICK_OR_ROLL when last step is MODE_BAN", () => { + test("returns LAST_STEP_MUST_BE_PICK_OR_ROLL when last step is MODE_BAN", () => { const steps = [{ action: "MODE_BAN" as const, side: "ALPHA" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toContain( @@ -111,7 +112,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("allows PICK as last step", () => { + test("allows PICK as last step", () => { const steps = [{ action: "PICK" as const, side: "ALPHA" as const }]; const errors = validateCustomFlowSection(steps, "preSet"); @@ -121,7 +122,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("allows ROLL as last step", () => { + test("allows ROLL as last step", () => { const steps = [{ action: "ROLL" as const }]; const errors = validateCustomFlowSection(steps, "postGame"); @@ -131,7 +132,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns WINNER_LOSER_IN_PRE_SET when WINNER is used in preSet", () => { + test("returns WINNER_LOSER_IN_PRE_SET when WINNER is used in preSet", () => { const steps = [{ action: "PICK" as const, side: "WINNER" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toContain( @@ -139,7 +140,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns WINNER_LOSER_IN_PRE_SET when LOSER is used in preSet", () => { + test("returns WINNER_LOSER_IN_PRE_SET when LOSER is used in preSet", () => { const steps = [{ action: "PICK" as const, side: "LOSER" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toContain( @@ -147,7 +148,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("allows WINNER/LOSER in postGame", () => { + test("allows WINNER/LOSER in postGame", () => { const steps = [ { action: "BAN" as const, side: "WINNER" as const }, { action: "PICK" as const, side: "LOSER" as const }, @@ -156,7 +157,7 @@ describe("validateCustomFlowSection", () => { expect(validateCustomFlowSection(steps, "postGame")).toEqual([]); }); - it("returns TOO_MANY_MODE_PICKS when more than one MODE_PICK", () => { + test("returns TOO_MANY_MODE_PICKS when more than one MODE_PICK", () => { const steps = [ { action: "MODE_PICK" as const, side: "ALPHA" as const }, { action: "MODE_PICK" as const, side: "BRAVO" as const }, @@ -168,7 +169,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns TOO_MANY_MAP_PICKS when section has PICK and ROLL", () => { + test("returns TOO_MANY_MAP_PICKS when section has PICK and ROLL", () => { const steps = [ { action: "BAN" as const, side: "ALPHA" as const }, { action: "PICK" as const, side: "BRAVO" as const }, @@ -180,7 +181,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns TOO_MANY_MAP_PICKS when section has two ROLLs", () => { + test("returns TOO_MANY_MAP_PICKS when section has two ROLLs", () => { const steps = [{ action: "ROLL" as const }, { action: "ROLL" as const }]; expect(validateCustomFlowSection(steps, "preSet")).toContain( @@ -188,7 +189,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns TOO_MANY_MAP_PICKS when section has two PICKs", () => { + test("returns TOO_MANY_MAP_PICKS when section has two PICKs", () => { const steps = [ { action: "PICK" as const, side: "ALPHA" as const }, { action: "MODE_BAN" as const, side: "BRAVO" as const }, @@ -200,7 +201,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("allows exactly one PICK or ROLL", () => { + test("allows exactly one PICK or ROLL", () => { const stepsWithPick = [ { action: "BAN" as const, side: "ALPHA" as const }, { action: "PICK" as const, side: "BRAVO" as const }, @@ -218,7 +219,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("allows exactly one MODE_PICK", () => { + test("allows exactly one MODE_PICK", () => { const steps = [ { action: "MODE_PICK" as const, side: "ALPHA" as const }, { action: "PICK" as const, side: "BRAVO" as const }, @@ -231,13 +232,13 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns LAST_STEP_MUST_BE_PICK_OR_ROLL for empty steps array", () => { + test("returns LAST_STEP_MUST_BE_PICK_OR_ROLL for empty steps array", () => { expect(validateCustomFlowSection([], "preSet")).toContain( CUSTOM_FLOW_VALIDATION_ERRORS.LAST_STEP_MUST_BE_PICK_OR_ROLL, ); }); - it("returns SAME_TEAM_MODE_AND_MAP_PICK when same side does MODE_PICK and PICK", () => { + test("returns SAME_TEAM_MODE_AND_MAP_PICK when same side does MODE_PICK and PICK", () => { const steps = [ { action: "MODE_PICK" as const, side: "ALPHA" as const }, { action: "PICK" as const, side: "ALPHA" as const }, @@ -248,7 +249,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("returns SAME_TEAM_MODE_AND_MAP_PICK even with bans between", () => { + test("returns SAME_TEAM_MODE_AND_MAP_PICK even with bans between", () => { const steps = [ { action: "MODE_PICK" as const, side: "HIGHER_SEED" as const }, { action: "BAN" as const, side: "LOWER_SEED" as const }, @@ -260,7 +261,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("does not return SAME_TEAM_MODE_AND_MAP_PICK when different sides", () => { + test("does not return SAME_TEAM_MODE_AND_MAP_PICK when different sides", () => { const steps = [ { action: "MODE_PICK" as const, side: "ALPHA" as const }, { action: "PICK" as const, side: "BRAVO" as const }, @@ -271,7 +272,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("does not return SAME_TEAM_MODE_AND_MAP_PICK for MODE_PICK followed by ROLL", () => { + test("does not return SAME_TEAM_MODE_AND_MAP_PICK for MODE_PICK followed by ROLL", () => { const steps = [ { action: "MODE_PICK" as const, side: "ALPHA" as const }, { action: "ROLL" as const }, @@ -282,7 +283,7 @@ describe("validateCustomFlowSection", () => { ); }); - it("can return multiple errors at once", () => { + test("can return multiple errors at once", () => { const steps = [ { action: "MODE_PICK" as const, side: "WINNER" as const }, { action: "MODE_PICK" as const, side: "LOSER" as const }, @@ -312,7 +313,7 @@ describe("resolveCurrentStep", () => { { action: "PICK" as const, side: "LOSER" as const }, ]; - it("returns preSet steps when eventCount < preSet.length", () => { + test("returns preSet steps when eventCount < preSet.length", () => { expect( resolveCurrentStep({ eventCount: 0, preSet, postGame, resultsCount: 0 }), ).toEqual(preSet[0]); @@ -324,13 +325,13 @@ describe("resolveCurrentStep", () => { ).toEqual(preSet[2]); }); - it("returns null when waiting for game result after preSet", () => { + test("returns null when waiting for game result after preSet", () => { expect( resolveCurrentStep({ eventCount: 3, preSet, postGame, resultsCount: 0 }), ).toBeNull(); }); - it("throws when postGame is empty", () => { + test("throws when postGame is empty", () => { expect(() => resolveCurrentStep({ eventCount: 3, @@ -341,7 +342,7 @@ describe("resolveCurrentStep", () => { ).toThrow(); }); - it("returns postGame steps after first game result", () => { + test("returns postGame steps after first game result", () => { expect( resolveCurrentStep({ eventCount: 3, preSet, postGame, resultsCount: 1 }), ).toEqual(postGame[0]); @@ -350,13 +351,13 @@ describe("resolveCurrentStep", () => { ).toEqual(postGame[1]); }); - it("returns null when waiting for next game result after postGame cycle", () => { + test("returns null when waiting for next game result after postGame cycle", () => { expect( resolveCurrentStep({ eventCount: 5, preSet, postGame, resultsCount: 1 }), ).toBeNull(); }); - it("cycles postGame steps after subsequent results", () => { + test("cycles postGame steps after subsequent results", () => { expect( resolveCurrentStep({ eventCount: 5, preSet, postGame, resultsCount: 2 }), ).toEqual(postGame[0]); @@ -371,120 +372,50 @@ describe("resolveTeamFromSide", () => { { id: 100, seed: 2 }, { id: 200, seed: 1 }, ]; + // the same two teams the other way around, so seed-based sides cannot pass + // by reading an array position + const swappedTeams: [PickBanTeam, PickBanTeam] = [teams[1], teams[0]]; - it("resolves ALPHA to teams[0]", () => { - expect(resolveTeamFromSide({ side: "ALPHA", teams, results: [] })).toBe( - 100, - ); + test.each<{ + side: WhoSide; + args: Omit[0]>, "teams"> & { + teams: [PickBanTeam, PickBanTeam]; + }; + expected: number; + }>([ + { side: "ALPHA", args: { teams }, expected: 100 }, + { side: "BRAVO", args: { teams }, expected: 200 }, + { side: "HIGHER_SEED", args: { teams }, expected: 200 }, + { side: "HIGHER_SEED", args: { teams: swappedTeams }, expected: 200 }, + { side: "LOWER_SEED", args: { teams }, expected: 100 }, + { side: "LOWER_SEED", args: { teams: swappedTeams }, expected: 100 }, + { + side: "WINNER", + args: { teams, results: [{ winnerTeamId: 200 }] }, + expected: 200, + }, + { + side: "LOSER", + args: { teams, results: [{ winnerTeamId: 200 }] }, + expected: 100, + }, + { side: "RANDOM", args: { teams, randomTeamIndex: 0 }, expected: 100 }, + { side: "RANDOM", args: { teams, randomTeamIndex: 1 }, expected: 200 }, + { + side: "RANDOM_OTHER", + args: { teams, randomTeamIndex: 0 }, + expected: 200, + }, + { + side: "RANDOM_OTHER", + args: { teams, randomTeamIndex: 1 }, + expected: 100, + }, + ])("resolves $side to $expected", ({ side, args, expected }) => { + expect(resolveTeamFromSide({ side, results: [], ...args })).toBe(expected); }); - it("resolves BRAVO to teams[1]", () => { - expect(resolveTeamFromSide({ side: "BRAVO", teams, results: [] })).toBe( - 200, - ); - }); - - it("resolves HIGHER_SEED to teams[1]", () => { - expect( - resolveTeamFromSide({ side: "HIGHER_SEED", teams, results: [] }), - ).toBe(200); - }); - - it("resolves LOWER_SEED to teams[0]", () => { - expect( - resolveTeamFromSide({ side: "LOWER_SEED", teams, results: [] }), - ).toBe(100); - }); - - it("resolves HIGHER_SEED by seed, not array position", () => { - const swappedTeams: [PickBanTeam, PickBanTeam] = [ - { id: 200, seed: 1 }, - { id: 100, seed: 2 }, - ]; - - expect( - resolveTeamFromSide({ - side: "HIGHER_SEED", - teams: swappedTeams, - results: [], - }), - ).toBe(200); - }); - - it("resolves LOWER_SEED by seed, not array position", () => { - const swappedTeams: [PickBanTeam, PickBanTeam] = [ - { id: 200, seed: 1 }, - { id: 100, seed: 2 }, - ]; - - expect( - resolveTeamFromSide({ - side: "LOWER_SEED", - teams: swappedTeams, - results: [], - }), - ).toBe(100); - }); - - it("resolves WINNER to last game winner", () => { - expect( - resolveTeamFromSide({ - side: "WINNER", - teams, - results: [{ winnerTeamId: 200 }], - }), - ).toBe(200); - }); - - it("resolves LOSER to last game loser", () => { - expect( - resolveTeamFromSide({ - side: "LOSER", - teams, - results: [{ winnerTeamId: 200 }], - }), - ).toBe(100); - }); - - it("resolves RANDOM to the coin-flip team index", () => { - expect( - resolveTeamFromSide({ - side: "RANDOM", - teams, - results: [], - randomTeamIndex: 0, - }), - ).toBe(100); - expect( - resolveTeamFromSide({ - side: "RANDOM", - teams, - results: [], - randomTeamIndex: 1, - }), - ).toBe(200); - }); - - it("resolves RANDOM_OTHER to the complement of the coin flip", () => { - expect( - resolveTeamFromSide({ - side: "RANDOM_OTHER", - teams, - results: [], - randomTeamIndex: 0, - }), - ).toBe(200); - expect( - resolveTeamFromSide({ - side: "RANDOM_OTHER", - teams, - results: [], - randomTeamIndex: 1, - }), - ).toBe(100); - }); - - it("throws when RANDOM side is missing randomTeamIndex", () => { + test("throws when RANDOM side is missing randomTeamIndex", () => { expect(() => resolveTeamFromSide({ side: "RANDOM", teams, results: [] }), ).toThrow(); @@ -492,7 +423,7 @@ describe("resolveTeamFromSide", () => { }); describe("randomWhoTeamIndex", () => { - it("is deterministic for the same match and draw group", () => { + test("is deterministic for the same match and draw group", () => { const args = { matchId: 42, eventIndex: 0, @@ -503,7 +434,7 @@ describe("randomWhoTeamIndex", () => { expect(randomWhoTeamIndex(args)).toBe(randomWhoTeamIndex(args)); }); - it("returns 0 or 1", () => { + test("returns 0 or 1", () => { for (let matchId = 1; matchId <= 20; matchId++) { const result = randomWhoTeamIndex({ matchId, @@ -515,7 +446,7 @@ describe("randomWhoTeamIndex", () => { } }); - it("shares a single flip across all pre-set steps", () => { + test("shares a single flip across all pre-set steps", () => { const base = { matchId: 7, preSetLength: 3, postGameLength: 2 }; const draw0 = randomWhoTeamIndex({ ...base, eventIndex: 0 }); const draw1 = randomWhoTeamIndex({ ...base, eventIndex: 1 }); @@ -525,7 +456,7 @@ describe("randomWhoTeamIndex", () => { expect(draw2).toBe(draw0); }); - it("shares a single flip within a post-game cycle but re-keys per map", () => { + test("shares a single flip within a post-game cycle but re-keys per map", () => { const base = { matchId: 7, preSetLength: 3, postGameLength: 2 }; // cycle 0 => eventIndex 3, 4 const cycle0a = randomWhoTeamIndex({ ...base, eventIndex: 3 }); @@ -538,7 +469,7 @@ describe("randomWhoTeamIndex", () => { expect(cycle1b).toBe(cycle1a); }); - it("re-flips independently across maps (some match reflips)", () => { + test("re-flips independently across maps (some match reflips)", () => { const differs = Array.from({ length: 30 }, (_, i) => i + 1).some( (matchId) => { const base = { matchId, preSetLength: 0, postGameLength: 1 }; @@ -572,7 +503,7 @@ describe("turnOf / teamOfEvent — RANDOM sides", () => { ]; const teamIds = [100, 200]; - it("resolves RANDOM to one of the two teams", () => { + test("resolves RANDOM to one of the two teams", () => { const result = turnOf({ matchId: 55, results: [], @@ -585,7 +516,7 @@ describe("turnOf / teamOfEvent — RANDOM sides", () => { expect(result?.action).toBe("BAN"); }); - it("resolves RANDOM_OTHER to the complement of RANDOM within the same pre-set", () => { + test("resolves RANDOM_OTHER to the complement of RANDOM within the same pre-set", () => { const randomTurn = turnOf({ matchId: 55, results: [], @@ -605,7 +536,7 @@ describe("turnOf / teamOfEvent — RANDOM sides", () => { expect(teamIds).toContain(randomOtherTurn?.teamId); }); - it("keeps RANDOM stable across steps sharing a draw group", () => { + test("keeps RANDOM stable across steps sharing a draw group", () => { const firstRandom = turnOf({ matchId: 55, results: [], @@ -624,7 +555,7 @@ describe("turnOf / teamOfEvent — RANDOM sides", () => { expect(secondRandom?.teamId).toBe(firstRandom?.teamId); }); - it("is deterministic across repeated calls", () => { + test("is deterministic across repeated calls", () => { const first = turnOf({ matchId: 55, results: [], @@ -643,7 +574,7 @@ describe("turnOf / teamOfEvent — RANDOM sides", () => { expect(second).toEqual(first); }); - it("teamOfEvent agrees with the pending turnOf resolution for the same event", () => { + test("teamOfEvent agrees with the pending turnOf resolution for the same event", () => { for (const eventIndex of [0, 1, 2]) { const pending = turnOf({ matchId: 55, @@ -687,7 +618,7 @@ describe("turnOf — CUSTOM flow", () => { { id: 200, seed: 1 }, ]; - it("returns first preSet step", () => { + test("returns first preSet step", () => { const result = turnOf({ matchId: 1, results: [], @@ -704,7 +635,7 @@ describe("turnOf — CUSTOM flow", () => { }); }); - it("returns second preSet step", () => { + test("returns second preSet step", () => { const result = turnOf({ matchId: 1, results: [], @@ -721,7 +652,7 @@ describe("turnOf — CUSTOM flow", () => { }); }); - it("returns null when waiting for game result", () => { + test("returns null when waiting for game result", () => { const result = turnOf({ matchId: 1, results: [], @@ -733,7 +664,7 @@ describe("turnOf — CUSTOM flow", () => { expect(result).toBeNull(); }); - it("returns postGame step after result", () => { + test("returns postGame step after result", () => { const result = turnOf({ matchId: 1, results: [{ winnerTeamId: 200 }], @@ -750,7 +681,7 @@ describe("turnOf — CUSTOM flow", () => { }); }); - it("returns null for ROLL steps", () => { + test("returns null for ROLL steps", () => { const rollMaps: TournamentRoundMaps = { count: 3, type: "BEST_OF", @@ -772,7 +703,7 @@ describe("turnOf — CUSTOM flow", () => { expect(result).toBeNull(); }); - it("returns null when set is over", () => { + test("returns null when set is over", () => { const result = turnOf({ matchId: 1, results: [ @@ -788,7 +719,7 @@ describe("turnOf — CUSTOM flow", () => { expect(result).toBeNull(); }); - it("returns null when no customFlow defined", () => { + test("returns null when no customFlow defined", () => { const result = turnOf({ matchId: 1, results: [], @@ -807,7 +738,7 @@ describe("turnOf — CUSTOM flow stepCurrent/stepTotal", () => { { id: 200, seed: 1 }, ]; - it("counts consecutive bans by same side in preSet", () => { + test("counts consecutive bans by same side in preSet", () => { const maps: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -836,7 +767,7 @@ describe("turnOf — CUSTOM flow stepCurrent/stepTotal", () => { ).toMatchObject({ stepCurrent: 1, stepTotal: 1 }); }); - it("counts consecutive bans by same side in postGame", () => { + test("counts consecutive bans by same side in postGame", () => { const maps: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -882,7 +813,7 @@ describe("turnOf — CUSTOM flow stepCurrent/stepTotal", () => { ).toMatchObject({ stepCurrent: 1, stepTotal: 1 }); }); - it("does not group consecutive steps with different sides", () => { + test("does not group consecutive steps with different sides", () => { const maps: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -906,7 +837,7 @@ describe("turnOf — CUSTOM flow stepCurrent/stepTotal", () => { ).toMatchObject({ stepCurrent: 1, stepTotal: 1 }); }); - it("does not group consecutive steps with different actions", () => { + test("does not group consecutive steps with different actions", () => { const maps: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -942,7 +873,7 @@ describe("turnOf — BAN_2 flow", () => { { id: 200, seed: 1 }, ]; - it("returns action BAN for first picker", () => { + test("returns action BAN for first picker", () => { const result = turnOf({ matchId: 1, results: [], @@ -967,7 +898,7 @@ describe("turnOf — BAN_2 flow", () => { expect(result).toEqual({ teamId: 200, action: "BAN" }); }); - it("returns null when both teams have banned", () => { + test("returns null when both teams have banned", () => { const result = turnOf({ matchId: 1, results: [], @@ -1015,7 +946,7 @@ describe("mapsListWithLegality — MODE_PICK restriction survives intervening ev }, }; - it("restricts to picked mode even when bans happen after MODE_PICK", () => { + test("restricts to picked mode even when bans happen after MODE_PICK", () => { const pickBanEvents: PickBanEvent[] = [ { type: "MODE_PICK", stageId: null, mode: SZ }, { type: "BAN", stageId: 3 as StageId, mode: TC }, @@ -1044,7 +975,7 @@ describe("mapsListWithLegality — MODE_PICK restriction survives intervening ev expect(legalModes.has(RM)).toBe(false); }); - it("does not carry MODE_PICK restriction from a previous game section", () => { + test("does not carry MODE_PICK restriction from a previous game section", () => { const mapsWithPostGameModePick: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -1121,7 +1052,7 @@ describe("mapsListWithLegality — pre-set MODE_PICK only restricts the first ma }, }; - it("does not lock the second map's mode to the pre-set MODE_PICK", () => { + test("does not lock the second map's mode to the pre-set MODE_PICK", () => { // preSet: HIGHER_SEED picks mode SZ, LOWER_SEED picks SZ stage 1 // game 1: SZ stage 1 played, team 200 wins // postGame cycle 1: LOSER (100) is now at PICK for game 2's map @@ -1179,7 +1110,7 @@ describe("mapsListWithLegality — PICK_NO_MODE_REPEAT", () => { }, }; - it("excludes modes already played in the set", () => { + test("excludes modes already played in the set", () => { // game 1: SZ stage 1 played, now at PICK_NO_MODE_REPEAT for game 2's map const pickBanEvents: PickBanEvent[] = [ { type: "PICK", stageId: 1 as StageId, mode: SZ }, @@ -1206,7 +1137,7 @@ describe("mapsListWithLegality — PICK_NO_MODE_REPEAT", () => { expect(legalModes.has(RM)).toBe(true); }); - it("falls back to allowing every mode once all modes have been played", () => { + test("falls back to allowing every mode once all modes have been played", () => { // every mode has been played, but each still has an unplayed stage; a mode // repeat is now unavoidable so the restriction lifts and the remaining // stages of already-played modes become legal again @@ -1273,7 +1204,7 @@ describe("mapsListWithLegality — pre-set MODE_BAN persists into postGame", () }, }; - it("keeps a mode banned in pre-set unavailable for picks in later postGame cycles", () => { + test("keeps a mode banned in pre-set unavailable for picks in later postGame cycles", () => { // preSet: HIGHER_SEED bans mode SZ, ROLL lands on TC stage 3 // game 1: TC stage 3 played, team 200 wins // postGame cycle 1: WINNER (200) bans stage 4 (TC); LOSER (100) is now at PICK @@ -1344,7 +1275,7 @@ describe("isModeLegal", () => { toSetMapPool, }; - it("returns true for a mode present in the pool with no bans", () => { + test("returns true for a mode present in the pool with no bans", () => { expect( isModeLegal({ mode: TC, @@ -1354,7 +1285,7 @@ describe("isModeLegal", () => { ).toBe(true); }); - it("returns false for a mode that has been banned", () => { + test("returns false for a mode that has been banned", () => { const pickBanEvents: PickBanEvent[] = [ { type: "MODE_BAN", stageId: null, mode: TC }, ]; @@ -1368,7 +1299,7 @@ describe("isModeLegal", () => { ).toBe(false); }); - it("returns false for a mode not in the map pool", () => { + test("returns false for a mode not in the map pool", () => { expect( isModeLegal({ mode: CB, @@ -1390,7 +1321,7 @@ describe("turnOf — COUNTERPICK flow", () => { { id: 200, seed: 1 }, ]; - it("returns action PICK for loser of last game", () => { + test("returns action PICK for loser of last game", () => { const result = turnOf({ matchId: 1, results: [{ winnerTeamId: 200 }], @@ -1409,7 +1340,7 @@ describe("turnOf — COUNTERPICK flow", () => { expect(result).toEqual({ teamId: 100, action: "PICK" }); }); - it("returns null when match was completed without per-game results (drop-out)", () => { + test("returns null when match was completed without per-game results (drop-out)", () => { const result = turnOf({ matchId: 1, results: [], @@ -1428,7 +1359,7 @@ describe("teamOfEvent", () => { { id: 200, seed: 1 }, ]; - it("returns null when setup is not pick/ban", () => { + test("returns null when setup is not pick/ban", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 0, @@ -1447,7 +1378,7 @@ describe("teamOfEvent", () => { pickBan: "BAN_2", }; - it("assigns event 0 to teams[1] (first picker)", () => { + test("assigns event 0 to teams[1] (first picker)", () => { expect( teamOfEvent({ matchId: 1, @@ -1459,7 +1390,7 @@ describe("teamOfEvent", () => { ).toBe(200); }); - it("assigns event 1 to teams[0] (second picker)", () => { + test("assigns event 1 to teams[0] (second picker)", () => { expect( teamOfEvent({ matchId: 1, @@ -1471,7 +1402,7 @@ describe("teamOfEvent", () => { ).toBe(100); }); - it("returns null for further indices", () => { + test("returns null for further indices", () => { expect( teamOfEvent({ matchId: 1, @@ -1491,7 +1422,7 @@ describe("teamOfEvent", () => { pickBan: "COUNTERPICK", }; - it("attributes the counterpick to the loser of the preceding result", () => { + test("attributes the counterpick to the loser of the preceding result", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 0, @@ -1503,7 +1434,7 @@ describe("teamOfEvent", () => { expect(result).toBe(200); }); - it("also works for COUNTERPICK_MODE_REPEAT_OK", () => { + test("also works for COUNTERPICK_MODE_REPEAT_OK", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 1, @@ -1515,7 +1446,7 @@ describe("teamOfEvent", () => { expect(result).toBe(100); }); - it("returns null when no corresponding result exists", () => { + test("returns null when no corresponding result exists", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 0, @@ -1545,7 +1476,7 @@ describe("teamOfEvent", () => { }, }; - it("resolves preSet steps via side (HIGHER_SEED → teams[1])", () => { + test("resolves preSet steps via side (HIGHER_SEED → teams[1])", () => { expect( teamOfEvent({ matchId: 1, @@ -1557,7 +1488,7 @@ describe("teamOfEvent", () => { ).toBe(200); }); - it("resolves preSet steps via side (LOWER_SEED → teams[0])", () => { + test("resolves preSet steps via side (LOWER_SEED → teams[0])", () => { expect( teamOfEvent({ matchId: 1, @@ -1569,7 +1500,7 @@ describe("teamOfEvent", () => { ).toBe(100); }); - it("resolves postGame WINNER using the result of that cycle", () => { + test("resolves postGame WINNER using the result of that cycle", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 2, @@ -1581,7 +1512,7 @@ describe("teamOfEvent", () => { expect(result).toBe(100); }); - it("resolves postGame LOSER using the result of that cycle", () => { + test("resolves postGame LOSER using the result of that cycle", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 3, @@ -1593,7 +1524,7 @@ describe("teamOfEvent", () => { expect(result).toBe(200); }); - it("uses the correct cycle's result across multiple post-game cycles", () => { + test("uses the correct cycle's result across multiple post-game cycles", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 4, @@ -1605,7 +1536,7 @@ describe("teamOfEvent", () => { expect(result).toBe(200); }); - it("returns null when customFlow is missing", () => { + test("returns null when customFlow is missing", () => { const result = teamOfEvent({ matchId: 1, eventIndex: 0, @@ -1617,7 +1548,7 @@ describe("teamOfEvent", () => { expect(result).toBeNull(); }); - it("returns null for ROLL steps (no side)", () => { + test("returns null for ROLL steps (no side)", () => { const rollMaps: TournamentRoundMaps = { count: 3, type: "BEST_OF", @@ -1647,7 +1578,7 @@ describe("currentTurnSessionStartedAt", () => { { id: 200, seed: 1 }, ]; - it("returns null when there is no current turn", () => { + test("returns null when there is no current turn", () => { const result = currentTurnSessionStartedAt({ matchId: 1, currentTurn: null, @@ -1661,7 +1592,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBeNull(); }); - it("returns null when matchStartedAt is null", () => { + test("returns null when matchStartedAt is null", () => { const result = currentTurnSessionStartedAt({ matchId: 1, currentTurn: { teamId: 200, action: "BAN" }, @@ -1675,7 +1606,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBeNull(); }); - it("falls back to matchStartedAt when no events or results exist", () => { + test("falls back to matchStartedAt when no events or results exist", () => { const result = currentTurnSessionStartedAt({ matchId: 1, currentTurn: { teamId: 200, action: "BAN" }, @@ -1689,7 +1620,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBe(1000); }); - it("BAN_2: second banner's session starts at the first ban's timestamp", () => { + test("BAN_2: second banner's session starts at the first ban's timestamp", () => { const result = currentTurnSessionStartedAt({ matchId: 1, currentTurn: { teamId: 100, action: "BAN" }, @@ -1703,7 +1634,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBe(1500); }); - it("COUNTERPICK: loser's session starts when the result is reported", () => { + test("COUNTERPICK: loser's session starts when the result is reported", () => { const result = currentTurnSessionStartedAt({ matchId: 1, currentTurn: { teamId: 200, action: "PICK" }, @@ -1717,7 +1648,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBe(2000); }); - it("CUSTOM: consecutive same-team events share the session start", () => { + test("CUSTOM: consecutive same-team events share the session start", () => { const customMaps: TournamentRoundMaps = { count: 5, type: "BEST_OF", @@ -1745,7 +1676,7 @@ describe("currentTurnSessionStartedAt", () => { expect(result).toBe(1000); }); - it("CUSTOM: a result restarts the session even when the same team is responsible again", () => { + test("CUSTOM: a result restarts the session even when the same team is responsible again", () => { const customMaps: TournamentRoundMaps = { count: 5, type: "BEST_OF", diff --git a/app/features/tournament-bracket/core/Progression.test.ts b/app/features/tournament-bracket/core/Progression.test.ts index 13f7909c6..fe1c61050 100644 --- a/app/features/tournament-bracket/core/Progression.test.ts +++ b/app/features/tournament-bracket/core/Progression.test.ts @@ -1,437 +1,221 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as Progression from "./Progression"; import { progressions } from "./tests/test-utils"; +type BracketInput = Omit< + Progression.InputBracket, + "id" | "name" | "requiresCheckIn" +> & { name?: string }; + +const ROUND_ROBIN: BracketInput = { type: "round_robin", settings: {} }; +const ROUND_ROBIN_OF_8: BracketInput = { + type: "round_robin", + settings: { teamsPerGroup: 8 }, +}; +const SINGLE_ELIMINATION: BracketInput = { + type: "single_elimination", + settings: {}, +}; +const DOUBLE_ELIMINATION: BracketInput = { + type: "double_elimination", + settings: {}, +}; +const SWISS: BracketInput = { type: "swiss", settings: {} }; +const SWISS_EARLY_ADVANCE: BracketInput = { + type: "swiss", + settings: { advanceThreshold: 3 }, +}; + +/** Fills in the ids and names `validatedBrackets` needs so tests only state what they vary. Bracket N is referenced as source bracketId `"N"`. */ +const getValidatedBrackets = (brackets: BracketInput[]) => + Progression.validatedBrackets( + brackets.map((bracket, i) => ({ + id: String(i), + name: bracket.name ?? `Bracket ${i + 1}`, + requiresCheckIn: false, + ...bracket, + })), + ); + +/** A single elimination bracket taking `placements` from one source bracket. */ +const getValidatedBracketsFromPlacements = ( + placements: string, + source: BracketInput = ROUND_ROBIN, +) => + getValidatedBrackets([ + source, + { + type: "single_elimination", + settings: {}, + sources: [{ bracketId: "0", placements }], + }, + ]); + describe("bracketsToValidationError - valid formats", () => { - it("accepts SE", () => { - expect( - Progression.bracketsToValidationError(progressions.singleElimination), - ).toBeNull(); - }); - - it("accepts RR->SE", () => { - expect( - Progression.bracketsToValidationError( - progressions.roundRobinToSingleElimination, - ), - ).toBeNull(); - }); - - it("accepts low ink", () => { - expect( - Progression.bracketsToValidationError(progressions.lowInk), - ).toBeNull(); - }); - - it("accepts many starter brackets", () => { - expect( - Progression.bracketsToValidationError(progressions.manyStartBrackets), - ).toBeNull(); - }); - - it("accepts swiss (one group)", () => { - expect( - Progression.bracketsToValidationError(progressions.swissOneGroup), - ).toBeNull(); - }); - - it("accepts a bracket with many source brackets", () => { - expect( - Progression.bracketsToValidationError(progressions.multiSourceTopCut), - ).toBeNull(); + test.each([ + ["SE", progressions.singleElimination], + ["RR->SE", progressions.roundRobinToSingleElimination], + ["low ink", progressions.lowInk], + ["many starter brackets", progressions.manyStartBrackets], + ["swiss (one group)", progressions.swissOneGroup], + ["a bracket with many source brackets", progressions.multiSourceTopCut], + ] as const)("accepts %s", (_, progression) => { + expect(Progression.bracketsToValidationError(progression)).toBeNull(); }); }); -describe("validatedSources - PLACEMENTS_PARSE_ERROR", () => { - const getValidatedBracketsFromPlacements = (placements: string) => { - return Progression.validatedBrackets([ - { - id: "1", - name: "Bracket 1", - type: "round_robin", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Bracket 2", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements, - }, - ], - }, - ]); - }; +describe("validatedSources - placements parsing", () => { + const PARSED: { + why: string; + placements: string; + expected: Progression.DBSource; + source?: BracketInput; + }[] = [ + { + why: "comma separated", + placements: "1,2,3,4", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4] }, + }, + { + why: "a range", + placements: "1-4", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4] }, + }, + { + why: "a mix of ranges and commas", + placements: "1,2,3-4", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4] }, + }, + { + why: "extra white space", + placements: "1, 2, 3,4 ", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4] }, + }, + { + why: "a range whose start and end are the same", + placements: "1-1", + expected: { bracketIdx: 0, placements: [1] }, + }, + { + why: "negative placements", + placements: "-1,-2", + expected: { bracketIdx: 0, placements: [-1, -2] }, + source: DOUBLE_ELIMINATION, + }, + { + why: "empty placements from a Swiss bracket with early advance", + placements: "", + expected: { bracketIdx: 0, placements: [] }, + source: SWISS_EARLY_ADVANCE, + }, + { + why: "a lone rest placement", + placements: "5+", + expected: { bracketIdx: 0, placements: [5], rest: true }, + source: ROUND_ROBIN_OF_8, + }, + { + why: "rest from the first placement", + placements: "1+", + expected: { bracketIdx: 0, placements: [1], rest: true }, + source: ROUND_ROBIN_OF_8, + }, + { + why: "rest combined with explicit placements", + placements: "1,2,3-4,5+", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true }, + source: ROUND_ROBIN_OF_8, + }, + { + why: "a range followed by rest, as a single element", + placements: "1-5+", + expected: { bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true }, + source: ROUND_ROBIN_OF_8, + }, + ]; - it("parses placements correctly (separated by comma)", () => { + test.each(PARSED)("parses $why", ({ placements, expected, source }) => { const result = getValidatedBracketsFromPlacements( - "1,2,3,4", + placements, + source, ) as Progression.ParsedBracket[]; - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4] }, - ]); + expect(result[1].sources).toEqual([expected]); }); - it("parses placements correctly (separated by line)", () => { - const result = getValidatedBracketsFromPlacements( - "1-4", - ) as Progression.ParsedBracket[]; + const PARSE_ERRORS: { + why: string; + placements: string; + source?: BracketInput; + }[] = [ + { why: "invalid characters", placements: "1st,2nd,3rd,4th" }, + { why: "a zero placement", placements: "0", source: DOUBLE_ELIMINATION }, + { why: "empty placements from a non-Swiss bracket", placements: "" }, + { + why: "empty placements from a Swiss bracket without early advance", + placements: "", + source: SWISS, + }, + { + why: "a reversed range", + placements: "3-1", + source: SWISS_EARLY_ADVANCE, + }, + { + why: "rest in a non-final position", + placements: "5+,6", + source: ROUND_ROBIN_OF_8, + }, + { why: "a double plus", placements: "5++", source: ROUND_ROBIN_OF_8 }, + { why: "a lone plus", placements: "+", source: ROUND_ROBIN_OF_8 }, + { + why: "rest on a zero placement", + placements: "0+", + source: ROUND_ROBIN_OF_8, + }, + { + why: "rest on a negative placement", + placements: "-1+", + source: DOUBLE_ELIMINATION, + }, + ]; - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4] }, - ]); - }); + test.each(PARSE_ERRORS)( + "flags PLACEMENTS_PARSE_ERROR on $why", + ({ placements, source }) => { + const error = getValidatedBracketsFromPlacements( + placements, + source, + ) as Progression.ValidationError; - it("parses placements correctly (separated by a mix)", () => { - const result = getValidatedBracketsFromPlacements( - "1,2,3-4", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4] }, - ]); - }); - - it("handles placement where ranges start and end is the same", () => { - const result = getValidatedBracketsFromPlacements( - "1-1", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([{ bracketIdx: 0, placements: [1] }]); - }); - - it("handles parsing with extra white space", () => { - const result = getValidatedBracketsFromPlacements( - "1, 2, 3,4 ", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4] }, - ]); - }); - - it("handles parsing with negative placements", () => { - const result = Progression.validatedBrackets([ - { - id: "1", - name: "Bracket 1", - type: "double_elimination", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Bracket 2", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "-1,-2", - }, - ], - }, - ]) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [-1, -2] }, - ]); - }); - - it("parsing fails if invalid characters", () => { - const error = getValidatedBracketsFromPlacements( - "1st,2nd,3rd,4th", - ) as Progression.ValidationError; - - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("parsing fails if zero placement", () => { - const error = Progression.validatedBrackets([ - { - id: "1", - name: "Bracket 1", - type: "double_elimination", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Bracket 2", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "0", - }, - ], - }, - ]) as Progression.ValidationError; - - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("allows empty string placements for Swiss brackets with early advance", () => { - const result = Progression.validatedBrackets([ - { - id: "1", - name: "Swiss Bracket", - type: "swiss", - settings: { - advanceThreshold: 3, - }, - requiresCheckIn: false, - }, - { - id: "2", - name: "Final Bracket", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "", - }, - ], - }, - ]) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([{ bracketIdx: 0, placements: [] }]); - }); - - it("parsing fails with empty string placements for non-Swiss brackets", () => { - const error = Progression.validatedBrackets([ - { - id: "1", - name: "Round Robin Bracket", - type: "round_robin", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Final Bracket", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "", - }, - ], - }, - ]) as Progression.ValidationError; - - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("parsing fails with a reversed placement range", () => { - const error = Progression.validatedBrackets([ - { - id: "1", - name: "Swiss Bracket", - type: "swiss", - settings: { - advanceThreshold: 3, - }, - requiresCheckIn: false, - }, - { - id: "2", - name: "Final Bracket", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "3-1", - }, - ], - }, - ]) as Progression.ValidationError; - - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("parsing fails with empty string placements for Swiss brackets without early advance", () => { - const error = Progression.validatedBrackets([ - { - id: "1", - name: "Swiss Bracket", - type: "swiss", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Final Bracket", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "", - }, - ], - }, - ]) as Progression.ValidationError; - - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); + expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); + }, + ); }); describe('validatedSources - rest "N+" syntax', () => { - const getValidatedBracketsFromPlacements = (placements: string) => { - return Progression.validatedBrackets([ - { - id: "1", - name: "Bracket 1", - type: "round_robin", - settings: { teamsPerGroup: 8 }, - requiresCheckIn: false, - }, - { - id: "2", - name: "Bracket 2", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements, - }, - ], - }, - ]); - }; - - it("parses lone rest placement", () => { - const result = getValidatedBracketsFromPlacements( - "5+", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [5], rest: true }, - ]); - }); - - it("parses rest from first placement", () => { - const result = getValidatedBracketsFromPlacements( - "1+", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1], rest: true }, - ]); - }); - - it("parses rest combined with explicit placements", () => { - const result = getValidatedBracketsFromPlacements( - "1,2,3-4,5+", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true }, - ]); - }); - - it("parses range-then-rest as a single element", () => { - const result = getValidatedBracketsFromPlacements( - "1-5+", - ) as Progression.ParsedBracket[]; - - expect(result[1].sources).toEqual([ - { bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true }, - ]); - }); - - it("rejects rest in non-final position", () => { - const error = getValidatedBracketsFromPlacements( - "5+,6", - ) as Progression.ValidationError; - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("rejects double plus", () => { - const error = getValidatedBracketsFromPlacements( - "5++", - ) as Progression.ValidationError; - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("rejects lone plus", () => { - const error = getValidatedBracketsFromPlacements( - "+", - ) as Progression.ValidationError; - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("rejects rest on zero placement", () => { - const error = getValidatedBracketsFromPlacements( - "0+", - ) as Progression.ValidationError; - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("rejects rest on negative placement", () => { - const error = Progression.validatedBrackets([ - { - id: "1", - name: "Bracket 1", - type: "double_elimination", - settings: {}, - requiresCheckIn: false, - }, - { - id: "2", - name: "Bracket 2", - type: "single_elimination", - settings: {}, - requiresCheckIn: false, - sources: [ - { - bracketId: "1", - placements: "-1+", - }, - ], - }, - ]) as Progression.ValidationError; - expect(error.type).toBe("PLACEMENTS_PARSE_ERROR"); - }); - - it("round-trips lone rest via input format", () => { + test("round-trips lone rest via input format", () => { const validated = getValidatedBracketsFromPlacements( "5+", + ROUND_ROBIN_OF_8, ) as Progression.ParsedBracket[]; const inputFormat = Progression.validatedBracketsToInputFormat(validated); expect(inputFormat[1].sources?.[0].placements).toBe("5+"); }); - it("round-trips combined rest via input format", () => { + test("round-trips combined rest via input format", () => { const validated = getValidatedBracketsFromPlacements( "1,2,3-4,5+", + ROUND_ROBIN_OF_8, ) as Progression.ParsedBracket[]; const inputFormat = Progression.validatedBracketsToInputFormat(validated); expect(inputFormat[1].sources?.[0].placements).toBe("1-4,5+"); }); - it("destinationByPlacement routes placements beyond the rest threshold", () => { + test("destinationByPlacement routes placements beyond the rest threshold", () => { const validated = getValidatedBracketsFromPlacements( "5+", + ROUND_ROBIN_OF_8, ) as Progression.ParsedBracket[]; expect( Progression.destinationByPlacement({ @@ -449,12 +233,9 @@ describe('validatedSources - rest "N+" syntax', () => { ).toBe(null); }); - it("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when two rest sources share a bracket", () => { + test("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when two rest sources share a bracket", () => { const error = getValidatedBrackets([ - { - settings: { teamsPerGroup: 8 }, - type: "round_robin", - }, + ROUND_ROBIN_OF_8, { settings: {}, type: "single_elimination", @@ -474,12 +255,9 @@ describe('validatedSources - rest "N+" syntax', () => { expect(error.type).toBe("SAME_PLACEMENT_TO_MULTIPLE_BRACKETS"); }); - it("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when rest overlaps an explicit placement", () => { + test("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when rest overlaps an explicit placement", () => { const error = getValidatedBrackets([ - { - settings: { teamsPerGroup: 8 }, - type: "round_robin", - }, + ROUND_ROBIN_OF_8, { settings: {}, type: "single_elimination", @@ -494,7 +272,7 @@ describe('validatedSources - rest "N+" syntax', () => { expect(error.type).toBe("SAME_PLACEMENT_TO_MULTIPLE_BRACKETS"); }); - it("still flags TOO_MANY_PLACEMENTS when rest's explicit max exceeds teamsPerGroup", () => { + test("still flags TOO_MANY_PLACEMENTS when rest's explicit max exceeds teamsPerGroup", () => { const error = getValidatedBrackets([ { settings: { teamsPerGroup: 4 }, @@ -515,34 +293,14 @@ describe('validatedSources - rest "N+" syntax', () => { }); }); -const getValidatedBrackets = ( - brackets: (Omit< - Progression.InputBracket, - "id" | "name" | "requiresCheckIn" - > & { name?: string })[], -) => - Progression.validatedBrackets( - brackets.map((b, i) => ({ - id: String(i), - name: b.name ?? `Bracket ${i + 1}`, - requiresCheckIn: false, - ...b, - })), - ); - describe("validatedSources - other rules", () => { - it("accepts a single round robin with no follow-ups", () => { - const result = getValidatedBrackets([ - { - settings: {}, - type: "round_robin", - }, - ]); + test("accepts a single round robin with no follow-ups", () => { + const result = getValidatedBrackets([ROUND_ROBIN]); expect(Array.isArray(result)).toBe(true); }); - it("accepts a single A/B round robin with no follow-ups", () => { + test("accepts a single A/B round robin with no follow-ups", () => { const result = getValidatedBrackets([ { settings: { @@ -556,49 +314,33 @@ describe("validatedSources - other rules", () => { expect(Array.isArray(result)).toBe(true); }); - it("accepts a swiss to round robin progression", () => { + test("accepts a swiss to round robin progression", () => { const result = getValidatedBrackets([ - { - settings: {}, - type: "swiss", - }, + SWISS, { settings: {}, type: "round_robin", - sources: [ - { - bracketId: "0", - placements: "1,2", - }, - ], + sources: [{ bracketId: "0", placements: "1,2" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("accepts a round robin to round robin progression", () => { + test("accepts a round robin to round robin progression", () => { const result = getValidatedBrackets([ + ROUND_ROBIN, { settings: {}, type: "round_robin", - }, - { - settings: {}, - type: "round_robin", - sources: [ - { - bracketId: "0", - placements: "1,2", - }, - ], + sources: [{ bracketId: "0", placements: "1,2" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("handles NOT_RESOLVING_WINNER (swiss with many groups)", () => { + test("handles NOT_RESOLVING_WINNER (swiss with many groups)", () => { const error = getValidatedBrackets([ { settings: { @@ -611,31 +353,18 @@ describe("validatedSources - other rules", () => { expect(error.type).toBe("NOT_RESOLVING_WINNER"); }); - it("handles SAME_PLACEMENT_TO_MULTIPLE_BRACKETS", () => { + test("handles SAME_PLACEMENT_TO_MULTIPLE_BRACKETS", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1-2" }], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "2-3", - }, - ], + sources: [{ bracketId: "0", placements: "2-3" }], }, ]) as Progression.ValidationError; @@ -643,31 +372,18 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdxs).toEqual([1, 2]); }); - it("handles GAP_IN_PLACEMENTS", () => { + test("handles GAP_IN_PLACEMENTS", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1" }], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "3", - }, - ], + sources: [{ bracketId: "0", placements: "3" }], }, ]) as Progression.ValidationError; @@ -675,41 +391,23 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdxs).toEqual([1, 2]); }); - it("only flags GAP_IN_PLACEMENTS brackets sourcing from the problematic bracket", () => { + test("only flags GAP_IN_PLACEMENTS brackets sourcing from the problematic bracket", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1" }], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1", - }, - ], + sources: [{ bracketId: "0", placements: "3" }], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "3", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "1", - placements: "1", - }, - ], + sources: [{ bracketId: "1", placements: "1" }], }, ]) as Progression.ValidationError; @@ -718,7 +416,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdxs).toEqual([1, 2]); }); - it("handles TOO_MANY_PLACEMENTS", () => { + test("handles TOO_MANY_PLACEMENTS", () => { const error = getValidatedBrackets([ { settings: { @@ -729,12 +427,7 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1,2,3,4,5", - }, - ], + sources: [{ bracketId: "0", placements: "1,2,3,4,5" }], }, ]) as Progression.ValidationError; @@ -742,7 +435,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("handles PLACEMENT_TOO_HIGH", () => { + test("handles PLACEMENT_TOO_HIGH", () => { const error = getValidatedBrackets([ { settings: { teamsPerGroup: 200 }, @@ -759,7 +452,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("does not flag PLACEMENT_TOO_HIGH at the max boundary", () => { + test("does not flag PLACEMENT_TOO_HIGH at the max boundary", () => { const result = getValidatedBrackets([ { settings: { teamsPerGroup: 200 }, @@ -775,7 +468,7 @@ describe("validatedSources - other rules", () => { expect(Array.isArray(result)).toBe(true); }); - it("does not flag TOO_MANY_PLACEMENTS when larger round robin has valid high placements", () => { + test("does not flag TOO_MANY_PLACEMENTS when larger round robin has valid high placements", () => { const result = getValidatedBrackets([ { settings: { teamsPerGroup: 6 }, @@ -795,7 +488,7 @@ describe("validatedSources - other rules", () => { expect(Array.isArray(result)).toBe(true); }); - it("flags TOO_MANY_PLACEMENTS on A/B divisions when placement exceeds per-division size", () => { + test("flags TOO_MANY_PLACEMENTS on A/B divisions when placement exceeds per-division size", () => { const error = getValidatedBrackets([ { settings: { @@ -807,12 +500,7 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1,2,3,4", - }, - ], + sources: [{ bracketId: "0", placements: "1,2,3,4" }], }, ]) as Progression.ValidationError; @@ -820,7 +508,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("accepts A/B divisions placements up to per-division size", () => { + test("accepts A/B divisions placements up to per-division size", () => { const result = getValidatedBrackets([ { settings: { @@ -832,19 +520,14 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1,2,3", - }, - ], + sources: [{ bracketId: "0", placements: "1,2,3" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("handles DUPLICATE_BRACKET_NAME", () => { + test("handles DUPLICATE_BRACKET_NAME", () => { const error = getValidatedBrackets([ { settings: {}, @@ -855,12 +538,7 @@ describe("validatedSources - other rules", () => { settings: {}, type: "single_elimination", name: "Bracket 1", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]) as Progression.ValidationError; @@ -868,7 +546,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdxs).toEqual([0, 1]); }); - it("handles NAME_MISSING", () => { + test("handles NAME_MISSING", () => { const error = getValidatedBrackets([ { settings: {}, @@ -878,12 +556,7 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]) as Progression.ValidationError; @@ -891,31 +564,18 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(0); }); - it("handles NEGATIVE_PROGRESSION", () => { + test("handles NEGATIVE_PROGRESSION", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "0", placements: "-1,-2" }], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "-1,-2", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1", - }, - ], + sources: [{ bracketId: "0", placements: "1" }], }, ]) as Progression.ValidationError; @@ -923,42 +583,26 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("allows single elimination positive progression", () => { + test("allows single elimination positive progression", () => { const result = getValidatedBrackets([ - { - settings: {}, - type: "single_elimination", - }, + SINGLE_ELIMINATION, { settings: {}, type: "double_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]); expect(Progression.isBrackets(result)).toBe(true); }); - it("handles MIXED_POSITIVE_NEGATIVE_PLACEMENTS", () => { + test("handles MIXED_POSITIVE_NEGATIVE_PLACEMENTS", () => { const error = getValidatedBrackets([ + SINGLE_ELIMINATION, { settings: {}, type: "single_elimination", - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1,-1", - }, - ], + sources: [{ bracketId: "0", placements: "1,-1" }], }, ]) as Progression.ValidationError; @@ -966,49 +610,33 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("allows single elimination to source an underground bracket", () => { + test("allows single elimination to source an underground bracket", () => { const result = getValidatedBrackets([ + SINGLE_ELIMINATION, { settings: {}, type: "single_elimination", - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "-1", - }, - ], + sources: [{ bracketId: "0", placements: "-1" }], }, ]); expect(Progression.isBrackets(result)).toBe(true); }); - it("allows double elimination positive progression", () => { + test("allows double elimination positive progression", () => { const result = getValidatedBrackets([ - { - settings: {}, - type: "double_elimination", - }, + DOUBLE_ELIMINATION, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]); expect(Progression.isBrackets(result)).toBe(true); }); - it("handles SWISS_EARLY_ADVANCE_NO_DESTINATION", () => { + test("handles SWISS_EARLY_ADVANCE_NO_DESTINATION", () => { // Swiss bracket with early advance but no destination const error = getValidatedBrackets([ { @@ -1023,7 +651,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(0); }); - it("allows Swiss early advance when bracket has destination", () => { + test("allows Swiss early advance when bracket has destination", () => { // Swiss bracket with early advance that leads to another bracket should be valid const result = getValidatedBrackets([ { @@ -1035,12 +663,7 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-4", - }, - ], + sources: [{ bracketId: "0", placements: "1-4" }], }, ]); @@ -1048,7 +671,7 @@ describe("validatedSources - other rules", () => { expect(Array.isArray(result)).toBe(true); }); - it("accepts A/B divisions on a round robin starting bracket with even teamsPerGroup", () => { + test("accepts A/B divisions on a round robin starting bracket with even teamsPerGroup", () => { const result = getValidatedBrackets([ { settings: { @@ -1060,19 +683,14 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("handles AB_DIVISIONS_NOT_ROUND_ROBIN", () => { + test("handles AB_DIVISIONS_NOT_ROUND_ROBIN", () => { const error = getValidatedBrackets([ { settings: { @@ -1085,12 +703,7 @@ describe("validatedSources - other rules", () => { settings: {}, type: "single_elimination", name: "Finals", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]) as Progression.ValidationError; @@ -1098,7 +711,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(0); }); - it("handles AB_DIVISIONS_NOT_STARTING", () => { + test("handles AB_DIVISIONS_NOT_STARTING", () => { const error = getValidatedBrackets([ { settings: {}, @@ -1112,23 +725,13 @@ describe("validatedSources - other rules", () => { }, type: "round_robin", name: "Second RR", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, { settings: {}, type: "single_elimination", name: "Finals", - sources: [ - { - bracketId: "1", - placements: "1-2", - }, - ], + sources: [{ bracketId: "1", placements: "1-2" }], }, ]) as Progression.ValidationError; @@ -1136,7 +739,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(1); }); - it("handles AB_DIVISIONS_ODD_TEAMS_PER_GROUP", () => { + test("handles AB_DIVISIONS_ODD_TEAMS_PER_GROUP", () => { const error = getValidatedBrackets([ { settings: { @@ -1148,12 +751,7 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]) as Progression.ValidationError; @@ -1161,7 +759,7 @@ describe("validatedSources - other rules", () => { expect((error as any).bracketIdx).toEqual(0); }); - it("accepts A/B divisions when teamsPerGroup is unset (default is even)", () => { + test("accepts A/B divisions when teamsPerGroup is unset (default is even)", () => { const result = getValidatedBrackets([ { settings: { @@ -1172,19 +770,14 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("does not apply A/B validation when hasAbDivisions is absent", () => { + test("does not apply A/B validation when hasAbDivisions is absent", () => { const result = getValidatedBrackets([ { settings: { @@ -1195,19 +788,14 @@ describe("validatedSources - other rules", () => { { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], + sources: [{ bracketId: "0", placements: "1-2" }], }, ]); expect(Array.isArray(result)).toBe(true); }); - it("handles EMPTY_PLACEMENTS_ON_NON_SWISS (DE source with empty placements)", () => { + test("handles EMPTY_PLACEMENTS_ON_NON_SWISS (DE source with empty placements)", () => { const error = Progression.bracketsToValidationError([ { name: "Bracket 1", @@ -1227,7 +815,7 @@ describe("validatedSources - other rules", () => { expect(error.type).toBe("EMPTY_PLACEMENTS_ON_NON_SWISS"); }); - it("allows empty placements when source is Swiss with advanceThreshold", () => { + test("allows empty placements when source is Swiss with advanceThreshold", () => { const result = Progression.bracketsToValidationError([ { name: "Swiss", @@ -1249,11 +837,11 @@ describe("validatedSources - other rules", () => { }); describe("isFinals", () => { - it("handles SE", () => { + test("handles SE", () => { expect(Progression.isFinals(0, progressions.singleElimination)).toBe(true); }); - it("handles RR->SE", () => { + test("handles RR->SE", () => { expect( Progression.isFinals(0, progressions.roundRobinToSingleElimination), ).toBe(false); @@ -1262,25 +850,25 @@ describe("isFinals", () => { ).toBe(true); }); - it("handles low ink", () => { + test("handles low ink", () => { expect(Progression.isFinals(0, progressions.lowInk)).toBe(false); expect(Progression.isFinals(1, progressions.lowInk)).toBe(false); expect(Progression.isFinals(2, progressions.lowInk)).toBe(false); expect(Progression.isFinals(3, progressions.lowInk)).toBe(true); }); - it("handles swiss (early advance", () => { + test("handles swiss (early advance", () => { expect(Progression.isFinals(1, progressions.swissEarlyAdvance)).toBe(true); }); - it("many starter brackets", () => { + test("many starter brackets", () => { expect(Progression.isFinals(0, progressions.manyStartBrackets)).toBe(false); expect(Progression.isFinals(1, progressions.manyStartBrackets)).toBe(false); expect(Progression.isFinals(2, progressions.manyStartBrackets)).toBe(true); expect(Progression.isFinals(3, progressions.manyStartBrackets)).toBe(false); }); - it("throws if given idx is out of bounds", () => { + test("throws if given idx is out of bounds", () => { expect(() => Progression.isFinals(1, progressions.singleElimination), ).toThrow(); @@ -1288,13 +876,13 @@ describe("isFinals", () => { }); describe("isUnderground", () => { - it("handles SE", () => { + test("handles SE", () => { expect(Progression.isUnderground(0, progressions.singleElimination)).toBe( false, ); }); - it("handles RR->SE", () => { + test("handles RR->SE", () => { expect( Progression.isUnderground(0, progressions.roundRobinToSingleElimination), ).toBe(false); @@ -1303,14 +891,14 @@ describe("isUnderground", () => { ).toBe(false); }); - it("handles low ink", () => { + test("handles low ink", () => { expect(Progression.isUnderground(0, progressions.lowInk)).toBe(false); expect(Progression.isUnderground(1, progressions.lowInk)).toBe(true); expect(Progression.isUnderground(2, progressions.lowInk)).toBe(false); expect(Progression.isUnderground(3, progressions.lowInk)).toBe(false); }); - it("many starter brackets", () => { + test("many starter brackets", () => { expect(Progression.isUnderground(0, progressions.manyStartBrackets)).toBe( false, ); @@ -1325,7 +913,7 @@ describe("isUnderground", () => { ); }); - it("handles SE w/ underground bracket", () => { + test("handles SE w/ underground bracket", () => { expect( Progression.isUnderground( 0, @@ -1340,7 +928,7 @@ describe("isUnderground", () => { ).toBe(true); }); - it("redemption bracket feeding the finals is not underground", () => { + test("redemption bracket feeding the finals is not underground", () => { expect(Progression.isUnderground(0, progressions.multiSourceTopCut)).toBe( false, ); @@ -1352,7 +940,7 @@ describe("isUnderground", () => { ); }); - it("throws if given idx is out of bounds", () => { + test("throws if given idx is out of bounds", () => { expect(() => Progression.isUnderground(1, progressions.singleElimination), ).toThrow(); @@ -1360,7 +948,7 @@ describe("isUnderground", () => { }); describe("changedBracketProgression", () => { - it("reports changed bracket indexes", () => { + test("reports changed bracket indexes", () => { const withChanges = structuredClone(progressions.lowInk); withChanges[0].name = "New name"; withChanges[1].type = "swiss"; @@ -1370,7 +958,7 @@ describe("changedBracketProgression", () => { ).toEqual([0, 1]); }); - it("returns an empty array if nothing changed", () => { + test("returns an empty array if nothing changed", () => { expect( Progression.changedBracketProgression( progressions.lowInk, @@ -1381,13 +969,13 @@ describe("changedBracketProgression", () => { }); describe("bracketIdxsForStandings", () => { - it("handles SE", () => { + test("handles SE", () => { expect( Progression.bracketIdxsForStandings(progressions.singleElimination), ).toEqual([0]); }); - it("handles RR->SE", () => { + test("handles RR->SE", () => { expect( Progression.bracketIdxsForStandings( progressions.roundRobinToSingleElimination, @@ -1395,7 +983,7 @@ describe("bracketIdxsForStandings", () => { ).toEqual([1, 0]); }); - it("handles low ink", () => { + test("handles low ink", () => { expect(Progression.bracketIdxsForStandings(progressions.lowInk)).toEqual([ 3, 2, 1, 0, @@ -1403,19 +991,19 @@ describe("bracketIdxsForStandings", () => { ]); }); - it("handles many starter brackets", () => { + test("handles many starter brackets", () => { expect( Progression.bracketIdxsForStandings(progressions.manyStartBrackets), ).toEqual([2, 0]); // NOTE, 3,1 excluded because they are not in the main progression }); - it("handles swiss (one group)", () => { + test("handles swiss (one group)", () => { expect( Progression.bracketIdxsForStandings(progressions.swissOneGroup), ).toEqual([0]); }); - it("handles DE w/ underground bracket", () => { + test("handles DE w/ underground bracket", () => { expect( Progression.bracketIdxsForStandings( progressions.doubleEliminationWithUnderground, @@ -1423,7 +1011,7 @@ describe("bracketIdxsForStandings", () => { ).toEqual([0]); // missing 1 because it's underground when DE is the source }); - it("handles SE w/ underground bracket", () => { + test("handles SE w/ underground bracket", () => { expect( Progression.bracketIdxsForStandings( progressions.singleEliminationWithUnderground, @@ -1431,7 +1019,7 @@ describe("bracketIdxsForStandings", () => { ).toEqual([0]); // missing 1 because it's underground when SE is the source }); - it("does not treat a bracket as intermediate just because an underground bracket sources from it", () => { + test("does not treat a bracket as intermediate just because an underground bracket sources from it", () => { expect( Progression.bracketIdxsForStandings( progressions.swissToTwoSingleEliminationsWithUnderground, @@ -1439,13 +1027,13 @@ describe("bracketIdxsForStandings", () => { ).toEqual([1, 2, 0]); // missing 3 because it's underground }); - it("keeps a finals bracket sourced positively from a SE redemption bracket", () => { + test("keeps a finals bracket sourced positively from a SE redemption bracket", () => { expect( Progression.bracketIdxsForStandings(progressions.multiSourceTopCut), ).toEqual([2, 1, 0]); }); - it("places a redemption bracket above the brackets taking lower placements from the same source", () => { + test("places a redemption bracket above the brackets taking lower placements from the same source", () => { expect( Progression.bracketIdxsForStandings( progressions.multiSourceTopCutWithConsolation, @@ -1453,7 +1041,7 @@ describe("bracketIdxsForStandings", () => { ).toEqual([2, 1, 3, 0]); }); - it("orders brackets by the placement of their teams in the shared ancestor bracket", () => { + test("orders brackets by the placement of their teams in the shared ancestor bracket", () => { expect( Progression.bracketIdxsForStandings( progressions.poolsToBracketsViaIntermediateBrackets, @@ -1472,19 +1060,19 @@ describe("bracketIdxsForStandings", () => { }); describe("startingBrackets", () => { - it("handles SE", () => { + test("handles SE", () => { expect( Progression.startingBrackets(progressions.singleElimination), ).toEqual([0]); }); - it("handles many starter brackets", () => { + test("handles many starter brackets", () => { expect( Progression.startingBrackets(progressions.manyStartBrackets), ).toEqual([0, 1]); }); - it("handles swiss (one group)", () => { + test("handles swiss (one group)", () => { expect(Progression.startingBrackets(progressions.swissOneGroup)).toEqual([ 0, ]); @@ -1492,7 +1080,7 @@ describe("startingBrackets", () => { }); describe("destinationsFromBracketIdx", () => { - it("returns correct destination (one destination)", () => { + test("returns correct destination (one destination)", () => { expect( Progression.destinationsFromBracketIdx( 0, @@ -1501,13 +1089,13 @@ describe("destinationsFromBracketIdx", () => { ).toEqual([1]); }); - it("returns correct destination (many destinations)", () => { + test("returns correct destination (many destinations)", () => { expect( Progression.destinationsFromBracketIdx(0, progressions.lowInk), ).toEqual([1, 2]); }); - it("returns an empty array if no destinations", () => { + test("returns an empty array if no destinations", () => { expect( Progression.destinationsFromBracketIdx(0, progressions.singleElimination), ).toEqual([]); @@ -1515,7 +1103,7 @@ describe("destinationsFromBracketIdx", () => { }); describe("destinationByPlacement", () => { - it("returns correct destination for a given placement", () => { + test("returns correct destination for a given placement", () => { const result = Progression.destinationByPlacement({ sourceBracketIdx: 0, placement: 1, @@ -1524,7 +1112,7 @@ describe("destinationByPlacement", () => { expect(result).toBe(1); }); - it("returns null if no destination for the given placement", () => { + test("returns null if no destination for the given placement", () => { const result = Progression.destinationByPlacement({ sourceBracketIdx: 0, placement: 5, @@ -1533,7 +1121,7 @@ describe("destinationByPlacement", () => { expect(result).toBeNull(); }); - it("returns correct destination for negative placements", () => { + test("returns correct destination for negative placements", () => { const result = Progression.destinationByPlacement({ sourceBracketIdx: 0, placement: -1, @@ -1542,7 +1130,7 @@ describe("destinationByPlacement", () => { expect(result).toBe(1); }); - it("returns correct destination for many start brackets", () => { + test("returns correct destination for many start brackets", () => { const result = Progression.destinationByPlacement({ sourceBracketIdx: 1, placement: 1, @@ -1553,7 +1141,7 @@ describe("destinationByPlacement", () => { }); describe("validatedBracketsToInputFormat", () => { - it("converts empty placements back to empty string", () => { + test("converts empty placements back to empty string", () => { const parsedBrackets: Progression.ParsedBracket[] = [ { type: "swiss", @@ -1587,11 +1175,11 @@ describe("validatedBracketsToInputFormat", () => { }); describe("bracketDepth", () => { - it("returns 0 for starting bracket with no sources", () => { + test("returns 0 for starting bracket with no sources", () => { expect(Progression.bracketDepth(0, progressions.singleElimination)).toBe(0); }); - it("returns 0 for starting bracket and 1 for bracket sourced from it", () => { + test("returns 0 for starting bracket and 1 for bracket sourced from it", () => { expect( Progression.bracketDepth(0, progressions.roundRobinToSingleElimination), ).toBe(0); @@ -1600,21 +1188,21 @@ describe("bracketDepth", () => { ).toBe(1); }); - it("handles complex progression with multiple depth levels", () => { + test("handles complex progression with multiple depth levels", () => { expect(Progression.bracketDepth(0, progressions.lowInk)).toBe(0); expect(Progression.bracketDepth(1, progressions.lowInk)).toBe(1); expect(Progression.bracketDepth(2, progressions.lowInk)).toBe(1); expect(Progression.bracketDepth(3, progressions.lowInk)).toBe(2); }); - it("handles multiple starting brackets", () => { + test("handles multiple starting brackets", () => { expect(Progression.bracketDepth(0, progressions.manyStartBrackets)).toBe(0); expect(Progression.bracketDepth(1, progressions.manyStartBrackets)).toBe(0); expect(Progression.bracketDepth(2, progressions.manyStartBrackets)).toBe(1); expect(Progression.bracketDepth(3, progressions.manyStartBrackets)).toBe(1); }); - it("handles underground brackets", () => { + test("handles underground brackets", () => { expect( Progression.bracketDepth( 0, @@ -1629,7 +1217,7 @@ describe("bracketDepth", () => { ).toBe(1); }); - it("throws if given idx is out of bounds", () => { + test("throws if given idx is out of bounds", () => { expect(() => Progression.bracketDepth(1, progressions.singleElimination), ).toThrow(); @@ -1637,24 +1225,15 @@ describe("bracketDepth", () => { }); describe("validatedSources - DUPLICATE_SOURCE_BRACKET", () => { - it("flags a destination sourcing the same bracket twice", () => { + test("flags a destination sourcing the same bracket twice", () => { const error = getValidatedBrackets([ - { - settings: {}, - type: "round_robin", - }, + ROUND_ROBIN, { settings: {}, type: "single_elimination", sources: [ - { - bracketId: "0", - placements: "1-2", - }, - { - bracketId: "0", - placements: "3-4", - }, + { bracketId: "0", placements: "1-2" }, + { bracketId: "0", placements: "3-4" }, ], }, ]) as Progression.ValidationError; @@ -1663,7 +1242,7 @@ describe("validatedSources - DUPLICATE_SOURCE_BRACKET", () => { expect((error as any).bracketIdx).toBe(1); }); - it("accepts different destinations sourcing the same bracket", () => { + test("accepts different destinations sourcing the same bracket", () => { expect( Progression.bracketsToValidationError(progressions.lowInk), ).toBeNull(); @@ -1671,35 +1250,21 @@ describe("validatedSources - DUPLICATE_SOURCE_BRACKET", () => { }); describe("validatedSources - CYCLIC_PROGRESSION", () => { - it("flags two brackets sourcing each other", () => { + test("flags two brackets sourcing each other", () => { const error = getValidatedBrackets([ - { - settings: {}, - type: "round_robin", - }, + ROUND_ROBIN, { settings: {}, type: "single_elimination", sources: [ - { - bracketId: "0", - placements: "1-2", - }, - { - bracketId: "2", - placements: "1", - }, + { bracketId: "0", placements: "1-2" }, + { bracketId: "2", placements: "1" }, ], }, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "1", - placements: "1", - }, - ], + sources: [{ bracketId: "1", placements: "1" }], }, ]) as Progression.ValidationError; @@ -1707,21 +1272,13 @@ describe("validatedSources - CYCLIC_PROGRESSION", () => { expect((error as any).bracketIdxs).toEqual([1, 2]); }); - it("flags a bracket sourcing itself", () => { + test("flags a bracket sourcing itself", () => { const error = getValidatedBrackets([ - { - settings: {}, - type: "round_robin", - }, + ROUND_ROBIN, { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "1", - placements: "1", - }, - ], + sources: [{ bracketId: "1", placements: "1" }], }, ]) as Progression.ValidationError; @@ -1729,28 +1286,20 @@ describe("validatedSources - CYCLIC_PROGRESSION", () => { expect((error as any).bracketIdxs).toEqual([1]); }); - it("accepts a bracket sourcing one that comes later in the list", () => { + test("accepts a bracket sourcing one that comes later in the list", () => { const result = getValidatedBrackets([ { settings: {}, type: "single_elimination", - sources: [ - { - bracketId: "1", - placements: "1-4", - }, - ], - }, - { - settings: {}, - type: "round_robin", + sources: [{ bracketId: "1", placements: "1-4" }], }, + ROUND_ROBIN, ]); expect(Progression.isBrackets(result)).toBe(true); }); - it("accepts brackets sharing a source (diamond shaped progression)", () => { + test("accepts brackets sharing a source (diamond shaped progression)", () => { expect( Progression.bracketsToValidationError(progressions.lowInk), ).toBeNull(); @@ -1758,28 +1307,16 @@ describe("validatedSources - CYCLIC_PROGRESSION", () => { }); describe("validatedSources - MERGED_STARTING_BRACKETS", () => { - it("flags a bracket sourcing two starting brackets", () => { + test("flags a bracket sourcing two starting brackets", () => { const error = getValidatedBrackets([ - { - settings: {}, - type: "round_robin", - }, - { - settings: {}, - type: "round_robin", - }, + ROUND_ROBIN, + ROUND_ROBIN, { settings: {}, type: "single_elimination", sources: [ - { - bracketId: "0", - placements: "1-2", - }, - { - bracketId: "1", - placements: "1-2", - }, + { bracketId: "0", placements: "1-2" }, + { bracketId: "1", placements: "1-2" }, ], }, ]) as Progression.ValidationError; @@ -1788,48 +1325,26 @@ describe("validatedSources - MERGED_STARTING_BRACKETS", () => { expect((error as any).bracketIdx).toBe(2); }); - it("flags a merge that happens through intermediate brackets", () => { + test("flags a merge that happens through intermediate brackets", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, + ROUND_ROBIN, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "0", placements: "1-2" }], }, { settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "1", placements: "1-2" }], }, { settings: {}, type: "single_elimination", sources: [ - { - bracketId: "0", - placements: "1-2", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "1", - placements: "1-2", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "2", - placements: "1", - }, - { - bracketId: "3", - placements: "1", - }, + { bracketId: "2", placements: "1" }, + { bracketId: "3", placements: "1" }, ], }, ]) as Progression.ValidationError; @@ -1838,38 +1353,21 @@ describe("validatedSources - MERGED_STARTING_BRACKETS", () => { expect((error as any).bracketIdx).toBe(4); }); - it("reports the bracket where the merge happens, not the ones after it", () => { + test("reports the bracket where the merge happens, not the ones after it", () => { const error = getValidatedBrackets([ + ROUND_ROBIN, + ROUND_ROBIN, { settings: {}, - type: "round_robin", - }, - { - settings: {}, - type: "round_robin", + type: "single_elimination", + sources: [{ bracketId: "3", placements: "1-2" }], }, { settings: {}, type: "single_elimination", sources: [ - { - bracketId: "3", - placements: "1-2", - }, - ], - }, - { - settings: {}, - type: "single_elimination", - sources: [ - { - bracketId: "0", - placements: "1-2", - }, - { - bracketId: "1", - placements: "1-2", - }, + { bracketId: "0", placements: "1-2" }, + { bracketId: "1", placements: "1-2" }, ], }, ]) as Progression.ValidationError; @@ -1878,13 +1376,13 @@ describe("validatedSources - MERGED_STARTING_BRACKETS", () => { expect((error as any).bracketIdx).toBe(3); }); - it("accepts many starting brackets that never merge", () => { + test("accepts many starting brackets that never merge", () => { expect( Progression.bracketsToValidationError(progressions.manyStartBrackets), ).toBeNull(); }); - it("accepts many sources that all come from the same starting bracket", () => { + test("accepts many sources that all come from the same starting bracket", () => { expect( Progression.bracketsToValidationError(progressions.multiSourceTopCut), ).toBeNull(); @@ -1892,7 +1390,7 @@ describe("validatedSources - MERGED_STARTING_BRACKETS", () => { }); describe("sortedSourcesForSeeding", () => { - it("orders a direct source above one that took a redemption route", () => { + test("orders a direct source above one that took a redemption route", () => { const topCut: Progression.ParsedBracket = progressions.multiSourceTopCut[2]; const sorted = Progression.sortedSourcesForSeeding( @@ -1903,7 +1401,7 @@ describe("sortedSourcesForSeeding", () => { expect(sorted.map((source) => source.bracketIdx)).toEqual([0, 1]); }); - it("keeps the original order when sources share no ancestor bracket", () => { + test("keeps the original order when sources share no ancestor bracket", () => { const progression: Progression.ParsedBracket[] = [ { name: "Group A", @@ -1937,7 +1435,7 @@ describe("sortedSourcesForSeeding", () => { expect(sorted.map((source) => source.bracketIdx)).toEqual([1, 0]); }); - it("compares at the deepest common ancestor", () => { + test("compares at the deepest common ancestor", () => { const progression: Progression.ParsedBracket[] = [ { name: "Pools", @@ -1979,7 +1477,7 @@ describe("sortedSourcesForSeeding", () => { expect(sorted.map((source) => source.bracketIdx)).toEqual([1, 2]); }); - it("orders teams eliminated from a follow-up bracket above lower direct placements", () => { + test("orders teams eliminated from a follow-up bracket above lower direct placements", () => { const progression: Progression.ParsedBracket[] = [ { name: "Pools", diff --git a/app/features/tournament-bracket/core/RunningTournaments.test.ts b/app/features/tournament-bracket/core/RunningTournaments.test.ts index b4797d96c..410d19fdb 100644 --- a/app/features/tournament-bracket/core/RunningTournaments.test.ts +++ b/app/features/tournament-bracket/core/RunningTournaments.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { RunningTournaments } from "./RunningTournaments.server"; import { testTournament, tournamentCtxTeam } from "./tests/test-utils"; @@ -26,7 +26,7 @@ describe("RunningTournaments", () => { }); describe("add", () => { - it("adds a tournament to the registry", () => { + test("adds a tournament to the registry", () => { const tournament = createTestTournament(1, [ { teamId: 1, userIds: [100, 101] }, ]); @@ -37,7 +37,7 @@ describe("RunningTournaments", () => { expect(RunningTournaments.get(1)).toBe(tournament); }); - it("updates existing tournament when added again with different instance", () => { + test("updates existing tournament when added again with different instance", () => { const tournament1 = createTestTournament(1, [ { teamId: 1, userIds: [100] }, ]); @@ -53,7 +53,7 @@ describe("RunningTournaments", () => { }); describe("remove", () => { - it("removes a tournament from the registry", () => { + test("removes a tournament from the registry", () => { const tournament = createTestTournament(1, [ { teamId: 1, userIds: [100] }, ]); @@ -65,7 +65,7 @@ describe("RunningTournaments", () => { expect(RunningTournaments.get(1)).toBeUndefined(); }); - it("does nothing when tournament not in registry", () => { + test("does nothing when tournament not in registry", () => { RunningTournaments.remove(999); expect(RunningTournaments.has(999)).toBe(false); @@ -73,7 +73,7 @@ describe("RunningTournaments", () => { }); describe("clear", () => { - it("removes all tournaments", () => { + test("removes all tournaments", () => { const tournament1 = createTestTournament(1, [ { teamId: 1, userIds: [100] }, ]); diff --git a/app/features/tournament-bracket/core/Seeding.test.ts b/app/features/tournament-bracket/core/Seeding.test.ts index 847fd2959..a5da6b698 100644 --- a/app/features/tournament-bracket/core/Seeding.test.ts +++ b/app/features/tournament-bracket/core/Seeding.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as Seeding from "./Seeding"; // first round lineups of the standard bracket ("space_between") by bracket size, @@ -8,7 +8,7 @@ const LINEUP_16 = [1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11]; describe("Seeding.forFollowUpBracket()", () => { describe("group spreading", () => { - it("spreads 4 groups of 4 across the quarters of a 16 bracket", () => { + test("spreads 4 groups of 4 across the quarters of a 16 bracket", () => { const { teams, source } = groupsOfFour(); const result = Seeding.forFollowUpBracket({ @@ -21,7 +21,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("keeps placement tiers intact while spreading", () => { + test("keeps placement tiers intact while spreading", () => { const { teams, source } = groupsOfFour(); const result = Seeding.forFollowUpBracket({ @@ -34,7 +34,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("does not reorder the group winners (best two can only meet in the finals)", () => { + test("does not reorder the group winners (best two can only meet in the finals)", () => { const { teams, source } = groupsOfFour(); const result = Seeding.forFollowUpBracket({ @@ -45,7 +45,7 @@ describe("Seeding.forFollowUpBracket()", () => { expect(result.slice(0, 4)).toEqual(teams.slice(0, 4)); }); - it("spreads 4 groups of 2 across the halves of an 8 bracket", () => { + test("spreads 4 groups of 2 across the halves of an 8 bracket", () => { const groups = [ [101, 102], [201, 202], @@ -64,7 +64,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("keeps 2 groups of 4 out of same group round 1 matches (Swiss top cut shape)", () => { + test("keeps 2 groups of 4 out of same group round 1 matches (Swiss top cut shape)", () => { const groups = [ [101, 102, 103, 104], [201, 202, 203, 204], @@ -81,7 +81,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("returns the incoming order when each group sends one team", () => { + test("returns the incoming order when each group sends one team", () => { const groups = [[101], [201], [301], [401], [501], [601], [701], [801]]; const teams = [101, 201, 301, 401, 501, 601, 701, 801]; @@ -93,7 +93,7 @@ describe("Seeding.forFollowUpBracket()", () => { expect(result).toEqual(teams); }); - it("spreads 3 groups of 4 across the quarters of a 12 team bracket (byes)", () => { + test("spreads 3 groups of 4 across the quarters of a 12 team bracket (byes)", () => { const groups = [ [101, 102, 103, 104], [201, 202, 203, 204], @@ -114,7 +114,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("spreads groups of uneven sizes (a team missing due to no check-in)", () => { + test("spreads groups of uneven sizes (a team missing due to no check-in)", () => { const groups = [ [101, 102, 103, 104], [201, 202, 203, 204], @@ -137,7 +137,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("halves the group block size when the ideal spread is unreachable", () => { + test("halves the group block size when the ideal spread is unreachable", () => { // The two groups of four cannot hold a quarter each: only two quarters are // reachable by their two lowest placement tiers, and both tiers are needed // by both groups. The halved block size is satisfiable though, and it still @@ -170,7 +170,7 @@ describe("Seeding.forFollowUpBracket()", () => { }); describe("previous encounter avoidance", () => { - it("single group: avoids a round 1 rematch by reordering the bottom half", () => { + test("single group: avoids a round 1 rematch by reordering the bottom half", () => { const teams = [1, 2, 3, 4, 5, 6, 7, 8]; const result = Seeding.forFollowUpBracket({ @@ -181,7 +181,7 @@ describe("Seeding.forFollowUpBracket()", () => { expect(result).toEqual([1, 2, 3, 4, 5, 6, 8, 7]); }); - it("single group: reorders only the bottom half even when every natural match would be a rematch", () => { + test("single group: reorders only the bottom half even when every natural match would be a rematch", () => { const teams = [1, 2, 3, 4, 5, 6, 7, 8]; const naturalMatches: Array<[number, number]> = [ [1, 8], @@ -204,7 +204,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("single group: falls back to the incoming order when rematches are unavoidable", () => { + test("single group: falls back to the incoming order when rematches are unavoidable", () => { const teams = [1, 2, 3, 4, 5, 6, 7, 8]; const everyPair: Array<[number, number]> = []; for (const one of teams) { @@ -221,7 +221,7 @@ describe("Seeding.forFollowUpBracket()", () => { expect(result).toEqual(teams); }); - it("single group: counts the middle seed of an odd team count into the bottom half", () => { + test("single group: counts the middle seed of an odd team count into the bottom half", () => { // with 13 teams the top half is seeds 1-6, leaving seeds 7-13 interchangeable. // Team 5 has played every one of those but team 7, so team 7 is the only team // that can take seed 12, the seed team 5 faces in round 1. @@ -250,7 +250,7 @@ describe("Seeding.forFollowUpBracket()", () => { } }); - it("single group: returns a complete lineup when the search runs out of nodes", () => { + test("single group: returns a complete lineup when the search runs out of nodes", () => { // team 1 has played every team of the bottom half pool, so no arrangement of // it avoids their round 1 rematch and the search exhausts its node budget // before the next relaxation rung takes over @@ -268,7 +268,7 @@ describe("Seeding.forFollowUpBracket()", () => { }); }); - it("returns the incoming order for fewer than 4 teams", () => { + test("returns the incoming order for fewer than 4 teams", () => { const teams = [1, 2, 3]; const result = Seeding.forFollowUpBracket({ diff --git a/app/features/tournament-bracket/core/Swiss.test.ts b/app/features/tournament-bracket/core/Swiss.test.ts index b8b171a17..dfc07e236 100644 --- a/app/features/tournament-bracket/core/Swiss.test.ts +++ b/app/features/tournament-bracket/core/Swiss.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { TournamentStageSettings } from "~/db/tables-json"; import { Tournament } from "~/features/tournament-bracket/core/Tournament"; import { @@ -35,19 +35,19 @@ describe("Swiss", () => { }; describe("create()", () => { - it("creates a swiss bracket with correct amount of initial matches", () => { + test("creates a swiss bracket with correct amount of initial matches", () => { const data = Swiss.create(createArgsWithDefaults()); expect(data.match).toHaveLength(2); }); - it("creates a swiss bracket with correct amount of rounds as default", () => { + test("creates a swiss bracket with correct amount of rounds as default", () => { const data = Swiss.create(createArgsWithDefaults()); expect(data.round).toHaveLength(5); }); - it("creates a swiss bracket with correct amount of rounds as parameter", () => { + test("creates a swiss bracket with correct amount of rounds as parameter", () => { const data = Swiss.create( createArgsWithDefaults({ settings: { @@ -60,7 +60,7 @@ describe("Swiss", () => { expect(data.round).toHaveLength(4); }); - it("creates a swiss bracket with two groups", () => { + test("creates a swiss bracket with two groups", () => { const data = Swiss.create( createArgsWithDefaults({ settings: { @@ -77,7 +77,7 @@ describe("Swiss", () => { expect(matchGroupIds).toContain(1); }); - it("every team has a match", () => { + test("every team has a match", () => { const data = Swiss.create(createArgsWithDefaults()); for (const teamId of [1, 2, 3, 4]) { @@ -90,7 +90,7 @@ describe("Swiss", () => { } }); - it("assigns a BYE if odd number of teams", () => { + test("assigns a BYE if odd number of teams", () => { const data = Swiss.create( createArgsWithDefaults({ seeding: [1, 2, 3, 4, 5], @@ -101,7 +101,7 @@ describe("Swiss", () => { expect(byes).toHaveLength(1); }); - it("if no teams, should generate a bracket data with no matches", () => { + test("if no teams, should generate a bracket data with no matches", () => { const data = Swiss.create(createArgsWithDefaults({ seeding: [] })); expect(data.match).toHaveLength(0); @@ -124,7 +124,7 @@ describe("Swiss", () => { }), ).matches; - it("finds new opponents for each team in the last round", () => { + test("finds new opponents for each team in the last round", () => { for (const match of matches) { if (match.opponent2 === null) continue; @@ -142,12 +142,12 @@ describe("Swiss", () => { } }); - it("generates a bye", () => { + test("generates a bye", () => { const byes = matches.filter((match) => match.opponent2 === null); expect(byes).toHaveLength(1); }); - it("every pair is max one set win from each other", () => { + test("every pair is max one set win from each other", () => { for (const match of matches) { if (match.opponent2 === null) continue; @@ -197,7 +197,7 @@ describe("Swiss", () => { stats: { setWins: record.setWins, setLosses: record.setLosses }, })); - it("gives a bye to the only team left in the running", () => { + test("gives a bye to the only team left in the running", () => { const round = unwrap( Engine.generateRound(bracketWithFinishedRound(), { groupId: 0, @@ -216,7 +216,7 @@ describe("Swiss", () => { ]); }); - it("generates no round if no team is left in the running", () => { + test("generates no round if no team is left in the running", () => { const round = Engine.generateRound(bracketWithFinishedRound(), { groupId: 0, standings: standingsOf([ @@ -235,7 +235,7 @@ describe("Swiss", () => { const PAIR_UP_TEST_CASES = [RUSH_WEEKEND_3, LOW_INK_AUGUST_2025]; describe("pairUp()", () => { - it.for(PAIR_UP_TEST_CASES)( + test.for(PAIR_UP_TEST_CASES)( "all teams have matches (pair up test cases idx %#)", (testCase) => { const result = Swiss.pairUp(testCase); @@ -252,7 +252,7 @@ describe("Swiss", () => { }, ); - it.for(PAIR_UP_TEST_CASES)( + test.for(PAIR_UP_TEST_CASES)( "every pair is max one set win from each other (pair up test cases idx %#)", (testCase) => { const result = Swiss.pairUp(testCase); @@ -276,8 +276,8 @@ describe("Swiss", () => { }, ); - it.for(PAIR_UP_TEST_CASES)( - "should match perfect records against each other as much as possible (pair up test cases idx %#)", + test.for(PAIR_UP_TEST_CASES)( + "matches perfect records against each other as much as possible (pair up test cases idx %#)", (testCase) => { const result = Swiss.pairUp(testCase); @@ -313,7 +313,7 @@ describe("Swiss", () => { }, ); - it.for(PAIR_UP_TEST_CASES)( + test.for(PAIR_UP_TEST_CASES)( "generates max one bye (pair up test cases idx %#)", (testCase) => { const result = Swiss.pairUp(testCase); @@ -327,13 +327,13 @@ describe("Swiss", () => { }, ); - it("gives a bye to a lone team", () => { + test("gives a bye to a lone team", () => { expect(Swiss.pairUp([{ id: 1, score: 2, avoid: [] }])).toEqual([ { opponentOne: 1, opponentTwo: null }, ]); }); - it("replays if a rematch free pairing does not exist for every team", () => { + test("replays if a rematch free pairing does not exist for every team", () => { // only 1 & 2 have not played each other yet const result = Swiss.pairUp([ { id: 1, score: 1, avoid: [3, 4] }, @@ -347,7 +347,7 @@ describe("Swiss", () => { expect(includesPair(result, 3, 4)).toBe(true); }); - it("prefers replaying teams that have met the fewest times", () => { + test("prefers replaying teams that have met the fewest times", () => { // everyone has played everyone, but 1 & 2 have already played twice const result = Swiss.pairUp([ { id: 1, score: 1, avoid: [2, 2, 3, 4] }, @@ -359,7 +359,7 @@ describe("Swiss", () => { expect(includesPair(result, 1, 2)).toBe(false); }); - it("prefers giving the bye to the lowest standing team without a previous bye", () => { + test("prefers giving the bye to the lowest standing team without a previous bye", () => { // five team swiss entering round 4: teams 3, 4 and 5 have already had a // bye, team 3 in the round right before this one. Teams 1 and 2 have not, // and a rematch free pairing where team 2 (the lowest standing team @@ -379,7 +379,7 @@ describe("Swiss", () => { }); describe("calculateTeamStatus()", () => { - it("returns 'advanced' when team has enough wins", () => { + test("returns 'advanced' when team has enough wins", () => { expect( Swiss.calculateTeamStatus({ wins: 3, @@ -406,7 +406,7 @@ describe("Swiss", () => { ).toBe("advanced"); }); - it("returns 'eliminated' when team has too many losses", () => { + test("returns 'eliminated' when team has too many losses", () => { expect( Swiss.calculateTeamStatus({ wins: 0, @@ -433,7 +433,7 @@ describe("Swiss", () => { ).toBe("eliminated"); }); - it("returns 'active' when team can still advance or be eliminated", () => { + test("returns 'active' when team can still advance or be eliminated", () => { expect( Swiss.calculateTeamStatus({ wins: 2, @@ -468,7 +468,7 @@ describe("Swiss", () => { ).toBe("active"); }); - it("handles different tournament configurations", () => { + test("handles different tournament configurations", () => { // 4-round tournament with advance threshold 2 expect( Swiss.calculateTeamStatus({ @@ -522,7 +522,7 @@ describe("Swiss", () => { ).toBe("active"); }); - it("handles edge cases correctly", () => { + test("handles edge cases correctly", () => { // Team reaches advance threshold exactly expect( Swiss.calculateTeamStatus({ @@ -565,7 +565,7 @@ describe("Swiss", () => { describe("Threshold validation utilities", () => { describe("maxAdvanceThreshold()", () => { - it("calculates maximum advance threshold correctly", () => { + test("calculates maximum advance threshold correctly", () => { expect(Swiss.maxAdvanceThreshold({ roundCount: 3 })).toBe(3); // ceil(3/2) + 1 = 2 + 1 = 3 expect(Swiss.maxAdvanceThreshold({ roundCount: 4 })).toBe(3); // ceil(4/2) + 1 = 2 + 1 = 3 expect(Swiss.maxAdvanceThreshold({ roundCount: 5 })).toBe(4); // ceil(5/2) + 1 = 3 + 1 = 4 @@ -575,7 +575,7 @@ describe("Swiss", () => { }); describe("isValidAdvanceThreshold()", () => { - it("validates correct thresholds", () => { + test("validates correct thresholds", () => { expect( Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 3 }), ).toBe(true); @@ -590,7 +590,7 @@ describe("Swiss", () => { ).toBe(true); }); - it("rejects invalid thresholds", () => { + test("rejects invalid thresholds", () => { // Threshold too high expect( Swiss.isValidAdvanceThreshold({ roundCount: 5, advanceThreshold: 5 }), @@ -611,7 +611,7 @@ describe("Swiss", () => { ).toBe(false); }); - it("handles edge cases", () => { + test("handles edge cases", () => { expect( Swiss.isValidAdvanceThreshold({ roundCount: 3, advanceThreshold: 2 }), ).toBe(true); // minimum valid @@ -622,7 +622,7 @@ describe("Swiss", () => { }); describe("validAdvanceThresholdOptions()", () => { - it("returns correct options for different round counts", () => { + test("returns correct options for different round counts", () => { expect(Swiss.validAdvanceThresholdOptions({ roundCount: 3 })).toEqual([ 2, 3, ]); @@ -634,7 +634,7 @@ describe("Swiss", () => { ]); }); - it("handles minimal round counts", () => { + test("handles minimal round counts", () => { expect(Swiss.validAdvanceThresholdOptions({ roundCount: 2 })).toEqual([ 2, ]); @@ -643,7 +643,7 @@ describe("Swiss", () => { ]); }); - it("includes thresholds up to the calculated maximum for large round counts", () => { + test("includes thresholds up to the calculated maximum for large round counts", () => { const roundCount = 9; const max = Swiss.maxAdvanceThreshold({ roundCount }); diff --git a/app/features/tournament-bracket/core/Tournament.test.ts b/app/features/tournament-bracket/core/Tournament.test.ts index 4e4e743ac..3cb494749 100644 --- a/app/features/tournament-bracket/core/Tournament.test.ts +++ b/app/features/tournament-bracket/core/Tournament.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, test } from "vitest"; +import { describe, expect, test } from "vitest"; import type { BracketData, GeneratedRound, @@ -231,7 +231,7 @@ describe("Follow-up bracket progression", () => { }); describe("Bracket progression override", () => { - it("handles no override", () => { + test("handles no override", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167(), }); @@ -250,7 +250,7 @@ describe("Bracket progression override", () => { ); }); - it("overrides causing the team to go to another bracket", () => { + test("overrides causing the team to go to another bracket", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ { @@ -266,7 +266,7 @@ describe("Bracket progression override", () => { ).toBeTruthy(); }); - it("overrides causing the team not to go to their original bracket", () => { + test("overrides causing the team not to go to their original bracket", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ { @@ -282,7 +282,7 @@ describe("Bracket progression override", () => { ).toBeFalsy(); }); - it("destinationBracketIdx = -1 eliminates the team", () => { + test("destinationBracketIdx = -1 eliminates the team", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ { @@ -307,7 +307,7 @@ describe("Bracket progression override", () => { ); }); - it("override teams seeded at the end", () => { + test("override teams seeded at the end", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ { @@ -321,7 +321,7 @@ describe("Bracket progression override", () => { expect(tournament.brackets[1].seeding?.at(-1)).toBe(14809); }); - it("if redundant override, still in the right bracket", () => { + test("if redundant override, still in the right bracket", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ { @@ -337,7 +337,7 @@ describe("Bracket progression override", () => { ).toBeTruthy(); }); - it("redundants override does not affect the seed", () => { + test("redundants override does not affect the seed", () => { const tournamentTeamId = 14735; const tournament = new Tournament({ ...SWIM_OR_SINK_167(), @@ -362,7 +362,7 @@ describe("Bracket progression override", () => { }); // note there is also logic for avoiding replays - it("override teams seeded according to their placement in the source bracket", () => { + test("override teams seeded according to their placement in the source bracket", () => { const tournament = new Tournament({ ...SWIM_OR_SINK_167([ // throw these to different brackets to avoid replays @@ -420,26 +420,26 @@ describe("Adjusting team starting bracket", () => { }); }; - it("defaults to bracket idx = 0", () => { + test("defaults to bracket idx = 0", () => { const tournament = createTournament([null, null, null, null]); expect(tournament.brackets[0].participantTournamentTeamIds).toHaveLength(4); }); - it("setting starting bracket idx has an effect", () => { + test("setting starting bracket idx has an effect", () => { const tournament = createTournament([0, 0, 1, 1]); expect(tournament.brackets[0].participantTournamentTeamIds).toHaveLength(2); expect(tournament.brackets[1].participantTournamentTeamIds).toHaveLength(2); }); - it("handles too high bracket idx gracefully", () => { + test("handles too high bracket idx gracefully", () => { const tournament = createTournament([0, 0, 0, 10]); expect(tournament.brackets[0].participantTournamentTeamIds).toHaveLength(4); }); - it("handles bracket idx is not a valid starting bracket idx gracefully", () => { + test("handles bracket idx is not a valid starting bracket idx gracefully", () => { // 2 is not valid because it is a follow-up bracket const tournament = createTournament([0, 0, 0, 2]); @@ -466,13 +466,13 @@ describe("Resolving the team a user is a member of", () => { }, }); - it("resolves the only team the user is a member of", () => { + test("resolves the only team the user is a member of", () => { const tournament = tournamentWithTeams([{ id: 1, createdAt: 1 }]); expect(tournament.teamMemberOfByUser({ id: USER_ID })?.id).toBe(1); }); - it("resolves the team the user joined most recently when on many teams", () => { + test("resolves the team the user joined most recently when on many teams", () => { // e.g. the user's first team dropped out and the organizer added them to an // older team afterwards const tournament = tournamentWithTeams( @@ -486,7 +486,7 @@ describe("Resolving the team a user is a member of", () => { expect(tournament.teamMemberOfByUser({ id: USER_ID })?.id).toBe(1); }); - it("falls back to the first team when the most recently joined one is not visible", () => { + test("falls back to the first team when the most recently joined one is not visible", () => { const tournament = tournamentWithTeams( [ { id: 1, createdAt: 1 }, @@ -498,7 +498,7 @@ describe("Resolving the team a user is a member of", () => { expect(tournament.teamMemberOfByUser({ id: USER_ID })?.id).toBe(1); }); - it("returns null if the user is not a member of any team", () => { + test("returns null if the user is not a member of any team", () => { const tournament = tournamentWithTeams([{ id: 1, createdAt: 1 }]); expect(tournament.teamMemberOfByUser({ id: USER_ID + 1 })).toBeNull(); @@ -510,7 +510,7 @@ describe("teamMemberOfProgressStatus in swiss", () => { tournamentCtxTeam(teamId, { memberUserIds: [100 + teamId] }), ); - it("resolves an early advanced team as waiting for the follow-up bracket", () => { + test("resolves an early advanced team as waiting for the follow-up bracket", () => { const data = playOutEarlyAdvanceSwiss(progressions.swissEarlyAdvance); const tournament = testTournament({ @@ -530,7 +530,7 @@ describe("teamMemberOfProgressStatus in swiss", () => { ); }); - it("resolves a dropped out team's status as thanks for playing", () => { + test("resolves a dropped out team's status as thanks for playing", () => { const data = Engine.create({ type: "swiss", seeding: [1, 2, 3, 4], @@ -581,7 +581,7 @@ describe("Swiss early advance bracket sourcing", () => { }, ]; - it("sources a consolation bracket by its placements instead of the advance threshold", () => { + test("sources a consolation bracket by its placements instead of the advance threshold", () => { const data = playOutEarlyAdvanceSwiss(progressionWithConsolation); const tournament = testTournament({ @@ -601,7 +601,7 @@ describe("Swiss early advance bracket sourcing", () => { }); describe("teamById division seeds", () => { - it("assigns unique seeds within a division when a late registrant has null startingBracketIdx", () => { + test("assigns unique seeds within a division when a late registrant has null startingBracketIdx", () => { const tournament = testTournament({ ctx: { settings: { diff --git a/app/features/tournament-bracket/core/Tournament.timezone.test.ts b/app/features/tournament-bracket/core/Tournament.timezone.test.ts index f4b6869d9..6fc2cb9c4 100644 --- a/app/features/tournament-bracket/core/Tournament.timezone.test.ts +++ b/app/features/tournament-bracket/core/Tournament.timezone.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import { testTournament } from "./tests/test-utils"; @@ -19,7 +19,7 @@ describe("regularCheckInStartsAt in a DST observing timezone", () => { } }); - it("check-in opens one hour of real time before the start also on the spring forward night", () => { + test("check-in opens one hour of real time before the start also on the spring forward night", () => { // 3:30 AM EDT on the night the USA moves to daylight saving time const startsAt = new Date("2025-03-09T07:30:00Z"); @@ -32,7 +32,7 @@ describe("regularCheckInStartsAt in a DST observing timezone", () => { ); }); - it("check-in opens one hour of real time before the start also on the fall back night", () => { + test("check-in opens one hour of real time before the start also on the fall back night", () => { // 1:30 AM EST on the night the USA moves off daylight saving time const startsAt = new Date("2025-11-02T06:30:00Z"); diff --git a/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts b/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts index 97f6e4f52..d65720d5f 100644 --- a/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts +++ b/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest"; import { createResolved } from "./index"; describe("Create double elimination stage", () => { - test("should create a double elimination stage", () => { + test("creates a double elimination stage", () => { const data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], @@ -16,7 +16,7 @@ describe("Create double elimination stage", () => { expect(data.match.length).toBe(31); }); - test("should create a tournament with 256+ tournaments", () => { + test("creates a tournament with 256+ tournaments", () => { expect(() => createResolved({ type: "double_elimination", @@ -26,7 +26,7 @@ describe("Create double elimination stage", () => { ).not.toThrow(); }); - test("should create a tournament with a double grand final", () => { + test("creates a tournament with a double grand final", () => { const data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8], diff --git a/app/features/tournament-bracket/core/engine/create/helpers.test.ts b/app/features/tournament-bracket/core/engine/create/helpers.test.ts index d590ccf0c..ab61de344 100644 --- a/app/features/tournament-bracket/core/engine/create/helpers.test.ts +++ b/app/features/tournament-bracket/core/engine/create/helpers.test.ts @@ -8,7 +8,7 @@ import { import { ordering } from "./seeding"; describe("Round-robin groups", () => { - test("should place participants in groups", () => { + test("places participants in groups", () => { expect(makeGroups([1, 2, 3, 4, 5], 2)).toEqual([ [1, 2, 3], [4, 5], @@ -24,7 +24,7 @@ describe("Round-robin groups", () => { ]); }); - test("should make the rounds for a round-robin group", () => { + test("makes the rounds for a round-robin group", () => { assertRoundRobin([1, 2, 3], makeRoundRobinMatches([1, 2, 3])); assertRoundRobin([1, 2, 3, 4], makeRoundRobinMatches([1, 2, 3, 4])); assertRoundRobin([1, 2, 3, 4, 5], makeRoundRobinMatches([1, 2, 3, 4, 5])); @@ -36,7 +36,7 @@ describe("Round-robin groups", () => { }); describe("A/B divisions round-robin groups", () => { - test("should pair every A with every B exactly once for N=2..6", () => { + test("pairs every A with every B exactly once for N=2..6", () => { for (const n of [2, 3, 4, 5, 6]) { const divisionA = Array.from({ length: n }, (_, i) => i + 1); const divisionB = Array.from({ length: n }, (_, i) => i + 1 + n); @@ -49,7 +49,7 @@ describe("A/B divisions round-robin groups", () => { } }); - test("should produce N rounds and N^2 matches total", () => { + test("produces N rounds and N^2 matches total", () => { for (const n of [2, 3, 4, 5, 6]) { const divisionA = Array.from({ length: n }, (_, i) => i + 1); const divisionB = Array.from({ length: n }, (_, i) => i + 1 + n); @@ -286,37 +286,37 @@ describe("A/B division group distribution", () => { }); describe("Seed ordering methods", () => { - test("should make a natural ordering", () => { + test("makes a natural ordering", () => { expect(ordering.natural([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([ 1, 2, 3, 4, 5, 6, 7, 8, ]); }); - test("should make a reverse ordering", () => { + test("makes a reverse ordering", () => { expect(ordering.reverse([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([ 8, 7, 6, 5, 4, 3, 2, 1, ]); }); - test("should make a half shift ordering", () => { + test("makes a half shift ordering", () => { expect(ordering.half_shift([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([ 5, 6, 7, 8, 1, 2, 3, 4, ]); }); - test("should make a reverse half shift ordering", () => { + test("makes a reverse half shift ordering", () => { expect(ordering.reverse_half_shift([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([ 4, 3, 2, 1, 8, 7, 6, 5, ]); }); - test("should make a pair flip ordering", () => { + test("makes a pair flip ordering", () => { expect(ordering.pair_flip([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([ 2, 1, 4, 3, 6, 5, 8, 7, ]); }); - test("should make a snake ordering for groups", () => { + test("makes a snake ordering for groups", () => { expect( ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4), ).toEqual([1, 8, 2, 7, 3, 6, 4, 5]); diff --git a/app/features/tournament-bracket/core/engine/create/round-robin.test.ts b/app/features/tournament-bracket/core/engine/create/round-robin.test.ts index 03a8ba263..7d6bdaa99 100644 --- a/app/features/tournament-bracket/core/engine/create/round-robin.test.ts +++ b/app/features/tournament-bracket/core/engine/create/round-robin.test.ts @@ -3,7 +3,7 @@ import type { BracketData, MatchData } from "../types"; import { createResolved } from "./index"; describe("Create a round-robin stage", () => { - test("should create a round-robin stage", () => { + test("creates a round-robin stage", () => { const data = createResolved({ type: "round_robin", seeding: [1, 2, 3, 4, 5, 6, 7, 8], @@ -17,7 +17,7 @@ describe("Create a round-robin stage", () => { expect(data.match.length).toBe(12); }); - test("should drop empty slots instead of creating BYE matches", () => { + test("drops empty slots instead of creating BYE matches", () => { const data = createResolved({ type: "round_robin", seeding: [1, 2, 3, 4, 5, null, null, null], @@ -32,7 +32,7 @@ describe("Create a round-robin stage", () => { } }); - test("should not pad a short group with empty rounds when teams divide unevenly", () => { + test("does not pad a short group with empty rounds when teams divide unevenly", () => { // 5 teams in 2 groups -> groups of 3 and 2. The 2-team group must be a // clean single-round single-match group, not padded with BYE-only rounds // that strand the real match in a later round. @@ -66,7 +66,7 @@ describe("Create a round-robin stage", () => { expect(realMatchRound.number).toBe(1); }); - test("should create a round-robin stage split across multiple groups", () => { + test("creates a round-robin stage split across multiple groups", () => { const data = createResolved({ type: "round_robin", seeding: Array.from({ length: 16 }, (_, i) => i + 1), @@ -80,7 +80,7 @@ describe("Create a round-robin stage", () => { expect(data.match.length).toBe(4 * 3 * 2); }); - test("should order the groups with snake seeding", () => { + test("orders the groups with snake seeding", () => { const data = createResolved({ type: "round_robin", seeding: [1, 2, 3, 4, 5, 6, 7, 8], @@ -93,7 +93,7 @@ describe("Create a round-robin stage", () => { expect(matchById(data, 0).opponent2?.id).toBe(8); }); - test("should throw if no group count given", () => { + test("throws if no group count given", () => { expect(() => createResolved({ type: "round_robin", @@ -103,7 +103,7 @@ describe("Create a round-robin stage", () => { ).toThrow("You must specify a group count for round-robin stages."); }); - test("should throw if the group count is not strictly positive", () => { + test("throws if the group count is not strictly positive", () => { expect(() => createResolved({ type: "round_robin", diff --git a/app/features/tournament-bracket/core/engine/create/single-elimination.test.ts b/app/features/tournament-bracket/core/engine/create/single-elimination.test.ts index 0014654c4..b6e4773d4 100644 --- a/app/features/tournament-bracket/core/engine/create/single-elimination.test.ts +++ b/app/features/tournament-bracket/core/engine/create/single-elimination.test.ts @@ -3,7 +3,7 @@ import type { BracketData } from "../types"; import { createResolved } from "./index"; describe("Create single elimination stage", () => { - test("should create a single elimination stage", () => { + test("creates a single elimination stage", () => { const data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], @@ -17,7 +17,7 @@ describe("Create single elimination stage", () => { expect(data.match.length).toBe(15); }); - test("should create a single elimination stage with BYEs", () => { + test("creates a single elimination stage with BYEs", () => { const data = createResolved({ type: "single_elimination", seeding: [1, null, 3, 4, null, null, 7, 8], @@ -30,7 +30,7 @@ describe("Create single elimination stage", () => { expect(matchById(data, 5).opponent2?.id).toBe(3); }); - test("should create a single elimination stage with consolation final", () => { + test("creates a single elimination stage with consolation final", () => { const data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8], @@ -42,7 +42,7 @@ describe("Create single elimination stage", () => { expect(data.match.length).toBe(8); }); - test("should create a single elimination stage with consolation final and BYEs", () => { + test("creates a single elimination stage with consolation final and BYEs", () => { const data = createResolved({ type: "single_elimination", seeding: [null, null, null, 4, 5, 6, 7, 8], @@ -57,7 +57,7 @@ describe("Create single elimination stage", () => { expect(matchById(data, 7).opponent2?.id).toBe(null); }); - test("should create a single elimination stage with Bo3 matches", () => { + test("creates a single elimination stage with Bo3 matches", () => { const data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8], @@ -69,7 +69,7 @@ describe("Create single elimination stage", () => { expect(data.match.length).toBe(7); }); - test("should throw if the seeding has duplicate participants", () => { + test("throws if the seeding has duplicate participants", () => { expect(() => createResolved({ type: "single_elimination", diff --git a/app/features/tournament-bracket/core/engine/general.test.ts b/app/features/tournament-bracket/core/engine/general.test.ts index 0e4f2a685..632c39314 100644 --- a/app/features/tournament-bracket/core/engine/general.test.ts +++ b/app/features/tournament-bracket/core/engine/general.test.ts @@ -4,7 +4,7 @@ import * as Engine from "./index"; import type { BracketData } from "./types"; describe("BYE handling", () => { - test("should propagate BYEs through the brackets", () => { + test("propagates BYEs through the brackets", () => { const data = createResolved({ type: "double_elimination", seeding: [1, null, null, null], @@ -24,7 +24,7 @@ describe("BYE handling", () => { expect(matchById(data, 5).opponent2).toBe(null); }); - test("should handle incomplete seeding during creation", () => { + test("handles incomplete seeding during creation", () => { const data = createResolved({ type: "double_elimination", seeding: [1, 2, null, null], @@ -50,7 +50,7 @@ describe("Position checks", () => { }); }); - test("should not have a position when we don't need the origin of a participant", () => { + test("does not have a position when we don't need the origin of a participant", () => { const matchFromWbRound2 = matchById(data, 4); expect(matchFromWbRound2.opponent1?.position).toBe(undefined); expect(matchFromWbRound2.opponent2?.position).toBe(undefined); @@ -62,7 +62,7 @@ describe("Position checks", () => { expect(matchFromGrandFinal.opponent1?.position).toBe(undefined); }); - test("should have a position where we need the origin of a participant", () => { + test("has a position where we need the origin of a participant", () => { const matchFromWbRound1 = matchById(data, 0); expect(matchFromWbRound1.opponent1?.position).toBe(1); expect(matchFromWbRound1.opponent2?.position).toBe(8); @@ -80,7 +80,7 @@ describe("Position checks", () => { }); describe("Special cases", () => { - test("should pad the seeding with BYEs to the next power of two", () => { + test("pads the seeding with BYEs to the next power of two", () => { const data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4, 5, 6, 7], @@ -91,7 +91,7 @@ describe("Special cases", () => { expect(matchById(data, 0).opponent2).toBe(null); }); - test("should throw if the participant count of a stage is less than two", () => { + test("throws if the participant count of a stage is less than two", () => { expect(() => createResolved({ type: "single_elimination", @@ -121,7 +121,7 @@ describe("Seeding and ordering in elimination", () => { }); }); - test("should have the good orderings everywhere", () => { + test("has the good orderings everywhere", () => { const firstRoundMatchWB = matchById(data, 0); expect(firstRoundMatchWB.opponent1?.position).toBe(1); expect(firstRoundMatchWB.opponent2?.position).toBe(16); @@ -145,7 +145,7 @@ describe("Seeding and ordering in elimination", () => { }); describe("Reset match", () => { - test("should reset results of a match", () => { + test("resets results of a match", () => { // Seeds 1 and 2 are placed into the same first-round match (positions 1 // and 8) so that match 0 is a real two-team match under the default // space_between ordering, while the rest of the bracket is BYEs. @@ -184,7 +184,7 @@ describe("Reset match", () => { expect(matchById(data, 6).opponent2).toBe(null); }); - test("should throw when at least one of the following match is locked", () => { + test("throws when at least one of the following match is locked", () => { let data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4], diff --git a/app/features/tournament-bracket/core/engine/propagation/double-elimination.test.ts b/app/features/tournament-bracket/core/engine/propagation/double-elimination.test.ts index 5a92e9e89..3e6845e2a 100644 --- a/app/features/tournament-bracket/core/engine/propagation/double-elimination.test.ts +++ b/app/features/tournament-bracket/core/engine/propagation/double-elimination.test.ts @@ -4,7 +4,7 @@ import * as Engine from "../index"; import type { BracketData } from "../types"; describe("Previous and next match update in double elimination stage", () => { - test("should end a match and determine next matches", () => { + test("ends a match and determine next matches", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], @@ -49,7 +49,7 @@ describe("Previous and next match update in double elimination stage", () => { ).toBe(matchById(data, 0).opponent2?.id); // Loser of first match round 1 }); - test("should propagate winner when BYE is already in next match in loser bracket", () => { + test("propagates winner when BYE is already in next match in loser bracket", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, null], @@ -81,7 +81,7 @@ describe("Previous and next match update in double elimination stage", () => { expect(matchById(data, 4).opponent2?.id).toBeNull(); // Propagated winner is removed. }); - test("should determine matches in grand final", () => { + test("determines matches in grand final", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4], @@ -148,7 +148,7 @@ describe("Previous and next match update in double elimination stage", () => { ).not.toThrow(); }); - test("should determine next matches and reset them", () => { + test("determines next matches and reset them", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4], @@ -172,7 +172,7 @@ describe("Previous and next match update in double elimination stage", () => { expect(afterReset.opponent1?.position).toBe(1); // It must stay. }); - test("should choose the correct previous and next matches based on losers ordering", () => { + test("chooses the correct previous and next matches based on losers ordering", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], @@ -219,7 +219,7 @@ describe("Previous and next match update in double elimination stage", () => { expect(Engine.matchStatus(data, 8)).toBe("COMPLETED"); // WB 2.1 }); - test("should send the losers to the right LB matches in round 1", () => { + test("sends the losers to the right LB matches in round 1", () => { let data = createResolved({ type: "double_elimination", seeding: [1, 2, 3, 4, 5, 6, 7, 8], diff --git a/app/features/tournament-bracket/core/engine/propagation/round-robin.test.ts b/app/features/tournament-bracket/core/engine/propagation/round-robin.test.ts index 5bdfd1a3c..0f4c4524e 100644 --- a/app/features/tournament-bracket/core/engine/propagation/round-robin.test.ts +++ b/app/features/tournament-bracket/core/engine/propagation/round-robin.test.ts @@ -14,7 +14,7 @@ describe("Update scores in a round-robin stage", () => { }); }); - test("should set all the scores", () => { + test("sets all the scores", () => { const results: Engine.ReportResultInput[] = [ { matchId: 0, @@ -59,7 +59,7 @@ describe("Update scores in a round-robin stage", () => { } }); - test("should unlock next round matches as soon as both participants are ready", () => { + test("unlocks next round matches as soon as both participants are ready", () => { // Round robin with 4 teams: [1, 2, 3, 4] // Round 1: Match 0 (1 vs 2), Match 1 (3 vs 4) // Round 2: Match 2 (1 vs 3), Match 3 (2 vs 4) @@ -97,7 +97,7 @@ describe("Update scores in a round-robin stage", () => { expect(Engine.matchStatus(data, 3)).toBe("STARTED"); // Ready }); - test("should lock the next round again if a result of the previous round is reset", () => { + test("locks the next round again if a result of the previous round is reset", () => { data = Engine.reportResult(data, { matchId: 0, scores: [16, 9], @@ -116,7 +116,7 @@ describe("Update scores in a round-robin stage", () => { expect(Engine.matchStatus(data, 2)).toBe("PENDING"); }); - test("should keep a started next round match playable if a result of the previous round is reset (issue #2690)", () => { + test("keeps a started next round match playable if a result of the previous round is reset (issue #2690)", () => { data = Engine.reportResult(data, { matchId: 0, scores: [16, 9], @@ -146,7 +146,7 @@ describe("Update scores in a round-robin stage", () => { ).not.toThrow(); }); - test("should leave every match Ready when independentRounds is set", () => { + test("leaves every match Ready when independentRounds is set", () => { data = createResolved({ type: "round_robin", seeding: [1, 2, 3, 4], @@ -167,7 +167,7 @@ describe("Update scores in a round-robin stage", () => { ).not.toThrow(); }); - test("should let the only real match be played in a group with fewer teams than slots", () => { + test("lets the only real match be played in a group with fewer teams than slots", () => { // Group sized for 3 but only 2 teams placed (the 3rd slot is a BYE). // The two real teams only meet in round 3, preceded by two BYE rounds // that can never be reported. The real match must still be playable. @@ -192,7 +192,7 @@ describe("Update scores in a round-robin stage", () => { ).not.toThrow(); }); - test("should unlock next round matches with BYE participants", () => { + test("unlocks next round matches with BYE participants", () => { // Create a round robin with 3 teams (odd number creates rounds where one team doesn't play) data = createResolved({ type: "round_robin", diff --git a/app/features/tournament-bracket/core/engine/propagation/set.test.ts b/app/features/tournament-bracket/core/engine/propagation/set.test.ts index 69916e90b..2891656cb 100644 --- a/app/features/tournament-bracket/core/engine/propagation/set.test.ts +++ b/app/features/tournament-bracket/core/engine/propagation/set.test.ts @@ -14,7 +14,7 @@ describe("Set results in a bracket with map info", () => { data = bracketWithMaps({ count: 3, type: "BEST_OF" }); }); - test("should resolve the winner from the scores once the set is over", () => { + test("resolves the winner from the scores once the set is over", () => { data = Engine.reportGameResult(data, { matchId: 0, winnerTeamId: TEAM_ONE, @@ -31,13 +31,13 @@ describe("Set results in a bracket with map info", () => { expect(matchById(data, 0).winnerSide).toBe("opponent1"); }); - test("should resolve the winner when only the scores are reported", () => { + test("resolves the winner when only the scores are reported", () => { data = Engine.reportResult(data, { matchId: 0, scores: [0, 2] }).data; expect(matchById(data, 0).winnerSide).toBe("opponent2"); }); - test("should not resolve a winner for a play all set that ended in a tie", () => { + test("does not resolve a winner for a play all set that ended in a tie", () => { data = bracketWithMaps({ count: 2, type: "PLAY_ALL" }); data = Engine.reportResult(data, { matchId: 0, scores: [1, 1] }).data; @@ -45,7 +45,7 @@ describe("Set results in a bracket with map info", () => { expect(matchById(data, 0).winnerSide).toBeFalsy(); }); - test("should end the set early with the given winner, keeping the scores", () => { + test("ends the set early with the given winner, keeping the scores", () => { data = Engine.reportGameResult(data, { matchId: 0, winnerTeamId: TEAM_TWO, @@ -59,7 +59,7 @@ describe("Set results in a bracket with map info", () => { expect(after.opponent2?.score).toBe(1); }); - test("should clear the winner when a game is undone", () => { + test("clears the winner when a game is undone", () => { for (const _ of [1, 2]) { data = Engine.reportGameResult(data, { matchId: 0, @@ -76,7 +76,7 @@ describe("Set results in a bracket with map info", () => { expect(matchById(data, 0).opponent1?.score).toBe(1); }); - test("should clear the winner when the match is reopened", () => { + test("clears the winner when the match is reopened", () => { for (const _ of [1, 2]) { data = Engine.reportGameResult(data, { matchId: 0, @@ -92,7 +92,7 @@ describe("Set results in a bracket with map info", () => { expect(matchById(data, 0).opponent1?.score).toBe(1); }); - test("should clear the winner when a set that ended early is reopened", () => { + test("clears the winner when a set that ended early is reopened", () => { data = Engine.reportGameResult(data, { matchId: 0, winnerTeamId: TEAM_ONE, diff --git a/app/features/tournament-bracket/core/engine/propagation/single-elimination.test.ts b/app/features/tournament-bracket/core/engine/propagation/single-elimination.test.ts index 2ec74e878..48378afab 100644 --- a/app/features/tournament-bracket/core/engine/propagation/single-elimination.test.ts +++ b/app/features/tournament-bracket/core/engine/propagation/single-elimination.test.ts @@ -4,7 +4,7 @@ import * as Engine from "../index"; import type { BracketData } from "../types"; describe("Previous and next match update", () => { - test("should determine matches in consolation final", () => { + test("determines matches in consolation final", () => { let data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4], @@ -33,7 +33,7 @@ describe("Previous and next match update", () => { expect(Engine.matchStatus(data, 3)).toBe("STARTED"); }); - test("should play both the final and consolation final in parallel", () => { + test("plays both the final and consolation final in parallel", () => { let data = createResolved({ type: "single_elimination", seeding: [1, 2, 3, 4], diff --git a/app/features/tournament-bracket/core/engine/propagation/update.test.ts b/app/features/tournament-bracket/core/engine/propagation/update.test.ts index 4f8762c63..a7de5cfb6 100644 --- a/app/features/tournament-bracket/core/engine/propagation/update.test.ts +++ b/app/features/tournament-bracket/core/engine/propagation/update.test.ts @@ -16,7 +16,7 @@ describe("Update matches", () => { data = createResolved(EXAMPLE); }); - test("should start a match", () => { + test("starts a match", () => { expect(Engine.matchStatus(data, 0)).toBe("STARTED"); data = Engine.reportResult(data, { @@ -28,7 +28,7 @@ describe("Update matches", () => { expect(matchById(data, 0).opponent1?.score).toBe(0); }); - test("should update the scores for a match", () => { + test("updates the scores for a match", () => { data = Engine.reportResult(data, { matchId: 0, scores: [2, 1], @@ -42,7 +42,7 @@ describe("Update matches", () => { expect(after.opponent1?.id).toBe(1); }); - test("should end the match by only setting the winner", () => { + test("ends the match by only setting the winner", () => { expect(matchById(data, 0).winnerSide).toBeFalsy(); data = Engine.reportResult(data, { @@ -54,7 +54,7 @@ describe("Update matches", () => { expect(matchById(data, 0).winnerSide).toBe("opponent1"); }); - test("should change the winner of the match and update in the next match", () => { + test("changes the winner of the match and update in the next match", () => { data = Engine.reportResult(data, { matchId: 0, winnerSide: "opponent1", @@ -74,7 +74,7 @@ describe("Update matches", () => { expect(matchById(data, 8).opponent1?.id).toBe(16); }); - test("should update the status of the next match", () => { + test("updates the status of the next match", () => { data = Engine.reportResult(data, { matchId: 0, winnerSide: "opponent1", @@ -90,7 +90,7 @@ describe("Update matches", () => { expect(Engine.matchStatus(data, 8)).toBe("STARTED"); }); - test("should remove results from a match without score", () => { + test("removes results from a match without score", () => { data = Engine.reportResult(data, { matchId: 0, winnerSide: "opponent1", @@ -102,7 +102,7 @@ describe("Update matches", () => { expect(matchById(data, 0).winnerSide).toBeFalsy(); }); - test("should remove results from a match with score", () => { + test("removes results from a match with score", () => { data = Engine.reportResult(data, { matchId: 0, scores: [16, 12], @@ -115,7 +115,7 @@ describe("Update matches", () => { expect(matchById(data, 0).winnerSide).toBeFalsy(); }); - test("should keep the scores as they are if none given", () => { + test("keeps the scores as they are if none given", () => { data = Engine.reportResult(data, { matchId: 1, scores: [1, 0], @@ -129,7 +129,7 @@ describe("Update matches", () => { expect(after.opponent2?.score).toBe(0); }); - test("should end the match by setting the winner and the scores", () => { + test("ends the match by setting the winner and the scores", () => { data = Engine.reportResult(data, { matchId: 1, scores: [6, 3], @@ -152,7 +152,7 @@ describe("Locked matches", () => { data = createResolved(EXAMPLE); }); - test("should throw when the matches leading to the match have not been completed yet", () => { + test("throws when the matches leading to the match have not been completed yet", () => { expect(() => Engine.reportResult(data, { matchId: 0 })).not.toThrow(); // No problem when no previous match. expect(() => Engine.reportResult(data, { matchId: 8 })).toThrow( diff --git a/app/features/tournament-bracket/core/summarizer.test.ts b/app/features/tournament-bracket/core/summarizer.test.ts index bb5279e95..0187a60ba 100644 --- a/app/features/tournament-bracket/core/summarizer.test.ts +++ b/app/features/tournament-bracket/core/summarizer.test.ts @@ -6,6 +6,7 @@ import type { Tables } from "../../../db/tables"; import type * as Progression from "./Progression"; import { tournamentSummary } from "./summarizer.server"; import type { TournamentDataTeam } from "./Tournament.server"; +import { tournamentCtxTeam } from "./tests/test-utils"; const createOpponent = ( id: number, @@ -22,27 +23,13 @@ const createOpponent = ( }); describe("tournamentSummary()", () => { - const createTeam = ( - teamId: number, - userIds: number[], - ): TournamentDataTeam => ({ - checkIns: [], - createdAt: 0, - id: teamId, - avgSeedingSkillOrdinal: null, - startingBracketIdx: null, - abDivision: null, - hasMapPool: false, - inviteCode: null, - memberUserIds: userIds, - ownerUserId: userIds[0] ?? null, - name: `Team ${teamId}`, - prefersNotToHost: 0, - droppedOut: 0, - logoUrl: null, - seed: 1, - activeRosterUserIds: [], - }); + const createTeam = (teamId: number, userIds: number[]) => + tournamentCtxTeam(teamId, { + checkIns: [], + memberUserIds: userIds, + ownerUserId: userIds[0] ?? null, + seed: 1, + }); function summarize({ results, diff --git a/app/features/tournament-bracket/tournament-bracket-search-params.test.ts b/app/features/tournament-bracket/tournament-bracket-search-params.test.ts index 37153874e..2c487f54b 100644 --- a/app/features/tournament-bracket/tournament-bracket-search-params.test.ts +++ b/app/features/tournament-bracket/tournament-bracket-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,14 +6,14 @@ import { import { tournamentBracketsSearchParams } from "./tournament-bracket-search-params"; describe("tournamentBracketsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentBracketsSearchParams, { idx: [0, 3], group: [1, 173], }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tournamentBracketsSearchParams, "idx", [ ["-1"], ["abc"], diff --git a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts index 7a42684b2..e49a5f2e4 100644 --- a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts +++ b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("~/features/chat/ChatSystemMessage.server", () => ({ send: vi.fn(), @@ -98,7 +98,7 @@ const removeMemberAction = ({ }, ); -const createTeam = (tournamentId: number, memberUserIds: number[]) => +const createTournamentTeam = (tournamentId: number, memberUserIds: number[]) => TournamentTeamFactory.create( { tournamentId, memberUserIds }, { isCheckedIn: true }, @@ -115,21 +115,24 @@ describe("Tournament match page", () => { tournamentId = tournament.id; // six members, so that team one has subs and a roster to pick from them - teamOne = await createTeam(tournamentId, [organizerId, ...users.ids(5)]); - await createTeam(tournamentId, users.ids(9).slice(5)); + teamOne = await createTournamentTeam(tournamentId, [ + organizerId, + ...users.ids(5), + ]); + await createTournamentTeam(tournamentId, users.ids(9).slice(5)); [{ id: matchId }] = await TournamentFactory.startBracket(tournamentId); }); describe("results", () => { - it("is empty array for new match", async () => { + test("is empty array for new match", async () => { const data = await loadMatchData(); expect(data.results).toBeDefined(); expect(data.results.length).toBe(0); }); - it("returns results for an in-progress match with correct fields", async () => { + test("returns results for an in-progress match with correct fields", async () => { await setActiveRosterAction(); await reportScoreAction({ position: 0 }); @@ -152,7 +155,7 @@ describe("Tournament match page", () => { expect(result.winnerTeamId).toBe(teamOne.id); }); - it("returns results for a completed match", async () => { + test("returns results for a completed match", async () => { await setActiveRosterAction(); await reportScoreAction({ position: 0 }); await reportScoreAction({ position: 1 }); @@ -164,7 +167,7 @@ describe("Tournament match page", () => { }); describe("mapList", () => { - it("returns TO picked map list for match", async () => { + test("returns TO picked map list for match", async () => { const data = await loadMatchData(); expect(data.mapList).toBeDefined(); @@ -176,13 +179,13 @@ describe("Tournament match page", () => { }); describe("matchIsOver", () => { - it("is false for new match", async () => { + test("is false for new match", async () => { const data = await loadMatchData(); expect(data.matchIsOver).toBe(false); }); - it("is true for a completed match", async () => { + test("is true for a completed match", async () => { await setActiveRosterAction(); await reportScoreAction({ position: 0 }); await reportScoreAction({ position: 1 }); @@ -194,7 +197,7 @@ describe("Tournament match page", () => { }); describe("active roster", () => { - it("should return error if submitted active roster contains user id not in the team", async () => { + test("returns error if submitted active roster contains user id not in the team", async () => { const res = await setActiveRosterAction(teamOne.id, [ ...activeRoster().slice(0, ROSTER_SIZE - 1), users.id(6), @@ -203,7 +206,7 @@ describe("Tournament match page", () => { assertResponseErrored(res, "Invalid roster"); }); - it("should return error if submitted active roster is not of correct length", async () => { + test("returns error if submitted active roster is not of correct length", async () => { const res = await setActiveRosterAction( teamOne.id, activeRoster().slice(0, ROSTER_SIZE - 1), @@ -212,13 +215,13 @@ describe("Tournament match page", () => { assertResponseErrored(res, "Invalid roster length"); }); - it("should return error if trying to report score without active roster", async () => { + test("returns error if trying to report score without active roster", async () => { const res = await reportScoreAction({ position: 0 }); assertResponseErrored(res, "Team one has no active roster"); }); - it("should wipe active roster if member in it removed by tournament admin", async () => { + test("wipes active roster if member in it removed by tournament admin", async () => { await setActiveRosterAction(); await removeMemberAction({ teamId: teamOne.id, userId: users.id(1) }); @@ -227,7 +230,7 @@ describe("Tournament match page", () => { assertResponseErrored(res, "Team one has no active roster"); }); - it("should retain active roster if member removed by tournament admin was not in it", async () => { + test("retains active roster if member removed by tournament admin was not in it", async () => { await setActiveRosterAction(); // team one's sixth member, so not one of the four it fields @@ -238,15 +241,18 @@ describe("Tournament match page", () => { expect(res).toBe(null); }); - it("should not require setting active roster if both teams have no subs", async () => { + test("does not require setting active roster if both teams have no subs", async () => { const tournament = await TournamentFactory.create({ authorId: organizerId, }); - const subLessTeam = await createTeam(tournament.id, [ + const subLessTeam = await createTournamentTeam(tournament.id, [ organizerId, ...users.ids(ROSTER_SIZE - 1), ]); - await createTeam(tournament.id, users.ids(ROSTER_SIZE * 2 - 1).slice(3)); + await createTournamentTeam( + tournament.id, + users.ids(ROSTER_SIZE * 2 - 1).slice(3), + ); const [match] = await TournamentFactory.startBracket(tournament.id); @@ -261,7 +267,7 @@ describe("Tournament match page", () => { }); describe("locked match", () => { - it("should return error when reporting score for a match waiting on previous matches", async () => { + test("returns error when reporting score for a match waiting on previous matches", async () => { await setActiveRosterAction(); // the state under test is one an earlier match of a larger bracket puts this // row in, not one the match was created in @@ -281,7 +287,7 @@ describe("Tournament match page", () => { describe("BYE matches", () => { // as above: a BYE and a TBD opponent are states the surrounding bracket // produces, so they are written here rather than seeded - it("should 404 when accessing a BYE match", async () => { + test("404s when accessing a BYE match", async () => { // biome-ignore lint/plugin: as above await db .updateTable("TournamentMatch") @@ -292,7 +298,7 @@ describe("Tournament match page", () => { await expect(loadMatchData()).rejects.toThrow("404"); }); - it("should not 404 when an opponent is a TBD placeholder waiting for an earlier match", async () => { + test("nots 404 when an opponent is a TBD placeholder waiting for an earlier match", async () => { // biome-ignore lint/plugin: as above await db .updateTable("TournamentMatch") diff --git a/app/features/tournament-organization/tournament-organization-search-params.test.ts b/app/features/tournament-organization/tournament-organization-search-params.test.ts index 729cc9bca..14114ced5 100644 --- a/app/features/tournament-organization/tournament-organization-search-params.test.ts +++ b/app/features/tournament-organization/tournament-organization-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,7 +6,7 @@ import { import { tournamentOrganizationSearchParams } from "./tournament-organization-search-params"; describe("tournamentOrganizationSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentOrganizationSearchParams, { month: [0, 11], year: [2020, 2100], @@ -16,7 +16,7 @@ describe("tournamentOrganizationSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tournamentOrganizationSearchParams, "month", [ ["12"], ["-1"], diff --git a/app/features/tournament/TournamentAuditLogRepository.server.test.ts b/app/features/tournament/TournamentAuditLogRepository.server.test.ts index 24f6df5aa..a346539b8 100644 --- a/app/features/tournament/TournamentAuditLogRepository.server.test.ts +++ b/app/features/tournament/TournamentAuditLogRepository.server.test.ts @@ -9,13 +9,16 @@ import { withUserId } from "~/utils/Test"; import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server"; import * as TournamentTeamRepository from "./TournamentTeamRepository.server"; -let actor: { id: number }; -let subject: { id: number }; +const users = UserFactory.pool(); +/** Whoever performs the audited action, and whoever it is performed on. */ +const actorId = () => users.id(1); +const subjectId = () => users.id(2); -const createTournament = () => TournamentFactory.create({ authorId: actor.id }); +const createTournament = () => + TournamentFactory.create({ authorId: actorId() }); /** Registering a team is itself audited, so the team comes with a `TEAM_REGISTERED` event. */ -const createTeam = ( +const createTournamentTeam = ( tournamentId: number, name: string, options?: { isCheckedIn: boolean }, @@ -23,14 +26,14 @@ const createTeam = ( TournamentTeamFactory.create( { tournamentId, - memberUserIds: [actor.id], + memberUserIds: [actorId()], team: { name, prefersNotToHost: 0, teamId: null }, }, options, ); const deleteTeam = (tournamentTeamId: number) => - withUserId(actor.id, () => + withUserId(actorId(), () => TournamentTeamRepository.deleteById(tournamentTeamId), ); @@ -52,12 +55,12 @@ const insertEvent = ({ describe("TournamentAuditLogRepository", () => { beforeEach(async () => { - [actor, subject] = await UserFactory.createMany(3); + await users.create(3); }); test("insert creates a stable history row from the live team", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Team Olive"); + const team = await createTournamentTeam(tournament.id, "Team Olive"); const teams = await TournamentAuditLogRepository.findTeamsByTournamentId( tournament.id, @@ -72,7 +75,7 @@ describe("TournamentAuditLogRepository", () => { const tournament = await createTournament(); await TournamentTeamFactory.create({ tournamentId: tournament.id, - memberUserIds: [actor.id, subject.id], + memberUserIds: [actorId(), subjectId()], team: { name: "Team Olive", prefersNotToHost: 0, teamId: null }, }); @@ -85,15 +88,15 @@ describe("TournamentAuditLogRepository", () => { expect(events).toHaveLength(2); // newest first expect(events[0].type).toBe("MEMBER_ADDED"); - expect(events[0].actor?.id).toBe(actor.id); - expect(events[0].subject?.id).toBe(subject.id); + expect(events[0].actor?.id).toBe(actorId()); + expect(events[0].subject?.id).toBe(subjectId()); expect(events[0].team?.name).toBe("Team Olive"); expect(events[1].type).toBe("TEAM_REGISTERED"); }); test("team name survives the team being deleted", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Team Olive"); + const team = await createTournamentTeam(tournament.id, "Team Olive"); await deleteTeam(team.id); @@ -109,11 +112,11 @@ describe("TournamentAuditLogRepository", () => { test("a reused team id does not collapse two teams into one history", async () => { const tournament = await createTournament(); - const teamA = await createTeam(tournament.id, "Team A"); + const teamA = await createTournamentTeam(tournament.id, "Team A"); await deleteTeam(teamA.id); - const teamB = await createTeam(tournament.id, "Team B"); + const teamB = await createTournamentTeam(tournament.id, "Team B"); // SQLite reuses the highest deleted rowid for the next insert expect(teamB.id).toBe(teamA.id); @@ -144,10 +147,10 @@ describe("TournamentAuditLogRepository", () => { test("filters by event type and by team", async () => { const tournament = await createTournament(); - const teamA = await createTeam(tournament.id, "Team A", { + const teamA = await createTournamentTeam(tournament.id, "Team A", { isCheckedIn: true, }); - await createTeam(tournament.id, "Team B"); + await createTournamentTeam(tournament.id, "Team B"); const byType = await TournamentAuditLogRepository.findByTournamentId({ tournamentId: tournament.id, @@ -181,13 +184,13 @@ describe("TournamentAuditLogRepository", () => { test("paginates via limit and offset", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Team Olive"); + const team = await createTournamentTeam(tournament.id, "Team Olive"); // two more on top of the team's own TEAM_REGISTERED for (let i = 0; i < 2; i++) { await insertEvent({ type: "TEAM_CHECKED_IN", - actorUserId: actor.id, + actorUserId: actorId(), tournamentTeamId: team.id, }); } @@ -209,11 +212,11 @@ describe("TournamentAuditLogRepository", () => { test("stores and reads back metadata", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Team Olive"); + const team = await createTournamentTeam(tournament.id, "Team Olive"); await insertEvent({ type: "TEAM_CHECKED_IN", - actorUserId: actor.id, + actorUserId: actorId(), tournamentTeamId: team.id, metadata: { bracketIdx: 2 }, }); @@ -229,13 +232,13 @@ describe("TournamentAuditLogRepository", () => { test("stores and reads back the in-game name for name change events", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Team Olive"); + const team = await createTournamentTeam(tournament.id, "Team Olive"); await insertEvent({ type: "UPDATE_IN_GAME_NAME", - actorUserId: actor.id, + actorUserId: actorId(), tournamentTeamId: team.id, - subjectUserId: subject.id, + subjectUserId: subjectId(), metadata: { inGameName: "New IGN#1234" }, }); @@ -246,13 +249,13 @@ describe("TournamentAuditLogRepository", () => { }); expect(events[0].type).toBe("UPDATE_IN_GAME_NAME"); - expect(events[0].subject?.id).toBe(subject.id); + expect(events[0].subject?.id).toBe(subjectId()); expect(events[0].metadata?.inGameName).toBe("New IGN#1234"); }); test("updateTeamHistoryName keeps the preserved name current", async () => { const tournament = await createTournament(); - const team = await createTeam(tournament.id, "Old Name"); + const team = await createTournamentTeam(tournament.id, "Old Name"); await db.transaction().execute((trx) => TournamentAuditLogRepository.updateTeamHistoryName(trx, { diff --git a/app/features/tournament/TournamentRepository.finalize.test.ts b/app/features/tournament/TournamentRepository.finalize.test.ts index b62b4b223..da5a067dd 100644 --- a/app/features/tournament/TournamentRepository.finalize.test.ts +++ b/app/features/tournament/TournamentRepository.finalize.test.ts @@ -6,10 +6,10 @@ import { db } from "~/db/sql"; import type { TournamentSummary } from "../tournament-bracket/core/summarizer.server"; import * as TournamentRepository from "./TournamentRepository.server"; -let player: { id: number }; +const users = UserFactory.pool(); const createTournament = () => - TournamentFactory.create({ authorId: player.id }); + TournamentFactory.create({ authorId: users.id(1) }); const emptySummary = ( skills: TournamentSummary["skills"], @@ -39,12 +39,12 @@ const finalizePriorSeason = async ( describe("TournamentRepository.finalize", () => { beforeEach(async () => { // four users so that the "1-2-3-4" team identifier the tests use names real ones - [player] = await UserFactory.createMany(4); + await users.create(4); }); test("matchesCount on a new season's Skill row does not include prior seasons", async () => { await finalizePriorSeason({ - userId: player.id, + userId: users.id(1), identifier: null, mu: 25, sigma: 8.333, @@ -58,7 +58,7 @@ describe("TournamentRepository.finalize", () => { season: 1, summary: emptySummary([ { - userId: player.id, + userId: users.id(1), identifier: null, mu: 25, sigma: 8.333, @@ -70,7 +70,7 @@ describe("TournamentRepository.finalize", () => { const inserted = await db .selectFrom("Skill") .select("matchesCount") - .where("userId", "=", player.id) + .where("userId", "=", users.id(1)) .where("season", "=", 1) .executeTakeFirstOrThrow(); @@ -114,7 +114,7 @@ describe("TournamentRepository.finalize", () => { test("finalizes and records placements when season is undefined (between-seasons tournament)", async () => { await finalizePriorSeason({ - userId: player.id, + userId: users.id(1), identifier: null, mu: 25, sigma: 8.333, @@ -124,7 +124,7 @@ describe("TournamentRepository.finalize", () => { const { id: tournamentId } = await createTournament(); const { id: tournamentTeamId } = await TournamentTeamFactory.create({ tournamentId, - memberUserIds: [player.id], + memberUserIds: [users.id(1)], }); await TournamentRepository.finalize({ @@ -137,7 +137,7 @@ describe("TournamentRepository.finalize", () => { playerResultDeltas: [], tournamentResults: [ { - userId: player.id, + userId: users.id(1), placement: 1, participantCount: 1, tournamentTeamId, @@ -163,7 +163,7 @@ describe("TournamentRepository.finalize", () => { .selectFrom("TournamentResult") .select("placement") .where("tournamentId", "=", tournamentId) - .where("userId", "=", player.id) + .where("userId", "=", users.id(1)) .executeTakeFirstOrThrow(); expect(tournament.isFinalized).toBe(1); @@ -178,7 +178,7 @@ describe("TournamentRepository.finalize", () => { season: 1, summary: emptySummary([ { - userId: player.id, + userId: users.id(1), identifier: null, mu: 25, sigma: 8.333, @@ -193,7 +193,7 @@ describe("TournamentRepository.finalize", () => { season: 1, summary: emptySummary([ { - userId: player.id, + userId: users.id(1), identifier: null, mu: 25, sigma: 8.333, @@ -205,7 +205,7 @@ describe("TournamentRepository.finalize", () => { const second = await db .selectFrom("Skill") .select("matchesCount") - .where("userId", "=", player.id) + .where("userId", "=", users.id(1)) .where("tournamentId", "=", secondTournamentId) .executeTakeFirstOrThrow(); diff --git a/app/features/tournament/TournamentRepository.searchByName.test.ts b/app/features/tournament/TournamentRepository.searchByName.test.ts index 3c3450fff..80b116cba 100644 --- a/app/features/tournament/TournamentRepository.searchByName.test.ts +++ b/app/features/tournament/TournamentRepository.searchByName.test.ts @@ -7,11 +7,11 @@ import * as TournamentRepository from "./TournamentRepository.server"; const QUERY = "In The Zone"; -let author: { id: number }; +const users = UserFactory.pool(); const createTournament = (name: string, startsAt: Date) => TournamentFactory.create({ - authorId: author.id, + authorId: users.id(1), name, startTimes: [dateToDatabaseTimestamp(startsAt)], }); @@ -23,7 +23,7 @@ const search = async (limit = 10) => describe("TournamentRepository.searchByName", () => { beforeEach(async () => { - author = await UserFactory.create(); + await users.create(1); }); test("sorts a tournament that is happening right now first", async () => { diff --git a/app/features/tournament/TournamentRepository.showcaseCounts.test.ts b/app/features/tournament/TournamentRepository.showcaseCounts.test.ts index aca3ec737..010f23856 100644 --- a/app/features/tournament/TournamentRepository.showcaseCounts.test.ts +++ b/app/features/tournament/TournamentRepository.showcaseCounts.test.ts @@ -9,12 +9,12 @@ import * as TournamentTeamRepository from "./TournamentTeamRepository.server"; const MEMBERS_PER_TEAM = 4; const TEAM_COUNT = 3; -let users: Array<{ id: number }>; +const users = UserFactory.pool(); const createTournament = () => - TournamentFactory.create({ authorId: users[0].id }); + TournamentFactory.create({ authorId: users.id(1) }); -const createTeam = ( +const createTournamentTeam = ( tournamentId: number, { nth, isCheckedIn }: { nth: number; isCheckedIn: boolean }, ) => @@ -22,8 +22,8 @@ const createTeam = ( { tournamentId, memberUserIds: users - .slice(nth * MEMBERS_PER_TEAM, (nth + 1) * MEMBERS_PER_TEAM) - .map((user) => user.id), + .ids() + .slice(nth * MEMBERS_PER_TEAM, (nth + 1) * MEMBERS_PER_TEAM), }, { isCheckedIn }, ); @@ -38,14 +38,14 @@ const showcaseCounts = async (tournamentId: number) => { describe("TournamentRepository.findShowcaseCountsById", () => { beforeEach(async () => { - users = await UserFactory.createMany(MEMBERS_PER_TEAM * TEAM_COUNT); + await users.create(MEMBERS_PER_TEAM * TEAM_COUNT); }); test("counts every registered team before a bracket has been started", async () => { const { id: tournamentId } = await createTournament(); - await createTeam(tournamentId, { nth: 0, isCheckedIn: true }); - await createTeam(tournamentId, { nth: 1, isCheckedIn: true }); - await createTeam(tournamentId, { nth: 2, isCheckedIn: false }); + await createTournamentTeam(tournamentId, { nth: 0, isCheckedIn: true }); + await createTournamentTeam(tournamentId, { nth: 1, isCheckedIn: true }); + await createTournamentTeam(tournamentId, { nth: 2, isCheckedIn: false }); const counts = await showcaseCounts(tournamentId); @@ -55,9 +55,9 @@ describe("TournamentRepository.findShowcaseCountsById", () => { test("counts only checked in teams after a bracket has been started", async () => { const { id: tournamentId } = await createTournament(); - await createTeam(tournamentId, { nth: 0, isCheckedIn: true }); - await createTeam(tournamentId, { nth: 1, isCheckedIn: true }); - await createTeam(tournamentId, { nth: 2, isCheckedIn: false }); + await createTournamentTeam(tournamentId, { nth: 0, isCheckedIn: true }); + await createTournamentTeam(tournamentId, { nth: 1, isCheckedIn: true }); + await createTournamentTeam(tournamentId, { nth: 2, isCheckedIn: false }); await TournamentFactory.startBracket(tournamentId); @@ -69,8 +69,11 @@ describe("TournamentRepository.findShowcaseCountsById", () => { test("counts a team having several check in rows once", async () => { const { id: tournamentId } = await createTournament(); - const team = await createTeam(tournamentId, { nth: 0, isCheckedIn: true }); - await createTeam(tournamentId, { nth: 1, isCheckedIn: true }); + const team = await createTournamentTeam(tournamentId, { + nth: 0, + isCheckedIn: true, + }); + await createTournamentTeam(tournamentId, { nth: 1, isCheckedIn: true }); await TournamentFactory.startBracket(tournamentId); await actAs(team.ownerUserId, () => diff --git a/app/features/tournament/TournamentTeamRepository.server.test.ts b/app/features/tournament/TournamentTeamRepository.server.test.ts index 671b9087e..f5d981567 100644 --- a/app/features/tournament/TournamentTeamRepository.server.test.ts +++ b/app/features/tournament/TournamentTeamRepository.server.test.ts @@ -37,10 +37,11 @@ const FINAL_MAPS: TournamentFactory.RoundMaps = { list: [{ mode: "TC", stageId: 2 }], }; -let organizer: { id: number }; -let owner: { id: number }; -let member: { id: number }; -let anotherMember: { id: number }; +const users = UserFactory.pool(); +const organizerId = () => users.id(1); +const ownerId = () => users.id(2); +const memberId = () => users.id(3); +const anotherMemberId = () => users.id(4); const membersByTeamId = (tournamentTeamId: number) => db @@ -69,24 +70,24 @@ const roleOf = ( describe("TournamentTeamRepository", () => { beforeEach(async () => { - [organizer, owner, member, anotherMember] = await UserFactory.createMany(4); + await users.create(4); }); describe("upsertRegistration", () => { test("gives a new team's members their roles", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); - await withUserId(organizer.id, () => + await withUserId(organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, - membersToAdd: [owner.id, member.id, anotherMember.id], + membersToAdd: [ownerId(), memberId(), anotherMemberId()], membersToRemove: [], inGameNameUpdates: [], tournamentNameUpdates: [], @@ -102,31 +103,31 @@ describe("TournamentTeamRepository", () => { const members = await membersByTeamId(team.id); expect(members).toHaveLength(3); - expect(roleOf(members, owner.id)).toBe("OWNER"); - expect(roleOf(members, member.id)).toBe("REGULAR"); - expect(roleOf(members, anotherMember.id)).toBe("REGULAR"); + expect(roleOf(members, ownerId())).toBe("OWNER"); + expect(roleOf(members, memberId())).toBe("REGULAR"); + expect(roleOf(members, anotherMemberId())).toBe("REGULAR"); }); test("added members of an existing team don't take the owner role", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); const team = await TournamentTeamFactory.create({ tournamentId: tournament.id, - memberUserIds: [owner.id], + memberUserIds: [ownerId()], team: { name: "Team Olive", prefersNotToHost: 0, teamId: null }, }); - await withUserId(organizer.id, () => + await withUserId(organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentTeamId: team.id, tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, - membersToAdd: [member.id, anotherMember.id], + membersToAdd: [memberId(), anotherMemberId()], membersToRemove: [], inGameNameUpdates: [], tournamentNameUpdates: [], @@ -136,25 +137,25 @@ describe("TournamentTeamRepository", () => { const members = await membersByTeamId(team.id); expect(members).toHaveLength(3); - expect(roleOf(members, owner.id)).toBe("OWNER"); - expect(roleOf(members, member.id)).toBe("REGULAR"); - expect(roleOf(members, anotherMember.id)).toBe("REGULAR"); + expect(roleOf(members, ownerId())).toBe("OWNER"); + expect(roleOf(members, memberId())).toBe("REGULAR"); + expect(roleOf(members, anotherMemberId())).toBe("REGULAR"); }); test("marks added members as organizer added", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); - await withUserId(organizer.id, () => + await withUserId(organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, - membersToAdd: [owner.id, member.id], + membersToAdd: [ownerId(), memberId()], membersToRemove: [], inGameNameUpdates: [], tournamentNameUpdates: [], @@ -176,58 +177,58 @@ describe("TournamentTeamRepository", () => { test("updates tournament names of members", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); const { appliedTournamentNameChanges } = await withUserId( - organizer.id, + organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, - membersToAdd: [owner.id, member.id], + membersToAdd: [ownerId(), memberId()], membersToRemove: [], inGameNameUpdates: [], tournamentNameUpdates: [ - { userId: owner.id, tournamentName: "Sendou" }, - { userId: member.id, tournamentName: null }, + { userId: ownerId(), tournamentName: "Sendou" }, + { userId: memberId(), tournamentName: null }, ], }), ); expect(appliedTournamentNameChanges).toEqual([ { - userId: owner.id, + userId: ownerId(), previousTournamentName: null, tournamentName: "Sendou", }, ]); - expect(await tournamentNameOf(owner.id)).toBe("Sendou"); - expect(await tournamentNameOf(member.id)).toBeNull(); + expect(await tournamentNameOf(ownerId())).toBe("Sendou"); + expect(await tournamentNameOf(memberId())).toBeNull(); }); test("logs a tournament name change in the audit log", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); - await withUserId(organizer.id, () => + await withUserId(organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, - membersToAdd: [owner.id], + membersToAdd: [ownerId()], membersToRemove: [], inGameNameUpdates: [], tournamentNameUpdates: [ - { userId: owner.id, tournamentName: "Sendou" }, + { userId: ownerId(), tournamentName: "Sendou" }, ], }), ); @@ -243,35 +244,35 @@ describe("TournamentTeamRepository", () => { .execute(); expect(events).toHaveLength(1); - expect(events[0].actorUserId).toBe(organizer.id); - expect(events[0].subjectUserId).toBe(owner.id); + expect(events[0].actorUserId).toBe(organizerId()); + expect(events[0].subjectUserId).toBe(ownerId()); expect(events[0].metadata?.tournamentName).toBe("Sendou"); }); test("does not touch a tournament name that did not change", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); const team = await TournamentTeamFactory.create({ tournamentId: tournament.id, - memberUserIds: [owner.id], + memberUserIds: [ownerId()], team: { name: "Team Olive", prefersNotToHost: 0, teamId: null }, }); const upsert = (tournamentName: string) => - withUserId(organizer.id, () => + withUserId(organizerId(), () => TournamentTeamRepository.upsertRegistration({ tournamentTeamId: team.id, tournamentId: tournament.id, name: "Team Olive", teamId: null, avatarImgId: null, - ownerUserId: owner.id, + ownerUserId: ownerId(), ownerChange: null, membersToAdd: [], membersToRemove: [], inGameNameUpdates: [], - tournamentNameUpdates: [{ userId: owner.id, tournamentName }], + tournamentNameUpdates: [{ userId: ownerId(), tournamentName }], }), ); @@ -293,25 +294,25 @@ describe("TournamentTeamRepository", () => { describe("join", () => { test("joining on your own is not marked as organizer added", async () => { const tournament = await TournamentFactory.create({ - authorId: organizer.id, + authorId: organizerId(), }); const team = await TournamentTeamFactory.create({ tournamentId: tournament.id, - memberUserIds: [owner.id], + memberUserIds: [ownerId()], team: { name: "Team Olive", prefersNotToHost: 0, teamId: null }, }); - await withUserId(member.id, () => + await withUserId(memberId(), () => TournamentTeamRepository.join({ newTeamId: team.id, - userId: member.id, + userId: memberId(), }), ); const members = await membersByTeamId(team.id); expect( - members.find((teamMember) => teamMember.userId === member.id) + members.find((teamMember) => teamMember.userId === memberId()) ?.isOrganizerAdded, ).toBe(0); }); @@ -325,7 +326,7 @@ describe("TournamentTeamRepository", () => { const players = await UserFactory.createMany(TEAM_COUNT); const tournament = await TournamentFactory.createPlayed( { - authorId: organizer.id, + authorId: organizerId(), bracketProgression: POOLS_TO_FINAL, minMembersPerTeam: 1, }, diff --git a/app/features/tournament/core/Standings.test.ts b/app/features/tournament/core/Standings.test.ts index 391a789b7..7cf25a284 100644 --- a/app/features/tournament/core/Standings.test.ts +++ b/app/features/tournament/core/Standings.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as Engine from "~/features/tournament-bracket/core/engine"; import { createResolved } from "~/features/tournament-bracket/core/engine/create"; import type { BracketData } from "~/features/tournament-bracket/core/engine/types"; @@ -16,7 +16,7 @@ import { } from "./Standings"; describe("tournamentStandings", () => { - it("returns single-division standings for a tournament with one starting bracket", () => { + test("returns single-division standings for a tournament with one starting bracket", () => { const tournament = singleEliminationTournament(); const result = tournamentStandings(tournament); @@ -28,7 +28,7 @@ describe("tournamentStandings", () => { expect(result.standings[0].team.id).toBe(1); }); - it("returns one div per starting bracket for a tournament with multiple starting brackets", () => { + test("returns one div per starting bracket for a tournament with multiple starting brackets", () => { const tournament = testTournament({ ctx: { settings: { bracketProgression: progressions.manyStartBrackets }, @@ -54,7 +54,7 @@ describe("tournamentStandings", () => { expect(new Set(divs).size).toBe(2); }); - it("splits A/B divisions finals into 'A' and 'B' divs with teams partitioned by abDivision", () => { + test("splits A/B divisions finals into 'A' and 'B' divs with teams partitioned by abDivision", () => { const tournament = abDivisionsTournament(); const result = tournamentStandings(tournament); @@ -70,7 +70,7 @@ describe("tournamentStandings", () => { expect(b.standings.every((s) => s.team.abDivision === 1)).toBe(true); }); - it("re-numbers placements within each A/B division starting from 1", () => { + test("re-numbers placements within each A/B division starting from 1", () => { const tournament = abDivisionsTournament(); const result = tournamentStandings(tournament); @@ -81,7 +81,7 @@ describe("tournamentStandings", () => { expect(b.standings.map((s) => s.placement)).toEqual([1, 2]); }); - it("breaks ties of a bracket with the results of its underground bracket", () => { + test("breaks ties of a bracket with the results of its underground bracket", () => { const tournament = singleEliminationWithUndergroundTournament(); const result = tournamentStandings(tournament); @@ -97,7 +97,7 @@ describe("tournamentStandings", () => { ]); }); - it("keeps teams that skipped the underground bracket tied below those who played it", () => { + test("keeps teams that skipped the underground bracket tied below those who played it", () => { const tournament = singleEliminationWithUndergroundTournament({ undergroundSeeding: [7, 8], }); @@ -113,7 +113,7 @@ describe("tournamentStandings", () => { ]); }); - it("does not break ties with an underground bracket that is still in progress", () => { + test("does not break ties with an underground bracket that is still in progress", () => { // only the semi-finals of the underground bracket have been played so the two teams // still alive there have no placement yet const tournament = singleEliminationWithUndergroundTournament({ @@ -134,7 +134,7 @@ describe("tournamentStandings", () => { ]); }); - it("places teams eliminated in a redemption bracket above the teams of a lower placed bracket", () => { + test("places teams eliminated in a redemption bracket above the teams of a lower placed bracket", () => { const tournament = groupsToRedemptionAndConsolationTournament(); const result = tournamentStandings(tournament); @@ -150,7 +150,7 @@ describe("tournamentStandings", () => { ]); }); - it("does not break ties with an underground bracket that was never started", () => { + test("does not break ties with an underground bracket that was never started", () => { // an underground bracket set in the progression can be skipped altogether const tournament = singleEliminationWithUndergroundTournament({ undergroundStarted: false, @@ -170,7 +170,7 @@ describe("tournamentStandings", () => { }); describe("reNumberPlacements", () => { - it("keeps already contiguous placements unchanged", () => { + test("keeps already contiguous placements unchanged", () => { const result = reNumberPlacements([ { placement: 1 }, { placement: 2 }, @@ -180,7 +180,7 @@ describe("reNumberPlacements", () => { expect(result.map((s) => s.placement)).toEqual([1, 2, 3]); }); - it("groups tied placements and skips numbers to match team count", () => { + test("groups tied placements and skips numbers to match team count", () => { const result = reNumberPlacements([ { placement: 1 }, { placement: 1 }, @@ -192,7 +192,7 @@ describe("reNumberPlacements", () => { expect(result.map((s) => s.placement)).toEqual([1, 1, 3, 3, 5]); }); - it("re-numbers from 1 when the input has been filtered (e.g. top finishers removed)", () => { + test("re-numbers from 1 when the input has been filtered (e.g. top finishers removed)", () => { const result = reNumberPlacements([ { placement: 3 }, { placement: 3 }, @@ -203,7 +203,7 @@ describe("reNumberPlacements", () => { expect(result.map((s) => s.placement)).toEqual([1, 1, 3, 4]); }); - it("adds the offset to every placement", () => { + test("adds the offset to every placement", () => { const result = reNumberPlacements( [{ placement: 1 }, { placement: 1 }, { placement: 3 }], 10, @@ -212,7 +212,7 @@ describe("reNumberPlacements", () => { expect(result.map((s) => s.placement)).toEqual([11, 11, 13]); }); - it("preserves non-placement fields on each standing", () => { + test("preserves non-placement fields on each standing", () => { const result = reNumberPlacements([ { placement: 1, team: { id: 7 }, note: "a" }, { placement: 2, team: { id: 8 }, note: "b" }, @@ -224,14 +224,14 @@ describe("reNumberPlacements", () => { ]); }); - it("returns an empty array when given an empty array", () => { + test("returns an empty array when given an empty array", () => { expect(reNumberPlacements([])).toEqual([]); expect(reNumberPlacements([], 5)).toEqual([]); }); }); describe("matchesPlayed", () => { - it("tags each match with the bracket index it was actually played in", () => { + test("tags each match with the bracket index it was actually played in", () => { const tournament = roundRobinToSingleEliminationTournament(); const matches = matchesPlayed({ tournament, teamId: 1 }); @@ -245,7 +245,7 @@ describe("matchesPlayed", () => { expect(singleEliminationMatches).toHaveLength(1); }); - it("includes matches of brackets that are not part of the standings, in the order they were played", () => { + test("includes matches of brackets that are not part of the standings, in the order they were played", () => { const tournament = roundRobinWithRedemptionTournament(); const matches = matchesPlayed({ tournament, teamId: 4 }); diff --git a/app/features/tournament/core/sets.server.test.ts b/app/features/tournament/core/sets.server.test.ts index 755da4314..c45022ff7 100644 --- a/app/features/tournament/core/sets.server.test.ts +++ b/app/features/tournament/core/sets.server.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { winCounts } from "./sets.server"; describe("winCounts", () => { - it("returns 0% (not NaN) when there are no played sets", () => { + test("returns 0% (not NaN) when there are no played sets", () => { const result = winCounts([]); expect(result.sets.percentage).toBe(0); diff --git a/app/features/tournament/core/tiering.test.ts b/app/features/tournament/core/tiering.test.ts index d2c51b429..056920f95 100644 --- a/app/features/tournament/core/tiering.test.ts +++ b/app/features/tournament/core/tiering.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { calculateAdjustedScore, calculateTentativeTier, @@ -14,22 +14,22 @@ import { } from "./tiering"; describe("calculateTierNumber", () => { - it("returns null for null input", () => { + test("returns null for null input", () => { expect(calculateTierNumber(null)).toBe(null); }); - it("returns X tier (1) for scores >= 32", () => { + test("returns X tier (1) for scores >= 32", () => { expect(calculateTierNumber(32)).toBe(1); expect(calculateTierNumber(35)).toBe(1); expect(calculateTierNumber(100)).toBe(1); }); - it("returns A tier (5) for scores >= 21 and < 24", () => { + test("returns A tier (5) for scores >= 21 and < 24", () => { expect(calculateTierNumber(21)).toBe(5); expect(calculateTierNumber(23.9)).toBe(5); }); - it("returns C tier (9) for scores < 5", () => { + test("returns C tier (9) for scores < 5", () => { expect(calculateTierNumber(4.9)).toBe(9); expect(calculateTierNumber(0)).toBe(9); expect(calculateTierNumber(-10)).toBe(9); @@ -37,7 +37,7 @@ describe("calculateTierNumber", () => { }); describe("tierNumberToName", () => { - it("converts all tier numbers correctly", () => { + test("converts all tier numbers correctly", () => { expect(tierNumberToName(1)).toBe("X"); expect(tierNumberToName(2)).toBe("S+"); expect(tierNumberToName(3)).toBe("S"); @@ -49,22 +49,22 @@ describe("tierNumberToName", () => { expect(tierNumberToName(9)).toBe("C"); }); - it("throws for invalid tier numbers", () => { + test("throws for invalid tier numbers", () => { expect(() => tierNumberToName(0)).toThrow("Invalid tier number: 0"); expect(() => tierNumberToName(10)).toThrow("Invalid tier number: 10"); }); }); describe("calculateAdjustedScore", () => { - it("returns raw score when at X-tier threshold (no bonus)", () => { + test("returns raw score when at X-tier threshold (no bonus)", () => { expect(calculateAdjustedScore(32, 50)).toBe(32); }); - it("returns raw score when below MIN_TEAMS_FOR_TIERING", () => { + test("returns raw score when below MIN_TEAMS_FOR_TIERING", () => { expect(calculateAdjustedScore(20, MIN_TEAMS_FOR_TIERING)).toBe(20); }); - it("applies size bonus for lower skill tournaments with many teams", () => { + test("applies size bonus for lower skill tournaments with many teams", () => { const rawScore = 20; const teamCount = 50; const adjusted = calculateAdjustedScore(rawScore, teamCount); @@ -72,7 +72,7 @@ describe("calculateAdjustedScore", () => { expect(adjusted).toBeGreaterThan(rawScore); }); - it("applies larger bonus at lower skill levels", () => { + test("applies larger bonus at lower skill levels", () => { const teamCount = 50; const bonusAtScore20 = calculateAdjustedScore(20, teamCount) - 20; const bonusAtScore10 = calculateAdjustedScore(10, teamCount) - 10; @@ -80,7 +80,7 @@ describe("calculateAdjustedScore", () => { expect(bonusAtScore10).toBeGreaterThan(bonusAtScore20); }); - it("applies larger bonus for more teams", () => { + test("applies larger bonus for more teams", () => { const rawScore = 15; const bonusAt30Teams = calculateAdjustedScore(rawScore, 30) - rawScore; const bonusAt50Teams = calculateAdjustedScore(rawScore, 50) - rawScore; @@ -88,7 +88,7 @@ describe("calculateAdjustedScore", () => { expect(bonusAt50Teams).toBeGreaterThan(bonusAt30Teams); }); - it("calculates correct bonus for specific case", () => { + test("calculates correct bonus for specific case", () => { const rawScore = 0; const teamCount = 18; const teamsAboveMin = teamCount - MIN_TEAMS_FOR_TIERING; @@ -100,7 +100,7 @@ describe("calculateAdjustedScore", () => { }); describe("calculateTournamentTierFromTeams", () => { - it("returns null tier for tournaments below MIN_TEAMS_FOR_TIERING", () => { + test("returns null tier for tournaments below MIN_TEAMS_FOR_TIERING", () => { const teams = [{ avgOrdinal: 30 }, { avgOrdinal: 28 }]; const result = calculateTournamentTierFromTeams(teams, 7); @@ -109,20 +109,20 @@ describe("calculateTournamentTierFromTeams", () => { expect(result.adjustedScore).toBe(null); }); - it("returns null tier when all teams have null ordinals", () => { + test("returns null tier when all teams have null ordinals", () => { const teams = [{ avgOrdinal: null }, { avgOrdinal: null }]; const result = calculateTournamentTierFromTeams(teams, 10); expect(result.tierNumber).toBe(null); }); - it("returns null tier for empty teams array with sufficient count", () => { + test("returns null tier for empty teams array with sufficient count", () => { const result = calculateTournamentTierFromTeams([], 10); expect(result.tierNumber).toBe(null); }); - it("calculates tier from top 8 teams by ordinal", () => { + test("calculates tier from top 8 teams by ordinal", () => { const teams = [ { avgOrdinal: 35 }, { avgOrdinal: 34 }, @@ -141,7 +141,7 @@ describe("calculateTournamentTierFromTeams", () => { expect(result.tierNumber).toBe(TIER_TO_NUMBER["S+"]); }); - it("ignores teams with null ordinals when calculating", () => { + test("ignores teams with null ordinals when calculating", () => { const teams = [ { avgOrdinal: 30 }, { avgOrdinal: null }, @@ -159,14 +159,14 @@ describe("calculateTournamentTierFromTeams", () => { expect(result.tierNumber).not.toBe(null); }); - it("applies size bonus correctly", () => { + test("applies size bonus correctly", () => { const teams = Array.from({ length: 50 }, () => ({ avgOrdinal: 20 })); const result = calculateTournamentTierFromTeams(teams, 50); expect(result.adjustedScore).toBeGreaterThan(result.rawScore!); }); - it("uses fewer than 8 teams if not enough available", () => { + test("uses fewer than 8 teams if not enough available", () => { const teams = [{ avgOrdinal: 30 }, { avgOrdinal: 28 }, { avgOrdinal: 26 }]; const result = calculateTournamentTierFromTeams(teams, 8); @@ -176,7 +176,7 @@ describe("calculateTournamentTierFromTeams", () => { }); describe("TIER_THRESHOLDS matches TIER_TO_NUMBER", () => { - it("all tiers in TIER_THRESHOLDS have corresponding number", () => { + test("all tiers in TIER_THRESHOLDS have corresponding number", () => { for (const tier of Object.keys(TIER_THRESHOLDS)) { expect(TIER_TO_NUMBER).toHaveProperty(tier); } @@ -184,53 +184,53 @@ describe("TIER_THRESHOLDS matches TIER_TO_NUMBER", () => { }); describe("calculateTentativeTier", () => { - it("returns null for empty history", () => { + test("returns null for empty history", () => { expect(calculateTentativeTier([])).toBe(null); }); - it("returns the single value for history with one element", () => { + test("returns the single value for history with one element", () => { expect(calculateTentativeTier([4])).toBe(4); }); - it("returns median for odd number of elements", () => { + test("returns median for odd number of elements", () => { expect(calculateTentativeTier([5, 4, 3])).toBe(4); expect(calculateTentativeTier([1, 2, 3, 4, 5])).toBe(3); }); - it("rounds toward lower tier (higher number) for even count", () => { + test("rounds toward lower tier (higher number) for even count", () => { expect(calculateTentativeTier([4, 5])).toBe(5); expect(calculateTentativeTier([3, 4])).toBe(4); }); - it("returns median for unsorted input", () => { + test("returns median for unsorted input", () => { expect(calculateTentativeTier([3, 5, 4, 2, 6])).toBe(4); }); - it("handles duplicate values correctly", () => { + test("handles duplicate values correctly", () => { expect(calculateTentativeTier([4, 4, 4])).toBe(4); expect(calculateTentativeTier([5, 5, 4, 4])).toBe(5); }); }); describe("updateTierHistory", () => { - it("creates new history from null", () => { + test("creates new history from null", () => { expect(updateTierHistory(null, 4)).toEqual([4]); }); - it("appends to existing history", () => { + test("appends to existing history", () => { expect(updateTierHistory([5, 4], 3)).toEqual([5, 4, 3]); }); - it("keeps history at max length", () => { + test("keeps history at max length", () => { const fullHistory: TournamentTierNumber[] = [1, 2, 3, 4, 5]; expect(updateTierHistory(fullHistory, 6)).toEqual([2, 3, 4, 5, 6]); }); - it("preserves history under max length", () => { + test("preserves history under max length", () => { expect(updateTierHistory([4, 5], 3)).toEqual([4, 5, 3]); }); - it("slices from beginning when exceeding max length", () => { + test("slices from beginning when exceeding max length", () => { const history: TournamentTierNumber[] = [1, 2, 3, 4, 5]; const result = updateTierHistory(history, 9); expect(result.length).toBe(TIER_HISTORY_LENGTH); diff --git a/app/features/tournament/tournament-search-params.test.ts b/app/features/tournament/tournament-search-params.test.ts index 3073745d4..45a84f88d 100644 --- a/app/features/tournament/tournament-search-params.test.ts +++ b/app/features/tournament/tournament-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -10,7 +10,7 @@ import { } from "./tournament-search-params"; describe("tournamentSearchSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentSearchSearchParams, { q: ["", "paddling pool", "with spaces & symbols?"], limit: [25, 1, 6], @@ -19,7 +19,7 @@ describe("tournamentSearchSearchParams", () => { }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tournamentSearchSearchParams, "limit", [ ["0"], ["26"], @@ -35,7 +35,7 @@ describe("tournamentSearchSearchParams", () => { }); describe("tournamentJoinSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentJoinSearchParams, { code: ["abc123XYZ", "F3-9_xyz"], }); @@ -43,13 +43,13 @@ describe("tournamentJoinSearchParams", () => { }); describe("tournamentTeamsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(tournamentTeamsSearchParams, { page: [1, 2, 17], }); }); - it("decodes garbage to defaults", () => { + test("decodes garbage to defaults", () => { assertDecodesToDefault(tournamentTeamsSearchParams, "page", [ ["0"], ["abc"], diff --git a/app/features/tournament/tournament-utils.test.ts b/app/features/tournament/tournament-utils.test.ts index 4bca8fa0d..badd88bfd 100644 --- a/app/features/tournament/tournament-utils.test.ts +++ b/app/features/tournament/tournament-utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { CastedMatchesInfo } from "~/db/tables-json"; import * as Seasons from "../mmr/core/Seasons"; import type { ParsedBracket } from "../tournament-bracket/core/Progression"; @@ -16,7 +16,7 @@ import { updatedCastedMatchesInfo, } from "./tournament-utils"; -const createTeam = ( +const teamForOrdering = ( id: number, options: { seed?: number | null; @@ -39,191 +39,147 @@ const createTeam = ( const MIN_MEMBERS = 4; -describe("compareTeamsForOrdering", () => { - describe("full teams priority", () => { - it("places full teams before not-full teams", () => { - const fullTeam = createTeam(1, { members: 4 }); - const notFullTeam = createTeam(2, { members: 3 }); - - const result = compareTeamsForOrdering( - fullTeam, - notFullTeam, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); - - it("places not-full teams after full teams", () => { - const notFullTeam = createTeam(1, { members: 3 }); - const fullTeam = createTeam(2, { members: 4 }); - - const result = compareTeamsForOrdering( - notFullTeam, - fullTeam, - MIN_MEMBERS, - ); - - expect(result).toBeGreaterThan(0); - }); - }); - - describe("seed priority", () => { - it("orders by seed when both have seeds", () => { - const team1 = createTeam(1, { seed: 1 }); - const team2 = createTeam(2, { seed: 2 }); - - const result = compareTeamsForOrdering(team1, team2, MIN_MEMBERS); - - expect(result).toBeLessThan(0); - }); - - it("places seeded team before unseeded team when unseeded has no skill", () => { - const seededTeam = createTeam(1, { seed: 5 }); - const unseededTeam = createTeam(2); - - const result = compareTeamsForOrdering( - seededTeam, - unseededTeam, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); - - it("compares by skill ordinal when both full teams have skill but only one has seed", () => { - const seededLowSkill = createTeam(1, { - seed: 5, - avgSeedingSkillOrdinal: 100, - }); - const unseededHighSkill = createTeam(2, { avgSeedingSkillOrdinal: 300 }); - - const result = compareTeamsForOrdering( - seededLowSkill, - unseededHighSkill, - MIN_MEMBERS, - ); - - expect(result).toBeGreaterThan(0); - }); - - it("places seeded team first when only seeded team has skill ordinal", () => { - const seededWithSkill = createTeam(1, { - seed: 5, - avgSeedingSkillOrdinal: 100, - }); - const unseededNoSkill = createTeam(2); - - const result = compareTeamsForOrdering( - seededWithSkill, - unseededNoSkill, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); - - it("places seeded team first when not-full team has higher skill", () => { - const seededFull = createTeam(1, { - seed: 5, - avgSeedingSkillOrdinal: 100, - }); - const unseededNotFull = createTeam(2, { - members: 3, +/** + * Every ordering rule, stated as the order callers actually see: what + * `sortTeamsBySeeding` produces. `compareTeamsForOrdering` is only checked + * against its antisymmetry contract over the same rows. + */ +const ORDERING_RULES: { + rule: string; + teams: TeamForOrdering[]; + expectedIds: number[]; +}[] = [ + { + rule: "the starting bracket decides before skill does", + teams: [ + teamForOrdering(1, { + startingBracketIdx: 1, avgSeedingSkillOrdinal: 500, - }); - - const result = compareTeamsForOrdering( - seededFull, - unseededNotFull, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); - }); - - describe("skill ordinal priority", () => { - it("orders by skill ordinal when no seeds (higher skill first)", () => { - const highSkill = createTeam(1, { avgSeedingSkillOrdinal: 300 }); - const lowSkill = createTeam(2, { avgSeedingSkillOrdinal: 100 }); - - const result = compareTeamsForOrdering(highSkill, lowSkill, MIN_MEMBERS); - - expect(result).toBeLessThan(0); - }); - - it("places team with skill before team without skill", () => { - const withSkill = createTeam(1, { avgSeedingSkillOrdinal: 100 }); - const withoutSkill = createTeam(2, { avgSeedingSkillOrdinal: null }); - - const result = compareTeamsForOrdering( - withSkill, - withoutSkill, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); - - it("places rated team before unrated team even when unrated was created earlier", () => { - const rated = createTeam(1, { - avgSeedingSkillOrdinal: 100, - createdAt: 200, - }); - const unrated = createTeam(2, { - avgSeedingSkillOrdinal: null, - createdAt: 100, - }); - - const sorted = sortTeamsBySeeding([unrated, rated], MIN_MEMBERS); - - expect(sorted.map((t) => t.id)).toEqual([1, 2]); - }); - }); - - describe("createdAt tiebreaker", () => { - it("orders by createdAt when all else is equal", () => { - const olderTeam = createTeam(1, { createdAt: 100 }); - const newerTeam = createTeam(2, { createdAt: 200 }); - - const result = compareTeamsForOrdering(olderTeam, newerTeam, MIN_MEMBERS); - - expect(result).toBeLessThan(0); - }); - }); - - describe("startingBracketIdx null vs explicit 0", () => { - it("orders by seed when one team has null and the other explicit 0 starting bracket", () => { - const nullBracketTeam = createTeam(1, { - seed: 1, - startingBracketIdx: null, - }); - const zeroBracketTeam = createTeam(2, { - seed: 2, + }), + teamForOrdering(2, { startingBracketIdx: 0, - }); + avgSeedingSkillOrdinal: 100, + }), + teamForOrdering(3, { + startingBracketIdx: 0, + avgSeedingSkillOrdinal: 200, + }), + ], + expectedIds: [3, 2, 1], + }, + { + rule: "an unset starting bracket is the same as bracket 0", + teams: [ + teamForOrdering(1, { seed: 1, startingBracketIdx: null }), + teamForOrdering(2, { seed: 2, startingBracketIdx: 0 }), + ], + expectedIds: [1, 2], + }, + { + rule: "two seeded teams go in seed order", + teams: [teamForOrdering(1, { seed: 1 }), teamForOrdering(2, { seed: 2 })], + expectedIds: [1, 2], + }, + { + rule: "seeded teams keep their order while an unseeded one slots in by skill", + teams: [ + teamForOrdering(1, { seed: 2 }), + teamForOrdering(2, { seed: 1 }), + teamForOrdering(3, { avgSeedingSkillOrdinal: 500 }), + ], + expectedIds: [3, 2, 1], + }, + { + rule: "a full team goes before a not-full one", + teams: [ + teamForOrdering(1, { members: 4 }), + teamForOrdering(2, { members: 3 }), + ], + expectedIds: [1, 2], + }, + { + rule: "a seeded team goes before an unseeded one of equal skill", + teams: [teamForOrdering(1, { seed: 5 }), teamForOrdering(2)], + expectedIds: [1, 2], + }, + { + rule: "an unseeded team of higher skill goes before a lower-skilled seeded one", + teams: [ + teamForOrdering(1, { seed: 5, avgSeedingSkillOrdinal: 100 }), + teamForOrdering(2, { avgSeedingSkillOrdinal: 300 }), + ], + expectedIds: [2, 1], + }, + { + rule: "a seeded team goes before a not-full unseeded one however high its skill", + teams: [ + teamForOrdering(1, { seed: 5, avgSeedingSkillOrdinal: 100 }), + teamForOrdering(2, { members: 3, avgSeedingSkillOrdinal: 500 }), + ], + expectedIds: [1, 2], + }, + { + rule: "higher skill goes first when neither team is seeded", + teams: [ + teamForOrdering(1, { avgSeedingSkillOrdinal: 300 }), + teamForOrdering(2, { avgSeedingSkillOrdinal: 100 }), + ], + expectedIds: [1, 2], + }, + { + rule: "a rated team goes before an unrated one", + teams: [ + teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }), + teamForOrdering(2, { avgSeedingSkillOrdinal: null }), + ], + expectedIds: [1, 2], + }, + { + rule: "a rated team goes before an unrated one that registered earlier", + teams: [ + teamForOrdering(2, { avgSeedingSkillOrdinal: null, createdAt: 100 }), + teamForOrdering(1, { avgSeedingSkillOrdinal: 100, createdAt: 200 }), + ], + expectedIds: [1, 2], + }, + { + rule: "registration time breaks a tie", + teams: [ + teamForOrdering(1, { createdAt: 100 }), + teamForOrdering(2, { createdAt: 200 }), + ], + expectedIds: [1, 2], + }, +]; - const result = compareTeamsForOrdering( - nullBracketTeam, - zeroBracketTeam, - MIN_MEMBERS, - ); - - expect(result).toBeLessThan(0); - }); +describe("compareTeamsForOrdering", () => { + test.each(ORDERING_RULES)("is antisymmetric where $rule", ({ teams }) => { + for (const a of teams) { + for (const b of teams) { + expect( + Math.sign(compareTeamsForOrdering(a, b, MIN_MEMBERS)) + + Math.sign(compareTeamsForOrdering(b, a, MIN_MEMBERS)), + ).toBe(0); + } + } }); }); describe("sortTeamsBySeeding", () => { - it("sorts teams correctly with mixed properties", () => { + test.each(ORDERING_RULES)("$rule", ({ teams, expectedIds }) => { + const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS); + + expect(sorted.map((team) => team.id)).toEqual(expectedIds); + }); + + test("sorts teams correctly with mixed properties", () => { const teams = [ - createTeam(1, { members: 3, avgSeedingSkillOrdinal: 500 }), - createTeam(2, { seed: 2 }), - createTeam(3, { avgSeedingSkillOrdinal: 300 }), - createTeam(4, { seed: 1 }), - createTeam(5, { avgSeedingSkillOrdinal: 400 }), - createTeam(6, { members: 3 }), + teamForOrdering(1, { members: 3, avgSeedingSkillOrdinal: 500 }), + teamForOrdering(2, { seed: 2 }), + teamForOrdering(3, { avgSeedingSkillOrdinal: 300 }), + teamForOrdering(4, { seed: 1 }), + teamForOrdering(5, { avgSeedingSkillOrdinal: 400 }), + teamForOrdering(6, { members: 3 }), ]; const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS); @@ -231,15 +187,15 @@ describe("sortTeamsBySeeding", () => { expect(sorted.map((t) => t.id)).toEqual([5, 3, 4, 2, 1, 6]); }); - it("keeps manually seeded teams in seed order when unseeded teams are present", () => { + test("keeps manually seeded teams in seed order when unseeded teams are present", () => { const seededSkills = [31, 18, 20, 37, 2, 46, 19, 37]; const seededTeams = seededSkills.map((skill, i) => - createTeam(i + 1, { seed: i + 1, avgSeedingSkillOrdinal: skill }), + teamForOrdering(i + 1, { seed: i + 1, avgSeedingSkillOrdinal: skill }), ); // input mirrors the DB query order (seed ASC = NULL seeds first in SQLite) const teams = [ - createTeam(9, { avgSeedingSkillOrdinal: 40 }), - createTeam(10, { avgSeedingSkillOrdinal: 31 }), + teamForOrdering(9, { avgSeedingSkillOrdinal: 40 }), + teamForOrdering(10, { avgSeedingSkillOrdinal: 31 }), ...seededTeams, ]; @@ -250,12 +206,18 @@ describe("sortTeamsBySeeding", () => { ).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); }); - it("returns the same order regardless of input order", () => { - const seedOne = createTeam(1, { seed: 1, avgSeedingSkillOrdinal: 5 }); - const seedTwo = createTeam(2, { seed: 2, avgSeedingSkillOrdinal: 30 }); - const seedThree = createTeam(3, { seed: 3, avgSeedingSkillOrdinal: 20 }); - const seedFour = createTeam(4, { seed: 4, avgSeedingSkillOrdinal: 25 }); - const unseeded = createTeam(5, { avgSeedingSkillOrdinal: 28 }); + test("returns the same order regardless of input order", () => { + const seedOne = teamForOrdering(1, { seed: 1, avgSeedingSkillOrdinal: 5 }); + const seedTwo = teamForOrdering(2, { seed: 2, avgSeedingSkillOrdinal: 30 }); + const seedThree = teamForOrdering(3, { + seed: 3, + avgSeedingSkillOrdinal: 20, + }); + const seedFour = teamForOrdering(4, { + seed: 4, + avgSeedingSkillOrdinal: 25, + }); + const unseeded = teamForOrdering(5, { avgSeedingSkillOrdinal: 28 }); const sortedA = sortTeamsBySeeding( [seedOne, seedTwo, seedThree, seedFour, unseeded], @@ -271,10 +233,10 @@ describe("sortTeamsBySeeding", () => { ); }); - it("slots an unseeded team below every seeded team with a higher skill ordinal", () => { - const seedOne = createTeam(1, { seed: 1, avgSeedingSkillOrdinal: 5 }); - const seedTwo = createTeam(2, { seed: 2, avgSeedingSkillOrdinal: 30 }); - const unseeded = createTeam(5, { avgSeedingSkillOrdinal: 28 }); + test("slots an unseeded team below every seeded team with a higher skill ordinal", () => { + const seedOne = teamForOrdering(1, { seed: 1, avgSeedingSkillOrdinal: 5 }); + const seedTwo = teamForOrdering(2, { seed: 2, avgSeedingSkillOrdinal: 30 }); + const unseeded = teamForOrdering(5, { avgSeedingSkillOrdinal: 28 }); const sorted = sortTeamsBySeeding( [unseeded, seedOne, seedTwo], @@ -284,10 +246,10 @@ describe("sortTeamsBySeeding", () => { expect(sorted.map((team) => team.id)).toEqual([1, 2, 5]); }); - it("does not mutate original array", () => { + test("does not mutate original array", () => { const teams = [ - createTeam(2, { avgSeedingSkillOrdinal: 100 }), - createTeam(1, { avgSeedingSkillOrdinal: 200 }), + teamForOrdering(2, { avgSeedingSkillOrdinal: 100 }), + teamForOrdering(1, { avgSeedingSkillOrdinal: 200 }), ]; sortTeamsBySeeding(teams, MIN_MEMBERS); @@ -297,15 +259,15 @@ describe("sortTeamsBySeeding", () => { }); describe("findTeamInsertPosition", () => { - it("inserts at beginning when new team should be first", () => { - const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 }); - const team2 = createTeam(2, { avgSeedingSkillOrdinal: 200 }); + test("inserts at beginning when new team should be first", () => { + const team1 = teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }); + const team2 = teamForOrdering(2, { avgSeedingSkillOrdinal: 200 }); const teamMap = new Map([ [1, team1], [2, team2], ]); const existingOrder = [2, 1]; - const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 300 }); + const newTeam = teamForOrdering(3, { avgSeedingSkillOrdinal: 300 }); const position = findTeamInsertPosition( existingOrder, @@ -317,15 +279,15 @@ describe("findTeamInsertPosition", () => { expect(position).toBe(0); }); - it("inserts at end when new team should be last", () => { - const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 }); - const team2 = createTeam(2, { avgSeedingSkillOrdinal: 200 }); + test("inserts at end when new team should be last", () => { + const team1 = teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }); + const team2 = teamForOrdering(2, { avgSeedingSkillOrdinal: 200 }); const teamMap = new Map([ [1, team1], [2, team2], ]); const existingOrder = [2, 1]; - const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 50 }); + const newTeam = teamForOrdering(3, { avgSeedingSkillOrdinal: 50 }); const position = findTeamInsertPosition( existingOrder, @@ -337,17 +299,17 @@ describe("findTeamInsertPosition", () => { expect(position).toBe(2); }); - it("inserts in middle based on comparison", () => { - const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 }); - const team2 = createTeam(2, { avgSeedingSkillOrdinal: 300 }); - const team3 = createTeam(3, { avgSeedingSkillOrdinal: 200 }); + test("inserts in middle based on comparison", () => { + const team1 = teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }); + const team2 = teamForOrdering(2, { avgSeedingSkillOrdinal: 300 }); + const team3 = teamForOrdering(3, { avgSeedingSkillOrdinal: 200 }); const teamMap = new Map([ [1, team1], [2, team2], [3, team3], ]); const existingOrder = [2, 3, 1]; - const newTeam = createTeam(4, { avgSeedingSkillOrdinal: 150 }); + const newTeam = teamForOrdering(4, { avgSeedingSkillOrdinal: 150 }); const position = findTeamInsertPosition( existingOrder, @@ -359,10 +321,10 @@ describe("findTeamInsertPosition", () => { expect(position).toBe(2); }); - it("handles empty existing order", () => { + test("handles empty existing order", () => { const teamMap = new Map(); const existingOrder: number[] = []; - const newTeam = createTeam(1, { avgSeedingSkillOrdinal: 100 }); + const newTeam = teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }); const position = findTeamInsertPosition( existingOrder, @@ -374,11 +336,11 @@ describe("findTeamInsertPosition", () => { expect(position).toBe(0); }); - it("skips missing teams in map", () => { - const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 }); + test("skips missing teams in map", () => { + const team1 = teamForOrdering(1, { avgSeedingSkillOrdinal: 100 }); const teamMap = new Map([[1, team1]]); const existingOrder = [2, 1]; - const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 150 }); + const newTeam = teamForOrdering(3, { avgSeedingSkillOrdinal: 150 }); const position = findTeamInsertPosition( existingOrder, @@ -391,41 +353,6 @@ describe("findTeamInsertPosition", () => { }); }); -describe("sortTeamsBySeeding with startingBracketIdx", () => { - it("orders by startingBracketIdx first", () => { - const teams = [ - createTeam(1, { - startingBracketIdx: 1, - avgSeedingSkillOrdinal: 500, - }), - createTeam(2, { - startingBracketIdx: 0, - avgSeedingSkillOrdinal: 100, - }), - createTeam(3, { - startingBracketIdx: 0, - avgSeedingSkillOrdinal: 200, - }), - ]; - - const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS); - - expect(sorted.map((t) => t.id)).toEqual([3, 2, 1]); - }); - - it("uses seeds within same bracket", () => { - const teams = [ - createTeam(1, { seed: 2 }), - createTeam(2, { seed: 1 }), - createTeam(3, { avgSeedingSkillOrdinal: 500 }), - ]; - - const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS); - - expect(sorted.map((t) => t.id)).toEqual([3, 2, 1]); - }); -}); - const createBracket = (name: string): ParsedBracket => ({ name, type: "single_elimination", @@ -434,7 +361,7 @@ const createBracket = (name: string): ParsedBracket => ({ }); describe("getBracketProgressionLabel", () => { - it("returns single bracket name when only one bracket is reachable", () => { + test("returns single bracket name when only one bracket is reachable", () => { const progression: ParsedBracket[] = [createBracket("Main Bracket")]; const result = getBracketProgressionLabel(0, progression); @@ -442,7 +369,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Main Bracket"); }); - it("returns common prefix when multiple brackets share a prefix", () => { + test("returns common prefix when multiple brackets share a prefix", () => { const progression: ParsedBracket[] = [ createBracket("Alpha"), createBracket("Alpha A"), @@ -457,7 +384,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Alpha"); }); - it("trims whitespace from common prefix", () => { + test("trims whitespace from common prefix", () => { const progression: ParsedBracket[] = [ createBracket("Playoff "), createBracket("Playoff Winner"), @@ -472,7 +399,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Playoff"); }); - it("returns deepest bracket name when no common prefix exists", () => { + test("returns deepest bracket name when no common prefix exists", () => { const progression: ParsedBracket[] = [ createBracket("Round Robin"), createBracket("Winner Bracket"), @@ -492,7 +419,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Grand Finals"); }); - it("handles single character prefix", () => { + test("handles single character prefix", () => { const progression: ParsedBracket[] = [ createBracket("A"), createBracket("A1"), @@ -507,7 +434,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("A"); }); - it("handles bracket progression with multiple levels", () => { + test("handles bracket progression with multiple levels", () => { const progression: ParsedBracket[] = [ createBracket("Qualifier"), createBracket("Group A"), @@ -527,7 +454,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Finals"); }); - it("returns bracket name for progression with partial common prefix", () => { + test("returns bracket name for progression with partial common prefix", () => { const progression: ParsedBracket[] = [ createBracket("Swiss"), createBracket("Swiss Upper"), @@ -542,7 +469,7 @@ describe("getBracketProgressionLabel", () => { expect(result).toBe("Swiss"); }); - it("handles empty string prefix by returning deepest bracket", () => { + test("handles empty string prefix by returning deepest bracket", () => { const progression: ParsedBracket[] = [ createBracket("A"), createBracket("B"), @@ -566,7 +493,7 @@ const emptyCastedMatchesInfo = (): CastedMatchesInfo => ({ describe("updatedCastedMatchesInfo", () => { describe("assigning a cast", () => { - it("adds entry to castedMatches and history", () => { + test("adds entry to castedMatches and history", () => { const result = updatedCastedMatchesInfo(emptyCastedMatchesInfo(), { matchId: 1, twitchAccount: "streamer_a", @@ -581,7 +508,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("removes prior castedMatches entry for same matchId", () => { + test("removes prior castedMatches entry for same matchId", () => { const current = emptyCastedMatchesInfo(); current.castedMatches = [{ twitchAccount: "old_streamer", matchId: 1 }]; @@ -596,7 +523,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("removes prior castedMatches entry for same twitchAccount", () => { + test("removes prior castedMatches entry for same twitchAccount", () => { const current = emptyCastedMatchesInfo(); current.castedMatches = [{ twitchAccount: "streamer_a", matchId: 1 }]; @@ -611,7 +538,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("removes matchId from lockedMatches", () => { + test("removes matchId from lockedMatches", () => { const current = emptyCastedMatchesInfo(); current.lockedMatches = [ { twitchAccount: "streamer_a", matchId: 1 }, @@ -629,7 +556,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("deduplicates history by matchId when channel is corrected", () => { + test("deduplicates history by matchId when channel is corrected", () => { const current = emptyCastedMatchesInfo(); current.castedMatchHistory = [ { twitchAccount: "wrong_channel", matchId: 1, timestamp: 500 }, @@ -648,7 +575,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("deduplicates history when same account+matchId is reassigned", () => { + test("deduplicates history when same account+matchId is reassigned", () => { const current = emptyCastedMatchesInfo(); current.castedMatchHistory = [ { twitchAccount: "streamer_a", matchId: 1, timestamp: 500 }, @@ -665,7 +592,7 @@ describe("updatedCastedMatchesInfo", () => { ]); }); - it("initializes history when undefined", () => { + test("initializes history when undefined", () => { const current: CastedMatchesInfo = { castedMatches: [], lockedMatches: [], @@ -684,7 +611,7 @@ describe("updatedCastedMatchesInfo", () => { }); describe("unassigning a cast", () => { - it("removes matchId from castedMatches and lockedMatches", () => { + test("removes matchId from castedMatches and lockedMatches", () => { const current = emptyCastedMatchesInfo(); current.castedMatches = [ { twitchAccount: "streamer_a", matchId: 1 }, @@ -704,7 +631,7 @@ describe("updatedCastedMatchesInfo", () => { expect(result.lockedMatches).toEqual([]); }); - it("does not modify castedMatchHistory", () => { + test("does not modify castedMatchHistory", () => { const current = emptyCastedMatchesInfo(); current.castedMatchHistory = [ { twitchAccount: "streamer_a", matchId: 1, timestamp: 500 }, @@ -735,7 +662,7 @@ describe("tournamentInWeaponReportingWindow", () => { (previousSeason.ends.getTime() + anchorSeason.starts.getTime()) / 2, ); - it("allows tournaments started in the off-season before current season (in-season)", () => { + test("allows tournaments started in the off-season before current season (in-season)", () => { expect( tournamentInWeaponReportingWindow({ tournamentStartTime: offSeasonNow, @@ -744,7 +671,7 @@ describe("tournamentInWeaponReportingWindow", () => { ).toBe(true); }); - it("rejects tournaments started before the previous season ended (in-season)", () => { + test("rejects tournaments started before the previous season ended (in-season)", () => { expect( tournamentInWeaponReportingWindow({ tournamentStartTime: dateInside(previousSeason), @@ -753,7 +680,7 @@ describe("tournamentInWeaponReportingWindow", () => { ).toBe(false); }); - it("allows tournaments started during the previous season (off-season)", () => { + test("allows tournaments started during the previous season (off-season)", () => { expect( tournamentInWeaponReportingWindow({ tournamentStartTime: dateInside(previousSeason), @@ -766,60 +693,60 @@ describe("tournamentInWeaponReportingWindow", () => { describe("splitTournamentName", () => { const series = [{ name: "In The Zone" }, { name: "Low Ink" }]; - it("splits the trailing number subtext after the series name", () => { + test("splits the trailing number subtext after the series name", () => { expect(splitTournamentName("In The Zone 54", series)).toEqual({ name: "In The Zone", subtext: "54", }); }); - it("splits a non-numeric subtext after the series name", () => { + test("splits a non-numeric subtext after the series name", () => { expect(splitTournamentName("Low Ink May 2026", series)).toEqual({ name: "Low Ink", subtext: "May 2026", }); }); - it("matches the series name case-insensitively", () => { + test("matches the series name case-insensitively", () => { expect(splitTournamentName("in the zone 54", series)).toEqual({ name: "In The Zone", subtext: "54", }); }); - it("strips separators between the series name and the subtext", () => { + test("strips separators between the series name and the subtext", () => { expect(splitTournamentName("In The Zone - 54", series)).toEqual({ name: "In The Zone", subtext: "54", }); }); - it("trims trailing whitespace after the subtext", () => { + test("trims trailing whitespace after the subtext", () => { expect(splitTournamentName("In The Zone 54 ", series)).toEqual({ name: "In The Zone", subtext: "54", }); }); - it("returns name only when the name does not start with a series name", () => { + test("returns name only when the name does not start with a series name", () => { expect(splitTournamentName("Picnic Weekly", series)).toEqual({ name: "Picnic Weekly", }); }); - it("returns name only when the name equals the series name", () => { + test("returns name only when the name equals the series name", () => { expect(splitTournamentName("In The Zone", series)).toEqual({ name: "In The Zone", }); }); - it("returns name only when there are no series", () => { + test("returns name only when there are no series", () => { expect(splitTournamentName("In The Zone 54", [])).toEqual({ name: "In The Zone 54", }); }); - it("prefers the longest matching series name", () => { + test("prefers the longest matching series name", () => { expect( splitTournamentName("In The Zone Masters 5", [ { name: "In The Zone" }, @@ -833,7 +760,7 @@ describe("splitTournamentName", () => { }); describe("tournamentNameParts", () => { - it("uses the parent tournament name and division subtext for a league division", () => { + test("uses the parent tournament name and division subtext for a league division", () => { const tournament = testTournament({ ctx: { name: "LUTI: Season 17 - Division 1", @@ -848,7 +775,7 @@ describe("tournamentNameParts", () => { }); }); - it("falls back to the organization series when not a league division", () => { + test("falls back to the organization series when not a league division", () => { const tournament = testTournament({ ctx: { name: "In The Zone 54", @@ -881,13 +808,13 @@ describe("bracketProgressionLabel", () => { ...bracket, }); - it("returns the short code for a single stage", () => { + test("returns the short code for a single stage", () => { expect( bracketProgressionLabel([bracket({ type: "single_elimination" })]), ).toEqual({ label: "SE", hasUnderground: false }); }); - it("joins stages with an arrow", () => { + test("joins stages with an arrow", () => { expect( bracketProgressionLabel([ bracket({ type: "round_robin" }), @@ -896,7 +823,7 @@ describe("bracketProgressionLabel", () => { ).toEqual({ label: "RR → SE", hasUnderground: false }); }); - it("collapses consecutive duplicate stages", () => { + test("collapses consecutive duplicate stages", () => { expect( bracketProgressionLabel([ bracket({ type: "single_elimination" }), @@ -906,7 +833,7 @@ describe("bracketProgressionLabel", () => { ).toEqual({ label: "SE → DE", hasUnderground: false }); }); - it("leaves an underground bracket out of the label", () => { + test("leaves an underground bracket out of the label", () => { expect( bracketProgressionLabel([ bracket({ type: "double_elimination" }), @@ -918,7 +845,7 @@ describe("bracketProgressionLabel", () => { ).toEqual({ label: "DE", hasUnderground: true }); }); - it("flags many underground brackets the same as one", () => { + test("flags many underground brackets the same as one", () => { expect( bracketProgressionLabel([ bracket({ type: "swiss" }), @@ -938,7 +865,7 @@ describe("bracketProgressionLabel", () => { ).toEqual({ label: "SW → SE", hasUnderground: true }); }); - it("returns empty label for empty progression", () => { + test("returns empty label for empty progression", () => { expect(bracketProgressionLabel([])).toEqual({ label: "", hasUnderground: false, diff --git a/app/features/user-card/UserCardRepository.server.test.ts b/app/features/user-card/UserCardRepository.server.test.ts index a0faf01a6..67d826f68 100644 --- a/app/features/user-card/UserCardRepository.server.test.ts +++ b/app/features/user-card/UserCardRepository.server.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as ImageFactory from "~/db/seed/factories/ImageFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory"; @@ -8,8 +8,7 @@ import { withNoUser, withUserId } from "~/utils/Test"; import * as UserCardRepository from "./UserCardRepository.server"; import type { UserCardData } from "./user-card-types"; -let owner: { id: number }; -let other: { id: number }; +const users = UserFactory.pool(); const insertVerifiedXp = ( userId: number, @@ -22,10 +21,10 @@ const findXpStat = (card: UserCardData | undefined) => describe("UserCardRepository.findAllByUserIds", () => { beforeEach(async () => { - [owner, other] = await UserFactory.createMany(2); + await users.create(2); }); - it("returns an empty map when given no user ids", async () => { + test("returns an empty map when given no user ids", async () => { const { userCards } = await withNoUser(() => UserCardRepository.findAllByUserIds({ userIds: [], @@ -35,7 +34,7 @@ describe("UserCardRepository.findAllByUserIds", () => { expect(userCards.size).toBe(0); }); - it("keys cards by user id and builds the stats array from db fields", async () => { + test("keys cards by user id and builds the stats array from db fields", async () => { const plusMember = await UserFactory.create(null, { plusTier: 2, div: "1", @@ -44,7 +43,7 @@ describe("UserCardRepository.findAllByUserIds", () => { const { userCards } = await withNoUser(() => UserCardRepository.findAllByUserIds({ - userIds: [plusMember.id, other.id], + userIds: [plusMember.id, users.id(2)], }), ); @@ -73,12 +72,12 @@ describe("UserCardRepository.findAllByUserIds", () => { }); // user 2 has none of the optional fields -> no stats - expect(userCards.get(other.id)?.stats).toHaveLength(0); + expect(userCards.get(users.id(2))?.stats).toHaveLength(0); }); - it("surfaces self-reported peak XP only when it beats the verified XP", async () => { - await insertVerifiedXp(owner.id, 2500); - await withUserId(owner.id, () => + test("surfaces self-reported peak XP only when it beats the verified XP", async () => { + await insertVerifiedXp(users.id(1), 2500); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -90,10 +89,10 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [ { isVerified: false, region: "WEST", points: 2600 }, @@ -102,9 +101,9 @@ describe("UserCardRepository.findAllByUserIds", () => { }); }); - it("ignores self-reported peak XP that does not beat the verified XP", async () => { - await insertVerifiedXp(owner.id, 2500); - await withUserId(owner.id, () => + test("ignores self-reported peak XP that does not beat the verified XP", async () => { + await insertVerifiedXp(users.id(1), 2500); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -116,18 +115,18 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: true, region: "WEST", points: 2500 }], }); }); - it("ignores self-reported peak XP more than 200 above the verified XP", async () => { - await insertVerifiedXp(owner.id, 2500); - await withUserId(owner.id, () => + test("ignores self-reported peak XP more than 200 above the verified XP", async () => { + await insertVerifiedXp(users.id(1), 2500); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -139,19 +138,19 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: true, region: "WEST", points: 2500 }], }); }); - it("shows the verified peak XP of the picked division", async () => { - await insertVerifiedXp(owner.id, 3010, "WEST"); - await insertVerifiedXp(owner.id, 3000, "JPN"); - await withUserId(owner.id, () => + test("shows the verified peak XP of the picked division", async () => { + await insertVerifiedXp(users.id(1), 3010, "WEST"); + await insertVerifiedXp(users.id(1), 3000, "JPN"); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -163,32 +162,32 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: true, region: "JPN", points: 3000 }], }); }); - it("shows the highest verified peak XP across divisions when none is picked", async () => { - await insertVerifiedXp(owner.id, 3010, "WEST"); - await insertVerifiedXp(owner.id, 3000, "JPN"); + test("shows the highest verified peak XP across divisions when none is picked", async () => { + await insertVerifiedXp(users.id(1), 3010, "WEST"); + await insertVerifiedXp(users.id(1), 3000, "JPN"); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: true, region: "WEST", points: 3010 }], }); }); - it("falls back to the other division when the picked one has no placements", async () => { - await insertVerifiedXp(owner.id, 3010, "WEST"); - await withUserId(owner.id, () => + test("falls back to the other division when the picked one has no placements", async () => { + await insertVerifiedXp(users.id(1), 3010, "WEST"); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -200,19 +199,19 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: true, region: "WEST", points: 3010 }], }); }); - it("judges self-reported peak XP against the picked division's verified XP", async () => { - await insertVerifiedXp(owner.id, 3010, "WEST"); - await insertVerifiedXp(owner.id, 3000, "JPN"); - await withUserId(owner.id, () => + test("judges self-reported peak XP against the picked division's verified XP", async () => { + await insertVerifiedXp(users.id(1), 3010, "WEST"); + await insertVerifiedXp(users.id(1), 3000, "JPN"); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -224,11 +223,11 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); // 3005 does not beat the 3010 of the other division, but that ladder is not the one shown - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [ { isVerified: false, region: "JPN", points: 3005 }, @@ -237,10 +236,10 @@ describe("UserCardRepository.findAllByUserIds", () => { }); }); - it("surfaces self-reported peak XP equal to the other division's verified peak", async () => { - await insertVerifiedXp(owner.id, 3100, "WEST"); - await insertVerifiedXp(owner.id, 3000, "JPN"); - await withUserId(owner.id, () => + test("surfaces self-reported peak XP equal to the other division's verified peak", async () => { + await insertVerifiedXp(users.id(1), 3100, "WEST"); + await insertVerifiedXp(users.id(1), 3000, "JPN"); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -252,10 +251,10 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [ { isVerified: false, region: "JPN", points: 3100 }, @@ -264,9 +263,9 @@ describe("UserCardRepository.findAllByUserIds", () => { }); }); - it("keeps the self-reported peak XP in the picked division without placements there", async () => { - await insertVerifiedXp(owner.id, 2639.5, "WEST"); - await withUserId(owner.id, () => + test("keeps the self-reported peak XP in the picked division without placements there", async () => { + await insertVerifiedXp(users.id(1), 2639.5, "WEST"); + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -278,17 +277,17 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + expect(findXpStat(userCards.get(users.id(1)))).toMatchObject({ type: "XP", values: [{ isVerified: false, region: "JPN", points: 2650 }], }); }); - it("ignores self-reported peak XP when there is no verified XP", async () => { - await withUserId(owner.id, () => + test("ignores self-reported peak XP when there is no verified XP", async () => { + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -300,13 +299,13 @@ describe("UserCardRepository.findAllByUserIds", () => { ); const { userCards } = await withNoUser(() => - UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + UserCardRepository.findAllByUserIds({ userIds: [users.id(1)] }), ); - expect(findXpStat(userCards.get(owner.id))).toBeUndefined(); + expect(findXpStat(userCards.get(users.id(1)))).toBeUndefined(); }); - it("persists edited card fields and surfaces hidden stats", async () => { + test("persists edited card fields and surfaces hidden stats", async () => { const plusMember = await UserFactory.create(null, { plusTier: 2 }); await insertVerifiedXp(plusMember.id, 2500); @@ -343,10 +342,10 @@ describe("UserCardRepository.findAllByUserIds", () => { expect(extras.hiddenCardStats).toEqual(["XP"]); }); - it("keeps hidden stats in `stats` when includeHiddenStats is set", async () => { - await insertVerifiedXp(owner.id, 2500); + test("keeps hidden stats in `stats` when includeHiddenStats is set", async () => { + await insertVerifiedXp(users.id(1), 2500); - await withUserId(owner.id, () => + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -357,13 +356,13 @@ describe("UserCardRepository.findAllByUserIds", () => { }), ); - const { userCards } = await withUserId(owner.id, () => + const { userCards } = await withUserId(users.id(1), () => UserCardRepository.findAllByUserIds({ - userIds: [owner.id], + userIds: [users.id(1)], includeHiddenStats: true, }), ); - const card = userCards.get(owner.id); + const card = userCards.get(users.id(1)); expect(findXpStat(card)).toMatchObject({ type: "XP", @@ -371,13 +370,13 @@ describe("UserCardRepository.findAllByUserIds", () => { }); }); - it("produces a URL banner when an uploaded banner image is set", async () => { + test("produces a URL banner when an uploaded banner image is set", async () => { const image = await ImageFactory.create( - { submitterUserId: owner.id }, + { submitterUserId: users.id(1) }, { isValidated: true }, ); - await withUserId(owner.id, () => + await withUserId(users.id(1), () => UserCardRepository.updateOwnCard({ shortBio: null, bannerPresetImg: null, @@ -388,13 +387,13 @@ describe("UserCardRepository.findAllByUserIds", () => { }), ); - const { userCards } = await withUserId(owner.id, () => + const { userCards } = await withUserId(users.id(1), () => UserCardRepository.findAllByUserIds({ - userIds: [owner.id], + userIds: [users.id(1)], }), ); - const banner = userCards.get(owner.id)?.banner; + const banner = userCards.get(users.id(1))?.banner; expect(banner?.type).toBe("URL"); expect(banner).toHaveProperty("url"); }); @@ -420,7 +419,7 @@ describe("UserCardRepository.findAllByUserIdsCached", () => { : withNoUser(find); }; - it("gives every viewer their own private note", async () => { + test("gives every viewer their own private note", async () => { await withUserId(viewer.id, () => PrivateUserNoteRepository.upsertOwnNote({ targetId: target.id, @@ -443,7 +442,7 @@ describe("UserCardRepository.findAllByUserIdsCached", () => { expect(forAnonymous.userCards.get(target.id)?.privateNote).toBeNull(); }); - it("serves cards from the cache and queries only the users missing from it", async () => { + test("serves cards from the cache and queries only the users missing from it", async () => { const first = await cachedCard(target.id); expect(first.userCards.get(target.id)?.shortBio).toBeNull(); @@ -460,7 +459,7 @@ describe("UserCardRepository.findAllByUserIdsCached", () => { expect(userCards.get(viewer.id)?.id).toBe(viewer.id); }); - it("returns a fresh card once the cached one has expired", async () => { + test("returns a fresh card once the cached one has expired", async () => { await cachedCard(target.id); await updateShortBioDirectly(target.id, "edited"); @@ -470,7 +469,7 @@ describe("UserCardRepository.findAllByUserIdsCached", () => { expect(userCards.get(target.id)?.shortBio).toBe("edited"); }); - it("returns a fresh card right after its owner edited it", async () => { + test("returns a fresh card right after its owner edited it", async () => { await cachedCard(target.id); await withUserId(target.id, () => @@ -488,7 +487,7 @@ describe("UserCardRepository.findAllByUserIdsCached", () => { expect(userCards.get(target.id)?.shortBio).toBe("edited"); }); - it("coalesces concurrent misses for the same user into one query", async () => { + test("coalesces concurrent misses for the same user into one query", async () => { const [first, second] = await Promise.all([ cachedCard(target.id, viewer.id), cachedCard(target.id, otherViewer.id), diff --git a/app/features/user-card/user-card-search-params.test.ts b/app/features/user-card/user-card-search-params.test.ts index 2f632609f..b0499909c 100644 --- a/app/features/user-card/user-card-search-params.test.ts +++ b/app/features/user-card/user-card-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -9,13 +9,13 @@ import { } from "./user-card-search-params"; describe("userCardEditSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userCardEditSearchParams, { returnTo: ["/u/sendou", "/plans?weapon=1"], }); }); - it("unsafe redirect targets decode to default", () => { + test("unsafe redirect targets decode to default", () => { assertDecodesToDefault(userCardEditSearchParams, "returnTo", [ ["https://evil.example"], ["//evil.example"], @@ -25,13 +25,13 @@ describe("userCardEditSearchParams", () => { }); describe("userCardFriendshipSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userCardFriendshipSearchParams, { mutuals: [true, false], }); }); - it("garbage decodes to default", () => { + test("garbage decodes to default", () => { assertDecodesToDefault(userCardFriendshipSearchParams, "mutuals", [ ["1"], ["yes"], diff --git a/app/features/user-page/bio-json.server.test.ts b/app/features/user-page/bio-json.server.test.ts index bcffb6d1f..374824ac3 100644 --- a/app/features/user-page/bio-json.server.test.ts +++ b/app/features/user-page/bio-json.server.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as UserRepository from "./UserRepository.server"; describe("profile bio is always a string", () => { - it("keeps a JSON-object-shaped bio as text (not a parsed object)", async () => { + test("keeps a JSON-object-shaped bio as text (not a parsed object)", async () => { // a bio the user typed that happens to be valid JSON of object shape const user = await UserFactory.create({ profile: { bio: '{"note":"gg"}' }, diff --git a/app/features/user-page/core/badge-sorting.server.test.ts b/app/features/user-page/core/badge-sorting.server.test.ts index e754c2df5..d8738d047 100644 --- a/app/features/user-page/core/badge-sorting.server.test.ts +++ b/app/features/user-page/core/badge-sorting.server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { sortBadgesByFavorites } from "./badge-sorting.server"; const badge = (id: number) => ({ @@ -8,7 +8,7 @@ const badge = (id: number) => ({ }); describe("sortBadgesByFavorites", () => { - it("returns badges sorted by descending id when no favorites", () => { + test("returns badges sorted by descending id when no favorites", () => { const result = sortBadgesByFavorites({ favoriteBadgeIds: null, badges: [badge(1), badge(3), badge(2)], @@ -19,7 +19,7 @@ describe("sortBadgesByFavorites", () => { expect(result.favoriteBadgeIds).toBeNull(); }); - it("places favorites first in order for supporters", () => { + test("places favorites first in order for supporters", () => { const result = sortBadgesByFavorites({ favoriteBadgeIds: [2, 1], badges: [badge(1), badge(2), badge(3)], @@ -30,7 +30,7 @@ describe("sortBadgesByFavorites", () => { expect(result.favoriteBadgeIds).toEqual([2, 1]); }); - it("limits non-supporters to one favorite", () => { + test("limits non-supporters to one favorite", () => { const result = sortBadgesByFavorites({ favoriteBadgeIds: [2, 3], badges: [badge(1), badge(2), badge(3)], @@ -41,7 +41,7 @@ describe("sortBadgesByFavorites", () => { expect(result.badges[0].id).toBe(2); }); - it("filters out unowned favorite badge ids", () => { + test("filters out unowned favorite badge ids", () => { const result = sortBadgesByFavorites({ favoriteBadgeIds: [99, 1], badges: [badge(1), badge(2)], @@ -51,7 +51,7 @@ describe("sortBadgesByFavorites", () => { expect(result.favoriteBadgeIds).toEqual([1]); }); - it("returns null favoriteBadgeIds when all favorites are unowned", () => { + test("returns null favoriteBadgeIds when all favorites are unowned", () => { const result = sortBadgesByFavorites({ favoriteBadgeIds: [99], badges: [badge(1), badge(2)], diff --git a/app/features/user-page/core/build-sorting.server.test.ts b/app/features/user-page/core/build-sorting.server.test.ts index befdaf6da..9e84d5e60 100644 --- a/app/features/user-page/core/build-sorting.server.test.ts +++ b/app/features/user-page/core/build-sorting.server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { databaseTimestampNow } from "~/utils/dates"; import { sortBuilds } from "./build-sorting.server"; @@ -31,7 +31,7 @@ const mockBuild = ( }; describe("sortBuilds()", () => { - it("sorts by UPDATED_AT", () => { + test("sorts by UPDATED_AT", () => { const builds = [ mockBuild({ id: 1, updatedAt: 1 }), mockBuild({ id: 2, updatedAt: 3 }), @@ -48,7 +48,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[1].id).toBe(3); }); - it("sorts by TOP_500", () => { + test("sorts by TOP_500", () => { const builds = [ mockBuild({ id: 1 }), mockBuild({ @@ -76,7 +76,7 @@ describe("sortBuilds()", () => { // Add other test cases similarly... - it("sorts by both PUBLIC_BUILD and PRIVATE_BUILD", () => { + test("sorts by both PUBLIC_BUILD and PRIVATE_BUILD", () => { const builds = [ mockBuild({ id: 1, isPrivate: 1 }), mockBuild({ id: 2, isPrivate: 0 }), @@ -107,7 +107,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds2[3].id).toBe(4); }); - it("sorts by WEAPON_POOL", () => { + test("sorts by WEAPON_POOL", () => { const builds = [ mockBuild({ id: 1, @@ -133,7 +133,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[1].id).toBe(2); }); - it("sorts by WEAPON_POOL (alt kits are same priority)", () => { + test("sorts by WEAPON_POOL (alt kits are same priority)", () => { const mockBuildBuilder = ( id: number, weaponIds: MainWeaponId[], @@ -192,7 +192,7 @@ describe("sortBuilds()", () => { } }); - it("sorts by ALPHABETICAL_TITLE", () => { + test("sorts by ALPHABETICAL_TITLE", () => { const builds = [ mockBuild({ id: 1, @@ -218,7 +218,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[1].id).toBe(2); }); - it("sorts by WEAPON_IN_GAME_ORDER", () => { + test("sorts by WEAPON_IN_GAME_ORDER", () => { const builds = [ mockBuild({ id: 1, @@ -246,7 +246,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[2].id).toBe(2); }); - it("sorts by MODE", () => { + test("sorts by MODE", () => { const builds = [ mockBuild({ id: 1, @@ -271,7 +271,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[2].id).toBe(2); }); - it("sorts by MODE (no mode last)", () => { + test("sorts by MODE (no mode last)", () => { const builds = [ mockBuild({ id: 1, @@ -297,7 +297,7 @@ describe("sortBuilds()", () => { }); for (const identifier of ["HEADGEAR_ID", "CLOTHES_ID", "SHOES_ID"]) { - it(`sorts by ${identifier}`, () => { + test(`sorts by ${identifier}`, () => { const key = ( { HEADGEAR_ID: "headGearSplId", @@ -330,7 +330,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[2].id).toBe(1); }); - it(`sorts ${identifier} with null gear last`, () => { + test(`sorts ${identifier} with null gear last`, () => { const key = ( { HEADGEAR_ID: "headGearSplId", @@ -366,7 +366,7 @@ describe("sortBuilds()", () => { }); } - it("sorts when buildSort not given", () => { + test("sorts when buildSort not given", () => { const builds = [mockBuild({}), mockBuild({}), mockBuild({})]; sortBuilds({ @@ -376,7 +376,7 @@ describe("sortBuilds()", () => { }); }); - it("sorts by UPDATED_AT and ALPHABETICAL_TITLE", () => { + test("sorts by UPDATED_AT and ALPHABETICAL_TITLE", () => { const builds = [ mockBuild({ id: 1, updatedAt: 3, title: "C" }), mockBuild({ id: 2, updatedAt: 2, title: "B" }), @@ -393,7 +393,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[1].id).toBe(3); }); - it("sorts by ALPHABETICAL_TITLE and UPDATED_AT (reverse)", () => { + test("sorts by ALPHABETICAL_TITLE and UPDATED_AT (reverse)", () => { const builds = [ mockBuild({ id: 1, updatedAt: 3, title: "C" }), mockBuild({ id: 2, updatedAt: 2, title: "B" }), @@ -409,7 +409,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[0].id).toBe(3); }); - it("sorts by PUBLIC_BUILD", () => { + test("sorts by PUBLIC_BUILD", () => { const builds = [ mockBuild({ id: 1, isPrivate: 1 }), mockBuild({ id: 2, isPrivate: 1 }), @@ -427,7 +427,7 @@ describe("sortBuilds()", () => { expect(sortedBuilds[2].id).toBe(2); }); - it("sorts by PRIVATE_BUILD", () => { + test("sorts by PRIVATE_BUILD", () => { const builds = [ mockBuild({ id: 1, isPrivate: 0 }), mockBuild({ id: 2, isPrivate: 1 }), diff --git a/app/features/user-page/core/widgets/leaderboard-cache.test.ts b/app/features/user-page/core/widgets/leaderboard-cache.test.ts index 41e9cb715..b94966b4a 100644 --- a/app/features/user-page/core/widgets/leaderboard-cache.test.ts +++ b/app/features/user-page/core/widgets/leaderboard-cache.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; // One finished season, one user sitting at rank 5 (so: top-10 AND top-100). vi.mock("~/features/mmr/core/Seasons", () => ({ @@ -11,7 +11,7 @@ vi.mock("~/features/leaderboards/LeaderboardRepository.server", () => ({ import { cachedUserSQLeaderboardTopData } from "./utils.server"; describe("SendouQ leaderboard widget cache", () => { - it("counts a season once when the cache is filled concurrently", async () => { + test("counts a season once when the cache is filled concurrently", async () => { // This is exactly how UserRepository.findWidgetsByUserId drives it: the // top-10-seasons and top-100-seasons widgets both call the cache inside the // same Promise.all, so a cold cache is populated by two concurrent callers. diff --git a/app/features/user-page/custom-theme-json.server.test.ts b/app/features/user-page/custom-theme-json.server.test.ts index 1d95c797e..71b62e6dc 100644 --- a/app/features/user-page/custom-theme-json.server.test.ts +++ b/app/features/user-page/custom-theme-json.server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { clampThemeToGamut } from "~/utils/oklch-gamut"; import * as UserRepository from "./UserRepository.server"; @@ -19,7 +19,7 @@ const CUSTOM_THEME = clampThemeToGamut({ }); describe("supporter custom theme on the profile layout", () => { - it("comes back parsed", async () => { + test("comes back parsed", async () => { const user = await UserFactory.create(null, { patronTier: 2, customTheme: CUSTOM_THEME, @@ -36,7 +36,7 @@ describe("supporter custom theme on the profile layout", () => { ); }); - it("is null for a user who is not a supporter", async () => { + test("is null for a user who is not a supporter", async () => { const user = await UserFactory.create(null, { customTheme: CUSTOM_THEME, }); diff --git a/app/features/user-page/in-game-name.test.ts b/app/features/user-page/in-game-name.test.ts index 7be45e633..e7f2ea9ee 100644 --- a/app/features/user-page/in-game-name.test.ts +++ b/app/features/user-page/in-game-name.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { inGameNameIsValid } from "./in-game-name"; describe("inGameNameIsValid", () => { - it("should pass valid in-game names", () => { + test("passes valid in-game names", () => { const validNames = [ "Sendou#12345", "The Player#12345", @@ -38,7 +38,7 @@ describe("inGameNameIsValid", () => { } }); - it("should not pass invalid in-game names", () => { + test("does not pass invalid in-game names", () => { const invalidNames = [ "#1234", "Sendou1234", @@ -58,7 +58,7 @@ describe("inGameNameIsValid", () => { } }); - it("should reject characters the Switch keyboard does not allow in names", () => { + test("rejects characters the Switch keyboard does not allow in names", () => { const invalidNames = [ "test@me#1234", "100%#1234", diff --git a/app/features/user-page/routes/u.$identifier.edit.test.ts b/app/features/user-page/routes/u.$identifier.edit.test.ts index ee4ddacd9..42bf9c052 100644 --- a/app/features/user-page/routes/u.$identifier.edit.test.ts +++ b/app/features/user-page/routes/u.$identifier.edit.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { wrappedAction } from "~/utils/Test"; @@ -37,7 +37,7 @@ describe("user page editing", () => { userId = (await UserFactory.createRegular()).id; }); - it("saves profile with default fields", async () => { + test("saves profile with default fields", async () => { const response = await action( { ...DEFAULT_FIELDS, diff --git a/app/features/user-page/user-page-search-params.test.ts b/app/features/user-page/user-page-search-params.test.ts index d6ff0a049..62ddd73e5 100644 --- a/app/features/user-page/user-page-search-params.test.ts +++ b/app/features/user-page/user-page-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import * as Seasons from "~/features/mmr/core/Seasons"; import { BEST_TIER_NUMBER, @@ -28,7 +28,7 @@ const notStartedSeason = newestSeason + 1000; const currentYear = new Date().getFullYear(); describe("userResultsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userResultsSearchParams, { highlightsOnly: [false, true], page: [1, 2, 1000], @@ -45,7 +45,7 @@ describe("userResultsSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(userResultsSearchParams, "highlightsOnly", [ ["1"], ["yes"], @@ -87,7 +87,7 @@ describe("userResultsSearchParams", () => { }); describe("userSeasonsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userSeasonsSearchParams, { page: [1, 2, 99], info: ["weapons", "stages", "mates", "enemies"], @@ -95,7 +95,7 @@ describe("userSeasonsSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(userSeasonsSearchParams, "page", [["0"], ["abc"]]); assertDecodesToDefault(userSeasonsSearchParams, "info", [["INVALID"]]); assertDecodesToDefault(userSeasonsSearchParams, "season", [ @@ -105,7 +105,7 @@ describe("userSeasonsSearchParams", () => { ]); }); - it("decodes a season correctly even if the same URL was visited before the season started", () => { + test("decodes a season correctly even if the same URL was visited before the season started", () => { vi.useFakeTimers(); try { // crawler or a user replays ?season=1 a few days before season 1 opens @@ -126,13 +126,13 @@ describe("userSeasonsSearchParams", () => { }); describe("userSeasonSummaryGraphicSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userSeasonSummaryGraphicSearchParams, { season: [null, newestSeason, oldestSeason], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(userSeasonSummaryGraphicSearchParams, "season", [ [String(notStartedSeason)], ["abc"], @@ -141,14 +141,14 @@ describe("userSeasonSummaryGraphicSearchParams", () => { }); describe("userBuildsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userBuildsSearchParams, { weapon: ["ALL", "PUBLIC", "PRIVATE", 0, 40, 8010], sorting: [false, true], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(userBuildsSearchParams, "weapon", [ ["999999"], ["foo"], @@ -159,14 +159,14 @@ describe("userBuildsSearchParams", () => { }); describe("userArtSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userArtSearchParams, { source: ["ALL", "MADE-BY", "MADE-OF"], tag: ["chibi", "tag with spaces"], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(userArtSearchParams, "source", [["INVALID"]]); }); }); diff --git a/app/features/vods/vods-search-params.test.ts b/app/features/vods/vods-search-params.test.ts index 4a0baea2c..a732da9c1 100644 --- a/app/features/vods/vods-search-params.test.ts +++ b/app/features/vods/vods-search-params.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -11,7 +11,7 @@ import { } from "./vods-search-params"; describe("vodsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(vodsSearchParams, { page: [1, 2, 1000], weapon: [null, 0, 40, 8010], @@ -21,7 +21,7 @@ describe("vodsSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(vodsSearchParams, "page", [ ["0"], ["1001"], @@ -44,7 +44,7 @@ describe("vodsNewSearchParams", () => { weapons: [40 as const, null], }; - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(vodsNewSearchParams, { vod: [null, 1, 12345], ingest: [ @@ -55,7 +55,7 @@ describe("vodsNewSearchParams", () => { }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(vodsNewSearchParams, "vod", [ ["0"], ["-1"], @@ -71,13 +71,13 @@ describe("vodsNewSearchParams", () => { }); describe("vodsVodSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(vodsVodSearchParams, { start: [0, 1, 5312], }); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(vodsVodSearchParams, "start", [ ["-1"], ["NaN"], @@ -87,7 +87,7 @@ describe("vodsVodSearchParams", () => { }); describe("userVodsSearchParams", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(userVodsSearchParams, { page: [1, 7, 1000], }); diff --git a/app/features/vods/vods-utils.test.ts b/app/features/vods/vods-utils.test.ts index f4c3b422a..b19f87aec 100644 --- a/app/features/vods/vods-utils.test.ts +++ b/app/features/vods/vods-utils.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import type { MainWeaponId, ModeShort, @@ -15,55 +15,55 @@ import { } from "./vods-utils"; describe("extractYoutubeIdFromVideoUrl", () => { - it("should extract YouTube ID from a standard YouTube URL", () => { + test("extracts YouTube ID from a standard YouTube URL", () => { const url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should extract YouTube ID from a shortened YouTube URL", () => { + test("extracts YouTube ID from a shortened YouTube URL", () => { const url = "https://youtu.be/dQw4w9WgXcQ"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should extract YouTube ID from a YouTube live URL", () => { + test("extracts YouTube ID from a YouTube live URL", () => { const url = "https://www.youtube.com/live/dQw4w9WgXcQ"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should strip share tracking params from a shortened YouTube URL", () => { + test("strips share tracking params from a shortened YouTube URL", () => { const url = "https://youtu.be/fuj_pSAbU-A?si=mAzDxgrIJWLO1ykq"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("fuj_pSAbU-A"); }); - it("should strip extra query params from a standard YouTube URL", () => { + test("strips extra query params from a standard YouTube URL", () => { const url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=120"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should strip query params from a YouTube live URL", () => { + test("strips query params from a YouTube live URL", () => { const url = "https://www.youtube.com/live/dQw4w9WgXcQ?feature=shared"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should strip url fragments", () => { + test("strips url fragments", () => { const url = "https://youtu.be/dQw4w9WgXcQ#t=1m"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBe("dQw4w9WgXcQ"); }); - it("should return null for an invalid YouTube URL", () => { + test("returns null for an invalid YouTube URL", () => { const url = "https://www.example.com/watch?v=dQw4w9WgXcQ"; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBeNull(); }); - it("should return null for a URL without a video ID", () => { + test("returns null for a URL without a video ID", () => { const url = "https://www.youtube.com/watch?v="; const result = extractYoutubeIdFromVideoUrl(url); expect(result).toBeNull(); @@ -71,28 +71,28 @@ describe("extractYoutubeIdFromVideoUrl", () => { }); describe("secondsToHoursMinutesSecondString", () => { - it("should convert seconds to HH:MM:SS format", () => { + test("converts seconds to HH:MM:SS format", () => { const result = secondsToHoursMinutesSecondString(3661); expect(result).toBe("1:01:01"); }); - it("should convert seconds to MM:SS format if less than an hour", () => { + test("converts seconds to MM:SS format if less than an hour", () => { const result = secondsToHoursMinutesSecondString(61); expect(result).toBe("1:01"); }); - it("should handle zero seconds", () => { + test("handles zero seconds", () => { const result = secondsToHoursMinutesSecondString(0); expect(result).toBe("0:00"); }); - it("should throw an error for a negative number of seconds", () => { + test("throws an error for a negative number of seconds", () => { expect(() => secondsToHoursMinutesSecondString(-1)).toThrow( "Negative number of seconds", ); }); - it("should throw an error for a non-integer number of seconds", () => { + test("throws an error for a non-integer number of seconds", () => { expect(() => secondsToHoursMinutesSecondString(1.5)).toThrow( "Non-integer number of seconds", ); @@ -141,7 +141,7 @@ const LONG_MODE_RESOLVERS = { }; describe("generateYoutubeTimestamps", () => { - it("should include intro line when first match starts after 0", () => { + test("includes intro line when first match starts after 0", () => { const matches = [ makeMatch({ startsAt: 521, @@ -164,7 +164,7 @@ describe("generateYoutubeTimestamps", () => { ); }); - it("should not include intro line when first match starts at 0", () => { + test("does not include intro line when first match starts at 0", () => { const matches = [ makeMatch({ startsAt: 0, @@ -179,7 +179,7 @@ describe("generateYoutubeTimestamps", () => { expect(result).toBe("0:00 Splattershot / RM Scorch Gorge"); }); - it("should not include weapon for CAST type", () => { + test("does not include weapon for CAST type", () => { const matches = [ makeMatch({ startsAt: 25, @@ -194,7 +194,7 @@ describe("generateYoutubeTimestamps", () => { expect(result).toBe("0:00 Intro\n0:25 CB MakoMart"); }); - it("should use long mode names when resolver returns them", () => { + test("uses long mode names when resolver returns them", () => { const matches = [ makeMatch({ startsAt: 521, @@ -235,7 +235,7 @@ describe("vodToVideoBeingAdded", () => { process.env.TZ = originalTimezone; }); - it("round-trips the stored day/month/year regardless of server timezone", () => { + test("round-trips the stored day/month/year regardless of server timezone", () => { const date = { day: 5, month: 0, year: 2024 }; const vod: Vod = { id: 1, @@ -254,27 +254,27 @@ describe("vodToVideoBeingAdded", () => { }); describe("hoursMinutesSecondsStringToSeconds", () => { - it("should convert HH:MM:SS format to seconds", () => { + test("converts HH:MM:SS format to seconds", () => { const result = hoursMinutesSecondsStringToSeconds("1:01:01"); expect(result).toBe(3661); }); - it("should convert MM:SS format to seconds", () => { + test("converts MM:SS format to seconds", () => { const result = hoursMinutesSecondsStringToSeconds("1:01"); expect(result).toBe(61); }); - it("should convert MM:SS format to seconds (zero padded minutes)", () => { + test("converts MM:SS format to seconds (zero padded minutes)", () => { const result = hoursMinutesSecondsStringToSeconds("01:01"); expect(result).toBe(61); }); - it("should handle zero seconds", () => { + test("handles zero seconds", () => { const result = hoursMinutesSecondsStringToSeconds("0:00"); expect(result).toBe(0); }); - it("should throw an error for an invalid format", () => { + test("throws an error for an invalid format", () => { expect(() => hoursMinutesSecondsStringToSeconds("1:01:01:01")).toThrow( "Invalid time format", ); diff --git a/app/form/SendouForm.browser.test.tsx b/app/form/SendouForm.browser.test.tsx index 421d31e35..6c3737d33 100644 --- a/app/form/SendouForm.browser.test.tsx +++ b/app/form/SendouForm.browser.test.tsx @@ -27,6 +27,58 @@ import type { ArrayItemRenderContext } from "./types"; let mockFetcherData: { fieldErrors?: Record } | undefined; +/** `fieldset` registers its metadata onto the object it is handed, so anything nested needs a fresh one. */ +const singleTextField = () => + z.object({ + name: textField({ label: "labels.name", maxLength: 100 }), + }); + +const SINGLE_TEXT_FIELD = singleTextField(); + +const NESTED_FIELDSET = z.object({ + member: fieldset({ + label: "labels.member", + fields: singleTextField(), + }), +}); + +const FIELDSET_ARRAY = z.object({ + members: array({ + label: "labels.members", + min: 0, + max: 10, + field: fieldset({ fields: singleTextField() }), + }), +}); + +const TEXT_FIELD_ARRAY = z.object({ + urls: array({ + label: "labels.urls", + min: 0, + max: 5, + field: textField({ maxLength: 100 }), + }), +}); + +const TOGGLE = z.object({ + noScreen: toggleField({ label: "labels.noScreen" }), +}); + +const CHECKBOX_GROUP = z.object({ + modes: checkboxGroup({ + label: "labels.buildModes", + items: [ + { label: "modes.TW", value: "TW" }, + { label: "modes.SZ", value: "SZ" }, + ], + minLength: 1, + }), +}); + +const TIME_RANGE = z.object({ + times: timeRangeOptional({}), +}); + vi.mock("react-router", async () => { const actual = await vi.importActual("react-router"); return { @@ -86,21 +138,15 @@ describe("SendouForm", () => { describe("basic form rendering", () => { test("renders form with title", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), + const screen = await renderForm(SINGLE_TEXT_FIELD, { + title: "Test Form", }); - const screen = await renderForm(schema, { title: "Test Form" }); - await expect.element(screen.getByText("Test Form")).toBeVisible(); }); test("renders submit button with default text", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); await expect .element(screen.getByRole("button", { name: "Submit" })) @@ -108,11 +154,7 @@ describe("SendouForm", () => { }); test("renders submit button with custom text", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(SINGLE_TEXT_FIELD, { submitButtonText: "Save Changes", }); @@ -122,12 +164,10 @@ describe("SendouForm", () => { }); test("hides submit button in autoSubmit mode", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), + const screen = await renderForm(SINGLE_TEXT_FIELD, { + mode: "autoSubmit", }); - const screen = await renderForm(schema, { mode: "autoSubmit" }); - const submitButton = screen.container.querySelector( 'button[type="submit"]', ); @@ -137,21 +177,13 @@ describe("SendouForm", () => { describe("text field", () => { test("renders with label", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); await expect.element(screen.getByLabelText("Name")).toBeVisible(); }); test("typing updates value", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); const input = screen.getByLabelText("Name"); await userEvent.type(input.element(), "Test Value"); @@ -160,11 +192,7 @@ describe("SendouForm", () => { }); test("shows error on blur when required field is empty", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); const input = screen.getByLabelText("Name"); await userEvent.click(input.element()); @@ -176,11 +204,7 @@ describe("SendouForm", () => { }); test("shows error on submit when required field is empty", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); await screen.getByRole("button", { name: "Submit" }).click(); @@ -190,11 +214,7 @@ describe("SendouForm", () => { }); test("clears error when valid value is entered after submit", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(SINGLE_TEXT_FIELD); await screen.getByRole("button", { name: "Submit" }).click(); await expect @@ -222,11 +242,7 @@ describe("SendouForm", () => { }); test("initializes with default value", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(SINGLE_TEXT_FIELD, { defaultValues: { name: "Default Name" }, }); @@ -410,11 +426,7 @@ describe("SendouForm", () => { describe("toggle/switch field", () => { test("renders toggle with label", async () => { - const schema = z.object({ - noScreen: toggleField({ label: "labels.noScreen" }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TOGGLE); await expect .element(screen.getByText("[Accessibility] Avoid Splattercolor Screen")) @@ -422,11 +434,7 @@ describe("SendouForm", () => { }); test("clicking toggles value", async () => { - const schema = z.object({ - noScreen: toggleField({ label: "labels.noScreen" }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TOGGLE); const switchElement = screen.getByRole("switch"); await expect.element(switchElement).not.toBeChecked(); @@ -440,11 +448,7 @@ describe("SendouForm", () => { }); test("initializes with default value", async () => { - const schema = z.object({ - noScreen: toggleField({ label: "labels.noScreen" }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(TOGGLE, { defaultValues: { noScreen: true }, }); @@ -577,18 +581,7 @@ describe("SendouForm", () => { }); test("shows error when minimum selections not met", async () => { - const schema = z.object({ - modes: checkboxGroup({ - label: "labels.buildModes", - items: [ - { label: "modes.TW", value: "TW" }, - { label: "modes.SZ", value: "SZ" }, - ], - minLength: 1, - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(CHECKBOX_GROUP); await screen.getByRole("button", { name: "Submit" }).click(); @@ -598,18 +591,7 @@ describe("SendouForm", () => { }); test("checking an option that satisfies the minimum shows no error", async () => { - const schema = z.object({ - modes: checkboxGroup({ - label: "labels.buildModes", - items: [ - { label: "modes.TW", value: "TW" }, - { label: "modes.SZ", value: "SZ" }, - ], - minLength: 1, - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(CHECKBOX_GROUP); await userEvent.click(screen.getByLabelText("Turf War").element()); @@ -619,18 +601,7 @@ describe("SendouForm", () => { }); test("unchecking below the minimum shows the error without a submit", async () => { - const schema = z.object({ - modes: checkboxGroup({ - label: "labels.buildModes", - items: [ - { label: "modes.TW", value: "TW" }, - { label: "modes.SZ", value: "SZ" }, - ], - minLength: 1, - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(CHECKBOX_GROUP); const twCheckbox = screen.getByLabelText("Turf War"); await userEvent.click(twCheckbox.element()); @@ -749,15 +720,11 @@ describe("SendouForm", () => { describe("server error fallback", () => { test("shows fallback error when server returns error for field without DOM element", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - mockFetcherData = { fieldErrors: { hiddenField: "forms:errors.required" }, }; - const screen = await renderForm(schema, { + const screen = await renderForm(SINGLE_TEXT_FIELD, { defaultValues: { name: "Test" }, }); @@ -767,15 +734,11 @@ describe("SendouForm", () => { }); test("does not show fallback error when server error has corresponding DOM element", async () => { - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); - mockFetcherData = { fieldErrors: { name: "forms:errors.required" }, }; - const screen = await renderForm(schema, { + const screen = await renderForm(SINGLE_TEXT_FIELD, { defaultValues: { name: "Test" }, }); @@ -786,10 +749,7 @@ describe("SendouForm", () => { describe("time range field", () => { test("renders two time inputs", async () => { - const schema = z.object({ - times: timeRangeOptional({}), - }); - const screen = await renderForm(schema); + const screen = await renderForm(TIME_RANGE); const timeInputs = screen.container.querySelectorAll('input[type="time"]'); @@ -797,11 +757,7 @@ describe("SendouForm", () => { }); test("initializes with default value", async () => { - const schema = z.object({ - times: timeRangeOptional({}), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(TIME_RANGE, { defaultValues: { times: { start: "09:00", end: "17:00" } }, }); @@ -812,11 +768,7 @@ describe("SendouForm", () => { }); test("updating time input changes value", async () => { - const schema = z.object({ - times: timeRangeOptional({}), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TIME_RANGE); const timeInputs = screen.container.querySelectorAll('input[type="time"]'); @@ -831,9 +783,6 @@ describe("SendouForm", () => { describe("onApply callback", () => { test("calls onApply with form values instead of fetcher.submit", async () => { const onApply = vi.fn(); - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); const router = createMemoryRouter( [ @@ -841,7 +790,7 @@ describe("SendouForm", () => { path: "/", element: ( @@ -861,16 +810,13 @@ describe("SendouForm", () => { test("does not call onApply when validation fails", async () => { const onApply = vi.fn(); - const schema = z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }); const router = createMemoryRouter( [ { path: "/", element: ( - + ), @@ -891,16 +837,7 @@ describe("SendouForm", () => { describe("fieldset field", () => { test("renders fieldset with legend", async () => { - const schema = z.object({ - member: fieldset({ - label: "labels.member", - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(NESTED_FIELDSET); await expect.element(screen.getByText("Member")).toBeVisible(); }); @@ -923,16 +860,7 @@ describe("SendouForm", () => { }); test("typing in nested field updates value", async () => { - const schema = z.object({ - member: fieldset({ - label: "labels.member", - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(NESTED_FIELDSET); const input = screen.getByLabelText("Name"); await userEvent.type(input.element(), "Test Name"); @@ -941,16 +869,7 @@ describe("SendouForm", () => { }); test("initializes nested fields with default values", async () => { - const schema = z.object({ - member: fieldset({ - label: "labels.member", - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(NESTED_FIELDSET, { defaultValues: { member: { name: "Default Name" } }, }); @@ -962,16 +881,7 @@ describe("SendouForm", () => { describe("array field with primitive items", () => { test("renders add button", async () => { - const schema = z.object({ - urls: array({ - label: "labels.urls", - min: 0, - max: 5, - field: textField({ maxLength: 100 }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TEXT_FIELD_ARRAY); await expect .element(screen.getByRole("button", { name: "Add" })) @@ -979,16 +889,7 @@ describe("SendouForm", () => { }); test("renders one starter item for an empty array", async () => { - const schema = z.object({ - urls: array({ - label: "labels.urls", - min: 0, - max: 5, - field: textField({ maxLength: 100 }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TEXT_FIELD_ARRAY); const inputs = screen.container.querySelectorAll('input[type="text"]'); expect(inputs.length).toBe(1); @@ -1000,16 +901,7 @@ describe("SendouForm", () => { }); test("clicking add creates new item", async () => { - const schema = z.object({ - urls: array({ - label: "labels.urls", - min: 0, - max: 5, - field: textField({ maxLength: 100 }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(TEXT_FIELD_ARRAY); // Adding from the single empty starter row materializes it and appends a // new one, so one click goes from 1 visible row to 2. @@ -1025,16 +917,7 @@ describe("SendouForm", () => { }); test("renders remove button for each item when above minimum", async () => { - const schema = z.object({ - urls: array({ - label: "labels.urls", - min: 0, - max: 5, - field: textField({ maxLength: 100 }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(TEXT_FIELD_ARRAY, { defaultValues: { urls: ["http://example.com"] }, }); @@ -1045,16 +928,7 @@ describe("SendouForm", () => { }); test("clicking remove deletes item", async () => { - const schema = z.object({ - urls: array({ - label: "labels.urls", - min: 0, - max: 5, - field: textField({ maxLength: 100 }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(TEXT_FIELD_ARRAY, { defaultValues: { urls: ["http://example.com", "http://test.com"] }, }); @@ -1091,20 +965,7 @@ describe("SendouForm", () => { describe("array field with fieldset items", () => { test("renders array items as fieldsets", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(FIELDSET_ARRAY, { defaultValues: { members: [{ name: "Alice" }] }, }); @@ -1113,20 +974,7 @@ describe("SendouForm", () => { }); test("renders one starter fieldset for an empty array", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(FIELDSET_ARRAY); await expect.element(screen.getByText("#1")).toBeVisible(); @@ -1140,20 +988,7 @@ describe("SendouForm", () => { }); test("add button creates new fieldset item", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema); + const screen = await renderForm(FIELDSET_ARRAY); await screen.getByRole("button", { name: "Add" }).click(); await screen.getByRole("button", { name: "Add" }).click(); @@ -1162,20 +997,7 @@ describe("SendouForm", () => { }); test("remove button removes fieldset item", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(FIELDSET_ARRAY, { defaultValues: { members: [{ name: "Alice" }, { name: "Bob" }] }, }); @@ -1199,9 +1021,7 @@ describe("SendouForm", () => { max: 10, sortable: true, field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), + fields: singleTextField(), }), }), }); @@ -1231,20 +1051,7 @@ describe("SendouForm", () => { }); test("non-sortable array renders no move buttons", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(FIELDSET_ARRAY, { defaultValues: { members: [{ name: "Alice" }, { name: "Bob" }] }, }); @@ -1306,20 +1113,7 @@ describe("SendouForm", () => { }); test("typing in nested fieldset field updates value", async () => { - const schema = z.object({ - members: array({ - label: "labels.members", - min: 0, - max: 10, - field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), - }), - }), - }); - - const screen = await renderForm(schema, { + const screen = await renderForm(FIELDSET_ARRAY, { defaultValues: { members: [{ name: "" }] }, }); @@ -1414,9 +1208,7 @@ describe("SendouForm", () => { min: 1, max: 10, field: fieldset({ - fields: z.object({ - name: textField({ label: "labels.name", maxLength: 100 }), - }), + fields: singleTextField(), }), }), }); diff --git a/app/hooks/useRecentlyReportedWeapons.test.ts b/app/hooks/useRecentlyReportedWeapons.test.ts index 84e68c357..f75a1a2d1 100644 --- a/app/hooks/useRecentlyReportedWeapons.test.ts +++ b/app/hooks/useRecentlyReportedWeapons.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,11 +6,11 @@ import { import { recentlyReportedWeaponsPersisted } from "./useRecentlyReportedWeapons"; describe("recentlyReportedWeaponsPersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(recentlyReportedWeaponsPersisted, [[], [0, 10, 8000]]); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(recentlyReportedWeaponsPersisted, [ "not json", "[99999]", diff --git a/app/hooks/useSpoilerFree.test.ts b/app/hooks/useSpoilerFree.test.ts index 78212a457..1a348a712 100644 --- a/app/hooks/useSpoilerFree.test.ts +++ b/app/hooks/useSpoilerFree.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,11 +6,11 @@ import { import { revealedTournamentsPersisted } from "./useSpoilerFree"; describe("revealedTournamentsPersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(revealedTournamentsPersisted, [[], [1, 2, 3]]); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(revealedTournamentsPersisted, ["not json", "1"]); }); }); diff --git a/app/hooks/useUnseenFriendRequests.test.ts b/app/hooks/useUnseenFriendRequests.test.ts index 3a4f1987b..b93df3acd 100644 --- a/app/hooks/useUnseenFriendRequests.test.ts +++ b/app/hooks/useUnseenFriendRequests.test.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, test } from "vitest"; import { assertDecodesToDefault, assertRoundTrips, @@ -6,11 +6,11 @@ import { import { seenFriendRequestsPersisted } from "./useUnseenFriendRequests"; describe("seenFriendRequestsPersisted", () => { - it("round-trips", () => { + test("round-trips", () => { assertRoundTrips(seenFriendRequestsPersisted, [[], [3, 2, 1]]); }); - it("malformed values decode to the default", () => { + test("malformed values decode to the default", () => { assertDecodesToDefault(seenFriendRequestsPersisted, [ "not json", '{"a":1}', diff --git a/app/modules/cache/index.test.ts b/app/modules/cache/index.test.ts index 191eec42e..364cd06f9 100644 --- a/app/modules/cache/index.test.ts +++ b/app/modules/cache/index.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { LRUCache } from "./index"; describe("LRUCache", () => { - it("stores and retrieves values", () => { + test("stores and retrieves values", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); @@ -10,14 +10,14 @@ describe("LRUCache", () => { expect(cache.has("a")).toBe(true); }); - it("returns undefined for missing keys", () => { + test("returns undefined for missing keys", () => { const cache = new LRUCache({ max: 2 }); expect(cache.get("missing")).toBeUndefined(); expect(cache.has("missing")).toBe(false); }); - it("evicts the least recently used entry once over capacity", () => { + test("evicts the least recently used entry once over capacity", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); cache.set("b", 2); @@ -28,7 +28,7 @@ describe("LRUCache", () => { expect(cache.get("c")).toBe(3); }); - it("counts reads as recent use", () => { + test("counts reads as recent use", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); cache.set("b", 2); @@ -39,7 +39,7 @@ describe("LRUCache", () => { expect(cache.has("b")).toBe(false); }); - it("updates value without growing past capacity on re-set", () => { + test("updates value without growing past capacity on re-set", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); cache.set("a", 2); @@ -47,7 +47,7 @@ describe("LRUCache", () => { expect(cache.get("a")).toBe(2); }); - it("deletes a single entry", () => { + test("deletes a single entry", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); cache.delete("a"); @@ -55,7 +55,7 @@ describe("LRUCache", () => { expect(cache.has("a")).toBe(false); }); - it("clears all entries", () => { + test("clears all entries", () => { const cache = new LRUCache({ max: 2 }); cache.set("a", 1); cache.set("b", 2); diff --git a/app/modules/csv.test.ts b/app/modules/csv.test.ts index 86eef9030..0359acc24 100644 --- a/app/modules/csv.test.ts +++ b/app/modules/csv.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import * as CSV from "./csv"; describe("CSV.serialize", () => { - it("joins cells with commas and rows with CRLF", () => { + test("joins cells with commas and rows with CRLF", () => { expect( CSV.serialize([ ["a", "b"], @@ -11,40 +11,40 @@ describe("CSV.serialize", () => { ).toBe("a,b\r\nc,d"); }); - it("quotes cells containing the delimiter", () => { + test("quotes cells containing the delimiter", () => { expect(CSV.serialize([["a,b", "c"]])).toBe('"a,b",c'); }); - it("escapes quotes by doubling them", () => { + test("escapes quotes by doubling them", () => { expect(CSV.serialize([['say "hi"']])).toBe('"say ""hi"""'); }); - it("quotes cells containing line breaks", () => { + test("quotes cells containing line breaks", () => { expect(CSV.serialize([["line1\nline2"]])).toBe('"line1\nline2"'); expect(CSV.serialize([["line1\rline2"]])).toBe('"line1\rline2"'); }); - it("leaves plain cells unquoted", () => { + test("leaves plain cells unquoted", () => { expect(CSV.serialize([["plain", "text"]])).toBe("plain,text"); }); - it("handles empty input", () => { + test("handles empty input", () => { expect(CSV.serialize([])).toBe(""); }); - it("neutralizes formula injection by prefixing a quote", () => { + test("neutralizes formula injection by prefixing a quote", () => { expect(CSV.serialize([["=1+1"]])).toBe("'=1+1"); expect(CSV.serialize([["+1"]])).toBe("'+1"); expect(CSV.serialize([["@SUM(A1)"]])).toBe("'@SUM(A1)"); expect(CSV.serialize([["=HYPERLINK(1,2)"]])).toBe('"\'=HYPERLINK(1,2)"'); }); - it("does not treat negative numbers as formulas", () => { + test("does not treat negative numbers as formulas", () => { expect(CSV.serialize([["-5"]])).toBe("-5"); expect(CSV.serialize([["-5.5"]])).toBe("-5.5"); }); - it("guards a leading minus that is not numeric", () => { + test("guards a leading minus that is not numeric", () => { expect(CSV.serialize([["-cmd"]])).toBe("'-cmd"); }); }); diff --git a/app/modules/persisted-state/persisted-state.test.ts b/app/modules/persisted-state/persisted-state.test.ts index fb0f4e3d8..b2fbb765c 100644 --- a/app/modules/persisted-state/persisted-state.test.ts +++ b/app/modules/persisted-state/persisted-state.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; import * as PersistedState from "./persisted-state"; import { @@ -35,21 +35,21 @@ const counts = PersistedState.defineMap({ }); describe("define", () => { - it("round-trips values", () => { + test("round-trips values", () => { assertRoundTrips(numberList, [[], [1, 2, 3]]); assertRoundTrips(searchType, ["weapons", "users"]); assertRoundTrips(dismissed, [true, false]); assertRoundTrips(counts, [0, 42]); }); - it("malformed values decode to defaults", () => { + test("malformed values decode to defaults", () => { assertDecodesToDefault(numberList, ["not json", '{"a":1}', '["a","b"]']); assertDecodesToDefault(searchType, ["teams", "[1,2]"]); assertDecodesToDefault(dismissed, ["yes"]); assertDecodesToDefault(counts, ["abc", "", "Infinity"]); }); - it("decodes legacy plain-string values where the schema allows them", () => { + test("decodes legacy plain-string values where the schema allows them", () => { expect(searchType.decode("users")).toBe("users"); expect(dismissed.decode("true")).toBe(true); expect(counts.decode("7")).toBe(7); @@ -57,17 +57,17 @@ describe("define", () => { }); describe("prependToRecentList", () => { - it("prepends a new item", () => { + test("prepends a new item", () => { expect(PersistedState.prependToRecentList([1, 2], 3, 5)).toEqual([3, 1, 2]); }); - it("moves an existing item to the front", () => { + test("moves an existing item to the front", () => { expect(PersistedState.prependToRecentList([1, 2, 3], 2, 5)).toEqual([ 2, 1, 3, ]); }); - it("caps the list length", () => { + test("caps the list length", () => { expect(PersistedState.prependToRecentList([1, 2, 3], 4, 3)).toEqual([ 4, 1, 2, ]); diff --git a/app/modules/search-params/search-params.test.ts b/app/modules/search-params/search-params.test.ts index a28e467a3..d1219c925 100644 --- a/app/modules/search-params/search-params.test.ts +++ b/app/modules/search-params/search-params.test.ts @@ -1,5 +1,5 @@ import type { ShouldRevalidateFunction } from "react-router"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; import * as SearchParams from "./search-params"; import { SP } from "./search-params"; @@ -36,7 +36,7 @@ const testDefinition = SearchParams.define({ }); describe("SearchParams round trips", () => { - it("round-trips representative and edge-case values", () => { + test("round-trips representative and edge-case values", () => { assertRoundTrips(testDefinition, { limit: [24, 1, 100, 55], name: ["", "hello", "with space", "ä&=?#ö", "lz~sneaky", "lz~~x"], @@ -54,7 +54,7 @@ describe("SearchParams round trips", () => { }); describe("SearchParams.define", () => { - it("decodes garbage to the default", () => { + test("decodes garbage to the default", () => { assertDecodesToDefault(testDefinition, "limit", [ [""], ["abc"], @@ -80,7 +80,7 @@ describe("SearchParams.define", () => { ]); }); - it("parses a Request, URL and URLSearchParams", () => { + test("parses a Request, URL and URLSearchParams", () => { const url = "http://localhost/builds?limit=50&mode=SZ"; const expected = { limit: 50, mode: "SZ" }; @@ -91,7 +91,7 @@ describe("SearchParams.define", () => { ).toMatchObject(expected); }); - it("resolves every missing param to its default", () => { + test("resolves every missing param to its default", () => { expect(testDefinition.parse(new URLSearchParams())).toEqual({ limit: 24, name: "", @@ -104,13 +104,13 @@ describe("SearchParams.define", () => { }); }); - it("drops invalid array members instead of the whole array", () => { + test("drops invalid array members instead of the whole array", () => { expect( testDefinition.parse(new URLSearchParams("ids=1&ids=x&ids=-2&ids=3")).ids, ).toEqual([1, 3]); }); - it("decodes legacy JSON-encoded arrays", () => { + test("decodes legacy JSON-encoded arrays", () => { const definitionWithModes = SearchParams.define({ modes: SP.param(z.array(z.enum(["SZ", "TC", "RM", "CB"])), { default: ["SZ", "TC", "RM", "CB"], @@ -128,20 +128,20 @@ describe("SearchParams.define", () => { ]); }); - it("decodes legacy comma-joined numeric arrays", () => { + test("decodes legacy comma-joined numeric arrays", () => { expect(testDefinition.parse(new URLSearchParams("ids=1,2,3")).ids).toEqual([ 1, 2, 3, ]); }); - it("returns referentially equal values for the same raw input", () => { + test("returns referentially equal values for the same raw input", () => { const first = testDefinition.parse(new URLSearchParams("ids=1&ids=2")); const second = testDefinition.parse(new URLSearchParams("ids=1&ids=2")); expect(first.ids).toBe(second.ids); }); - it("rejects schemas outside the derivation table at define time", () => { + test("rejects schemas outside the derivation table at define time", () => { expect(() => SP.param(z.object({ a: z.string() }) as any, { default: { a: "" }, @@ -162,7 +162,7 @@ describe("SearchParams.define", () => { ).toThrow(/derive/); }); - it("defaults .nullable() params to null without declaring it", () => { + test("defaults .nullable() params to null without declaring it", () => { const omitted = SP.param(z.number().int().nullable(), { loader: true }); const declared = SP.param(z.number().int().nullable(), { default: null, @@ -176,7 +176,7 @@ describe("SearchParams.define", () => { expect(declared.default).toBeNull(); }); - it("rejects .optional() and non-null defaults for .nullable()", () => { + test("rejects .optional() and non-null defaults for .nullable()", () => { expect(() => SP.param(z.number().optional() as any, { default: 1, loader: true }), ).toThrow(/nullable/); @@ -185,7 +185,7 @@ describe("SearchParams.define", () => { ).toThrow(/null as its default/); }); - it("supports SP.custom codecs with total decode via issues", () => { + test("supports SP.custom codecs with total decode via issues", () => { const isoDate = z.codec(z.string(), z.date(), { decode: (value, payload) => { const date = new Date(value); @@ -217,7 +217,7 @@ describe("SearchParams.define", () => { assertDecodesToDefault(customDefinition, "from", [["garbage"], [""]]); }); - it("rejects resets pointing at unknown params", () => { + test("rejects resets pointing at unknown params", () => { expect(() => SearchParams.define({ a: SP.param(z.number(), { default: 0, loader: true, resets: ["b"] }), @@ -227,7 +227,7 @@ describe("SearchParams.define", () => { }); describe("SearchParams compression", () => { - it("decodes a compressed arrival of any param identically to plain", () => { + test("decodes a compressed arrival of any param identically to plain", () => { const def = testDefinition.shape.filters; const value = { minValue: 7, tags: ["x"] }; const plain = def.encodePlain(value)[0]; @@ -239,7 +239,7 @@ describe("SearchParams compression", () => { ).toEqual(value); }); - it("always emits the compressed form for compress: true params", () => { + test("always emits the compressed form for compress: true params", () => { const encoded = SearchParams.encodeParam(testDefinition.shape.blob, { text: "hello world", }); @@ -248,13 +248,13 @@ describe("SearchParams compression", () => { expect(encoded[0]).toMatch(/^lz~/); }); - it("resolves a corrupt compressed payload to the default", () => { + test("resolves a corrupt compressed payload to the default", () => { expect( SearchParams.decodeParam(testDefinition.shape.blob, ["lz~$$$$"]), ).toEqual({ text: "" }); }); - it("resolves a compression bomb to the default", () => { + test("resolves a compression bomb to the default", () => { const bomb = SearchParams.compressTransportValue( JSON.stringify({ text: "a".repeat(10 * 1024 * 1024) }), ); @@ -264,7 +264,7 @@ describe("SearchParams compression", () => { ); }); - it("compresses on demand only when it shortens the value", () => { + test("compresses on demand only when it shortens the value", () => { const longFilters = { minValue: 1, tags: Array.from({ length: 30 }, (_, i) => `long-tag-number-${i}`), @@ -283,7 +283,7 @@ describe("SearchParams compression", () => { ).toMatchObject({ filters: longFilters, limit: 50 }); }); - it("compares the forms percent-encoded, not as raw strings", () => { + test("compares the forms percent-encoded, not as raw strings", () => { // shorter than its compressed form as a raw string, longer once percent-encoded const filters = { minValue: 1, tags: ["ゲームのタグ"] }; const compactHref = testDefinition.href( @@ -301,14 +301,14 @@ describe("SearchParams compression", () => { }); describe("SearchParams.href", () => { - it("omits values equal to their default", () => { + test("omits values equal to their default", () => { expect( testDefinition.href("/builds", { limit: 24, mode: "SZ", season: null }), ).toBe("/builds?mode=SZ"); expect(testDefinition.href("/builds", { limit: 24 })).toBe("/builds"); }); - it("encodes arrays as repeated keys and empty arrays as one empty value", () => { + test("encodes arrays as repeated keys and empty arrays as one empty value", () => { const definitionWithDefault = SearchParams.define({ modes: SP.param(z.array(z.enum(["SZ", "TC"])), { default: ["SZ", "TC"], @@ -326,7 +326,7 @@ describe("SearchParams.href", () => { }); describe("SearchParams.applyToSearchParams", () => { - it("preserves params outside the definition", () => { + test("preserves params outside the definition", () => { const { next } = SearchParams.applyToSearchParams( testDefinition, new URLSearchParams("unrelated=yes&limit=50"), @@ -338,7 +338,7 @@ describe("SearchParams.applyToSearchParams", () => { expect(next.get("mode")).toBe("SZ"); }); - it("applies declared resets", () => { + test("applies declared resets", () => { const { next } = SearchParams.applyToSearchParams( testDefinition, new URLSearchParams("limit=50&enabled=true"), @@ -349,7 +349,7 @@ describe("SearchParams.applyToSearchParams", () => { expect(next.get("enabled")).toBe("true"); }); - it("does not reset a param written in the same batch", () => { + test("does not reset a param written in the same batch", () => { const { next } = SearchParams.applyToSearchParams( testDefinition, new URLSearchParams(), @@ -359,7 +359,7 @@ describe("SearchParams.applyToSearchParams", () => { expect(next.get("limit")).toBe("50"); }); - it("removes params written back to their default", () => { + test("removes params written back to their default", () => { const { next } = SearchParams.applyToSearchParams( testDefinition, new URLSearchParams("mode=SZ"), @@ -369,7 +369,7 @@ describe("SearchParams.applyToSearchParams", () => { expect(next.has("mode")).toBe(false); }); - it("needs navigation exactly when a loader: true param is written", () => { + test("needs navigation exactly when a loader: true param is written", () => { expect( SearchParams.applyToSearchParams(testDefinition, new URLSearchParams(), { enabled: true, @@ -401,16 +401,16 @@ describe("SearchParams.shouldRevalidate", () => { } as Parameters[0]); } - it("revalidates when a loader: true param's decoded value changes", () => { + test("revalidates when a loader: true param's decoded value changes", () => { expect(run("?limit=50", "?limit=60")).toBe(true); expect(run("", "?mode=SZ")).toBe(true); }); - it("does not revalidate for loader: false params", () => { + test("does not revalidate for loader: false params", () => { expect(run("", "?enabled=true&ids=1")).toBe(false); }); - it("does not revalidate for non-canonical but equal values", () => { + test("does not revalidate for non-canonical but equal values", () => { expect( run( '?filters={"minValue":1,"tags":[]}', @@ -420,7 +420,7 @@ describe("SearchParams.shouldRevalidate", () => { expect(run("?limit=24", "")).toBe(false); }); - it("defers to the default for other pathnames, submissions and unknown params", () => { + test("defers to the default for other pathnames, submissions and unknown params", () => { expect( testDefinition.shouldRevalidate({ currentUrl: new URL("http://localhost/x"), diff --git a/app/modules/twitch/vods.test.ts b/app/modules/twitch/vods.test.ts index fce3e5ecc..e46ee0f76 100644 --- a/app/modules/twitch/vods.test.ts +++ b/app/modules/twitch/vods.test.ts @@ -1,40 +1,40 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { parseTwitchDuration } from "./vods"; describe("parseTwitchDuration()", () => { - it("parses hours, minutes and seconds", () => { + test("parses hours, minutes and seconds", () => { expect(parseTwitchDuration("1h2m3s")).toBe(3723); }); - it("parses hours only", () => { + test("parses hours only", () => { expect(parseTwitchDuration("2h")).toBe(7200); }); - it("parses minutes only", () => { + test("parses minutes only", () => { expect(parseTwitchDuration("45m")).toBe(2700); }); - it("parses seconds only", () => { + test("parses seconds only", () => { expect(parseTwitchDuration("30s")).toBe(30); }); - it("parses hours and minutes without seconds", () => { + test("parses hours and minutes without seconds", () => { expect(parseTwitchDuration("1h30m")).toBe(5400); }); - it("parses hours and seconds without minutes", () => { + test("parses hours and seconds without minutes", () => { expect(parseTwitchDuration("2h15s")).toBe(7215); }); - it("parses minutes and seconds without hours", () => { + test("parses minutes and seconds without hours", () => { expect(parseTwitchDuration("5m10s")).toBe(310); }); - it("returns 0 for empty string", () => { + test("returns 0 for empty string", () => { expect(parseTwitchDuration("")).toBe(0); }); - it("parses large values", () => { + test("parses large values", () => { expect(parseTwitchDuration("99h59m59s")).toBe(359999); }); }); diff --git a/app/routines/closeExpiredContinueVotes.test.ts b/app/routines/closeExpiredContinueVotes.test.ts index 96a416099..18a08f54c 100644 --- a/app/routines/closeExpiredContinueVotes.test.ts +++ b/app/routines/closeExpiredContinueVotes.test.ts @@ -15,8 +15,10 @@ import { db } from "~/db/sql"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; import { CloseExpiredContinueVotesRoutine } from "./closeExpiredContinueVotes"; -let alphaUserIds: number[]; -let bravoUserIds: number[]; +const users = UserFactory.pool(); +/** The two SendouQ groups: the first FULL_GROUP_SIZE users against the rest. */ +const alphaUserIds = () => users.ids().slice(0, FULL_GROUP_SIZE); +const bravoUserIds = () => users.ids().slice(FULL_GROUP_SIZE); const setupMatch = async ({ isMatchmade, @@ -26,7 +28,7 @@ const setupMatch = async ({ confirmedAt: Date; }) => { const match = await SQMatchFactory.create( - { alphaUserIds, bravoUserIds, isMatchmade }, + { alphaUserIds: alphaUserIds(), bravoUserIds: bravoUserIds(), isMatchmade }, { isConcluded: true, confirmedAt }, ); @@ -51,9 +53,7 @@ describe("CloseExpiredContinueVotesRoutine", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T12:00:00Z")); - const users = await UserFactory.createMany(FULL_GROUP_SIZE * 2); - alphaUserIds = users.slice(0, FULL_GROUP_SIZE).map((user) => user.id); - bravoUserIds = users.slice(FULL_GROUP_SIZE).map((user) => user.id); + await users.create(FULL_GROUP_SIZE * 2); }); afterEach(() => { @@ -65,8 +65,8 @@ describe("CloseExpiredContinueVotesRoutine", () => { isMatchmade: true, confirmedAt: sub(new Date(), { hours: 2 }), }); - await castContinueVote(alphaGroupId, alphaUserIds[0]); - await castContinueVote(alphaGroupId, alphaUserIds[1]); + await castContinueVote(alphaGroupId, users.id(1)); + await castContinueVote(alphaGroupId, users.id(2)); await CloseExpiredContinueVotesRoutine.run(); @@ -76,7 +76,7 @@ describe("CloseExpiredContinueVotesRoutine", () => { expect(alphaVotes).toHaveLength(4); expect(alphaVotes.every((v) => v.isContinuing === 0)).toBe(true); expect(new Set(alphaVotes.map((v) => v.userId))).toEqual( - new Set(alphaUserIds), + new Set(alphaUserIds()), ); expect(bravoVotes).toHaveLength(4); @@ -88,7 +88,7 @@ describe("CloseExpiredContinueVotesRoutine", () => { isMatchmade: true, confirmedAt: sub(new Date(), { minutes: 30 }), }); - await castContinueVote(alphaGroupId, alphaUserIds[0]); + await castContinueVote(alphaGroupId, users.id(1)); await CloseExpiredContinueVotesRoutine.run(); @@ -115,7 +115,7 @@ describe("CloseExpiredContinueVotesRoutine", () => { isMatchmade: true, confirmedAt: sub(new Date(), { hours: 2 }), }); - for (const userId of alphaUserIds) { + for (const userId of alphaUserIds()) { await castContinueVote(alphaGroupId, userId); } diff --git a/app/routines/notifyCheckInStart.test.ts b/app/routines/notifyCheckInStart.test.ts index 45ad6165d..31c7d0b3f 100644 --- a/app/routines/notifyCheckInStart.test.ts +++ b/app/routines/notifyCheckInStart.test.ts @@ -6,8 +6,7 @@ import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/ import { dateToDatabaseTimestamp } from "~/utils/dates"; import { NotifyCheckInStartRoutine } from "./notifyCheckInStart"; -let author: { id: number }; -let otherAuthor: { id: number }; +const users = UserFactory.pool(); const { mockNotify } = vi.hoisted(() => ({ mockNotify: vi.fn(), @@ -22,7 +21,7 @@ describe("NotifyCheckInStartRoutine", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-15T12:00:00Z")); clearAllTournamentDataCache(); - [author, otherAuthor] = await UserFactory.createMany(2); + await users.create(2); mockNotify.mockClear(); }); @@ -36,7 +35,7 @@ describe("NotifyCheckInStartRoutine", () => { await TournamentFactory.create({ name: "Tournament 1 Hour Away", - authorId: author.id, + authorId: users.id(1), startTimes: [dateToDatabaseTimestamp(oneHourFromNow)], }); @@ -60,7 +59,7 @@ describe("NotifyCheckInStartRoutine", () => { await TournamentFactory.create({ name: "Tournament Starting Now", - authorId: author.id, + authorId: users.id(1), startTimes: [dateToDatabaseTimestamp(now)], }); @@ -75,7 +74,7 @@ describe("NotifyCheckInStartRoutine", () => { await TournamentFactory.create({ name: "Tournament 30 Minutes Away", - authorId: author.id, + authorId: users.id(1), startTimes: [dateToDatabaseTimestamp(thirtyMinutesFromNow)], }); @@ -100,7 +99,7 @@ describe("NotifyCheckInStartRoutine", () => { await TournamentFactory.create({ name: "Tournament 1.5 Hours Away", - authorId: author.id, + authorId: users.id(1), startTimes: [dateToDatabaseTimestamp(oneAndHalfHoursFromNow)], }); @@ -116,13 +115,13 @@ describe("NotifyCheckInStartRoutine", () => { await TournamentFactory.create({ name: "Tournament A", - authorId: author.id, + authorId: users.id(1), startTimes: [dateToDatabaseTimestamp(thirtyMinutesFromNow)], }); await TournamentFactory.create({ name: "Tournament B", - authorId: otherAuthor.id, + authorId: users.id(2), startTimes: [dateToDatabaseTimestamp(fortyFiveMinutesFromNow)], }); diff --git a/app/routines/resolveStaleSQMatches.test.ts b/app/routines/resolveStaleSQMatches.test.ts index d2e06bf8c..5194e8e8f 100644 --- a/app/routines/resolveStaleSQMatches.test.ts +++ b/app/routines/resolveStaleSQMatches.test.ts @@ -24,11 +24,16 @@ import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server"; import { ResolveStaleSQMatchesRoutine } from "./resolveStaleSQMatches"; -let alphaUserIds: number[]; -let bravoUserIds: number[]; +const users = UserFactory.pool(); +/** The two SendouQ groups: the first FULL_GROUP_SIZE users against the rest. */ +const alphaUserIds = () => users.ids().slice(0, FULL_GROUP_SIZE); +const bravoUserIds = () => users.ids().slice(FULL_GROUP_SIZE); const setupMatch = (options: { isReported?: boolean; createdAt?: Date } = {}) => - SQMatchFactory.create({ alphaUserIds, bravoUserIds }, options); + SQMatchFactory.create( + { alphaUserIds: alphaUserIds(), bravoUserIds: bravoUserIds() }, + options, + ); const fetchMatch = (matchId: number) => db @@ -52,9 +57,7 @@ describe("ResolveStaleSQMatchesRoutine", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T12:00:00Z")); - const users = await UserFactory.createMany(FULL_GROUP_SIZE * 2); - alphaUserIds = users.slice(0, FULL_GROUP_SIZE).map((user) => user.id); - bravoUserIds = users.slice(FULL_GROUP_SIZE).map((user) => user.id); + await users.create(FULL_GROUP_SIZE * 2); }); afterEach(() => { @@ -85,7 +88,7 @@ describe("ResolveStaleSQMatchesRoutine", () => { await SQMatchRepository.reportMapWinner({ matchId: match.id, winnerId: match.alphaGroup.id, - reportedByUserId: alphaUserIds[0], + reportedByUserId: users.id(1), reportedCount, }); } diff --git a/app/utils/arrays.test.ts b/app/utils/arrays.test.ts index d6a06e67a..e1704e25d 100644 --- a/app/utils/arrays.test.ts +++ b/app/utils/arrays.test.ts @@ -1,128 +1,70 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { diff, flatZip, mostPopularArrayElement } from "./arrays"; describe("diff", () => { - it("should return elements in arr2 but not in arr1", () => { - const arr1 = [1, 2, 3]; - const arr2 = [2, 3, 4, 4]; - const result = diff(arr1, arr2); - expect(result).toEqual([4, 4]); + test.each([ + [ + [1, 2, 3], + [2, 3, 4, 4], + [4, 4], + ], + [[1, 2, 3], [], []], + [[], [1, 2, 3], [1, 2, 3]], + [ + [1, 2, 2, 3], + [2, 2, 3, 3, 4], + [3, 4], + ], + [[1, 2, 3], [1, 2, 3], []], + ])("%j vs %j leaves %j", (arr1, arr2, expected) => { + expect(diff(arr1, arr2)).toEqual(expected); }); - it("should return an empty array if arr2 is empty", () => { - const arr1 = [1, 2, 3]; - const arr2: number[] = []; - const result = diff(arr1, arr2); - expect(result).toEqual([]); - }); - - it("should return all elements of arr2 if arr1 is empty", () => { - const arr1: number[] = []; - const arr2 = [1, 2, 3]; - const result = diff(arr1, arr2); - expect(result).toEqual([1, 2, 3]); - }); - - it("should handle arrays with duplicate elements", () => { - const arr1 = [1, 2, 2, 3]; - const arr2 = [2, 2, 3, 3, 4]; - const result = diff(arr1, arr2); - expect(result).toEqual([3, 4]); - }); - - it("should return an empty array if both arrays are the same", () => { - const arr1 = [1, 2, 3]; - const arr2 = [1, 2, 3]; - const result = diff(arr1, arr2); - expect(result).toEqual([]); - }); - - it("should not overflow the stack for very large counts", () => { + test("does not overflow the stack for very large counts", () => { const arr2 = new Array(200_000).fill(1); - const result = diff([], arr2); - expect(result).toHaveLength(200_000); + + expect(diff([], arr2)).toHaveLength(200_000); }); }); describe("mostPopularArrayElement", () => { - it("should return the most frequent element in an array of numbers", () => { - const arr = [1, 2, 2, 3, 3, 3, 4]; - const result = mostPopularArrayElement(arr); - expect(result).toBe(3); - }); - - it("should return the most frequent element in an array of strings", () => { - const arr = ["a", "b", "b", "c", "a", "b"]; - const result = mostPopularArrayElement(arr); - expect(result).toBe("b"); - }); - - it("should return the first most frequent element if there is a tie", () => { - const arr = [1, 2, 2, 1]; - const result = mostPopularArrayElement(arr); - expect(result).toBe(1); - }); - - it("should return null for an empty array", () => { - const arr: number[] = []; - const result = mostPopularArrayElement(arr); - expect(result).toBeNull(); - }); - - it("should return the element itself for a single-element array", () => { - const arr = ["only"]; - const result = mostPopularArrayElement(arr); - expect(result).toBe("only"); - }); + test.each([ + [[1, 2, 2, 3, 3, 3, 4], 3], + [["a", "b", "b", "c", "a", "b"], "b"], + // the first of the tied elements wins + [[1, 2, 2, 1], 1], + [["only"], "only"], + [[], null], + ] as [(string | number)[], string | number | null][])( + "%j is most popularly %j", + (arr, expected) => { + expect(mostPopularArrayElement(arr)).toBe(expected); + }, + ); }); describe("flatZip", () => { - it("should zip arrays of equal length by alternating elements", () => { - const arr1 = [1, 2, 3]; - const arr2 = ["a", "b", "c"]; - const result = flatZip(arr1, arr2); - expect(result).toEqual([1, "a", 2, "b", 3, "c"]); - }); - - it("should zip and append remaining elements when second array is longer", () => { - const arr1 = [1, 2]; - const arr2 = ["a", "b", "c"]; - const result = flatZip(arr1, arr2); - expect(result).toEqual([1, "a", 2, "b", "c"]); - }); - - it("should zip and append remaining elements when first array is longer", () => { - const arr1 = [1, 2, 3, 4]; - const arr2 = ["a", "b"]; - const result = flatZip(arr1, arr2); - expect(result).toEqual([1, "a", 2, "b", 3, 4]); - }); - - it("should handle empty first array", () => { - const arr1: number[] = []; - const arr2 = ["a", "b"]; - const result = flatZip(arr1, arr2); - expect(result).toEqual(["a", "b"]); - }); - - it("should handle empty second array", () => { - const arr1 = [1, 2]; - const arr2: string[] = []; - const result = flatZip(arr1, arr2); - expect(result).toEqual([1, 2]); - }); - - it("should handle both empty arrays", () => { - const arr1: number[] = []; - const arr2: string[] = []; - const result = flatZip(arr1, arr2); - expect(result).toEqual([]); - }); - - it("should handle single element arrays", () => { - const arr1 = [1]; - const arr2 = ["a"]; - const result = flatZip(arr1, arr2); - expect(result).toEqual([1, "a"]); + test.each([ + [ + [1, 2, 3], + ["a", "b", "c"], + [1, "a", 2, "b", 3, "c"], + ], + [ + [1, 2], + ["a", "b", "c"], + [1, "a", 2, "b", "c"], + ], + [ + [1, 2, 3, 4], + ["a", "b"], + [1, "a", 2, "b", 3, 4], + ], + [[], ["a", "b"], ["a", "b"]], + [[1, 2], [], [1, 2]], + [[], [], []], + [[1], ["a"], [1, "a"]], + ])("zips %j and %j into %j", (arr1, arr2, expected) => { + expect(flatZip(arr1, arr2)).toEqual(expected); }); }); diff --git a/app/utils/compression.test.ts b/app/utils/compression.test.ts index a9ae49758..892c4b02a 100644 --- a/app/utils/compression.test.ts +++ b/app/utils/compression.test.ts @@ -1,20 +1,20 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { compressToBase64, decompressFromBase64 } from "./compression"; describe("compressToBase64 & decompressFromBase64", () => { - it("round-trips a string", () => { + test("round-trips a string", () => { const value = JSON.stringify({ name: "trophy", mesh: [1, 2, 3] }); expect(decompressFromBase64(compressToBase64(value))).toBe(value); }); - it("round-trips unicode content", () => { + test("round-trips unicode content", () => { const value = "tröphy \u{1f3c6} テスト"; expect(decompressFromBase64(compressToBase64(value))).toBe(value); }); - it("round-trips with the url safe alphabet", () => { + test("round-trips with the url safe alphabet", () => { const value = "a".repeat(1000) + JSON.stringify({ b: [4, 5, 6] }); const compressed = compressToBase64(value, { urlSafe: true }); @@ -22,19 +22,19 @@ describe("compressToBase64 & decompressFromBase64", () => { expect(decompressFromBase64(compressed)).toBe(value); }); - it("returns null for corrupt input", () => { + test("returns null for corrupt input", () => { expect(decompressFromBase64("!!!not base64!!!")).toBeNull(); expect(decompressFromBase64(btoa("not deflate data"))).toBeNull(); expect(decompressFromBase64("")).toBeNull(); }); - it("returns null for truncated input", () => { + test("returns null for truncated input", () => { const compressed = compressToBase64("some longer input to compress"); expect(decompressFromBase64(compressed.slice(0, 4))).toBeNull(); }); - it("returns null for a value inflating past maxDecompressedBytes", () => { + test("returns null for a value inflating past maxDecompressedBytes", () => { const bomb = compressToBase64("a".repeat(10 * 1024 * 1024)); expect(bomb.length).toBeLessThan(64 * 1024); @@ -43,7 +43,7 @@ describe("compressToBase64 & decompressFromBase64", () => { ).toBeNull(); }); - it("allows a value exactly at maxDecompressedBytes", () => { + test("allows a value exactly at maxDecompressedBytes", () => { const value = "a".repeat(1024); expect( diff --git a/app/utils/dates.test.ts b/app/utils/dates.test.ts index 477497a91..b9b46b1b3 100644 --- a/app/utils/dates.test.ts +++ b/app/utils/dates.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { getDateAtNextFullHour, weekNumberToDate, @@ -6,19 +6,19 @@ import { } from "./dates"; describe("getDateAtNextFullHour", () => { - it("returns a date sitting exactly on a full hour (no leftover minutes/seconds/milliseconds)", () => { + test("returns a date sitting exactly on a full hour (no leftover minutes/seconds/milliseconds)", () => { const result = getDateAtNextFullHour(new Date(2024, 0, 1, 14, 30, 0, 500)); expect(result).toEqual(new Date(2024, 0, 1, 15, 0, 0, 0)); }); - it("advances to the next hour when only seconds/milliseconds are past the hour", () => { + test("advances to the next hour when only seconds/milliseconds are past the hour", () => { const result = getDateAtNextFullHour(new Date(2024, 0, 1, 14, 0, 30, 0)); expect(result).toEqual(new Date(2024, 0, 1, 15, 0, 0, 0)); }); - it("keeps the same hour when already exactly on a full hour", () => { + test("keeps the same hour when already exactly on a full hour", () => { const result = getDateAtNextFullHour(new Date(2024, 0, 1, 14, 0, 0, 0)); expect(result).toEqual(new Date(2024, 0, 1, 14, 0, 0, 0)); @@ -36,14 +36,14 @@ describe("weekNumberToDate", () => { process.env.TZ = originalTimezone; }); - it("returns the Monday of the ISO week regardless of server timezone", () => { + test("returns the Monday of the ISO week regardless of server timezone", () => { // ISO week 1 of 2024 starts on Monday 2024-01-01 const start = weekNumberToDate({ week: 1, year: 2024 }); expect(start.toISOString().slice(0, 10)).toBe("2024-01-01"); }); - it("returns the Sunday of the ISO week regardless of server timezone", () => { + test("returns the Sunday of the ISO week regardless of server timezone", () => { // ISO week 1 of 2024 ends on Sunday 2024-01-07 const end = weekNumberToDate({ week: 1, year: 2024, position: "end" }); @@ -64,7 +64,7 @@ describe("weekNumberToDateRange", () => { const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; - it("spans exactly seven days even when the week contains a DST transition", () => { + test("spans exactly seven days even when the week contains a DST transition", () => { // US spring-forward 2025 happened on Sunday 2025-03-09, which falls inside // ISO week 10 of 2025 (Mon 2025-03-03 .. Mon 2025-03-10). const { startTime, endTime } = weekNumberToDateRange({ @@ -75,7 +75,7 @@ describe("weekNumberToDateRange", () => { expect(endTime.getTime() - startTime.getTime()).toBe(SEVEN_DAYS_MS); }); - it("spans exactly seven days for an ordinary week", () => { + test("spans exactly seven days for an ordinary week", () => { const { startTime, endTime } = weekNumberToDateRange({ week: 20, year: 2025, diff --git a/app/utils/i18n.test.ts b/app/utils/i18n.test.ts index 030e8ec4b..c2c983d51 100644 --- a/app/utils/i18n.test.ts +++ b/app/utils/i18n.test.ts @@ -1,8 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { countryCodeToTranslatedName } from "./i18n"; describe("countryCodeToTranslatedName()", () => { - it("returns the translated country name for a valid code", () => { + test("returns the translated country name for a valid code", () => { const result = countryCodeToTranslatedName({ countryCode: "FI", language: "fi", @@ -10,7 +10,7 @@ describe("countryCodeToTranslatedName()", () => { expect(result).toBe("Suomi"); }); - it("returns the country name in english if the code contains a dash", () => { + test("returns the country name in english if the code contains a dash", () => { const result = countryCodeToTranslatedName({ countryCode: "GB-WLS", language: "fi", @@ -18,7 +18,7 @@ describe("countryCodeToTranslatedName()", () => { expect(result).toBe("Wales"); }); - it("returns the country code as is for unknown country", () => { + test("returns the country code as is for unknown country", () => { const result = countryCodeToTranslatedName({ countryCode: "UNKNOWN", language: "en", @@ -27,7 +27,7 @@ describe("countryCodeToTranslatedName()", () => { expect(result).toBe("UNKNOWN"); }); - it("defaults to english for unknown language", () => { + test("defaults to english for unknown language", () => { const result = countryCodeToTranslatedName({ countryCode: "FI", language: "unknown", diff --git a/app/utils/numbers.test.ts b/app/utils/numbers.test.ts index c6b904d69..a6da62b6e 100644 --- a/app/utils/numbers.test.ts +++ b/app/utils/numbers.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, test } from "vitest"; +import { describe, expect, test } from "vitest"; import { averageArray, cutToNDecimalPlaces, @@ -7,133 +7,71 @@ import { } from "./number"; describe("roundToNDecimalPlaces()", () => { - it("rounds to 2 decimal places by default", () => { - expect(roundToNDecimalPlaces(1.234)).toBe(1.23); - expect(roundToNDecimalPlaces(1.235)).toBe(1.24); - expect(roundToNDecimalPlaces(1.2)).toBe(1.2); - expect(roundToNDecimalPlaces(1)).toBe(1); + test.each([ + [1.234, 1.23], + [1.235, 1.24], + [1.2, 1.2], + [1, 1], + ])("rounds %d to %d with the default 2 decimal places", (input, expected) => { + expect(roundToNDecimalPlaces(input)).toBe(expected); }); - it("rounds to 0 decimal places", () => { - expect(roundToNDecimalPlaces(1.6, 0)).toBe(2); - expect(roundToNDecimalPlaces(1.4, 0)).toBe(1); - expect(roundToNDecimalPlaces(2.5, 0)).toBe(3); - }); - - it("rounds to 3 decimal places", () => { - expect(roundToNDecimalPlaces(1.23456, 3)).toBe(1.235); - expect(roundToNDecimalPlaces(1.23444, 3)).toBe(1.234); - }); - - it("handles negative numbers", () => { - expect(roundToNDecimalPlaces(-1.2345, 2)).toBe(-1.23); - expect(roundToNDecimalPlaces(-1.2355, 2)).toBe(-1.24); - }); - - it("handles zero", () => { - expect(roundToNDecimalPlaces(0, 2)).toBe(0); - expect(roundToNDecimalPlaces(0, 0)).toBe(0); - }); - - it("handles large numbers", () => { - expect(roundToNDecimalPlaces(123456.789, 1)).toBe(123456.8); - expect(roundToNDecimalPlaces(123456.789, 0)).toBe(123457); + test.each([ + [1.6, 0, 2], + [1.4, 0, 1], + [2.5, 0, 3], + [1.23456, 3, 1.235], + [1.23444, 3, 1.234], + [-1.2345, 2, -1.23], + [-1.2355, 2, -1.24], + [0, 2, 0], + [0, 0, 0], + [123456.789, 1, 123456.8], + [123456.789, 0, 123457], + ])("rounds %d to %d decimal places as %d", (input, decimals, expected) => { + expect(roundToNDecimalPlaces(input, decimals)).toBe(expected); }); }); describe("cutToNDecimalPlaces()", () => { - test("cutOff truncates decimal places correctly", () => { - const result = cutToNDecimalPlaces(3.9999, 2); - expect(result).toBe(3.99); - }); - - test("cutOff can change amount of decimals returned", () => { - const result = cutToNDecimalPlaces(3.12, 1); - expect(result).toBe(3.1); - }); - - test("cutOff preserves decimal values with the desired number of decimal places correctly", () => { - const result = cutToNDecimalPlaces(100, 2); - expect(result).toBe(100); - }); - - test("cutOff cuts off decimal places and removes trailing zeros correctly", () => { - const result = cutToNDecimalPlaces(3.0001, 2); - expect(result).toBe(3); - }); - - test("cutOff preserves a value already at the desired number of decimal places", () => { - const result = cutToNDecimalPlaces(0.29, 2); - expect(result).toBe(0.29); - }); - - test("cutOff is not thrown off by floating point representation error", () => { - expect(cutToNDecimalPlaces(2.32, 2)).toBe(2.32); - expect(cutToNDecimalPlaces(-0.29, 2)).toBe(-0.29); + test.each([ + [3.9999, 2, 3.99], + [3.12, 1, 3.1], + [100, 2, 100], + [3.0001, 2, 3], + [0.29, 2, 0.29], + // values whose binary representation is just below the decimal shown + [2.32, 2, 2.32], + [-0.29, 2, -0.29], + ])("cuts %d to %d decimal places as %d", (input, decimals, expected) => { + expect(cutToNDecimalPlaces(input, decimals)).toBe(expected); }); }); describe("averageArray()", () => { - it("returns the average of positive numbers", () => { - const result = averageArray([2, 4, 6, 8]); - expect(result).toBe(5); - }); - - it("returns the average of negative numbers", () => { - const result = averageArray([-2, -4, -6, -8]); - expect(result).toBe(-5); - }); - - it("returns the average of mixed positive and negative numbers", () => { - const result = averageArray([10, -10, 20, -20]); - expect(result).toBe(0); - }); - - it("returns the value itself for a single-element array", () => { - const result = averageArray([42]); - expect(result).toBe(42); - }); - - it("returns 0 for an empty array", () => { - const result = averageArray([]); - expect(result).toBe(0); + test.each([ + [[2, 4, 6, 8], 5], + [[-2, -4, -6, -8], -5], + [[10, -10, 20, -20], 0], + [[42], 42], + [[], 0], + ])("averages %j to %d", (input, expected) => { + expect(averageArray(input)).toBe(expected); }); }); describe("safeNumberParse()", () => { - it("returns null for null input", () => { - expect(safeNumberParse(null)).toBeNull(); - }); - - it("parses valid integer string", () => { - expect(safeNumberParse("42")).toBe(42); - }); - - it("parses valid float string", () => { - expect(safeNumberParse("3.14")).toBe(3.14); - }); - - it("returns null for non-numeric string", () => { - expect(safeNumberParse("abc")).toBeNull(); - }); - - it("returns null for empty string", () => { - expect(safeNumberParse("")).toBeNull(); - }); - - it("parses string with leading/trailing spaces", () => { - expect(safeNumberParse(" 7 ")).toBe(7); - }); - - it("parses negative numbers", () => { - expect(safeNumberParse("-123")).toBe(-123); - }); - - it("parses zero", () => { - expect(safeNumberParse("0")).toBe(0); - }); - - it("returns null for string with only spaces", () => { - expect(safeNumberParse(" ")).toBeNull(); + test.each([ + ["42", 42], + ["3.14", 3.14], + [" 7 ", 7], + ["-123", -123], + ["0", 0], + ["abc", null], + ["", null], + [" ", null], + [null, null], + ])("parses %j as %j", (input, expected) => { + expect(safeNumberParse(input)).toBe(expected); }); }); diff --git a/app/utils/random.test.ts b/app/utils/random.test.ts index 211db11c0..9ac3c53b4 100644 --- a/app/utils/random.test.ts +++ b/app/utils/random.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { seededRandom } from "./random"; describe("seededRandom", () => { describe("random", () => { - it("produces same values for same seed", () => { + test("produces same values for same seed", () => { const rng1 = seededRandom("test-seed"); const rng2 = seededRandom("test-seed"); @@ -12,14 +12,14 @@ describe("seededRandom", () => { expect(rng1.random()).toBe(rng2.random()); }); - it("produces different values for different seeds", () => { + test("produces different values for different seeds", () => { const rng1 = seededRandom("seed-1"); const rng2 = seededRandom("seed-2"); expect(rng1.random()).not.toBe(rng2.random()); }); - it("returns values between 0 and 1 by default", () => { + test("returns values between 0 and 1 by default", () => { const rng = seededRandom("test"); for (let i = 0; i < 100; i++) { const value = rng.random(); @@ -28,7 +28,7 @@ describe("seededRandom", () => { } }); - it("returns values between lo and hi when both provided", () => { + test("returns values between lo and hi when both provided", () => { const rng = seededRandom("test"); for (let i = 0; i < 100; i++) { const value = rng.random(5, 10); @@ -37,7 +37,7 @@ describe("seededRandom", () => { } }); - it("returns values between 0 and hi when only hi provided", () => { + test("returns values between 0 and hi when only hi provided", () => { const rng = seededRandom("test"); for (let i = 0; i < 100; i++) { const value = rng.random(5); @@ -48,7 +48,7 @@ describe("seededRandom", () => { }); describe("randomInteger", () => { - it("produces same values for same seed", () => { + test("produces same values for same seed", () => { const rng1 = seededRandom("test-seed"); const rng2 = seededRandom("test-seed"); @@ -57,14 +57,14 @@ describe("seededRandom", () => { expect(rng1.randomInteger(10)).toBe(rng2.randomInteger(10)); }); - it("produces different values for different seeds", () => { + test("produces different values for different seeds", () => { const rng1 = seededRandom("seed-1"); const rng2 = seededRandom("seed-2"); expect(rng1.randomInteger(100)).not.toBe(rng2.randomInteger(100)); }); - it("returns integers between 0 and hi when only hi provided", () => { + test("returns integers between 0 and hi when only hi provided", () => { const rng = seededRandom("test"); for (let i = 0; i < 100; i++) { const value = rng.randomInteger(10); @@ -74,7 +74,7 @@ describe("seededRandom", () => { } }); - it("returns integers between lo and hi when both provided", () => { + test("returns integers between lo and hi when both provided", () => { const rng = seededRandom("test"); for (let i = 0; i < 100; i++) { const value = rng.randomInteger(5, 10); @@ -86,7 +86,7 @@ describe("seededRandom", () => { }); describe("seededShuffle", () => { - it("produces same shuffle for same seed", () => { + test("produces same shuffle for same seed", () => { const array = [1, 2, 3, 4, 5]; const rng1 = seededRandom("test-seed"); const rng2 = seededRandom("test-seed"); @@ -94,7 +94,7 @@ describe("seededRandom", () => { expect(rng1.seededShuffle(array)).toEqual(rng2.seededShuffle(array)); }); - it("produces different shuffles for different seeds", () => { + test("produces different shuffles for different seeds", () => { const array = [1, 2, 3, 4, 5]; const rng1 = seededRandom("seed-1"); const rng2 = seededRandom("seed-2"); @@ -102,7 +102,7 @@ describe("seededRandom", () => { expect(rng1.seededShuffle(array)).not.toEqual(rng2.seededShuffle(array)); }); - it("does not mutate original array", () => { + test("does not mutate original array", () => { const array = [1, 2, 3, 4, 5]; const original = [...array]; const rng = seededRandom("test"); @@ -112,7 +112,7 @@ describe("seededRandom", () => { expect(array).toEqual(original); }); - it("returns array with same elements", () => { + test("returns array with same elements", () => { const array = [1, 2, 3, 4, 5]; const rng = seededRandom("test"); @@ -121,7 +121,7 @@ describe("seededRandom", () => { expect(shuffled.sort()).toEqual(array.sort()); }); - it("handles empty array", () => { + test("handles empty array", () => { const array: number[] = []; const rng = seededRandom("test"); @@ -130,7 +130,7 @@ describe("seededRandom", () => { expect(shuffled).toEqual([]); }); - it("handles single element array", () => { + test("handles single element array", () => { const array = [1]; const rng = seededRandom("test"); diff --git a/app/utils/remix.server.test.ts b/app/utils/remix.server.test.ts index e292d5e3b..9eb1ee5fa 100644 --- a/app/utils/remix.server.test.ts +++ b/app/utils/remix.server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { paginate } from "./remix.server"; const buildUrl = (url: string) => new URL(url); @@ -14,7 +14,7 @@ const captureRedirect = (fn: () => void) => { }; describe("paginate()", () => { - it("returns the page count rounded up", () => { + test("returns the page count rounded up", () => { const result = paginate({ url: buildUrl("https://sendou.ink/vods?page=1"), page: 1, @@ -25,7 +25,7 @@ describe("paginate()", () => { expect(result).toEqual({ currentPage: 1, pagesCount: 5 }); }); - it("does not redirect when page is within bounds", () => { + test("does not redirect when page is within bounds", () => { const response = captureRedirect(() => paginate({ url: buildUrl("https://sendou.ink/vods?page=2"), @@ -38,7 +38,7 @@ describe("paginate()", () => { expect(response).toBeNull(); }); - it("does not redirect when page equals pagesCount", () => { + test("does not redirect when page equals pagesCount", () => { const response = captureRedirect(() => paginate({ url: buildUrl("https://sendou.ink/vods?page=5"), @@ -51,7 +51,7 @@ describe("paginate()", () => { expect(response).toBeNull(); }); - it("redirects to last page when page exceeds pagesCount", () => { + test("redirects to last page when page exceeds pagesCount", () => { const response = captureRedirect(() => paginate({ url: buildUrl("https://sendou.ink/vods?page=99"), @@ -65,7 +65,7 @@ describe("paginate()", () => { expect(response?.headers.get("Location")).toBe("/vods?page=5"); }); - it("preserves other search params when redirecting", () => { + test("preserves other search params when redirecting", () => { const response = captureRedirect(() => paginate({ url: buildUrl( @@ -88,7 +88,7 @@ describe("paginate()", () => { // biome-ignore-end lint/plugin: asserting on the raw redirect URL is the point of the test }); - it("stays on page 1 when there are no results", () => { + test("stays on page 1 when there are no results", () => { const result = paginate({ url: buildUrl("https://sendou.ink/vods?page=1"), page: 1, @@ -99,7 +99,7 @@ describe("paginate()", () => { expect(result).toEqual({ currentPage: 1, pagesCount: 1 }); }); - it("redirects to page 1 when there are no results and page exceeds 1", () => { + test("redirects to page 1 when there are no results and page exceeds 1", () => { const response = captureRedirect(() => paginate({ url: buildUrl("https://sendou.ink/vods?page=4"), diff --git a/app/utils/remix.test.ts b/app/utils/remix.test.ts index 0292caba3..d39a0ccdd 100644 --- a/app/utils/remix.test.ts +++ b/app/utils/remix.test.ts @@ -1,5 +1,5 @@ import type { Location } from "react-router"; -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { metaTags } from "./remix"; import { COMMON_PREVIEW_IMAGE } from "./urls"; @@ -9,13 +9,13 @@ const contentOf = (tags: ReturnType, property: string) => tags.find((tag) => "property" in tag && tag.property === property)?.content; describe("metaTags()", () => { - it("uses the common preview image when no image given", () => { + test("uses the common preview image when no image given", () => { const tags = metaTags({ title: "sendou.ink", location }); expect(contentOf(tags, "og:image")).toBe(COMMON_PREVIEW_IMAGE); }); - it("uses the given image url", () => { + test("uses the given image url", () => { const tags = metaTags({ title: "sendou.ink", location, @@ -27,13 +27,13 @@ describe("metaTags()", () => { ); }); - it("resolves og:url from the location pathname", () => { + test("resolves og:url from the location pathname", () => { const tags = metaTags({ title: "sendou.ink", location }); expect(contentOf(tags, "og:url")).toBe("https://sendou.ink/to/1/brackets"); }); - it("prefers the url override over the location pathname", () => { + test("prefers the url override over the location pathname", () => { const tags = metaTags({ title: "sendou.ink", location, url: "/to/1" }); expect(contentOf(tags, "og:url")).toBe("https://sendou.ink/to/1"); diff --git a/app/utils/urls.test.ts b/app/utils/urls.test.ts index e478711a6..82e6b8855 100644 --- a/app/utils/urls.test.ts +++ b/app/utils/urls.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { leaderboardsPage, tournamentOrganizationPage, @@ -6,7 +6,7 @@ import { } from "./urls"; describe("leaderboardsPage()", () => { - it("encodes season 0 in the query string", () => { + test("encodes season 0 in the query string", () => { const url = leaderboardsPage({ season: 0, type: "USER" }); const params = new URLSearchParams(url.split("?")[1]); @@ -15,7 +15,7 @@ describe("leaderboardsPage()", () => { }); describe("userArtPage()", () => { - it("joins source and bigArtId params with a single query string", () => { + test("joins source and bigArtId params with a single query string", () => { const url = userArtPage({ discordId: "123" }, "MADE-BY", 456); const params = new URLSearchParams(url.split("?")[1]); @@ -25,7 +25,7 @@ describe("userArtPage()", () => { }); describe("tournamentOrganizationPage()", () => { - it("round-trips the tournament name through the source param", () => { + test("round-trips the tournament name through the source param", () => { const tournamentName = "100% Series"; const url = tournamentOrganizationPage({ diff --git a/app/utils/zod.test.ts b/app/utils/zod.test.ts index 18bfe9c0e..81d665d64 100644 --- a/app/utils/zod.test.ts +++ b/app/utils/zod.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, test } from "vitest"; import { actuallyNonEmptyStringOrNull, hasZalgo, @@ -8,45 +8,45 @@ import { } from "./zod"; describe("normalizeFriendCode", () => { - it("returns well formatted friend code as is", () => { + test("returns well formatted friend code as is", () => { expect(normalizeFriendCode("1234-5678-9012")).toBe("1234-5678-9012"); }); - it("handles no dashes", () => { + test("handles no dashes", () => { expect(normalizeFriendCode("123456789012")).toBe("1234-5678-9012"); }); - it("handles SW-suffix", () => { + test("handles SW-suffix", () => { expect(normalizeFriendCode("SW-1234-5678-9012")).toBe("1234-5678-9012"); }); - it("handles a mix", () => { + test("handles a mix", () => { expect(normalizeFriendCode("SW-1234-56789012")).toBe("1234-5678-9012"); }); }); describe("hasZalgo", () => { - it("returns true for text containing Zalgo characters", () => { + test("returns true for text containing Zalgo characters", () => { expect(hasZalgo("z͎͗ͣḁ̵̑l̉̃ͦg̐̓̒o͓̔ͥ")).toBe(true); }); - it("returns false for text without Zalgo characters", () => { + test("returns false for text without Zalgo characters", () => { expect(hasZalgo("normal text")).toBe(false); }); - it("returns false for an empty string", () => { + test("returns false for an empty string", () => { expect(hasZalgo("")).toBe(false); }); - it("returns false for text with special but non-Zalgo characters", () => { + test("returns false for text with special but non-Zalgo characters", () => { expect(hasZalgo("!@#$%^&*()")).toBe(false); }); - it("accepts japanese characters", () => { + test("accepts japanese characters", () => { expect(hasZalgo("こんにちは")).toBe(false); }); - it("returns a stable result when called repeatedly with the same input", () => { + test("returns a stable result when called repeatedly with the same input", () => { const withCombiningMark = "á"; // "á" as base letter + single combining accent expect(hasZalgo(withCombiningMark)).toBe(true); @@ -55,67 +55,67 @@ describe("hasZalgo", () => { }); describe("actuallyNonEmptyStringOrNull", () => { - it("returns null for an empty string", () => { + test("returns null for an empty string", () => { expect(actuallyNonEmptyStringOrNull("")).toBeNull(); }); - it("returns null for a string with only spaces", () => { + test("returns null for a string with only spaces", () => { expect(actuallyNonEmptyStringOrNull(" ")).toBeNull(); }); - it("returns trimmed string for a string with visible characters and spaces", () => { + test("returns trimmed string for a string with visible characters and spaces", () => { expect(actuallyNonEmptyStringOrNull(" hello world ")).toBe("hello world"); }); - it("removes invisible characters and trims", () => { + test("removes invisible characters and trims", () => { expect(actuallyNonEmptyStringOrNull("​​​​test​​​​")).toBe("test"); }); - it("returns original value if not a string", () => { + test("returns original value if not a string", () => { expect(actuallyNonEmptyStringOrNull(123)).toBe(123); expect(actuallyNonEmptyStringOrNull(null)).toBe(null); expect(actuallyNonEmptyStringOrNull(undefined)).toBe(undefined); expect(actuallyNonEmptyStringOrNull({})).toEqual({}); }); - it("returns null for a string with only zero width spaces", () => { + test("returns null for a string with only zero width spaces", () => { expect(actuallyNonEmptyStringOrNull("​​​​​​​​​​")).toBeNull(); }); - it("returns null for a string with only tag space emoji", () => { + test("returns null for a string with only tag space emoji", () => { expect(actuallyNonEmptyStringOrNull("󠀠󠀠󠀠󠀠󠀠")).toBeNull(); }); - it("returns null for a string with only Hangul Filler", () => { + test("returns null for a string with only Hangul Filler", () => { expect(actuallyNonEmptyStringOrNull("\u3164")).toBeNull(); expect(actuallyNonEmptyStringOrNull("ㅤㅤㅤ")).toBeNull(); }); - it("returns null for other invisible characters", () => { + test("returns null for other invisible characters", () => { expect(actuallyNonEmptyStringOrNull("\u115F")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\u1160")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\uFEFF")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\u2060")).toBeNull(); }); - it("returns null for a string with only soft hyphens", () => { + test("returns null for a string with only soft hyphens", () => { expect(actuallyNonEmptyStringOrNull("\u00AD")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\u00AD\u00AD\u00AD")).toBeNull(); }); - it("returns null for a string with only braille blanks", () => { + test("returns null for a string with only braille blanks", () => { expect(actuallyNonEmptyStringOrNull("\u2800")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\u2800\u2800\u2800\u2800")).toBeNull(); }); - it("returns null for a string with only variation selectors", () => { + test("returns null for a string with only variation selectors", () => { expect(actuallyNonEmptyStringOrNull("\ufe0e")).toBeNull(); expect(actuallyNonEmptyStringOrNull("\ufe0e\ufe0e")).toBeNull(); }); }); describe("hexCodeWithoutAlpha", () => { - it("accepts valid 3 and 6 digit hex colors", () => { + test("accepts valid 3 and 6 digit hex colors", () => { expect(hexCodeWithoutAlpha.safeParse("#fff").success).toBe(true); expect(hexCodeWithoutAlpha.safeParse("#FFF").success).toBe(true); expect(hexCodeWithoutAlpha.safeParse("#abc").success).toBe(true); @@ -123,43 +123,43 @@ describe("hexCodeWithoutAlpha", () => { expect(hexCodeWithoutAlpha.safeParse("#a1b2c3").success).toBe(true); }); - it("rejects strings that are not valid hex colors", () => { + test("rejects strings that are not valid hex colors", () => { expect(hexCodeWithoutAlpha.safeParse("#fff99").success).toBe(false); expect(hexCodeWithoutAlpha.safeParse("#abc12").success).toBe(false); expect(hexCodeWithoutAlpha.safeParse("#12345").success).toBe(false); expect(hexCodeWithoutAlpha.safeParse("#ffffff99").success).toBe(false); }); - it("rejects alpha (4 and 8 digit) hex colors", () => { + test("rejects alpha (4 and 8 digit) hex colors", () => { expect(hexCodeWithoutAlpha.safeParse("#ffff").success).toBe(false); expect(hexCodeWithoutAlpha.safeParse("#ffffffff").success).toBe(false); }); }); describe("timeString", () => { - it("accepts valid time in HH:MM format", () => { + test("accepts valid time in HH:MM format", () => { expect(timeString.safeParse("00:00").success).toBe(true); expect(timeString.safeParse("12:30").success).toBe(true); expect(timeString.safeParse("23:59").success).toBe(true); }); - it("accepts times with leading zeros", () => { + test("accepts times with leading zeros", () => { expect(timeString.safeParse("01:05").success).toBe(true); expect(timeString.safeParse("09:00").success).toBe(true); }); - it("rejects invalid hour values", () => { + test("rejects invalid hour values", () => { expect(timeString.safeParse("24:00").success).toBe(false); expect(timeString.safeParse("25:30").success).toBe(false); expect(timeString.safeParse("99:00").success).toBe(false); }); - it("rejects invalid minute values", () => { + test("rejects invalid minute values", () => { expect(timeString.safeParse("12:60").success).toBe(false); expect(timeString.safeParse("12:99").success).toBe(false); }); - it("rejects malformed time strings", () => { + test("rejects malformed time strings", () => { expect(timeString.safeParse("1:30").success).toBe(false); expect(timeString.safeParse("12:3").success).toBe(false); expect(timeString.safeParse("12-30").success).toBe(false); @@ -167,13 +167,13 @@ describe("timeString", () => { expect(timeString.safeParse("12:30:00").success).toBe(false); }); - it("rejects non-string values", () => { + test("rejects non-string values", () => { expect(timeString.safeParse(1230).success).toBe(false); expect(timeString.safeParse(null).success).toBe(false); expect(timeString.safeParse(undefined).success).toBe(false); }); - it("rejects empty string", () => { + test("rejects empty string", () => { expect(timeString.safeParse("").success).toBe(false); }); }); diff --git a/vitest.scanner.config.ts b/vitest.scanner.config.ts index ea07a42b0..bbc550a8a 100644 --- a/vitest.scanner.config.ts +++ b/vitest.scanner.config.ts @@ -5,11 +5,12 @@ import { configDefaults, defineConfig } from "vitest/config"; // out of the unit project so `--project unit` stays fast, and out of CI // (`test:unit:browser`) because the game icons come from a sibling // sendou-ink/assets checkout the runner does not have — run it locally with -// `pnpm test:scanner`. +// `pnpm test:scanner`. The fixture-free logic tests live in +// app/features/scanner/tests/logic and run in the unit project instead. export default defineConfig({ test: { name: "scanner", - include: ["app/features/scanner/**/*.test.{ts,tsx}"], + include: ["app/features/scanner/tests/*.test.{ts,tsx}"], exclude: [...configDefaults.exclude, "**/*.browser.test.{ts,tsx}"], // OpenCV WASM init takes seconds and the fixture sweeps are heavy testTimeout: 120_000, diff --git a/vitest.unit.config.ts b/vitest.unit.config.ts index a1d594f31..07f3c020b 100644 --- a/vitest.unit.config.ts +++ b/vitest.unit.config.ts @@ -8,8 +8,10 @@ export default defineConfig({ ...configDefaults.exclude, "e2e/**", "**/*.browser.test.{ts,tsx}", - // the scanner golden-file suite has its own heavy project (vitest.scanner.config.ts) - "app/features/scanner/**", + // the scanner golden-file suites have their own heavy project + // (vitest.scanner.config.ts); tests/logic/ stays here because it is + // pure logic over synthetic events and needs no image fixtures + "app/features/scanner/tests/*.test.{ts,tsx}", ], setupFiles: ["./app/test-setup.ts"], },