From 9701169bab9b18be1acd5e148def14e2eb6cb7a1 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:16:51 +0200 Subject: [PATCH 1/3] chore(devops): move lint coverage to oxlint Enable the correctness category and import/no-duplicates while keeping the four type-aware rules that ESLint never enforced off. Move the engine determinism restrictions to oxlint and its official ESLint-rules JS plugin, then fix the resulting lint findings. This includes six unsafe optional-chain cases that oxlint catches but ESLint misses, plus the import-deduplication gap from #6025. ESLint remains as a safety net for the removal follow-up. --- .oxlintrc.json | 131 +++++++++++++++++- .../engine-lib/src/game/TeamAssignment.ts | 6 +- .../engine/src/execution/EmojiExecution.ts | 2 +- scripts/split-packages/imported-names.ts | 2 +- scripts/split-packages/run.ts | 3 +- src/client/ClanApi.ts | 9 +- src/client/FriendsApi.ts | 9 +- src/client/HostLobbyModal.ts | 2 + src/client/InGameModal.ts | 2 + src/client/LangSelector.ts | 2 + src/client/Main.ts | 7 +- src/client/SavedGamesApi.ts | 7 +- src/client/SteamLink.ts | 2 +- src/client/Store.ts | 1 - src/client/UserSettingModal.ts | 4 + src/client/components/LevelBadge.ts | 2 +- src/client/components/NavAccountMenu.ts | 1 - src/client/components/ProfileCard.ts | 1 - .../stats/PlayerGameHistoryView.ts | 2 +- .../components/clan/ClanGameHistoryView.ts | 2 +- src/client/replay/ReplayViewer.ts | 1 - src/server/Client.ts | 2 +- tests/ColumnPicker.test.ts | 4 +- tests/DesktopStatusBar.test.ts | 1 - tests/NationCreation.test.ts | 4 +- tests/NationName.test.ts | 14 +- tests/PseudoRandom.test.ts | 2 +- tests/ReachabilityGating.test.ts | 4 +- tests/client/CosmeticPackPurchase.test.ts | 4 +- tests/client/CosmeticPurchaseDebt.test.ts | 4 +- tests/client/CreatorCodePanel.test.ts | 2 +- tests/client/EffectsGrid.test.ts | 2 +- tests/client/InlineCheckout.test.ts | 4 +- tests/client/InventoryModal.test.ts | 2 +- tests/client/ListLobbyDialog.test.ts | 2 + tests/client/NewsModal.test.ts | 5 +- tests/client/SavedGamesApi.test.ts | 10 +- tests/client/UsernameBareClaim.test.ts | 8 +- tests/client/UsernameClaimPrefill.test.ts | 2 +- tests/client/clan/ClanApiDonate.test.ts | 6 +- tests/client/clan/ClanApiQueries.test.ts | 21 +-- tests/client/clan/ClanModal.rendering.test.ts | 4 +- .../graphics/layers/ChatModalShortcut.test.ts | 1 - .../client/graphics/layers/EmojiTable.test.ts | 1 - .../graphics/layers/PlayerPanelKick.test.ts | 5 +- .../client/graphics/layers/SpawnTimer.test.ts | 3 +- tests/client/replay/ReplayGameAdapter.test.ts | 4 +- tests/client/view/GameView.test.ts | 2 +- tests/matchmaking/e2e.mjs | 4 +- tests/server/AnonymizeNames.test.ts | 4 +- tests/server/HostedLobbyListing.test.ts | 8 +- tests/server/PoolRouting.test.ts | 2 +- tests/util/GameServerHarness.ts | 2 +- 53 files changed, 238 insertions(+), 103 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index d30c21f1cd8..cc5a33093b2 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -6,10 +6,12 @@ }, "ignorePatterns": [ "src/server/gatekeeper/**", - "tests/pathfinding/playground/**" + "tests/pathfinding/playground/**", + ".claude/**" ], + "plugins": ["eslint", "typescript", "oxc", "import"], "categories": { - "correctness": "off", + "correctness": "error", "suspicious": "off", "pedantic": "off", "perf": "off", @@ -22,9 +24,14 @@ }, "rules": { "typescript/no-explicit-any": "off", + "typescript/no-floating-promises": "off", + "typescript/restrict-template-expressions": "off", + "typescript/unbound-method": "off", + "typescript/no-base-to-string": "off", "typescript/prefer-nullish-coalescing": "error", "eqeqeq": "error", "no-case-declarations": "error", + "import/no-duplicates": "error", "no-unused-vars": [ "error", { @@ -36,5 +43,123 @@ } } ] - } + }, + "jsPlugins": [{ "name": "eslint-js", "specifier": "oxlint-plugin-eslint" }], + "overrides": [ + { + "files": [ + "packages/engine/src/**/*.ts", + "packages/engine-api/src/**/*.ts", + "packages/engine-lib/src/**/*.ts" + ], + "excludeFiles": ["packages/engine-lib/src/DetMath.ts"], + "rules": { + "eslint/no-restricted-properties": [ + "error", + { + "object": "Math", + "property": "exp", + "message": "Math.exp differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "expm1", + "message": "Math.expm1 differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "log", + "message": "Math.log differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "log1p", + "message": "Math.log1p differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "log2", + "message": "Math.log2 differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "log10", + "message": "Math.log10 differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "pow", + "message": "Math.pow differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "sin", + "message": "Math.sin differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "cos", + "message": "Math.cos differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "tan", + "message": "Math.tan differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "asin", + "message": "Math.asin differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "acos", + "message": "Math.acos differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "atan", + "message": "Math.atan differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "atan2", + "message": "Math.atan2 differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "sinh", + "message": "Math.sinh differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "cosh", + "message": "Math.cosh differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "tanh", + "message": "Math.tanh differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "cbrt", + "message": "Math.cbrt differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + }, + { + "object": "Math", + "property": "hypot", + "message": "Math.hypot differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + } + ], + "eslint-js/no-restricted-syntax": [ + "error", + { + "selector": "BinaryExpression[operator='**']:not([right.type='Literal'][right.value=2])", + "message": "`**` with a non-2 exponent differs between JS engines; use packages/engine-lib/src/DetMath.ts to keep the simulation deterministic." + } + ] + } + } + ] } diff --git a/packages/engine-lib/src/game/TeamAssignment.ts b/packages/engine-lib/src/game/TeamAssignment.ts index 54577140282..0aa194e9b4a 100644 --- a/packages/engine-lib/src/game/TeamAssignment.ts +++ b/packages/engine-lib/src/game/TeamAssignment.ts @@ -17,8 +17,8 @@ export function assignTeams( teams: Team[], isDuosTriosQuads: boolean, maxTeamSize: number = getMaxTeamSize(players.length, teams.length), -): Map { - const result = new Map(); +): Map { + const result = new Map(); const teamPlayerCount = new Map(); // Matchmade games arrive with a server-pinned team slot (teamIndex). The @@ -165,7 +165,7 @@ export function assignTeamsLobbyPreview( teams: Team[], teamCount: TeamCountConfig, nationCount: number, -): Map { +): Map { const maxTeamSize = getMaxTeamSize( players.length + nationCount, teams.length, diff --git a/packages/engine/src/execution/EmojiExecution.ts b/packages/engine/src/execution/EmojiExecution.ts index d022973d46d..597926a261b 100644 --- a/packages/engine/src/execution/EmojiExecution.ts +++ b/packages/engine/src/execution/EmojiExecution.ts @@ -26,7 +26,7 @@ export class EmojiExecution implements Execution { constructor( private requestor: Player, - private recipientID: PlayerID | typeof AllPlayers, + private recipientID: PlayerID, private emoji: number, ) {} diff --git a/scripts/split-packages/imported-names.ts b/scripts/split-packages/imported-names.ts index 1c3960ea720..21f7b65ced2 100644 --- a/scripts/split-packages/imported-names.ts +++ b/scripts/split-packages/imported-names.ts @@ -41,5 +41,5 @@ for (const f of listSourceFiles()) { } else console.log("other ref", r, ref.kind); } } -for (const [n, fs] of [...names].sort()) +for (const [n, fs] of [...names].sort(([a], [b]) => a.localeCompare(b))) console.log(n.padEnd(28), fs.size, [...fs].slice(0, 3).join(" ")); diff --git a/scripts/split-packages/run.ts b/scripts/split-packages/run.ts index 2d1e8c8d62c..efe0ce9fda5 100644 --- a/scripts/split-packages/run.ts +++ b/scripts/split-packages/run.ts @@ -290,7 +290,8 @@ console.log( `files rewritten: ${rewritten.size}, specifiers rewritten: ` + `${[...rewritten.values()].reduce((a, b) => a + b, 0)}`, ); -for (const [f, n] of [...rewritten].sort()) console.log(` ${n}\t${f}`); +for (const [f, n] of [...rewritten].sort(([a], [b]) => a.localeCompare(b))) + console.log(` ${n}\t${f}`); if (warnings.length > 0) { console.log(`\nwarnings (${warnings.length}):`); for (const w of warnings) console.log(` ${w}`); diff --git a/src/client/ClanApi.ts b/src/client/ClanApi.ts index beea1bbb079..801faff17ba 100644 --- a/src/client/ClanApi.ts +++ b/src/client/ClanApi.ts @@ -59,13 +59,12 @@ async function clanFetch( options?: RequestInit, ): Promise { const url = `${getApiBase()}${path}`; + const headers = new Headers(options?.headers); + if (!headers.has("Accept")) headers.set("Accept", "application/json"); + headers.set("Authorization", await getAuthHeader()); return fetch(url, { ...options, - headers: { - Accept: "application/json", - ...options?.headers, - Authorization: await getAuthHeader(), - }, + headers, }); } diff --git a/src/client/FriendsApi.ts b/src/client/FriendsApi.ts index 4c47b933c37..89808eac18a 100644 --- a/src/client/FriendsApi.ts +++ b/src/client/FriendsApi.ts @@ -13,13 +13,12 @@ async function friendsFetch( path: string, options?: RequestInit, ): Promise { + const headers = new Headers(options?.headers); + if (!headers.has("Accept")) headers.set("Accept", "application/json"); + headers.set("Authorization", await getAuthHeader()); return fetch(`${getApiBase()}${path}`, { ...options, - headers: { - Accept: "application/json", - ...options?.headers, - Authorization: await getAuthHeader(), - }, + headers, }); } diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 0cb1f7f7c52..259899e7085 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -36,6 +36,8 @@ import { CopyButton } from "./components/CopyButton"; import "./components/GameConfigSettings"; import "./components/InputCard"; import "./components/InsufficientCurrencyDialog"; +// The type import below is erased at runtime; keep this to register the dialog. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./components/ListLobbyDialog"; import { ListLobbyOptions } from "./components/ListLobbyDialog"; import "./components/LobbyPlayerView"; diff --git a/src/client/InGameModal.ts b/src/client/InGameModal.ts index d92adbece25..0371da09700 100644 --- a/src/client/InGameModal.ts +++ b/src/client/InGameModal.ts @@ -1,3 +1,5 @@ +// The type-only use below is erased at runtime; keep this to register the dialog. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./components/ConfirmDialog"; import { ConfirmDialog } from "./components/ConfirmDialog"; import { gamePortal } from "./GamePortal"; diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 1bc4d74a832..1f5f14dc9a2 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -2,6 +2,8 @@ import { assetUrl } from "@openfront/shared/AssetUrls"; import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { desktopSteamLocale } from "./DesktopShell"; +// The type-only use below is erased at runtime; keep this to register the modal. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./LanguageModal"; import { LanguageModal } from "./LanguageModal"; import { formatDebugTranslation } from "./Utils"; diff --git a/src/client/Main.ts b/src/client/Main.ts index 49fec5bebba..512e3b5d76e 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -67,7 +67,6 @@ import { } from "./DesktopShell"; import "./FeaturedStream"; import { loadGameClient, prefetchGameClient } from "./GameClientLoader"; -import "./GameModeSelector"; import { GameModeSelector, joinIsGateable, @@ -83,6 +82,8 @@ import type { HomepagePromos } from "./HomepagePromos"; import type { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { showInGameAlert, showInGameConfirm } from "./InGameModal"; import type { JoinLobbyModal } from "./JoinLobbyModal"; +// The LangSelector class is used as a type; retain this for registration. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./LangSelector"; import { LangSelector } from "./LangSelector"; import { initLayout } from "./Layout"; @@ -152,8 +153,10 @@ import { // Before the first userMeResponse, for the modals that load after it. import "./UserMeBroadcast"; import { classicReplayHref } from "./replay/ReplayEntry"; -import "./UsernameInput"; import { parseReplayViewerHash } from "./replay/ReplayViewerRoute"; +// The UsernameInput class is used as a type; retain this for registration. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. +import "./UsernameInput"; import { UsernameInput } from "./UsernameInput"; import { UserSettings } from "./UserSettings"; import "./components/BannedModal"; diff --git a/src/client/SavedGamesApi.ts b/src/client/SavedGamesApi.ts index be16c4e20f8..600dc0d9c19 100644 --- a/src/client/SavedGamesApi.ts +++ b/src/client/SavedGamesApi.ts @@ -25,12 +25,11 @@ export type SavedGameError = | "request_failed"; async function authedFetch(path: string, init?: RequestInit) { + const headers = new Headers(init?.headers); + headers.set("Authorization", await getAuthHeader()); return fetch(`${getApiBase()}${path}`, { ...init, - headers: { - ...init?.headers, - Authorization: await getAuthHeader(), - }, + headers, }); } diff --git a/src/client/SteamLink.ts b/src/client/SteamLink.ts index 336f3087cd4..32d38d1ca3f 100644 --- a/src/client/SteamLink.ts +++ b/src/client/SteamLink.ts @@ -74,7 +74,7 @@ export function normalizeSteamLinkCode(raw: string): string { export function isValidSteamLinkCode(code: string): boolean { return ( code.length === CODE_LENGTH && - [...code].every((ch) => CODE_ALPHABET.includes(ch)) + code.split("").every((ch) => CODE_ALPHABET.includes(ch)) ); } diff --git a/src/client/Store.ts b/src/client/Store.ts index 82ca3408e25..7adb8326c8a 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -23,7 +23,6 @@ import type { InlineCheckout } from "./components/InlineCheckout"; import "./components/NotLoggedInWarning"; import "./components/PackContentsDialog"; import { ProgressiveList } from "./components/ProgressiveList"; -import "./components/PurchaseButton"; import { alignPurchaseRows } from "./components/PurchaseButton"; import "./components/TribesPanel"; import { modalHeader } from "./components/ui/ModalHeader"; diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 84e7866515e..4b1b0ab6e76 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -2,9 +2,13 @@ import type { MapLayer } from "@openfront/engine-api/game/MapFiles"; import { html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { formatKeyForDisplay, translateText } from "../client/Utils"; +// This type is erased at runtime; retain the import to register the setting. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./components/baseComponents/setting/SettingKeybind"; import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind"; import "./components/baseComponents/setting/SettingNumber"; +// This type is erased at runtime; retain the import to register the setting. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "./components/baseComponents/setting/SettingSelect"; import { SettingSelect } from "./components/baseComponents/setting/SettingSelect"; import "./components/baseComponents/setting/SettingSlider"; diff --git a/src/client/components/LevelBadge.ts b/src/client/components/LevelBadge.ts index cb40341e7cd..a11c0a0d45c 100644 --- a/src/client/components/LevelBadge.ts +++ b/src/client/components/LevelBadge.ts @@ -166,7 +166,7 @@ const DIGITS: Record = { // Width of the digit block in digit pixels: glyphs plus the gaps between // them (no trailing gap, so the block centres exactly). const textWidth = (text: string): number => - [...text].reduce((a, c) => a + DIGITS[c][0].length, 0) + text.length - 1; + text.split("").reduce((a, c) => a + DIGITS[c][0].length, 0) + text.length - 1; // The digits as one path of pixel runs. function digitsPath(text: string, x0: number, y0: number, s: number): string { diff --git a/src/client/components/NavAccountMenu.ts b/src/client/components/NavAccountMenu.ts index 2751c83dee9..41864630963 100644 --- a/src/client/components/NavAccountMenu.ts +++ b/src/client/components/NavAccountMenu.ts @@ -26,7 +26,6 @@ import { import { steamSDK } from "../SteamSDK"; import { playerProfileUrl } from "../utilities/PlayerProfileUrl"; import { copyToClipboard, showToast, translateText } from "../Utils"; -import "./LevelBadge"; import { levelBadgeLabel } from "./LevelBadge"; import { xpBar, xpProgressText } from "./XpBar"; diff --git a/src/client/components/ProfileCard.ts b/src/client/components/ProfileCard.ts index 61ecddfe842..cc1e579745f 100644 --- a/src/client/components/ProfileCard.ts +++ b/src/client/components/ProfileCard.ts @@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators.js"; import { clampPrestige, levelFraction } from "../Progression"; import { prefersReducedMotion } from "../utilities/ReducedMotion"; import { translateText } from "../Utils"; -import "./LevelBadge"; import { levelBadgeAccent } from "./LevelBadge"; import { splitAccountUsername, usernameText } from "./ui/UsernameText"; import { formatXp, xpProgressText } from "./XpBar"; diff --git a/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts b/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts index 27386d9b84d..71d52a29c16 100644 --- a/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts +++ b/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts @@ -272,7 +272,7 @@ export class PlayerGameHistoryView extends LitElement { const url = `${ClientEnv.shareOrigin()}${ClientEnv.gamePath(gameId)}`; try { - await void copyToClipboard(url); + await copyToClipboard(url); showToast(translateText("common.copied"), "green"); } catch { showToast(translateText("common.failed_copy"), "red"); diff --git a/src/client/components/clan/ClanGameHistoryView.ts b/src/client/components/clan/ClanGameHistoryView.ts index ba1a70cf45c..db0c8b386dc 100644 --- a/src/client/components/clan/ClanGameHistoryView.ts +++ b/src/client/components/clan/ClanGameHistoryView.ts @@ -228,7 +228,7 @@ export class ClanGameHistoryView extends LitElement { const url = `${ClientEnv.shareOrigin()}${ClientEnv.gamePath(gameId)}`; try { - await void copyToClipboard(url); + await copyToClipboard(url); showToast(translateText("common.copied"), "green"); } catch { showToast(translateText("common.failed_copy"), "red"); diff --git a/src/client/replay/ReplayViewer.ts b/src/client/replay/ReplayViewer.ts index d9cdcd5bfbb..917eafa61f4 100644 --- a/src/client/replay/ReplayViewer.ts +++ b/src/client/replay/ReplayViewer.ts @@ -22,7 +22,6 @@ import "../hud/layers/PlayerInfoOverlay"; import type { PlayerInfoOverlay } from "../hud/layers/PlayerInfoOverlay"; import "../hud/layers/PlayerStats"; import type { PlayerStats } from "../hud/layers/PlayerStats"; -import "../hud/layers/SettingsModal"; import { ShowSettingsModalEvent, type SettingsModal, diff --git a/src/server/Client.ts b/src/server/Client.ts index 0c3b1d5e7f2..39c3f7cdf0a 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -50,7 +50,7 @@ export class Client { // From the server's /users/@me lookup at join (levelBadgeForPlayer); // undefined for guests, players who hide their level, or when the API sent // no progress. Display-only. - public readonly levelBadge: LevelBadge | undefined = undefined, + public readonly levelBadge?: LevelBadge, ) { this.wireLevelBadge = packLevelBadge(levelBadge); } diff --git a/tests/ColumnPicker.test.ts b/tests/ColumnPicker.test.ts index d4b3fd9ee16..1912257405b 100644 --- a/tests/ColumnPicker.test.ts +++ b/tests/ColumnPicker.test.ts @@ -33,10 +33,10 @@ describe("ColumnPicker", () => { selection = (event as CustomEvent).detail; }); // Registry order: index 0 is clan, 1 is playerType, 2 is tiles, 3 is gold. - (popup?.querySelectorAll("input")[3] as HTMLInputElement).click(); + (popup!.querySelectorAll("input")[3] as HTMLInputElement).click(); expect(selection).toEqual(["tiles", "gold"]); - (popup?.querySelectorAll("input")[0] as HTMLInputElement).click(); + (popup!.querySelectorAll("input")[0] as HTMLInputElement).click(); expect(selection).toEqual(["clan", "tiles"]); sidebar.remove(); diff --git a/tests/DesktopStatusBar.test.ts b/tests/DesktopStatusBar.test.ts index bd9fff04105..57cde4b7092 100644 --- a/tests/DesktopStatusBar.test.ts +++ b/tests/DesktopStatusBar.test.ts @@ -1,7 +1,6 @@ // @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ClientEnv } from "../src/client/ClientEnv"; -import "../src/client/components/DesktopStatusBar"; import { barSource } from "../src/client/components/DesktopStatusBar"; import { backendUnreachableConfirmed, diff --git a/tests/NationCreation.test.ts b/tests/NationCreation.test.ts index 6cc2ada6414..87236c260e4 100644 --- a/tests/NationCreation.test.ts +++ b/tests/NationCreation.test.ts @@ -291,7 +291,9 @@ describe("createNationsForGame: additionalNations pool", () => { ); expect(nations).toHaveLength(2); - const flags = nations.map((n) => n.playerInfo.nationFlag).sort(); + const flags = nations + .map((n) => n.playerInfo.nationFlag) + .sort((a, b) => String(a).localeCompare(String(b))); expect(flags).toEqual(["in", "pk"]); }); diff --git a/tests/NationName.test.ts b/tests/NationName.test.ts index 2b774ed8670..337949ff43d 100644 --- a/tests/NationName.test.ts +++ b/tests/NationName.test.ts @@ -46,12 +46,14 @@ describe("Map manifests: nation name constraints", () => { } // Allow only printable safe-extended-ASCII characters // within [0x20-0x7E] or [0xA0-0xFF], as in https://www.ascii-code.com/. - const excludededCharacters = [...name].filter( - (c) => - c.charCodeAt(0) < 0x20 || - (0x7e < c.charCodeAt(0) && c.charCodeAt(0) < 0xa0) || - 0xff < c.charCodeAt(0), - ); + const excludededCharacters = name + .split("") + .filter( + (c) => + c.charCodeAt(0) < 0x20 || + (0x7e < c.charCodeAt(0) && c.charCodeAt(0) < 0xa0) || + 0xff < c.charCodeAt(0), + ); if (0 < excludededCharacters.length) { violations.push( `${manifestPath} -> nations[${idx}].name "${name}" has ${excludededCharacters.length} non valid characters: ${excludededCharacters}`, diff --git a/tests/PseudoRandom.test.ts b/tests/PseudoRandom.test.ts index 26ad8c54145..585ccfa0041 100644 --- a/tests/PseudoRandom.test.ts +++ b/tests/PseudoRandom.test.ts @@ -79,7 +79,7 @@ describe("PseudoRandom", () => { expect(v).toBeLessThan(8); seen.add(v); } - expect([...seen].sort()).toEqual([3, 4, 5, 6, 7]); + expect([...seen].sort((a, b) => a - b)).toEqual([3, 4, 5, 6, 7]); }); test("nextInt with a single-value range always returns it", () => { diff --git a/tests/ReachabilityGating.test.ts b/tests/ReachabilityGating.test.ts index fc7f6c8f502..628b42b380f 100644 --- a/tests/ReachabilityGating.test.ts +++ b/tests/ReachabilityGating.test.ts @@ -7,6 +7,7 @@ import type { } from "@openfront/shared/WireSchemas"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ClientEnv } from "../src/client/ClientEnv"; +// This named import also registers as a side effect. import { shouldBlockMultiplayerAction } from "../src/client/GameModeSelector"; import { attemptInFlight, @@ -42,9 +43,6 @@ vi.mock("../src/client/LazyModals", async (importOriginal) => ({ whenModalLoaded: (_tag: string, open: () => void) => open(), })); -// Registers as a side effect. -import "../src/client/GameModeSelector"; - /** * OPE-439. The server-list heartbeat already knows whether the API answers; * this is the half that turns that into something the player can see, on the diff --git a/tests/client/CosmeticPackPurchase.test.ts b/tests/client/CosmeticPackPurchase.test.ts index 43c384d376e..db240e5acb2 100644 --- a/tests/client/CosmeticPackPurchase.test.ts +++ b/tests/client/CosmeticPackPurchase.test.ts @@ -72,7 +72,9 @@ describe("purchaseCosmetic for a cosmetic pack", () => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: { ...originalLocation, reload: reloadMock }, + value: Object.assign(new URL(originalLocation.href), { + reload: reloadMock, + }), }); }); diff --git a/tests/client/CosmeticPurchaseDebt.test.ts b/tests/client/CosmeticPurchaseDebt.test.ts index 98a2f989f78..1e4f565e206 100644 --- a/tests/client/CosmeticPurchaseDebt.test.ts +++ b/tests/client/CosmeticPurchaseDebt.test.ts @@ -72,7 +72,9 @@ describe("purchaseCosmetic when the wallet is in debt", () => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: { ...originalLocation, reload: reloadMock }, + value: Object.assign(new URL(originalLocation.href), { + reload: reloadMock, + }), }); vi.mocked(getUserMe).mockResolvedValue(userWith(500, 5000)); }); diff --git a/tests/client/CreatorCodePanel.test.ts b/tests/client/CreatorCodePanel.test.ts index 2bfe96cbddb..704606109b3 100644 --- a/tests/client/CreatorCodePanel.test.ts +++ b/tests/client/CreatorCodePanel.test.ts @@ -125,7 +125,7 @@ describe("CreatorCodePanel", () => { // an in-place refresh via the `creator-changed` event, not a reload. Object.defineProperty(window, "location", { configurable: true, - value: { ...realLocation, reload, hash: "" }, + value: Object.assign(new URL(realLocation.href), { reload, hash: "" }), }); }); diff --git a/tests/client/EffectsGrid.test.ts b/tests/client/EffectsGrid.test.ts index 232d09d64f0..f5fd947755d 100644 --- a/tests/client/EffectsGrid.test.ts +++ b/tests/client/EffectsGrid.test.ts @@ -300,7 +300,7 @@ describe("EffectsGrid", () => { await card.updateComplete; card.querySelector("[data-cosmetic-main]")!.click(); - expect((onMessage.mock.lastCall?.[0] as CustomEvent).detail.message).toBe( + expect((onMessage.mock.lastCall![0] as CustomEvent).detail.message).toBe( "Selected Localized Wake", ); diff --git a/tests/client/InlineCheckout.test.ts b/tests/client/InlineCheckout.test.ts index 30511188f51..4420c50b6e1 100644 --- a/tests/client/InlineCheckout.test.ts +++ b/tests/client/InlineCheckout.test.ts @@ -110,7 +110,9 @@ beforeEach(() => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: { ...originalLocation, reload: reloadMock }, + value: Object.assign(new URL(originalLocation.href), { + reload: reloadMock, + }), }); }); diff --git a/tests/client/InventoryModal.test.ts b/tests/client/InventoryModal.test.ts index 37eecc6e16b..b01d417de75 100644 --- a/tests/client/InventoryModal.test.ts +++ b/tests/client/InventoryModal.test.ts @@ -525,7 +525,7 @@ describe("InventoryModal", () => { it("announces flag, crown and skin selections too", async () => { const onMessage = vi.fn(); const lastMessage = () => - (onMessage.mock.lastCall?.[0] as CustomEvent).detail.message; + (onMessage.mock.lastCall![0] as CustomEvent).detail.message; window.addEventListener("show-message", onMessage); try { await showTab(modal, "skins"); diff --git a/tests/client/ListLobbyDialog.test.ts b/tests/client/ListLobbyDialog.test.ts index 551884efdb7..6c89b0cb8c3 100644 --- a/tests/client/ListLobbyDialog.test.ts +++ b/tests/client/ListLobbyDialog.test.ts @@ -3,6 +3,8 @@ import { MIN_HOSTED_LOBBY_PLAYERS, } from "@openfront/shared/WireSchemas"; import { describe, expect, it } from "vitest"; +// The class is used as a type below, so this import is needed for registration. +// oxlint-disable-next-line import/no-duplicates -- Required custom-element registration. import "../../src/client/components/ListLobbyDialog"; import { ListLobbyDialog } from "../../src/client/components/ListLobbyDialog"; diff --git a/tests/client/NewsModal.test.ts b/tests/client/NewsModal.test.ts index 91cd986db5d..629391eada7 100644 --- a/tests/client/NewsModal.test.ts +++ b/tests/client/NewsModal.test.ts @@ -1,7 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -// Side-effect import registers ; a type-only import would be -// elided and leave the element inert. -import "../../src/client/NewsModal"; +// The named import registers as a side effect; a type-only import +// would be elided and leave the element inert. import { NewsModal } from "../../src/client/NewsModal"; beforeEach(() => { diff --git a/tests/client/SavedGamesApi.test.ts b/tests/client/SavedGamesApi.test.ts index 8316e0821e6..1da045396fb 100644 --- a/tests/client/SavedGamesApi.test.ts +++ b/tests/client/SavedGamesApi.test.ts @@ -87,7 +87,9 @@ describe("SavedGamesApi", () => { tick: "1200", }); expect(init.method).toBe("POST"); - expect(init.headers.Authorization).toBe("Bearer test-jwt"); + expect(new Headers(init.headers).get("Authorization")).toBe( + "Bearer test-jwt", + ); // A byte body, so the browser sends the Content-Length the API checks. expect(init.body).toBeInstanceOf(Uint8Array); expect(new Uint8Array(gunzipSync(init.body))).toEqual(snapshot); @@ -116,7 +118,7 @@ describe("SavedGamesApi", () => { await expect(downloadSavedGame(SAVE.id, true)).resolves.toEqual(snapshot); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe(`https://api.test/public/saved_games/${SAVE.id}/data`); - expect(init?.headers?.Authorization).toBeUndefined(); + expect(new Headers(init?.headers).get("Authorization")).toBeNull(); }); it("downloads the player's own save with their session", async () => { @@ -126,7 +128,9 @@ describe("SavedGamesApi", () => { await expect(downloadSavedGame(SAVE.id, false)).resolves.toEqual(snapshot); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe(`https://api.test/users/@me/saved_games/${SAVE.id}/data`); - expect(init.headers.Authorization).toBe("Bearer test-jwt"); + expect(new Headers(init.headers).get("Authorization")).toBe( + "Bearer test-jwt", + ); }); it("reports a missing save as not_found", async () => { diff --git a/tests/client/UsernameBareClaim.test.ts b/tests/client/UsernameBareClaim.test.ts index f4a000a8a6f..931fef08bf9 100644 --- a/tests/client/UsernameBareClaim.test.ts +++ b/tests/client/UsernameBareClaim.test.ts @@ -156,7 +156,7 @@ describe("UsernamePanel bare-claim fallback", () => { // configurable here, so replacing the object is the way in. Object.defineProperty(window, "location", { configurable: true, - value: { ...realLocation, reload, hash: "" }, + value: Object.assign(new URL(realLocation.href), { reload, hash: "" }), }); }); @@ -287,7 +287,11 @@ describe("UsernamePanel bare-claim fallback", () => { history.replaceState(null, "", "/?x=1#modal=change-username"); Object.defineProperty(window, "location", { configurable: true, - value: { ...realLocation, pathname: "/", search: "?x=1", reload }, + value: Object.assign(new URL(realLocation.href), { + pathname: "/", + search: "?x=1", + reload, + }), }); const replaceState = vi.spyOn(history, "replaceState"); updateUsername.mockResolvedValue({ ok: true, data: okBody() }); diff --git a/tests/client/UsernameClaimPrefill.test.ts b/tests/client/UsernameClaimPrefill.test.ts index 2825933b201..1fb8aa580b0 100644 --- a/tests/client/UsernameClaimPrefill.test.ts +++ b/tests/client/UsernameClaimPrefill.test.ts @@ -167,7 +167,7 @@ describe("claim form prefill", () => { const realDescriptor = Object.getOwnPropertyDescriptor(window, "location")!; Object.defineProperty(window, "location", { configurable: true, - value: { ...window.location, reload, hash: "" }, + value: Object.assign(new URL(window.location.href), { reload, hash: "" }), }); try { getUser.mockResolvedValue({ steamId: "x", name: "Ada.Lovelace" }); diff --git a/tests/client/clan/ClanApiDonate.test.ts b/tests/client/clan/ClanApiDonate.test.ts index 42025e34a24..109512f7330 100644 --- a/tests/client/clan/ClanApiDonate.test.ts +++ b/tests/client/clan/ClanApiDonate.test.ts @@ -41,9 +41,9 @@ describe("donateToClan", () => { amount: "300", idempotencyKey: "key-1234567", }); - const headers = init.headers as Record; - expect(headers["Content-Type"]).toBe("application/json"); - expect(headers.Authorization).toBe("Bearer test-token"); + const headers = new Headers(init.headers); + expect(headers.get("Content-Type")).toBe("application/json"); + expect(headers.get("Authorization")).toBe("Bearer test-token"); }); it("keeps a bigint-sized amount exact on the wire", async () => { diff --git a/tests/client/clan/ClanApiQueries.test.ts b/tests/client/clan/ClanApiQueries.test.ts index 0c4b2fa4ecd..a13ce74bfa7 100644 --- a/tests/client/clan/ClanApiQueries.test.ts +++ b/tests/client/clan/ClanApiQueries.test.ts @@ -562,11 +562,8 @@ describe("fetchClanMembers", () => { await fetchClanMembers("TEST"); - const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< - string, - string - >; - expect(headers.Authorization).toBe("Bearer test-token"); + const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); + expect(headers.get("Authorization")).toBe("Bearer test-token"); }); }); @@ -634,11 +631,8 @@ describe("fetchClanRequests", () => { await fetchClanRequests("TEST"); - const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< - string, - string - >; - expect(headers.Authorization).toBe("Bearer test-token"); + const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); + expect(headers.get("Authorization")).toBe("Bearer test-token"); }); }); @@ -757,11 +751,8 @@ describe("fetchClanGames", () => { await fetchClanGames("TEST"); - const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< - string, - string - >; - expect(headers.Authorization).toBe("Bearer test-token"); + const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); + expect(headers.get("Authorization")).toBe("Bearer test-token"); }); }); diff --git a/tests/client/clan/ClanModal.rendering.test.ts b/tests/client/clan/ClanModal.rendering.test.ts index ad81d3f9673..6f893378a78 100644 --- a/tests/client/clan/ClanModal.rendering.test.ts +++ b/tests/client/clan/ClanModal.rendering.test.ts @@ -553,12 +553,12 @@ describe("ClanModal — rendering", () => { it("uses the compact card variant, not the header size", async () => { const card = await showMyClan(makeClan({ hardBalance: "25" })); const cardSize = ( - card?.querySelector("plutonium-icon") as HTMLElement & { size: number } + card!.querySelector("plutonium-icon") as HTMLElement & { size: number } ).size; const header = await openDetail(makeClan({ hardBalance: "25" })); const headerSize = ( - header?.querySelector("plutonium-icon") as HTMLElement & { + header!.querySelector("plutonium-icon") as HTMLElement & { size: number; } ).size; diff --git a/tests/client/graphics/layers/ChatModalShortcut.test.ts b/tests/client/graphics/layers/ChatModalShortcut.test.ts index ff58f4c3650..87d6220b549 100644 --- a/tests/client/graphics/layers/ChatModalShortcut.test.ts +++ b/tests/client/graphics/layers/ChatModalShortcut.test.ts @@ -1,6 +1,5 @@ import { EventBus } from "@openfront/shared/EventBus"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import "../../../../src/client/hud/layers/ChatModal"; import { ShowPlayerChatEvent, type ChatModal, diff --git a/tests/client/graphics/layers/EmojiTable.test.ts b/tests/client/graphics/layers/EmojiTable.test.ts index d6418ed25de..104b17898ed 100644 --- a/tests/client/graphics/layers/EmojiTable.test.ts +++ b/tests/client/graphics/layers/EmojiTable.test.ts @@ -2,7 +2,6 @@ import { AllPlayers } from "@openfront/engine-api/game/GameTypes"; import { flattenedEmojiTable } from "@openfront/engine-api/Schemas"; import { EventBus } from "@openfront/shared/EventBus"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import "../../../../src/client/hud/layers/EmojiTable"; import { type EmojiTable, ShowPlayerEmojiMenuEvent, diff --git a/tests/client/graphics/layers/PlayerPanelKick.test.ts b/tests/client/graphics/layers/PlayerPanelKick.test.ts index 62cd1f796ab..49f6746c244 100644 --- a/tests/client/graphics/layers/PlayerPanelKick.test.ts +++ b/tests/client/graphics/layers/PlayerPanelKick.test.ts @@ -202,11 +202,10 @@ describe("PlayerPanel - report player", () => { (panel as any).renderModeration(other, other, false); expect(mockActionButton).not.toHaveBeenCalled(); - const nation = { - ...other, + const nation = Object.assign({}, other, { type: () => PlayerType.Nation, clientID: () => null, - } as unknown as PlayerView; + }) as unknown as PlayerView; (panel as any).renderModeration(me, nation, false); expect(mockActionButton).not.toHaveBeenCalled(); }); diff --git a/tests/client/graphics/layers/SpawnTimer.test.ts b/tests/client/graphics/layers/SpawnTimer.test.ts index ac4a3f3c2ee..471d0b8ef36 100644 --- a/tests/client/graphics/layers/SpawnTimer.test.ts +++ b/tests/client/graphics/layers/SpawnTimer.test.ts @@ -1,9 +1,8 @@ import { GameMode, GameType } from "@openfront/engine-api/game/GameTypes"; import { EventBus } from "@openfront/shared/EventBus"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import "../../../../src/client/hud/layers/SpawnTimer"; -import type { SpawnTimer } from "../../../../src/client/hud/layers/SpawnTimer"; import { SpawnBarVisibleEvent } from "../../../../src/client/hud/layers/SpawnTimer"; +import type { SpawnTimer } from "../../../../src/client/hud/layers/SpawnTimer"; import type { GameView } from "../../../../src/client/view"; function makePlayer(team: string | null, tiles: number) { diff --git a/tests/client/replay/ReplayGameAdapter.test.ts b/tests/client/replay/ReplayGameAdapter.test.ts index 6396567aec9..a06fcf7f184 100644 --- a/tests/client/replay/ReplayGameAdapter.test.ts +++ b/tests/client/replay/ReplayGameAdapter.test.ts @@ -103,8 +103,8 @@ describe("ReplayGameAdapter", () => { view .allies() .map((a) => a.smallID()) - .sort(), - ).toEqual([...state.allies].sort()); + .sort((a, b) => a - b), + ).toEqual([...state.allies].sort((a, b) => a - b)); } // A spectator's view: the replay belongs to nobody. expect(adapter.myPlayer()).toBeNull(); diff --git a/tests/client/view/GameView.test.ts b/tests/client/view/GameView.test.ts index 6403446e7cd..5aca7ea6f4c 100644 --- a/tests/client/view/GameView.test.ts +++ b/tests/client/view/GameView.test.ts @@ -496,7 +496,7 @@ describe("GameView.update — units", () => { game .units() .map((u) => u.id()) - .sort(), + .sort((a, b) => a - b), ).toEqual([1, 2]); expect(game.units(UnitType.City).map((u) => u.id())).toEqual([1]); // The inactive one is still present until the NEXT tick sweeps it. diff --git a/tests/matchmaking/e2e.mjs b/tests/matchmaking/e2e.mjs index 93e79ebf7c6..ffd3bd02f2e 100644 --- a/tests/matchmaking/e2e.mjs +++ b/tests/matchmaking/e2e.mjs @@ -266,7 +266,9 @@ try { } c.check( `in-game team split is 2 vs 2 (${JSON.stringify(teamCounts)})`, - Object.values(teamCounts).sort().join(",") === "2,2", + Object.values(teamCounts) + .sort((a, b) => a - b) + .join(",") === "2,2", ); c.check( "team split is identical on every client (deterministic)", diff --git a/tests/server/AnonymizeNames.test.ts b/tests/server/AnonymizeNames.test.ts index ee921b2f345..c322e41e960 100644 --- a/tests/server/AnonymizeNames.test.ts +++ b/tests/server/AnonymizeNames.test.ts @@ -15,9 +15,9 @@ function makeClient( username: string, clanTag: string | null, role: string | null = null, - publicId: string | undefined = undefined, + publicId?: string, friends: string[] = [], - cosmetics: { verified?: boolean } | undefined = undefined, + cosmetics?: { verified?: boolean }, ): Client { return harnessClient({ clientID, diff --git a/tests/server/HostedLobbyListing.test.ts b/tests/server/HostedLobbyListing.test.ts index 3744b544a8c..181d9e96b25 100644 --- a/tests/server/HostedLobbyListing.test.ts +++ b/tests/server/HostedLobbyListing.test.ts @@ -876,11 +876,9 @@ describe("MasterLobbyService hosted lobbies", () => { const created = workers.flatMap((w) => sentMessages(w).filter((m) => m.type === "createGame"), ); - expect(created.map((m) => m.publicGameType).sort()).toEqual([ - "ffa", - "special", - "team", - ]); + expect( + created.map((m) => m.publicGameType).sort((a, b) => a.localeCompare(b)), + ).toEqual(["ffa", "special", "team"]); }); }); diff --git a/tests/server/PoolRouting.test.ts b/tests/server/PoolRouting.test.ts index b45fd9a8110..7782319c36c 100644 --- a/tests/server/PoolRouting.test.ts +++ b/tests/server/PoolRouting.test.ts @@ -32,7 +32,7 @@ describe("poolIndexFor", () => { for (let i = 0; i < 200; i++) { hits.add(poolIndexFor(`player-${i}`, 4)); } - expect([...hits].sort()).toEqual([0, 1, 2, 3]); + expect([...hits].sort((a, b) => a - b)).toEqual([0, 1, 2, 3]); }); }); diff --git a/tests/util/GameServerHarness.ts b/tests/util/GameServerHarness.ts index 6a8e779907a..4a6ec03fdd6 100644 --- a/tests/util/GameServerHarness.ts +++ b/tests/util/GameServerHarness.ts @@ -90,7 +90,7 @@ export function makeMockWs(): MockWs { trigger: async (event, ...args) => { if (event === "close") ws.readyState = CLOSED; for (const handler of listeners.get(event) ?? []) { - await handler(...args); + await Promise.resolve(handler(...args)); } }, emit: (msg) => ws.trigger("message", clientFrame(msg)), From b17f19b4a5825c395ddb078fa796fb4da5f51925 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:29:19 +0200 Subject: [PATCH 2/3] chore(devops): keep extra type-aware rules off Keep no-misused-spread, require-array-sort-compare, no-redundant-type-constituents, await-thenable, and no-useless-default-assignment explicitly disabled. These checks were not enabled by ESLint and are outside this migration. Remove their associated code and test changes, retaining the six unsafe optional-chain fixes and import deduplication. --- .oxlintrc.json | 5 +++++ .../engine-lib/src/game/TeamAssignment.ts | 6 +++--- .../engine/src/execution/EmojiExecution.ts | 2 +- scripts/split-packages/imported-names.ts | 2 +- scripts/split-packages/run.ts | 3 +-- src/client/ClanApi.ts | 9 ++++---- src/client/FriendsApi.ts | 9 ++++---- src/client/SavedGamesApi.ts | 7 ++++--- src/client/SteamLink.ts | 2 +- src/client/components/LevelBadge.ts | 2 +- .../stats/PlayerGameHistoryView.ts | 2 +- .../components/clan/ClanGameHistoryView.ts | 2 +- src/server/Client.ts | 2 +- tests/NationCreation.test.ts | 4 +--- tests/NationName.test.ts | 14 ++++++------- tests/PseudoRandom.test.ts | 2 +- tests/client/CosmeticPackPurchase.test.ts | 4 +--- tests/client/CosmeticPurchaseDebt.test.ts | 4 +--- tests/client/CreatorCodePanel.test.ts | 2 +- tests/client/InlineCheckout.test.ts | 4 +--- tests/client/SavedGamesApi.test.ts | 10 +++------ tests/client/UsernameBareClaim.test.ts | 8 ++----- tests/client/UsernameClaimPrefill.test.ts | 2 +- tests/client/clan/ClanApiDonate.test.ts | 6 +++--- tests/client/clan/ClanApiQueries.test.ts | 21 +++++++++++++------ .../graphics/layers/PlayerPanelKick.test.ts | 5 +++-- tests/client/replay/ReplayGameAdapter.test.ts | 4 ++-- tests/client/view/GameView.test.ts | 2 +- tests/matchmaking/e2e.mjs | 4 +--- tests/server/AnonymizeNames.test.ts | 4 ++-- tests/server/HostedLobbyListing.test.ts | 8 ++++--- tests/server/PoolRouting.test.ts | 2 +- tests/util/GameServerHarness.ts | 2 +- 33 files changed, 82 insertions(+), 83 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index cc5a33093b2..f6130f5ca8b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -28,6 +28,11 @@ "typescript/restrict-template-expressions": "off", "typescript/unbound-method": "off", "typescript/no-base-to-string": "off", + "typescript/no-misused-spread": "off", + "typescript/require-array-sort-compare": "off", + "typescript/no-redundant-type-constituents": "off", + "typescript/await-thenable": "off", + "typescript/no-useless-default-assignment": "off", "typescript/prefer-nullish-coalescing": "error", "eqeqeq": "error", "no-case-declarations": "error", diff --git a/packages/engine-lib/src/game/TeamAssignment.ts b/packages/engine-lib/src/game/TeamAssignment.ts index 0aa194e9b4a..54577140282 100644 --- a/packages/engine-lib/src/game/TeamAssignment.ts +++ b/packages/engine-lib/src/game/TeamAssignment.ts @@ -17,8 +17,8 @@ export function assignTeams( teams: Team[], isDuosTriosQuads: boolean, maxTeamSize: number = getMaxTeamSize(players.length, teams.length), -): Map { - const result = new Map(); +): Map { + const result = new Map(); const teamPlayerCount = new Map(); // Matchmade games arrive with a server-pinned team slot (teamIndex). The @@ -165,7 +165,7 @@ export function assignTeamsLobbyPreview( teams: Team[], teamCount: TeamCountConfig, nationCount: number, -): Map { +): Map { const maxTeamSize = getMaxTeamSize( players.length + nationCount, teams.length, diff --git a/packages/engine/src/execution/EmojiExecution.ts b/packages/engine/src/execution/EmojiExecution.ts index 597926a261b..d022973d46d 100644 --- a/packages/engine/src/execution/EmojiExecution.ts +++ b/packages/engine/src/execution/EmojiExecution.ts @@ -26,7 +26,7 @@ export class EmojiExecution implements Execution { constructor( private requestor: Player, - private recipientID: PlayerID, + private recipientID: PlayerID | typeof AllPlayers, private emoji: number, ) {} diff --git a/scripts/split-packages/imported-names.ts b/scripts/split-packages/imported-names.ts index 21f7b65ced2..1c3960ea720 100644 --- a/scripts/split-packages/imported-names.ts +++ b/scripts/split-packages/imported-names.ts @@ -41,5 +41,5 @@ for (const f of listSourceFiles()) { } else console.log("other ref", r, ref.kind); } } -for (const [n, fs] of [...names].sort(([a], [b]) => a.localeCompare(b))) +for (const [n, fs] of [...names].sort()) console.log(n.padEnd(28), fs.size, [...fs].slice(0, 3).join(" ")); diff --git a/scripts/split-packages/run.ts b/scripts/split-packages/run.ts index efe0ce9fda5..2d1e8c8d62c 100644 --- a/scripts/split-packages/run.ts +++ b/scripts/split-packages/run.ts @@ -290,8 +290,7 @@ console.log( `files rewritten: ${rewritten.size}, specifiers rewritten: ` + `${[...rewritten.values()].reduce((a, b) => a + b, 0)}`, ); -for (const [f, n] of [...rewritten].sort(([a], [b]) => a.localeCompare(b))) - console.log(` ${n}\t${f}`); +for (const [f, n] of [...rewritten].sort()) console.log(` ${n}\t${f}`); if (warnings.length > 0) { console.log(`\nwarnings (${warnings.length}):`); for (const w of warnings) console.log(` ${w}`); diff --git a/src/client/ClanApi.ts b/src/client/ClanApi.ts index 801faff17ba..beea1bbb079 100644 --- a/src/client/ClanApi.ts +++ b/src/client/ClanApi.ts @@ -59,12 +59,13 @@ async function clanFetch( options?: RequestInit, ): Promise { const url = `${getApiBase()}${path}`; - const headers = new Headers(options?.headers); - if (!headers.has("Accept")) headers.set("Accept", "application/json"); - headers.set("Authorization", await getAuthHeader()); return fetch(url, { ...options, - headers, + headers: { + Accept: "application/json", + ...options?.headers, + Authorization: await getAuthHeader(), + }, }); } diff --git a/src/client/FriendsApi.ts b/src/client/FriendsApi.ts index 89808eac18a..4c47b933c37 100644 --- a/src/client/FriendsApi.ts +++ b/src/client/FriendsApi.ts @@ -13,12 +13,13 @@ async function friendsFetch( path: string, options?: RequestInit, ): Promise { - const headers = new Headers(options?.headers); - if (!headers.has("Accept")) headers.set("Accept", "application/json"); - headers.set("Authorization", await getAuthHeader()); return fetch(`${getApiBase()}${path}`, { ...options, - headers, + headers: { + Accept: "application/json", + ...options?.headers, + Authorization: await getAuthHeader(), + }, }); } diff --git a/src/client/SavedGamesApi.ts b/src/client/SavedGamesApi.ts index 600dc0d9c19..be16c4e20f8 100644 --- a/src/client/SavedGamesApi.ts +++ b/src/client/SavedGamesApi.ts @@ -25,11 +25,12 @@ export type SavedGameError = | "request_failed"; async function authedFetch(path: string, init?: RequestInit) { - const headers = new Headers(init?.headers); - headers.set("Authorization", await getAuthHeader()); return fetch(`${getApiBase()}${path}`, { ...init, - headers, + headers: { + ...init?.headers, + Authorization: await getAuthHeader(), + }, }); } diff --git a/src/client/SteamLink.ts b/src/client/SteamLink.ts index 32d38d1ca3f..336f3087cd4 100644 --- a/src/client/SteamLink.ts +++ b/src/client/SteamLink.ts @@ -74,7 +74,7 @@ export function normalizeSteamLinkCode(raw: string): string { export function isValidSteamLinkCode(code: string): boolean { return ( code.length === CODE_LENGTH && - code.split("").every((ch) => CODE_ALPHABET.includes(ch)) + [...code].every((ch) => CODE_ALPHABET.includes(ch)) ); } diff --git a/src/client/components/LevelBadge.ts b/src/client/components/LevelBadge.ts index a11c0a0d45c..cb40341e7cd 100644 --- a/src/client/components/LevelBadge.ts +++ b/src/client/components/LevelBadge.ts @@ -166,7 +166,7 @@ const DIGITS: Record = { // Width of the digit block in digit pixels: glyphs plus the gaps between // them (no trailing gap, so the block centres exactly). const textWidth = (text: string): number => - text.split("").reduce((a, c) => a + DIGITS[c][0].length, 0) + text.length - 1; + [...text].reduce((a, c) => a + DIGITS[c][0].length, 0) + text.length - 1; // The digits as one path of pixel runs. function digitsPath(text: string, x0: number, y0: number, s: number): string { diff --git a/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts b/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts index 71d52a29c16..27386d9b84d 100644 --- a/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts +++ b/src/client/components/baseComponents/stats/PlayerGameHistoryView.ts @@ -272,7 +272,7 @@ export class PlayerGameHistoryView extends LitElement { const url = `${ClientEnv.shareOrigin()}${ClientEnv.gamePath(gameId)}`; try { - await copyToClipboard(url); + await void copyToClipboard(url); showToast(translateText("common.copied"), "green"); } catch { showToast(translateText("common.failed_copy"), "red"); diff --git a/src/client/components/clan/ClanGameHistoryView.ts b/src/client/components/clan/ClanGameHistoryView.ts index db0c8b386dc..ba1a70cf45c 100644 --- a/src/client/components/clan/ClanGameHistoryView.ts +++ b/src/client/components/clan/ClanGameHistoryView.ts @@ -228,7 +228,7 @@ export class ClanGameHistoryView extends LitElement { const url = `${ClientEnv.shareOrigin()}${ClientEnv.gamePath(gameId)}`; try { - await copyToClipboard(url); + await void copyToClipboard(url); showToast(translateText("common.copied"), "green"); } catch { showToast(translateText("common.failed_copy"), "red"); diff --git a/src/server/Client.ts b/src/server/Client.ts index 39c3f7cdf0a..0c3b1d5e7f2 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -50,7 +50,7 @@ export class Client { // From the server's /users/@me lookup at join (levelBadgeForPlayer); // undefined for guests, players who hide their level, or when the API sent // no progress. Display-only. - public readonly levelBadge?: LevelBadge, + public readonly levelBadge: LevelBadge | undefined = undefined, ) { this.wireLevelBadge = packLevelBadge(levelBadge); } diff --git a/tests/NationCreation.test.ts b/tests/NationCreation.test.ts index 87236c260e4..6cc2ada6414 100644 --- a/tests/NationCreation.test.ts +++ b/tests/NationCreation.test.ts @@ -291,9 +291,7 @@ describe("createNationsForGame: additionalNations pool", () => { ); expect(nations).toHaveLength(2); - const flags = nations - .map((n) => n.playerInfo.nationFlag) - .sort((a, b) => String(a).localeCompare(String(b))); + const flags = nations.map((n) => n.playerInfo.nationFlag).sort(); expect(flags).toEqual(["in", "pk"]); }); diff --git a/tests/NationName.test.ts b/tests/NationName.test.ts index 337949ff43d..2b774ed8670 100644 --- a/tests/NationName.test.ts +++ b/tests/NationName.test.ts @@ -46,14 +46,12 @@ describe("Map manifests: nation name constraints", () => { } // Allow only printable safe-extended-ASCII characters // within [0x20-0x7E] or [0xA0-0xFF], as in https://www.ascii-code.com/. - const excludededCharacters = name - .split("") - .filter( - (c) => - c.charCodeAt(0) < 0x20 || - (0x7e < c.charCodeAt(0) && c.charCodeAt(0) < 0xa0) || - 0xff < c.charCodeAt(0), - ); + const excludededCharacters = [...name].filter( + (c) => + c.charCodeAt(0) < 0x20 || + (0x7e < c.charCodeAt(0) && c.charCodeAt(0) < 0xa0) || + 0xff < c.charCodeAt(0), + ); if (0 < excludededCharacters.length) { violations.push( `${manifestPath} -> nations[${idx}].name "${name}" has ${excludededCharacters.length} non valid characters: ${excludededCharacters}`, diff --git a/tests/PseudoRandom.test.ts b/tests/PseudoRandom.test.ts index 585ccfa0041..26ad8c54145 100644 --- a/tests/PseudoRandom.test.ts +++ b/tests/PseudoRandom.test.ts @@ -79,7 +79,7 @@ describe("PseudoRandom", () => { expect(v).toBeLessThan(8); seen.add(v); } - expect([...seen].sort((a, b) => a - b)).toEqual([3, 4, 5, 6, 7]); + expect([...seen].sort()).toEqual([3, 4, 5, 6, 7]); }); test("nextInt with a single-value range always returns it", () => { diff --git a/tests/client/CosmeticPackPurchase.test.ts b/tests/client/CosmeticPackPurchase.test.ts index db240e5acb2..43c384d376e 100644 --- a/tests/client/CosmeticPackPurchase.test.ts +++ b/tests/client/CosmeticPackPurchase.test.ts @@ -72,9 +72,7 @@ describe("purchaseCosmetic for a cosmetic pack", () => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(originalLocation.href), { - reload: reloadMock, - }), + value: { ...originalLocation, reload: reloadMock }, }); }); diff --git a/tests/client/CosmeticPurchaseDebt.test.ts b/tests/client/CosmeticPurchaseDebt.test.ts index 1e4f565e206..98a2f989f78 100644 --- a/tests/client/CosmeticPurchaseDebt.test.ts +++ b/tests/client/CosmeticPurchaseDebt.test.ts @@ -72,9 +72,7 @@ describe("purchaseCosmetic when the wallet is in debt", () => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(originalLocation.href), { - reload: reloadMock, - }), + value: { ...originalLocation, reload: reloadMock }, }); vi.mocked(getUserMe).mockResolvedValue(userWith(500, 5000)); }); diff --git a/tests/client/CreatorCodePanel.test.ts b/tests/client/CreatorCodePanel.test.ts index 704606109b3..2bfe96cbddb 100644 --- a/tests/client/CreatorCodePanel.test.ts +++ b/tests/client/CreatorCodePanel.test.ts @@ -125,7 +125,7 @@ describe("CreatorCodePanel", () => { // an in-place refresh via the `creator-changed` event, not a reload. Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(realLocation.href), { reload, hash: "" }), + value: { ...realLocation, reload, hash: "" }, }); }); diff --git a/tests/client/InlineCheckout.test.ts b/tests/client/InlineCheckout.test.ts index 4420c50b6e1..30511188f51 100644 --- a/tests/client/InlineCheckout.test.ts +++ b/tests/client/InlineCheckout.test.ts @@ -110,9 +110,7 @@ beforeEach(() => { reloadMock = vi.fn(); Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(originalLocation.href), { - reload: reloadMock, - }), + value: { ...originalLocation, reload: reloadMock }, }); }); diff --git a/tests/client/SavedGamesApi.test.ts b/tests/client/SavedGamesApi.test.ts index 1da045396fb..8316e0821e6 100644 --- a/tests/client/SavedGamesApi.test.ts +++ b/tests/client/SavedGamesApi.test.ts @@ -87,9 +87,7 @@ describe("SavedGamesApi", () => { tick: "1200", }); expect(init.method).toBe("POST"); - expect(new Headers(init.headers).get("Authorization")).toBe( - "Bearer test-jwt", - ); + expect(init.headers.Authorization).toBe("Bearer test-jwt"); // A byte body, so the browser sends the Content-Length the API checks. expect(init.body).toBeInstanceOf(Uint8Array); expect(new Uint8Array(gunzipSync(init.body))).toEqual(snapshot); @@ -118,7 +116,7 @@ describe("SavedGamesApi", () => { await expect(downloadSavedGame(SAVE.id, true)).resolves.toEqual(snapshot); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe(`https://api.test/public/saved_games/${SAVE.id}/data`); - expect(new Headers(init?.headers).get("Authorization")).toBeNull(); + expect(init?.headers?.Authorization).toBeUndefined(); }); it("downloads the player's own save with their session", async () => { @@ -128,9 +126,7 @@ describe("SavedGamesApi", () => { await expect(downloadSavedGame(SAVE.id, false)).resolves.toEqual(snapshot); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe(`https://api.test/users/@me/saved_games/${SAVE.id}/data`); - expect(new Headers(init.headers).get("Authorization")).toBe( - "Bearer test-jwt", - ); + expect(init.headers.Authorization).toBe("Bearer test-jwt"); }); it("reports a missing save as not_found", async () => { diff --git a/tests/client/UsernameBareClaim.test.ts b/tests/client/UsernameBareClaim.test.ts index 931fef08bf9..f4a000a8a6f 100644 --- a/tests/client/UsernameBareClaim.test.ts +++ b/tests/client/UsernameBareClaim.test.ts @@ -156,7 +156,7 @@ describe("UsernamePanel bare-claim fallback", () => { // configurable here, so replacing the object is the way in. Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(realLocation.href), { reload, hash: "" }), + value: { ...realLocation, reload, hash: "" }, }); }); @@ -287,11 +287,7 @@ describe("UsernamePanel bare-claim fallback", () => { history.replaceState(null, "", "/?x=1#modal=change-username"); Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(realLocation.href), { - pathname: "/", - search: "?x=1", - reload, - }), + value: { ...realLocation, pathname: "/", search: "?x=1", reload }, }); const replaceState = vi.spyOn(history, "replaceState"); updateUsername.mockResolvedValue({ ok: true, data: okBody() }); diff --git a/tests/client/UsernameClaimPrefill.test.ts b/tests/client/UsernameClaimPrefill.test.ts index 1fb8aa580b0..2825933b201 100644 --- a/tests/client/UsernameClaimPrefill.test.ts +++ b/tests/client/UsernameClaimPrefill.test.ts @@ -167,7 +167,7 @@ describe("claim form prefill", () => { const realDescriptor = Object.getOwnPropertyDescriptor(window, "location")!; Object.defineProperty(window, "location", { configurable: true, - value: Object.assign(new URL(window.location.href), { reload, hash: "" }), + value: { ...window.location, reload, hash: "" }, }); try { getUser.mockResolvedValue({ steamId: "x", name: "Ada.Lovelace" }); diff --git a/tests/client/clan/ClanApiDonate.test.ts b/tests/client/clan/ClanApiDonate.test.ts index 109512f7330..42025e34a24 100644 --- a/tests/client/clan/ClanApiDonate.test.ts +++ b/tests/client/clan/ClanApiDonate.test.ts @@ -41,9 +41,9 @@ describe("donateToClan", () => { amount: "300", idempotencyKey: "key-1234567", }); - const headers = new Headers(init.headers); - expect(headers.get("Content-Type")).toBe("application/json"); - expect(headers.get("Authorization")).toBe("Bearer test-token"); + const headers = init.headers as Record; + expect(headers["Content-Type"]).toBe("application/json"); + expect(headers.Authorization).toBe("Bearer test-token"); }); it("keeps a bigint-sized amount exact on the wire", async () => { diff --git a/tests/client/clan/ClanApiQueries.test.ts b/tests/client/clan/ClanApiQueries.test.ts index a13ce74bfa7..0c4b2fa4ecd 100644 --- a/tests/client/clan/ClanApiQueries.test.ts +++ b/tests/client/clan/ClanApiQueries.test.ts @@ -562,8 +562,11 @@ describe("fetchClanMembers", () => { await fetchClanMembers("TEST"); - const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); - expect(headers.get("Authorization")).toBe("Bearer test-token"); + const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< + string, + string + >; + expect(headers.Authorization).toBe("Bearer test-token"); }); }); @@ -631,8 +634,11 @@ describe("fetchClanRequests", () => { await fetchClanRequests("TEST"); - const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); - expect(headers.get("Authorization")).toBe("Bearer test-token"); + const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< + string, + string + >; + expect(headers.Authorization).toBe("Bearer test-token"); }); }); @@ -751,8 +757,11 @@ describe("fetchClanGames", () => { await fetchClanGames("TEST"); - const headers = new Headers(fetchSpy.mock.calls[0]![1]?.headers); - expect(headers.get("Authorization")).toBe("Bearer test-token"); + const headers = fetchSpy.mock.calls[0]![1]?.headers as Record< + string, + string + >; + expect(headers.Authorization).toBe("Bearer test-token"); }); }); diff --git a/tests/client/graphics/layers/PlayerPanelKick.test.ts b/tests/client/graphics/layers/PlayerPanelKick.test.ts index 49f6746c244..62cd1f796ab 100644 --- a/tests/client/graphics/layers/PlayerPanelKick.test.ts +++ b/tests/client/graphics/layers/PlayerPanelKick.test.ts @@ -202,10 +202,11 @@ describe("PlayerPanel - report player", () => { (panel as any).renderModeration(other, other, false); expect(mockActionButton).not.toHaveBeenCalled(); - const nation = Object.assign({}, other, { + const nation = { + ...other, type: () => PlayerType.Nation, clientID: () => null, - }) as unknown as PlayerView; + } as unknown as PlayerView; (panel as any).renderModeration(me, nation, false); expect(mockActionButton).not.toHaveBeenCalled(); }); diff --git a/tests/client/replay/ReplayGameAdapter.test.ts b/tests/client/replay/ReplayGameAdapter.test.ts index a06fcf7f184..6396567aec9 100644 --- a/tests/client/replay/ReplayGameAdapter.test.ts +++ b/tests/client/replay/ReplayGameAdapter.test.ts @@ -103,8 +103,8 @@ describe("ReplayGameAdapter", () => { view .allies() .map((a) => a.smallID()) - .sort((a, b) => a - b), - ).toEqual([...state.allies].sort((a, b) => a - b)); + .sort(), + ).toEqual([...state.allies].sort()); } // A spectator's view: the replay belongs to nobody. expect(adapter.myPlayer()).toBeNull(); diff --git a/tests/client/view/GameView.test.ts b/tests/client/view/GameView.test.ts index 5aca7ea6f4c..6403446e7cd 100644 --- a/tests/client/view/GameView.test.ts +++ b/tests/client/view/GameView.test.ts @@ -496,7 +496,7 @@ describe("GameView.update — units", () => { game .units() .map((u) => u.id()) - .sort((a, b) => a - b), + .sort(), ).toEqual([1, 2]); expect(game.units(UnitType.City).map((u) => u.id())).toEqual([1]); // The inactive one is still present until the NEXT tick sweeps it. diff --git a/tests/matchmaking/e2e.mjs b/tests/matchmaking/e2e.mjs index ffd3bd02f2e..93e79ebf7c6 100644 --- a/tests/matchmaking/e2e.mjs +++ b/tests/matchmaking/e2e.mjs @@ -266,9 +266,7 @@ try { } c.check( `in-game team split is 2 vs 2 (${JSON.stringify(teamCounts)})`, - Object.values(teamCounts) - .sort((a, b) => a - b) - .join(",") === "2,2", + Object.values(teamCounts).sort().join(",") === "2,2", ); c.check( "team split is identical on every client (deterministic)", diff --git a/tests/server/AnonymizeNames.test.ts b/tests/server/AnonymizeNames.test.ts index c322e41e960..ee921b2f345 100644 --- a/tests/server/AnonymizeNames.test.ts +++ b/tests/server/AnonymizeNames.test.ts @@ -15,9 +15,9 @@ function makeClient( username: string, clanTag: string | null, role: string | null = null, - publicId?: string, + publicId: string | undefined = undefined, friends: string[] = [], - cosmetics?: { verified?: boolean }, + cosmetics: { verified?: boolean } | undefined = undefined, ): Client { return harnessClient({ clientID, diff --git a/tests/server/HostedLobbyListing.test.ts b/tests/server/HostedLobbyListing.test.ts index 181d9e96b25..3744b544a8c 100644 --- a/tests/server/HostedLobbyListing.test.ts +++ b/tests/server/HostedLobbyListing.test.ts @@ -876,9 +876,11 @@ describe("MasterLobbyService hosted lobbies", () => { const created = workers.flatMap((w) => sentMessages(w).filter((m) => m.type === "createGame"), ); - expect( - created.map((m) => m.publicGameType).sort((a, b) => a.localeCompare(b)), - ).toEqual(["ffa", "special", "team"]); + expect(created.map((m) => m.publicGameType).sort()).toEqual([ + "ffa", + "special", + "team", + ]); }); }); diff --git a/tests/server/PoolRouting.test.ts b/tests/server/PoolRouting.test.ts index 7782319c36c..b45fd9a8110 100644 --- a/tests/server/PoolRouting.test.ts +++ b/tests/server/PoolRouting.test.ts @@ -32,7 +32,7 @@ describe("poolIndexFor", () => { for (let i = 0; i < 200; i++) { hits.add(poolIndexFor(`player-${i}`, 4)); } - expect([...hits].sort((a, b) => a - b)).toEqual([0, 1, 2, 3]); + expect([...hits].sort()).toEqual([0, 1, 2, 3]); }); }); diff --git a/tests/util/GameServerHarness.ts b/tests/util/GameServerHarness.ts index 4a6ec03fdd6..6a8e779907a 100644 --- a/tests/util/GameServerHarness.ts +++ b/tests/util/GameServerHarness.ts @@ -90,7 +90,7 @@ export function makeMockWs(): MockWs { trigger: async (event, ...args) => { if (event === "close") ws.readyState = CLOSED; for (const handler of listeners.get(event) ?? []) { - await Promise.resolve(handler(...args)); + await handler(...args); } }, emit: (msg) => ws.trigger("message", clientFrame(msg)), From 37c89f89069e67ab53ad937ad3784cdda68a662e Mon Sep 17 00:00:00 2001 From: Josh Harris Date: Fri, 23 Oct 2026 11:02:15 +0100 Subject: [PATCH 3/3] chore: merge duplicate SavedGamesPanel import from main Co-Authored-By: Claude Opus 5.5 --- src/client/SinglePlayerModal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 9206edd18ce..c030852f341 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -22,7 +22,6 @@ import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; import "./components/GameConfigSettings"; -import "./components/SavedGamesPanel"; import { MEDAL_ORDER, medalIcon } from "./components/map/Medals"; import { type LoadSavedGameDetail,