@cybermem/dashboard 0.5.10 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/health/route.ts +60 -46
- package/components/dashboard/audit-log-table.tsx +209 -130
- package/components/dashboard/chart-card.tsx +151 -76
- package/components/dashboard/header.tsx +131 -50
- package/components/dashboard/mcp-config-modal.tsx +1 -1
- package/components/dashboard/metric-card.tsx +46 -10
- package/components/dashboard/metrics-chart.tsx +137 -110
- package/components/dashboard/metrics-grid.tsx +138 -73
- package/components/dashboard/settings-modal.tsx +548 -114
- package/e2e/ui-elements.spec.ts +267 -0
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Elements E2E Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests for all dashboard UI components:
|
|
5
|
+
* - Visibility: Elements exist on page
|
|
6
|
+
* - Functional: Interactions work (clicks, modals)
|
|
7
|
+
* - Data: Values are displayed correctly
|
|
8
|
+
*
|
|
9
|
+
* Run with: npm run test:e2e -- ui-elements.spec.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { expect, test } from "@playwright/test";
|
|
13
|
+
|
|
14
|
+
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
|
|
15
|
+
|
|
16
|
+
// Helper to login
|
|
17
|
+
async function login(page: any) {
|
|
18
|
+
await page.goto(BASE_URL);
|
|
19
|
+
const passwordInput = page.getByPlaceholder("Enter admin password");
|
|
20
|
+
if (await passwordInput.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
21
|
+
await passwordInput.fill("admin");
|
|
22
|
+
await page.keyboard.press("Enter");
|
|
23
|
+
await expect(page.getByRole("heading", { name: "CyberMem" })).toBeVisible({
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
// Dismiss password warning if present
|
|
28
|
+
const dontShowBtn = page.locator('button:has-text("Don\'t show again")');
|
|
29
|
+
if (await dontShowBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
30
|
+
await dontShowBtn.click();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test.describe("UI Elements - Visibility", () => {
|
|
35
|
+
test.beforeEach(async ({ page }) => {
|
|
36
|
+
await login(page);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("Header: Logo, Title, and All Systems badge visible", async ({
|
|
40
|
+
page,
|
|
41
|
+
}) => {
|
|
42
|
+
// Logo
|
|
43
|
+
await expect(page.locator('img[alt="CyberMem Logo"]')).toBeVisible();
|
|
44
|
+
|
|
45
|
+
// Title
|
|
46
|
+
await expect(page.getByRole("heading", { name: "CyberMem" })).toBeVisible();
|
|
47
|
+
|
|
48
|
+
// All Systems badge should transition from shimmer to status
|
|
49
|
+
// Wait for shimmer to disappear (loading complete)
|
|
50
|
+
await page.waitForTimeout(3000);
|
|
51
|
+
|
|
52
|
+
// Should show one of: "All Systems OK", "Degraded", "System Error"
|
|
53
|
+
const badge = page.locator("text=/All Systems OK|Degraded|System Error/");
|
|
54
|
+
await expect(badge).toBeVisible({ timeout: 10000 });
|
|
55
|
+
console.log("✅ Header: All elements visible");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("MetricCards: All 4 numeric cards visible with labels", async ({
|
|
59
|
+
page,
|
|
60
|
+
}) => {
|
|
61
|
+
const cards = [
|
|
62
|
+
"Memory Records",
|
|
63
|
+
"Total Clients",
|
|
64
|
+
"Success Rate",
|
|
65
|
+
"Total Requests",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const label of cards) {
|
|
69
|
+
await expect(page.getByText(label)).toBeVisible();
|
|
70
|
+
}
|
|
71
|
+
console.log("✅ MetricCards: All 4 labels visible");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("MetricCards: Values are not stuck on shimmer", async ({ page }) => {
|
|
75
|
+
// Wait for data to load
|
|
76
|
+
await page.waitForTimeout(3000);
|
|
77
|
+
|
|
78
|
+
// Check that Memory Records shows a number (not shimmer placeholder)
|
|
79
|
+
const memoryCard = page
|
|
80
|
+
.locator("text=Memory Records")
|
|
81
|
+
.locator("..")
|
|
82
|
+
.locator("..");
|
|
83
|
+
const valueText = await memoryCard.locator(".text-4xl").textContent();
|
|
84
|
+
expect(valueText).not.toBe("");
|
|
85
|
+
expect(valueText).not.toContain("...");
|
|
86
|
+
console.log(`✅ Memory Records value: ${valueText}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("ClientCards: Top/Last Writer/Reader visible", async ({ page }) => {
|
|
90
|
+
const cards = ["Top Writer", "Top Reader", "Last Writer", "Last Reader"];
|
|
91
|
+
|
|
92
|
+
for (const label of cards) {
|
|
93
|
+
await expect(page.getByText(label)).toBeVisible();
|
|
94
|
+
}
|
|
95
|
+
console.log("✅ ClientCards: All 4 visible");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("ChartCards: All 4 time series charts present", async ({ page }) => {
|
|
99
|
+
const charts = [
|
|
100
|
+
"Creates by Client",
|
|
101
|
+
"Reads by Client",
|
|
102
|
+
"Updates by Client",
|
|
103
|
+
"Deletes by Client",
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const title of charts) {
|
|
107
|
+
await expect(page.getByText(title)).toBeVisible();
|
|
108
|
+
}
|
|
109
|
+
console.log("✅ ChartCards: All 4 visible");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("ChartCards: Period selector works", async ({ page }) => {
|
|
113
|
+
// Find first chart's period button
|
|
114
|
+
const periodBtn = page.locator('button:has-text("24 Hours")').first();
|
|
115
|
+
await expect(periodBtn).toBeVisible();
|
|
116
|
+
await periodBtn.click();
|
|
117
|
+
|
|
118
|
+
// Dropdown should appear
|
|
119
|
+
await expect(page.getByText("7 Days")).toBeVisible();
|
|
120
|
+
console.log("✅ Chart period selector works");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("AuditLog: Table and headers visible", async ({ page }) => {
|
|
124
|
+
// Scroll to audit log
|
|
125
|
+
const auditHeader = page.getByRole("heading", { name: "Audit Log" });
|
|
126
|
+
await auditHeader.scrollIntoViewIfNeeded();
|
|
127
|
+
await expect(auditHeader).toBeVisible();
|
|
128
|
+
|
|
129
|
+
// Check column headers
|
|
130
|
+
const headers = [
|
|
131
|
+
"Timestamp",
|
|
132
|
+
"Client",
|
|
133
|
+
"Operation",
|
|
134
|
+
"Description",
|
|
135
|
+
"Status",
|
|
136
|
+
];
|
|
137
|
+
for (const header of headers) {
|
|
138
|
+
await expect(page.locator(`th:has-text("${header}")`)).toBeVisible();
|
|
139
|
+
}
|
|
140
|
+
console.log("✅ AuditLog: Table headers visible");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("AuditLog: Sorting works", async ({ page }) => {
|
|
144
|
+
const timestampHeader = page.locator('th:has-text("Timestamp")');
|
|
145
|
+
await timestampHeader.scrollIntoViewIfNeeded();
|
|
146
|
+
|
|
147
|
+
// Click to sort
|
|
148
|
+
await timestampHeader.click();
|
|
149
|
+
// Second click to reverse
|
|
150
|
+
await timestampHeader.click();
|
|
151
|
+
|
|
152
|
+
// If no error, sorting works
|
|
153
|
+
console.log("✅ AuditLog: Sorting works");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test.describe("UI Elements - Functional", () => {
|
|
158
|
+
test.beforeEach(async ({ page }) => {
|
|
159
|
+
await login(page);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("Settings Modal: Opens and shows system info", async ({ page }) => {
|
|
163
|
+
// Click settings button
|
|
164
|
+
const settingsBtn = page.locator("button:has(svg.lucide-settings)");
|
|
165
|
+
await settingsBtn.click();
|
|
166
|
+
|
|
167
|
+
// Modal should open
|
|
168
|
+
await expect(page.getByText("Dashboard Settings")).toBeVisible({
|
|
169
|
+
timeout: 5000,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// System info should be present
|
|
173
|
+
await expect(page.getByText("System")).toBeVisible();
|
|
174
|
+
|
|
175
|
+
// Close modal
|
|
176
|
+
await page.keyboard.press("Escape");
|
|
177
|
+
console.log("✅ Settings Modal: Opens and shows system info");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("Settings Modal: Data Management buttons visible", async ({ page }) => {
|
|
181
|
+
const settingsBtn = page.locator("button:has(svg.lucide-settings)");
|
|
182
|
+
await settingsBtn.click();
|
|
183
|
+
|
|
184
|
+
await expect(page.getByText("Dashboard Settings")).toBeVisible({
|
|
185
|
+
timeout: 5000,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Check for data management buttons
|
|
189
|
+
await expect(page.getByRole("button", { name: /Backup/i })).toBeVisible();
|
|
190
|
+
await expect(page.getByRole("button", { name: /Restore/i })).toBeVisible();
|
|
191
|
+
await expect(page.getByRole("button", { name: /Reset/i })).toBeVisible();
|
|
192
|
+
|
|
193
|
+
await page.keyboard.press("Escape");
|
|
194
|
+
console.log("✅ Settings Modal: Data Management buttons visible");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("MCP Config Modal: Opens and shows instructions", async ({ page }) => {
|
|
198
|
+
// Click Connect MCP button
|
|
199
|
+
const mcpBtn = page.getByRole("button", { name: "Connect MCP" });
|
|
200
|
+
await mcpBtn.click();
|
|
201
|
+
|
|
202
|
+
// Modal should open with instructions
|
|
203
|
+
await expect(page.getByText(/MCP Configuration|Connect MCP/i)).toBeVisible({
|
|
204
|
+
timeout: 5000,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Should show config instructions
|
|
208
|
+
await expect(page.locator("code, pre")).toBeVisible();
|
|
209
|
+
|
|
210
|
+
// Close modal
|
|
211
|
+
await page.keyboard.press("Escape");
|
|
212
|
+
console.log("✅ MCP Config Modal: Opens and shows instructions");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("Docs button: Links to documentation", async ({ page }) => {
|
|
216
|
+
const docsBtn = page.getByRole("link", { name: "Docs" });
|
|
217
|
+
const href = await docsBtn.getAttribute("href");
|
|
218
|
+
expect(href).toContain("docs.cybermem.dev");
|
|
219
|
+
console.log("✅ Docs button: Correct link");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test.describe("UI Elements - Data Validation", () => {
|
|
224
|
+
test.beforeEach(async ({ page }) => {
|
|
225
|
+
await login(page);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("API returns valid stats structure", async ({ request }) => {
|
|
229
|
+
const res = await request.get(`${BASE_URL}/api/metrics`);
|
|
230
|
+
expect(res.ok()).toBeTruthy();
|
|
231
|
+
|
|
232
|
+
const data = await res.json();
|
|
233
|
+
expect(data.stats).toBeDefined();
|
|
234
|
+
expect(typeof data.stats.memoryRecords).toBe("number");
|
|
235
|
+
expect(typeof data.stats.totalClients).toBe("number");
|
|
236
|
+
expect(typeof data.stats.successRate).toBe("number");
|
|
237
|
+
expect(typeof data.stats.totalRequests).toBe("number");
|
|
238
|
+
expect(data.stats.topWriter).toBeDefined();
|
|
239
|
+
expect(data.stats.topReader).toBeDefined();
|
|
240
|
+
console.log("✅ API: Valid stats structure");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("Dashboard displays consistent data with API", async ({
|
|
244
|
+
page,
|
|
245
|
+
request,
|
|
246
|
+
}) => {
|
|
247
|
+
// Get API data
|
|
248
|
+
const res = await request.get(`${BASE_URL}/api/metrics`);
|
|
249
|
+
const apiData = await res.json();
|
|
250
|
+
|
|
251
|
+
// Wait for dashboard to load
|
|
252
|
+
await page.waitForTimeout(3000);
|
|
253
|
+
|
|
254
|
+
// Memory Records should match API
|
|
255
|
+
const memoryValue = await page
|
|
256
|
+
.locator("text=Memory Records")
|
|
257
|
+
.locator("..")
|
|
258
|
+
.locator("..")
|
|
259
|
+
.locator(".text-4xl")
|
|
260
|
+
.textContent();
|
|
261
|
+
const apiMemory = apiData.stats.memoryRecords.toLocaleString();
|
|
262
|
+
|
|
263
|
+
// Values should match (allowing for locale formatting)
|
|
264
|
+
expect(memoryValue?.replace(/,/g, "")).toBe(apiMemory.replace(/,/g, ""));
|
|
265
|
+
console.log(`✅ Dashboard data matches API: ${memoryValue}`);
|
|
266
|
+
});
|
|
267
|
+
});
|