diff --git a/src/services/__tests__/firefox-api.test.ts b/src/services/__tests__/firefox-api.test.ts new file mode 100644 index 0000000..7458131 --- /dev/null +++ b/src/services/__tests__/firefox-api.test.ts @@ -0,0 +1,53 @@ +import { afterAll, beforeEach, describe, expect, it, spyOn } from "bun:test"; + +import { createFirefoxApi } from "../firefox-api"; + +const fetchSpy = spyOn(globalThis, "fetch"); + +const addonResponse = (overrides: Record = {}) => ({ + id: 123, + slug: "example-addon", + guid: "example-addon@example.com", + icon_url: "https://addons.mozilla.org/user-media/addon_icons/0/123-64.png", + last_updated: "2026-09-30T00:00:00Z", + description: { "en-US": "Long description" }, + name: { "en-US": "Example Addon" }, + summary: { "en-US": "Short description" }, + ratings: { average: 5, bayesian_average: 5, count: 1, text_count: 1 }, + url: "https://addons.mozilla.org/en-US/firefox/addon/example-addon/", + current_version: { version: "1.0.0" }, + average_daily_users: 3, + previews: [], + ...overrides, +}); + +describe("Firefox API", () => { + beforeEach(() => { + fetchSpy.mockReset(); + }); + + afterAll(() => { + fetchSpy.mockRestore(); + }); + + it("should return the first localized value for each text field", async () => { + fetchSpy.mockResolvedValue(Response.json(addonResponse())); + + const addon = await createFirefoxApi().getAddon("example-addon"); + + expect(addon.name).toBe("Example Addon"); + expect(addon.shortDescription).toBe("Short description"); + expect(addon.longDescription).toBe("Long description"); + }); + + // AMO returns `null` instead of a locale map when the listing leaves a field empty + it("should return an empty string when a localized field is null", async () => { + fetchSpy.mockResolvedValue(Response.json(addonResponse({ description: null, summary: null }))); + + const addon = await createFirefoxApi().getAddon("example-addon"); + + expect(addon.name).toBe("Example Addon"); + expect(addon.shortDescription).toBe(""); + expect(addon.longDescription).toBe(""); + }); +}); diff --git a/src/services/firefox-api.ts b/src/services/firefox-api.ts index ceac978..52f4fa1 100644 --- a/src/services/firefox-api.ts +++ b/src/services/firefox-api.ts @@ -11,17 +11,21 @@ export interface FirefoxApi { } export function createFirefoxApi(): FirefoxApi { + // AMO returns `null` instead of a locale map when a listing leaves a field empty + const firstLocalizedValue = (value: { [locale: string]: string } | null): string => + Object.values(value ?? {})[0] ?? ""; + const toGqlFirefoxAddon = (res: GetAddon200Response): Gql.FirefoxAddon => ({ id: String(res.id), slug: res.slug, guid: res.guid, iconUrl: res.icon_url, lastUpdated: res.last_updated, - longDescription: Object.values(res.description)[0]!, - name: Object.values(res.name)[0]!, + longDescription: firstLocalizedValue(res.description), + name: firstLocalizedValue(res.name), rating: res.ratings.average, reviewCount: res.ratings.count, - shortDescription: Object.values(res.summary)[0]!, + shortDescription: firstLocalizedValue(res.summary), storeUrl: res.url, version: res.current_version.version, users: res.average_daily_users, @@ -105,7 +109,7 @@ type GetAddon200Response = { default_locale: string; description: { [locale: string]: string; - }; + } | null; developer_comments: string | null; edit_url: string; guid: string; @@ -153,7 +157,7 @@ type GetAddon200Response = { status: string; summary: { [locale: string]: string; - }; + } | null; support_email: { [locale: string]: string; };