mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-20 18:15:35 -05:00
Refactor tests for consistency
This commit is contained in:
@@ -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/<feature>/**/tests/fixtures.ts`); don't copy a builder into a second file
|
||||
|
||||
## i18n
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", [
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"] },
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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<Parameters<typeof BuildRepository.insert>[0]> = {},
|
||||
): Parameters<typeof BuildRepository.insert>[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],
|
||||
});
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
|
||||
@@ -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"],
|
||||
]);
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<number>();
|
||||
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],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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<typeof MatchProfileRepository.updateOwnMatchProfile>[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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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")!;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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> = {}): 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 } = {}) {
|
||||
|
||||
@@ -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> = {},
|
||||
): ScannerMatchPlayer {
|
||||
return {
|
||||
name,
|
||||
weaponId,
|
||||
paint: null,
|
||||
ka: null,
|
||||
d: null,
|
||||
s: null,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
function testMatch(partial: Partial<ScannerMatch> = {}): 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([
|
||||
|
||||
@@ -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]!,
|
||||
|
||||
@@ -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
|
||||
|
||||
75
app/features/scanner-ingest/core/tests/fixtures.ts
Normal file
75
app/features/scanner-ingest/core/tests/fixtures.ts
Normal file
@@ -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> = {},
|
||||
): 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> = {},
|
||||
): 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]],
|
||||
};
|
||||
}
|
||||
@@ -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 <dir> <startT> <fps> # 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/<detector>/<case-name>/` with
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
@@ -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];
|
||||
@@ -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"];
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<MapRow> & { 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: [],
|
||||
|
||||
@@ -30,7 +30,7 @@ const defaultNewScrimPostArgs = (): Parameters<typeof newScrimAction>[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<typeof newScrimAction>[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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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", [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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] }],
|
||||
|
||||
@@ -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"]]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<typeof editTeamFormSchema>({
|
||||
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(
|
||||
{
|
||||
|
||||
@@ -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<typeof createTeamSchema>({
|
||||
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<Parameters<typeof TeamFactory.create>[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" });
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
20
app/features/team/tests/fixtures.ts
Normal file
20
app/features/team/tests/fixtures.ts
Normal file
@@ -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<Parameters<typeof TeamFactory.create>[0]> = {},
|
||||
) =>
|
||||
TeamFactory.create({
|
||||
memberUserIds: [ADMIN_ID, REGULAR_USER_TEST_ID],
|
||||
...overrides,
|
||||
});
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Partial<Parameters<typeof resolveTeamFromSide>[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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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] },
|
||||
]);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user