feat: cycle availability sorting with part-count tie breakers
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sortPackageOrderByService } from "../src/renderer/App";
|
||||
import { sortPackageOrderByAvailability } from "../src/renderer/package-order";
|
||||
import { createAvailabilitySortCycle, sortPackageOrderByAvailability } from "../src/renderer/package-order";
|
||||
|
||||
describe("download package sorting", () => {
|
||||
it("sorts the Verfügbarkeit column from fully online through partial and unchecked down to fully offline", () => {
|
||||
@@ -30,7 +30,58 @@ describe("download package sorting", () => {
|
||||
const order = ["offline", "partialLow", "unchecked", "online", "partialHigh", "empty"];
|
||||
|
||||
expect(sortPackageOrderByAvailability(order, packages, items, false)).toEqual(["online", "partialHigh", "partialLow", "unchecked", "empty", "offline"]);
|
||||
expect(sortPackageOrderByAvailability(order, packages, items, true)).toEqual(["offline", "unchecked", "empty", "partialLow", "partialHigh", "online"]);
|
||||
expect(sortPackageOrderByAvailability(order, packages, items, true)).toEqual(["offline", "empty", "unchecked", "partialLow", "partialHigh", "online"]);
|
||||
});
|
||||
|
||||
it("uses part counts only for equal availability, including offline and partial packages", () => {
|
||||
const definitions = [
|
||||
["offlineSmall", 0, 2], ["onlineSmall", 2, 2], ["partialSmall", 1, 2],
|
||||
["offlineLarge", 0, 8], ["onlineLarge", 8, 8], ["partialLarge", 4, 8],
|
||||
["partialHigh", 3, 4], ["onlineEqual", 2, 2]
|
||||
] as const;
|
||||
const packages: any = {};
|
||||
const items: any = {};
|
||||
for (const [id, online, total] of definitions) {
|
||||
const itemIds = Array.from({ length: total }, (_, index) => `${id}-${index}`);
|
||||
packages[id] = { id, itemIds };
|
||||
itemIds.forEach((itemId, index) => { items[itemId] = { status: "queued", onlineStatus: index < online ? "online" : "offline" }; });
|
||||
}
|
||||
const order = definitions.map(([id]) => id);
|
||||
expect(sortPackageOrderByAvailability(order, packages, items, false)).toEqual([
|
||||
"onlineLarge", "onlineSmall", "onlineEqual", "partialHigh", "partialLarge", "partialSmall", "offlineLarge", "offlineSmall"
|
||||
]);
|
||||
expect(sortPackageOrderByAvailability(order, packages, items, true)).toEqual([
|
||||
"offlineSmall", "offlineLarge", "partialSmall", "partialLarge", "partialHigh", "onlineSmall", "onlineEqual", "onlineLarge"
|
||||
]);
|
||||
});
|
||||
|
||||
it("cycles online first, offline first, original order, then starts a new cycle", () => {
|
||||
const cycle = createAvailabilitySortCycle();
|
||||
const original = ["b", "a", "c"];
|
||||
expect(cycle.next(original).descending).toBe(false);
|
||||
expect(cycle.next(["a", "b", "c"]).descending).toBe(true);
|
||||
expect(cycle.next(["c", "b", "a"])).toEqual({ descending: null, order: original });
|
||||
expect(cycle.next(["new", "a"]).descending).toBe(false);
|
||||
expect(cycle.next(["a", "new"]).descending).toBe(true);
|
||||
expect(cycle.next(["a", "new"])).toEqual({ descending: null, order: ["new", "a"] });
|
||||
});
|
||||
|
||||
it("restores surviving packages and appends new packages without resurrecting deletions", () => {
|
||||
const cycle = createAvailabilitySortCycle();
|
||||
const original = ["b", "a", "deleted"];
|
||||
cycle.next(original);
|
||||
original.reverse();
|
||||
cycle.next(["a", "b", "new"]);
|
||||
expect(cycle.next(["new", "a", "b", "newer"])).toEqual({ descending: null, order: ["b", "a", "new", "newer"] });
|
||||
});
|
||||
|
||||
it("starts afresh after another column and handles an empty queue", () => {
|
||||
const cycle = createAvailabilitySortCycle();
|
||||
cycle.next(["old"]);
|
||||
cycle.reset();
|
||||
expect(cycle.next([])).toEqual({ descending: false, order: [] });
|
||||
cycle.next([]);
|
||||
expect(cycle.next([])).toEqual({ descending: null, order: [] });
|
||||
});
|
||||
|
||||
it("sorts the Service column by its visible provider labels", () => {
|
||||
|
||||
@@ -2119,6 +2119,24 @@ describe("download table row contracts", () => {
|
||||
expect(sorted).toEqual(["availability"]);
|
||||
});
|
||||
|
||||
it("shows no active sorting or direction arrow when sorting is switched off", () => {
|
||||
const header = DownloadsTableHeader({
|
||||
actions: createActions(),
|
||||
columnOrder: ["name", "availability"],
|
||||
gridTemplate: "100px 100px",
|
||||
selectedCount: 0,
|
||||
sortColumn: undefined,
|
||||
sortDirection: "asc",
|
||||
visibleIds: []
|
||||
});
|
||||
for (const column of ["name", "availability"]) {
|
||||
expect(findElement(header, (element) => element.props["data-download-column"] === column).props["aria-sort"]).toBe("none");
|
||||
}
|
||||
const html = renderToStaticMarkup(header);
|
||||
expect(html).not.toContain("↑");
|
||||
expect(html).not.toContain("↓");
|
||||
});
|
||||
|
||||
it("shows an actively downloading item as online even without a stored availability result", () => {
|
||||
const html = renderToStaticMarkup(ItemRowContent({
|
||||
actions: createActions(),
|
||||
|
||||
@@ -42,13 +42,15 @@ async function verify(): Promise<void> {
|
||||
const sortButton = findSortButton();
|
||||
if (!sortButton) throw new Error("Verfügbarkeitsspalte fehlt");
|
||||
const results = [];
|
||||
for (let turn = 0; turn < 4; turn++) {
|
||||
const expected = sortPackageOrderByAvailability(
|
||||
const originalOrder = [...fixture.snapshot.session.packageOrder];
|
||||
for (let turn = 0; turn < 6; turn++) {
|
||||
const phase = turn % 3;
|
||||
const expected = (phase === 2 ? originalOrder : sortPackageOrderByAvailability(
|
||||
fixture.snapshot.session.packageOrder,
|
||||
fixture.snapshot.session.packages,
|
||||
fixture.snapshot.session.items,
|
||||
turn % 2 === 1
|
||||
).map((id) => fixture.snapshot.session.packages[id].name);
|
||||
phase === 1
|
||||
)).map((id) => fixture.snapshot.session.packages[id].name);
|
||||
let wrongOrderUpdates = 0;
|
||||
const orderChanges: string[][] = [];
|
||||
const body = document.querySelector<HTMLElement>(".downloads-table-body")!;
|
||||
@@ -77,9 +79,11 @@ async function verify(): Promise<void> {
|
||||
})) movingFrames++;
|
||||
}
|
||||
observer.disconnect();
|
||||
results.push({ direction: turn % 2 === 1 ? "offline-first" : "online-first", frames, wrongOrderFrames, wrongOrderUpdates, movingFrames, orderChanges });
|
||||
const expectedAriaSort = phase === 2 ? "none" : phase === 0 ? "descending" : "ascending";
|
||||
const correctIndicator = sortButton.closest("[aria-sort]")?.getAttribute("aria-sort") === expectedAriaSort;
|
||||
results.push({ direction: ["online-first", "offline-first", "off"][phase], correctIndicator, frames, wrongOrderFrames, wrongOrderUpdates, movingFrames, orderChanges });
|
||||
}
|
||||
report.textContent = JSON.stringify({ passed: results.every((result) => result.wrongOrderFrames === 0 && result.wrongOrderUpdates === 0 && result.movingFrames === 0), results });
|
||||
report.textContent = JSON.stringify({ passed: results.every((result) => result.correctIndicator && result.wrongOrderFrames === 0 && result.wrongOrderUpdates === 0 && result.movingFrames === 0), results });
|
||||
}
|
||||
|
||||
void verify().catch((error) => { report.textContent = String(error); });
|
||||
|
||||
Reference in New Issue
Block a user