Initial curated EventLens source from 0d80d3b38dfb817285481665b9e71e4b8a0656a0
This commit is contained in:
commit
663ea8c829
85 files changed
+12395
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("workspace navigation and primary controls expose accessible state", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
for (const workspace of ["Analysis", "Timeline", "Hunting", "Reports", "Settings"]) {
|
||||
await expect(page.getByRole("button", { name: workspace, exact: false }).first()).toBeEnabled();
|
||||
}
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await expect(page.getByRole("button", { name: "Timeline", exact: false }).first()).toHaveAttribute("aria-current", "page");
|
||||
await expect(page.getByRole("textbox", { name: "Search normalized evidence" })).toBeVisible();
|
||||
await expect(page.getByLabel("Filter by category")).toBeVisible();
|
||||
await expect(page.getByLabel("Filter by level")).toBeVisible();
|
||||
await expect(page.getByLabel("Filter by provider")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Previous evidence" })).toHaveAttribute("aria-label", "Previous evidence");
|
||||
await expect(page.getByRole("listitem").first()).toHaveAttribute("aria-setsize", /[0-9]+/);
|
||||
await page.getByRole("textbox", { name: "Search normalized evidence" }).focus();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.locator(":focus")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Hunting", exact: false }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Search hunting evidence" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Reports", exact: false }).click();
|
||||
await expect(page.getByRole("button", { name: "Export JSON", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Settings", exact: false }).click();
|
||||
await expect(page.getByRole("group", { name: "Timestamp display" })).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 700, height: 900 });
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await expect(page.getByLabel("Evidence time window")).toBeVisible();
|
||||
await expect(page.getByLabel("Hide informational noise")).toBeVisible();
|
||||
await expect(page.getByLabel("Focus incident")).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const validUpload = { name: "sample.evtx", mimeType: "application/octet-stream", buffer: Buffer.from("synthetic") };
|
||||
|
||||
test("malformed successful analysis keeps the previous investigation", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toContainText("bundled-demo");
|
||||
|
||||
await page.route("**/api/analyze", (route) => route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ source: "bad.evtx", events: [], incidents: [{ id: "broken" }] }),
|
||||
}));
|
||||
await page.locator('input[type="file"]').setInputFiles(validUpload);
|
||||
|
||||
await expect(page.getByRole("alert")).toContainText("Analysis returned an invalid response.");
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toContainText("bundled-demo");
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Inspector", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("structured parser errors have safe, useful wording", async ({ page }) => {
|
||||
await page.route("**/api/analyze", (route) => route.fulfill({
|
||||
status: 422,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ detail: { code: "no_readable_events", message: "No readable Windows events were found in this file." } }),
|
||||
}));
|
||||
await page.goto("/#analysis");
|
||||
await page.locator('input[type="file"]').setInputFiles(validUpload);
|
||||
|
||||
await expect(page.getByRole("alert")).toContainText("No readable Windows events were found in this file.");
|
||||
await expect(page.getByRole("alert")).not.toContainText("HTTP 422");
|
||||
});
|
||||
|
||||
test("rejecting an invalid file cancels an older pending analysis", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toContainText("bundled-demo");
|
||||
|
||||
let routeStarted!: () => void;
|
||||
let releaseRoute!: () => void;
|
||||
const started = new Promise<void>((resolve) => { routeStarted = resolve; });
|
||||
const released = new Promise<void>((resolve) => { releaseRoute = resolve; });
|
||||
await page.route("**/api/analyze", async (route) => {
|
||||
routeStarted();
|
||||
await released;
|
||||
try {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ source: "late.evtx", events: [], incidents: [] }) });
|
||||
} catch {
|
||||
// The route can already be cancelled by the browser request abort.
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
await fileInput.setInputFiles(validUpload);
|
||||
await started;
|
||||
await fileInput.setInputFiles({ name: "invalid.txt", mimeType: "text/plain", buffer: Buffer.from("invalid") });
|
||||
await expect(page.getByRole("alert")).toContainText("Choose a Windows .evtx file.");
|
||||
releaseRoute();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toContainText("bundled-demo");
|
||||
await expect(page.getByRole("alert")).toContainText("Choose a Windows .evtx file.");
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const assets = [
|
||||
["favicon.svg", /image\/svg\+xml/i],
|
||||
["favicon.ico", /image\/(x-icon|vnd\.microsoft\.icon)/i],
|
||||
["apple-touch-icon.png", /image\/png/i],
|
||||
["eventlens-icon-192.png", /image\/png/i],
|
||||
["eventlens-icon-512.png", /image\/png/i],
|
||||
["site.webmanifest", /(manifest|json)/i],
|
||||
] as const;
|
||||
|
||||
test("canonical EventLens branding and icon package are locally served", async ({ page, request }) => {
|
||||
await page.goto("/#analysis");
|
||||
await expect(page).toHaveTitle("EventLens — Windows Incident Reconstruction");
|
||||
await expect(page.locator('meta[name="application-name"]')).toHaveAttribute("content", "EventLens");
|
||||
await expect(page.locator('link[rel="manifest"]')).toHaveAttribute("href", "/site.webmanifest");
|
||||
await expect(page.locator(".event-logo svg")).toBeVisible();
|
||||
|
||||
for (const [asset, contentType] of assets) {
|
||||
const response = await request.get(`/${asset}`);
|
||||
expect(response.status(), `${asset} should be served`).toBe(200);
|
||||
expect(response.headers()["content-type"] ?? "", `${asset} MIME type`).toMatch(contentType);
|
||||
}
|
||||
|
||||
for (const workspace of ["Analysis", "Timeline", "Hunting", "Reports", "Settings"]) {
|
||||
await expect(page.getByRole("button", { name: workspace, exact: false }).first().locator("svg")).toHaveCount(1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("demo investigation workflow crosses Analysis, Timeline and Reports", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await expect(page.getByRole("heading", { name: "No event log loaded", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" }).getByText("bundled-demo", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Incident windows", exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Analysis status", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Deterministic demo fixture", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Current source", { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText("How to continue", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByText("Service failure recorded", { exact: true }).click();
|
||||
await expect(page).toHaveURL(/#timeline$/);
|
||||
await expect(page.getByRole("heading", { name: "Event timeline", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Example Sync Service terminated unexpectedly", exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Focus active", { exact: true })).toBeVisible();
|
||||
|
||||
const search = page.getByRole("textbox", { name: "Search normalized evidence" });
|
||||
await search.fill("DNS name resolution");
|
||||
await expect(page.locator(".event-row")).toHaveCount(0);
|
||||
await search.fill("");
|
||||
await page.getByRole("button", { name: "Stop focus", exact: true }).click();
|
||||
await page.getByLabel("Filter by category").selectOption("network");
|
||||
await expect(page.locator(".event-row")).toHaveCount(1);
|
||||
await page.getByLabel("Filter by category").selectOption("");
|
||||
await page.getByRole("button", { name: /Next evidence/ }).click();
|
||||
await expect(page.getByText(/of [0-9]+/).first()).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Reports", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export report", exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Redaction preview", { exact: true })).toBeVisible();
|
||||
const jsonDownload = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Export JSON", exact: true }).click();
|
||||
expect((await jsonDownload).suggestedFilename()).toMatch(/eventlens\.json$/);
|
||||
const markdownDownload = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Export Markdown", exact: true }).click();
|
||||
expect((await markdownDownload).suggestedFilename()).toMatch(/eventlens\.md$/);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const partialAnalysis = {
|
||||
source: "partial.evtx",
|
||||
events: [{ id: "1", timestamp: "2026-09-09T10:00:00Z", timestamp_source: "event", timestamp_valid: true, provider: "Synthetic", event_id: 1000, level: "error", channel: "Application", computer: "TEST-PC", category: "crash", summary: "Synthetic crash", fields: {} }],
|
||||
incidents: [{ id: "incident-1", rule_id: "windows.generic.attention", start: "2026-09-09T10:00:00Z", end: "2026-09-09T10:00:00Z", title: "Synthetic crash", category: "crash", severity: "warning", reasons: ["Parser retained only a partial result."], evidence_event_ids: ["1"], original_count: 1, compressed_count: 1 }],
|
||||
parse: { parser: "evtx-rs", status: "partial", parsed_events: 1, records_seen: 4, skipped_records: 2, issue_count: 1, truncated: true, truncation_reason: "event_limit_reached", duration_ms: 8, issues: [{ code: "event_limit_reached", record_id: 4, message: "later records were not analyzed", skipped_record: true }] },
|
||||
};
|
||||
|
||||
test("partial result is unmistakable and invalid extension is rejected", async ({ page }) => {
|
||||
await page.route("**/api/analyze", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(partialAnalysis) }));
|
||||
await page.goto("/#analysis");
|
||||
await expect(page.getByRole("heading", { name: "Analysis", exact: true })).toBeVisible();
|
||||
await page.locator('input[type="file"]').setInputFiles({ name: "partial.evtx", mimeType: "application/octet-stream", buffer: Buffer.from("synthetic") });
|
||||
await expect(page.getByRole("alert")).toContainText("Partial analysis");
|
||||
await expect(page.getByRole("alert")).toContainText("incident reconstruction may be incomplete");
|
||||
await expect(page.getByText("event_limit_reached", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Analysis status", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("2 skipped · 1 parser issue", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await expect(page.getByRole("alert")).toContainText("incident reconstruction may be incomplete");
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles({ name: "not-an-evtx.txt", mimeType: "text/plain", buffer: Buffer.from("nope") });
|
||||
await expect(page.getByRole("alert").filter({ hasText: "Choose a Windows .evtx file." })).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("capture deterministic v1.0.6 portfolio views", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await expect(page.getByRole("heading", { name: "Incident windows", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-demo-overview.png", fullPage: true });
|
||||
|
||||
await page.getByText("Service failure recorded", { exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Event timeline", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-timeline-evidence.png", fullPage: true });
|
||||
|
||||
await page.getByRole("button", { name: "Stop focus", exact: true }).click();
|
||||
await page.getByRole("textbox", { name: "Search normalized evidence" }).fill("DNS name resolution");
|
||||
await expect(page.locator(".event-row")).toHaveCount(1);
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-filter-search.png", fullPage: true });
|
||||
|
||||
await page.getByRole("button", { name: "Hunting", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Hunting", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-hunting.png", fullPage: true });
|
||||
|
||||
await page.getByRole("button", { name: "Reports", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export report", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-redaction-export.png", fullPage: true });
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Local data & privacy", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: "../docs/assets/eventlens-settings.png", fullPage: true });
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const fixture = process.env.EVENTLENS_EVTX_FIXTURE;
|
||||
|
||||
test("pinned real EVTX fixture survives the browser upload path", async ({ page }) => {
|
||||
test.skip(!fixture || !existsSync(fixture), "CI supplies the hash-verified public EVTX fixture");
|
||||
await page.goto("/#analysis");
|
||||
await page.locator('input[type="file"]').setInputFiles(fixture!);
|
||||
await expect(page.locator(".metric-card").first()).toContainText("7", { timeout: 30_000 });
|
||||
await expect(page.getByText("events parsed", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await page.getByLabel("Hide informational noise").uncheck();
|
||||
await expect(page.locator(".event-row").first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /4625/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Reports", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export report", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: /Export JSON/ }).click();
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("core workspace actions remain usable across supported viewport widths", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
for (const viewport of [{ width: 360, height: 800 }, { width: 390, height: 844 }, { width: 768, height: 900 }, { width: 1024, height: 768 }, { width: 1366, height: 768 }, { width: 1440, height: 900 }, { width: 1920, height: 1080 }]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await expect(page.getByRole("button", { name: "Open .evtx", exact: true }).first()).toBeVisible();
|
||||
for (const workspace of ["Analysis", "Timeline", "Hunting", "Reports", "Settings"]) {
|
||||
const button = page.getByRole("button", { name: workspace, exact: false }).first();
|
||||
await button.click();
|
||||
await expect(page.getByRole("heading", { name: workspace, exact: true })).toBeVisible();
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1);
|
||||
expect(overflow, `horizontal overflow in ${workspace} at ${viewport.width}px`).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("settings keeps session state and reports a persistence failure", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const originalSetItem = Storage.prototype.setItem;
|
||||
Storage.prototype.setItem = function setItem(key: string, value: string) {
|
||||
if (key === "eventlens.preferences.v1") {
|
||||
throw new DOMException("Storage is unavailable", "QuotaExceededError");
|
||||
}
|
||||
return originalSetItem.call(this, key, value);
|
||||
};
|
||||
});
|
||||
|
||||
await page.goto("/#settings");
|
||||
|
||||
const compact = page.getByRole("radio", { name: /Compact/i });
|
||||
await compact.press("Space");
|
||||
|
||||
await expect(compact).toBeChecked();
|
||||
await expect(page.locator(".settings-save-feedback")).toHaveText("Preferences changed for this session only");
|
||||
await expect(page.getByText("Saved automatically in this browser", { exact: true })).toBeHidden();
|
||||
await expect(page.getByText("Preferences are available for this session only because browser storage is unavailable.")).toBeVisible();
|
||||
await expect(page.getByText("Not saved to browser storage")).toBeVisible();
|
||||
await expect(page.getByText("Interface preferences for this session")).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(2_600);
|
||||
await expect(page.getByRole("status")).toHaveText("Preferences changed for this session only");
|
||||
await expect(page.getByText("Saved automatically in this browser", { exact: true })).toBeHidden();
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("radio", { name: /Comfortable/i })).toBeChecked();
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("Settings communicates browser-local saving and resets accessible preference controls", async ({ page }) => {
|
||||
await page.goto("/#settings");
|
||||
|
||||
await expect(page.getByText("Saved automatically in this browser", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("group", { name: "Timestamp display" })).toBeVisible();
|
||||
await expect(page.getByRole("group", { name: "Interface density" })).toBeVisible();
|
||||
await expect(page.getByRole("switch", { name: "Export redaction by default" })).toBeChecked();
|
||||
|
||||
await page.locator("label.preference-option").filter({ hasText: "Local browser time" }).click();
|
||||
await expect(page.getByRole("status").filter({ hasText: "Preferences saved" })).toBeVisible();
|
||||
await page.getByRole("switch", { name: "Export redaction by default" }).press("Space");
|
||||
await page.getByLabel("Default noise mode").selectOption("all");
|
||||
await page.getByLabel("Default workspace").selectOption("hunting");
|
||||
await page.getByRole("radio", { name: /Compact/ }).press("Space");
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("radio", { name: /Local browser time/ })).toBeChecked();
|
||||
await expect(page.getByRole("switch", { name: "Export redaction by default" })).not.toBeChecked();
|
||||
await expect(page.getByLabel("Default noise mode")).toHaveValue("all");
|
||||
await expect(page.getByLabel("Default workspace")).toHaveValue("hunting");
|
||||
await expect(page.getByRole("radio", { name: /Compact/ })).toBeChecked();
|
||||
|
||||
const storedKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||
expect(storedKeys).toEqual(["eventlens.preferences.v1"]);
|
||||
await page.getByText("Technical details", { exact: true }).press("Enter");
|
||||
await expect(page.getByText("eventlens.preferences.v1", { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Reset all preferences" }).press("Enter");
|
||||
await expect(page.getByRole("status").filter({ hasText: "Preferences reset to defaults" })).toBeVisible();
|
||||
await expect(page.getByRole("radio", { name: /^UTC/ })).toBeChecked();
|
||||
await expect(page.getByRole("switch", { name: "Export redaction by default" })).toBeChecked();
|
||||
await expect(page.getByLabel("Default noise mode")).toHaveValue("attention");
|
||||
await expect(page.getByLabel("Default workspace")).toHaveValue("analysis");
|
||||
});
|
||||
|
||||
test("a browser-local default workspace applies only without a deep-link override", async ({ page }) => {
|
||||
await page.goto("/#settings");
|
||||
await page.getByLabel("Default workspace").selectOption("hunting");
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/#hunting$/);
|
||||
await expect(page.getByRole("heading", { name: "Hunting", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/#timeline");
|
||||
await expect(page).toHaveURL(/#timeline$/);
|
||||
await expect(page.getByRole("heading", { name: "Timeline", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("the active Settings tab remains visible in the compact workspace navigation", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/#settings");
|
||||
const settingsTab = page.getByRole("button", { name: "Settings", exact: true });
|
||||
await expect(settingsTab).toHaveAttribute("aria-current", "page");
|
||||
const box = await settingsTab.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(390);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
function largeAnalysis(count: number) {
|
||||
return {
|
||||
source: "synthetic-large.evtx",
|
||||
events: Array.from({ length: count }, (_, index) => ({ id: String(index), timestamp: "2026-09-09T10:00:00Z", timestamp_source: "event", timestamp_valid: true, provider: "Synthetic Provider", event_id: index, level: "warning", channel: "Application", computer: "TEST-PC", category: "other", summary: `Synthetic event ${index}`, fields: { message: `Synthetic event ${index}` } })),
|
||||
incidents: [],
|
||||
};
|
||||
}
|
||||
|
||||
test("large timelines virtualize DOM rows and keep search usable", async ({ page }) => {
|
||||
const analysis = largeAnalysis(10_000);
|
||||
await page.route("**/api/demo", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(analysis) }));
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await expect(page.getByText("10,000", { exact: false }).first()).toBeVisible();
|
||||
expect(await page.locator(".event-row").count()).toBeLessThan(80);
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await page.getByRole("textbox", { name: "Search normalized evidence" }).fill("Synthetic event 9999");
|
||||
await expect(page.locator(".event-row")).toHaveCount(1);
|
||||
await expect(page.locator(".event-row").first()).toContainText("9999");
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("all workspaces are actionable and preserve the loaded analysis", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
|
||||
for (const workspace of ["Analysis", "Timeline", "Hunting", "Reports", "Settings"]) {
|
||||
const button = page.getByRole("button", { name: workspace, exact: false }).first();
|
||||
await expect(button).toBeEnabled();
|
||||
await button.click();
|
||||
await expect(button).toHaveAttribute("aria-current", "page");
|
||||
}
|
||||
|
||||
await expect(page.getByRole("region", { name: "Local preferences context" })).toBeVisible();
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Timeline", exact: false }).click();
|
||||
await expect(page.getByRole("heading", { name: "Event timeline", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Inspector", exact: true })).toBeVisible();
|
||||
await page.goBack();
|
||||
await expect(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible();
|
||||
await page.goForward();
|
||||
await expect(page.getByRole("heading", { name: "Event timeline", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("context and global actions keep an ordered, workspace-appropriate layout", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
const geometry = await page.evaluate(() => {
|
||||
const rect = (selector: string) => document.querySelector(selector)?.getBoundingClientRect();
|
||||
const topbar = rect(".event-topbar");
|
||||
const context = rect(".investigation-context");
|
||||
const actions = rect(".global-actions");
|
||||
return { topbar, context, actions };
|
||||
});
|
||||
expect(geometry.topbar?.bottom).toBeLessThanOrEqual((geometry.context?.top ?? 0) + 1);
|
||||
expect(geometry.context?.bottom).toBeLessThanOrEqual((geometry.actions?.top ?? 0) + 1);
|
||||
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
await expect(page.getByRole("region", { name: "Local preferences context" })).toBeVisible();
|
||||
await expect(page.getByRole("region", { name: "Current investigation context" })).toHaveCount(0);
|
||||
await expect(page.getByRole("region", { name: "Analysis actions" })).toBeVisible();
|
||||
await expect(page.getByText("Source", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Hunting combines facets and opens a result in the inspector", async ({ page }) => {
|
||||
await page.goto("/#analysis");
|
||||
await page.getByRole("button", { name: "Load demo", exact: true }).first().click();
|
||||
await page.getByRole("button", { name: "Hunting", exact: false }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Search hunting evidence" }).fill("DNS name resolution");
|
||||
await expect(page.getByRole("status")).toHaveText("1 of 6 matching events");
|
||||
await expect(page.getByRole("button", { name: /DNS name resolution timed out/ })).toBeVisible();
|
||||
await page.getByRole("button", { name: /DNS name resolution timed out/ }).click();
|
||||
await expect(page.getByRole("heading", { name: "Inspector", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "DNS name resolution timed out", exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Open in Timeline", exact: false }).click();
|
||||
await expect(page).toHaveURL(/#timeline$/);
|
||||
await expect(page.getByRole("heading", { name: "Event timeline", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Settings persist preferences without persisting event data", async ({ page }) => {
|
||||
await page.goto("/#settings");
|
||||
await page.getByRole("radio", { name: /Local browser time/ }).press("Space");
|
||||
await page.getByLabel("Default noise mode").selectOption("all");
|
||||
await page.getByLabel("Default workspace").selectOption("hunting");
|
||||
await page.getByRole("radio", { name: /Compact/ }).press("Space");
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByLabel("Local browser time", { exact: false })).toBeChecked();
|
||||
await expect(page.getByLabel("Default noise mode")).toHaveValue("all");
|
||||
await expect(page.getByLabel("Default workspace")).toHaveValue("hunting");
|
||||
await expect(page.getByLabel("Compact", { exact: false })).toBeChecked();
|
||||
const stored = await page.evaluate(() => JSON.stringify(localStorage));
|
||||
expect(stored).toContain("eventlens.preferences.v1");
|
||||
expect(stored).not.toContain("bundled-demo");
|
||||
expect(stored).not.toContain("Example Sync Service");
|
||||
});
|
||||
Reference in new issue
Block a user