Add Windows x64 and x86 installers and refresh release screenshots
This commit is contained in:
1 parent
f3b4205ee1
commit
f7e0030d93
18 files changed
+266
-42
No files matched your search
@@ -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` },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
Reference in new issue
Block a user