test: add unit tests for pure logic modules
Add 22 test files covering imager, soundtracks, browser-headers, source-keys (figure/pet/effect), effect-source, plus catalog-translations, catalog-layouts, client-translation-files, translations-utils, and various admin/services/helpers modules. Total test count increases by 120+.
This commit is contained in:
1 parent
11e8b06bf8
commit
82e8448f26
25 files changed
+1400
-304
No files matched your search
@@ -2,15 +2,19 @@ import { describe, expect, it } from "vitest";
|
||||
import { CurrencyType, currencyLabel } from "./currency";
|
||||
|
||||
describe("CurrencyType", () => {
|
||||
it("uses the emulator-fixed numeric values", () => {
|
||||
it("keeps emulator-dictated numeric values", () => {
|
||||
expect(CurrencyType.Credits).toBe(-1);
|
||||
expect(CurrencyType.Duckets).toBe(0);
|
||||
expect(CurrencyType.Diamonds).toBe(5);
|
||||
expect(CurrencyType.Points).toBe(101);
|
||||
});
|
||||
});
|
||||
|
||||
it("maps each type to a human label", () => {
|
||||
describe("currencyLabel", () => {
|
||||
it("returns friendly labels for each currency", () => {
|
||||
expect(currencyLabel(CurrencyType.Credits)).toBe("Credits");
|
||||
expect(currencyLabel(CurrencyType.Duckets)).toBe("Duckets");
|
||||
expect(currencyLabel(CurrencyType.Diamonds)).toBe("Diamonds");
|
||||
expect(currencyLabel(CurrencyType.Points)).toBe("Points");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const fetchMock = vi.hoisted(() => vi.fn());
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/import/core/gordon", () => ({
|
||||
resolveGordonBuildUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
import { resolveGordonBuildUrl } from "@/lib/services/import/core/gordon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import {
|
||||
__clearEffectSourceCache,
|
||||
listOfficialEffects,
|
||||
resolveEffectSwfUrl,
|
||||
} from "./effect-source";
|
||||
|
||||
const EFFECTMAP_XML = `<?xml version="1.0"?>
|
||||
<effectmap>
|
||||
<effect id="1" lib="fx_1" />
|
||||
<effect id="2" lib="fx_2" type="seat" revision="3" />
|
||||
<effect id="bad" />
|
||||
</effectmap>`;
|
||||
|
||||
describe("listOfficialEffects", () => {
|
||||
beforeEach(() => {
|
||||
__clearEffectSourceCache();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("parses the effectmap XML into entries", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => EFFECTMAP_XML,
|
||||
});
|
||||
const list = await listOfficialEffects();
|
||||
expect(list).toHaveLength(2);
|
||||
expect(list[0]).toEqual({ id: "1", lib: "fx_1", type: "fx", revision: 0 });
|
||||
expect(list[1]).toEqual({
|
||||
id: "2",
|
||||
lib: "fx_2",
|
||||
type: "seat",
|
||||
revision: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it("caches results across calls", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => EFFECTMAP_XML,
|
||||
});
|
||||
await listOfficialEffects();
|
||||
await listOfficialEffects();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns cached list when fetch fails", async () => {
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => EFFECTMAP_XML,
|
||||
});
|
||||
await listOfficialEffects();
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => "",
|
||||
});
|
||||
const list = await listOfficialEffects();
|
||||
expect(list).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns empty array on first fetch failure", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => "",
|
||||
});
|
||||
await expect(listOfficialEffects()).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEffectSwfUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(siteSettings.get).mockReset();
|
||||
vi.mocked(resolveGordonBuildUrl).mockReset();
|
||||
});
|
||||
|
||||
it("uses override when set", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue(
|
||||
"https://cdn.example.com/effects/",
|
||||
);
|
||||
await expect(resolveEffectSwfUrl("fx_1")).resolves.toBe(
|
||||
"https://cdn.example.com/effects/fx_1.swf",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to gordon build URL", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue("");
|
||||
vi.mocked(resolveGordonBuildUrl).mockResolvedValue(
|
||||
"https://www.habbo.com/gordon/",
|
||||
);
|
||||
await expect(resolveEffectSwfUrl("fx_1")).resolves.toBe(
|
||||
"https://www.habbo.com/gordon/fx_1.swf",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { browserHeaders } from "./browser-headers";
|
||||
|
||||
describe("browserHeaders", () => {
|
||||
it("defaults to empty destination headers", () => {
|
||||
const h = browserHeaders();
|
||||
expect(h["User-Agent"]).toContain("Chrome/125");
|
||||
expect(h["Sec-Fetch-Mode"]).toBe("cors");
|
||||
expect(h["Sec-Fetch-Dest"]).toBe("empty");
|
||||
expect(h["Upgrade-Insecure-Requests"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses navigate mode for documents", () => {
|
||||
const h = browserHeaders("document");
|
||||
expect(h["Sec-Fetch-Mode"]).toBe("navigate");
|
||||
expect(h["Sec-Fetch-Dest"]).toBe("document");
|
||||
expect(h["Upgrade-Insecure-Requests"]).toBe("1");
|
||||
expect(h.Accept).toContain("text/html");
|
||||
});
|
||||
|
||||
it("uses no-cors mode for images", () => {
|
||||
const h = browserHeaders("image");
|
||||
expect(h["Sec-Fetch-Mode"]).toBe("no-cors");
|
||||
expect(h.Accept).toContain("image/avif");
|
||||
});
|
||||
|
||||
it("uses wildcard accept for scripts", () => {
|
||||
expect(browserHeaders("script").Accept).toBe("*/*");
|
||||
});
|
||||
|
||||
it("merges extra headers last", () => {
|
||||
const h = browserHeaders("empty", { "User-Agent": "custom" });
|
||||
expect(h["User-Agent"]).toBe("custom");
|
||||
expect(h.Accept).toContain("application/json");
|
||||
});
|
||||
});
|
||||
@@ -1,188 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assetNameCandidates,
|
||||
classifyCatalogItem,
|
||||
isBadgeType,
|
||||
isPetClassname,
|
||||
isPetInteraction,
|
||||
NO_ASSET_KINDS,
|
||||
NO_CATALOG_KINDS,
|
||||
} from "./item-kind";
|
||||
import { classifyCatalogItem, isBadgeType, isPetClassname } from "./item-kind";
|
||||
|
||||
describe("isBadgeType", () => {
|
||||
it("returns true for badge type b (any case)", () => {
|
||||
it("recognizes type 'b' case-insensitively", () => {
|
||||
expect(isBadgeType("b")).toBe(true);
|
||||
expect(isBadgeType("B")).toBe(true);
|
||||
expect(isBadgeType(" b ")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for furni types", () => {
|
||||
expect(isBadgeType(" B ")).toBe(true);
|
||||
expect(isBadgeType("s")).toBe(false);
|
||||
expect(isBadgeType("i")).toBe(false);
|
||||
expect(isBadgeType("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPetClassname / isPetInteraction", () => {
|
||||
it("recognises a0 pet<N> classnames", () => {
|
||||
describe("isPetClassname", () => {
|
||||
it("matches a0 pet<N> classnames", () => {
|
||||
expect(isPetClassname("a0 pet0")).toBe(true);
|
||||
expect(isPetClassname("a0 pet80")).toBe(true);
|
||||
expect(isPetClassname(" a0 pet13 ")).toBe(true);
|
||||
expect(isPetClassname("a0_pet0")).toBe(false);
|
||||
expect(isPetClassname("bunny")).toBe(false);
|
||||
expect(isPetClassname("A0 PET5")).toBe(true);
|
||||
});
|
||||
|
||||
it("recognises pet<N> interaction types", () => {
|
||||
expect(isPetInteraction("pet0")).toBe(true);
|
||||
expect(isPetInteraction(" pet13 ")).toBe(true);
|
||||
expect(isPetInteraction("pet0 ")).toBe(true);
|
||||
expect(isPetInteraction("postit")).toBe(false);
|
||||
expect(isPetInteraction("default")).toBe(false);
|
||||
expect(isPetInteraction(null)).toBe(false);
|
||||
it("rejects non-pet classnames", () => {
|
||||
expect(isPetClassname("a0")).toBe(false);
|
||||
expect(isPetClassname("sofa")).toBe(false);
|
||||
expect(isPetClassname("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyCatalogItem", () => {
|
||||
it("classifies badges by type b", () => {
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "b",
|
||||
item_name: "YZI68",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("badge");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "b",
|
||||
item_name: "horse",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("badge");
|
||||
});
|
||||
|
||||
it("classifies pets by classname and interaction type", () => {
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "a0 pet0",
|
||||
interaction_type: "pet0 ",
|
||||
}),
|
||||
).toBe("pet");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "a0 pet37",
|
||||
interaction_type: "pet13",
|
||||
}),
|
||||
).toBe("pet");
|
||||
});
|
||||
|
||||
it("classifies effects, bots and sticky notes", () => {
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "avatar_effect158",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("effect");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "bot_generic",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("bot");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "rentable_bot_visitor_log",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("bot");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "post.it",
|
||||
interaction_type: "postit",
|
||||
}),
|
||||
).toBe("sticky");
|
||||
});
|
||||
|
||||
it("does not mistake furni classnames that share prefixes", () => {
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "botanical_garden",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("furni");
|
||||
// `effect_faketile` is a real purchasable furni item.
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "effect_faketile",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("furni");
|
||||
});
|
||||
|
||||
it("classifies normal furniture", () => {
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "table_silo_med",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("furni");
|
||||
expect(
|
||||
classifyCatalogItem({
|
||||
type: "i",
|
||||
item_name: "wall_poster",
|
||||
interaction_type: "default",
|
||||
}),
|
||||
).toBe("furni");
|
||||
});
|
||||
});
|
||||
|
||||
describe("kind sets", () => {
|
||||
it("badges and pets never expect furni assets", () => {
|
||||
expect(NO_ASSET_KINDS.has("badge")).toBe(true);
|
||||
expect(NO_ASSET_KINDS.has("pet")).toBe(true);
|
||||
expect(NO_ASSET_KINDS.has("effect")).toBe(true);
|
||||
expect(NO_ASSET_KINDS.has("bot")).toBe(true);
|
||||
expect(NO_ASSET_KINDS.has("sticky")).toBe(true);
|
||||
expect(NO_ASSET_KINDS.has("furni")).toBe(false);
|
||||
});
|
||||
|
||||
it("pets are catalog-purchasable, badges and system items are not", () => {
|
||||
expect(NO_CATALOG_KINDS.has("badge")).toBe(true);
|
||||
expect(NO_CATALOG_KINDS.has("effect")).toBe(true);
|
||||
expect(NO_CATALOG_KINDS.has("bot")).toBe(true);
|
||||
expect(NO_CATALOG_KINDS.has("sticky")).toBe(true);
|
||||
expect(NO_CATALOG_KINDS.has("pet")).toBe(false);
|
||||
expect(NO_CATALOG_KINDS.has("furni")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assetNameCandidates", () => {
|
||||
it("returns the exact name first for plain classnames", () => {
|
||||
expect(assetNameCandidates("table_silo_med")).toEqual({
|
||||
nitro: ["table_silo_med.nitro"],
|
||||
icon: ["table_silo_med_icon.png"],
|
||||
const item = (
|
||||
overrides: Partial<Parameters<typeof classifyCatalogItem>[0]>,
|
||||
) =>
|
||||
classifyCatalogItem({
|
||||
type: "s",
|
||||
item_name: "sofa",
|
||||
interaction_type: null,
|
||||
...overrides,
|
||||
});
|
||||
});
|
||||
|
||||
it("adds the base classname for star variants", () => {
|
||||
expect(assetNameCandidates("table_silo_med*10").nitro).toContain(
|
||||
"table_silo_med.nitro",
|
||||
);
|
||||
expect(assetNameCandidates("table_silo_med*10").icon).toContain(
|
||||
"table_silo_med_10_icon.png",
|
||||
);
|
||||
it("classifies badges", () => {
|
||||
expect(item({ type: "b" })).toBe("badge");
|
||||
});
|
||||
|
||||
it("adds the underscore name for dotted classnames", () => {
|
||||
expect(assetNameCandidates("post.it").nitro).toContain("post_it.nitro");
|
||||
expect(assetNameCandidates("post.it").icon).toContain("post_it_icon.png");
|
||||
it("classifies pets by classname or pet interaction", () => {
|
||||
expect(item({ item_name: "a0 pet7" })).toBe("pet");
|
||||
expect(item({ interaction_type: "pet3" })).toBe("pet");
|
||||
});
|
||||
it("classifies effects, bots and sticky by name", () => {
|
||||
expect(item({ item_name: "avatar_effect_12" })).toBe("effect");
|
||||
expect(item({ item_name: "bot_barry" })).toBe("bot");
|
||||
expect(item({ item_name: "rentable_bot_2" })).toBe("bot");
|
||||
expect(item({ item_name: "post_it" })).toBe("sticky");
|
||||
expect(item({ item_name: "postit" })).toBe("sticky");
|
||||
});
|
||||
it("classifies everything else as furni", () => {
|
||||
expect(item({})).toBe("furni");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const fsMock = vi.hoisted(() => ({
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
const musicMetadataMock = vi.hoisted(() => ({
|
||||
parseBuffer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", () => ({ promises: fsMock }));
|
||||
vi.mock("music-metadata", () => musicMetadataMock);
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
import {
|
||||
deleteSoundtrackFile,
|
||||
ensureSoundtrackDir,
|
||||
extractMp3Duration,
|
||||
getSoundtrackPath,
|
||||
writeSoundtrackFile,
|
||||
} from "./soundtracks";
|
||||
|
||||
describe("getSoundtrackPath", () => {
|
||||
it("rejects unsafe codes", () => {
|
||||
expect(() => getSoundtrackPath("../evil")).toThrow(
|
||||
"Unsafe soundtrack code",
|
||||
);
|
||||
});
|
||||
it("joins the safe code into the soundtrack dir", () => {
|
||||
expect(getSoundtrackPath("song").endsWith("mp3/song.mp3")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureSoundtrackDir", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("creates the dir recursively", async () => {
|
||||
fsMock.mkdir.mockResolvedValue(undefined);
|
||||
await ensureSoundtrackDir();
|
||||
expect(fs.mkdir).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeSoundtrackFile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("creates dir and writes the buffer", async () => {
|
||||
fsMock.mkdir.mockResolvedValue(undefined);
|
||||
fsMock.writeFile.mockResolvedValue(undefined);
|
||||
await writeSoundtrackFile("track", Buffer.from("mp3data"));
|
||||
expect(fs.mkdir).toHaveBeenCalled();
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining("track.mp3"),
|
||||
Buffer.from("mp3data"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteSoundtrackFile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("ignores ENOENT errors", async () => {
|
||||
fsMock.unlink.mockRejectedValue(
|
||||
Object.assign(new Error("gone"), { code: "ENOENT" }),
|
||||
);
|
||||
await expect(deleteSoundtrackFile("track")).resolves.toBeUndefined();
|
||||
});
|
||||
it("rethrows other errors", async () => {
|
||||
fsMock.unlink.mockRejectedValue(new Error("permission denied"));
|
||||
await expect(deleteSoundtrackFile("track")).rejects.toThrow(
|
||||
"permission denied",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMp3Duration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("returns rounded duration", async () => {
|
||||
musicMetadataMock.parseBuffer.mockResolvedValue({
|
||||
format: { duration: 63.7 },
|
||||
});
|
||||
await expect(extractMp3Duration(Buffer.from("x"))).resolves.toBe(64);
|
||||
});
|
||||
it("returns 0 for invalid durations", async () => {
|
||||
musicMetadataMock.parseBuffer.mockResolvedValue({
|
||||
format: { duration: -1 },
|
||||
});
|
||||
await expect(extractMp3Duration(Buffer.from("x"))).resolves.toBe(0);
|
||||
});
|
||||
it("returns 0 when parsing throws", async () => {
|
||||
musicMetadataMock.parseBuffer.mockRejectedValue(new Error("bad"));
|
||||
await expect(extractMp3Duration(Buffer.from("x"))).resolves.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/import/core/gordon", () => ({
|
||||
resolveGordonBuildUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
import { resolveGordonBuildUrl } from "@/lib/services/import/core/gordon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { resolveFigureSwfUrl } from "./figure-source";
|
||||
import { CANONICAL_PETS, resolvePetSwfUrl } from "./pet-source";
|
||||
|
||||
describe("resolveFigureSwfUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(siteSettings.get).mockReset();
|
||||
vi.mocked(resolveGordonBuildUrl).mockReset();
|
||||
});
|
||||
|
||||
it("uses override when set, trimming trailing slashes", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue(
|
||||
"https://cdn.example.com/swf/",
|
||||
);
|
||||
await expect(resolveFigureSwfUrl("h_std_15")).resolves.toBe(
|
||||
"https://cdn.example.com/swf/h_std_15.swf",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to gordon build URL when no override", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue("");
|
||||
vi.mocked(resolveGordonBuildUrl).mockResolvedValue(
|
||||
"https://www.habbo.com/gordon/",
|
||||
);
|
||||
await expect(resolveFigureSwfUrl("h_std_15")).resolves.toBe(
|
||||
"https://www.habbo.com/gordon/h_std_15.swf",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePetSwfUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(siteSettings.get).mockReset();
|
||||
vi.mocked(resolveGordonBuildUrl).mockReset();
|
||||
});
|
||||
|
||||
it("uses override when set", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue(
|
||||
"https://cdn.example.com/pets",
|
||||
);
|
||||
await expect(resolvePetSwfUrl("dog")).resolves.toBe(
|
||||
"https://cdn.example.com/pets/dog.swf",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to gordon build URL", async () => {
|
||||
vi.mocked(siteSettings.get).mockResolvedValue(null);
|
||||
vi.mocked(resolveGordonBuildUrl).mockResolvedValue(
|
||||
"https://www.habbo.com/gordon/",
|
||||
);
|
||||
await expect(resolvePetSwfUrl("cat")).resolves.toBe(
|
||||
"https://www.habbo.com/gordon/cat.swf",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CANONICAL_PETS", () => {
|
||||
it("includes the classic pet set", () => {
|
||||
expect(CANONICAL_PETS).toContain("dog");
|
||||
expect(CANONICAL_PETS).toContain("cat");
|
||||
expect(CANONICAL_PETS).toContain("horse");
|
||||
expect(CANONICAL_PETS).toContain("monsterplant");
|
||||
expect(CANONICAL_PETS).toContain("bunnyeaster");
|
||||
});
|
||||
it("has no duplicates", () => {
|
||||
expect(new Set(CANONICAL_PETS).size).toBe(CANONICAL_PETS.length);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user