fix: polish download status and application menus
Animate nested application menus while keeping hidden actions out of keyboard navigation and respecting reduced-motion preferences. Normalize Mega-Debrid service labels, simplify active download and extraction statuses, and preserve full diagnostics in tooltips. Brand the Windows development runtime and isolate concurrent launchers with PID-aware cleanup and regression coverage.
This commit is contained in:
@@ -31,10 +31,17 @@ describe("desktop shell", () => {
|
||||
});
|
||||
|
||||
it("keeps application submenus visible inside narrow right-aligned windows", () => {
|
||||
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8");
|
||||
const shellCss = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
||||
|
||||
expect(source.match(/aria-hidden=\{openSubmenu !==/g)).toHaveLength(4);
|
||||
expect(source.match(/inert: ""/g)).toHaveLength(4);
|
||||
expect(source.match(/menu-submenu-dropdown\$\{openSubmenu ===/g)).toHaveLength(4);
|
||||
expect(source).not.toMatch(/\{openSubmenu === "(?:sicherung|hilfe-log|hilfe-remote|hilfe-diagnose)" && \(/);
|
||||
expect(shellCss).toMatch(/\.md-application-menu-tree :where\(\.menu-dropdown, \.menu-submenu-dropdown\)\s*\{[^}]*overflow:\s*visible;/s);
|
||||
expect(shellCss).toMatch(/\.md-application-menu-tree \.menu-submenu-dropdown\s*\{[^}]*right:\s*100%;[^}]*left:\s*auto;/s);
|
||||
expect(shellCss).toMatch(/\.md-application-menu-tree \.menu-submenu-dropdown\s*\{[^}]*right:\s*100%;[^}]*left:\s*auto;[^}]*opacity:\s*0;[^}]*transform:\s*translateX\(8px\);[^}]*visibility:\s*hidden;[^}]*pointer-events:\s*none;[^}]*transition:[^}]*transform 220ms cubic-bezier\(0\.22, 0\.76, 0\.22, 1\)/s);
|
||||
expect(shellCss).toMatch(/\.md-application-menu-tree \.menu-submenu-dropdown\.is-open\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translateX\(0\);[^}]*visibility:\s*visible;[^}]*pointer-events:\s*auto;/s);
|
||||
expect(shellCss).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.md-application-menu-tree \.menu-submenu-dropdown\s*\{[^}]*transition-duration:\s*0\.01ms !important;[^}]*transition-delay:\s*0s !important;/);
|
||||
});
|
||||
|
||||
it("uses the product asset in the header brand", () => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("development Electron branding", () => {
|
||||
it("uses a unique executable for each development launcher", () => {
|
||||
const source = readFileSync(path.resolve("scripts/run-dev-electron.ts"), "utf8");
|
||||
|
||||
expect(source).toContain("`${productName}-dev-${process.pid}.exe`");
|
||||
expect(source).not.toContain("path.join(path.dirname(source), executableName)");
|
||||
expect(source).toContain("process.kill(pid, 0)");
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "win32")("prepares a branded development executable", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "mdd-dev-electron-"));
|
||||
roots.push(root);
|
||||
const source = path.resolve("node_modules/electron/dist/electron.exe");
|
||||
const target = path.join(root, "Multi-Debrid-Downloader.exe");
|
||||
const icon = path.resolve("assets/app_icon.ico");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.resolve("node_modules/tsx/dist/cli.mjs"),
|
||||
"scripts/run-dev-electron.ts",
|
||||
"--prepare-only",
|
||||
source,
|
||||
target,
|
||||
icon,
|
||||
"2.0.17"
|
||||
],
|
||||
{ cwd: path.resolve("."), encoding: "utf8" }
|
||||
);
|
||||
|
||||
expect(result.status, result.stderr || result.stdout).toBe(0);
|
||||
const metadata = JSON.parse(execFileSync(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"$v=(Get-Item -LiteralPath $env:MDD_TEST_EXE).VersionInfo; [pscustomobject]@{FileDescription=$v.FileDescription;ProductName=$v.ProductName;OriginalFilename=$v.OriginalFilename;FileVersion=$v.FileVersion;ProductVersion=$v.ProductVersion}|ConvertTo-Json -Compress"
|
||||
],
|
||||
{ encoding: "utf8", env: { ...process.env, MDD_TEST_EXE: target } }
|
||||
));
|
||||
|
||||
expect(metadata).toEqual({
|
||||
FileDescription: "Multi-Debrid-Downloader",
|
||||
ProductName: "Multi-Debrid-Downloader",
|
||||
OriginalFilename: "Multi-Debrid-Downloader.exe",
|
||||
FileVersion: "2.0.17",
|
||||
ProductVersion: "2.0.17"
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves executables owned by active launchers while removing stale ones", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "mdd-dev-cleanup-"));
|
||||
roots.push(root);
|
||||
const owner = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" });
|
||||
const activeTarget = path.join(root, `Multi-Debrid-Downloader-dev-${process.pid}.exe`);
|
||||
const liveTarget = path.join(root, `Multi-Debrid-Downloader-dev-${owner.pid}.exe`);
|
||||
const staleTarget = path.join(root, "Multi-Debrid-Downloader-dev-999999.exe");
|
||||
writeFileSync(activeTarget, "active");
|
||||
writeFileSync(liveTarget, "live");
|
||||
writeFileSync(staleTarget, "stale");
|
||||
try {
|
||||
const result = spawnSync(process.execPath, [
|
||||
path.resolve("node_modules/tsx/dist/cli.mjs"),
|
||||
"scripts/run-dev-electron.ts",
|
||||
"--cleanup-only",
|
||||
root,
|
||||
activeTarget
|
||||
], { cwd: path.resolve("."), encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr || result.stdout).toBe(0);
|
||||
expect(existsSync(activeTarget)).toBe(true);
|
||||
expect(existsSync(liveTarget)).toBe(true);
|
||||
expect(existsSync(staleTarget)).toBe(false);
|
||||
} finally {
|
||||
owner.kill();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -151,14 +151,17 @@ describe("laufender Queue-Linkzähler", () => {
|
||||
describe("responsive Downloadstatus und Servicebezeichnungen", () => {
|
||||
it("keeps full status details while providing compact table text", () => {
|
||||
expect(compactDownloadStatus("Link wird umgewandelt")).toBe("Umwandeln");
|
||||
expect(compactDownloadStatus("Download läuft (Mega-Debrid)")).toBe("DL läuft");
|
||||
expect(compactDownloadStatus("Entpacken 1% (1/1) · Tonspur: Deutsch")).toBe("Entpacken 1%");
|
||||
expect(compactDownloadStatus("0/11 · Entpacken 1% (1/1) · Tonspur: Deutsch")).toBe("Entpacken 1%");
|
||||
expect(compactDownloadStatus("Download läuft (Mega-Debrid API)")).toBe("Download läuft");
|
||||
expect(compactDownloadStatus("Download running (Mega-Debrid API)")).toBe("Download running");
|
||||
expect(compactDownloadStatus("Entpacken 1% (1/1) · Tonspur: Deutsch")).toBe("Entpacken - 1%");
|
||||
expect(compactDownloadStatus("0/11 · Entpacken 53% (1/1) · scn2-httpv7-S01E102.rar")).toBe("Entpacken - 53%");
|
||||
expect(compactDownloadStatus("Extracting 53% (1/1) · archive.rar")).toBe("Extracting - 53%");
|
||||
});
|
||||
|
||||
it("removes duplicated access-mode wording from service labels", () => {
|
||||
expect(normalizeDownloadServiceLabel("Mega-Debrid Web (Web Account)")).toBe("Mega-Debrid Web");
|
||||
expect(normalizeDownloadServiceLabel("Mega-Debrid API (API Account)")).toBe("Mega-Debrid API");
|
||||
expect(normalizeDownloadServiceLabel("Mega-Debrid API (API Access)")).toBe("Mega-Debrid API");
|
||||
expect(normalizeDownloadServiceLabel("Real-Debrid (Web Account)")).toBe("Real-Debrid (Web Account)");
|
||||
expect(normalizeDownloadServiceLabel("Mega-Debrid Web (Web Account), Mega-Debrid API (API Account)")).toBe("Mega-Debrid Web, Mega-Debrid API");
|
||||
expect(compactDownloadServiceLabel("Mega-Debrid Web (Web Account), Mega-Debrid API (API Account)")).toBe("Mega-Debrid");
|
||||
@@ -827,10 +830,11 @@ describe("download table row contracts", () => {
|
||||
selected: false
|
||||
}));
|
||||
|
||||
expect(html).toContain('aria-label="Download läuft (Mega-Debrid)"');
|
||||
expect(html).toContain('aria-label="Download läuft"');
|
||||
expect(html).toContain('class="downloads-status-full"');
|
||||
expect(html).toContain('class="downloads-status-compact"');
|
||||
expect(html).toContain("DL läuft");
|
||||
expect(html.match(/>Download läuft<\/span>/g)).toHaveLength(2);
|
||||
expect(html).toContain('title="Download läuft (Mega-Debrid)"');
|
||||
expect(html).toContain('title="Mega-Debrid Web (Web Account)"');
|
||||
expect(html).toContain('class="downloads-service-full">Mega-Debrid Web</span>');
|
||||
expect(html).toContain('class="downloads-service-compact">Mega-Debrid</span>');
|
||||
@@ -968,6 +972,24 @@ describe("download table row contracts", () => {
|
||||
expect(html).toMatch(/title="0\/1 · Entpacken 1% · Tonspur: 1 OK[^\"]*episode\.mkv: remuxed \(German kept\)"/s);
|
||||
});
|
||||
|
||||
it("shows only the operation in an actively downloading package status", () => {
|
||||
const activePackage = pkg("active-package", "Active package", ["active-item"]);
|
||||
const html = renderToStaticMarkup(PackageCardContent({
|
||||
actions: createActions(),
|
||||
columnOrder: ["status"],
|
||||
editing: false,
|
||||
editingName: "",
|
||||
gridTemplate: "220px",
|
||||
packageSpeedBps: 1_000,
|
||||
row: { package: activePackage, items: [item("active-item", activePackage.id, "downloading", { fullStatus: "Download läuft (Mega-Debrid API)" })], collapsed: true },
|
||||
selectedIds: new Set<string>(),
|
||||
selectedVersion: 0
|
||||
}));
|
||||
|
||||
expect(html.match(/>Download läuft<\/span>/g)).toHaveLength(2);
|
||||
expect(html).toContain('title="0/1"');
|
||||
});
|
||||
|
||||
it("commits Enter and the resulting Blur rename sequence exactly once", () => {
|
||||
const commits: string[] = [];
|
||||
const model = withRuntime(createInput());
|
||||
|
||||
Reference in New Issue
Block a user