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) {
|
||||
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.once('ready-to-show', () => {
|
||||
nativePaintReady = true;
|
||||
reveal();
|
||||
});
|
||||
window.webContents.once('did-finish-load', () => {
|
||||
rendererLoadFinished = true;
|
||||
reveal();
|
||||
});
|
||||
|
||||
return {
|
||||
window,
|
||||
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-cluster header-utilities">
|
||||
<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>
|
||||
</div>
|
||||
<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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
display: block;
|
||||
@@ -4053,7 +4073,7 @@ input[type="checkbox"] {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.upload-speed-sparkline canvas {
|
||||
.upload-speed-chart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, c
|
||||
class TestBrowserWindow extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.webContents = new EventEmitter();
|
||||
this.options = options;
|
||||
this.showCalls = 0;
|
||||
this.startupEvents = [];
|
||||
@@ -27,7 +28,7 @@ class TestBrowserWindow extends EventEmitter {
|
||||
loadFile(target, options) {
|
||||
this.startupEvents.push(`load:${target}`);
|
||||
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 () => {
|
||||
const startup = createStartupWindow(TestBrowserWindow, {});
|
||||
startup.window.loadError = null;
|
||||
const loading = startup.load('renderer/index.html', () => {});
|
||||
|
||||
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');
|
||||
assert.equal(startup.window.showCalls, 0);
|
||||
startup.window.webContents.emit('did-finish-load');
|
||||
startup.window.emit('ready-to-show');
|
||||
startup.window.webContents.emit('did-finish-load');
|
||||
await loading;
|
||||
|
||||
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 () => {
|
||||
const startup = createStartupWindow(TestBrowserWindow, {});
|
||||
let handledError;
|
||||
@@ -150,4 +164,6 @@ test('header occupies its final geometry before asynchronous initialization', ()
|
||||
assert.ok(firstFrameInitialization < asynchronousInitialization);
|
||||
assert.match(mainSource, /createStartupQuery\([^,]+,\s*app\.getVersion\(\)\)/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