1234567import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";89const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));10vi.mock("../App", () => ({ sendMessage: mockSendMessage }));11vi.mock("../components/RuneBackground", () => ({12 RuneBackground: () => null,13}));14vi.mock("../components/RuneCanvas", () => ({ RuneCanvas: () => null }));1516import { IMPORT_LIMITS } from "../../shared/import-limits";17import { LockView } from "./Lock";18import { flushView, renderView } from "./view-test-utils";1920let mounted: Array<{ dispose: () => void }> = [];212223242526function oversizedFile(name: string): File & { readCalls: number } {27 const file = {28 name,29 size: IMPORT_LIMITS.MAX_JSON_BYTES + 1,30 type: "text/plain",31 readCalls: 0,32 async text() {33 file.readCalls += 1;34 return "";35 },36 async arrayBuffer() {37 file.readCalls += 1;38 return new ArrayBuffer(0);39 },40 } as unknown as File & { readCalls: number };41 return file;42}4344function renderSymbolsRestore() {45 const view = renderView(() => (46 <LockView47 hasVault={true}48 ready={true}49 onUnlock={() => undefined}50 initialRestore={true}51 initialRestoreSource="symbols"52 />53 ));54 mounted.push(view);55 return view;56}5758function renderCsvRestore() {59 const view = renderView(() => (60 <LockView61 hasVault={true}62 ready={true}63 onUnlock={() => undefined}64 initialRestore={true}65 initialRestoreSource="csv"66 />67 ));68 mounted.push(view);69 return view;70}7172beforeEach(() => {73 mounted = [];74 mockSendMessage.mockReset();75 mockSendMessage.mockImplementation(() => Promise.resolve({ ok: false }));76 (globalThis as unknown as { chrome: unknown }).chrome = {77 runtime: {78 onMessage: { addListener: () => undefined, removeListener: () => undefined },79 getURL: (path: string) => `chrome-extension://test/${path}`,80 },81 storage: {82 local: {83 get: () => Promise.resolve({}),84 set: () => Promise.resolve(),85 remove: () => Promise.resolve(),86 },87 session: {88 get: () => Promise.resolve({}),89 set: () => Promise.resolve(),90 remove: () => Promise.resolve(),91 },92 },93 };94});9596afterEach(() => {97 for (const view of mounted) view.dispose();98 mounted = [];99 vi.restoreAllMocks();100});101102describe("PB-26-310: lock-screen CSV/JSON paths bound the file before reading it", () => {103 it("the CSV/JSON picker refuses an oversized file without reading it", async () => {104 const view = renderCsvRestore();105 await flushView();106107 const picker = view.container.querySelector<HTMLInputElement>(108 'input[type="file"][accept=".csv,.json,.peach,application/x-peach"]',109 );110 expect(picker, "expected a CSV/JSON file input").not.toBeNull();111112 const file = oversizedFile("huge.csv");113 Object.defineProperty(picker!, "files", {114 configurable: true,115 get: () => [file],116 });117 picker!.dispatchEvent(new Event("change", { bubbles: true }));118 await flushView();119120 expect(file.readCalls).toBe(0);121 expect(view.container.textContent).toContain("too large");122 });123124 it("CSV/JSON drag/drop refuses an oversized file without reading it", async () => {125 const view = renderCsvRestore();126 await flushView();127128 const form = view.container.querySelector<HTMLElement>(".peach-restore-form");129 expect(form, "expected the CSV/JSON restore form").not.toBeNull();130131 const file = oversizedFile("huge.json");132 const drop = new Event("drop", { bubbles: true, cancelable: true }) as Event & {133 dataTransfer?: { files: File[] };134 };135 drop.dataTransfer = { files: [file] };136 form!.dispatchEvent(drop);137 await flushView();138139 expect(file.readCalls).toBe(0);140 expect(view.container.textContent).toContain("too large");141 });142});143144describe("PB-26-208: lock-screen .peach paths bound the file before reading it", () => {145 it("drag/drop refuses an oversized file without reading it", async () => {146 const view = renderSymbolsRestore();147 await flushView();148149 const form = view.container.querySelector<HTMLElement>(".peach-restore-form");150 expect(form, "expected the symbols restore form").not.toBeNull();151152 const file = oversizedFile("vault.peach");153 const drop = new Event("drop", { bubbles: true, cancelable: true }) as Event & {154 dataTransfer?: { files: File[] };155 };156 drop.dataTransfer = { files: [file] };157 form!.dispatchEvent(drop);158 await flushView();159160 expect(file.readCalls).toBe(0);161 expect(view.container.textContent).toContain("too large");162 });163164 it("the file input refuses an oversized file without reading it", async () => {165 const view = renderSymbolsRestore();166 await flushView();167168 const picker = view.container.querySelector<HTMLInputElement>(169 'input[type="file"][accept=".peach,text/plain"]',170 );171 expect(picker, "expected a .peach file input").not.toBeNull();172173 const file = oversizedFile("vault.peach");174 Object.defineProperty(picker!, "files", {175 configurable: true,176 get: () => [file],177 });178 picker!.dispatchEvent(new Event("change", { bubbles: true }));179 await flushView();180181 expect(file.readCalls).toBe(0);182 expect(view.container.textContent).toContain("too large");183 });184});