fix: gate native window reveal on renderer load
Keep BrowserWindow hidden until both ready-to-show and did-finish-load are complete, preventing the incomplete header from reaching the screen. Render a static upload-speed baseline beneath the transparent canvas so the zero line exists independently of renderer timers. Add regression coverage for both reveal event orders, load failures, and the script-independent header baseline.
This commit is contained in:
+19
-2
@@ -15,14 +15,31 @@ function createStartupQuery(config, version) {
|
|||||||
|
|
||||||
function createStartupWindow(BrowserWindow, options) {
|
function createStartupWindow(BrowserWindow, options) {
|
||||||
const window = new BrowserWindow({ ...options, show: false });
|
const window = new BrowserWindow({ ...options, show: false });
|
||||||
window.once('ready-to-show', () => {
|
let nativePaintReady = false;
|
||||||
|
let rendererLoadFinished = false;
|
||||||
|
let revealed = false;
|
||||||
|
const reveal = () => {
|
||||||
|
if (revealed || !nativePaintReady || !rendererLoadFinished) return;
|
||||||
|
revealed = true;
|
||||||
window.show();
|
window.show();
|
||||||
|
};
|
||||||
|
window.once('ready-to-show', () => {
|
||||||
|
nativePaintReady = true;
|
||||||
|
reveal();
|
||||||
|
});
|
||||||
|
window.webContents.once('did-finish-load', () => {
|
||||||
|
rendererLoadFinished = true;
|
||||||
|
reveal();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
window,
|
window,
|
||||||
load(target, onLoadError, options) {
|
load(target, onLoadError, options) {
|
||||||
return window.loadFile(target, options).catch(onLoadError);
|
return window.loadFile(target, options).catch((error) => {
|
||||||
|
rendererLoadFinished = true;
|
||||||
|
reveal();
|
||||||
|
return onLoadError(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -55,7 +55,10 @@
|
|||||||
<div class="header-spacer" aria-hidden="true"></div>
|
<div class="header-spacer" aria-hidden="true"></div>
|
||||||
<div class="header-cluster header-utilities">
|
<div class="header-cluster header-utilities">
|
||||||
<div class="upload-speed-sparkline" id="uploadSpeedSparkline" title="Aktuelle Upload-Geschwindigkeit" aria-label="Aktuelle Upload-Geschwindigkeit">
|
<div class="upload-speed-sparkline" id="uploadSpeedSparkline" title="Aktuelle Upload-Geschwindigkeit" aria-label="Aktuelle Upload-Geschwindigkeit">
|
||||||
<canvas id="uploadSpeedCanvas" width="232" height="44" aria-hidden="true"></canvas>
|
<span class="upload-speed-chart" aria-hidden="true">
|
||||||
|
<span class="upload-speed-baseline"></span>
|
||||||
|
<canvas id="uploadSpeedCanvas" width="232" height="44"></canvas>
|
||||||
|
</span>
|
||||||
<strong id="uploadSpeedValue">0 B/s</strong>
|
<strong id="uploadSpeedValue">0 B/s</strong>
|
||||||
</div>
|
</div>
|
||||||
<button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen">
|
<button class="header-update-button" id="headerUpdateBtn" title="Nach Aktualisierungen suchen" aria-label="Nach Aktualisierungen suchen" data-tooltip="Nach Aktualisierungen suchen">
|
||||||
|
|||||||
+21
-1
@@ -1960,7 +1960,27 @@ select.hs-input { max-width: none; width: auto; min-width: 140px; }
|
|||||||
transition: opacity .16s ease;
|
transition: opacity .16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-speed-chart {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-speed-baseline {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 2px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--success);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-speed-sparkline canvas {
|
.upload-speed-sparkline canvas {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -4053,7 +4073,7 @@ input[type="checkbox"] {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-speed-sparkline canvas {
|
.upload-speed-chart {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, c
|
|||||||
class TestBrowserWindow extends EventEmitter {
|
class TestBrowserWindow extends EventEmitter {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super();
|
super();
|
||||||
|
this.webContents = new EventEmitter();
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.showCalls = 0;
|
this.showCalls = 0;
|
||||||
this.startupEvents = [];
|
this.startupEvents = [];
|
||||||
@@ -27,7 +28,7 @@ class TestBrowserWindow extends EventEmitter {
|
|||||||
loadFile(target, options) {
|
loadFile(target, options) {
|
||||||
this.startupEvents.push(`load:${target}`);
|
this.startupEvents.push(`load:${target}`);
|
||||||
this.loadOptions = options;
|
this.loadOptions = options;
|
||||||
return Promise.reject(this.loadError);
|
return this.loadError ? Promise.reject(this.loadError) : Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ test('main window uses the branded application icon', () => {
|
|||||||
|
|
||||||
test('startup load registers visibility before navigation and shows only once', async () => {
|
test('startup load registers visibility before navigation and shows only once', async () => {
|
||||||
const startup = createStartupWindow(TestBrowserWindow, {});
|
const startup = createStartupWindow(TestBrowserWindow, {});
|
||||||
|
startup.window.loadError = null;
|
||||||
const loading = startup.load('renderer/index.html', () => {});
|
const loading = startup.load('renderer/index.html', () => {});
|
||||||
|
|
||||||
assert.deepEqual(startup.window.startupEvents, [
|
assert.deepEqual(startup.window.startupEvents, [
|
||||||
@@ -86,12 +88,24 @@ test('startup load registers visibility before navigation and shows only once',
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
startup.window.emit('ready-to-show');
|
startup.window.emit('ready-to-show');
|
||||||
|
assert.equal(startup.window.showCalls, 0);
|
||||||
|
startup.window.webContents.emit('did-finish-load');
|
||||||
startup.window.emit('ready-to-show');
|
startup.window.emit('ready-to-show');
|
||||||
|
startup.window.webContents.emit('did-finish-load');
|
||||||
await loading;
|
await loading;
|
||||||
|
|
||||||
assert.equal(startup.window.showCalls, 1);
|
assert.equal(startup.window.showCalls, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('startup waits for native paint readiness when renderer loading finishes first', () => {
|
||||||
|
const startup = createStartupWindow(TestBrowserWindow, {});
|
||||||
|
|
||||||
|
startup.window.webContents.emit('did-finish-load');
|
||||||
|
assert.equal(startup.window.showCalls, 0);
|
||||||
|
startup.window.emit('ready-to-show');
|
||||||
|
assert.equal(startup.window.showCalls, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('startup load forwards a rejected navigation to the error handler', async () => {
|
test('startup load forwards a rejected navigation to the error handler', async () => {
|
||||||
const startup = createStartupWindow(TestBrowserWindow, {});
|
const startup = createStartupWindow(TestBrowserWindow, {});
|
||||||
let handledError;
|
let handledError;
|
||||||
@@ -150,4 +164,6 @@ test('header occupies its final geometry before asynchronous initialization', ()
|
|||||||
assert.ok(firstFrameInitialization < asynchronousInitialization);
|
assert.ok(firstFrameInitialization < asynchronousInitialization);
|
||||||
assert.match(mainSource, /createStartupQuery\([^,]+,\s*app\.getVersion\(\)\)/u);
|
assert.match(mainSource, /createStartupQuery\([^,]+,\s*app\.getVersion\(\)\)/u);
|
||||||
assert.doesNotMatch(mainSource, /runAutomaticUpdateCheck\(true\);\s*\},\s*3000\)/u);
|
assert.doesNotMatch(mainSource, /runAutomaticUpdateCheck\(true\);\s*\},\s*3000\)/u);
|
||||||
|
assert.match(html, /class="upload-speed-baseline"/u);
|
||||||
|
assert.match(css, /\.upload-speed-baseline\s*\{[^}]*background:\s*var\(--success\);/su);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user