Merge pull request #4863 from rommapp/chore/lint-rom-fixture-casts

chore(lint): ban double-cast ROM fixtures in tests
This commit is contained in:
Georges-Antoine Assi
2026-09-28 09:14:28 -04:00
committed by GitHub
12 changed files with 65 additions and 43 deletions
+20
View File
@@ -213,6 +213,26 @@ export default tseslint.config(
"romm/no-layout-media-query": "error",
},
},
{
files: ["src/**/*.test.ts"],
ignores: [
"src/views/**",
"src/components/**",
"src/console/**",
"src/layouts/**",
],
rules: {
"no-restricted-syntax": [
"error",
{
selector:
"TSAsExpression[expression.type='TSAsExpression'][expression.typeAnnotation.type='TSUnknownKeyword'][typeAnnotation.typeName.name=/^(DetailedRom|SimpleRom)$/]",
message:
"Build ROM fixtures with makeRom or makeDetailedRom from @/utils/rom.fixtures instead of `as unknown as`.",
},
],
},
},
// Keep last: Prettier owns formatting, so this switches off every
// stylistic rule the two tools would otherwise fight over.
prettierConfig,
+2 -5
View File
@@ -6,6 +6,7 @@ import router, { applyRouteTitle, ROUTES } from "@/plugins/router";
import storeAuth from "@/stores/auth";
import storeRoms, { type DetailedRom } from "@/stores/roms";
import type { User } from "@/stores/users";
import { makeDetailedRom } from "@/utils/rom.fixtures";
const { getRom, stubView } = vi.hoisted(() => ({
getRom: vi.fn(),
@@ -24,11 +25,7 @@ vi.mock("@/v2/layouts/AppLayout.vue", stubView);
vi.mock("@/v2/views/GameDetails.vue", stubView);
function makeRom(overrides: Partial<DetailedRom> = {}): DetailedRom {
return {
id: 1,
name: "Chrono Trigger",
...overrides,
} as unknown as DetailedRom;
return makeDetailedRom({ name: "Chrono Trigger", ...overrides });
}
describe("route titles", () => {
+6 -4
View File
@@ -8,6 +8,7 @@ import type {
PlatformSupport,
PlatformSupportQuery,
} from "@/types/rommNative";
import { makeRom as baseRom } from "@/utils/rom.fixtures";
const shellPresent = { value: true };
const fetchPlatformSupport = vi.fn(
@@ -74,8 +75,7 @@ vi.mock("@/utils", () => ({
const { useNativeStore } = await import("@/stores/native");
function makeRom(overrides: Partial<SimpleRom> = {}): SimpleRom {
return {
id: 1,
return baseRom({
name: "Chrono Trigger",
fs_name: "Chrono Trigger.sfc",
fs_name_no_ext: "Chrono Trigger",
@@ -84,9 +84,11 @@ function makeRom(overrides: Partial<SimpleRom> = {}): SimpleRom {
platform_slug: "snes",
has_file_on_disk: true,
// As a single-rom endpoint answers. A gallery card passes `files: []`.
files: [{ full_path: "snes/game.sfc", file_size_bytes: 4194304 }],
files: [
{ full_path: "snes/game.sfc", file_size_bytes: 4194304 },
] as SimpleRom["files"],
...overrides,
} as unknown as SimpleRom;
});
}
beforeEach(() => {
@@ -3,6 +3,7 @@ import { flushPromises, mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { defineComponent } from "vue";
import type { SimpleRom } from "@/stores/roms";
import { makeRom } from "@/utils/rom.fixtures";
import AnniversaryWidget from "./AnniversaryWidget.vue";
const { getRoms } = vi.hoisted(() => ({ getRoms: vi.fn() }));
@@ -58,15 +59,15 @@ function releasedOn(year: number, month: number, day: number): number {
}
function rom(id: number, name: string, released: number): SimpleRom {
return {
return makeRom({
id,
name,
fs_name: `${name}.sfc`,
platform_slug: "snes",
platform_display_name: "Super Nintendo",
is_identified: true,
metadatum: { first_release_date: released },
} as unknown as SimpleRom;
metadatum: { first_release_date: released } as SimpleRom["metadatum"],
});
}
/** A page of the shared rom list, as `getRoms` resolves it. */
@@ -1,6 +1,7 @@
import { mount, type VueWrapper } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SimpleRom } from "@/stores/roms";
import { makeRom } from "@/utils/rom.fixtures";
import PlayerShell from "./PlayerShell.vue";
const mocks = vi.hoisted(() => ({ setStageActive: vi.fn() }));
@@ -21,7 +22,7 @@ vi.mock("@/v2/components/shared/GameCover.vue", () => ({
default: { template: "<div class='game-cover' />" },
}));
const heroRom = { id: 1, platform_id: 2 } as unknown as SimpleRom;
const heroRom = makeRom({ id: 1, platform_id: 2 });
function mountShell(
props: Partial<{
@@ -106,7 +107,7 @@ describe("PlayerShell", () => {
it("disables the gallery link when the hero carries no platform", () => {
const { toPlatform } = backLinks(
mountShell({ heroRom: { id: 1 } as unknown as SimpleRom }),
mountShell({ heroRom: makeRom({ id: 1 }) }),
);
expect(toPlatform.props("to")).toBeUndefined();
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ref } from "vue";
import type { SimpleRom } from "@/stores/roms";
import { makeRom as baseRom } from "@/utils/rom.fixtures";
import { useCanPlay } from "./index";
// Each engine's support check is stubbed so a test can enable one route at a
@@ -49,13 +50,12 @@ vi.mock("@/utils", () => ({
}));
function makeRom(overrides: Partial<SimpleRom> = {}): SimpleRom {
return {
id: 1,
return baseRom({
name: "Chrono Trigger",
platform_slug: "snes",
has_file_on_disk: true,
...overrides,
} as unknown as SimpleRom;
});
}
beforeEach(() => {
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ActionKey } from "@/__generated__";
import type { SimpleRom } from "@/stores/roms";
import { makeRom as baseRom } from "@/utils/rom.fixtures";
import { useGameActions } from "./index";
// Controllable stubs shared with the mocked modules below.
@@ -124,14 +125,13 @@ vi.mock("@/v2/composables/useViewTransition", () => ({
}));
function makeRom(status: SimpleRom["rom_user"]["status"] = null): SimpleRom {
return {
id: 1,
return baseRom({
name: "Chrono Trigger",
fs_name_no_ext: "Chrono Trigger",
platform_slug: "snes",
has_file_on_disk: true,
rom_user: { status },
} as unknown as SimpleRom;
rom_user: { status } as SimpleRom["rom_user"],
});
}
beforeEach(() => {
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import playSessionApi from "@/services/api/play-session";
import type { SimpleRom } from "@/stores/roms";
import { makeRom as baseRom } from "@/utils/rom.fixtures";
import { usePlaySession } from "./index";
vi.mock("@/services/api/play-session", () => ({
@@ -21,7 +22,7 @@ vi.mock("@/stores/auth", () => ({
const ingest = vi.mocked(playSessionApi.ingestPlaySessionsKeepalive);
function makeRom(id = 7): SimpleRom {
return { id } as unknown as SimpleRom;
return baseRom({ id });
}
beforeEach(() => {
@@ -4,6 +4,7 @@ import storeCollections, { type Collection } from "@/stores/collections";
import storeGalleryFilter from "@/stores/galleryFilter";
import type { Platform } from "@/stores/platforms";
import storeRoms, { type DetailedRom, type SimpleRom } from "@/stores/roms";
import { makeDetailedRom, makeRom as baseRom } from "@/utils/rom.fixtures";
import storeGalleryRoms from "@/v2/stores/galleryRoms";
import storeGallerySelection from "@/v2/stores/gallerySelection";
import { useRomSync } from "./index";
@@ -18,11 +19,7 @@ vi.mock("@/services/api/rom", () => ({
}));
function makeRom(overrides: Partial<SimpleRom> = {}): SimpleRom {
return {
id: 1,
name: "Chrono Trigger",
...overrides,
} as unknown as SimpleRom;
return baseRom({ name: "Chrono Trigger", ...overrides });
}
/** Put the gallery in a platform context with one loaded window so
@@ -257,7 +254,7 @@ describe("useRomSync", () => {
describe("refetchRom", () => {
const detailed = (id: number) =>
({ id, name: "Chrono Trigger" }) as unknown as DetailedRom;
makeDetailedRom({ id, name: "Chrono Trigger" });
it("applies the fresh rom to the open view and the caches", async () => {
const romsStore = storeRoms();
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import type { MusicTrackSchema, TrackMetaSchema } from "@/__generated__";
import type { DetailedRom } from "@/stores/roms";
import { makeDetailedRom } from "@/utils/rom.fixtures";
import {
isAudioFile,
nowPlayingCaption,
@@ -15,7 +16,7 @@ function romFile(id: number, fileName: string, category = "soundtrack") {
}
function rom(files: unknown[]): DetailedRom {
return { id: 7, files } as unknown as DetailedRom;
return makeDetailedRom({ id: 7, files: files as DetailedRom["files"] });
}
describe("isAudioFile", () => {
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { defineComponent } from "vue";
import type { DetailedRom } from "@/stores/roms";
import type { LaunchState, SaveSyncOutcome } from "@/types/rommNative";
import { makeDetailedRom } from "@/utils/rom.fixtures";
import EmulatorJS from "./EmulatorJS.vue";
const mocks = vi.hoisted(() => ({
@@ -168,7 +169,7 @@ vi.mock("@/v2/composables/useStageActive", () => ({ useStageActive: vi.fn() }));
vi.mock("@/v2/composables/useUnloadGuard", () => ({ useUnloadGuard: vi.fn() }));
const ROM = {
const ROM = makeDetailedRom({
id: 7,
name: "Shadow of the Colossus",
platform_id: 4,
@@ -176,25 +177,26 @@ const ROM = {
has_file_on_disk: true,
files: [],
user_saves: [],
user_states: [],
user_screenshots: [],
} as unknown as DetailedRom;
});
// Two discs, so the panel renders its disc selector and there is a pick to
// carry. Ids rather than names: a pick is a rom file's id.
const DISC_SET = {
const DISC_SET = makeDetailedRom({
...ROM,
files: [
{ id: 101, file_name: "Game (Disc 1).chd" },
{ id: 102, file_name: "Game (Disc 2).chd" },
],
} as unknown as DetailedRom;
] as DetailedRom["files"],
});
// A scraped bezel, which is what puts the panel's bezel switch on screen.
const BEZELED_ROM = {
const BEZELED_ROM = makeDetailedRom({
...ROM,
ss_metadata: { bezel_path: "roms/7/bezel/bezel.png" },
} as unknown as DetailedRom;
ss_metadata: {
bezel_path: "roms/7/bezel/bezel.png",
} as DetailedRom["ss_metadata"],
});
// One BIOS file for the platform, which is what puts the firmware picker on
// screen. Its contents do not matter: the panel lists what the server returns.
+5 -5
View File
@@ -5,6 +5,7 @@ import { defineComponent, type Slots, type VNodeChild } from "vue";
import type { SaveSchema, StateSchema } from "@/__generated__";
import type { DetailedRom } from "@/stores/roms";
import { saveFixture, stateFixture } from "@/utils/assets.fixtures";
import { makeDetailedRom } from "@/utils/rom.fixtures";
import AssetPreview from "@/v2/components/Player/AssetPreview.vue";
import SaveDataPanel from "@/v2/components/Player/SaveDataPanel.vue";
import AssetList from "@/v2/components/shared/AssetList.vue";
@@ -214,19 +215,18 @@ const ARCHIVES = [
];
function romWith(saves: SaveSchema[], states: StateSchema[] = []): DetailedRom {
return {
return makeDetailedRom({
id: 3,
name: "Archer Maclean's 3D Pool (USA)",
platform_slug: "gba",
platform_name: "Game Boy Advance",
platform_display_name: "Game Boy Advance",
fs_name: "Archer Maclean's 3D Pool (USA).gba",
files: [],
user_saves: saves,
all_user_states: states,
all_user_states: states.map((state) => ({ ...state, username: "admin" })),
user_screenshots: [],
metadatum: {},
} as unknown as DetailedRom;
metadatum: {} as DetailedRom["metadatum"],
});
}
// The view listens on document and window, so a mount left standing would