feat: add unit tests for backend collectors and frontend
Backend tests: - CPU, memory, disk, network collector tests (existing) - Added temperature, processes, system, AMD GPU collector tests - All tests use mock filesystem data Frontend tests: - Added Vitest with jsdom environment - Tests for formatters (formatBytes, formatUptime, etc.) - Tests for theme store 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
166
backend/internal/collectors/amdgpu_test.go
Normal file
166
backend/internal/collectors/amdgpu_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAMDGPUCollector(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sysPath := filepath.Join(tmpDir, "sys")
|
||||
|
||||
// Create mock AMD GPU sysfs structure
|
||||
gpuPath := filepath.Join(sysPath, "class/drm/card0/device")
|
||||
if err := os.MkdirAll(gpuPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create driver symlink (required for AMD GPU detection)
|
||||
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
|
||||
if err := os.MkdirAll(driverTarget, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(driverTarget, filepath.Join(gpuPath, "driver")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// GPU utilization
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("75\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// VRAM
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_used"), []byte("4294967296\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "mem_info_vram_total"), []byte("17179869184\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Clock frequencies (format: "0: 500Mhz\n1: 800Mhz *\n2: 1200Mhz")
|
||||
sclk := "0: 500Mhz\n1: 800Mhz\n2: 1200Mhz *\n"
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_sclk"), []byte(sclk), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mclk := "0: 400Mhz\n1: 875Mhz *\n"
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "pp_dpm_mclk"), []byte(mclk), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create hwmon for temperature, power, fan
|
||||
hwmonPath := filepath.Join(gpuPath, "hwmon/hwmon5")
|
||||
if err := os.MkdirAll(hwmonPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Temperature (65°C in millidegrees)
|
||||
if err := os.WriteFile(filepath.Join(hwmonPath, "temp1_input"), []byte("65000\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Power (150W in microwatts)
|
||||
if err := os.WriteFile(filepath.Join(hwmonPath, "power1_average"), []byte("150000000\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fan RPM
|
||||
if err := os.WriteFile(filepath.Join(hwmonPath, "fan1_input"), []byte("1500\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewAMDGPUCollector(sysPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if !stats.Available {
|
||||
t.Error("Expected GPU to be available")
|
||||
}
|
||||
|
||||
if stats.Utilization != 75 {
|
||||
t.Errorf("Expected utilization 75, got %d", stats.Utilization)
|
||||
}
|
||||
|
||||
if stats.VRAMUsed != 4294967296 {
|
||||
t.Errorf("Expected VRAM used 4294967296, got %d", stats.VRAMUsed)
|
||||
}
|
||||
|
||||
if stats.VRAMTotal != 17179869184 {
|
||||
t.Errorf("Expected VRAM total 17179869184, got %d", stats.VRAMTotal)
|
||||
}
|
||||
|
||||
if stats.ClockGPU != 1200 {
|
||||
t.Errorf("Expected GPU clock 1200, got %d", stats.ClockGPU)
|
||||
}
|
||||
|
||||
if stats.ClockMemory != 875 {
|
||||
t.Errorf("Expected memory clock 875, got %d", stats.ClockMemory)
|
||||
}
|
||||
|
||||
if stats.Temperature != 65.0 {
|
||||
t.Errorf("Expected temperature 65.0, got %f", stats.Temperature)
|
||||
}
|
||||
|
||||
if stats.PowerWatts != 150.0 {
|
||||
t.Errorf("Expected power 150.0W, got %f", stats.PowerWatts)
|
||||
}
|
||||
|
||||
if stats.FanRPM != 1500 {
|
||||
t.Errorf("Expected fan 1500 RPM, got %d", stats.FanRPM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMDGPUCollector_NoGPU(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
collector := NewAMDGPUCollector(tmpDir)
|
||||
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if stats.Available {
|
||||
t.Error("Expected GPU to be unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMDGPUCollector_PartialData(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sysPath := filepath.Join(tmpDir, "sys")
|
||||
|
||||
// Create minimal GPU structure (only utilization)
|
||||
gpuPath := filepath.Join(sysPath, "class/drm/card0/device")
|
||||
if err := os.MkdirAll(gpuPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create driver symlink (required for AMD GPU detection)
|
||||
driverTarget := filepath.Join(tmpDir, "drivers/amdgpu")
|
||||
if err := os.MkdirAll(driverTarget, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(driverTarget, filepath.Join(gpuPath, "driver")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(gpuPath, "gpu_busy_percent"), []byte("50\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewAMDGPUCollector(sysPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if !stats.Available {
|
||||
t.Error("Expected GPU to be available with partial data")
|
||||
}
|
||||
|
||||
if stats.Utilization != 50 {
|
||||
t.Errorf("Expected utilization 50, got %d", stats.Utilization)
|
||||
}
|
||||
}
|
||||
71
backend/internal/collectors/disk_test.go
Normal file
71
backend/internal/collectors/disk_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDiskCollector_IOStats(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
procPath := filepath.Join(tmpDir, "proc")
|
||||
|
||||
if err := os.MkdirAll(procPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create mock /proc/diskstats
|
||||
diskstatsContent := ` 8 0 sda 12345 6789 1234567 12345 54321 9876 7654321 54321 0 12345 66666 0 0 0 0 0 0
|
||||
8 1 sda1 12340 6780 1234000 12340 54320 9870 7654000 54320 0 12340 66660 0 0 0 0 0 0
|
||||
259 0 nvme0n1 98765 4321 9876543 98765 87654 3210 8765432 87654 0 98765 186419 0 0 0 0 0 0
|
||||
259 1 nvme0n1p1 98760 4320 9876000 98760 87650 3200 8765000 87650 0 98760 186410 0 0 0 0 0 0
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(procPath, "diskstats"), []byte(diskstatsContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create mock /proc/mounts (will fail statfs but shouldn't crash)
|
||||
mountsContent := `/dev/sda1 /boot ext4 rw,relatime 0 0
|
||||
/dev/nvme0n1p2 / ext4 rw,relatime 0 0
|
||||
tmpfs /tmp tmpfs rw 0 0
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(procPath, "mounts"), []byte(mountsContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewDiskCollector(procPath, filepath.Join(procPath, "mounts"))
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
// Should have IO stats for sda and nvme0n1 (not partitions)
|
||||
if len(stats.IO) < 2 {
|
||||
t.Errorf("Expected at least 2 IO entries, got %d", len(stats.IO))
|
||||
}
|
||||
|
||||
// Verify we're not including partitions
|
||||
for _, io := range stats.IO {
|
||||
if io.Device == "sda1" || io.Device == "nvme0n1p1" {
|
||||
t.Errorf("Should not include partition %s", io.Device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskCollector_MissingFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
collector := NewDiskCollector(tmpDir, filepath.Join(tmpDir, "nonexistent"))
|
||||
|
||||
stats, err := collector.Collect()
|
||||
// Should not error, just return empty stats
|
||||
if err != nil {
|
||||
t.Logf("Error (acceptable): %v", err)
|
||||
}
|
||||
|
||||
if stats.Mounts == nil {
|
||||
t.Error("Mounts should not be nil")
|
||||
}
|
||||
if stats.IO == nil {
|
||||
t.Error("IO should not be nil")
|
||||
}
|
||||
}
|
||||
100
backend/internal/collectors/processes_test.go
Normal file
100
backend/internal/collectors/processes_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessCollector(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
procPath := filepath.Join(tmpDir, "proc")
|
||||
|
||||
// Create mock process directories
|
||||
pid1 := filepath.Join(procPath, "1")
|
||||
pid2 := filepath.Join(procPath, "100")
|
||||
if err := os.MkdirAll(pid1, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(pid2, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/1/stat
|
||||
// Format: pid (comm) state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime ...
|
||||
stat1 := "1 (systemd) S 0 1 1 0 -1 4194560 12345 67890 100 200 1000 500 0 0 20 0 1 0 1 123456789 2000 18446744073709551615 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0"
|
||||
if err := os.WriteFile(filepath.Join(pid1, "stat"), []byte(stat1), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stat2 := "100 (bash) S 1 100 100 0 -1 4194304 5678 1234 50 100 500 250 0 0 20 0 1 0 100 98765432 1500 18446744073709551615 0 0 0 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0"
|
||||
if err := os.WriteFile(filepath.Join(pid2, "stat"), []byte(stat2), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/1/cmdline
|
||||
if err := os.WriteFile(filepath.Join(pid1, "cmdline"), []byte("/usr/lib/systemd/systemd\x00--system\x00"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(pid2, "cmdline"), []byte("/bin/bash\x00"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/uptime for CPU calculation
|
||||
if err := os.WriteFile(filepath.Join(procPath, "uptime"), []byte("12345.67 98765.43\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/meminfo for memory percentage
|
||||
meminfo := `MemTotal: 32000000 kB
|
||||
MemFree: 16000000 kB
|
||||
MemAvailable: 20000000 kB
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(procPath, "meminfo"), []byte(meminfo), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewProcessCollector(procPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if stats.Total < 2 {
|
||||
t.Errorf("Expected at least 2 processes, got %d", stats.Total)
|
||||
}
|
||||
|
||||
// Check that we have some processes in the lists
|
||||
if len(stats.TopByCPU) == 0 && len(stats.TopByMemory) == 0 {
|
||||
t.Error("Expected some processes in top lists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCollector_EmptyProc(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
procPath := filepath.Join(tmpDir, "proc")
|
||||
if err := os.MkdirAll(procPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create meminfo (required)
|
||||
meminfo := `MemTotal: 32000000 kB
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(procPath, "meminfo"), []byte(meminfo), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewProcessCollector(procPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if stats.TopByCPU == nil {
|
||||
t.Error("TopByCPU should not be nil")
|
||||
}
|
||||
if stats.TopByMemory == nil {
|
||||
t.Error("TopByMemory should not be nil")
|
||||
}
|
||||
}
|
||||
62
backend/internal/collectors/system_test.go
Normal file
62
backend/internal/collectors/system_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSystemCollector(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
procPath := filepath.Join(tmpDir, "proc")
|
||||
|
||||
if err := os.MkdirAll(procPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/version (format: "Linux version 6.1.0-test ...")
|
||||
versionContent := "Linux version 6.1.0-test (gcc) #1 SMP PREEMPT\n"
|
||||
if err := os.WriteFile(filepath.Join(procPath, "version"), []byte(versionContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mock /proc/uptime
|
||||
if err := os.WriteFile(filepath.Join(procPath, "uptime"), []byte("12345.67 98765.43\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewSystemCollector(procPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
// Hostname uses os.Hostname(), so we can't mock it easily
|
||||
// Just check it's not empty
|
||||
if stats.Hostname == "" {
|
||||
t.Error("Expected non-empty hostname")
|
||||
}
|
||||
|
||||
if stats.Kernel != "6.1.0-test" {
|
||||
t.Errorf("Expected kernel '6.1.0-test', got '%s'", stats.Kernel)
|
||||
}
|
||||
|
||||
if stats.Uptime != 12345 {
|
||||
t.Errorf("Expected uptime 12345, got %d", stats.Uptime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemCollector_MissingFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
collector := NewSystemCollector(tmpDir)
|
||||
|
||||
stats, err := collector.Collect()
|
||||
// Should not crash, may have errors but returns partial data
|
||||
if err != nil {
|
||||
t.Logf("Error (may be acceptable): %v", err)
|
||||
}
|
||||
|
||||
// Should have some default values or empty strings
|
||||
t.Logf("Stats with missing files: hostname=%s, kernel=%s, uptime=%d",
|
||||
stats.Hostname, stats.Kernel, stats.Uptime)
|
||||
}
|
||||
110
backend/internal/collectors/temperature_test.go
Normal file
110
backend/internal/collectors/temperature_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTemperatureCollector(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sysPath := filepath.Join(tmpDir, "sys")
|
||||
|
||||
// Create mock hwmon structure
|
||||
hwmon0 := filepath.Join(sysPath, "class/hwmon/hwmon0")
|
||||
if err := os.MkdirAll(hwmon0, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sensor name
|
||||
if err := os.WriteFile(filepath.Join(hwmon0, "name"), []byte("k10temp\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Temperature input (65.5°C in millidegrees)
|
||||
if err := os.WriteFile(filepath.Join(hwmon0, "temp1_input"), []byte("65500\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Temperature label
|
||||
if err := os.WriteFile(filepath.Join(hwmon0, "temp1_label"), []byte("Tctl\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Critical temperature
|
||||
if err := os.WriteFile(filepath.Join(hwmon0, "temp1_crit"), []byte("95000\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
collector := NewTemperatureCollector(sysPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if len(stats.Sensors) == 0 {
|
||||
t.Fatal("Expected at least one sensor")
|
||||
}
|
||||
|
||||
sensor := stats.Sensors[0]
|
||||
if sensor.Name != "k10temp" {
|
||||
t.Errorf("Expected name 'k10temp', got '%s'", sensor.Name)
|
||||
}
|
||||
if sensor.Label != "Tctl" {
|
||||
t.Errorf("Expected label 'Tctl', got '%s'", sensor.Label)
|
||||
}
|
||||
if sensor.Temperature != 65.5 {
|
||||
t.Errorf("Expected temperature 65.5, got %f", sensor.Temperature)
|
||||
}
|
||||
if sensor.Critical != 95.0 {
|
||||
t.Errorf("Expected critical 95.0, got %f", sensor.Critical)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemperatureCollector_MultipleSensors(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sysPath := filepath.Join(tmpDir, "sys")
|
||||
|
||||
// Create two hwmon entries
|
||||
for i, name := range []string{"hwmon0", "hwmon1"} {
|
||||
hwmon := filepath.Join(sysPath, "class/hwmon", name)
|
||||
if err := os.MkdirAll(hwmon, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sensorName := []string{"k10temp", "nvme"}[i]
|
||||
if err := os.WriteFile(filepath.Join(hwmon, "name"), []byte(sensorName+"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
temp := []string{"55000", "45000"}[i]
|
||||
if err := os.WriteFile(filepath.Join(hwmon, "temp1_input"), []byte(temp+"\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
collector := NewTemperatureCollector(sysPath)
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
t.Fatalf("Collect failed: %v", err)
|
||||
}
|
||||
|
||||
if len(stats.Sensors) != 2 {
|
||||
t.Errorf("Expected 2 sensors, got %d", len(stats.Sensors))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemperatureCollector_MissingHwmon(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
collector := NewTemperatureCollector(tmpDir)
|
||||
|
||||
stats, err := collector.Collect()
|
||||
if err != nil {
|
||||
// Error is acceptable
|
||||
return
|
||||
}
|
||||
|
||||
if stats.Sensors == nil {
|
||||
t.Error("Sensors should not be nil")
|
||||
}
|
||||
}
|
||||
2367
frontend/package-lock.json
generated
2367
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,24 @@
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@testing-library/svelte": "^5.2.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8.4.49",
|
||||
"svelte": "^5.11.0",
|
||||
"svelte-check": "^4.1.0",
|
||||
"tailwindcss": "^3.4.16",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.3"
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
33
frontend/src/lib/stores/theme.test.ts
Normal file
33
frontend/src/lib/stores/theme.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
import { theme } from './theme';
|
||||
|
||||
describe('theme store', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has a valid initial value', () => {
|
||||
const value = get(theme);
|
||||
expect(['dark', 'light']).toContain(value);
|
||||
});
|
||||
|
||||
it('can toggle theme', () => {
|
||||
const initial = get(theme);
|
||||
theme.toggle();
|
||||
const toggled = get(theme);
|
||||
expect(toggled).not.toBe(initial);
|
||||
|
||||
// Toggle back
|
||||
theme.toggle();
|
||||
expect(get(theme)).toBe(initial);
|
||||
});
|
||||
|
||||
it('can set theme directly', () => {
|
||||
theme.set('light');
|
||||
expect(get(theme)).toBe('light');
|
||||
|
||||
theme.set('dark');
|
||||
expect(get(theme)).toBe('dark');
|
||||
});
|
||||
});
|
||||
108
frontend/src/lib/utils/formatters.test.ts
Normal file
108
frontend/src/lib/utils/formatters.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
formatBytes,
|
||||
formatPercent,
|
||||
formatUptime,
|
||||
formatFrequency,
|
||||
formatTemperature,
|
||||
formatWatts
|
||||
} from './formatters';
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('formats 0 bytes', () => {
|
||||
expect(formatBytes(0)).toBe('0 B');
|
||||
});
|
||||
|
||||
it('formats bytes', () => {
|
||||
expect(formatBytes(512)).toBe('512 B');
|
||||
});
|
||||
|
||||
it('formats kilobytes', () => {
|
||||
expect(formatBytes(1024)).toBe('1 KB');
|
||||
expect(formatBytes(1536)).toBe('1.5 KB');
|
||||
});
|
||||
|
||||
it('formats megabytes', () => {
|
||||
expect(formatBytes(1024 * 1024)).toBe('1 MB');
|
||||
expect(formatBytes(1.5 * 1024 * 1024)).toBe('1.5 MB');
|
||||
});
|
||||
|
||||
it('formats gigabytes', () => {
|
||||
expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB');
|
||||
expect(formatBytes(16 * 1024 * 1024 * 1024)).toBe('16 GB');
|
||||
});
|
||||
|
||||
it('formats terabytes', () => {
|
||||
expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe('1 TB');
|
||||
});
|
||||
|
||||
it('respects decimals parameter', () => {
|
||||
expect(formatBytes(1536, 2)).toBe('1.5 KB');
|
||||
expect(formatBytes(1536, 0)).toBe('2 KB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPercent', () => {
|
||||
it('formats percentage with default decimals', () => {
|
||||
expect(formatPercent(50)).toBe('50.0%');
|
||||
expect(formatPercent(99.9)).toBe('99.9%');
|
||||
});
|
||||
|
||||
it('respects decimals parameter', () => {
|
||||
expect(formatPercent(33.333, 2)).toBe('33.33%');
|
||||
expect(formatPercent(33.333, 0)).toBe('33%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatUptime', () => {
|
||||
it('formats less than a minute', () => {
|
||||
expect(formatUptime(30)).toBe('< 1m');
|
||||
expect(formatUptime(0)).toBe('< 1m');
|
||||
});
|
||||
|
||||
it('formats minutes', () => {
|
||||
expect(formatUptime(60)).toBe('1m');
|
||||
expect(formatUptime(300)).toBe('5m');
|
||||
});
|
||||
|
||||
it('formats hours and minutes', () => {
|
||||
expect(formatUptime(3600)).toBe('1h');
|
||||
expect(formatUptime(3660)).toBe('1h 1m');
|
||||
expect(formatUptime(7200)).toBe('2h');
|
||||
});
|
||||
|
||||
it('formats days, hours and minutes', () => {
|
||||
expect(formatUptime(86400)).toBe('1d');
|
||||
expect(formatUptime(90000)).toBe('1d 1h');
|
||||
expect(formatUptime(90060)).toBe('1d 1h 1m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatFrequency', () => {
|
||||
it('formats MHz', () => {
|
||||
expect(formatFrequency(500)).toBe('500 MHz');
|
||||
expect(formatFrequency(800)).toBe('800 MHz');
|
||||
});
|
||||
|
||||
it('formats GHz', () => {
|
||||
expect(formatFrequency(1000)).toBe('1.00 GHz');
|
||||
expect(formatFrequency(3500)).toBe('3.50 GHz');
|
||||
expect(formatFrequency(4200)).toBe('4.20 GHz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTemperature', () => {
|
||||
it('formats temperature in Celsius', () => {
|
||||
expect(formatTemperature(65)).toBe('65.0°C');
|
||||
expect(formatTemperature(45.5)).toBe('45.5°C');
|
||||
expect(formatTemperature(0)).toBe('0.0°C');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatWatts', () => {
|
||||
it('formats power in Watts', () => {
|
||||
expect(formatWatts(150)).toBe('150.0 W');
|
||||
expect(formatWatts(42.5)).toBe('42.5 W');
|
||||
expect(formatWatts(0)).toBe('0.0 W');
|
||||
});
|
||||
});
|
||||
4
frontend/src/tests/mocks/app/environment.ts
Normal file
4
frontend/src/tests/mocks/app/environment.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const browser = true;
|
||||
export const dev = false;
|
||||
export const building = false;
|
||||
export const version = 'test';
|
||||
17
frontend/src/tests/mocks/app/stores.ts
Normal file
17
frontend/src/tests/mocks/app/stores.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { writable, readable } from 'svelte/store';
|
||||
|
||||
export const page = readable({
|
||||
url: new URL('http://localhost'),
|
||||
params: {},
|
||||
route: { id: '/' },
|
||||
status: 200,
|
||||
error: null,
|
||||
data: {},
|
||||
form: null
|
||||
});
|
||||
|
||||
export const navigating = readable(null);
|
||||
export const updated = {
|
||||
subscribe: readable(false).subscribe,
|
||||
check: async () => false
|
||||
};
|
||||
37
frontend/src/tests/setup.ts
Normal file
37
frontend/src/tests/setup.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import '@testing-library/svelte/vitest';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
// Mock matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => true
|
||||
})
|
||||
});
|
||||
18
frontend/vitest.config.ts
Normal file
18
frontend/vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte({ hot: !process.env.VITEST })],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['src/tests/setup.ts']
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib: '/src/lib',
|
||||
$app: '/src/tests/mocks/app'
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user