fix(release): harden updater status and cutter layout
Windows CI / verify (push) Failing after 3m37s
Windows CI / verify (push) Failing after 3m37s
Ignore malformed update notifications that do not carry a version, preventing unusable release prompts in installed builds. Keep the loaded cutter workspace free of the source selector and give cutter export selects an explicit non-repeating indicator after workspace styling. Add regression coverage, bump the patch release to 1.0.10, and update public installation documentation.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
const styles = readFileSync(join(__dirname, 'styles.css'), 'utf8');
|
||||
const workspaceStyles = readFileSync(join(__dirname, 'workspace.css'), 'utf8');
|
||||
|
||||
describe('cutter workspace style production paths', () => {
|
||||
test('hides the source bar when the non-adjacent workspace is shown', () => {
|
||||
expect(styles).toContain('.cutter-source-bar:has(~ .cutter-workspace.shown)');
|
||||
});
|
||||
|
||||
test('keeps the export profile indicator from tiling after workspace background styling', () => {
|
||||
const selector = '#cutterTab .cutter-export-options select {';
|
||||
const start = workspaceStyles.indexOf(selector);
|
||||
const end = workspaceStyles.indexOf('}', start);
|
||||
expect(workspaceStyles.slice(start, end)).toMatch(/background-repeat:\s*no-repeat/);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -940,7 +940,7 @@
|
||||
|
||||
<div class="settings-card" data-settings-pane="updates" hidden>
|
||||
<h3 id="updateTitle">Updates</h3>
|
||||
<p id="versionInfo" class="card-intro">Version: v1.0.9</p>
|
||||
<p id="versionInfo" class="card-intro">Version: v1.0.10</p>
|
||||
<button type="button" class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
function sourceFragment(start: string, end: string): string {
|
||||
const source = readFileSync(join(__dirname, 'renderer-updates.ts'), 'utf8');
|
||||
const from = source.indexOf(start);
|
||||
const to = source.indexOf(end, from);
|
||||
if (from < 0 || to < 0) throw new Error('Missing renderer updates production fragment');
|
||||
return source.slice(from, to);
|
||||
}
|
||||
|
||||
function evaluate(source: string, context: Record<string, unknown>): { rememberUpdateInfo: (info?: { version?: string } | null) => unknown } {
|
||||
context.globalThis = context;
|
||||
const compiled = transpileModule(`${source}\nObject.assign(globalThis, { __updatesProductionPath: { rememberUpdateInfo } });`, {
|
||||
compilerOptions: { module: ModuleKind.None, target: ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
runInNewContext(compiled, context);
|
||||
return (context as { __updatesProductionPath: { rememberUpdateInfo: (info?: { version?: string } | null) => unknown } }).__updatesProductionPath;
|
||||
}
|
||||
|
||||
describe('renderer update production paths', () => {
|
||||
test('does not create an update state without a version', () => {
|
||||
const api = evaluate(sourceFragment('function rememberUpdateInfo', 'function getActiveUpdateInfo'), {
|
||||
latestUpdateVersion: '',
|
||||
latestUpdateInfo: null,
|
||||
});
|
||||
|
||||
expect(api.rememberUpdateInfo({})).toBeNull();
|
||||
});
|
||||
});
|
||||
+23
-8
@@ -33,8 +33,9 @@ function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
||||
}
|
||||
}
|
||||
|
||||
function rememberUpdateInfo(info?: UpdateInfo | null): UpdateInfo {
|
||||
const version = info?.version || latestUpdateVersion || latestUpdateInfo?.version || '?';
|
||||
function rememberUpdateInfo(info?: UpdateInfo | null): UpdateInfo | null {
|
||||
const version = (info?.version || latestUpdateVersion || latestUpdateInfo?.version || '').trim();
|
||||
if (!version) return null;
|
||||
latestUpdateVersion = version;
|
||||
latestUpdateInfo = {
|
||||
...(latestUpdateInfo || { version }),
|
||||
@@ -44,7 +45,7 @@ function rememberUpdateInfo(info?: UpdateInfo | null): UpdateInfo {
|
||||
return latestUpdateInfo;
|
||||
}
|
||||
|
||||
function getActiveUpdateInfo(): UpdateInfo {
|
||||
function getActiveUpdateInfo(): UpdateInfo | null {
|
||||
return rememberUpdateInfo();
|
||||
}
|
||||
|
||||
@@ -182,7 +183,8 @@ function handleWorkspaceUpdateAction(): void {
|
||||
}
|
||||
|
||||
if (updateBannerState === 'available' || updateBannerState === 'ready') {
|
||||
openUpdateModal(getActiveUpdateInfo());
|
||||
const info = getActiveUpdateInfo();
|
||||
if (info) openUpdateModal(info);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,6 +193,7 @@ function handleWorkspaceUpdateAction(): void {
|
||||
|
||||
function setUpdateBannerAvailableUi(info: UpdateInfo, reveal = true): void {
|
||||
const activeInfo = rememberUpdateInfo(info);
|
||||
if (!activeInfo) return;
|
||||
updateReady = false;
|
||||
updateDownloadInProgress = false;
|
||||
latestDownloadProgress = null;
|
||||
@@ -240,6 +243,7 @@ function setDownloadPendingUi(): void {
|
||||
|
||||
function setDownloadReadyUi(info?: UpdateInfo): void {
|
||||
const activeInfo = rememberUpdateInfo(info);
|
||||
if (!activeInfo) return;
|
||||
updateReady = true;
|
||||
updateDownloadInProgress = false;
|
||||
updateBannerState = 'ready';
|
||||
@@ -390,6 +394,7 @@ function refreshUpdateChangelogToggleText(): void {
|
||||
|
||||
function refreshUpdateModalTexts(): void {
|
||||
const info = getActiveUpdateInfo();
|
||||
if (!info) return;
|
||||
const isReady = updateReady;
|
||||
|
||||
byId('updateModalTitle').textContent = isReady
|
||||
@@ -421,7 +426,7 @@ function refreshUpdateModalTexts(): void {
|
||||
}
|
||||
|
||||
function openUpdateModal(info?: UpdateInfo): void {
|
||||
rememberUpdateInfo(info);
|
||||
if (!rememberUpdateInfo(info)) return;
|
||||
updateChangelogExpanded = false;
|
||||
RendererAccessibility.openDialog('updateModal', { onEscape: dismissUpdateModal });
|
||||
refreshUpdateModalTexts();
|
||||
@@ -546,7 +551,8 @@ async function checkUpdate(): Promise<void> {
|
||||
updateCheckInProgress = false;
|
||||
setCheckButtonCheckingState(false);
|
||||
if (latestUpdateInfo || updateReady) {
|
||||
openUpdateModal(getActiveUpdateInfo());
|
||||
const info = getActiveUpdateInfo();
|
||||
if (info) openUpdateModal(info);
|
||||
} else {
|
||||
notifyUpdate(UI_TEXT.updates.readyToInstall, 'info');
|
||||
}
|
||||
@@ -611,8 +617,11 @@ function downloadUpdate(): void {
|
||||
}
|
||||
|
||||
if (result?.skipped === 'ready-to-install') {
|
||||
setDownloadReadyUi(getActiveUpdateInfo());
|
||||
openUpdateModal(getActiveUpdateInfo());
|
||||
const info = getActiveUpdateInfo();
|
||||
if (info) {
|
||||
setDownloadReadyUi(info);
|
||||
openUpdateModal(info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -646,6 +655,11 @@ window.api.onUpdateAvailable((info: UpdateInfo) => {
|
||||
latestDownloadProgress = null;
|
||||
setCheckButtonCheckingState(false);
|
||||
|
||||
if (!activeInfo) {
|
||||
shouldOpenUpdateModalOnAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user explicitly skipped this exact version, suppress the auto
|
||||
// notification entirely — banner stays hidden, no modal popup. A manual
|
||||
// "Check for updates" click overrides the skip so the user can change
|
||||
@@ -707,6 +721,7 @@ window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||
// by a stale entry.
|
||||
clearSkippedUpdateVersion();
|
||||
const activeInfo = rememberUpdateInfo(info);
|
||||
if (!activeInfo) return;
|
||||
setDownloadReadyUi(activeInfo);
|
||||
openUpdateModal(activeInfo);
|
||||
});
|
||||
|
||||
+1
-1
@@ -3091,7 +3091,7 @@ input[type="checkbox"].vod-select-checkbox {
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
#cutterTab .cutter-source-bar:has(+ .cutter-workspace.shown) {
|
||||
#cutterTab .cutter-source-bar:has(~ .cutter-workspace.shown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1420,6 +1420,21 @@ textarea:hover:not(:disabled) {
|
||||
border-color: var(--workspace-border-strong);
|
||||
}
|
||||
|
||||
#cutterTab .cutter-export-options select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
min-width: 0;
|
||||
padding-right: 28px;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23999'%3E%3Cpath d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 14px;
|
||||
}
|
||||
|
||||
#cutterTab .cutter-export-options select:hover:not(:disabled) {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%239146FF'%3E%3Cpath d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]):focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
|
||||
Reference in New Issue
Block a user