Refine all primary views across supported desktop widths, preserve compact download actions, strengthen keyboard and assistive semantics, confirm irreversible actions, complete new bilingual UI strings, and unify live speed visualization colors.
507 lines
20 KiB
TypeScript
507 lines
20 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,
|
|
type StatisticsRange
|
|
} from "../src/renderer/views/statistics/statistics-model";
|
|
import {
|
|
StatisticsContent,
|
|
StatisticsSidebar,
|
|
StatisticsView,
|
|
type StatisticsViewActions
|
|
} from "../src/renderer/views/statistics/StatisticsView";
|
|
import { createVisualFixture } from "./visual/fixtures";
|
|
|
|
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 daily provider usage only for the matching local day", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloaded = 999_999;
|
|
snapshot.settings.providerDailyUsageDay = "2026-08-10";
|
|
snapshot.settings.providerDailyUsageBytes = { realdebrid: 500, alldebrid: 1_500 };
|
|
|
|
const model = buildStatisticsViewModel(snapshot, "today", now);
|
|
|
|
expect(model.metrics.downloadedBytes).toMatchObject({ value: 2_000, available: true });
|
|
expect(model.providers.map((row) => [row.id, row.bytes])).toEqual([
|
|
["alldebrid", 1_500],
|
|
["realdebrid", 500]
|
|
]);
|
|
expectUnavailable(model.metrics.files);
|
|
expectUnavailable(model.metrics.successRate);
|
|
expectUnavailable(model.metrics.errors);
|
|
expectUnavailable(model.metrics.averageSpeedBps);
|
|
});
|
|
|
|
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.each(["week", "month"] satisfies StatisticsRange[])("keeps %s unavailable without inventing historical buckets", (range) => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloaded = 5_000;
|
|
snapshot.stats.totalDownloadedAllTime = 50_000;
|
|
snapshot.settings.providerDailyUsageDay = "2026-08-10";
|
|
snapshot.settings.providerDailyUsageBytes = { realdebrid: 4_000 };
|
|
snapshot.settings.providerTotalUsageBytes = { realdebrid: 40_000 };
|
|
|
|
const model = buildStatisticsViewModel(snapshot, range, now);
|
|
|
|
expect(model.coverage).toBe("unavailable");
|
|
expect(model.message).toBe("Für diesen Zeitraum werden noch keine historischen Daten gespeichert.");
|
|
expect(model.providers).toEqual([]);
|
|
expect(model.providerScope).toBeNull();
|
|
Object.values(model.metrics).forEach(expectUnavailable);
|
|
});
|
|
|
|
it("uses all-time counters and provider totals without inventing historical outcomes", () => {
|
|
const snapshot = createSnapshot();
|
|
snapshot.stats.totalDownloadedAllTime = 25_000;
|
|
snapshot.stats.totalFilesAllTime = 42;
|
|
snapshot.settings.providerTotalUsageBytes = { realdebrid: 5_000, debridlink: 20_000 };
|
|
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.every((row) => row.completed === null && row.failed === null)).toBe(true);
|
|
expectUnavailable(model.metrics.successRate);
|
|
expectUnavailable(model.metrics.errors);
|
|
expectUnavailable(model.metrics.averageSpeedBps);
|
|
});
|
|
|
|
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-primary-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"');
|
|
});
|
|
});
|