MAESTRO: add red tests for header input flow

This commit is contained in:
Mariusz Banach
2026-02-18 00:26:06 +01:00
parent da3f2da210
commit 1908ed1d33
4 changed files with 358 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import type { ReactElement } from "react";
import { act } from "react-dom/test-utils";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import AnalyseButton from "../components/AnalyseButton";
type RenderResult = {
container: HTMLDivElement;
};
const cleanups: Array<() => void> = [];
const render = (ui: ReactElement): RenderResult => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
cleanups.push(() => {
act(() => {
root.unmount();
});
container.remove();
});
return { container };
};
const getButton = (container: HTMLElement): HTMLButtonElement => {
const button = container.querySelector("button");
if (!button) {
throw new Error("Expected analyse button to be rendered.");
}
return button as HTMLButtonElement;
};
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
if (cleanup) {
cleanup();
}
}
});
describe("AnalyseButton", () => {
it("renders an analyse button", () => {
const { container } = render(<AnalyseButton hasInput onAnalyse={() => undefined} />);
const button = getButton(container);
expect(button.textContent ?? "").toMatch(/analyse/i);
});
it("disables itself when input is empty", () => {
const { container } = render(<AnalyseButton hasInput={false} onAnalyse={() => undefined} />);
const button = getButton(container);
expect(button.disabled).toBe(true);
});
it("triggers analyse on Ctrl+Enter", () => {
const handleAnalyse = vi.fn();
render(<AnalyseButton hasInput onAnalyse={handleAnalyse} />);
act(() => {
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", ctrlKey: true }));
});
expect(handleAnalyse).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,131 @@
import type { ReactElement } from "react";
import { act } from "react-dom/test-utils";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import FileDropZone from "../components/FileDropZone";
type RenderResult = {
container: HTMLDivElement;
};
const cleanups: Array<() => void> = [];
const render = (ui: ReactElement): RenderResult => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
cleanups.push(() => {
act(() => {
root.unmount();
});
container.remove();
});
return { container };
};
const getDropZone = (container: HTMLElement): HTMLElement => {
const zone = container.querySelector('[data-testid="file-drop-zone"]');
if (!zone) {
throw new Error("Expected file drop zone to be rendered.");
}
return zone as HTMLElement;
};
const createDropEvent = (files: File[]): Event => {
const event = new Event("drop", { bubbles: true });
const items = files.map((file) => ({
kind: "file",
type: file.type,
getAsFile: () => file,
}));
Object.defineProperty(event, "dataTransfer", {
value: {
files,
items,
types: ["Files"],
},
});
return event;
};
const mockFileReader = (result: string): (() => void) => {
const original = globalThis.FileReader;
class MockFileReader {
result: string | ArrayBuffer | null = null;
onload: ((this: FileReader, ev: ProgressEvent<FileReader>) => void) | null = null;
onerror: ((this: FileReader, ev: ProgressEvent<FileReader>) => void) | null = null;
readAsText() {
this.result = result;
if (this.onload) {
this.onload(new ProgressEvent("load"));
}
}
}
globalThis.FileReader = MockFileReader as unknown as typeof FileReader;
return () => {
globalThis.FileReader = original;
};
};
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
if (cleanup) {
cleanup();
}
}
});
describe("FileDropZone", () => {
it("renders a drop zone for EML/TXT files", () => {
const { container } = render(<FileDropZone onFileContent={() => undefined} />);
expect(getDropZone(container)).toBeTruthy();
});
it("reads dropped EML/TXT file content", () => {
const handleContent = vi.fn();
const restore = mockFileReader("Header from file");
const { container } = render(<FileDropZone onFileContent={handleContent} />);
const dropZone = getDropZone(container);
const file = new File(["Header from file"], "sample.eml", { type: "message/rfc822" });
act(() => {
dropZone.dispatchEvent(createDropEvent([file]));
});
restore();
expect(handleContent).toHaveBeenCalledWith("Header from file");
});
it("rejects unsupported file types with feedback", () => {
const handleContent = vi.fn();
const { container } = render(<FileDropZone onFileContent={handleContent} />);
const dropZone = getDropZone(container);
const file = new File(["irrelevant"], "payload.pdf", { type: "application/pdf" });
act(() => {
dropZone.dispatchEvent(createDropEvent([file]));
});
expect(handleContent).not.toHaveBeenCalled();
const alert = container.querySelector('[role="alert"]');
expect(alert).not.toBeNull();
expect(alert?.textContent ?? "").toMatch(/\.eml/i);
expect(alert?.textContent ?? "").toMatch(/\.txt/i);
});
});

View File

@@ -0,0 +1,108 @@
import type { ReactElement } from "react";
import { act } from "react-dom/test-utils";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import HeaderInput from "../components/HeaderInput";
type RenderResult = {
container: HTMLDivElement;
};
const cleanups: Array<() => void> = [];
const render = (ui: ReactElement): RenderResult => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
cleanups.push(() => {
act(() => {
root.unmount();
});
container.remove();
});
return { container };
};
const getTextarea = (container: HTMLElement): HTMLTextAreaElement => {
const textarea = container.querySelector("textarea");
if (!textarea) {
throw new Error("Expected textarea to be rendered.");
}
return textarea as HTMLTextAreaElement;
};
const getAlert = (container: HTMLElement): HTMLElement | null =>
container.querySelector("[role=\"alert\"]");
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
if (cleanup) {
cleanup();
}
}
});
describe("HeaderInput", () => {
it("renders a textarea for SMTP headers", () => {
const { container } = render(<HeaderInput value="" onChange={() => undefined} />);
expect(getTextarea(container)).toBeTruthy();
});
it("accepts pasted header text", () => {
const handleChange = vi.fn();
const { container } = render(<HeaderInput value="" onChange={handleChange} />);
const textarea = getTextarea(container);
const pasted = "Received: from mail.example.com by mx.example.net";
act(() => {
const pasteEvent = new Event("paste", { bubbles: true });
Object.defineProperty(pasteEvent, "clipboardData", {
value: {
getData: () => pasted,
},
});
textarea.value = pasted;
textarea.dispatchEvent(pasteEvent);
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
expect(handleChange).toHaveBeenCalledWith(pasted);
});
it("shows a validation error when left empty", () => {
const { container } = render(<HeaderInput value="" onChange={() => undefined} />);
const textarea = getTextarea(container);
act(() => {
textarea.dispatchEvent(new Event("blur", { bubbles: true }));
});
const alert = getAlert(container);
expect(alert).not.toBeNull();
expect(alert?.textContent ?? "").toMatch(/empty|required/i);
expect(textarea.getAttribute("aria-invalid")).toBe("true");
});
it("shows a validation error when input exceeds 1 MB", () => {
const oversized = "a".repeat(1024 * 1024 + 1);
const { container } = render(<HeaderInput value={oversized} onChange={() => undefined} />);
const textarea = getTextarea(container);
act(() => {
textarea.dispatchEvent(new Event("blur", { bubbles: true }));
});
const alert = getAlert(container);
expect(alert).not.toBeNull();
expect(alert?.textContent ?? "").toMatch(/1\s?mb/i);
expect(textarea.getAttribute("aria-invalid")).toBe("true");
});
});