release: ship v2.0.23 queue and account reliability fixes
Normalize RapidGator aliases, stabilize active queue ordering, preserve user-controlled package expansion, and align compact queue status presentation. Separate Mega-Debrid API and Web credential pools, migrate legacy credentials and disabled states safely, refresh live account availability without restart, honor disabled credential persistence, and invalidate Web sessions without allowing stale in-flight, retry, or queued logins to restore old cookies. Expand regression coverage for account isolation, legacy migration, session races, live scheduler refresh, update notes, and responsive queue behavior.
This commit is contained in:
+186
-174
@@ -1,144 +1,144 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
|
||||
import type { UpdateCheckResult } from "../src/shared/types";
|
||||
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
||||
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
||||
|
||||
const callbacks = {
|
||||
onOpen: () => {},
|
||||
onClose: () => {},
|
||||
onInstall: () => {},
|
||||
onLater: () => {}
|
||||
};
|
||||
|
||||
describe("update experience", () => {
|
||||
it("renders the available update and prompt as one accessible experience", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={0}
|
||||
releaseNotes="Changes"
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
expect(html).toContain("role=\"tooltip\"");
|
||||
expect(html).toContain("Eine neue Version ist bereit. Klicke hier, um sie zu installieren.");
|
||||
expect(html).toContain("role=\"dialog\"");
|
||||
expect(html).toContain("aria-modal=\"true\"");
|
||||
expect(html).toContain("Update installieren");
|
||||
expect(html).toContain("Jetzt aktualisieren");
|
||||
expect(html).toContain("Später");
|
||||
expect(html).toContain("Changes");
|
||||
expect(html).toContain("<details");
|
||||
});
|
||||
|
||||
it("keeps the update affordance but removes the dialog when the prompt is closed", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes="Changes"
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
expect(html).not.toContain("role=\"dialog\"");
|
||||
});
|
||||
|
||||
it("renders active progress without controls that could close the installation", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={{ percent: 47, text: "Update-Download: 47% (47 MB / 100 MB)" }}
|
||||
releaseNotes=""
|
||||
state="downloading"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("Update-Download: 47% (47 MB / 100 MB)");
|
||||
expect(html).toContain("aria-valuenow=\"47\"");
|
||||
expect(html).not.toContain("Später");
|
||||
expect(html).not.toContain("Jetzt aktualisieren");
|
||||
expect(html).not.toContain("aria-label=\"Schließen\"");
|
||||
});
|
||||
|
||||
it("preserves the original installation error in the reusable dialog", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={{ percent: null, text: "Update-Fehler: Originale Prüfsummenmeldung" }}
|
||||
releaseNotes=""
|
||||
state="error"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("Update-Fehler: Originale Prüfsummenmeldung");
|
||||
expect(html).toContain("aria-label=\"Schließen\"");
|
||||
});
|
||||
|
||||
it("renders nothing when no update is available and no dialog is active", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available={false}
|
||||
currentVersion="v2.0.12"
|
||||
latestTag=""
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes=""
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("places the update affordance in the accessible global header action group", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AppHeader
|
||||
activeView="downloads"
|
||||
actions={(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes=""
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
)}
|
||||
onViewChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("role=\"group\"");
|
||||
expect(html).toContain("aria-label=\"Globale Aktionen\"");
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
});
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
|
||||
import type { UpdateCheckResult } from "../src/shared/types";
|
||||
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
||||
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
||||
|
||||
const callbacks = {
|
||||
onOpen: () => {},
|
||||
onClose: () => {},
|
||||
onInstall: () => {},
|
||||
onLater: () => {}
|
||||
};
|
||||
|
||||
describe("update experience", () => {
|
||||
it("renders the available update and prompt as one accessible experience", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={0}
|
||||
releaseNotes="Changes"
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
expect(html).toContain("role=\"tooltip\"");
|
||||
expect(html).toContain("Eine neue Version ist bereit. Klicke hier, um sie zu installieren.");
|
||||
expect(html).toContain("role=\"dialog\"");
|
||||
expect(html).toContain("aria-modal=\"true\"");
|
||||
expect(html).toContain("Update installieren");
|
||||
expect(html).toContain("Jetzt aktualisieren");
|
||||
expect(html).toContain("Später");
|
||||
expect(html).toContain("Changes");
|
||||
expect(html).toContain("<details");
|
||||
});
|
||||
|
||||
it("keeps the update affordance but removes the dialog when the prompt is closed", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes="Changes"
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
expect(html).not.toContain("role=\"dialog\"");
|
||||
});
|
||||
|
||||
it("renders active progress without controls that could close the installation", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={{ percent: 47, text: "Update-Download: 47% (47 MB / 100 MB)" }}
|
||||
releaseNotes=""
|
||||
state="downloading"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("Update-Download: 47% (47 MB / 100 MB)");
|
||||
expect(html).toContain("aria-valuenow=\"47\"");
|
||||
expect(html).not.toContain("Später");
|
||||
expect(html).not.toContain("Jetzt aktualisieren");
|
||||
expect(html).not.toContain("aria-label=\"Schließen\"");
|
||||
});
|
||||
|
||||
it("preserves the original installation error in the reusable dialog", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open
|
||||
progress={{ percent: null, text: "Update-Fehler: Originale Prüfsummenmeldung" }}
|
||||
releaseNotes=""
|
||||
state="error"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("Update-Fehler: Originale Prüfsummenmeldung");
|
||||
expect(html).toContain("aria-label=\"Schließen\"");
|
||||
});
|
||||
|
||||
it("renders nothing when no update is available and no dialog is active", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<UpdateExperience
|
||||
available={false}
|
||||
currentVersion="v2.0.12"
|
||||
latestTag=""
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes=""
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("places the update affordance in the accessible global header action group", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AppHeader
|
||||
activeView="downloads"
|
||||
actions={(
|
||||
<UpdateExperience
|
||||
available
|
||||
currentVersion="v2.0.12"
|
||||
latestTag="v9.9.9"
|
||||
open={false}
|
||||
progress={0}
|
||||
releaseNotes=""
|
||||
state="prompt"
|
||||
{...callbacks}
|
||||
/>
|
||||
)}
|
||||
onViewChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("role=\"group\"");
|
||||
expect(html).toContain("aria-label=\"Globale Aktionen\"");
|
||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||
});
|
||||
|
||||
it("uses the specified transient and modal elevation tokens", () => {
|
||||
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
||||
|
||||
@@ -146,37 +146,49 @@ describe("update experience", () => {
|
||||
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
|
||||
});
|
||||
|
||||
it("keeps forward and reverse tabbing inside the update dialog", () => {
|
||||
expect(getUpdateDialogFocusTarget(false, -1, 4)).toBe(0);
|
||||
expect(getUpdateDialogFocusTarget(true, -1, 4)).toBe(3);
|
||||
expect(getUpdateDialogFocusTarget(false, 3, 4)).toBe(0);
|
||||
expect(getUpdateDialogFocusTarget(true, 0, 4)).toBe(3);
|
||||
expect(getUpdateDialogFocusTarget(false, 1, 4)).toBeNull();
|
||||
expect(getUpdateDialogFocusTarget(false, -1, 0)).toBeNull();
|
||||
it("uses a light-blue update affordance and a bounded scrollable changelog", () => {
|
||||
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
||||
const theme = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8");
|
||||
|
||||
expect(theme).toMatch(/--ui-update:\s*#BAD0FC;/);
|
||||
expect(theme).toMatch(/--ui-update-hover:\s*#8AA5DC;/);
|
||||
expect(theme).toMatch(/--ui-update-text:\s*#181A1F;/);
|
||||
expect(css).toMatch(/\.md-update-trigger\s*\{[^}]*background:\s*var\(--ui-update\);[^}]*color:\s*var\(--ui-update-text\);/s);
|
||||
expect(css).toMatch(/\.md-update-trigger:hover\s*\{[^}]*background:\s*var\(--ui-update-hover\);/s);
|
||||
expect(css).toMatch(/\.md-update-release-notes pre\s*\{[^}]*max-height:\s*min\(360px, 45vh\);[^}]*overflow-y:\s*auto;/s);
|
||||
});
|
||||
|
||||
it("rejects stale update-check completions without discarding the latest state", () => {
|
||||
expect(shouldApplyUpdateCheckResult(4, 4)).toBe(true);
|
||||
expect(shouldApplyUpdateCheckResult(3, 4)).toBe(false);
|
||||
expect(shouldApplyUpdateCheckResult(4, 5)).toBe(false);
|
||||
});
|
||||
|
||||
it("applies only the latest result when update checks complete out of order", async () => {
|
||||
const generation = { current: 0 };
|
||||
const applied: string[] = [];
|
||||
let finishStartup: ((result: UpdateCheckResult) => void) | undefined;
|
||||
let finishManual: ((result: UpdateCheckResult) => void) | undefined;
|
||||
const startup = new Promise<UpdateCheckResult>((resolve) => { finishStartup = resolve; });
|
||||
const manual = new Promise<UpdateCheckResult>((resolve) => { finishManual = resolve; });
|
||||
const apply = (result: UpdateCheckResult): void => { applied.push(result.latestTag); };
|
||||
|
||||
const startupRun = runLatestUpdateCheck(generation, () => startup, apply);
|
||||
const manualRun = runLatestUpdateCheck(generation, () => manual, apply);
|
||||
finishManual?.({ updateAvailable: true, currentVersion: "2.0.12", latestVersion: "9.9.9", latestTag: "v9.9.9", releaseUrl: "https://example.test/v9.9.9" });
|
||||
await manualRun;
|
||||
finishStartup?.({ updateAvailable: false, currentVersion: "2.0.12", latestVersion: "2.0.12", latestTag: "v2.0.12", releaseUrl: "https://example.test/v2.0.12" });
|
||||
await startupRun;
|
||||
|
||||
expect(applied).toEqual(["v9.9.9"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps forward and reverse tabbing inside the update dialog", () => {
|
||||
expect(getUpdateDialogFocusTarget(false, -1, 4)).toBe(0);
|
||||
expect(getUpdateDialogFocusTarget(true, -1, 4)).toBe(3);
|
||||
expect(getUpdateDialogFocusTarget(false, 3, 4)).toBe(0);
|
||||
expect(getUpdateDialogFocusTarget(true, 0, 4)).toBe(3);
|
||||
expect(getUpdateDialogFocusTarget(false, 1, 4)).toBeNull();
|
||||
expect(getUpdateDialogFocusTarget(false, -1, 0)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects stale update-check completions without discarding the latest state", () => {
|
||||
expect(shouldApplyUpdateCheckResult(4, 4)).toBe(true);
|
||||
expect(shouldApplyUpdateCheckResult(3, 4)).toBe(false);
|
||||
expect(shouldApplyUpdateCheckResult(4, 5)).toBe(false);
|
||||
});
|
||||
|
||||
it("applies only the latest result when update checks complete out of order", async () => {
|
||||
const generation = { current: 0 };
|
||||
const applied: string[] = [];
|
||||
let finishStartup: ((result: UpdateCheckResult) => void) | undefined;
|
||||
let finishManual: ((result: UpdateCheckResult) => void) | undefined;
|
||||
const startup = new Promise<UpdateCheckResult>((resolve) => { finishStartup = resolve; });
|
||||
const manual = new Promise<UpdateCheckResult>((resolve) => { finishManual = resolve; });
|
||||
const apply = (result: UpdateCheckResult): void => { applied.push(result.latestTag); };
|
||||
|
||||
const startupRun = runLatestUpdateCheck(generation, () => startup, apply);
|
||||
const manualRun = runLatestUpdateCheck(generation, () => manual, apply);
|
||||
finishManual?.({ updateAvailable: true, currentVersion: "2.0.12", latestVersion: "9.9.9", latestTag: "v9.9.9", releaseUrl: "https://example.test/v9.9.9" });
|
||||
await manualRun;
|
||||
finishStartup?.({ updateAvailable: false, currentVersion: "2.0.12", latestVersion: "2.0.12", latestTag: "v2.0.12", releaseUrl: "https://example.test/v2.0.12" });
|
||||
await startupRun;
|
||||
|
||||
expect(applied).toEqual(["v9.9.9"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user