feat: cycle availability sorting with part-count tie breakers
This commit is contained in:
@@ -8,6 +8,7 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält
|
|||||||
|
|
||||||
## Zuletzt verifizierter Stand
|
## Zuletzt verifizierter Stand
|
||||||
|
|
||||||
|
- Availability-Sortierung nach `v2.0.88`: Klickfolge online/viele Parts zuerst → offline/wenige Parts zuerst → ursprüngliche Reihenfolge ohne aktiven Sortierpfeil. Online-/Offline-Anteile bleiben vorrangig; Part-Anzahl entscheidet nur bei identischer Verfügbarkeit. Neue Pakete bleiben beim Ausschalten hinten erhalten, gelöschte werden nicht wieder eingefügt. Wechsel auf eine andere Sortierspalte setzt den Availability-Zyklus zurück. Bestehender Pending-Snapshot-Schutz und sofortiger Sortierwechsel bleiben erhalten. TypeScript und gezielte Sortier-/Ansichtstests erfolgreich; isolierte Electron-Browserprobe mit Testdaten prüft zwei vollständige Zyklen samt verzögerten Snapshots, Sortierindikatoren und DOM-Reihenfolge ohne falsche Zwischenstände. Kein neues Release.
|
||||||
- Hot-Dev-Start am 5. September 2026 geklärt und behoben: Der frühere Befund „blocked by policy“ war kein Windows- oder Sicherheitsproblem, sondern die Werkzeug-Sandbox der Agentensitzung plus ein echter Fehler im Skriptaufruf. Das `&` im Projektpfad („Claude & ChatGPT Projekte“) zerlegt die npm-`cmd`-Shims in `node_modules/.bin` (`SET dp0=%~dp0` ohne Anführungszeichen), cmd meldet „Der Befehl "ChatGPT" ist entweder falsch geschrieben“ und sucht `C:UsersSaschaDesktopwait-oninwait-on`. Fix: Die Skripte `dev`, `dev:renderer`, `dev:main:watch` und `dev:electron` rufen jetzt die Bin-Dateien direkt über `node node_modules/<paket>/...` auf; `npm run dev` startet damit aus dem Projektordner Vite (5180), tsup-Watch und das Electron-Fenster „Multi Debrid Downloader - v2.0.88“ (Prozess `Multi-Debrid-Downloader-dev-<pid>`), verifiziert am 5. September 2026. Regeln für Agenten (Codex und Claude): `npm run dev` als Hintergrundprozess starten, kein `Start-Process`, kein Aufruf von `.cmd`-Shims aus `node_modules/.bin`; wenn Port 5180 schon belegt ist, läuft bereits eine Dev-Instanz, dann nur `npm run dev:electron` starten. Übrige Skripte (`build`, `test:client`, `self-check`, `start`) nutzen weiterhin Shims und brauchen bei Bedarf denselben Umbau oder direkte `node node_modules/...`-Aufrufe.
|
- Hot-Dev-Start am 5. September 2026 geklärt und behoben: Der frühere Befund „blocked by policy“ war kein Windows- oder Sicherheitsproblem, sondern die Werkzeug-Sandbox der Agentensitzung plus ein echter Fehler im Skriptaufruf. Das `&` im Projektpfad („Claude & ChatGPT Projekte“) zerlegt die npm-`cmd`-Shims in `node_modules/.bin` (`SET dp0=%~dp0` ohne Anführungszeichen), cmd meldet „Der Befehl "ChatGPT" ist entweder falsch geschrieben“ und sucht `C:UsersSaschaDesktopwait-oninwait-on`. Fix: Die Skripte `dev`, `dev:renderer`, `dev:main:watch` und `dev:electron` rufen jetzt die Bin-Dateien direkt über `node node_modules/<paket>/...` auf; `npm run dev` startet damit aus dem Projektordner Vite (5180), tsup-Watch und das Electron-Fenster „Multi Debrid Downloader - v2.0.88“ (Prozess `Multi-Debrid-Downloader-dev-<pid>`), verifiziert am 5. September 2026. Regeln für Agenten (Codex und Claude): `npm run dev` als Hintergrundprozess starten, kein `Start-Process`, kein Aufruf von `.cmd`-Shims aus `node_modules/.bin`; wenn Port 5180 schon belegt ist, läuft bereits eine Dev-Instanz, dann nur `npm run dev:electron` starten. Übrige Skripte (`build`, `test:client`, `self-check`, `start`) nutzen weiterhin Shims und brauchen bei Bedarf denselben Umbau oder direkte `node node_modules/...`-Aufrufe.
|
||||||
- Release-Vorprüfung v2.0.88: 2.765 Client-Tests erfolgreich; vier bestehende optionale Tests übersprungen, zwei Windows-Symlink-Fixtures wegen fehlender Berechtigung gezielt ausgenommen. Alle 16 Backup-API-Tests, TypeScript und Node-Self-Check erfolgreich. Main-/Renderer-Build sowie Windows-Installer und portable Anwendung gebaut; `verify_public_release.mjs --verify-archives` bestätigt beide Archive einschließlich Lizenzen, Versionsstand und Update-Metadaten. Bekannte Vite-Chunkgrößenwarnung unverändert.
|
- Release-Vorprüfung v2.0.88: 2.765 Client-Tests erfolgreich; vier bestehende optionale Tests übersprungen, zwei Windows-Symlink-Fixtures wegen fehlender Berechtigung gezielt ausgenommen. Alle 16 Backup-API-Tests, TypeScript und Node-Self-Check erfolgreich. Main-/Renderer-Build sowie Windows-Installer und portable Anwendung gebaut; `verify_public_release.mjs --verify-archives` bestätigt beide Archive einschließlich Lizenzen, Versionsstand und Update-Metadaten. Bekannte Vite-Chunkgrößenwarnung unverändert.
|
||||||
- Verifiziert am: 5. September 2026, Europe/Berlin
|
- Verifiziert am: 5. September 2026, Europe/Berlin
|
||||||
|
|||||||
+16
-6
@@ -39,7 +39,7 @@ import {
|
|||||||
getProviderDailyUsageBytes,
|
getProviderDailyUsageBytes,
|
||||||
getProviderUsageDayKey
|
getProviderUsageDayKey
|
||||||
} from "../shared/provider-daily-limits";
|
} from "../shared/provider-daily-limits";
|
||||||
import { preservePackageOrderForDisplay, sortPackageOrderByAvailability, sortPackageOrderByName } from "./package-order";
|
import { createAvailabilitySortCycle, preservePackageOrderForDisplay, sortPackageOrderByAvailability, sortPackageOrderByName } from "./package-order";
|
||||||
import { createPackageOrderState } from "./package-order-state";
|
import { createPackageOrderState } from "./package-order-state";
|
||||||
import { getPackagesWithOfflineLinks } from "../shared/offline-packages";
|
import { getPackagesWithOfflineLinks } from "../shared/offline-packages";
|
||||||
import type { OfflineSkipScope } from "../shared/types";
|
import type { OfflineSkipScope } from "../shared/types";
|
||||||
@@ -1894,7 +1894,8 @@ export function App(): ReactElement {
|
|||||||
const [downloadDisplayMode, setDownloadDisplayMode] = useState<DownloadDisplayMode>("packages");
|
const [downloadDisplayMode, setDownloadDisplayMode] = useState<DownloadDisplayMode>("packages");
|
||||||
const [downloadFilter, setDownloadFilter] = useState<DownloadSidebarFilter>("all");
|
const [downloadFilter, setDownloadFilter] = useState<DownloadSidebarFilter>("all");
|
||||||
const [downloadProviderFilter, setDownloadProviderFilter] = useState("all");
|
const [downloadProviderFilter, setDownloadProviderFilter] = useState("all");
|
||||||
const [downloadsSortColumn, setDownloadsSortColumn] = useState<DownloadSortColumn>("name");
|
const [downloadsSortColumn, setDownloadsSortColumn] = useState<DownloadSortColumn | undefined>("name");
|
||||||
|
const availabilitySortCycleRef = useRef(createAvailabilitySortCycle());
|
||||||
const [downloadsSortRevision, setDownloadsSortRevision] = useState(0);
|
const [downloadsSortRevision, setDownloadsSortRevision] = useState(0);
|
||||||
const [downloadsSortDescending, setDownloadsSortDescending] = useState(false);
|
const [downloadsSortDescending, setDownloadsSortDescending] = useState(false);
|
||||||
const [showAllPackages, setShowAllPackages] = useState(false);
|
const [showAllPackages, setShowAllPackages] = useState(false);
|
||||||
@@ -5271,11 +5272,22 @@ export function App(): ReactElement {
|
|||||||
}, [snapshot.packageSpeedBps]);
|
}, [snapshot.packageSpeedBps]);
|
||||||
|
|
||||||
const sortDownloadsByColumn = useCallback((column: DownloadSortColumn): void => {
|
const sortDownloadsByColumn = useCallback((column: DownloadSortColumn): void => {
|
||||||
|
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
|
||||||
|
if (column === "availability") {
|
||||||
|
const step = availabilitySortCycleRef.current.next(baseOrder);
|
||||||
|
setDownloadsSortColumn(step.descending === null ? undefined : column);
|
||||||
|
setDownloadsSortDescending(step.descending === false);
|
||||||
|
setDownloadsSortRevision((revision) => revision + 1);
|
||||||
|
commitPackageOrder(step.descending === null ? step.order : sortPackageOrderByAvailability(
|
||||||
|
step.order, snapshot.session.packages, snapshot.session.items, step.descending
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
availabilitySortCycleRef.current.reset();
|
||||||
const nextDescending = downloadsSortColumn === column ? !downloadsSortDescending : false;
|
const nextDescending = downloadsSortColumn === column ? !downloadsSortDescending : false;
|
||||||
setDownloadsSortColumn(column);
|
setDownloadsSortColumn(column);
|
||||||
setDownloadsSortDescending(nextDescending);
|
setDownloadsSortDescending(nextDescending);
|
||||||
setDownloadsSortRevision((revision) => revision + 1);
|
setDownloadsSortRevision((revision) => revision + 1);
|
||||||
const baseOrder = packageOrderRef.current.length > 0 ? packageOrderRef.current : snapshot.session.packageOrder;
|
|
||||||
const sorted = column === "progress"
|
const sorted = column === "progress"
|
||||||
? sortPackageOrderByProgress(baseOrder, snapshot.session.packages, snapshot.session.items, nextDescending)
|
? sortPackageOrderByProgress(baseOrder, snapshot.session.packages, snapshot.session.items, nextDescending)
|
||||||
: column === "size"
|
: column === "size"
|
||||||
@@ -5290,9 +5302,7 @@ export function App(): ReactElement {
|
|||||||
nextDescending,
|
nextDescending,
|
||||||
Object.fromEntries(downloadsViewCore.packageRows.map((row) => [row.package.id, row.items]))
|
Object.fromEntries(downloadsViewCore.packageRows.map((row) => [row.package.id, row.items]))
|
||||||
)
|
)
|
||||||
: column === "availability"
|
: sortPackageOrderByName(baseOrder, snapshot.session.packages, nextDescending);
|
||||||
? sortPackageOrderByAvailability(baseOrder, snapshot.session.packages, snapshot.session.items, nextDescending)
|
|
||||||
: sortPackageOrderByName(baseOrder, snapshot.session.packages, nextDescending);
|
|
||||||
commitPackageOrder(sorted);
|
commitPackageOrder(sorted);
|
||||||
}, [commitPackageOrder, downloadsSortColumn, downloadsSortDescending, downloadsViewCore.packageRows, snapshot.session.items, snapshot.session.packageOrder, snapshot.session.packages]);
|
}, [commitPackageOrder, downloadsSortColumn, downloadsSortDescending, downloadsViewCore.packageRows, snapshot.session.items, snapshot.session.packageOrder, snapshot.session.packages]);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export function sortPackageOrderByAvailability(
|
|||||||
}));
|
}));
|
||||||
const sorted = [...order];
|
const sorted = [...order];
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
const cmp = compareAvailabilitySummaries(summaries.get(a)!, summaries.get(b)!);
|
const summaryA = summaries.get(a)!;
|
||||||
|
const summaryB = summaries.get(b)!;
|
||||||
|
const cmp = compareAvailabilitySummaries(summaryA, summaryB) || summaryB.total - summaryA.total;
|
||||||
return descending ? -cmp : cmp;
|
return descending ? -cmp : cmp;
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
@@ -46,3 +48,31 @@ export function sortPackageOrderByAvailability(
|
|||||||
export function preservePackageOrderForDisplay(packages: PackageEntry[]): PackageEntry[] {
|
export function preservePackageOrderForDisplay(packages: PackageEntry[]): PackageEntry[] {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createAvailabilitySortCycle() {
|
||||||
|
let phase: "off" | "online" | "offline" = "off";
|
||||||
|
let originalOrder: string[] = [];
|
||||||
|
return {
|
||||||
|
reset(): void {
|
||||||
|
phase = "off";
|
||||||
|
originalOrder = [];
|
||||||
|
},
|
||||||
|
next(currentOrder: string[]): { descending: boolean | null; order: string[] } {
|
||||||
|
if (phase === "off") {
|
||||||
|
originalOrder = [...currentOrder];
|
||||||
|
phase = "online";
|
||||||
|
return { descending: false, order: currentOrder };
|
||||||
|
}
|
||||||
|
if (phase === "online") {
|
||||||
|
phase = "offline";
|
||||||
|
return { descending: true, order: currentOrder };
|
||||||
|
}
|
||||||
|
phase = "off";
|
||||||
|
const available = new Set(currentOrder);
|
||||||
|
const order = originalOrder.filter((id) => available.delete(id));
|
||||||
|
order.push(...available);
|
||||||
|
originalOrder = [];
|
||||||
|
return { descending: null, order };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ export interface DownloadsTableHeaderProps {
|
|||||||
actions: DownloadsTableActions;
|
actions: DownloadsTableActions;
|
||||||
columnOrder: readonly string[];
|
columnOrder: readonly string[];
|
||||||
gridTemplate: string;
|
gridTemplate: string;
|
||||||
sortColumn: DownloadSortColumn;
|
sortColumn?: DownloadSortColumn;
|
||||||
sortDirection: "asc" | "desc";
|
sortDirection: "asc" | "desc";
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
visibleIds: string[];
|
visibleIds: string[];
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ export function DownloadsContent({ actions, model }: { actions: DownloadsViewAct
|
|||||||
return (
|
return (
|
||||||
<main className="downloads-content">
|
<main className="downloads-content">
|
||||||
<div className="downloads-table" role="table" aria-label="Downloads">
|
<div className="downloads-table" role="table" aria-label="Downloads">
|
||||||
<DownloadsTableHeader actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} selectedCount={model.actionableSelectedIds.length} sortColumn={model.sortColumn ?? "name"} sortDirection={model.sortDirection ?? "asc"} visibleIds={model.visibleRowIds} />
|
<DownloadsTableHeader actions={actions} columnOrder={model.columnOrder} gridTemplate={model.gridTemplate} selectedCount={model.actionableSelectedIds.length} sortColumn={model.sortColumn} sortDirection={model.sortDirection ?? "asc"} visibleIds={model.visibleRowIds} />
|
||||||
<VirtualizedDownloadsBody actions={actions} model={model} state={state} />
|
<VirtualizedDownloadsBody actions={actions} model={model} state={state} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { sortPackageOrderByService } from "../src/renderer/App";
|
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", () => {
|
describe("download package sorting", () => {
|
||||||
it("sorts the Verfügbarkeit column from fully online through partial and unchecked down to fully offline", () => {
|
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"];
|
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, 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", () => {
|
it("sorts the Service column by its visible provider labels", () => {
|
||||||
|
|||||||
@@ -2119,6 +2119,24 @@ describe("download table row contracts", () => {
|
|||||||
expect(sorted).toEqual(["availability"]);
|
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", () => {
|
it("shows an actively downloading item as online even without a stored availability result", () => {
|
||||||
const html = renderToStaticMarkup(ItemRowContent({
|
const html = renderToStaticMarkup(ItemRowContent({
|
||||||
actions: createActions(),
|
actions: createActions(),
|
||||||
|
|||||||
@@ -42,13 +42,15 @@ async function verify(): Promise<void> {
|
|||||||
const sortButton = findSortButton();
|
const sortButton = findSortButton();
|
||||||
if (!sortButton) throw new Error("Verfügbarkeitsspalte fehlt");
|
if (!sortButton) throw new Error("Verfügbarkeitsspalte fehlt");
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let turn = 0; turn < 4; turn++) {
|
const originalOrder = [...fixture.snapshot.session.packageOrder];
|
||||||
const expected = sortPackageOrderByAvailability(
|
for (let turn = 0; turn < 6; turn++) {
|
||||||
|
const phase = turn % 3;
|
||||||
|
const expected = (phase === 2 ? originalOrder : sortPackageOrderByAvailability(
|
||||||
fixture.snapshot.session.packageOrder,
|
fixture.snapshot.session.packageOrder,
|
||||||
fixture.snapshot.session.packages,
|
fixture.snapshot.session.packages,
|
||||||
fixture.snapshot.session.items,
|
fixture.snapshot.session.items,
|
||||||
turn % 2 === 1
|
phase === 1
|
||||||
).map((id) => fixture.snapshot.session.packages[id].name);
|
)).map((id) => fixture.snapshot.session.packages[id].name);
|
||||||
let wrongOrderUpdates = 0;
|
let wrongOrderUpdates = 0;
|
||||||
const orderChanges: string[][] = [];
|
const orderChanges: string[][] = [];
|
||||||
const body = document.querySelector<HTMLElement>(".downloads-table-body")!;
|
const body = document.querySelector<HTMLElement>(".downloads-table-body")!;
|
||||||
@@ -77,9 +79,11 @@ async function verify(): Promise<void> {
|
|||||||
})) movingFrames++;
|
})) movingFrames++;
|
||||||
}
|
}
|
||||||
observer.disconnect();
|
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); });
|
void verify().catch((error) => { report.textContent = String(error); });
|
||||||
|
|||||||
Reference in New Issue
Block a user