diff --git a/src/ds/money.test.ts b/src/ds/money.test.ts new file mode 100644 index 0000000..7ebd34d --- /dev/null +++ b/src/ds/money.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { tugrik, tugrikShort, grouped, tugrikRaw, isHidden, setHidden, MASKED } from "./money"; + +beforeEach(() => { try { localStorage.clear(); } catch {} }); + +describe("money", () => { + it("groups tugrik", () => expect(tugrik("452200")).toBe("452,200₮")); + it("groups digits only", () => expect(grouped("500000")).toBe("500,000")); + it("abbreviates millions", () => expect(tugrikShort("1200000")).toBe("1,2сая₮")); + it("keeps sub-million as full", () => expect(tugrikShort("900000")).toBe("900,000₮")); + it("masks when hidden", () => { setHidden(true); expect(tugrik("100")).toBe(MASKED); }); + it("raw never masks", () => { setHidden(true); expect(tugrikRaw("100")).toBe("100₮"); }); + it("isHidden reflects storage", () => { setHidden(true); expect(isHidden()).toBe(true); }); +}); diff --git a/src/ds/money.ts b/src/ds/money.ts new file mode 100644 index 0000000..747b011 --- /dev/null +++ b/src/ds/money.ts @@ -0,0 +1,23 @@ +export const MASKED = "••••••"; +const KEY = "mercury.hideAmounts"; + +export function isHidden(): boolean { + try { return localStorage.getItem(KEY) === "true"; } catch { return false; } +} +export function setHidden(b: boolean): void { + try { localStorage.setItem(KEY, String(b)); } catch {} +} +function num(v: string | number): number { + return typeof v === "number" ? v : parseFloat(v || "0") || 0; +} +export function grouped(v: string | number): string { + return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(num(v)); +} +export function tugrikRaw(v: string | number): string { return `${grouped(v)}₮`; } +export function tugrikShortRaw(v: string | number): string { + const m = num(v) / 1_000_000; + if (m >= 1) return `${m.toFixed(1).replace(".", ",")}сая₮`; + return tugrikRaw(v); +} +export function tugrik(v: string | number): string { return isHidden() ? MASKED : tugrikRaw(v); } +export function tugrikShort(v: string | number): string { return isHidden() ? MASKED : tugrikShortRaw(v); } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..643bb3b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +export default defineConfig({ + plugins: [react()], + test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"], globals: true }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..2a9307e --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,24 @@ +import "@testing-library/jest-dom/vitest"; + +// Polyfill localStorage for jsdom if not available +if (typeof localStorage === "undefined") { + const store: Record = {}; + (global as any).localStorage = { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = String(value); + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + for (const key in store) { + delete store[key]; + } + }, + key: (index: number) => Object.keys(store)[index] || null, + get length() { + return Object.keys(store).length; + }, + }; +}