Add Windows x64 and x86 installers and refresh release screenshots

This commit is contained in:
Dario P. committed 2026-10-25 01:41:21 +02:00
1 parent f3b4205ee1
commit f7e0030d93
18 files changed
+266 -42

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
import { build, Platform, Arch } from 'electron-builder';
const requested = process.argv[2];
if (requested && !['portable', 'nsis'].includes(requested)) throw new Error('Expected portable or nsis.');
const targets = requested ? [requested] : ['portable', 'nsis'];
// Electron 43 is the last series with Windows ia32 binaries.
for (const [label, arch, electronVersion] of [['x64', Arch.x64, '44.3.0'], ['x86', Arch.ia32, '43.8.3']]) {
await build({
targets: Platform.WINDOWS.createTarget(targets, arch),
publish: 'never',
config: {
electronVersion,
directories: { output: `release/${label}` },
portable: { artifactName: `Untrace-Desktop-\${version}-portable-${label}.exe` },
nsis: { artifactName: `Untrace-Desktop-\${version}-setup-${label}.exe` },
},
});
}
+54
View File
@@ -0,0 +1,54 @@
import { _electron as electron, expect } from '@playwright/test';
import { mkdtemp, rm, mkdir } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
const data = await mkdtemp(path.join(os.tmpdir(), 'untrace-screenshots-'));
const env = { ...process.env, UNTRACE_TEST: '1', UNTRACE_DATA_DIR: data };
delete env.ELECTRON_RUN_AS_NODE;
const app = await electron.launch({ args: ['.', '--disable-features=CalculateNativeWinOcclusion', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows'], env });
await mkdir('docs/screenshots', { recursive: true });
try {
const page = await app.firstWindow();
await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; win.setContentSize(1440, 960); win.webContents.setBackgroundThrottling(false); });
await page.waitForLoadState('networkidle');
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.evaluate(() => localStorage.setItem('untrace-theme', 'graphite'));
await page.reload();
// Original generated artwork, not a private photo. Only sample pixels enter the app.
const bytes = Buffer.from(await page.evaluate(() => {
const canvas = document.createElement('canvas'); canvas.width = 1800; canvas.height = 1200;
const c = canvas.getContext('2d');
const sky = c.createLinearGradient(0, 0, 0, 750); sky.addColorStop(0, '#254e6b'); sky.addColorStop(.65, '#9bb7b2'); sky.addColorStop(1, '#f6d5aa'); c.fillStyle = sky; c.fillRect(0, 0, 1800, 1200);
c.fillStyle = '#f6e7bd'; c.beginPath(); c.arc(1300, 270, 68, 0, Math.PI * 2); c.fill();
function mountain(points, color) { c.fillStyle = color; c.beginPath(); c.moveTo(0, 760); for (const [x,y] of points) c.lineTo(x,y); c.lineTo(1800,760); c.closePath(); c.fill(); }
mountain([[0,520],[240,300],[420,460],[680,180],[950,480],[1220,260],[1500,430],[1800,220]], '#637e8b');
mountain([[0,680],[340,430],[600,680],[890,360],[1170,610],[1430,390],[1800,640]], '#345867');
c.fillStyle = '#dce5df'; c.beginPath(); c.moveTo(510,395); c.lineTo(680,180); c.lineTo(800,323); c.lineTo(710,292); c.lineTo(670,335); c.lineTo(625,307); c.fill();
const lake = c.createLinearGradient(0, 740, 0, 1200); lake.addColorStop(0,'#9bb5b0'); lake.addColorStop(1,'#274d5d'); c.fillStyle = lake; c.fillRect(0,740,1800,460);
for(let i=0;i<60;i++){ const y=770+i*7; c.fillStyle=i%3===0?'#99b0aa66':'#c9d5bd33'; c.fillRect((i*137)%1800,y,80+(i*31)%340,2); }
for(let i=0;i<42;i++){const x=i*47, h=65+(i*43)%130; c.fillStyle='#193f47'; c.beginPath(); c.moveTo(x,760-h); c.lineTo(x-24,770); c.lineTo(x+24,770); c.fill();}
c.fillStyle='#183c45'; c.beginPath(); c.moveTo(0,970); c.quadraticCurveTo(260,880,470,1200); c.lineTo(0,1200); c.fill();
return canvas.toDataURL('image/jpeg', .95).split(',')[1];
}), 'base64');
const sample = { name: 'Alpine-lake.jpg', mimeType: 'image/jpeg', buffer: bytes };
await page.getByLabel('Open image file', { exact: true }).setInputFiles(sample);
await page.getByLabel('File format', { exact: true }).selectOption('image/jpeg');
await page.getByRole('button', { name: 'Clean image', exact: true }).click();
await expect(page.getByRole('link', { name: 'Save clean image' })).toBeVisible();
await page.screenshot({ path: 'docs/screenshots/workspace-dark.png' });
await page.evaluate(() => localStorage.setItem('untrace-theme', 'light'));
await page.reload();
await page.getByLabel('Open image file', { exact: true }).setInputFiles(sample);
await expect(page.getByRole('button', { name: 'Clean image', exact: true })).toBeEnabled();
await page.screenshot({ path: 'docs/screenshots/workspace-light.png' });
await page.getByLabel('Open image file', { exact: true }).setInputFiles(['Alpine-lake.jpg','Mountain-reflections.jpg','Morning-light.jpg','Forest-shore.jpg'].map(name => ({ ...sample, name })));
await expect(page.getByRole('button', { name: 'Clean batch', exact: true })).toBeEnabled();
await page.getByRole('button', { name: 'Clean batch', exact: true }).click();
await expect(page.getByText('Cleaned', { exact: true })).toHaveCount(4);
await page.screenshot({ path: 'docs/screenshots/batch-light.png' });
console.log('Captured three real application screenshots using generated sample artwork.');
} finally {
await app.close();
await rm(data, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
}
+22
View File
@@ -0,0 +1,22 @@
import { _electron as electron, expect } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const [executablePath, architecture] = process.argv.slice(2);
if (!executablePath || !['x64', 'ia32'].includes(architecture)) throw new Error('Pass installed executable path and x64 or ia32.');
const env = { ...process.env, UNTRACE_TEST: '1' };
delete env.ELECTRON_RUN_AS_NODE;
delete env.UNTRACE_DATA_DIR;
delete env.PORTABLE_EXECUTABLE_DIR;
const app = await electron.launch({ executablePath, env });
try {
const page = await app.firstWindow();
await expect(page.getByRole('heading', { name: 'Inspector', exact: true })).toBeVisible();
const actual = await app.evaluate(({ app }) => ({ architecture: process.arch, data: app.getPath('userData'), appData: app.getPath('appData'), version: app.getVersion() }));
const { version } = JSON.parse(await readFile('package.json', 'utf8'));
if (actual.architecture !== architecture || actual.version !== version) throw new Error('Installed architecture/version mismatch.');
if (actual.data !== path.join(actual.appData, 'Untrace Desktop')) throw new Error('Installed settings path mismatch.');
if (await page.evaluate(() => typeof window.require) !== 'undefined') throw new Error('Renderer has Node access.');
if (await page.evaluate(() => fetch('https://example.com').then(() => true).catch(() => false))) throw new Error('Unexpected network access.');
console.log(`PASS: installed ${architecture} ${version} launches offline with isolated renderer and per-user settings.`);
} finally { await app.close(); }
+8 -3
View File
@@ -8,11 +8,16 @@ const data = await mkdtemp(path.join(os.tmpdir(), 'untrace-portable-'));
const env = { ...process.env, UNTRACE_TEST: '1', UNTRACE_DATA_DIR: data };
delete env.ELECTRON_RUN_AS_NODE;
const { version, build } = JSON.parse(await readFile('package.json', 'utf8'));
const artifact = build.portable.artifactName.replace('${version}', version);
const processHandle = spawn(path.resolve('release', artifact), ['--remote-debugging-port=19384'], { env, windowsHide: true, stdio: 'ignore' });
const architecture = process.argv[2] || 'x64';
if (!['x64', 'x86'].includes(architecture)) throw new Error('Expected x64 or x86.');
const artifact = `Untrace-Desktop-${version}-portable-${architecture}.exe`;
const processHandle = spawn(path.resolve('release', architecture, artifact), ['--remote-debugging-port=19384'], { env, windowsHide: true, stdio: 'ignore' });
let launchError;
processHandle.on('error', error => { launchError = error; });
let browser;
try {
for (let attempt = 0; attempt < 120; attempt++) {
if (launchError) throw launchError;
try { browser = await chromium.connectOverCDP('http://127.0.0.1:19384', { timeout: 1000 }); break; }
catch { await new Promise(resolve => setTimeout(resolve, 500)); }
}
@@ -21,7 +26,7 @@ try {
await page.waitForURL('untrace://app/');
await page.getByLabel('Open image file', { exact: true }).waitFor({ state: 'attached' });
if (await page.title() !== build.productName) throw new Error('Unexpected window');
console.log('PASS: single-file portable extracted and loaded the offline app.');
console.log(`PASS: ${architecture} single-file portable extracted and loaded the offline app.`);
const cdp = await browser.newBrowserCDPSession();
await Promise.race([cdp.send('Browser.close').catch(() => {}), new Promise(resolve => setTimeout(resolve, 1000))]);
} finally {
+35
View File
@@ -0,0 +1,35 @@
param([ValidateSet('x64','x86')][string]$Architecture)
$ErrorActionPreference='Stop'
$root=Split-Path $PSScriptRoot -Parent
$version=(Get-Content -LiteralPath (Join-Path $root 'package.json') -Raw | ConvertFrom-Json).version
$existing=@(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object DisplayName -like 'Untrace*')
if($existing.Count){throw 'An Untrace installation already exists. Run installer verification in a clean Windows account.'}
$installer=Join-Path $root "release/$Architecture/Untrace-Desktop-$version-setup-$Architecture.exe"
$target=Join-Path ([IO.Path]::GetTempPath()) ('Untrace-install-check-'+$Architecture+'-'+[guid]::NewGuid().ToString('N'))
$profile=Join-Path $env:APPDATA 'Untrace Desktop'
New-Item -ItemType Directory -Path $profile -Force | Out-Null
$sentinel=Join-Path $profile ('.install-check-'+[guid]::NewGuid().ToString('N'))
Set-Content -LiteralPath $sentinel -Value 'preserve-settings'
Push-Location $root
try {
$process=Start-Process -FilePath $installer -ArgumentList "/S /currentuser /D=$target" -PassThru -Wait -WindowStyle Hidden
if($process.ExitCode -ne 0){throw "Installer failed: $($process.ExitCode)"}
$exe=Join-Path $target 'Untrace Desktop.exe'
if(-not (Test-Path -LiteralPath $exe)){throw 'Installed executable is missing'}
$arch=if($Architecture -eq 'x86'){'ia32'}else{'x64'}
node scripts/smoke-installed.mjs $exe $arch
if($LASTEXITCODE -ne 0){throw 'Installed smoke test failed'}
$uninstaller=Get-ChildItem -LiteralPath $target -Filter 'Uninstall*.exe' | Select-Object -First 1
if(-not $uninstaller){throw 'Uninstaller is missing'}
$process=Start-Process -FilePath $uninstaller.FullName -ArgumentList '/S /currentuser' -PassThru -Wait -WindowStyle Hidden
if($process.ExitCode -ne 0){throw "Uninstaller failed: $($process.ExitCode)"}
for($attempt=0;$attempt -lt 30 -and (Test-Path -LiteralPath $exe);$attempt++){Start-Sleep -Milliseconds 500}
if(Test-Path -LiteralPath $exe){throw 'Uninstall did not remove the installed executable'}
if((Get-Content -LiteralPath $sentinel -Raw).Trim() -ne 'preserve-settings'){throw 'Uninstall removed settings'}
$left=@(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object DisplayName -like 'Untrace*')
if($left.Count){throw 'Uninstall registration remains'}
Write-Output "PASS: $Architecture install, installed startup, uninstall, and settings preservation."
} finally {
Pop-Location
if(Test-Path -LiteralPath $sentinel){Remove-Item -LiteralPath $sentinel}
}