import { afterEach, expect, it } from 'vitest'; import { flushSync, mount, unmount } from 'svelte'; import TemperatureChart from '../../src/lib/ui/components/capacity/TemperatureChart.svelte'; import type { CapacityHistory } from '../../src/lib/capacity.js'; let component: ReturnType | undefined; afterEach(async () => { if (component) await unmount(component); component = undefined; document.body.replaceChildren(); }); function history(): CapacityHistory { return { range: '1h', startAt: 1000, endAt: 4000, bucketMs: 1000, points: [1000, 2000, 3000].map((at) => ({ at, cpuCoresAvailable: null, memoryBytesAvailable: null, reportingHosts: 0, expectedHosts: 1 })), temperatures: [ { id: 'host:cpu', hostId: 'host', label: 'CPU temperature', kind: 'cpu', points: [65, null, 67].map((celsius, i) => ({ at: 1000 + i * 1000, celsius })) }, { id: 'host:drive', hostId: 'host', label: 'NVMe drive 1', kind: 'nvme', points: [49, 50.9, 52].map((celsius, i) => ({ at: 1000 + i * 1000, celsius })) } ] }; } function show(report = history()) { component = mount(TemperatureChart, { target: document.body, props: { history: report, scope: 'ec-2' } }); flushSync(); } it('defaults to CPU and toggles device lines and readouts together', () => { show(); const buttons = document.querySelectorAll('.legend button'); expect(buttons[0].getAttribute('aria-pressed')).toBe('true'); expect(buttons[1].getAttribute('aria-pressed')).toBe('false'); expect(document.querySelectorAll('[data-series]')).toHaveLength(1); // A missing middle bucket splits the CPU line into two isolated segments. expect(document.querySelectorAll('[data-series="host:cpu"] path')).toHaveLength(2); buttons[1].click(); flushSync(); expect(document.querySelectorAll('[data-series]')).toHaveLength(2); expect(document.querySelector('.readout')?.textContent).toContain('52 °C'); buttons[0].click(); buttons[1].click(); flushSync(); expect(document.querySelectorAll('[data-series]')).toHaveLength(0); expect(document.body.textContent).toContain('Select a sensor to show history'); expect(document.querySelector('.readout')).toBeNull(); }); it('supports keyboard and pointer inspection without turning missing readings into zero', () => { show(); const plot = document.querySelector('[role="slider"]')!; expect(plot.getAttribute('aria-valuenow')).toBe('2'); plot.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })); flushSync(); expect(plot.getAttribute('aria-valuetext')).toContain('65 °C'); plot.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); flushSync(); expect(plot.getAttribute('aria-valuetext')).toContain('Unavailable'); expect(document.querySelector('.readout')?.textContent).not.toContain('0 °C'); Object.defineProperty(plot, 'getBoundingClientRect', { value: () => ({ left: 0, width: 1000 }) }); plot.dispatchEvent( new PointerEvent('pointerdown', { clientX: 8, pointerType: 'touch', bubbles: true }) ); flushSync(); expect(plot.getAttribute('aria-valuenow')).toBe('0'); expect(document.querySelector('.readout')?.textContent).toContain('65 °C'); }); it('shows unsupported hosts as unavailable', () => { const report = history(); report.temperatures[0].points = report.temperatures[0].points.map((point) => ({ ...point, celsius: null })); show(report); expect(document.body.textContent).toContain('No temperature readings in this period'); expect(document.body.textContent).toContain('Unavailable'); }); it('plots a genuine zero-degree reading', () => { const report = history(); report.temperatures[0].points[2].celsius = 0; show(report); expect(document.querySelector('.readout')?.textContent).toContain('0 °C'); expect(document.querySelector('.empty-chart')).toBeNull(); });