Implement 8 rich dashboard widgets replacing placeholder stat cards: - ThreatScoreWidget: SVG circular gauge (0-100) with color-coded score - AlertFeedWidget: Real-time alert stream with mark-as-read actions - ExposureWidget: DarkWatch exposure summary with run-scan button - VoicePrintWidget: Enrollment/analysis counts with mini bar chart - SpamShieldWidget: Blocked calls/SMS stats with custom rules - HomeTitleWidget: Watched properties and recent changes - RemoveBrokersWidget: Broker registry progress with completion bar - QuickActionsWidget: Shortcut buttons for common tasks Update dashboard route with responsive 2-column grid layout, auto-refresh via 60-second intervals, Suspense boundaries, and skeleton loading states. Update tests for new widget layout.
317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render } from "solid-js/web";
|
|
import { MetaProvider } from "@solidjs/meta";
|
|
import type { JSX } from "solid-js";
|
|
|
|
vi.mock("~/lib/api", () => ({
|
|
api: {
|
|
correlation: {
|
|
getStats: { query: vi.fn().mockResolvedValue({ threatScore: 25, totalAlerts: 5, bySeverity: {}, bySource: {}, activeGroups: 2, resolvedCount: 3, falsePositiveCount: 1, threatBreakdown: [] }) },
|
|
getAlerts: { query: vi.fn().mockResolvedValue({ items: [
|
|
{ id: "1", title: "New credential leak detected", severity: "HIGH", source: "DARKWATCH", createdAt: new Date().toISOString() },
|
|
{ id: "2", title: "Suspicious call", severity: "WARNING", source: "SPAMSHIELD", createdAt: new Date().toISOString() },
|
|
], total: 2, page: 1, limit: 10, totalPages: 1 }) },
|
|
resolveAlert: { mutate: vi.fn().mockResolvedValue({}) },
|
|
},
|
|
darkwatch: {
|
|
getExposures: { query: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 1, totalPages: 0 }) },
|
|
runScan: { mutate: vi.fn().mockResolvedValue({}) },
|
|
},
|
|
voiceprint: {
|
|
getEnrollments: { query: vi.fn().mockResolvedValue([]) },
|
|
getAnalyses: { query: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 10, totalPages: 0 }) },
|
|
},
|
|
spamshield: {
|
|
getStats: { query: vi.fn().mockResolvedValue({ period: "week", totalDetections: 10, spamCount: 8, notSpamCount: 2, accuracy: 80, activeRules: 3 }) },
|
|
getRules: { query: vi.fn().mockResolvedValue({ userRules: [], globalRules: [] }) },
|
|
},
|
|
hometitle: {
|
|
getProperties: { query: vi.fn().mockResolvedValue([]) },
|
|
getAlerts: { query: vi.fn().mockResolvedValue([]) },
|
|
},
|
|
removebrokers: {
|
|
getStats: { query: vi.fn().mockResolvedValue({ total: 0, byStatus: {}, totalListings: 0, listingsRemoved: 0, completionRate: 0 }) },
|
|
getBrokerRegistry: { query: vi.fn().mockResolvedValue([]) },
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@solidjs/router", () => ({
|
|
A: (props: { href?: string; children?: JSX.Element; class?: string; onClick?: () => void }) => (
|
|
<a href={props.href || "#"} class={props.class} onClick={props.onClick}>
|
|
{props.children}
|
|
</a>
|
|
),
|
|
useLocation: () => ({ pathname: "/dashboard" }),
|
|
useParams: () => ({ slug: "ai-scam-trends-2026" }),
|
|
useSearchParams: () => [new URLSearchParams(), vi.fn()],
|
|
}));
|
|
|
|
vi.mock("~/hooks", () => ({
|
|
useAuth: () => ({
|
|
user: () => ({ name: "Test User", email: "test@shieldai.app" }),
|
|
isAuthenticated: () => true,
|
|
isLoading: () => false,
|
|
logout: vi.fn(),
|
|
}),
|
|
useSubscription: () => ({
|
|
subscription: () => ({ tier: "plus", status: "active" }),
|
|
tier: () => "plus",
|
|
isLoading: () => false,
|
|
hasFeature: () => true,
|
|
}),
|
|
useNotifications: () => ({
|
|
alerts: () => [
|
|
{ id: "1", title: "New credential leak detected", description: "Your email was found in a data breach", severity: "HIGH", createdAt: "5m ago" },
|
|
{ id: "2", title: "VoicePrint scan completed", description: "No deepfake voice activity detected", severity: "INFO", createdAt: "1h ago" },
|
|
{ id: "3", title: "RemoveBroker opt-out confirmed", description: "Your data has been removed from Whitepages", severity: "INFO", createdAt: "3h ago" },
|
|
{ id: "4", title: "Suspicious call blocked", description: "SpamShield blocked a call", severity: "WARNING", createdAt: "6h ago" },
|
|
{ id: "5", title: "HomeTitle alert", description: "A document was filed", severity: "CRITICAL", createdAt: "1d ago" },
|
|
],
|
|
unreadCount: () => 5,
|
|
markRead: vi.fn(),
|
|
isLoading: () => false,
|
|
}),
|
|
}));
|
|
|
|
import BlogPage from "./blog";
|
|
import BlogPostPage from "./blog/[slug]";
|
|
import AdsPage from "./ads";
|
|
import DashboardPage from "./(webapp)/dashboard";
|
|
import StatCard from "~/components/dashboard/StatCard";
|
|
import ActivityFeed from "~/components/dashboard/ActivityFeed";
|
|
import QuickActions from "~/components/dashboard/QuickActions";
|
|
|
|
function mount(comp: () => JSX.Element): HTMLDivElement {
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
render(() => <MetaProvider>{comp()}</MetaProvider>, container);
|
|
return container;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = "";
|
|
});
|
|
|
|
describe("BlogPage (listing)", () => {
|
|
it("renders hero section with blog headline", () => {
|
|
mount(() => <BlogPage />);
|
|
expect(document.body.textContent).toContain("ShieldAI Blog");
|
|
});
|
|
|
|
it("renders all 6 blog post cards", () => {
|
|
mount(() => <BlogPage />);
|
|
const cards = document.querySelectorAll(".gradient-card");
|
|
expect(cards.length).toBeGreaterThanOrEqual(4);
|
|
});
|
|
|
|
it("renders tag filter buttons", () => {
|
|
mount(() => <BlogPage />);
|
|
expect(document.body.textContent).toContain("All");
|
|
expect(document.body.textContent).toContain("AI Safety");
|
|
expect(document.body.textContent).toContain("Privacy");
|
|
expect(document.body.textContent).toContain("Deepfakes");
|
|
});
|
|
|
|
it("renders Load More button when there are more posts to show", () => {
|
|
mount(() => <BlogPage />);
|
|
expect(document.body.textContent).toContain("Load More Posts");
|
|
});
|
|
|
|
it("renders post titles and excerpts", () => {
|
|
mount(() => <BlogPage />);
|
|
expect(document.body.textContent).toContain("AI Scam Trends to Watch in 2026");
|
|
expect(document.body.textContent).toContain("Sarah Chen");
|
|
expect(document.body.textContent).toContain("Mike Reynolds");
|
|
});
|
|
});
|
|
|
|
describe("BlogPostPage ([slug])", () => {
|
|
it("renders post content for valid slug", () => {
|
|
mount(() => <BlogPostPage />);
|
|
expect(document.body.textContent).toContain("AI Scam Trends to Watch in 2026");
|
|
expect(document.body.textContent).toContain("Sarah Chen");
|
|
expect(document.body.textContent).toContain("Security Researcher");
|
|
expect(document.body.textContent).toContain("May 15, 2026");
|
|
expect(document.body.textContent).toContain("5 min read");
|
|
});
|
|
|
|
it("renders markdown content as HTML", () => {
|
|
mount(() => <BlogPostPage />);
|
|
expect(document.body.textContent).toContain("The Rise of AI-Powered Scams");
|
|
expect(document.body.textContent).toContain("Voice Cloning Scams");
|
|
expect(document.body.textContent).toContain("How to Protect Yourself");
|
|
});
|
|
|
|
it("renders social share buttons", () => {
|
|
mount(() => <BlogPostPage />);
|
|
const shareBtns = document.querySelectorAll("button[aria-label]");
|
|
const shareLabels = Array.from(shareBtns).map(b => b.getAttribute("aria-label"));
|
|
expect(shareLabels).toContain("Share on Twitter");
|
|
expect(shareLabels).toContain("Share on LinkedIn");
|
|
expect(shareLabels).toContain("Copy link");
|
|
});
|
|
|
|
it("renders related posts section", () => {
|
|
mount(() => <BlogPostPage />);
|
|
expect(document.body.textContent).toContain("Related Posts");
|
|
});
|
|
|
|
it("renders author card in sidebar", () => {
|
|
mount(() => <BlogPostPage />);
|
|
expect(document.body.textContent).toContain("Security Researcher");
|
|
});
|
|
|
|
it("renders back to blog link", () => {
|
|
mount(() => <BlogPostPage />);
|
|
expect(document.body.textContent).toContain("Back to Blog");
|
|
});
|
|
});
|
|
|
|
describe("AdsPage", () => {
|
|
it("renders conversion-focused headline", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("Stop AI Scams Before");
|
|
expect(document.body.textContent).toContain("They Reach You");
|
|
});
|
|
|
|
it("renders pricing section with 3 plans", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("Basic");
|
|
expect(document.body.textContent).toContain("Plus");
|
|
expect(document.body.textContent).toContain("Premium");
|
|
expect(document.body.textContent).toContain("$9");
|
|
expect(document.body.textContent).toContain("$19");
|
|
expect(document.body.textContent).toContain("$39");
|
|
});
|
|
|
|
it("marks Plus plan as Most Popular", () => {
|
|
mount(() => <AdsPage />);
|
|
const badges = document.body.querySelectorAll("span");
|
|
const popularBadge = Array.from(badges).find(b => b.textContent === "Most Popular");
|
|
expect(popularBadge).toBeTruthy();
|
|
});
|
|
|
|
it("renders FAQ section with toggle functionality", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("Frequently Asked Questions");
|
|
expect(document.body.textContent).toContain("How does ShieldAI detect voice clones?");
|
|
expect(document.body.textContent).toContain("Is my data encrypted?");
|
|
});
|
|
|
|
it("renders trust badges", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("14-day free trial");
|
|
expect(document.body.textContent).toContain("No credit card required");
|
|
expect(document.body.textContent).toContain("Cancel anytime");
|
|
});
|
|
|
|
it("renders trust metrics section", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("99.7%");
|
|
expect(document.body.textContent).toContain("200+");
|
|
expect(document.body.textContent).toContain("50K+");
|
|
});
|
|
|
|
it("renders testimonials", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("Trusted by Thousands");
|
|
});
|
|
|
|
it("renders CTA section with Get Started Free button", () => {
|
|
mount(() => <AdsPage />);
|
|
expect(document.body.textContent).toContain("Ready to protect your identity?");
|
|
expect(document.body.textContent).toContain("Get Started Free");
|
|
});
|
|
});
|
|
|
|
describe("DashboardPage", () => {
|
|
it("renders dashboard title", () => {
|
|
mount(() => <DashboardPage />);
|
|
expect(document.body.textContent).toContain("Dashboard");
|
|
});
|
|
|
|
it("renders sidebar with navigation links", () => {
|
|
mount(() => <DashboardPage />);
|
|
expect(document.body.textContent).toContain("Overview");
|
|
expect(document.body.textContent).toContain("DarkWatch");
|
|
expect(document.body.textContent).toContain("VoicePrint");
|
|
expect(document.body.textContent).toContain("SpamShield");
|
|
expect(document.body.textContent).toContain("HomeTitle");
|
|
expect(document.body.textContent).toContain("RemoveBrokers");
|
|
expect(document.body.textContent).toContain("Settings");
|
|
});
|
|
|
|
it("renders widget headers", async () => {
|
|
mount(() => <DashboardPage />);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(document.body.textContent).toContain("Threat Score");
|
|
expect(document.body.textContent).toContain("Alert Feed");
|
|
expect(document.body.textContent).toContain("Quick Actions");
|
|
});
|
|
|
|
it("renders QuickActionsWidget with action buttons", () => {
|
|
mount(() => <DashboardPage />);
|
|
expect(document.body.textContent).toContain("Quick Actions");
|
|
expect(document.body.textContent).toContain("Add to Watchlist");
|
|
expect(document.body.textContent).toContain("Upload Voice Sample");
|
|
expect(document.body.textContent).toContain("Check Number");
|
|
expect(document.body.textContent).toContain("Add Property");
|
|
expect(document.body.textContent).toContain("Start Removal");
|
|
});
|
|
});
|
|
|
|
describe("StatCard", () => {
|
|
it("renders label and value", () => {
|
|
mount(() => (
|
|
<StatCard label="Test Metric" value="42" icon={() => <svg />} />
|
|
));
|
|
expect(document.body.textContent).toContain("Test Metric");
|
|
expect(document.body.textContent).toContain("42");
|
|
});
|
|
|
|
it("renders up trend indicator", () => {
|
|
mount(() => (
|
|
<StatCard label="Up Trend" value="10" trend="up" trendLabel="+5%" icon={() => <svg />} />
|
|
));
|
|
expect(document.body.textContent).toContain("+5%");
|
|
});
|
|
|
|
it("renders down trend indicator", () => {
|
|
mount(() => (
|
|
<StatCard label="Down Trend" value="10" trend="down" trendLabel="-3%" icon={() => <svg />} />
|
|
));
|
|
expect(document.body.textContent).toContain("-3%");
|
|
});
|
|
});
|
|
|
|
describe("ActivityFeed", () => {
|
|
it("renders activities", () => {
|
|
const activities = [
|
|
{ id: "1", title: "Test Alert", description: "Test description", timestamp: "5m ago", type: "alert" as const },
|
|
];
|
|
mount(() => <ActivityFeed activities={activities} />);
|
|
expect(document.body.textContent).toContain("Recent Activity");
|
|
expect(document.body.textContent).toContain("Test Alert");
|
|
expect(document.body.textContent).toContain("Test description");
|
|
expect(document.body.textContent).toContain("5m ago");
|
|
});
|
|
});
|
|
|
|
describe("QuickActions", () => {
|
|
it("renders action buttons", () => {
|
|
const actions = [
|
|
{ label: "Action 1", href: "/test", icon: () => <svg /> },
|
|
{ label: "Action 2", href: "/test2", icon: () => <svg /> },
|
|
];
|
|
mount(() => <QuickActions actions={actions} />);
|
|
expect(document.body.textContent).toContain("Quick Actions");
|
|
expect(document.body.textContent).toContain("Action 1");
|
|
expect(document.body.textContent).toContain("Action 2");
|
|
});
|
|
});
|