Ctrl K
ComunidadGuías…

test/shared/currency.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { afterEach, beforeEach, describe, it as test } from "@std/testing/bdd";
import { ALL_SETTINGS_KEYS, settings } from "#db/settings.ts";
import {
  formatCurrency,
  formatSignedCurrency,
  getDecimalPlaces,
  toMajorUnits,
  toMinorUnits,
} from "#shared/currency.ts";
import { expectIncludesAll } from "#test-utils/assertions.ts";
import { createTestDbWithSetup, resetDb } from "#test-utils/db.ts";
import { setupTestEncryptionKey } from "#test-utils/env.ts";
import { testWithSetting } from "#test-utils/settings.ts";

describe("currency", () => {
  describe("getDecimalPlaces", () => {
    test("returns 2 for GBP", () => {
      expect(getDecimalPlaces("GBP")).toBe(2);
    });

    test("returns 2 for USD", () => {
      expect(getDecimalPlaces("USD")).toBe(2);
    });

    test("returns 2 for EUR", () => {
      expect(getDecimalPlaces("EUR")).toBe(2);
    });

    test("returns 0 for JPY", () => {
      expect(getDecimalPlaces("JPY")).toBe(0);
    });

    test("returns 3 for KWD", () => {
      expect(getDecimalPlaces("KWD")).toBe(3);
    });

    test("throws when Intl omits its guaranteed decimal places", () => {
      const original = Intl.NumberFormat.prototype.resolvedOptions;
      Intl.NumberFormat.prototype.resolvedOptions = function () {
        return {
          ...original.call(this),
          minimumFractionDigits: undefined as unknown as number,
        };
      };
      try {
        expect(() => getDecimalPlaces("CAD")).toThrow(
          "Intl omitted currency decimal places for CAD",
        );
      } finally {
        Intl.NumberFormat.prototype.resolvedOptions = original;
      }
    });
  });

  describe("formatCurrency", () => {
    testWithSetting(
      "formats GBP with pound symbol",
      { currency: "GBP" },
      () => {
        expect(formatCurrency(1050)).toBe("£10.50");
      },
    );

    testWithSetting("formats GBP zero amount", { currency: "GBP" }, () => {
      expect(formatCurrency(0)).toBe("£0");
    });

    testWithSetting(
      "does not sign a negative zero",
      { currency: "GBP" },
      () => {
        expect(formatCurrency(-0)).toBe("£0");
      },
    );

    testWithSetting(
      "formats USD with dollar symbol",
      { currency: "USD" },
      () => {
        expect(formatCurrency(1050)).toBe("$10.50");
      },
    );

    testWithSetting("formats EUR with euro symbol", { currency: "EUR" }, () => {
      const result = formatCurrency(1050);
      expectIncludesAll(result, ["10.50", "€"]);
    });

    testWithSetting(
      "formats JPY without decimal places",
      { currency: "JPY" },
      () => {
        expect(formatCurrency(1050)).toBe("¥1,050");
      },
    );

    testWithSetting(
      "formats KWD with 3 decimal places",
      { currency: "KWD" },
      () => {
        const result = formatCurrency(1050);
        assertStringIncludes(result, "1.050");
      },
    );

    testWithSetting("accepts string input", { currency: "GBP" }, () => {
      expect(formatCurrency("1050")).toBe("£10.50");
    });

    test("falls back to GBP when no currency loaded", () => {
      expect(formatCurrency(1050)).toBe("£10.50");
    });
  });

  describe("formatSignedCurrency", () => {
    testWithSetting(
      "uses one sign style and leaves zero unsigned",
      { currency: "GBP" },
      () => {
        expect(formatSignedCurrency(1250)).toBe("+£12.50");
        expect(formatSignedCurrency(-1250)).toBe("−£12.50");
        expect(formatSignedCurrency(0)).toBe("£0");
        // The sign follows the value's own sign, not whether it reaches a
        // whole minor unit, so a sliver still reads as added or removed.
        expect(formatSignedCurrency(0.5)).toBe("+£0.01");
        expect(formatSignedCurrency(-0.5)).toBe("−£0.01");
        expect(formatSignedCurrency(1250, false)).toBe("£12.50");
        expect(formatSignedCurrency(-1250, false)).toBe("−£12.50");
      },
    );
  });

  describe("toMinorUnits", () => {
    testWithSetting(
      "converts GBP major to minor units",
      { currency: "GBP" },
      () => {
        expect(toMinorUnits(10.5)).toBe(1050);
      },
    );

    testWithSetting("converts whole number", { currency: "GBP" }, () => {
      expect(toMinorUnits(25)).toBe(2500);
    });

    testWithSetting("rounds to nearest integer", { currency: "GBP" }, () => {
      expect(toMinorUnits(10.999)).toBe(1100);
    });

    testWithSetting("converts JPY (no decimals)", { currency: "JPY" }, () => {
      expect(toMinorUnits(1050)).toBe(1050);
    });

    testWithSetting("converts KWD (3 decimals)", { currency: "KWD" }, () => {
      expect(toMinorUnits(1.05)).toBe(1050);
    });
  });

  describe("toMajorUnits", () => {
    testWithSetting(
      "converts GBP minor to major units string",
      { currency: "GBP" },
      () => {
        expect(toMajorUnits(1050)).toBe("10.50");
      },
    );

    testWithSetting("converts zero", { currency: "GBP" }, () => {
      expect(toMajorUnits(0)).toBe("0.00");
    });

    testWithSetting("converts JPY (no decimals)", { currency: "JPY" }, () => {
      expect(toMajorUnits(1050)).toBe("1050");
    });

    testWithSetting("converts KWD (3 decimals)", { currency: "KWD" }, () => {
      expect(toMajorUnits(1050)).toBe("1.050");
    });
  });

  describe("settings.currency integration", () => {
    beforeEach(async () => {
      setupTestEncryptionKey();
      await createTestDbWithSetup("US");
      await settings.loadKeys(ALL_SETTINGS_KEYS);
    });

    afterEach(() => {
      resetDb();
    });

    test("reads currency from settings", () => {
      expect(settings.currency).toBe("USD");
    });

    test("uses settings currency for formatting", () => {
      expect(formatCurrency(1050)).toBe("$10.50");
    });
  });
});