// Run with the pinned Electron runtime against a finished app's resources folder. const { app, BrowserWindow, session } = require('electron'); const fs = require('node:fs'); const path = require('node:path'); const { createRequire } = require('node:module'); const assert = require('node:assert/strict'); const { once } = require('node:events'); const http = require('node:http'); const resources = path.resolve(process.argv[2]); const archive = path.join(resources, 'app.asar'); const isolatedData = process.env.SUBMINER_PACKAGE_SMOKE_DATA; assert( isolatedData && fs.existsSync(isolatedData), 'Use bun run test:package to create an isolated profile', ); app.setPath('userData', isolatedData); app.disableHardwareAcceleration(); app.on('window-all-closed', () => {}); const timeout = setTimeout(() => { console.error('Package smoke timed out'); app.exit(1); }, 60_000); async function smoke() { await app.whenReady(); const packagedRequire = createRequire(path.join(archive, 'package.json')); const Database = packagedRequire('libsql'); const database = new Database(':memory:'); assert.equal(database.prepare('select 42 as answer').get().answer, 42); database.close(); if (process.platform === 'win32') { const win32 = packagedRequire('./dist/window-trackers/win32.js'); assert(Array.isArray(win32.findMpvWindows().matches)); } const { Texthooker } = packagedRequire('./dist/core/services/texthooker.js'); const texthooker = new Texthooker(); const server = texthooker.start(0); assert(server, 'Packaged texthooker assets could not be found'); try { await once(server, 'listening'); const response = await fetch(`http://127.0.0.1:${server.address().port}/`); assert.equal(response.status, 200); assert((await response.text()).includes(' { if (details.error !== 'net::ERR_ABORTED') failedRequests.push(`${details.url}: ${details.error}`); }); for (const ui of ['renderer', 'settings', 'syncui']) { const win = new BrowserWindow({ show: false, webPreferences: { sandbox: false, preload: path.join(archive, 'dist', ui === 'renderer' ? 'preload.js' : `preload-${ui}.js`), }, }); try { await win.loadFile(path.join(archive, `dist/${ui}/index.html`)); const loaded = await win.webContents.executeJavaScript( `document.fonts.load('400 16px "M PLUS 1"', '日本語').then(fonts => fonts.length > 0 && fonts.every(font => font.status === 'loaded'))`, ); assert(loaded, `${ui}: shared Japanese font failed to load`); } finally { win.destroy(); } } // The stats dashboard uses HTTP for both assets and API requests in the app. const { ImmersionTrackerService } = packagedRequire( './dist/core/services/immersion-tracker-service.js', ); const { createStatsApp, startNodeHttpServer } = packagedRequire( './dist/core/services/stats-server.js', ); const tracker = new ImmersionTrackerService({ dbPath: path.join(isolatedData, 'stats.db') }); const statsConfig = { port: 0, staticDir: path.join(archive, 'stats/dist'), tracker }; let statsHttp; const statsServer = await startNodeHttpServer( createStatsApp(tracker, statsConfig), statsConfig, (listener) => (statsHttp = http.createServer(listener)), ); const statsWindow = new BrowserWindow({ show: false }); try { const url = `http://127.0.0.1:${statsHttp.address().port}`; session.defaultSession.webRequest.onCompleted({ urls: [`${url}/*`] }, (details) => { if (details.statusCode >= 400) failedRequests.push(`${details.url}: ${details.statusCode}`); }); await statsWindow.loadURL(url); assert.equal((await fetch(`${url}/api/stats/overview`)).status, 200); } finally { statsWindow.destroy(); await statsServer.close(); tracker.destroy(); } assert.deepEqual(failedRequests, [], 'Packaged UI resources failed to load'); console.log( 'Package smoke passed: SQLite, platform FFI, texthooker, both dictionary extensions, UI pages, stats HTTP, shared Japanese font.', ); } smoke() .then(() => { clearTimeout(timeout); app.exit(0); }) .catch((error) => { console.error(error); clearTimeout(timeout); app.exit(1); });