Right-align the speed before the separator and left-align ETA after it so the second metric divider sits exactly between the visible values.
188 lines
9.1 KiB
JavaScript
188 lines
9.1 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { EventEmitter } = require('node:events');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer');
|
|
|
|
class TestBrowserWindow extends EventEmitter {
|
|
constructor(options) {
|
|
super();
|
|
this.webContents = new EventEmitter();
|
|
this.options = options;
|
|
this.showCalls = 0;
|
|
this.startupEvents = [];
|
|
this.loadError = new Error('renderer load failed');
|
|
this.loadOptions = null;
|
|
}
|
|
|
|
once(eventName, listener) {
|
|
this.startupEvents.push(`listen:${eventName}`);
|
|
return super.once(eventName, listener);
|
|
}
|
|
|
|
show() {
|
|
this.showCalls++;
|
|
}
|
|
|
|
loadFile(target, options) {
|
|
this.startupEvents.push(`load:${target}`);
|
|
this.loadOptions = options;
|
|
return this.loadError ? Promise.reject(this.loadError) : Promise.resolve();
|
|
}
|
|
}
|
|
|
|
test('configureStartupRenderer leaves hardware acceleration enabled for a local Windows session', () => {
|
|
let calls = 0;
|
|
configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'Console' }, 'win32');
|
|
assert.equal(calls, 0);
|
|
});
|
|
|
|
test('configureStartupRenderer disables hardware acceleration for a Windows Remote Desktop session', () => {
|
|
let calls = 0;
|
|
configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'RDP-Tcp#12' }, 'win32');
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test('resolveStartupLanguage accepts only the supported persisted language', () => {
|
|
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'de' } }), 'de');
|
|
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'en' } }), 'en');
|
|
assert.equal(resolveStartupLanguage({ globalSettings: { language: 'fr' } }), 'en');
|
|
assert.equal(resolveStartupLanguage(null), 'en');
|
|
});
|
|
|
|
test('startup query carries language and installed version into the first renderer frame', () => {
|
|
assert.deepEqual(createStartupQuery({ globalSettings: { language: 'de' } }, '2.1.25'), {
|
|
language: 'de',
|
|
version: '2.1.25'
|
|
});
|
|
assert.deepEqual(createStartupQuery(null, 'invalid'), { language: 'en', version: '' });
|
|
});
|
|
|
|
test('createStartupWindow forces the main window to start hidden', () => {
|
|
const startup = createStartupWindow(TestBrowserWindow, { width: 1100, show: true });
|
|
|
|
assert.equal(startup.window.options.width, 1100);
|
|
assert.equal(startup.window.options.show, false);
|
|
});
|
|
|
|
test('main window uses the branded application icon', () => {
|
|
const projectRoot = path.join(__dirname, '..');
|
|
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
|
const createWindowStart = mainSource.indexOf('function createWindow()');
|
|
const createWindowEnd = mainSource.indexOf('\nfunction createTray()', createWindowStart);
|
|
const createWindowSource = mainSource.slice(createWindowStart, createWindowEnd);
|
|
|
|
assert.equal(fs.existsSync(path.join(projectRoot, 'assets', 'app_icon.ico')), true);
|
|
assert.match(createWindowSource, /icon:\s*path\.join\(__dirname, ['"]assets['"], ['"]app_icon\.ico['"]\)/u);
|
|
});
|
|
|
|
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, [
|
|
'listen:ready-to-show',
|
|
'load:renderer/index.html'
|
|
]);
|
|
|
|
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;
|
|
|
|
await startup.load('renderer/index.html', (err) => {
|
|
handledError = err;
|
|
});
|
|
|
|
assert.equal(handledError, startup.window.loadError);
|
|
});
|
|
|
|
test('startup load forwards navigation options before the renderer becomes visible', async () => {
|
|
const startup = createStartupWindow(TestBrowserWindow, {});
|
|
const options = { query: { language: 'de' } };
|
|
|
|
await startup.load('renderer/index.html', () => {}, options);
|
|
|
|
assert.deepEqual(startup.window.loadOptions, options);
|
|
});
|
|
|
|
test('desktop drag and drop is accepted before asynchronous renderer initialization', () => {
|
|
const projectRoot = path.join(__dirname, '..');
|
|
const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8');
|
|
const earlyBinding = appSource.lastIndexOf('\nsetupDragDrop();');
|
|
const initialization = appSource.lastIndexOf('\ninit().then(');
|
|
|
|
assert.notEqual(earlyBinding, -1);
|
|
assert.notEqual(initialization, -1);
|
|
assert.ok(earlyBinding < initialization);
|
|
assert.match(appSource, /dataTransfer\.dropEffect\s*=\s*['"]copy['"]/u);
|
|
});
|
|
|
|
test('upload sidebar renders and updates the remaining upload size', () => {
|
|
const projectRoot = path.join(__dirname, '..');
|
|
const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8');
|
|
const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8');
|
|
|
|
assert.match(html, /Verbleibende Größe[\s\S]*id="uploadTelemetryRemainingSize"[^>]*>0 B</u);
|
|
assert.match(appSource, /_setUploadTelemetryText\(['"]uploadTelemetryRemainingSize['"],\s*formatBytes\(stats\.bytesRemaining\)\)/u);
|
|
});
|
|
|
|
test('header occupies its final geometry before asynchronous initialization', () => {
|
|
const projectRoot = path.join(__dirname, '..');
|
|
const html = fs.readFileSync(path.join(projectRoot, 'renderer', 'index.html'), 'utf8');
|
|
const appSource = fs.readFileSync(path.join(projectRoot, 'renderer', 'app.js'), 'utf8');
|
|
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
|
const css = fs.readFileSync(path.join(projectRoot, 'renderer', 'styles.css'), 'utf8');
|
|
const updateButton = html.match(/<button class="header-update-button"[^>]*id="headerUpdateBtn"[^>]*>/u)?.[0] || '';
|
|
const updateSlotIndex = html.indexOf('class="header-update-slot"');
|
|
const speedWidgetIndex = html.indexOf('id="uploadSpeedSparkline"');
|
|
const firstFrameInitialization = appSource.lastIndexOf('\ninitializeStaticHeader();');
|
|
const asynchronousInitialization = appSource.lastIndexOf('\ninit().then(');
|
|
|
|
assert.doesNotMatch(updateButton, /\shidden(?:\s|>)/u);
|
|
assert.doesNotMatch(updateButton, /\stitle=/u);
|
|
assert.ok(updateSlotIndex >= 0 && updateSlotIndex < speedWidgetIndex);
|
|
assert.match(css, /\.header-update-slot\s*\{[^}]*width:\s*0;[^}]*flex:\s*0 0 0;[^}]*overflow:\s*hidden;/su);
|
|
assert.match(css, /\.header-update-slot\.is-visible\s*\{[^}]*width:\s*146px;[^}]*flex-basis:\s*146px;/su);
|
|
assert.match(css, /\.header-update-slot\.is-visible\s+\.header-update-button\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translateX\(0\);/su);
|
|
assert.match(css, /\.version-badge\s*\{[^}]*min-width:\s*48px;/su);
|
|
assert.notEqual(firstFrameInitialization, -1);
|
|
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);
|
|
assert.match(css, /\.header-update-button\.update-available\s*\{[^}]*background:\s*var\(--success\);[^}]*color:\s*#000;/su);
|
|
assert.match(css, /\.header-update-button\.update-available:hover\s*\{[^}]*background:\s*var\(--success-end\);[^}]*color:\s*#000;/su);
|
|
assert.match(css, /\.update-dialog\s*\{[^}]*width:\s*min\(576px,\s*100%\);/su);
|
|
assert.match(css, /\.update-release-notes\s*\{[^}]*height:\s*min\(264px,\s*48vh\);/su);
|
|
assert.match(html, /class="update-progress-footer"[\s\S]*id="updateProgressDetails"[\s\S]*id="updateProgressSize"[\s\S]*id="updateProgressSpeed"[\s\S]*id="updateProgressEta"[\s\S]*id="updateProgressText"/u);
|
|
assert.match(css, /#updateProgressDetails\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*19ch 2ch 11ch 2ch 10ch;[^}]*column-gap:\s*0;[^}]*font-variant-numeric:\s*tabular-nums;/su);
|
|
assert.match(css, /\.update-progress-separator\s*\{[^}]*display:\s*grid;[^}]*place-items:\s*center;/su);
|
|
assert.match(css, /#updateProgressSpeed\s*\{[^}]*text-align:\s*right;/su);
|
|
assert.match(css, /#updateProgressEta\s*\{[^}]*text-align:\s*left;/su);
|
|
assert.match(css, /\.update-release-heading\s*\{[^}]*color:\s*var\(--success\);[^}]*font-size:\s*14px;[^}]*font-weight:\s*750;/su);
|
|
assert.match(css, /\.update-release-category\s*\{[^}]*font-size:\s*13px;[^}]*font-weight:\s*700;/su);
|
|
assert.match(css, /#updateProgressDetails\[hidden\]\s*\{[^}]*display:\s*none;/su);
|
|
});
|