mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:40:37 +00:00
feat(updater): support ignoring a specific version
This commit is contained in:
@@ -172,11 +172,13 @@ const {
|
||||
updateDownloaded,
|
||||
isInstallingUpdate,
|
||||
updateReady,
|
||||
isIgnoringUpdate,
|
||||
activeTaskCount: activeUpdateTaskCount,
|
||||
hasUpdateAvailable,
|
||||
openUrl,
|
||||
checkUpdates,
|
||||
openLatestRelease,
|
||||
ignoreCurrentVersion,
|
||||
downloadAndInstallUpdate,
|
||||
cancelDownload,
|
||||
installDownloadedUpdate,
|
||||
@@ -2857,12 +2859,14 @@ onUnmounted(() => {
|
||||
:update-downloaded="updateDownloaded"
|
||||
:is-installing-update="isInstallingUpdate"
|
||||
:update-ready="updateReady"
|
||||
:is-ignoring-update="isIgnoringUpdate"
|
||||
:active-task-count="activeUpdateTaskCount"
|
||||
@open-latest-release="openLatestRelease"
|
||||
@download-and-install="downloadAndInstallUpdate"
|
||||
@cancel-download="cancelDownload"
|
||||
@install-downloaded="installDownloadedUpdate"
|
||||
@restart="restartApp"
|
||||
@ignore-version="ignoreCurrentVersion"
|
||||
/>
|
||||
<ExternalSqlFileChangeDialog :prompt="externalSqlFilePrompt" @decide="externalSqlFileChanges.resolvePrompt" />
|
||||
<CloseActionPromptDialog v-if="isDesktop && showCloseActionPrompt" :open="showCloseActionPrompt" @update:open="handleCloseActionPromptOpenChange" @quit="chooseQuit" @minimize="chooseMinimize" />
|
||||
|
||||
+31
-1
@@ -20,6 +20,7 @@ interface DialogState {
|
||||
updateDownloaded: boolean;
|
||||
isInstallingUpdate: boolean;
|
||||
updateReady: boolean;
|
||||
isIgnoringUpdate: boolean;
|
||||
}
|
||||
|
||||
async function flushDialog() {
|
||||
@@ -37,10 +38,12 @@ async function mountDialog(activeTaskCount: number, initialState: Partial<Dialog
|
||||
updateDownloaded: false,
|
||||
isInstallingUpdate: false,
|
||||
updateReady: false,
|
||||
isIgnoringUpdate: false,
|
||||
...initialState,
|
||||
});
|
||||
const downloadAndInstall = vi.fn();
|
||||
const cancelDownload = vi.fn();
|
||||
const ignoreVersion = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
@@ -81,10 +84,12 @@ async function mountDialog(activeTaskCount: number, initialState: Partial<Dialog
|
||||
updateDownloaded: state.updateDownloaded,
|
||||
isInstallingUpdate: state.isInstallingUpdate,
|
||||
updateReady: state.updateReady,
|
||||
isIgnoringUpdate: state.isIgnoringUpdate,
|
||||
activeTaskCount,
|
||||
"onDownload-and-install": downloadAndInstall,
|
||||
"onCancel-download": cancelDownload,
|
||||
"onInstall-downloaded": handleInstallDownloaded,
|
||||
"onIgnore-version": ignoreVersion,
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -94,7 +99,7 @@ async function mountDialog(activeTaskCount: number, initialState: Partial<Dialog
|
||||
app.mount(container);
|
||||
await flushDialog();
|
||||
|
||||
return { state, downloadAndInstall, cancelDownload, installDownloaded };
|
||||
return { state, downloadAndInstall, cancelDownload, installDownloaded, ignoreVersion };
|
||||
}
|
||||
|
||||
function buttonWithText(text: string): HTMLButtonElement | undefined {
|
||||
@@ -275,3 +280,28 @@ describe("UpdateDialog close protection", () => {
|
||||
expect(downloadAndInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateDialog ignore version", () => {
|
||||
it("emits ignore-version when the ignore button is clicked", async () => {
|
||||
const { ignoreVersion } = await mountDialog(0);
|
||||
|
||||
const ignoreButton = buttonWithText("Ignore this version");
|
||||
expect(ignoreButton).toBeDefined();
|
||||
ignoreButton?.click();
|
||||
await flushDialog();
|
||||
|
||||
expect(ignoreVersion).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("hides the ignore button once an update has been downloaded", async () => {
|
||||
await mountDialog(0, { updateDownloaded: true, downloadProgress: 100 });
|
||||
|
||||
expect(buttonWithText("Ignore this version")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("disables the ignore button while the setting is being persisted", async () => {
|
||||
await mountDialog(0, { isIgnoringUpdate: true });
|
||||
|
||||
expect(buttonWithText("Ignore this version")?.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ const props = defineProps<{
|
||||
updateDownloaded: boolean;
|
||||
isInstallingUpdate: boolean;
|
||||
updateReady: boolean;
|
||||
isIgnoringUpdate: boolean;
|
||||
activeTaskCount: number;
|
||||
}>();
|
||||
|
||||
@@ -27,6 +28,7 @@ const emit = defineEmits<{
|
||||
"cancel-download": [];
|
||||
"install-downloaded": [];
|
||||
restart: [];
|
||||
"ignore-version": [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -35,6 +37,7 @@ const isDesktop = isTauriRuntime();
|
||||
const renderedNotes = ref("");
|
||||
// Only active file replacement (installation) must trap the dialog.
|
||||
const isCloseBlocked = computed(() => props.isInstallingUpdate);
|
||||
const canIgnoreVersion = computed(() => props.updateInfo?.update_available === true && !props.updateDownloaded && !props.isDownloadingUpdate && !props.isInstallingUpdate && !props.updateReady);
|
||||
|
||||
function handleCancel() {
|
||||
handleOpenChange(false);
|
||||
@@ -136,6 +139,10 @@ watch(
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button v-if="!isCloseBlocked" variant="outline" @click="handleCancel">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button v-if="canIgnoreVersion" variant="ghost" :disabled="isIgnoringUpdate" @click="emit('ignore-version')">
|
||||
<Loader2 v-if="isIgnoringUpdate" class="h-4 w-4 animate-spin" />
|
||||
{{ t("updates.ignoreVersion") }}
|
||||
</Button>
|
||||
<template v-if="updateInfo?.update_available">
|
||||
<Button variant="outline" @click="emit('open-latest-release')">{{ t("updates.openRelease") }}</Button>
|
||||
<template v-if="canDownloadAndInstallUpdate(updateInfo, isDesktop)">
|
||||
|
||||
@@ -11,16 +11,21 @@ const apiMock = vi.hoisted(() => ({
|
||||
installDownloadedUpdate: vi.fn<() => Promise<void>>(),
|
||||
}));
|
||||
const listenMock = vi.hoisted(() => vi.fn());
|
||||
const toastMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({
|
||||
isTauriRuntime: () => true,
|
||||
}));
|
||||
vi.mock("@/composables/useToast", () => ({
|
||||
useToast: () => ({ toast: vi.fn() }),
|
||||
useToast: () => ({ toast: toastMock }),
|
||||
}));
|
||||
const settingsStoreMock = vi.hoisted(() => ({
|
||||
editorSettings: { updateDownloadSource: "official", ignoredUpdateVersion: "" },
|
||||
updateEditorSettingsAndPersist: vi.fn<() => Promise<void>>(),
|
||||
}));
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({ editorSettings: { updateDownloadSource: "official" } }),
|
||||
useSettingsStore: () => settingsStoreMock,
|
||||
}));
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: listenMock,
|
||||
@@ -47,6 +52,8 @@ let container: HTMLDivElement | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
settingsStoreMock.editorSettings.ignoredUpdateVersion = "";
|
||||
settingsStoreMock.updateEditorSettingsAndPersist.mockResolvedValue();
|
||||
listenMock.mockResolvedValue(vi.fn());
|
||||
apiMock.installDownloadedUpdate.mockResolvedValue();
|
||||
});
|
||||
@@ -116,3 +123,73 @@ describe("useAppUpdater download attempts", () => {
|
||||
expect(updater.updateReady.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useAppUpdater ignore version", () => {
|
||||
it("persists the ignored latest version and closes the update dialog", async () => {
|
||||
let updater!: ReturnType<typeof useAppUpdater>;
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
updater = useAppUpdater();
|
||||
return () => h("div");
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
updater.updateInfo.value = {
|
||||
current_version: "0.5.69",
|
||||
latest_version: "0.5.70",
|
||||
update_available: true,
|
||||
release_name: "DBX v0.5.70",
|
||||
release_url: "https://github.com/t8y2/dbx/releases/tag/v0.5.70",
|
||||
release_notes: "",
|
||||
};
|
||||
updater.showUpdateDialog.value = true;
|
||||
|
||||
await updater.ignoreCurrentVersion();
|
||||
|
||||
expect(settingsStoreMock.updateEditorSettingsAndPersist).toHaveBeenCalledWith({ ignoredUpdateVersion: "0.5.70" });
|
||||
expect(updater.showUpdateDialog.value).toBe(false);
|
||||
expect(toastMock).toHaveBeenCalledWith("Version v0.5.70 ignored. You'll be reminded when the next version releases.", 5000);
|
||||
});
|
||||
|
||||
it("keeps the dialog open and allows retry when persistence fails", async () => {
|
||||
settingsStoreMock.updateEditorSettingsAndPersist.mockRejectedValueOnce(new Error("storage unavailable")).mockResolvedValueOnce();
|
||||
let updater!: ReturnType<typeof useAppUpdater>;
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
updater = useAppUpdater();
|
||||
return () => h("div");
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
updater.updateInfo.value = {
|
||||
current_version: "0.5.69",
|
||||
latest_version: "0.5.70",
|
||||
update_available: true,
|
||||
release_name: "DBX v0.5.70",
|
||||
release_url: "https://github.com/t8y2/dbx/releases/tag/v0.5.70",
|
||||
release_notes: "",
|
||||
};
|
||||
updater.showUpdateDialog.value = true;
|
||||
|
||||
await updater.ignoreCurrentVersion();
|
||||
|
||||
expect(updater.showUpdateDialog.value).toBe(true);
|
||||
expect(updater.isIgnoringUpdate.value).toBe(false);
|
||||
expect(toastMock).toHaveBeenLastCalledWith("Failed to save the ignored version: storage unavailable", 5000);
|
||||
|
||||
await updater.ignoreCurrentVersion();
|
||||
|
||||
expect(settingsStoreMock.updateEditorSettingsAndPersist).toHaveBeenCalledTimes(2);
|
||||
expect(updater.showUpdateDialog.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,62 @@ export function canDownloadAndInstallUpdate(info: api.UpdateInfo | null, isDeskt
|
||||
return isDesktop && info?.update_available === true && info.manual_update_only !== true;
|
||||
}
|
||||
|
||||
interface ParsedUpdateVersion {
|
||||
core: [string, string, string];
|
||||
prerelease: string[];
|
||||
}
|
||||
|
||||
function normalizeUpdateVersion(version: string): string {
|
||||
return version.trim().replace(/^[vV]/, "");
|
||||
}
|
||||
|
||||
function parseUpdateVersion(version: string): ParsedUpdateVersion | null {
|
||||
const match = normalizeUpdateVersion(version).match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
core: [match[1], match[2], match[3]],
|
||||
prerelease: match[4]?.split(".") ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function compareNumericVersionIdentifier(left: string, right: string): number {
|
||||
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
||||
return left === right ? 0 : left < right ? -1 : 1;
|
||||
}
|
||||
|
||||
function compareParsedUpdateVersions(left: ParsedUpdateVersion, right: ParsedUpdateVersion): number {
|
||||
for (let index = 0; index < left.core.length; index += 1) {
|
||||
const comparison = compareNumericVersionIdentifier(left.core[index], right.core[index]);
|
||||
if (comparison !== 0) return comparison;
|
||||
}
|
||||
if (!left.prerelease.length || !right.prerelease.length) {
|
||||
if (left.prerelease.length === right.prerelease.length) return 0;
|
||||
return left.prerelease.length ? -1 : 1;
|
||||
}
|
||||
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const leftIdentifier = left.prerelease[index];
|
||||
const rightIdentifier = right.prerelease[index];
|
||||
if (leftIdentifier === undefined || rightIdentifier === undefined) return leftIdentifier === undefined ? -1 : 1;
|
||||
if (leftIdentifier === rightIdentifier) continue;
|
||||
const leftNumeric = /^\d+$/.test(leftIdentifier);
|
||||
const rightNumeric = /^\d+$/.test(rightIdentifier);
|
||||
if (leftNumeric && rightNumeric) return compareNumericVersionIdentifier(leftIdentifier, rightIdentifier);
|
||||
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
||||
return leftIdentifier < rightIdentifier ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isUpdateIgnored(info: api.UpdateInfo | null, ignoredVersion: string | undefined): boolean {
|
||||
const latest = info?.latest_version;
|
||||
if (!latest || !ignoredVersion) return false;
|
||||
const parsedLatest = parseUpdateVersion(latest);
|
||||
const parsedIgnored = parseUpdateVersion(ignoredVersion);
|
||||
if (!parsedLatest || !parsedIgnored) return normalizeUpdateVersion(latest) === normalizeUpdateVersion(ignoredVersion);
|
||||
return compareParsedUpdateVersions(parsedLatest, parsedIgnored) <= 0;
|
||||
}
|
||||
|
||||
export function normalizeUpdateDownloadSource(value: unknown): SettingsUpdateDownloadSource {
|
||||
// Old persisted AtomGit preferences should retain their mainland mirror behavior.
|
||||
if (value === "atomgit") return "cnb";
|
||||
@@ -65,8 +121,9 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
const updateDownloaded = ref(false);
|
||||
const isInstallingUpdate = ref(false);
|
||||
const updateReady = ref(false);
|
||||
const isIgnoringUpdate = ref(false);
|
||||
const activeTaskCount = computed(() => Math.max(0, Math.trunc(options.getActiveTaskCount?.() ?? 0)));
|
||||
const hasUpdateAvailable = computed(() => updateInfo.value?.update_available === true);
|
||||
const hasUpdateAvailable = computed(() => updateInfo.value?.update_available === true && !isUpdateIgnored(updateInfo.value, settingsStore.editorSettings.ignoredUpdateVersion));
|
||||
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
|
||||
let activeDownloadAttempt = 0;
|
||||
let pendingCancellation: Promise<void> | undefined;
|
||||
@@ -123,6 +180,21 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
openUrl(url);
|
||||
}
|
||||
|
||||
async function ignoreCurrentVersion() {
|
||||
const latest = updateInfo.value?.latest_version;
|
||||
if (!latest || isIgnoringUpdate.value) return;
|
||||
isIgnoringUpdate.value = true;
|
||||
try {
|
||||
await settingsStore.updateEditorSettingsAndPersist({ ignoredUpdateVersion: latest });
|
||||
showUpdateDialog.value = false;
|
||||
toast(t("updates.versionIgnored", { version: tagVersion(latest) }), 5000);
|
||||
} catch (error) {
|
||||
toast(t("updates.ignoreVersionFailed", { error: error instanceof Error ? error.message : String(error) }), 5000);
|
||||
} finally {
|
||||
isIgnoringUpdate.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function blockUpdateForActiveTasks(): boolean {
|
||||
if (!shouldBlockAppUpdate(activeTaskCount.value)) return false;
|
||||
toast(t("updates.activeTasksBlockUpdate", { count: activeTaskCount.value }), 5000);
|
||||
@@ -250,6 +322,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
updateDownloaded,
|
||||
isInstallingUpdate,
|
||||
updateReady,
|
||||
isIgnoringUpdate,
|
||||
activeTaskCount,
|
||||
hasUpdateAvailable,
|
||||
latestReleaseUrl,
|
||||
@@ -257,6 +330,7 @@ export function useAppUpdater(options: UseAppUpdaterOptions = {}) {
|
||||
checkUpdates,
|
||||
formatUpdateError,
|
||||
openLatestRelease,
|
||||
ignoreCurrentVersion,
|
||||
downloadAndInstallUpdate,
|
||||
cancelDownload,
|
||||
installDownloadedUpdate,
|
||||
|
||||
@@ -284,6 +284,9 @@ export default {
|
||||
failed: "Failed to check updates: {error}",
|
||||
rateLimited: "GitHub update checks are temporarily rate limited. You can still open the release page to check manually.",
|
||||
openRelease: "Open Release",
|
||||
ignoreVersion: "Ignore this version",
|
||||
versionIgnored: "Version {version} ignored. You'll be reminded when the next version releases.",
|
||||
ignoreVersionFailed: "Failed to save the ignored version: {error}",
|
||||
downloadAndInstall: "Download & Install",
|
||||
activeTasksBlockUpdate: "{count} task(s) are still running. Wait for them to finish before updating DBX.",
|
||||
portableAutomaticUpdate: "DBX will download the signed portable ZIP, replace only DBX.exe after exit, and restart automatically. portable.dbx and data will be kept.",
|
||||
|
||||
@@ -285,6 +285,9 @@ export default withEnglishFallback({
|
||||
failed: "Error al buscar actualizaciones: {error}",
|
||||
rateLimited: "Las verificaciones de actualizaciones de GitHub están temporalmente limitadas. Puedes abrir la página de lanzamientos para verificar manualmente.",
|
||||
openRelease: "Abrir lanzamiento",
|
||||
ignoreVersion: "Ignorar esta versión",
|
||||
versionIgnored: "Versión {version} ignorada. Se te recordará cuando se publique la próxima versión.",
|
||||
ignoreVersionFailed: "No se pudo guardar la versión ignorada: {error}",
|
||||
downloadAndInstall: "Descargar e instalar",
|
||||
activeTasksBlockUpdate: "Hay {count} tarea(s) en ejecución. Espera a que terminen antes de actualizar DBX.",
|
||||
portableAutomaticUpdate: "DBX descargará el ZIP portable firmado, reemplazará solo DBX.exe después de salir y se reiniciará automáticamente. portable.dbx y data se conservarán.",
|
||||
|
||||
@@ -284,6 +284,9 @@ export default withEnglishFallback({
|
||||
failed: "Verifica aggiornamenti non riuscita: {error}",
|
||||
rateLimited: "Le verifiche degli aggiornamenti di GitHub sono temporaneamente limitate. Puoi comunque aprire la pagina delle release per verificare manualmente.",
|
||||
openRelease: "Apri Release",
|
||||
ignoreVersion: "Ignora questa versione",
|
||||
versionIgnored: "Versione {version} ignorata. Verrai avvisato al rilascio della prossima versione.",
|
||||
ignoreVersionFailed: "Impossibile salvare la versione ignorata: {error}",
|
||||
downloadAndInstall: "Scarica e Installa",
|
||||
activeTasksBlockUpdate: "Ci sono {count} attività in esecuzione. Attendi che terminino prima di aggiornare DBX.",
|
||||
portableAutomaticUpdate: "DBX scaricherà lo ZIP portatile firmato, sostituirà solo DBX.exe dopo l'uscita e si riavvierà automaticamente. portable.dbx e i dati verranno mantenuti.",
|
||||
|
||||
@@ -285,6 +285,9 @@ export default withEnglishFallback({
|
||||
failed: "アップデートの確認に失敗しました: {error}",
|
||||
rateLimited: "GitHubのアップデート確認が一時的に制限されています。リリースページで手動確認できます。",
|
||||
openRelease: "リリースページを開く",
|
||||
ignoreVersion: "このバージョンを無視",
|
||||
versionIgnored: "バージョン {version} を無視しました。次のバージョンがリリースされたときに再度通知します。",
|
||||
ignoreVersionFailed: "無視するバージョンを保存できませんでした: {error}",
|
||||
downloadAndInstall: "ダウンロード & インストール",
|
||||
activeTasksBlockUpdate: "{count} 件のタスクが実行中です。完了してから DBX を更新してください。",
|
||||
portableAutomaticUpdate: "DBX は署名済みのポータブル ZIP をダウンロードし、終了後に DBX.exe のみを置き換えて自動的に再起動します。portable.dbx とデータは保持されます。",
|
||||
|
||||
@@ -284,6 +284,9 @@ export default withEnglishFallback({
|
||||
failed: "업데이트 확인에 실패했습니다: {error}",
|
||||
rateLimited: "GitHub 업데이트 확인이 일시적으로 제한되었습니다. 릴리스 페이지를 직접 열어 수동으로 확인할 수 있습니다.",
|
||||
openRelease: "릴리스 열기",
|
||||
ignoreVersion: "이 버전 무시",
|
||||
versionIgnored: "{version} 버전을 무시했습니다. 다음 버전이 출시되면 다시 알려드립니다.",
|
||||
ignoreVersionFailed: "무시할 버전을 저장하지 못했습니다: {error}",
|
||||
downloadAndInstall: "다운로드 및 설치",
|
||||
activeTasksBlockUpdate: "{count}개의 작업이 아직 실행 중입니다. DBX를 업데이트하려면 작업이 완료될 때까지 기다려 주세요.",
|
||||
portableAutomaticUpdate: "DBX가 서명된 휴대용 ZIP을 다운로드하여 종료 후 DBX.exe만 교체하고 자동으로 다시 시작합니다. portable.dbx와 데이터는 보존됩니다.",
|
||||
|
||||
@@ -285,6 +285,9 @@ export default withEnglishFallback({
|
||||
failed: "Falha ao verificar atualizações: {error}",
|
||||
rateLimited: "As verificações de atualização do GitHub estão temporariamente limitadas. Você ainda pode abrir a página de lançamento para verificar manualmente.",
|
||||
openRelease: "Abrir Lançamento",
|
||||
ignoreVersion: "Ignorar esta versão",
|
||||
versionIgnored: "Versão {version} ignorada. Você será lembrado quando a próxima versão for lançada.",
|
||||
ignoreVersionFailed: "Não foi possível salvar a versão ignorada: {error}",
|
||||
downloadAndInstall: "Baixar e Instalar",
|
||||
activeTasksBlockUpdate: "Há {count} tarefa(s) em execução. Aguarde a conclusão antes de atualizar o DBX.",
|
||||
portableAutomaticUpdate: "O DBX baixará o ZIP portátil assinado, substituirá apenas o DBX.exe após sair e reiniciará automaticamente. O portable.dbx e os dados serão mantidos.",
|
||||
|
||||
@@ -210,6 +210,9 @@ export default withEnglishFallback({
|
||||
failed: "检查更新失败:{error}",
|
||||
rateLimited: "GitHub 更新检查暂时触发频率限制。你仍然可以打开下载页手动查看。",
|
||||
openRelease: "打开下载页",
|
||||
ignoreVersion: "忽略此版本",
|
||||
versionIgnored: "已忽略 {version},将在下个版本发布时再次提醒。",
|
||||
ignoreVersionFailed: "保存忽略版本失败:{error}",
|
||||
downloadAndInstall: "下载并安装",
|
||||
activeTasksBlockUpdate: "有 {count} 个任务正在执行,请等待任务完成后再更新 DBX。",
|
||||
portableAutomaticUpdate: "DBX 将下载已签名的便携版 ZIP,退出后仅替换 DBX.exe 并自动重启。portable.dbx 和 data 会保留。",
|
||||
|
||||
@@ -285,6 +285,9 @@ export default withEnglishFallback({
|
||||
failed: "檢查更新失敗:{error}",
|
||||
rateLimited: "GitHub 更新檢查暫時觸發頻率限制。你仍然可以開啟下載頁手動檢視。",
|
||||
openRelease: "開啟下載頁",
|
||||
ignoreVersion: "忽略此版本",
|
||||
versionIgnored: "已忽略 {version},將在下個版本發布時再次提醒。",
|
||||
ignoreVersionFailed: "儲存忽略版本失敗:{error}",
|
||||
downloadAndInstall: "下載並安裝",
|
||||
activeTasksBlockUpdate: "有 {count} 個任務正在執行,請等待任務完成後再更新 DBX。",
|
||||
portableAutomaticUpdate: "DBX 將下載已簽署的可攜版 ZIP,結束後僅替換 DBX.exe 並自動重新啟動。portable.dbx 和 data 會保留。",
|
||||
|
||||
@@ -667,6 +667,124 @@ describe("settingsStore persisted settings initialization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("settingsStore editor settings persistence", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("rolls back a failed atomic update and allows retry", async () => {
|
||||
const loadEditorSettings = vi.fn().mockResolvedValue({
|
||||
ignoredUpdateVersion: "",
|
||||
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
|
||||
});
|
||||
const saveEditorSettings = vi.fn().mockRejectedValueOnce(new Error("save failed")).mockResolvedValueOnce(undefined);
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
await expect(store.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.70" })).rejects.toThrow("save failed");
|
||||
expect(store.editorSettings.ignoredUpdateVersion).toBe("");
|
||||
|
||||
await store.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.70" });
|
||||
|
||||
expect(store.editorSettings.ignoredUpdateVersion).toBe("0.5.70");
|
||||
expect(saveEditorSettings).toHaveBeenCalledTimes(2);
|
||||
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ ignoredUpdateVersion: "0.5.70" }));
|
||||
});
|
||||
|
||||
it("loads the persisted ignored version in a new store instance", async () => {
|
||||
let persistedSettings: Record<string, unknown> = {
|
||||
ignoredUpdateVersion: "",
|
||||
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
|
||||
};
|
||||
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
|
||||
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
|
||||
persistedSettings = JSON.parse(JSON.stringify(settings));
|
||||
});
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const firstStore = useSettingsStore();
|
||||
await firstStore.initEditorSettings();
|
||||
await firstStore.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.70" });
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const restartedStore = useSettingsStore();
|
||||
await restartedStore.initEditorSettings();
|
||||
|
||||
expect(restartedStore.editorSettings.ignoredUpdateVersion).toBe("0.5.70");
|
||||
});
|
||||
|
||||
it("serializes overlapping saves so an older snapshot cannot finish last", async () => {
|
||||
let resolveFirstSave!: () => void;
|
||||
const loadEditorSettings = vi.fn().mockResolvedValue({
|
||||
ignoredUpdateVersion: "",
|
||||
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
|
||||
});
|
||||
const saveEditorSettings = vi.fn().mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveFirstSave = resolve;
|
||||
}),
|
||||
);
|
||||
saveEditorSettings.mockResolvedValueOnce(undefined);
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
const firstSave = store.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.70" });
|
||||
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledOnce());
|
||||
const secondSave = store.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.71" });
|
||||
|
||||
expect(saveEditorSettings).toHaveBeenCalledOnce();
|
||||
expect(store.editorSettings.ignoredUpdateVersion).toBe("0.5.70");
|
||||
resolveFirstSave();
|
||||
await Promise.all([firstSave, secondSave]);
|
||||
|
||||
expect(store.editorSettings.ignoredUpdateVersion).toBe("0.5.71");
|
||||
expect(saveEditorSettings).toHaveBeenCalledTimes(2);
|
||||
expect(saveEditorSettings.mock.calls[0][0]).toEqual(expect.objectContaining({ ignoredUpdateVersion: "0.5.70" }));
|
||||
expect(saveEditorSettings.mock.calls[1][0]).toEqual(expect.objectContaining({ ignoredUpdateVersion: "0.5.71" }));
|
||||
});
|
||||
|
||||
it("does not carry a failed atomic value into an already queued unrelated save", async () => {
|
||||
let rejectFirstSave!: (error: Error) => void;
|
||||
const loadEditorSettings = vi.fn().mockResolvedValue({
|
||||
ignoredUpdateVersion: "",
|
||||
theme: "system",
|
||||
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
|
||||
});
|
||||
const saveEditorSettings = vi.fn().mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
rejectFirstSave = reject;
|
||||
}),
|
||||
);
|
||||
saveEditorSettings.mockResolvedValueOnce(undefined);
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
const ignoredVersionSave = store.updateEditorSettingsAndPersist({ ignoredUpdateVersion: "0.5.70" });
|
||||
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledOnce());
|
||||
store.updateEditorSettings({ theme: "xcode-dark" });
|
||||
rejectFirstSave(new Error("save failed"));
|
||||
|
||||
await expect(ignoredVersionSave).rejects.toThrow("save failed");
|
||||
await vi.waitFor(() => expect(saveEditorSettings).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(store.editorSettings).toMatchObject({ ignoredUpdateVersion: "", theme: "xcode-dark" });
|
||||
expect(saveEditorSettings.mock.calls[1][0]).toEqual(expect.objectContaining({ ignoredUpdateVersion: "", theme: "xcode-dark" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("settingsStore sidebar connection sort persistence", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
|
||||
@@ -590,6 +590,7 @@ export interface EditorSettings {
|
||||
exportRowLimit: number;
|
||||
queryExportKeysetOptimizationEnabled: boolean;
|
||||
updateDownloadSource: UpdateDownloadSource;
|
||||
ignoredUpdateVersion: string;
|
||||
toolbarItems: ToolbarItems;
|
||||
objectBrowserShowCheckbox: boolean;
|
||||
objectBrowserViewMode: "list" | "grid";
|
||||
@@ -779,6 +780,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
exportRowLimit: 100000,
|
||||
queryExportKeysetOptimizationEnabled: true,
|
||||
updateDownloadSource: "official",
|
||||
ignoredUpdateVersion: "",
|
||||
toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS },
|
||||
objectBrowserShowCheckbox: false,
|
||||
objectBrowserViewMode: "list",
|
||||
@@ -1153,6 +1155,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit,
|
||||
queryExportKeysetOptimizationEnabled: typeof settings.queryExportKeysetOptimizationEnabled === "boolean" ? settings.queryExportKeysetOptimizationEnabled : DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled,
|
||||
updateDownloadSource: normalizeUpdateDownloadSource(settings.updateDownloadSource),
|
||||
ignoredUpdateVersion: typeof settings.ignoredUpdateVersion === "string" ? settings.ignoredUpdateVersion : DEFAULT_EDITOR_SETTINGS.ignoredUpdateVersion,
|
||||
toolbarItems: normalizeToolbarItems(settings.toolbarItems),
|
||||
objectBrowserShowCheckbox: typeof settings.objectBrowserShowCheckbox === "boolean" ? settings.objectBrowserShowCheckbox : DEFAULT_EDITOR_SETTINGS.objectBrowserShowCheckbox,
|
||||
objectBrowserViewMode: settings.objectBrowserViewMode === "grid" ? "grid" : DEFAULT_EDITOR_SETTINGS.objectBrowserViewMode,
|
||||
@@ -1197,10 +1200,6 @@ function editorSettingsPatchSnapshot(settings: Partial<EditorSettings>): Partial
|
||||
return JSON.parse(JSON.stringify(settings)) as Partial<EditorSettings>;
|
||||
}
|
||||
|
||||
function saveEditorSettings(settings: EditorSettings) {
|
||||
void api.saveEditorSettings(editorSettingsSnapshot(settings)).catch(() => {});
|
||||
}
|
||||
|
||||
export interface SettingsNavigationRequest {
|
||||
id: number;
|
||||
tab: string;
|
||||
@@ -1224,11 +1223,37 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
const isEditorSettingsLoaded = ref(false);
|
||||
let initEditorSettingsPromise: Promise<void> | null = null;
|
||||
let pendingEditorSettingsPatches: Partial<EditorSettings>[] = [];
|
||||
let editorSettingsSaveQueue: Promise<void> | null = null;
|
||||
let editorSettingsAtomicUpdateQueue: Promise<void> = Promise.resolve();
|
||||
let editorSettingsPatchRevision = 0;
|
||||
const editorSettingsFieldRevisions = new Map<keyof EditorSettings, number>();
|
||||
let pendingAiChatSelection: AiChatSelectionState | null = null;
|
||||
let aiChatSelectionSaveRunning = false;
|
||||
|
||||
const editorSettings = ref<EditorSettings>(normalizeEditorSettings({}));
|
||||
|
||||
function enqueueEditorSettingsSave(): Promise<void> {
|
||||
const saveCurrentSettings = () => api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
|
||||
const save = editorSettingsSaveQueue ? editorSettingsSaveQueue.catch(() => {}).then(saveCurrentSettings) : saveCurrentSettings();
|
||||
const trackedSave = save.finally(() => {
|
||||
if (editorSettingsSaveQueue === trackedSave) editorSettingsSaveQueue = null;
|
||||
});
|
||||
editorSettingsSaveQueue = trackedSave;
|
||||
return trackedSave;
|
||||
}
|
||||
|
||||
function saveEditorSettings() {
|
||||
void enqueueEditorSettingsSave().catch(() => {});
|
||||
}
|
||||
|
||||
function markEditorSettingsPatch(partial: Partial<EditorSettings>): number {
|
||||
const revision = ++editorSettingsPatchRevision;
|
||||
for (const key of Object.keys(partial) as (keyof EditorSettings)[]) {
|
||||
if (partial[key] !== undefined) editorSettingsFieldRevisions.set(key, revision);
|
||||
}
|
||||
return revision;
|
||||
}
|
||||
|
||||
function requestSettingsNavigation(tab: string, section?: string) {
|
||||
settingsNavigationRequest.value = {
|
||||
id: Date.now(),
|
||||
@@ -1246,7 +1271,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
pendingEditorSettingsPatches = [];
|
||||
for (const patch of pendingPatches) applyEditorSettingsPatch(patch);
|
||||
isEditorSettingsLoaded.value = true;
|
||||
if (pendingPatches.length) saveEditorSettings(editorSettings.value);
|
||||
if (pendingPatches.length) saveEditorSettings();
|
||||
}
|
||||
|
||||
async function initEditorSettings() {
|
||||
@@ -1265,7 +1290,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource;
|
||||
if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration) {
|
||||
// Persist one-time migrations so removed or unsafe defaults cannot reappear.
|
||||
await api.saveEditorSettings(normalized).catch(() => {});
|
||||
await enqueueEditorSettingsSave().catch(() => {});
|
||||
}
|
||||
completeEditorSettingsInitialization();
|
||||
return;
|
||||
@@ -1275,7 +1300,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (legacy) {
|
||||
editorSettings.value = legacy;
|
||||
try {
|
||||
await api.saveEditorSettings(legacy);
|
||||
await enqueueEditorSettingsSave();
|
||||
// Existing desktop users keep settings in localStorage; remove them only
|
||||
// after the async store has accepted the migrated value.
|
||||
clearLegacyEditorSettings();
|
||||
@@ -1666,6 +1691,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit)));
|
||||
if (partial.queryExportKeysetOptimizationEnabled !== undefined) editorSettings.value.queryExportKeysetOptimizationEnabled = partial.queryExportKeysetOptimizationEnabled;
|
||||
if (partial.updateDownloadSource !== undefined) editorSettings.value.updateDownloadSource = normalizeUpdateDownloadSource(partial.updateDownloadSource);
|
||||
if (partial.ignoredUpdateVersion !== undefined) editorSettings.value.ignoredUpdateVersion = typeof partial.ignoredUpdateVersion === "string" ? partial.ignoredUpdateVersion : "";
|
||||
if (partial.toolbarItems !== undefined) editorSettings.value.toolbarItems = normalizeToolbarItems(partial.toolbarItems);
|
||||
if (partial.objectBrowserShowCheckbox !== undefined) editorSettings.value.objectBrowserShowCheckbox = partial.objectBrowserShowCheckbox === true;
|
||||
if (partial.objectBrowserViewMode !== undefined) editorSettings.value.objectBrowserViewMode = partial.objectBrowserViewMode === "grid" ? "grid" : "list";
|
||||
@@ -1677,16 +1703,45 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
|
||||
function updateEditorSettings(partial: Partial<EditorSettings>) {
|
||||
applyEditorSettingsPatch(partial);
|
||||
markEditorSettingsPatch(partial);
|
||||
if (!isEditorSettingsLoaded.value) {
|
||||
pendingEditorSettingsPatches.push(editorSettingsPatchSnapshot(partial));
|
||||
return;
|
||||
}
|
||||
saveEditorSettings(editorSettings.value);
|
||||
saveEditorSettings();
|
||||
}
|
||||
|
||||
async function persistEditorSettings(): Promise<void> {
|
||||
await initEditorSettings();
|
||||
await api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
|
||||
await enqueueEditorSettingsSave();
|
||||
}
|
||||
|
||||
function updateEditorSettingsAndPersist(partial: Partial<EditorSettings>): Promise<void> {
|
||||
const update = editorSettingsAtomicUpdateQueue
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
await initEditorSettings();
|
||||
const previous = editorSettingsSnapshot(editorSettings.value);
|
||||
applyEditorSettingsPatch(partial);
|
||||
const revision = markEditorSettingsPatch(partial);
|
||||
try {
|
||||
await enqueueEditorSettingsSave();
|
||||
} catch (error) {
|
||||
const restored = editorSettingsSnapshot(editorSettings.value) as unknown as Record<string, unknown>;
|
||||
const previousSettings = previous as unknown as Record<string, unknown>;
|
||||
let changed = false;
|
||||
for (const key of Object.keys(partial) as (keyof EditorSettings)[]) {
|
||||
if (partial[key] === undefined || editorSettingsFieldRevisions.get(key) !== revision) continue;
|
||||
restored[key] = previousSettings[key];
|
||||
editorSettingsFieldRevisions.set(key, ++editorSettingsPatchRevision);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) editorSettings.value = normalizeEditorSettings(restored);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
editorSettingsAtomicUpdateQueue = update.catch(() => {});
|
||||
return update;
|
||||
}
|
||||
|
||||
function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) {
|
||||
@@ -1752,6 +1807,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
mcpGlobalPolicy,
|
||||
initEditorSettings,
|
||||
updateEditorSettings,
|
||||
updateEditorSettingsAndPersist,
|
||||
persistEditorSettings,
|
||||
initDesktopSettings,
|
||||
updateDesktopSettings,
|
||||
|
||||
+36
-1
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "vitest";
|
||||
|
||||
import { canDownloadAndInstallUpdate, normalizeUpdateDownloadSource, resolveUpdateReleaseUrl, tagVersion } from "../../apps/desktop/src/composables/useAppUpdater.ts";
|
||||
import { canDownloadAndInstallUpdate, isUpdateIgnored, normalizeUpdateDownloadSource, resolveUpdateReleaseUrl, tagVersion } from "../../apps/desktop/src/composables/useAppUpdater.ts";
|
||||
import { downloadAndInstallUpdateWhenIdle, installDownloadedUpdateWhenIdle } from "../../apps/desktop/src/lib/app/appUpdateInstallFlow.ts";
|
||||
import { countActiveUpdateBlockingTasks, shouldBlockAppUpdate } from "../../apps/desktop/src/lib/app/appUpdateTaskGuard.ts";
|
||||
import type { UpdateInfo } from "../../apps/desktop/src/lib/backend/api.ts";
|
||||
@@ -34,6 +34,39 @@ test("blocks in-app update installation outside desktop runtime or without an up
|
||||
assert.equal(canDownloadAndInstallUpdate(null, true), false);
|
||||
});
|
||||
|
||||
test("keeps the ignored state for the same or an older stable version", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.26" }), "0.5.26"), true);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.25" }), "0.5.26"), true);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.27" }), "0.5.26"), false);
|
||||
});
|
||||
|
||||
test("keeps the ignored state when an update source lags behind", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.69" }), "0.5.70"), true);
|
||||
});
|
||||
|
||||
test("does not ignore updates when the stored version is absent", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.26" }), ""), false);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.26" }), undefined), false);
|
||||
assert.equal(isUpdateIgnored(null, "0.5.26"), false);
|
||||
});
|
||||
|
||||
test("normalizes v-prefixed ignored versions before comparing", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "0.5.26" }), "v0.5.26"), true);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: " v0.5.26 " }), " 0.5.26 "), true);
|
||||
});
|
||||
|
||||
test("uses SemVer precedence for prereleases and ignores build metadata", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "1.0.0-beta.1" }), "1.0.0-beta.2"), true);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "1.0.0-beta.11" }), "1.0.0-beta.2"), false);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "1.0.0" }), "1.0.0-rc.1"), false);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "1.0.0+mirror.2" }), "1.0.0+github.1"), true);
|
||||
});
|
||||
|
||||
test("falls back conservatively when either version is not valid SemVer", () => {
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "release-next" }), "vrelease-next"), true);
|
||||
assert.equal(isUpdateIgnored(updateInfo({ latest_version: "release-next" }), "release-later"), false);
|
||||
});
|
||||
|
||||
test("normalizes update download source", () => {
|
||||
assert.equal(normalizeUpdateDownloadSource("official"), "official");
|
||||
assert.equal(normalizeUpdateDownloadSource("cnb"), "cnb");
|
||||
@@ -104,4 +137,6 @@ test("wires the active task guard into update installation and restart", () => {
|
||||
assert.equal(updaterSource.match(/if \(blockUpdateForActiveTasks\(\)\) return;/g)?.length, 2);
|
||||
assert.match(dialogSource, /role="alert"[\s\S]*updates\.activeTasksBlockUpdate/);
|
||||
assert.equal(dialogSource.match(/:disabled="activeTaskCount > 0"/g)?.length, 3);
|
||||
assert.match(dialogSource, /ignore-version/);
|
||||
assert.match(updaterSource, /ignoreCurrentVersion/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user