123456789101112import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";1314const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));15vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));1617import { IMPORT_LIMITS } from "../../shared/import-limits";18import { ImportView } from "./Import";19import { flushView, renderView } from "./view-test-utils";2021let mounted: Array<{ dispose: () => void }> = [];222324252627function oversizedFile(name: string): File & { readCalls: number } {28 const file = {29 name,30 size: IMPORT_LIMITS.MAX_JSON_BYTES + 1,31 type: "text/csv",32 readCalls: 0,33 async text() {34 file.readCalls += 1;35 return "";36 },37 async arrayBuffer() {38 file.readCalls += 1;39 return new ArrayBuffer(0);40 },41 } as unknown as File & { readCalls: number };42 return file;43}4445function attachOversized(input: HTMLInputElement, file: File): void {46 Object.defineProperty(input, "files", {47 configurable: true,48 get: () => [file],49 });50}5152beforeEach(() => {53 mounted = [];54 mockSendMessage.mockReset();55 mockSendMessage.mockImplementation(() => Promise.resolve({ ok: true, data: null }));56 (globalThis as unknown as { chrome: unknown }).chrome = {57 runtime: {58 onMessage: { addListener: () => undefined, removeListener: () => undefined },59 getURL: (path: string) => `chrome-extension://test/${path}`,60 },61 storage: {62 local: {63 get: () => Promise.resolve({}),64 set: () => Promise.resolve(),65 remove: () => Promise.resolve(),66 },67 session: {68 get: () => Promise.resolve({}),69 set: () => Promise.resolve(),70 remove: () => Promise.resolve(),71 },72 },73 };74});7576afterEach(() => {77 for (const view of mounted) view.dispose();78 mounted = [];79 vi.restoreAllMocks();80});8182describe("PB-26-112: import branches bound the file before reading it", () => {83 it("the CSV/JSON picker refuses an oversized file without reading it", async () => {84 const view = renderView(() => (85 <ImportView initialView="file" onBack={() => undefined} onImport={() => undefined} />86 ));87 mounted.push(view);88 await flushView();8990 const picker = view.container.querySelector<HTMLInputElement>(91 'input[type="file"][accept=".csv,.json,.peach,application/x-peach"]',92 );93 expect(picker, "expected a CSV/JSON file input").not.toBeNull();9495 const file = oversizedFile("huge.csv");96 attachOversized(picker!, file);97 picker!.dispatchEvent(new Event("change", { bubbles: true }));98 await flushView();99100 expect(file.readCalls).toBe(0);101 expect(view.container.textContent).toContain("too large");102 });103104 it("the .peach picker refuses an oversized file without reading it", async () => {105 const view = renderView(() => (106 <ImportView initialView="peachscript" onBack={() => undefined} onImport={() => undefined} />107 ));108 mounted.push(view);109 await flushView();110111 const picker = view.container.querySelector<HTMLInputElement>(112 'input[type="file"][accept=".peach,application/x-peach,text/plain"]',113 );114 expect(picker, "expected a .peach file input").not.toBeNull();115116 const file = oversizedFile("huge.peach");117 attachOversized(picker!, file);118 picker!.dispatchEvent(new Event("change", { bubbles: true }));119 await flushView();120121 expect(file.readCalls).toBe(0);122 expect(view.container.textContent).toContain("too large");123 });124});