mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
0cb17f8086
## Description: Commit `4d5b7c0` added `adfree: boolean` as a required field to `UserMeResponseSchema` in `ApiSchemas.ts`, but did not update the mock object in `tests/ResolveCosmetics.test.ts`. This caused `tsc --noEmit` to fail with a type overlap error, breaking the production build. **Fix:** Add `adfree: false` to the `player` mock inside `makeUserMe()` in the test file. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
import { resolveCosmetics } from "../src/client/Cosmetics";
|
||
import { UserMeResponse } from "../src/core/ApiSchemas";
|
||
import { Cosmetics } from "../src/core/CosmeticSchemas";
|
||
|
||
const product = { productId: "prod_1", priceId: "price_1", price: "$4.99" };
|
||
|
||
function makeCosmetics(overrides: Partial<Cosmetics> = {}): Cosmetics {
|
||
return {
|
||
patterns: {},
|
||
flags: {},
|
||
colorPalettes: {},
|
||
...overrides,
|
||
} as Cosmetics;
|
||
}
|
||
|
||
function makeUserMe(flares: string[] = []): UserMeResponse {
|
||
return {
|
||
user: {},
|
||
player: {
|
||
publicId: "test",
|
||
adfree: false,
|
||
flares,
|
||
achievements: { singleplayerMap: [] },
|
||
},
|
||
} as UserMeResponse;
|
||
}
|
||
|
||
describe("resolveCosmetics", () => {
|
||
test("returns empty array for null cosmetics", () => {
|
||
expect(resolveCosmetics(null, false, null)).toEqual([]);
|
||
});
|
||
|
||
test("always includes default pattern as first item, owned", () => {
|
||
const result = resolveCosmetics(makeCosmetics(), false, null);
|
||
expect(result[0]).toEqual({
|
||
type: "pattern",
|
||
cosmetic: null,
|
||
colorPalette: null,
|
||
relationship: "owned",
|
||
key: "pattern:default",
|
||
});
|
||
});
|
||
|
||
describe("patterns", () => {
|
||
const pattern = {
|
||
type: "pattern" as const,
|
||
name: "stripes",
|
||
pattern: "AAAAAA",
|
||
affiliateCode: null,
|
||
product,
|
||
priceSoft: undefined,
|
||
priceHard: undefined,
|
||
rarity: "common",
|
||
colorPalettes: [
|
||
{ name: "red", isArchived: false },
|
||
{ name: "blue", isArchived: false },
|
||
],
|
||
};
|
||
|
||
const colorPalettes = {
|
||
red: { name: "red", primaryColor: "#ff0000", secondaryColor: "#000000" },
|
||
blue: {
|
||
name: "blue",
|
||
primaryColor: "#0000ff",
|
||
secondaryColor: "#ffffff",
|
||
},
|
||
};
|
||
|
||
test("expands pattern × colorPalettes + null palette", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
// default + red + blue + null-palette
|
||
const patternItems = result.filter((r) =>
|
||
r.key.startsWith("pattern:stripes"),
|
||
);
|
||
expect(patternItems).toHaveLength(3);
|
||
expect(patternItems.map((r) => r.key)).toEqual([
|
||
"pattern:stripes:red",
|
||
"pattern:stripes:blue",
|
||
"pattern:stripes",
|
||
]);
|
||
});
|
||
|
||
test("resolves color palette from cosmetics.colorPalettes", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.colorPalette).toEqual(colorPalettes.red);
|
||
});
|
||
|
||
test("null palette entry has null colorPalette", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const nullPaletteItem = result.find((r) => r.key === "pattern:stripes");
|
||
expect(nullPaletteItem?.colorPalette).toBeNull();
|
||
});
|
||
|
||
test("pattern with no colorPalettes produces single null-palette entry", () => {
|
||
const noPalettePattern = { ...pattern, colorPalettes: undefined };
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: noPalettePattern as any },
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const patternItems = result.filter((r) =>
|
||
r.key.startsWith("pattern:stripes"),
|
||
);
|
||
expect(patternItems).toHaveLength(1);
|
||
expect(patternItems[0].key).toBe("pattern:stripes");
|
||
});
|
||
|
||
test("purchasable when user has no flares and product exists", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(), null);
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.relationship).toBe("purchasable");
|
||
});
|
||
|
||
test("owned when user has specific flare", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(
|
||
cosmetics,
|
||
makeUserMe(["pattern:stripes:red"]),
|
||
null,
|
||
);
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.relationship).toBe("owned");
|
||
});
|
||
|
||
test("owned when user has wildcard flare", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: pattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(
|
||
cosmetics,
|
||
makeUserMe(["pattern:*"]),
|
||
null,
|
||
);
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.relationship).toBe("owned");
|
||
});
|
||
|
||
test("blocked when affiliate code mismatch", () => {
|
||
const affiliatePattern = { ...pattern, affiliateCode: "partner1" };
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: affiliatePattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(), null);
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.relationship).toBe("blocked");
|
||
});
|
||
|
||
test("purchasable when affiliate code matches", () => {
|
||
const affiliatePattern = { ...pattern, affiliateCode: "partner1" };
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: affiliatePattern as any },
|
||
colorPalettes,
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(), "partner1");
|
||
const redItem = result.find((r) => r.key === "pattern:stripes:red");
|
||
expect(redItem?.relationship).toBe("purchasable");
|
||
});
|
||
|
||
test("archived palette is blocked unless owned", () => {
|
||
const archivedPattern = {
|
||
...pattern,
|
||
colorPalettes: [{ name: "old", isArchived: true }],
|
||
};
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: archivedPattern as any },
|
||
colorPalettes: {
|
||
old: {
|
||
name: "old",
|
||
primaryColor: "#111",
|
||
secondaryColor: "#222",
|
||
},
|
||
},
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(), null);
|
||
const oldItem = result.find((r) => r.key === "pattern:stripes:old");
|
||
expect(oldItem?.relationship).toBe("blocked");
|
||
});
|
||
|
||
test("archived palette is owned when user has specific flare", () => {
|
||
const archivedPattern = {
|
||
...pattern,
|
||
colorPalettes: [{ name: "old", isArchived: true }],
|
||
};
|
||
const cosmetics = makeCosmetics({
|
||
patterns: { stripes: archivedPattern as any },
|
||
colorPalettes: {
|
||
old: {
|
||
name: "old",
|
||
primaryColor: "#111",
|
||
secondaryColor: "#222",
|
||
},
|
||
},
|
||
});
|
||
const result = resolveCosmetics(
|
||
cosmetics,
|
||
makeUserMe(["pattern:stripes:old"]),
|
||
null,
|
||
);
|
||
const oldItem = result.find((r) => r.key === "pattern:stripes:old");
|
||
expect(oldItem?.relationship).toBe("owned");
|
||
});
|
||
});
|
||
|
||
describe("flags", () => {
|
||
const flag = {
|
||
type: "flag" as const,
|
||
name: "cool_flag",
|
||
url: "https://example.com/cool.png",
|
||
affiliateCode: null,
|
||
product,
|
||
priceSoft: undefined,
|
||
priceHard: undefined,
|
||
rarity: "rare",
|
||
};
|
||
|
||
test("includes flags with correct key", () => {
|
||
const cosmetics = makeCosmetics({
|
||
flags: { cool_flag: flag as any },
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const flagItem = result.find((r) => r.key === "flag:cool_flag");
|
||
expect(flagItem).toBeDefined();
|
||
expect(flagItem?.cosmetic).toEqual(flag);
|
||
expect(flagItem?.colorPalette).toBeNull();
|
||
});
|
||
|
||
test("purchasable when not logged in and product exists", () => {
|
||
const cosmetics = makeCosmetics({
|
||
flags: { cool_flag: flag as any },
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const flagItem = result.find((r) => r.key === "flag:cool_flag");
|
||
expect(flagItem?.relationship).toBe("purchasable");
|
||
});
|
||
|
||
test("owned with wildcard flare", () => {
|
||
const cosmetics = makeCosmetics({
|
||
flags: { cool_flag: flag as any },
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(["flag:*"]), null);
|
||
const flagItem = result.find((r) => r.key === "flag:cool_flag");
|
||
expect(flagItem?.relationship).toBe("owned");
|
||
});
|
||
|
||
test("owned with specific flare", () => {
|
||
const cosmetics = makeCosmetics({
|
||
flags: { cool_flag: flag as any },
|
||
});
|
||
const result = resolveCosmetics(
|
||
cosmetics,
|
||
makeUserMe(["flag:cool_flag"]),
|
||
null,
|
||
);
|
||
const flagItem = result.find((r) => r.key === "flag:cool_flag");
|
||
expect(flagItem?.relationship).toBe("owned");
|
||
});
|
||
|
||
test("blocked with no product", () => {
|
||
const freeFlag = { ...flag, product: null };
|
||
const cosmetics = makeCosmetics({
|
||
flags: { cool_flag: freeFlag as any },
|
||
});
|
||
const result = resolveCosmetics(cosmetics, makeUserMe(), null);
|
||
const flagItem = result.find((r) => r.key === "flag:cool_flag");
|
||
expect(flagItem?.relationship).toBe("blocked");
|
||
});
|
||
});
|
||
|
||
describe("mixed cosmetics", () => {
|
||
test("returns all types in order: default, patterns, flags", () => {
|
||
const cosmetics = makeCosmetics({
|
||
patterns: {
|
||
stripes: {
|
||
type: "pattern" as const,
|
||
name: "stripes",
|
||
pattern: "AAAAAA",
|
||
affiliateCode: null,
|
||
product,
|
||
priceSoft: null,
|
||
priceHard: null,
|
||
rarity: "common",
|
||
} as any,
|
||
},
|
||
flags: {
|
||
heart: {
|
||
type: "flag" as const,
|
||
name: "heart",
|
||
url: "/flags/heart.svg",
|
||
affiliateCode: null,
|
||
product,
|
||
priceSoft: null,
|
||
priceHard: null,
|
||
rarity: "common",
|
||
} as any,
|
||
},
|
||
});
|
||
const result = resolveCosmetics(cosmetics, false, null);
|
||
const keys = result.map((r) => r.key);
|
||
expect(keys[0]).toBe("pattern:default");
|
||
expect(keys).toContain("pattern:stripes");
|
||
expect(keys).toContain("flag:heart");
|
||
// patterns come before flags
|
||
const patternIdx = keys.indexOf("pattern:stripes");
|
||
const flagIdx = keys.indexOf("flag:heart");
|
||
expect(patternIdx).toBeLessThan(flagIdx);
|
||
});
|
||
});
|
||
});
|