scripts/test_desktop.mjs
Rama main · réplica de lectura del repositorio original.
// Run: node scripts/test_desktop.mjs
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import vm from 'node:vm';
const source = await readFile(new URL('../desktop/resources/js/main.js', import.meta.url), 'utf8');
function launch({ nativeRead = '', legacyRead = '', browserRead = '', failWrite = false, failBrowser = false, runtime = false, args = [] } = {}) {
const fields = Object.fromEntries(['setup-view', 'setup-form', 'server-url', 'setup-error'].map(id =>
[id, { value: '', style: {}, focus() {}, addEventListener() {} }]));
const locations = [];
const writes = [];
let finishWrite;
const context = vm.createContext({
URL, console, NL_ARGS: args, NL_PATH: '/app',
document: { getElementById: id => fields[id] },
window: { location: { replace(url) { locations.push(url); } } },
localStorage: {
getItem() { if (failBrowser) throw new Error('Storage disabled'); return browserRead; },
setItem() { if (failBrowser) throw new Error('Storage disabled'); }
},
Neutralino: { init() {}, filesystem: { async readFile(path) {
assert.equal(path, '/app/.storage/aoede_server_url.neustorage');
return legacyRead;
} }, storage: {
async getData() { if (nativeRead instanceof Error) throw nativeRead; return nativeRead; },
setData(key, url) {
writes.push({ key, url });
if (failWrite) return Promise.reject(new Error('Write denied'));
return new Promise(resolve => { finishWrite = resolve; });
}
} }
});
if (runtime) context.NL_APPID = 'com.aoede.desktop';
vm.runInContext(source, context);
return { context, fields, locations, writes, finish: () => finishWrite() };
}
const flush = async () => { for (let i = 0; i < 6; i++) await Promise.resolve(); };
const normal = launch();
await flush();
const pending = normal.context.loadServer('https://EXAMPLE.com:8443/aoede///');
assert.equal(normal.locations.length, 0); // Do not navigate while the native write is still in flight.
assert.equal(normal.writes[0].url, 'https://example.com:8443/aoede');
await normal.context.loadServer('https://other.example'); // Ignore a duplicate Connect while saving.
assert.equal(normal.writes.length, 1);
normal.finish();
await pending;
assert.deepEqual(normal.locations, ['https://example.com:8443/aoede']);
for (const url of ['javascript:alert(1)', 'file:///x', 'https://user:secret@example.com', 'https://example.com:70000', 'https://example.com?token=x']) {
const p = launch();
await flush();
await p.context.loadServer(url);
assert.equal(p.writes.length, 0);
assert.equal(p.locations.length, 0);
assert.equal(p.fields['setup-error'].hidden, false);
}
const fallback = launch({ failWrite: true });
await flush();
await fallback.context.loadServer('https://example.com');
assert.deepEqual(fallback.locations, ['https://example.com']);
const denied = launch({ nativeRead: new Error('Missing'), failWrite: true, failBrowser: true });
await flush();
await denied.context.loadServer('https://example.com');
assert.equal(denied.locations.length, 0);
assert.match(denied.fields['setup-error'].textContent, /Cannot save/);
const nativeDenied = launch({ failWrite: true, runtime: true });
await flush();
await nativeDenied.context.loadServer('https://example.com');
assert.equal(nativeDenied.locations.length, 0); // An ephemeral localhost origin cannot replace native persistence.
assert.match(nativeDenied.fields['setup-error'].textContent, /Cannot save/);
const recovery = launch({ nativeRead: 'https://offline.example', args: ['aoede', '--setup'] });
await flush();
assert.equal(recovery.locations.length, 0);
assert.equal(recovery.fields['server-url'].value, 'https://offline.example');
assert.equal(recovery.fields['setup-view'].style.display, 'flex');
const migration = launch({ nativeRead: new Error('Missing'), legacyRead: 'https://old.example' });
await flush();
assert.equal(migration.writes[0].url, 'https://old.example');
assert.equal(migration.locations.length, 0);
migration.finish();
await flush();
assert.deepEqual(migration.locations, ['https://old.example']);
console.log('PASS: desktop URL validation, awaited persistence, storage fallback and setup recovery.');