import { describe, it, expect } from "vitest"; import { priceCeilingScenario, type PriceCeilingParams, } from "../../src/scenarios/micro/consumer/price_ceiling"; describe("Scenario Module 2: Giá trần và thiếu hụt nguồn cung (Price Ceiling)", () => { const vietnameseCharRegex = /[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđĐ]/i; const illegalDashRegex = /[-–—]/; it("Metadata tuân thủ chuẩn FSPPM và trích dẫn Pindyck & Rubinfeld", () => { const meta = priceCeilingScenario.meta; expect(meta.id).toBe("micro.consumer.price_ceiling"); expect(meta.domain).toBe("micro"); expect(meta.category).toBe("consumer"); expect(meta.chapter).toBe("PR Ch.3 • Câu 10b"); expect(meta.title).toBe("Giá trần và thiếu hụt nguồn cung"); expect(meta.notebook_id).toBe("e8a30630-0df2-4932-8740-9ffe7d8882bc"); expect(meta.source).toContain("Pindyck, R. S., & Rubinfeld, D. L. (2015)"); expect(meta.source).toContain("Chương 3, Câu 10b, tr. 107"); expect(meta.inputBanner?.length).toBe(5); }); it("Khai báo đầy đủ 3 presets chuẩn hóa", () => { const presets = priceCeilingScenario.presets; expect(presets).toBeDefined(); expect(presets?.better_off.params).toEqual({ pFixed: 5, xCap: 6 }); expect(presets?.worse_off.params).toEqual({ pFixed: 5, xCap: 4 }); expect(presets?.breakeven.params).toEqual({ pFixed: 5, xCap: 5.86 }); }); describe("Bất biến toán học (Model Invariants)", () => { it("Preset better_off (Px' = 5, X = 6): B(6, 17), U = 102 > 100 (Khấm khá hơn)", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 6 }; const res = priceCeilingScenario.model(params); expect(res.optA.x).toBe(10); expect(res.optA.y).toBe(10); expect(res.optA.utility).toBe(100); expect(res.optB.x).toBe(6); expect(res.optB.y).toBe(17); expect(res.optB.utility).toBe(102); expect(res.isBetterOff).toBe(true); expect(res.isWorseOff).toBe(false); expect(res.deltaU).toBe(2); expect(res.desiredX).toBe(20); }); it("Preset worse_off (Px' = 5, X = 4): B(4, 18), U = 72 < 100 (Thiệt thòi hơn)", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 4 }; const res = priceCeilingScenario.model(params); expect(res.optB.x).toBe(4); expect(res.optB.y).toBe(18); expect(res.optB.utility).toBe(72); expect(res.isBetterOff).toBe(false); expect(res.isWorseOff).toBe(true); expect(res.deltaU).toBe(-28); }); it("Preset breakeven (Px' = 5, X = 5.86): U xấp xỉ 100 (Hòa vốn thỏa dụng)", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 5.86 }; const res = priceCeilingScenario.model(params); expect(res.breakevenX).toBeCloseTo(20 - Math.sqrt(200), 4); expect(res.optB.utility).toBeCloseTo(100, 0); expect(res.isBreakeven).toBe(true); expect(res.isBetterOff).toBe(false); expect(res.isWorseOff).toBe(false); }); }); describe("Cấu trúc View và Khung trực quan hóa", () => { it("Xuất đầy đủ các lớp đồ thị và vạch hòa vốn X*", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 6 }; const result = priceCeilingScenario.model(params); const view = priceCeilingScenario.view(result, params); // Graph Spec expect(view.graph.xAxis.max).toBe(28); expect(view.graph.yAxis.max).toBe(26); expect(view.graph.regions?.length).toBeGreaterThan(0); expect(view.graph.polylines?.length).toBe(3); // orig, unconstrained, effective expect(view.graph.curves?.length).toBe(2); // U0 & UB // GuideLayer chứa vạch dọc hòa vốn const breakevenGuide = view.graph.guides?.find( (g) => g.id === "guide-breakeven" ); expect(breakevenGuide).toBeDefined(); expect(breakevenGuide?.verticalX).toBeCloseTo(result.breakevenX, 4); // Điểm B* mong muốn hiển thị khi desiredX <= 26 const ptBStar = view.graph.points?.find((p) => p.id === "pt-b-star"); expect(ptBStar).toBeDefined(); expect(ptBStar?.x).toBe(20); // Stats expect(view.stats.length).toBe(4); expect(view.stats.find((s) => s.id === "breakeven")).toBeDefined(); // StatusBanner tương ứng expect(view.statusBanner?.type).toBe("success"); // BridgeLinks expect(view.bridgeLinks?.length).toBe(3); expect( view.bridgeLinks?.some( (l) => l.targetScenarioId === "micro.consumer.rationing" ) ).toBe(true); }); it("Khi worse_off, statusBanner chuyển sang danger", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 4 }; const result = priceCeilingScenario.model(params); const view = priceCeilingScenario.view(result, params); expect(view.statusBanner?.type).toBe("danger"); }); }); describe("Quy tắc Typography tiếng Việt trong View", () => { it("Không chứa bất kỳ dấu gạch nối/ngang sai quy tắc nào trong các chuỗi tiếng Việt của View", () => { const params: PriceCeilingParams = { pFixed: 5, xCap: 6 }; const result = priceCeilingScenario.model(params); const view = priceCeilingScenario.view(result, params); const inspectStrings = (obj: unknown, path: string): void => { if (typeof obj === "string") { if (vietnameseCharRegex.test(obj)) { if (illegalDashRegex.test(obj)) { expect.fail( `Phát hiện chuỗi tiếng Việt chứa ký tự gạch nối sai quy tắc tại ${path}: "${obj}"` ); } } } else if (Array.isArray(obj)) { obj.forEach((item, idx) => inspectStrings(item, `${path}[${idx}]`)); } else if (obj !== null && typeof obj === "object") { Object.entries(obj).forEach(([key, val]) => { if (key !== "id" && key !== "symbol") { inspectStrings(val, `${path}.${key}`); } }); } }; inspectStrings(view, "view"); }); }); });