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}5758beforeEach(() => {59 mounted = [];60 mockSendMessage.mockReset();61 mockSendMessage.mockImplementation(() => Promise.resolve({ ok: false }));62 (globalThis as unknown as { chrome: unknown }).chrome = {63 runtime: {64 onMessage: { addListener: () => undefined, removeListener: () => undefined },65 getURL: (path: string) => `chrome-extension://test/${path}`,66 },67 storage: {68 local: {69 get: () => Promise.resolve({}),70 set: () => Promise.resolve(),71 remove: () => Promise.resolve(),72 },73 session: {74 get: () => Promise.resolve({}),75 set: () => Promise.resolve(),76 remove: () => Promise.resolve(),77 },78 },79 };80});8182afterEach(() => {83 for (const view of mounted) view.dispose();84 mounted = [];85 vi.restoreAllMocks();86});8788describe("PB-26-208: lock-screen .peach paths bound the file before reading it", () => {89 it("drag/drop refuses an oversized file without reading it", async () => {90 const view = renderSymbolsRestore();91 await flushView();9293 const form = view.container.querySelector<HTMLElement>(".peach-restore-form");94 expect(form, "expected the symbols restore form").not.toBeNull();9596 const file = oversizedFile("vault.peach");97 const drop = new Event("drop", { bubbles: true, cancelable: true }) as Event & {98 dataTransfer?: { files: File[] };99 };100 drop.dataTransfer = { files: [file] };101 form!.dispatchEvent(drop);102 await flushView();103104 expect(file.readCalls).toBe(0);105 expect(view.container.textContent).toContain("too large");106 });107108 it("the file input refuses an oversized file without reading it", async () => {109 const view = renderSymbolsRestore();110 await flushView();111112 const picker = view.container.querySelector<HTMLInputElement>(113 'input[type="file"][accept=".peach,text/plain"]',114 );115 expect(picker, "expected a .peach file input").not.toBeNull();116117 const file = oversizedFile("vault.peach");118 Object.defineProperty(picker!, "files", {119 configurable: true,120 get: () => [file],121 });122 picker!.dispatchEvent(new Event("change", { bubbles: true }));123 await flushView();124125 expect(file.readCalls).toBe(0);126 expect(view.container.textContent).toContain("too large");127 });128});