532 lines
22 KiB
TypeScript
532 lines
22 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { DownloadItem, DownloadStatus, UiSnapshot } from "../src/shared/types";
|
|
import { appendBandwidthSample, readBandwidthChartPalette, readDownloadSpeedSparklinePalette } from "../src/renderer/App";
|
|
import {
|
|
buildStatisticsViewModel,
|
|
type StatisticsMetric
|
|
} from "../src/renderer/views/statistics/statistics-model";
|
|
import {
|
|
StatisticsContent,
|
|
StatisticsSidebar,
|
|
StatisticsView,
|
|
type StatisticsViewActions
|
|
} from "../src/renderer/views/statistics/StatisticsView";
|
|
import { createVisualFixture } from "./visual/fixtures";
|
|
import {
|
|
createStatisticsLedger,
|
|
recordStatisticsActiveInterval,
|
|
recordStatisticsBytes,
|
|
recordStatisticsOutcome
|
|
} from "../src/main/statistics-ledger";
|
|
|
|
const now = new Date(2026, 7, 10, 12, 0, 0, 0).getTime();
|
|
|
|
describe("bandwidth sampling", () => {
|
|
it("keeps the latest minute and normalizes invalid speeds", () => {
|
|
const history = [
|
|
{ time: now - 61000, speed: 1 },
|
|
{ time: now - 59000, speed: 2 }
|
|
];
|
|
|
|
expect(appendBandwidthSample(history, Number.NaN, now)).toEqual([
|
|
{ time: now - 59000, speed: 2 },
|
|
{ time: now, speed: 0 }
|
|
]);
|
|
expect(history).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
function createSnapshot(): UiSnapshot {
|
|
return structuredClone(createVisualFixture("empty").snapshot);
|
|
}
|
|
|
|
function item(
|
|
id: string,
|
|
status: DownloadStatus,
|
|
overrides: Partial<DownloadItem> = {}
|
|
): DownloadItem {
|
|
return {
|
|
id,
|
|
packageId: "statistics-package",
|
|
url: `https://url-host-${id}.example/file`,
|
|
provider: "realdebrid",
|
|
providerLabel: "Real-Debrid",
|
|
status,
|
|
retries: 0,
|
|
speedBps: 0,
|
|
downloadedBytes: 100,
|
|
totalBytes: 100,
|
|
progressPercent: status === "completed" ? 100 : 50,
|
|
fileName: `${id}.bin`,
|
|
targetPath: `C:\\Downloads\\${id}.bin`,
|
|
resumable: true,
|
|
attempts: 1,
|
|
lastError: status === "failed" ? "Fehlgeschlagen" : "",
|
|
fullStatus: status,
|
|
createdAt: now - 1000,
|
|
updatedAt: now,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function setItems(snapshot: UiSnapshot, items: DownloadItem[]): void {
|
|
snapshot.session.items = Object.fromEntries(items.map((entry) => [entry.id, entry]));
|
|
}
|
|
|
|
function visitElements(node: ReactNode, visit: (element: ReactElement) => void): void {
|
|
if (Array.isArray(node)) {
|
|
node.forEach((child) => visitElements(child, visit));
|
|
return;
|
|
}
|
|
if (!isValidElement(node)) {
|
|
return;
|
|
}
|
|
visit(node);
|
|
visitElements(node.props.children, visit);
|
|
}
|
|
|
|
function findButton(node: ReactNode, label: string): ReactElement {
|
|
let result: ReactElement | null = null;
|
|
visitElements(node, (element) => {
|
|
if (!result && element.type === "button" && element.props.children === label) {
|
|
result = element;
|
|
}
|
|
});
|
|
if (!result) {
|
|
throw new Error(`Button not found: ${label}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function createActions(overrides: Partial<StatisticsViewActions> = {}): StatisticsViewActions {
|
|
return {
|
|
onRangeChange: () => {},
|
|
onResetSession: () => {},
|
|
onResetAll: () => {},
|
|
onResetErrors: () => {},
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function expectUnavailable(metric: StatisticsMetric): void {
|
|
expect(metric).toMatchObject({ value: null, available: false });
|
|
expect(metric.sourceLabel.trim()).not.toBe("");
|
|
}
|
|
|
|
describe("statistics model", () => {
|
|
it("uses real snapshot session fields and excludes active or waiting downloads from the success denominator", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloaded = 600;
|
|
snapshot.stats.totalFilesSession = 3;
|
|
snapshot.session.running = true;
|
|
setItems(snapshot, [
|
|
item("complete-a", "completed"),
|
|
item("complete-b", "completed"),
|
|
item("complete-c", "completed"),
|
|
item("failed", "failed"),
|
|
item("active", "downloading"),
|
|
item("waiting", "queued")
|
|
]);
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "session", now);
|
|
|
|
expect(model.metrics.downloadedBytes.value).toBe(600);
|
|
expect(model.metrics.files.value).toBe(3);
|
|
expect(model.metrics.successRate.value).toBe(75);
|
|
expect(model.metrics.errors.value).toBe(1);
|
|
});
|
|
|
|
it("reports no success rate when the current queue has no completed or failed result", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.session.running = true;
|
|
setItems(snapshot, [item("active", "downloading"), item("waiting", "queued")]);
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "session", now);
|
|
|
|
expectUnavailable(model.metrics.successRate);
|
|
expect(model.metrics.errors).toMatchObject({ value: 0, available: true });
|
|
});
|
|
|
|
it("sorts current-queue providers by bytes and then id without deriving them from URL hostnames", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.session.running = true;
|
|
setItems(snapshot, [
|
|
item("real", "completed", {
|
|
url: "https://alldebrid.invalid/wrong-source",
|
|
provider: "realdebrid",
|
|
providerLabel: "Real-Debrid Konto",
|
|
downloadedBytes: 200
|
|
}),
|
|
item("all", "failed", {
|
|
url: "https://realdebrid.invalid/wrong-source",
|
|
provider: "alldebrid",
|
|
providerLabel: "AllDebrid",
|
|
downloadedBytes: 200
|
|
}),
|
|
item("link", "completed", {
|
|
url: "https://realdebrid.invalid/also-wrong",
|
|
provider: "debridlink",
|
|
providerLabel: "Debrid-Link",
|
|
downloadedBytes: 400
|
|
}),
|
|
item("unknown", "completed", {
|
|
url: "https://hoster-only.invalid/not-a-provider",
|
|
provider: null,
|
|
providerLabel: undefined,
|
|
downloadedBytes: 900
|
|
})
|
|
]);
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "session", now);
|
|
|
|
expect(model.providers.map((row) => row.id)).toEqual(["debridlink", "alldebrid", "realdebrid"]);
|
|
expect(model.providers.map((row) => row.label)).toEqual(["Debrid-Link", "AllDebrid", "Real-Debrid Konto"]);
|
|
expect(model.providers.map((row) => [row.completed, row.failed])).toEqual([[1, 0], [0, 1], [1, 0]]);
|
|
expect(model.providers.some((row) => row.id.includes("host"))).toBe(false);
|
|
});
|
|
|
|
it("uses persisted daily bytes, results and active time for every statistic shown today", () => {
|
|
const snapshot = createSnapshot();
|
|
let ledger = createStatisticsLedger(now);
|
|
ledger = recordStatisticsBytes(ledger, "realdebrid", 500, now);
|
|
ledger = recordStatisticsOutcome(ledger, "realdebrid", "completed", now);
|
|
ledger = recordStatisticsBytes(ledger, "alldebrid", 1_500, now);
|
|
ledger = recordStatisticsOutcome(ledger, "alldebrid", "failed", now);
|
|
ledger = recordStatisticsActiveInterval(ledger, now - 2_000, now);
|
|
snapshot.stats.statistics = ledger;
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "today", now);
|
|
|
|
expect(model.metrics.downloadedBytes).toMatchObject({ value: 2_000, available: true });
|
|
expect(model.metrics.files).toMatchObject({ value: 1, available: true });
|
|
expect(model.metrics.successRate).toMatchObject({ value: 50, available: true });
|
|
expect(model.metrics.errors).toMatchObject({ value: 1, available: true });
|
|
expect(model.metrics.averageSpeedBps).toMatchObject({ value: 1_000, available: true });
|
|
expect(model.providers.map((row) => [row.id, row.bytes])).toEqual([
|
|
["alldebrid", 1_500],
|
|
["realdebrid", 500]
|
|
]);
|
|
expect(model.providers.map((row) => [row.completed, row.failed])).toEqual([[0, 1], [1, 0]]);
|
|
});
|
|
|
|
it("treats a stale daily key as a genuine zero today without stale provider rows", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.settings.providerDailyUsageDay = "2026-08-09";
|
|
snapshot.settings.providerDailyUsageBytes = { realdebrid: 900 };
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "today", now);
|
|
|
|
expect(model.metrics.downloadedBytes).toMatchObject({ value: 0, available: true });
|
|
expect(model.providers).toEqual([]);
|
|
});
|
|
|
|
it("sums every available day in seven-day and 30-day windows without waiting for a full period", () => {
|
|
const snapshot = createSnapshot();
|
|
let ledger = createStatisticsLedger(new Date(2026, 7, 3, 12).getTime());
|
|
ledger = recordStatisticsBytes(ledger, "realdebrid", 300, new Date(2026, 7, 3, 12).getTime());
|
|
ledger = recordStatisticsOutcome(ledger, "realdebrid", "completed", new Date(2026, 7, 3, 12).getTime());
|
|
ledger = recordStatisticsBytes(ledger, "debridlink", 700, new Date(2026, 7, 8, 12).getTime());
|
|
ledger = recordStatisticsOutcome(ledger, "debridlink", "failed", new Date(2026, 7, 8, 12).getTime());
|
|
ledger = recordStatisticsBytes(ledger, "realdebrid", 500, now);
|
|
ledger = recordStatisticsOutcome(ledger, "realdebrid", "completed", now);
|
|
snapshot.stats.statistics = ledger;
|
|
|
|
const week = buildStatisticsViewModel(snapshot, "week", now);
|
|
const month = buildStatisticsViewModel(snapshot, "month", now);
|
|
|
|
expect(week.metrics.downloadedBytes.value).toBe(1_200);
|
|
expect(week.metrics.files.value).toBe(1);
|
|
expect(week.metrics.errors.value).toBe(1);
|
|
expect(week.message).toContain("2 erfasste Tage");
|
|
expect(month.metrics.downloadedBytes.value).toBe(1_500);
|
|
expect(month.metrics.files.value).toBe(2);
|
|
expect(month.metrics.errors.value).toBe(1);
|
|
expect(month.message).toContain("3 erfasste Tage");
|
|
});
|
|
|
|
it("combines existing all-time counters with persisted outcomes, provider results and measured average speed", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloadedAllTime = 25_000;
|
|
snapshot.stats.totalFilesAllTime = 42;
|
|
snapshot.settings.providerTotalUsageBytes = { realdebrid: 5_000, debridlink: 20_000 };
|
|
let ledger = createStatisticsLedger(now - 10_000);
|
|
ledger = recordStatisticsBytes(ledger, "realdebrid", 2_000, now);
|
|
ledger = recordStatisticsActiveInterval(ledger, now - 2_000, now);
|
|
ledger = recordStatisticsOutcome(ledger, "realdebrid", "completed", now);
|
|
ledger = recordStatisticsOutcome(ledger, "realdebrid", "completed", now);
|
|
ledger = recordStatisticsOutcome(ledger, "debridlink", "failed", now);
|
|
snapshot.stats.statistics = ledger;
|
|
snapshot.summary = {
|
|
total: 10,
|
|
success: 9,
|
|
failed: 1,
|
|
cancelled: 0,
|
|
extracted: 9,
|
|
durationSeconds: 10,
|
|
averageSpeedBps: 2_500
|
|
};
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "all", now);
|
|
|
|
expect(model.metrics.downloadedBytes.value).toBe(25_000);
|
|
expect(model.metrics.files.value).toBe(42);
|
|
expect(model.providers.map((row) => [row.id, row.bytes])).toEqual([
|
|
["debridlink", 20_000],
|
|
["realdebrid", 5_000]
|
|
]);
|
|
expect(model.providers.map((row) => [row.completed, row.failed])).toEqual([[0, 1], [2, 0]]);
|
|
expect(model.metrics.successRate.available).toBe(true);
|
|
expect(model.metrics.successRate.value).toBeCloseTo(200 / 3);
|
|
expect(model.metrics.errors).toMatchObject({ value: 1, available: true });
|
|
expect(model.metrics.averageSpeedBps).toMatchObject({ value: 1_000, available: true });
|
|
});
|
|
|
|
it("prefers live queue outcomes over an old summary and uses the summary only after the run ends", () => {
|
|
const snapshot = createSnapshot();
|
|
setItems(snapshot, [
|
|
item("complete-a", "completed"),
|
|
item("complete-b", "completed"),
|
|
item("complete-c", "completed"),
|
|
item("failed", "failed")
|
|
]);
|
|
snapshot.summary = {
|
|
total: 4,
|
|
success: 1,
|
|
failed: 3,
|
|
cancelled: 0,
|
|
extracted: 1,
|
|
durationSeconds: 100,
|
|
averageSpeedBps: 500
|
|
};
|
|
snapshot.session.running = true;
|
|
|
|
const active = buildStatisticsViewModel(snapshot, "session", now);
|
|
snapshot.session.running = false;
|
|
const ended = buildStatisticsViewModel(snapshot, "session", now);
|
|
|
|
expect(active.metrics.successRate.value).toBe(75);
|
|
expect(active.metrics.errors.value).toBe(1);
|
|
expectUnavailable(active.metrics.averageSpeedBps);
|
|
expect(ended.metrics.successRate.value).toBe(25);
|
|
expect(ended.metrics.errors.value).toBe(3);
|
|
expect(ended.metrics.averageSpeedBps).toMatchObject({ value: 500, available: true });
|
|
});
|
|
|
|
it("models empty, idle, active and paused session states separately", () => {
|
|
const empty = createSnapshot();
|
|
const idle = createSnapshot();
|
|
setItems(idle, [item("idle", "completed")]);
|
|
const active = createSnapshot();
|
|
active.session.running = true;
|
|
setItems(active, [item("active", "downloading")]);
|
|
const paused = createSnapshot();
|
|
paused.session.running = true;
|
|
paused.session.paused = true;
|
|
setItems(paused, [item("paused", "paused")]);
|
|
|
|
expect(buildStatisticsViewModel(empty, "session", now).sessionState).toBe("empty");
|
|
expect(buildStatisticsViewModel(idle, "session", now).sessionState).toBe("idle");
|
|
expect(buildStatisticsViewModel(active, "session", now).sessionState).toBe("active");
|
|
expect(buildStatisticsViewModel(paused, "session", now).sessionState).toBe("paused");
|
|
});
|
|
});
|
|
|
|
describe("statistics view", () => {
|
|
it("marks statistic ranges for one measured vertical selection indicator", () => {
|
|
const model = buildStatisticsViewModel(createSnapshot(), "today", now);
|
|
const html = renderToStaticMarkup(<StatisticsSidebar actions={createActions()} model={model} />);
|
|
|
|
expect(html).toContain("ui-sliding-selection ui-sliding-selection-vertical");
|
|
expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(5);
|
|
expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1);
|
|
});
|
|
|
|
it("renders each statistics marker exactly once, all ranges and no download toolbar or pagination", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloaded = 2_048;
|
|
snapshot.stats.totalFilesSession = 1;
|
|
setItems(snapshot, [item("complete", "completed")]);
|
|
const html = renderToStaticMarkup(
|
|
<StatisticsView
|
|
actions={createActions()}
|
|
chart={<div>Bestehender Bandbreitenverlauf</div>}
|
|
model={buildStatisticsViewModel(snapshot, "session", now)}
|
|
/>
|
|
);
|
|
|
|
for (const marker of ["statistics-sidebar", "statistics-kpis", "statistics-chart"]) {
|
|
expect(html.match(new RegExp(`data-visual-region=\\"${marker}\\"`, "g"))).toHaveLength(1);
|
|
}
|
|
for (const label of ["Sitzung", "Heute", "Sieben Tage", "30 Tage", "Gesamt"]) {
|
|
expect(html).toContain(`>${label}<`);
|
|
}
|
|
expect(html).toContain("Bestehender Bandbreitenverlauf");
|
|
expect(html).not.toContain("downloads-toolbar");
|
|
expect(html.toLocaleLowerCase("de-DE")).not.toContain("pagination");
|
|
});
|
|
|
|
it("dispatches range and reset controls only through the supplied callbacks", () => {
|
|
const snapshot = createSnapshot();
|
|
setItems(snapshot, [item("failed", "failed")]);
|
|
const model = buildStatisticsViewModel(snapshot, "session", now);
|
|
const calls: string[] = [];
|
|
const actions = createActions({
|
|
onRangeChange: (range) => calls.push(`range:${range}`),
|
|
onResetSession: () => calls.push("reset:session"),
|
|
onResetAll: () => calls.push("reset:all"),
|
|
onResetErrors: () => calls.push("reset:errors")
|
|
});
|
|
const sidebar = StatisticsSidebar({ actions, model });
|
|
const content = StatisticsContent({ actions, chart: <div />, model });
|
|
|
|
findButton(sidebar, "Heute").props.onClick();
|
|
findButton(content, "Sitzung zurücksetzen").props.onClick();
|
|
findButton(content, "Gesamt zurücksetzen").props.onClick();
|
|
findButton(content, "Fehler zurücksetzen").props.onClick();
|
|
|
|
expect(calls).toEqual(["range:today", "reset:session", "reset:all", "reset:errors"]);
|
|
});
|
|
|
|
it("enables error reset only for a positive genuine error metric", () => {
|
|
const clean = createSnapshot();
|
|
const failed = createSnapshot();
|
|
setItems(failed, [item("failed", "failed")]);
|
|
|
|
const cleanContent = StatisticsContent({
|
|
actions: createActions(),
|
|
chart: <div />,
|
|
model: buildStatisticsViewModel(clean, "session", now)
|
|
});
|
|
const failedContent = StatisticsContent({
|
|
actions: createActions(),
|
|
chart: <div />,
|
|
model: buildStatisticsViewModel(failed, "session", now)
|
|
});
|
|
|
|
expect(findButton(cleanContent, "Fehler zurücksetzen").props.disabled).toBe(true);
|
|
expect(findButton(failedContent, "Fehler zurücksetzen").props.disabled).toBe(false);
|
|
});
|
|
|
|
it("keeps the empty provider state inside the ARIA table as a row and spanning cell", () => {
|
|
const html = renderToStaticMarkup(
|
|
<StatisticsContent
|
|
actions={createActions()}
|
|
chart={<div />}
|
|
model={buildStatisticsViewModel(createSnapshot(), "session", now)}
|
|
/>
|
|
);
|
|
|
|
expect(html).toContain('class="statistics-provider-empty" role="row"');
|
|
expect(html).toContain('aria-colspan="3" role="cell"');
|
|
});
|
|
});
|
|
|
|
describe("bandwidth chart palette", () => {
|
|
it("collects bandwidth samples from the mounted application instead of the statistics tab", () => {
|
|
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
|
const chartBlock = source.slice(source.indexOf("const BandwidthChart"), source.indexOf("interface DownloadSpeedSparklineProps"));
|
|
|
|
expect(source).toContain("appendBandwidthSample(speedHistoryRef.current, liveDownloadSpeedBps");
|
|
expect(chartBlock).not.toContain("history.push");
|
|
expect(chartBlock).not.toContain('item.status === "downloading"');
|
|
});
|
|
|
|
it("defines semantic speed and progress text colors for both themes", () => {
|
|
const css = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8");
|
|
const dark = css.match(/:root,\s*:root\[data-theme="dark"\]\s*\{([\s\S]*?)\}/)?.[1];
|
|
const light = css.match(/:root\[data-theme="light"\]\s*\{([\s\S]*?)\}/)?.[1];
|
|
|
|
expect(dark).toContain("--ui-speed-accent: #4ADE80;");
|
|
expect(dark).toContain("--ui-primary-text: #181A1F;");
|
|
expect(dark).toContain("--ui-focus: #9AB8E8;");
|
|
expect(dark).toContain("--ui-success-text: #4ADE80;");
|
|
expect(dark).toContain("--ui-warning-text: #F1C786;");
|
|
expect(dark).toContain("--ui-danger-text: #F06464;");
|
|
expect(dark).toContain("--ui-progress-track-text: #FFFFFF;");
|
|
expect(dark).toContain("--ui-progress-fill-text: #181A1F;");
|
|
expect(light).toContain("--ui-speed-accent: #1E9E55;");
|
|
expect(light).toContain("--ui-primary-text: #FFFFFF;");
|
|
expect(light).toContain("--ui-focus: #24558D;");
|
|
expect(light).toContain("--ui-success-text: #137A3D;");
|
|
expect(light).toContain("--ui-warning-text: #7A4B00;");
|
|
expect(light).toContain("--ui-danger-text: #B4232F;");
|
|
expect(light).toContain("--ui-progress-track-text: #181A1F;");
|
|
expect(light).toContain("--ui-progress-fill-text: #181A1F;");
|
|
});
|
|
|
|
it("uses theme-aware primary text and visible focus colors", () => {
|
|
const theme = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8");
|
|
const shell = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
|
const collector = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
|
|
|
expect(theme).toMatch(/:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--ui-focus\);/s);
|
|
expect(shell).toContain("color: var(--ui-update-text);");
|
|
expect(collector.match(/color:\s*var\(--ui-primary-text\);/g)).toHaveLength(3);
|
|
});
|
|
|
|
it("requests only the semantic UI color properties and keeps the computed font family", () => {
|
|
const requested: string[] = [];
|
|
const values: Record<string, string> = {
|
|
"--ui-border": " rgb(61, 61, 61) ",
|
|
"--ui-text-muted": " rgb(145, 145, 145) ",
|
|
"--ui-speed-accent": " rgb(242, 148, 45) "
|
|
};
|
|
|
|
const palette = readBandwidthChartPalette((property) => {
|
|
requested.push(property);
|
|
return values[property];
|
|
}, "Inter, Segoe UI, sans-serif");
|
|
|
|
expect(requested).toEqual(["--ui-border", "--ui-text-muted", "--ui-speed-accent"]);
|
|
expect(palette).toEqual({
|
|
grid: "rgb(61, 61, 61)",
|
|
text: "rgb(145, 145, 145)",
|
|
accent: "rgb(242, 148, 45)",
|
|
fontFamily: "Inter, Segoe UI, sans-serif"
|
|
});
|
|
});
|
|
|
|
it("uses the semantic green speed accent for the header sparkline", () => {
|
|
const requested: string[] = [];
|
|
const palette = readDownloadSpeedSparklinePalette((property) => {
|
|
requested.push(property);
|
|
return property === "--ui-speed-accent" ? " rgb(74, 222, 128) " : "";
|
|
});
|
|
|
|
expect(requested).toEqual(["--ui-speed-accent"]);
|
|
expect(palette).toEqual({ accent: "rgb(74, 222, 128)" });
|
|
});
|
|
|
|
it("labels the live chart and slows redraws when reduced motion is requested", () => {
|
|
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
|
const chartBlock = source.slice(source.indexOf("const BandwidthChart"), source.indexOf("interface DownloadSpeedSparklineProps"));
|
|
|
|
expect(chartBlock).toContain('role="img"');
|
|
expect(chartBlock).toContain('aria-label="Bandbreitenverlauf der letzten 60 Sekunden"');
|
|
expect(chartBlock).toContain('window.matchMedia("(prefers-reduced-motion: reduce)")');
|
|
expect(chartBlock).toContain("reducedMotion ? 1000 : 250");
|
|
});
|
|
|
|
it("asks for confirmation before deleting all saved download statistics", () => {
|
|
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
|
const actions = source.slice(source.indexOf("const statisticsActions"), source.indexOf("const collectorActions"));
|
|
|
|
expect(actions).toContain("askConfirmPrompt");
|
|
expect(actions.indexOf("askConfirmPrompt")).toBeLessThan(actions.indexOf("resetDownloadStats"));
|
|
expect(actions).toContain('title: "Gesamtstatistik zurücksetzen"');
|
|
});
|
|
|
|
it("asks for confirmation before resetting session statistics", () => {
|
|
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
|
const actions = source.slice(source.indexOf("const statisticsActions"), source.indexOf("const collectorActions"));
|
|
const sessionReset = actions.slice(actions.indexOf("onResetSession"), actions.indexOf("onResetAll"));
|
|
|
|
expect(sessionReset).toContain("askConfirmPrompt");
|
|
expect(sessionReset.indexOf("askConfirmPrompt")).toBeLessThan(sessionReset.indexOf("resetSessionStats"));
|
|
expect(sessionReset).toContain('title: "Sitzungsstatistik zurücksetzen"');
|
|
});
|
|
});
|