mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-14 07:06:14 -05:00
Fix even even more bugs
This commit is contained in:
48
app/components/RelativeTime.browser.test.tsx
Normal file
48
app/components/RelativeTime.browser.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createMemoryRouter, RouterProvider } from "react-router";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { render } from "vitest-browser-react";
|
||||
import { RelativeTime } from "./RelativeTime";
|
||||
|
||||
const FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
timeZoneName: "short",
|
||||
};
|
||||
|
||||
function renderRelativeTime(timestamp: number) {
|
||||
const router = createMemoryRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
element: <RelativeTime timestamp={timestamp}>3 days ago</RelativeTime>,
|
||||
},
|
||||
],
|
||||
{ initialEntries: ["/"] },
|
||||
);
|
||||
|
||||
return render(<RouterProvider router={router} />);
|
||||
}
|
||||
|
||||
function expectedTitle(timestamp: number) {
|
||||
const language =
|
||||
navigator.languages.find(
|
||||
(lang) => lang.split("-")[0].toLowerCase() === "en",
|
||||
) ?? "en";
|
||||
|
||||
return new Intl.DateTimeFormat(language, FORMAT_OPTIONS).format(timestamp);
|
||||
}
|
||||
|
||||
describe("RelativeTime", () => {
|
||||
test("tooltip shows the date the millisecond timestamp points to", async () => {
|
||||
const timestamp = new Date("2025-08-02T12:00:00Z").getTime();
|
||||
|
||||
const screen = await renderRelativeTime(timestamp);
|
||||
|
||||
const abbr = screen.getByText("3 days ago");
|
||||
await expect.element(abbr).toBeVisible();
|
||||
|
||||
expect(abbr.element().getAttribute("title")).toBe(expectedTitle(timestamp));
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,8 @@ export function RelativeTime({
|
||||
});
|
||||
|
||||
return (
|
||||
<abbr title={formatter.format(timestamp) ?? undefined}>{children}</abbr>
|
||||
<abbr title={formatter.format(new Date(timestamp)) ?? undefined}>
|
||||
{children}
|
||||
</abbr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -972,6 +972,70 @@ describe("single elimination source - underground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("swiss between rounds", () => {
|
||||
const SWISS_MAIN_BRACKET = {
|
||||
type: "swiss" as const,
|
||||
name: "Main Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: { groupCount: 1, roundCount: 5 },
|
||||
sources: [],
|
||||
};
|
||||
|
||||
// swiss with round 1 fully reported but rounds 2-5 not yet paired
|
||||
const betweenRoundsSwissData = () => {
|
||||
let data = Engine.create({
|
||||
type: "swiss",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1, roundCount: 5 },
|
||||
});
|
||||
|
||||
// needed to make it "not preview"
|
||||
data.round = data.round.map((r) => ({
|
||||
...r,
|
||||
maps: { count: 3, type: "BEST_OF" },
|
||||
}));
|
||||
|
||||
for (const match of data.match) {
|
||||
data = reportLowerIdWinner(data, match.id);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
it("tournament is not over while swiss still has unpaired rounds", () => {
|
||||
const tournament = testTournament({
|
||||
ctx: {
|
||||
settings: { bracketProgression: [SWISS_MAIN_BRACKET] },
|
||||
},
|
||||
data: betweenRoundsSwissData(),
|
||||
});
|
||||
|
||||
expect(tournament.everyBracketOver).toBe(false);
|
||||
});
|
||||
|
||||
it("can't finalize between swiss rounds when progression also has an underground bracket", () => {
|
||||
const tournament = testTournament({
|
||||
ctx: {
|
||||
settings: {
|
||||
bracketProgression: [
|
||||
SWISS_MAIN_BRACKET,
|
||||
{
|
||||
type: "single_elimination" as const,
|
||||
name: "Underground Bracket",
|
||||
requiresCheckIn: false,
|
||||
settings: {},
|
||||
sources: [{ bracketIdx: 0, placements: [3, 4] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
data: betweenRoundsSwissData(),
|
||||
});
|
||||
|
||||
expect(tournament.canFinalize({ id: 1 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function reportLowerIdWinner(data: BracketData, matchId: number): BracketData {
|
||||
const match = matchById(data, matchId);
|
||||
const opponent1Lower = match.opponent1!.id! < match.opponent2!.id!;
|
||||
|
||||
@@ -77,8 +77,8 @@ export class SwissBracket extends Bracket {
|
||||
* Exception being rounds that can never be paired because every team of the group has already
|
||||
* advanced or been eliminated (early advance variation).
|
||||
*/
|
||||
get standingsAreFinal() {
|
||||
if (!this.everyMatchOver) return false;
|
||||
get everyMatchOver() {
|
||||
if (!super.everyMatchOver) return false;
|
||||
|
||||
return this.data.group.every((group) => {
|
||||
const groupsMatches = this.data.match.filter(
|
||||
|
||||
@@ -40,8 +40,6 @@ export type BracketDerivedMeta = {
|
||||
createdAt: number | null;
|
||||
preview: boolean;
|
||||
everyMatchOver: boolean;
|
||||
/** False only while a swiss bracket still has rounds whose matches have not been generated. */
|
||||
allRoundsHaveMatches: boolean;
|
||||
participantTournamentTeamIds: number[];
|
||||
teamsPendingCheckIn: number[] | null;
|
||||
seeding: number[] | null;
|
||||
@@ -247,9 +245,6 @@ export class Tournament {
|
||||
createdAt: bracket.createdAt ?? null,
|
||||
preview: bracket.preview,
|
||||
everyMatchOver: bracket.everyMatchOver,
|
||||
allRoundsHaveMatches: bracket.data.round.every((round) =>
|
||||
bracket.data.match.some((match) => match.roundId === round.id),
|
||||
),
|
||||
participantTournamentTeamIds: bracket.participantTournamentTeamIds,
|
||||
teamsPendingCheckIn: bracket.teamsPendingCheckIn ?? null,
|
||||
seeding: bracket.seeding ?? null,
|
||||
@@ -763,20 +758,7 @@ export class Tournament {
|
||||
(b) => !b.preview || !b.isUnderground,
|
||||
);
|
||||
|
||||
const everyRoundHasMatches = () => {
|
||||
// only in swiss matches get generated as tournament progresses
|
||||
if (
|
||||
this.ctx.settings.bracketProgression.length > 1 ||
|
||||
this.ctx.settings.bracketProgression[0].type !== "swiss"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.bracketsMeta[0].allRoundsHaveMatches;
|
||||
};
|
||||
|
||||
return (
|
||||
everyRoundHasMatches() &&
|
||||
relevantBrackets.every((b) => b.everyMatchOver) &&
|
||||
this.isOrganizer(user) &&
|
||||
!this.ctx.isFinalized
|
||||
|
||||
@@ -35,6 +35,38 @@ describe("filterWeapon", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const neoSplash = { type: "MAIN" as const, id: 22 as MainWeaponId };
|
||||
|
||||
test("matches a full alt name", () => {
|
||||
expect(
|
||||
filterWeapon({
|
||||
weapon: neoSplash,
|
||||
weaponName: "Neo Splash-o-matic",
|
||||
searchTerm: "gecko",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("alt names match on the full alt name only, not a partial one", () => {
|
||||
expect(
|
||||
filterWeapon({
|
||||
weapon: neoSplash,
|
||||
weaponName: "Neo Splash-o-matic",
|
||||
searchTerm: "geck",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("alt names match on the full alt name only, also when the weapon has a single alt name", () => {
|
||||
expect(
|
||||
filterWeapon({
|
||||
weapon: { type: "MAIN", id: 10 as MainWeaponId },
|
||||
weaponName: "Splattershot Jr.",
|
||||
searchTerm: "vj",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("does not match unrelated weapon", () => {
|
||||
expect(
|
||||
filterWeapon({
|
||||
|
||||
@@ -32,9 +32,12 @@ export function filterWeapon({
|
||||
}
|
||||
|
||||
if (weapon.type === "MAIN") {
|
||||
return (
|
||||
weaponAltNames.get(weapon.id)?.includes(normalizedSearchTerm) ?? false
|
||||
);
|
||||
const altNames = weaponAltNames.get(weapon.id);
|
||||
if (!altNames) return false;
|
||||
|
||||
const altNamesList = typeof altNames === "string" ? [altNames] : altNames;
|
||||
|
||||
return altNamesList.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -43,6 +43,30 @@ describe("queryToUserIdentifier()", () => {
|
||||
id: 42,
|
||||
});
|
||||
});
|
||||
|
||||
test("gets custom url from url with trailing slash", () => {
|
||||
expect(queryToUserIdentifier("https://sendou.ink/u/sendou/")).toEqual({
|
||||
customUrl: "sendou",
|
||||
});
|
||||
});
|
||||
|
||||
test("gets custom url from profile sub-page url", () => {
|
||||
expect(queryToUserIdentifier("https://sendou.ink/u/sendou/builds")).toEqual(
|
||||
{
|
||||
customUrl: "sendou",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("gets discord id from url with query string", () => {
|
||||
expect(
|
||||
queryToUserIdentifier(
|
||||
"https://sendou.ink/u/79237403620945920?utm_source=discord",
|
||||
),
|
||||
).toEqual({
|
||||
discordId: "79237403620945920",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("userDiscordIdIsAged()", () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { logger } from "./logger";
|
||||
import { isCustomUrl } from "./urls";
|
||||
|
||||
const longUrlRegExp = /(https:\/\/)?sendou.ink\/u\/(.+)/;
|
||||
const shortUrlRegExp = /(https:\/\/)?snd.ink\/(.+)/;
|
||||
const longUrlRegExp = /(https:\/\/)?sendou\.ink\/u\/([^/?#]+)/;
|
||||
const shortUrlRegExp = /(https:\/\/)?snd\.ink\/([^/?#]+)/;
|
||||
const DISCORD_ID_MIN_LENGTH = 17;
|
||||
export function queryToUserIdentifier(
|
||||
query: string,
|
||||
|
||||
Reference in New Issue
Block a user