MAESTRO: add browser cache tests

This commit is contained in:
Mariusz Banach
2026-02-18 03:37:24 +01:00
parent b1ef4721fd
commit 07ee139a00
3 changed files with 415 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
import type { ReactElement } from "react";
import { act } from "react-dom/test-utils";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Home from "../app/page";
import type { AnalysisConfig, AnalysisReport } from "../types/analysis";
const { submitSpy, cancelSpy, useAnalysisState } = vi.hoisted(() => {
const submitSpy = vi.fn().mockResolvedValue(undefined);
const cancelSpy = vi.fn();
return {
submitSpy,
cancelSpy,
useAnalysisState: {
status: "idle",
progress: null,
result: null,
error: null,
submit: submitSpy,
cancel: cancelSpy,
},
};
});
vi.mock("../hooks/useAnalysis", () => ({
__esModule: true,
default: () => useAnalysisState,
}));
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 header textarea to be rendered.");
}
return textarea as HTMLTextAreaElement;
};
const getClearCacheButton = (container: HTMLElement): HTMLButtonElement => {
const buttons = Array.from(container.querySelectorAll("button"));
const button = buttons.find((candidate) =>
(candidate.textContent ?? "").toLowerCase().includes("clear cache"),
);
if (!button) {
throw new Error("Expected clear cache button to be rendered.");
}
return button as HTMLButtonElement;
};
const cachedConfig: AnalysisConfig = {
testIds: [101, 202],
resolve: false,
decodeAll: false,
};
const cachedReport: AnalysisReport = {
results: [
{
testId: 101,
testName: "SpamAssassin Rule Hits",
headerName: "X-Spam-Status",
headerValue: "Yes",
analysis: "Hits: 6.2",
description: "Spam scoring summary.",
severity: "spam",
status: "success",
error: null,
},
],
hopChain: [],
securityAppliances: [],
metadata: {
totalTests: 1,
passedTests: 1,
failedTests: 0,
skippedTests: 0,
elapsedMs: 1200,
timedOut: false,
incompleteTests: [],
},
};
const seedCache = (headers: string) => {
localStorage.setItem("wha:headers", headers);
localStorage.setItem("wha:config", JSON.stringify(cachedConfig));
localStorage.setItem("wha:result", JSON.stringify(cachedReport));
localStorage.setItem("wha:timestamp", "1700000000000");
};
const resetUseAnalysisState = (): void => {
useAnalysisState.status = "idle";
useAnalysisState.progress = null;
useAnalysisState.result = null;
useAnalysisState.error = null;
};
beforeEach(() => {
localStorage.clear();
resetUseAnalysisState();
});
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
if (cleanup) {
cleanup();
}
}
submitSpy.mockClear();
cancelSpy.mockClear();
localStorage.clear();
});
describe("Home page cache", () => {
it("restores cached analysis on mount", () => {
seedCache("Received: from mail.example.com");
const { container } = render(<Home />);
const textarea = getTextarea(container);
expect(textarea.value).toBe("Received: from mail.example.com");
const report = container.querySelector('[data-testid="report-container"]');
expect(report).not.toBeNull();
});
it("clears cached data and resets the view", () => {
seedCache("Received: from mail.example.com");
const { container } = render(<Home />);
const button = getClearCacheButton(container);
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(localStorage.getItem("wha:headers")).toBeNull();
expect(localStorage.getItem("wha:config")).toBeNull();
expect(localStorage.getItem("wha:result")).toBeNull();
expect(localStorage.getItem("wha:timestamp")).toBeNull();
expect(getTextarea(container).value).toBe("");
expect(container.querySelector('[data-testid="report-container"]')).toBeNull();
});
});

View File

