Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions client/src/hooks/__tests__/useCardImage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,86 @@ describe("useCardImage", () => {
});
expect(result.current.src).toBe("second.png");
});
it("resolves a face-down marker from tokenImageRef alone — no name, no oracle id (#7549)", async () => {
// The #7535 marker request shape: cardName "" and only the ref naming the
// printing. The hook must NOT short-circuit on the empty name — that
// short-circuit is exactly what kept the merged marker feature from ever
// loading in the live client (the component tests stubbed this hook, so
// only a REAL-hook regression can hold the line).
vi.stubGlobal("fetch", vi.fn((url: string) => {
if (url === "/scryfall-token-images.json") {
return Promise.resolve(jsonResponse({
"oracle:8f92f8d7-ec89-426f-86dc-fbc259eb5559:morph": {
scryfall_id: "morph-token-dtk",
oracle_id: "8f92f8d7-ec89-426f-86dc-fbc259eb5559",
face_names: ["morph"],
faces: [{ normal: "https://img.example/morph.jpg", art_crop: "https://img.example/morph-art.jpg" }],
name: "Morph",
layout: "token",
},
}));
}
return Promise.resolve(jsonResponse({}));
}));

const { useCardImage } = await import("../useCardImage");
const { result } = renderHook(() =>
useCardImage("", {
size: "normal",
isToken: true,
tokenImageRef: {
scryfall_id: "",
scryfall_oracle_id: "8f92f8d7-ec89-426f-86dc-fbc259eb5559",
face_name: "morph",
preset_id: "face-down-morph",
},
}),
);

await waitFor(() => expect(result.current.src).toBe("https://img.example/morph.jpg"));
});

it("fetches nothing for a token ref that names no printing — both ids empty (#7550 review)", async () => {
// A `TokenImageRef` is only a pointer when it carries at least one id.
// With BOTH `scryfall_id` and `scryfall_oracle_id` empty there is nothing
// to resolve — the request must short-circuit exactly like the empty-name
// case, not fall through to a `fetchTokenImageUrl("")` junk search.
const fetchTokenImageByRef = vi.fn().mockResolvedValue(null);
const fetchTokenImageUrl = vi.fn().mockResolvedValue(null);
vi.doMock("../../services/scryfall.ts", () => ({
fetchCardImageAsset: vi.fn(),
fetchCardImageAssetByOracleId: vi.fn(),
fetchCardImageByOracleId: vi.fn(),
fetchCardImageUrl: vi.fn(),
fetchTokenImageByRef,
fetchTokenImageUrl,
findPrintingById: vi.fn(),
getCardPrintings: vi.fn().mockResolvedValue([]),
isCardImageRotatedSync: vi.fn().mockReturnValue(false),
isLocaleArtReady: vi.fn().mockReturnValue(true),
loadLocaleArt: vi.fn().mockResolvedValue(new Map()),
resolveFaceIndexSync: vi.fn().mockReturnValue(null),
resolveOracleIdSync: vi.fn().mockReturnValue(null),
resolvePrintingImageUrl: vi.fn(),
}));

const { useCardImage } = await import("../useCardImage");
const { result } = renderHook(() =>
useCardImage("", {
size: "normal",
isToken: true,
tokenImageRef: {
scryfall_id: "",
scryfall_oracle_id: "",
preset_id: "face-down-morph",
},
}),
);

await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.src).toBeNull();
expect(fetchTokenImageByRef).not.toHaveBeenCalled();
expect(fetchTokenImageUrl).not.toHaveBeenCalled();
});

});
19 changes: 17 additions & 2 deletions client/src/hooks/useCardImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,16 @@ export function useCardImage(
// blinding the dependency check on the large effect below.
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableTokenImageRef = useMemo(() => tokenImageRef, [tokenImageRefKey]);
// A token ref is only a pointer when it names a printing: with BOTH ids
// empty there is nothing to resolve, so such a ref must not hold the
// empty-name guards open — the request would fall through to a
// `fetchTokenImageUrl("")` junk search. Our face-down markers carry only an
// oracle id (empty `scryfall_id`), so either id keeps the guard open.
const resolvableTokenImageRef =
stableTokenImageRef &&
(stableTokenImageRef.scryfall_id || stableTokenImageRef.scryfall_oracle_id)
? stableTokenImageRef
: null;
const oracleId = options?.oracleId ?? "";
const faceName = options?.faceName ?? "";
const scryfallId = options?.scryfallId ?? "";
Expand Down Expand Up @@ -572,7 +582,11 @@ export function useCardImage(
return;
}

if (!cardName && !oracleId) {
// A face-down marker request carries NO name and NO oracle id — only the
// `tokenImageRef` names the printing. Bailing on the empty name here was
// what kept the #7535 markers from ever loading at runtime (#7549): the
// ref-driven fetch below never ran.
if (!cardName && !oracleId && !resolvableTokenImageRef) {
setStateRequestKey(requestKey);
setSrc(null);
setIsRotated(false);
Expand Down Expand Up @@ -642,6 +656,7 @@ export function useCardImage(
filterPower,
filterSubtypes,
filterToughness,
resolvableTokenImageRef,
stableTokenImageRef,
tokenImageRefKey,
isToken,
Expand All @@ -666,7 +681,7 @@ export function useCardImage(
isFlip: isCardImageFlipLayoutSync(resolvedOracleId, cardName),
};
}
if (!cardName && !oracleId) {
if (!cardName && !oracleId && !resolvableTokenImageRef) {
return { src: null, isLoading: false, isRotated: false, isFlip: false };
}
const cachedEntry = imageRequestCache.get(requestKey);
Expand Down
Loading