fix(update): verify and apply the actual latest version
This commit is contained in:
@@ -5,6 +5,7 @@ import os from "node:os";
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
|
import { extractFile } from "@electron/asar";
|
||||||
|
|
||||||
const EXPECTED_PUBLISH = Object.freeze({
|
const EXPECTED_PUBLISH = Object.freeze({
|
||||||
provider: "github",
|
provider: "github",
|
||||||
@@ -140,6 +141,14 @@ function hasExtraResource(extraResources, expected) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readPackagedMainVersion(asarPath) {
|
||||||
|
requireNonEmptyFile(asarPath, "packaged app.asar");
|
||||||
|
const packagePayload = JSON.parse(extractFile(asarPath, "package.json").toString("utf8"));
|
||||||
|
const mainBundle = extractFile(asarPath, path.win32.join("build", "main", "main", "main.js")).toString("utf8");
|
||||||
|
const bundled = /name:\s*["']multi-debrid-downloader["']\s*,\s*version:\s*["']([^"']+)["']/.exec(mainBundle)?.[1] || "";
|
||||||
|
return { packageVersion: String(packagePayload?.version || ""), bundledVersion: bundled };
|
||||||
|
}
|
||||||
|
|
||||||
function sha256File(filePath) {
|
function sha256File(filePath) {
|
||||||
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
||||||
}
|
}
|
||||||
@@ -325,6 +334,9 @@ export function verifyPublicRelease(rootDir = process.cwd()) {
|
|||||||
const expectedPortable = `${EXPECTED_PRODUCT_NAME}-${version}-portable.exe`;
|
const expectedPortable = `${EXPECTED_PRODUCT_NAME}-${version}-portable.exe`;
|
||||||
const expectedBlockmap = `${expectedSetup}.blockmap`;
|
const expectedBlockmap = `${expectedSetup}.blockmap`;
|
||||||
assertEqual(latestFields.path, expectedSetup, "latest.yml path");
|
assertEqual(latestFields.path, expectedSetup, "latest.yml path");
|
||||||
|
const packagedVersions = readPackagedMainVersion(path.join(releaseDir, "win-unpacked", "resources", "app.asar"));
|
||||||
|
assertEqual(packagedVersions.packageVersion, version, "packaged package version");
|
||||||
|
assertEqual(packagedVersions.bundledVersion, version, "packaged main bundle version");
|
||||||
|
|
||||||
const requiredArtifacts = [expectedSetup, expectedPortable, expectedBlockmap];
|
const requiredArtifacts = [expectedSetup, expectedPortable, expectedBlockmap];
|
||||||
const missingArtifacts = requiredArtifacts.filter((fileName) => !fs.existsSync(path.join(releaseDir, fileName)));
|
const missingArtifacts = requiredArtifacts.filter((fileName) => !fs.existsSync(path.join(releaseDir, fileName)));
|
||||||
|
|||||||
@@ -898,13 +898,9 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
|
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
|
||||||
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
|
|
||||||
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
|
|
||||||
? this.lastUpdateCheck
|
|
||||||
: undefined;
|
|
||||||
const result = await runInstallWithResume(
|
const result = await runInstallWithResume(
|
||||||
this.manager,
|
this.manager,
|
||||||
() => installLatestUpdate(this.settings.updateRepo, cached, onProgress)
|
() => installLatestUpdate(this.settings.updateRepo, undefined, onProgress)
|
||||||
);
|
);
|
||||||
if (result.started) {
|
if (result.started) {
|
||||||
this.lastUpdateCheck = null;
|
this.lastUpdateCheck = null;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function normalizedVersion(value: unknown): string {
|
||||||
|
const version = String(value || "").trim();
|
||||||
|
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version) ? version : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuntimeAppVersion(
|
||||||
|
fallbackVersion: string,
|
||||||
|
resourcesPath: string = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath || ""
|
||||||
|
): string {
|
||||||
|
if (resourcesPath) {
|
||||||
|
for (const relativePath of ["app.asar/package.json", "app/package.json"]) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(fs.readFileSync(path.join(resourcesPath, ...relativePath.split("/")), "utf8")) as { version?: unknown };
|
||||||
|
const version = normalizedVersion(payload.version);
|
||||||
|
if (version) return version;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizedVersion(fallbackVersion) || "0.0.0";
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ import os from "node:os";
|
|||||||
import { AppSettings } from "../shared/types";
|
import { AppSettings } from "../shared/types";
|
||||||
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
|
||||||
import packageJson from "../../package.json";
|
import packageJson from "../../package.json";
|
||||||
|
import { resolveRuntimeAppVersion } from "./app-version";
|
||||||
|
|
||||||
export const APP_NAME = "Multi Debrid Downloader";
|
export const APP_NAME = "Multi Debrid Downloader";
|
||||||
export const APP_VERSION: string = packageJson.version;
|
export const APP_VERSION: string = resolveRuntimeAppVersion(packageJson.version);
|
||||||
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
||||||
|
|
||||||
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||||
|
|||||||
+22
-11
@@ -1491,6 +1491,14 @@ export function shouldApplyUpdateCheckResult(
|
|||||||
return completedGeneration === currentGeneration;
|
return completedGeneration === currentGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldOpenUpdatePrompt(
|
||||||
|
source: "manual" | "startup",
|
||||||
|
latestTag: string,
|
||||||
|
dismissedTag: string
|
||||||
|
): boolean {
|
||||||
|
return source === "manual" || !latestTag || latestTag !== dismissedTag;
|
||||||
|
}
|
||||||
|
|
||||||
export async function runLatestUpdateCheck(
|
export async function runLatestUpdateCheck(
|
||||||
generationRef: { current: number },
|
generationRef: { current: number },
|
||||||
check: () => Promise<UpdateCheckResult>,
|
check: () => Promise<UpdateCheckResult>,
|
||||||
@@ -1523,6 +1531,7 @@ export function App(): ReactElement {
|
|||||||
const [scheduleCountdown, setScheduleCountdown] = useState("");
|
const [scheduleCountdown, setScheduleCountdown] = useState("");
|
||||||
const [runtimeNow, setRuntimeNow] = useState(() => Date.now());
|
const [runtimeNow, setRuntimeNow] = useState(() => Date.now());
|
||||||
const updateCheckGenerationRef = useRef(0);
|
const updateCheckGenerationRef = useRef(0);
|
||||||
|
const dismissedUpdateTagRef = useRef("");
|
||||||
const settingsDirtyRef = useRef(false);
|
const settingsDirtyRef = useRef(false);
|
||||||
const writeOnlySettingsDirtyRef = useRef(new Set<"archivePasswordList" | "notifyUrl">());
|
const writeOnlySettingsDirtyRef = useRef(new Set<"archivePasswordList" | "notifyUrl">());
|
||||||
const archivePasswordLoadGenerationRef = useRef(0);
|
const archivePasswordLoadGenerationRef = useRef(0);
|
||||||
@@ -2589,7 +2598,15 @@ export function App(): ReactElement {
|
|||||||
releaseNotes: changelogText
|
releaseNotes: changelogText
|
||||||
});
|
});
|
||||||
setUpdateInstallProgress(null);
|
setUpdateInstallProgress(null);
|
||||||
setUpdateDialogOpen(true);
|
setUpdateDialogOpen(shouldOpenUpdatePrompt(source, result.latestTag, dismissedUpdateTagRef.current));
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissUpdatePrompt = (): void => {
|
||||||
|
if (availableUpdate?.latestTag) {
|
||||||
|
dismissedUpdateTagRef.current = availableUpdate.latestTag;
|
||||||
|
}
|
||||||
|
setUpdateDialogOpen(false);
|
||||||
|
setUpdateInstallProgress(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const installUpdate = async (): Promise<void> => {
|
const installUpdate = async (): Promise<void> => {
|
||||||
@@ -5737,12 +5754,9 @@ export function App(): ReactElement {
|
|||||||
available={Boolean(availableUpdate)}
|
available={Boolean(availableUpdate)}
|
||||||
currentVersion={availableUpdate?.currentVersion ?? appVersion}
|
currentVersion={availableUpdate?.currentVersion ?? appVersion}
|
||||||
latestTag={availableUpdate?.latestTag ?? ""}
|
latestTag={availableUpdate?.latestTag ?? ""}
|
||||||
onClose={() => {
|
onClose={dismissUpdatePrompt}
|
||||||
setUpdateDialogOpen(false);
|
|
||||||
setUpdateInstallProgress(null);
|
|
||||||
}}
|
|
||||||
onInstall={() => { void installUpdate(); }}
|
onInstall={() => { void installUpdate(); }}
|
||||||
onLater={() => setUpdateDialogOpen(false)}
|
onLater={dismissUpdatePrompt}
|
||||||
onOpen={() => setUpdateDialogOpen(true)}
|
onOpen={() => setUpdateDialogOpen(true)}
|
||||||
open={updateDialogOpen || updateInstallProgress !== null}
|
open={updateDialogOpen || updateInstallProgress !== null}
|
||||||
progress={updateInstallProgress ? {
|
progress={updateInstallProgress ? {
|
||||||
@@ -6708,12 +6722,9 @@ export function App(): ReactElement {
|
|||||||
available={Boolean(availableUpdate)}
|
available={Boolean(availableUpdate)}
|
||||||
currentVersion={availableUpdate?.currentVersion ?? appVersion}
|
currentVersion={availableUpdate?.currentVersion ?? appVersion}
|
||||||
latestTag={availableUpdate?.latestTag ?? ""}
|
latestTag={availableUpdate?.latestTag ?? ""}
|
||||||
onClose={() => {
|
onClose={dismissUpdatePrompt}
|
||||||
setUpdateDialogOpen(false);
|
|
||||||
setUpdateInstallProgress(null);
|
|
||||||
}}
|
|
||||||
onInstall={() => { void installUpdate(); }}
|
onInstall={() => { void installUpdate(); }}
|
||||||
onLater={() => setUpdateDialogOpen(false)}
|
onLater={dismissUpdatePrompt}
|
||||||
onOpen={() => setUpdateDialogOpen(true)}
|
onOpen={() => setUpdateDialogOpen(true)}
|
||||||
open={updateDialogOpen || updateInstallProgress !== null}
|
open={updateDialogOpen || updateInstallProgress !== null}
|
||||||
progress={updateInstallProgress ? {
|
progress={updateInstallProgress ? {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--ui-update);
|
background: var(--ui-success);
|
||||||
color: var(--ui-update-text);
|
color: var(--ui-update-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-update-trigger:hover {
|
.md-update-trigger:hover {
|
||||||
background: var(--ui-update-hover);
|
background: color-mix(in srgb, var(--ui-success) 82%, #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-update-trigger:focus-visible,
|
.md-update-trigger:focus-visible,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { resolveRuntimeAppVersion } from "../src/main/app-version";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runtime app version", () => {
|
||||||
|
it("uses the version from the installed app package instead of a stale bundled fallback", () => {
|
||||||
|
const resourcesPath = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-version-"));
|
||||||
|
tempDirs.push(resourcesPath);
|
||||||
|
fs.mkdirSync(path.join(resourcesPath, "app.asar"));
|
||||||
|
fs.writeFileSync(path.join(resourcesPath, "app.asar", "package.json"), JSON.stringify({ version: "2.0.51" }), "utf8");
|
||||||
|
|
||||||
|
expect(resolveRuntimeAppVersion("2.0.50", resourcesPath)).toBe("2.0.51");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the bundled fallback when no installed package metadata is available", () => {
|
||||||
|
const resourcesPath = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-version-"));
|
||||||
|
tempDirs.push(resourcesPath);
|
||||||
|
|
||||||
|
expect(resolveRuntimeAppVersion("2.0.51", resourcesPath)).toBe("2.0.51");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { createPackage } from "@electron/asar";
|
||||||
|
|
||||||
type ReleaseVerification = {
|
type ReleaseVerification = {
|
||||||
publish: {
|
publish: {
|
||||||
@@ -39,6 +40,20 @@ const { verifyPublicRelease, verifyReleaseArchives } = await import(verifierUrl)
|
|||||||
) => ArchiveVerification;
|
) => ArchiveVerification;
|
||||||
};
|
};
|
||||||
const fixtureRoots: string[] = [];
|
const fixtureRoots: string[] = [];
|
||||||
|
async function createFixtureAsar(version: string): Promise<Buffer> {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "public-release-asar-"));
|
||||||
|
const sourceDir = path.join(rootDir, "source");
|
||||||
|
const outputPath = path.join(rootDir, "app.asar");
|
||||||
|
fs.mkdirSync(path.join(sourceDir, "build", "main", "main"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(sourceDir, "package.json"), JSON.stringify({ name: "multi-debrid-downloader", version }), "utf8");
|
||||||
|
fs.writeFileSync(path.join(sourceDir, "build", "main", "main", "main.js"), `var package_default = { name: "multi-debrid-downloader", version: "${version}" };`, "utf8");
|
||||||
|
await createPackage(sourceDir, outputPath);
|
||||||
|
const payload = fs.readFileSync(outputPath);
|
||||||
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
const validAppAsar = await createFixtureAsar("1.7.233");
|
||||||
|
const staleAppAsar = await createFixtureAsar("1.7.232");
|
||||||
const redistributionFiles = [
|
const redistributionFiles = [
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"THIRD_PARTY_NOTICES.md",
|
"THIRD_PARTY_NOTICES.md",
|
||||||
@@ -172,6 +187,7 @@ function createReleaseFixture(): string {
|
|||||||
writeRedistributionFiles(rootDir, true);
|
writeRedistributionFiles(rootDir, true);
|
||||||
writeFile(rootDir, "assets/app_icon.ico", "application-icon");
|
writeFile(rootDir, "assets/app_icon.ico", "application-icon");
|
||||||
writeFile(rootDir, "win-unpacked/resources/assets/app_icon.ico", "application-icon");
|
writeFile(rootDir, "win-unpacked/resources/assets/app_icon.ico", "application-icon");
|
||||||
|
writeFile(rootDir, "win-unpacked/resources/app.asar", validAppAsar);
|
||||||
writeFile(rootDir, "resources/installer.nsh", "!macro customCheckAppRunning\n${isUpdated}\nFIND_PROCESS\ntaskkill /f /im\n!macroend\n");
|
writeFile(rootDir, "resources/installer.nsh", "!macro customCheckAppRunning\n${isUpdated}\nFIND_PROCESS\ntaskkill /f /im\n!macroend\n");
|
||||||
|
|
||||||
return rootDir;
|
return rootDir;
|
||||||
@@ -357,6 +373,13 @@ describe("public release metadata", () => {
|
|||||||
expect(() => verifyPublicRelease(rootDir)).toThrow(/NSIS|update|installer/i);
|
expect(() => verifyPublicRelease(rootDir)).toThrow(/NSIS|update|installer/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a packaged main bundle built before the release version changed", () => {
|
||||||
|
const rootDir = createReleaseFixture();
|
||||||
|
fs.writeFileSync(path.join(rootDir, "win-unpacked", "resources", "app.asar"), staleAppAsar);
|
||||||
|
|
||||||
|
expect(() => verifyPublicRelease(rootDir)).toThrow(/main bundle|version/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects a packaged application without its window and tray icon", () => {
|
it("rejects a packaged application without its window and tray icon", () => {
|
||||||
const rootDir = createReleaseFixture();
|
const rootDir = createReleaseFixture();
|
||||||
fs.rmSync(path.join(rootDir, "win-unpacked", "resources", "assets", "app_icon.ico"));
|
fs.rmSync(path.join(rootDir, "win-unpacked", "resources", "assets", "app_icon.ico"));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
|
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult, shouldOpenUpdatePrompt } from "../src/renderer/App";
|
||||||
import type { UpdateCheckResult } from "../src/shared/types";
|
import type { UpdateCheckResult } from "../src/shared/types";
|
||||||
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
||||||
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
||||||
@@ -14,6 +14,12 @@ const callbacks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("update experience", () => {
|
describe("update experience", () => {
|
||||||
|
it("keeps a dismissed update version closed until the app restarts or a newer version appears", () => {
|
||||||
|
expect(shouldOpenUpdatePrompt("startup", "v2.0.51", "v2.0.51")).toBe(false);
|
||||||
|
expect(shouldOpenUpdatePrompt("startup", "v2.0.52", "v2.0.51")).toBe(true);
|
||||||
|
expect(shouldOpenUpdatePrompt("manual", "v2.0.51", "v2.0.51")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders the available update and prompt as one accessible experience", () => {
|
it("renders the available update and prompt as one accessible experience", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
@@ -146,15 +152,15 @@ describe("update experience", () => {
|
|||||||
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
|
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses a light-blue update affordance and a bounded scrollable changelog", () => {
|
it("uses a green update affordance and a bounded scrollable changelog", () => {
|
||||||
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
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");
|
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:\s*#BAD0FC;/);
|
||||||
expect(theme).toMatch(/--ui-update-hover:\s*#8AA5DC;/);
|
expect(theme).toMatch(/--ui-update-hover:\s*#8AA5DC;/);
|
||||||
expect(theme).toMatch(/--ui-update-text:\s*#181A1F;/);
|
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\s*\{[^}]*background:\s*var\(--ui-success\);[^}]*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-trigger:hover\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--ui-success\) 82%, #000\);/s);
|
||||||
expect(css).toMatch(/\.md-update-release-notes pre\s*\{[^}]*max-height:\s*min\(360px, 45vh\);[^}]*overflow-y:\s*auto;/s);
|
expect(css).toMatch(/\.md-update-release-notes pre\s*\{[^}]*max-height:\s*min\(360px, 45vh\);[^}]*overflow-y:\s*auto;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
|
it("always refreshes release metadata before installing instead of using the previous check result", () => {
|
||||||
|
const controller = fs.readFileSync(new URL("../src/main/app-controller.ts", import.meta.url), "utf8");
|
||||||
|
const install = controller.slice(controller.indexOf("public async installUpdate"), controller.indexOf("public addLinks"));
|
||||||
|
|
||||||
|
expect(install).toContain("installLatestUpdate(this.settings.updateRepo, undefined, onProgress)");
|
||||||
|
expect(install).not.toContain("cacheAgeMs");
|
||||||
|
expect(install).not.toContain("this.lastUpdateCheck &&");
|
||||||
|
});
|
||||||
|
|
||||||
it("normalizes update repo input", () => {
|
it("normalizes update repo input", () => {
|
||||||
expect(normalizeUpdateRepo("")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
|
expect(normalizeUpdateRepo("")).toBe("Sucukdeluxe/Multi-Debrid-Downloader");
|
||||||
expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo");
|
expect(normalizeUpdateRepo("owner/repo")).toBe("owner/repo");
|
||||||
|
|||||||
Reference in New Issue
Block a user