@@ -0,0 +1,204 @@
import type { ReactElement } from "react";
import { useState } from "react";
import { act } from "react-dom/test-utils";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { AnalysisConfig, AnalysisReport } from "../types/analysis";
import useAnalysisCache from "../hooks/useAnalysisCache";
type RenderResult = {
container: HTMLDivElement;
};
type CachedPayload = {
headers: string;
config: AnalysisConfig;
result: AnalysisReport;
};
type CachedData = CachedPayload & {
timestamp: number;
};
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 getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
const element = container.querySelector(`[data-testid="${testId}"]`);
if (!element) {
throw new Error(`Expected element ${testId} to be rendered.`);
}
return element as HTMLElement;
};
const baseConfig: AnalysisConfig = {
testIds: [101, 202],
resolve: false,
decodeAll: true,
};
const baseReport: AnalysisReport = {
results: [
{
testId: 101,
testName: "SpamAssassin Rule Hits",
headerName: "X-Spam-Status",
headerValue: "Yes",
analysis: "Hits: 6.2",
description: "Spam scoring summary.",
severity: "spam",
status: "success",
error: null,
},
],
hopChain: [],
securityAppliances: [],
metadata: {
totalTests: 3,
passedTests: 2,
failedTests: 1,
skippedTests: 0,
elapsedMs: 1200,
timedOut: false,
incompleteTests: [],
},
};
const basePayload: CachedPayload = {
headers: "Received: from mail.example.com",
config: baseConfig,
result: baseReport,
};
const CacheHarness = ({ payload }: { payload: CachedPayload }) => {
const { save, load, clear, hasCachedData, isNearLimit } = useAnalysisCache();
const [loaded, setLoaded] = useState<CachedData | null>(null);
const [hasCache, setHasCache] = useState(false);
return (
<div>
<span data-testid="has-cache">{hasCache ? "true" : "false"}</span>
<span data-testid="loaded-headers">{loaded?.headers ?? ""}</span>
<span data-testid="loaded-test-count">{loaded?.result.metadata.totalTests ?? ""}</span>
<span data-testid="loaded-test-ids">
{loaded ? loaded.config.testIds.join(",") : ""}
</span>
<span data-testid="near-limit">{isNearLimit ? "true" : "false"}</span>
<button type="button" data-testid="save" onClick={() => save(payload)}>
Save
</button>
<button type="button" data-testid="load" onClick={() => setLoaded(load())}>
Load
</button>
<button type="button" data-testid="check" onClick={() => setHasCache(hasCachedData())}>
Check
</button>
<button type="button" data-testid="clear" onClick={() => clear()}>
Clear
</button>
</div>
);
};
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
if (cleanup) {
cleanup();
}
}
localStorage.clear();
});
describe("useAnalysisCache", () => {
it("saves, loads, and clears cached analysis data", () => {
const { container } = render(<CacheHarness payload={basePayload} />);
act(() => {
getByTestId(container, "check").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(getByTestId(container, "has-cache").textContent).toBe("false");
act(() => {
getByTestId(container, "save").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(localStorage.getItem("wha:headers")).toBe(basePayload.headers);
expect(localStorage.getItem("wha:config")).toBe(JSON.stringify(basePayload.config));
expect(localStorage.getItem("wha:result")).toBe(JSON.stringify(basePayload.result));
const timestampValue = localStorage.getItem("wha:timestamp");
expect(timestampValue).not.toBeNull();
expect(Number.isNaN(Number(timestampValue))).toBe(false);
act(() => {
getByTestId(container, "check").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(getByTestId(container, "has-cache").textContent).toBe("true");
act(() => {
getByTestId(container, "load").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(getByTestId(container, "loaded-headers").textContent).toBe(basePayload.headers);
expect(getByTestId(container, "loaded-test-count").textContent).toBe("3");
expect(getByTestId(container, "loaded-test-ids").textContent).toBe("101,202");
act(() => {
getByTestId(container, "clear").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(localStorage.getItem("wha:headers")).toBeNull();
expect(localStorage.getItem("wha:config")).toBeNull();
expect(localStorage.getItem("wha:result")).toBeNull();
expect(localStorage.getItem("wha:timestamp")).toBeNull();
act(() => {
getByTestId(container, "check").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(getByTestId(container, "has-cache").textContent).toBe("false");
});
it("flags when cache usage is near the localStorage limit", () => {
const storageLimitBytes = 5 * 1024 * 1024;
const warningThresholdBytes = Math.floor(storageLimitBytes * 0.9) + 1024;
const largeHeaders = "a".repeat(warningThresholdBytes);
const payload: CachedPayload = {
...basePayload,
headers: largeHeaders,
};
const { container } = render(<CacheHarness payload={payload} />);
act(() => {
getByTestId(container, "save").dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(getByTestId(container, "near-limit").textContent).toBe("true");
});
});