Files
Haven/test/serverListSync.test.js

574 lines
27 KiB
JavaScript

'use strict';
// The server list in Haven Desktop: every server page keeps its own copy in
// its own browser storage, and the Desktop app keeps the shared one. These
// tests load servers.js once per server page, with its own storage, against
// one fake Desktop app, and check the pages agree.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
// Values from the page's context are compared as plain data.
const plain = v => JSON.parse(JSON.stringify(v));
const urls = list => plain(list.map(s => s.url));
const SOURCE = fs.readFileSync(path.join(__dirname, '..', 'public/js/servers.js'), 'utf8');
function createStorage(values = {}) {
const data = new Map(Object.entries(values));
return {
data,
getItem: key => (data.has(key) ? data.get(key) : null),
setItem: (key, value) => data.set(key, String(value)),
removeItem: key => data.delete(key),
};
}
function norm(url) {
const u = new URL(/^https?:\/\//.test(url) ? url : 'https://' + url);
const p = u.pathname.replace(/\/+$/, '').replace(/\/app(?:\.html)?$/i, '');
return p ? u.origin + p : u.origin;
}
// A stand-in for the Desktop app's shared list with the same rules as
// Haven Desktop's src/main/server-list.js.
function fakeDesktopStore() {
const s = { history: [], removed: [], order: [] };
const orderedUrls = () => {
const known = new Set(s.history.map(h => h.url));
const out = s.order.filter(u => known.has(u));
for (const h of s.history) if (!out.includes(h.url)) out.push(h.url);
return out;
};
const view = () => {
const byUrl = new Map(s.history.map(h => [h.url, h]));
const order = orderedUrls();
return JSON.parse(JSON.stringify({ servers: order.map(u => byUrl.get(u)), removed: s.removed, order, hasOrder: s.order.length > 0 }));
};
return {
state: s,
view,
add(url, name, opts) {
url = norm(url);
if (s.removed.includes(url)) {
if (!(opts && opts.userInitiated)) return 'refused';
s.removed = s.removed.filter(u => u !== url);
}
if (s.history.some(h => h.url === url)) return 'exists';
s.history.push({ url, name: name || url, lastConnected: 0 });
return 'added';
},
remove(url) {
url = norm(url);
s.history = s.history.filter(h => h.url !== url);
s.order = s.order.filter(u => u !== url);
if (!s.removed.includes(url)) s.removed.push(url);
},
rename(url, name, opts = {}) {
const e = s.history.find(h => h.url === norm(url));
if (!e || !name) return false;
if (typeof opts.custom !== 'boolean') {
if (e.customName) return false;
e.name = name;
return true;
}
e.name = name;
if (opts.custom) e.customName = true; else delete e.customName;
if ('icon' in opts) {
if (opts.icon) { e.icon = opts.icon; e.customIcon = true; } else { delete e.icon; delete e.customIcon; }
}
e.editedAt = opts.editedAt || Date.now();
return true;
},
setOrder(urls) {
// Only servers already listed are placed: an order cannot add one.
const want = [...new Set(urls.map(norm))].filter(u => s.history.some(h => h.url === u));
const base = [...new Set([...s.order, ...orderedUrls()])];
const inBase = want.filter(u => base.includes(u));
let i = 0;
s.order = base.map(u => (inBase.includes(u) ? inBase[i++] : u));
},
};
}
// The window.havenDesktop a page sees: new Desktop (shared list) or an old
// one that only has the plain history.
function desktopApi(store, { old = false } = {}) {
const api = {
calls: [],
initialServerHistory: store.view().servers,
getServerHistory: async () => store.view().servers,
addServerHistory: async (url, name, opts) => { api.calls.push(['add', url]); return old ? undefined : store.add(url, name, opts); },
removeServerHistory: async (url) => { api.calls.push(['remove', url]); store.remove(url); return store.view().servers; },
};
if (old) {
// An old Desktop forgets removals and drops the third argument.
api.addServerHistory = async (url, name) => { api.calls.push(['add', url]); store.state.removed = []; store.add(url, name); };
return api;
}
api.initialServerList = store.view();
api.getServerList = async () => store.view();
api.setServerOrder = async (urls) => { store.setOrder(urls); return true; };
api.updateServerName = async (url, name, opts) => store.rename(url, name, opts);
return api;
}
// Health answers per server address: { name, fingerprint } or null (down).
function fakeFetch(origin, health) {
return async (url) => {
const full = url.startsWith('/') ? origin + url : url;
if (full.endsWith('/api/health')) {
const base = full.slice(0, -'/api/health'.length);
// The page's own server always answers.
const info = health[base] || (base === origin ? { name: 'Self', fingerprint: 'self:' + origin } : null);
if (!info) throw new Error('offline');
return { ok: true, json: async () => ({ status: 'online', name: info.name, icon: info.icon || null, fingerprint: info.fingerprint }) };
}
throw new Error('unexpected fetch ' + full);
};
}
/** Open a server page: servers.js in its own context with its own storage. */
function openPage(origin, { storage = createStorage(), desktop = null, health = {} } = {}) {
const warnings = [];
const window = desktop ? { havenDesktop: desktop } : {};
const context = vm.createContext({
window,
localStorage: storage,
location: { origin },
fetch: fakeFetch(origin, health),
console: { warn: (...a) => warnings.push(a.join(' ')), log() {}, info() {}, error: (...a) => warnings.push(a.join(' ')) },
AbortController,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
URL,
Promise,
});
vm.runInContext(`${SOURCE}\nglobalThis.ServerManager = ServerManager;`, context, { filename: 'servers.js' });
const manager = new context.ServerManager();
return { manager, storage, warnings };
}
const A = 'https://a.example.com';
const B = 'https://b.example.com';
const X = 'https://x.example.com';
const Y = 'https://y.example.com';
const Z = 'https://z.example.com';
function seeded(urls) {
const store = fakeDesktopStore();
for (const u of urls) store.add(u, u);
return store;
}
function storageWith(urls) {
return createStorage({ haven_servers: JSON.stringify(urls.map(u => ({ name: u, url: u, addedAt: 1 }))) });
}
test('a server removed on one server stays removed on another', async () => {
const store = seeded([A, B, X, Y]);
// Server B's own storage still has X from an earlier sync.
const bStorage = storageWith([A, X, Y]);
const a = openPage(A, { desktop: desktopApi(store), storage: storageWith([B, X, Y]) });
await a.manager.reconcileWithDesktop();
a.manager.remove(X);
await new Promise(r => setImmediate(r));
assert.deepEqual(store.state.removed, [X]);
const b = openPage(B, { desktop: desktopApi(store), storage: bStorage });
assert.ok(!b.manager.servers.some(s => s.url === X), 'gone as soon as the page loads');
const stats = await b.manager.reconcileWithDesktop();
assert.equal(stats.added, 0);
assert.ok(!b.manager.servers.some(s => s.url === X));
assert.ok(!store.state.history.some(h => h.url === X), 'B does not hand it back to the app');
assert.ok(!b.manager.add('X', X), 'a sync cannot add it back either');
// Adding it on purpose brings it back for everyone.
assert.ok(b.manager.add('X', X, null, { userInitiated: true }));
assert.equal(store.add(X, 'X', { userInitiated: true }), 'added');
const c = openPage(A, { desktop: desktopApi(store), storage: a.storage });
await c.manager.reconcileWithDesktop();
assert.ok(c.manager.servers.some(s => s.url === X));
assert.deepEqual(a.warnings, []);
assert.deepEqual(b.warnings, []);
});
test('removals a server made before the shared list are handed over once', async () => {
const store = seeded([A, B, X]);
const storage = storageWith([B, X]);
storage.setItem('haven_servers_removed', JSON.stringify([Y]));
// On A, X was removed earlier but came back into the app's history.
storage.setItem('haven_servers', JSON.stringify([{ name: 'B', url: B }]));
storage.setItem('haven_servers_removed', JSON.stringify([X]));
const a = openPage(A, { desktop: desktopApi(store), storage });
assert.ok(!a.manager.servers.some(s => s.url === X), 'before the handover, this page keeps its removal');
await a.manager.reconcileWithDesktop();
assert.deepEqual(store.state.removed, [X]);
assert.equal(storage.getItem('haven_servers_desktop_list'), '1');
// Later, the user opens X again (which lifts the removal in the app); this
// page now follows the app instead of its old removed set.
store.state.removed = [];
store.add(X, 'X');
const again = openPage(A, { desktop: desktopApi(store), storage });
await again.manager.reconcileWithDesktop();
assert.ok(again.manager.servers.some(s => s.url === X));
});
test('a rename on one server shows on the others and the server name does not override it', async () => {
const store = seeded([A, B, X]);
const health = { [X]: { name: 'LIT', fingerprint: 'fx' } };
const a = openPage(A, { desktop: desktopApi(store), storage: storageWith([B, X]), health });
await a.manager.reconcileWithDesktop();
assert.ok(a.manager.editByUser(X, { name: 'Red Earth' }));
await new Promise(r => setImmediate(r));
const b = openPage(B, { desktop: desktopApi(store), storage: storageWith([A, X]), health });
const x = b.manager.servers.find(s => s.url === X);
assert.equal(x.name, 'Red Earth', 'applied at page load');
assert.equal(x.customName, true);
await b.manager.reconcileWithDesktop();
await b.manager.checkAll();
assert.equal(x.name, 'Red Earth', 'the server reporting LIT does not replace the user name');
// Going back to the server's own name lets it follow the server again.
await b.manager.checkAll();
assert.ok(b.manager.editByUser(X, { name: 'LIT' }));
await new Promise(r => setImmediate(r));
const a2 = openPage(A, { desktop: desktopApi(store), storage: a.storage, health });
await a2.manager.reconcileWithDesktop();
const ax = a2.manager.servers.find(s => s.url === X);
assert.equal(ax.name, 'LIT');
assert.ok(!ax.customName);
});
test('names follow the server everywhere when the user never chose one', async () => {
const store = seeded([A, B, X, Y]);
const health = {
[X]: { name: 'LIT', fingerprint: 'fx' },
[Y]: { name: 'Haven', fingerprint: 'fy' },
[B]: { name: 'Haven', fingerprint: 'fb' },
};
// A list saved since 4.19 (its old names were already kept once).
const bStorage = createStorage({ haven_servers: JSON.stringify([{ name: 'Red Earth', url: X }, { name: Y, url: Y }]), haven_servers_names_kept: '1' });
const b = openPage(B, { desktop: desktopApi(store), storage: bStorage, health });
await b.manager.reconcileWithDesktop();
assert.equal(await b.manager.checkAll(), 1);
assert.equal(b.manager.servers.find(s => s.url === X).name, 'LIT');
assert.equal(b.manager.servers.find(s => s.url === Y).name, Y, 'the default name "Haven" is skipped');
await new Promise(r => setImmediate(r));
assert.equal(store.state.history.find(h => h.url === X).name, 'LIT');
// A page that has never reached X (offline) still shows the real name.
const a = openPage(A, { desktop: desktopApi(store), storage: storageWith([X]) });
await a.manager.reconcileWithDesktop();
assert.equal(a.manager.servers.find(s => s.url === X).name, 'LIT');
});
test('a name typed when adding a server is kept and shared as the user\'s own', async () => {
const store = seeded([A, B]);
const health = { [X]: { name: 'LIT', fingerprint: 'fx' } };
const api = desktopApi(store);
const a = openPage(A, { desktop: api, storage: storageWith([B]), health });
await a.manager.reconcileWithDesktop();
assert.ok(a.manager.add('Red Earth', X, null, { userInitiated: true, customName: true }));
await api.addServerHistory(X, 'Red Earth', { userInitiated: true });
await a.manager.shareName(X);
await a.manager.checkAll();
const x = a.manager.servers.find(s => s.url === X);
assert.equal(x.name, 'Red Earth', 'the health check does not replace it');
assert.equal(x.customName, true);
const shared = store.state.history.find(h => h.url === X);
assert.equal(shared.name, 'Red Earth');
assert.equal(shared.customName, true, 'the Desktop list gets the custom flag');
// Another page that renames from the server's report keeps the user name.
const b = openPage(B, { desktop: desktopApi(store), storage: storageWith([A]), health });
await b.manager.reconcileWithDesktop();
await b.manager.checkAll();
assert.equal(b.manager.servers.find(s => s.url === X).name, 'Red Earth');
// An address or the default name typed in still follows the server.
assert.ok(a.manager.add(Y, Y, null, { userInitiated: true, customName: true }));
assert.ok(a.manager.add('Haven', Z, null, { userInitiated: true, customName: true }));
assert.ok(!a.manager.servers.find(s => s.url === Y).customName);
assert.ok(!a.manager.servers.find(s => s.url === Z).customName);
assert.deepEqual(a.warnings, []);
});
test('names saved before 4.19 are kept once, and later lists are left alone', async () => {
const health = { [X]: { name: 'LIT', fingerprint: 'fx' }, [Y]: { name: 'Why', fingerprint: 'fy' }, [Z]: { name: 'Zed', fingerprint: 'fz' } };
const storage = createStorage({ haven_servers: JSON.stringify([
{ name: 'Red Earth', url: X, addedAt: 1 },
{ name: Y, url: Y, addedAt: 1 },
{ name: 'Haven', url: Z, addedAt: 1 },
]) });
const page = openPage(A, { storage, health });
await page.manager.checkAll();
const byUrl = u => page.manager.servers.find(s => s.url === u);
assert.equal(byUrl(X).name, 'Red Earth');
assert.equal(byUrl(X).customName, true);
assert.equal(byUrl(Y).name, 'Why', 'a bare address follows the server');
assert.equal(byUrl(Z).name, 'Zed', 'the default name follows the server');
assert.equal(storage.getItem('haven_servers_names_kept'), '1');
assert.equal(JSON.parse(storage.getItem('haven_servers')).find(s => s.url === X).customName, true, 'saved');
// Runs once: a name that followed the server later is not frozen.
storage.setItem('haven_servers', JSON.stringify([{ name: 'Old', url: X, addedAt: 1 }]));
const again = openPage(A, { storage, health });
await again.manager.checkAll();
assert.equal(again.manager.servers[0].name, 'LIT');
// With the Desktop app, an edit made there wins over a kept old name,
// and a kept old name reaches the app when it has no edit of its own.
const store = seeded([A, X, Y]);
store.rename(X, 'Desk Name', { custom: true, editedAt: 5 });
const s2 = createStorage({ haven_servers: JSON.stringify([{ name: 'Red Earth', url: X }, { name: 'My Y', url: Y }]) });
const d = openPage(A, { desktop: desktopApi(store), storage: s2, health });
await d.manager.reconcileWithDesktop();
assert.equal(d.manager.servers.find(s => s.url === X).name, 'Desk Name');
const y = store.state.history.find(h => h.url === Y);
assert.equal(y.name, 'My Y');
assert.equal(y.customName, true);
});
test('a new order on one server shows on the others, and the hidden server keeps its place', async () => {
const store = seeded([A, B, X, Y, Z]);
const a = openPage(A, { desktop: desktopApi(store), storage: storageWith([A, B, X, Y, Z]) });
await a.manager.reconcileWithDesktop();
// The rail on A does not show A; the user drags Z to the top.
a.manager.reorder([Z, B, X, Y]);
await new Promise(r => setImmediate(r));
assert.deepEqual(urls(a.manager.servers), [A, Z, B, X, Y]);
const b = openPage(B, { desktop: desktopApi(store), storage: storageWith([Y, X, A, Z]) });
assert.deepEqual(urls(b.manager.servers), [A, Z, B, X, Y], 'applied at page load');
await b.manager.reconcileWithDesktop();
assert.deepEqual(urls(b.manager.servers), [A, Z, B, X, Y]);
});
test('the first page keeps its own order when the app has none yet', async () => {
const store = seeded([X, Y, Z]);
const a = openPage(A, { desktop: desktopApi(store), storage: storageWith([Z, Y, X]) });
await a.manager.reconcileWithDesktop();
assert.deepEqual(urls(a.manager.servers), [Z, Y, X]);
assert.deepEqual(store.view().order, [Z, Y, X]);
});
test('one server under two addresses shows once in the rail and twice, with a note, in Manage Servers', async () => {
const store = seeded([]);
const L = 'https://localhost:3000';
const D = 'https://anchaven.duckdns.org:3000';
const health = {
[L]: { name: 'Anc', fingerprint: 'same' },
[D]: { name: 'Anc', fingerprint: 'same' },
[X]: { name: 'LIT', fingerprint: 'fx' },
[B]: { name: 'Bee', fingerprint: 'fb' },
};
const page = openPage(B, { desktop: desktopApi(store), storage: storageWith([L, D, X, B]), health });
await page.manager.reconcileWithDesktop();
await page.manager.selfFingerprintReady;
await page.manager.checkAll();
const rail = urls(page.manager.railServers(B));
assert.deepEqual(rail, [L, X]);
const manage = page.manager.otherServers(B);
assert.deepEqual(urls(manage), [L, D, X]);
assert.deepEqual(plain(manage[1].duplicateOf), { url: L, name: 'Anc' });
assert.equal(manage[0].duplicateOf, undefined);
assert.equal(page.manager.servers.length, 4, 'nothing is deleted');
// On the server itself, both addresses are hidden as "this server".
const self = openPage(L, { storage: storageWith([L, D, X]), health });
await self.manager.selfFingerprintReady;
await self.manager.checkAll();
assert.deepEqual(urls(self.manager.railServers(L)), [X]);
});
test('a server on another hostname repeating a fingerprint is not hidden as a duplicate', async () => {
const store = seeded([]);
const R = 'https://real.example.com';
const F = 'https://fake.example.com';
const health = {
[F]: { name: 'Real', fingerprint: 'real' },
[R]: { name: 'Real', fingerprint: 'real' },
[B]: { name: 'Bee', fingerprint: 'fb' },
};
const page = openPage(B, { desktop: desktopApi(store), storage: storageWith([F, R, B]), health });
await page.manager.reconcileWithDesktop();
await page.manager.selfFingerprintReady;
await page.manager.checkAll();
assert.deepEqual(urls(page.manager.railServers(B)), [F, R]);
assert.equal(page.manager.otherServers(B)[1].duplicateOf, undefined);
});
test('an old Desktop app without the shared list still works as before', async () => {
const store = seeded([A, B, X]);
const api = desktopApi(store, { old: true });
const page = openPage(A, { desktop: api, storage: storageWith([B, Y]) });
assert.ok(page.manager.servers.some(s => s.url === X), 'bootstrap from the plain history');
const stats = await page.manager.reconcileWithDesktop();
assert.deepEqual({ ...stats }, { added: 0, removed: 0, renamed: 0 });
await new Promise(r => setImmediate(r));
assert.ok(store.state.history.some(h => h.url === Y), 'local servers reach the history');
page.manager.remove(X);
page.manager.reorder([Y, B]);
assert.ok(page.manager.editByUser(B, { name: 'Bee' }));
await new Promise(r => setImmediate(r));
assert.ok(api.calls.some(c => c[0] === 'remove' && c[1] === X));
// The page's own removed set keeps X out, as it always did.
const again = openPage(A, { desktop: desktopApi(seeded([A, B, X]), { old: true }), storage: page.storage });
await again.manager.reconcileWithDesktop();
assert.ok(!again.manager.servers.some(s => s.url === X));
assert.deepEqual(page.warnings, []);
});
test('in a browser without the Desktop app nothing is shared and nothing breaks', async () => {
const page = openPage(A, { storage: storageWith([B, X]) });
assert.deepEqual({ ...(await page.manager.reconcileWithDesktop()) }, { added: 0, removed: 0, renamed: 0 });
page.manager.remove(X);
page.manager.reorder([B]);
assert.ok(page.manager.editByUser(B, { name: 'Bee' }));
assert.deepEqual(plain(page.manager.servers.map(s => s.name)), ['Bee']);
assert.deepEqual(JSON.parse(page.storage.getItem('haven_servers_removed')), [X]);
assert.deepEqual(page.warnings, []);
});
// A Desktop app that asks the user itself before a page changes which
// servers the user has (Haven Desktop's server-list-gate.js): adds only for
// the user's own Add Server, removals and renames only with a yes, and a
// server's reported name only from its own page. answer(kind, url) is the
// user's answer to the app's question.
function gatedDesktopApi(store, answer = () => true) {
const api = desktopApi(store);
api.serverListGated = true;
api.asked = [];
const listed = (url) => store.state.history.some(h => h.url === norm(url));
api.addServerHistory = async (url, name, opts) => {
api.calls.push(['add', url]);
if (listed(url)) return 'exists';
if (!(opts && opts.userInitiated)) return 'refused';
api.asked.push(['add', norm(url)]);
if (!answer('add', norm(url))) return 'declined';
return store.add(url, name, opts);
};
// Like the app: removals and renames not marked as the user's are a page
// syncing on its own, and change nothing without asking.
api.removeServerHistory = async (url, opts) => {
api.calls.push(['remove', url]);
if (listed(url) && opts && opts.user === true) {
api.asked.push(['remove', norm(url)]);
if (answer('remove', norm(url))) store.remove(url);
}
return store.view().servers;
};
api.updateServerName = async (url, name, opts = {}) => {
api.calls.push(['rename', url]);
if (typeof opts.custom !== 'boolean') return false; // only the server's own page
if (!listed(url) || opts.user !== true) return false;
api.asked.push(['rename', norm(url)]);
if (!answer('rename', norm(url))) return false;
return store.rename(url, name, opts);
};
return api;
}
test('a Desktop app that asks first gets no automatic changes from the page', async () => {
const store = seeded([A, B, X]);
const storage = storageWith([B, X, Y]);
storage.setItem('haven_servers_removed', JSON.stringify([Z]));
const api = gatedDesktopApi(store);
const health = { [X]: { name: 'LIT', fingerprint: 'fx' } };
const a = openPage(A, { desktop: api, storage, health });
await a.manager.reconcileWithDesktop();
await a.manager.checkAll();
await new Promise(r => setImmediate(r));
assert.deepEqual(api.calls, [], 'no add, removal hand-over or reported name reaches the app');
assert.ok(a.manager.servers.some(s => s.url === Y), 'a server only this page knows stays here');
assert.ok(!store.state.history.some(h => h.url === Y));
assert.equal(a.manager.servers.find(s => s.url === X).name, 'LIT', 'names still follow the server on this page');
assert.equal(a.manager.desktopAsksFor(X), true);
assert.equal(a.manager.desktopAsksFor(Y), false, 'the app does not ask about a server it does not have');
// The order still reaches the app; it only moves servers it has.
a.manager.reorder([X, B, Y]);
await new Promise(r => setImmediate(r));
assert.deepEqual(plain(store.state.order.filter(u => u !== A)), [X, B]);
assert.deepEqual(a.warnings, []);
});
test('removing and renaming wait for the user\'s answer in the Desktop app', async () => {
const store = seeded([A, B, X]);
let yes = false;
const api = gatedDesktopApi(store, () => yes);
const a = openPage(A, { desktop: api, storage: storageWith([B, X]) });
await a.manager.reconcileWithDesktop();
assert.equal(await a.manager.removeThroughDesktop(X), false, 'the user said no');
assert.ok(a.manager.servers.some(s => s.url === X));
assert.ok(store.state.history.some(h => h.url === X));
assert.equal(await a.manager.editByUser(B, { name: 'Bee' }), false);
assert.equal(a.manager.servers.find(s => s.url === B).name, B);
yes = true;
assert.equal(await a.manager.editByUser(B, { name: 'Bee', icon: 'https://img.example.com/b.png' }), true);
const b = a.manager.servers.find(s => s.url === B);
assert.equal(b.name, 'Bee');
assert.equal(b.customIcon, true);
assert.equal(store.state.history.find(h => h.url === B).name, 'Bee');
assert.equal(await a.manager.removeThroughDesktop(X), true);
assert.ok(!a.manager.servers.some(s => s.url === X));
assert.deepEqual(plain(store.state.removed), [X]);
assert.deepEqual(api.asked.map(q => q[0]), ['remove', 'rename', 'rename', 'remove']);
// Saving an edit nobody would see is not asked about.
api.asked.length = 0;
assert.equal(await a.manager.editByUser(B, { name: 'Bee', icon: 'https://img.example.com/b.png' }), true);
assert.deepEqual(api.asked, []);
assert.deepEqual(a.warnings, []);
});
test('the server bar skips its own question only where the Desktop app asks', () => {
const bar = fs.readFileSync(path.join(__dirname, '..', 'public/js/modules/app-server-bar.js'), 'utf8');
const remove = bar.slice(bar.indexOf('_removeServerByUser(url, name, done) {'));
assert.match(remove, /if \(!manager\.desktopAsksFor\(url\)\) \{\s+if \(!confirm\(t\('confirm\.remove_server'/);
assert.match(remove, /manager\.removeThroughDesktop\(url\)/);
assert.equal((bar.match(/confirm\(t\('confirm\.remove_server'/g) || []).length, 1, 'both remove buttons go through one place');
assert.match(bar, /else if \(this\.serverManager\.desktopGated\(\)\) \{\s+await this\._addServerThroughDesktop/);
assert.match(bar, /if \(result === 'declined'\) return;/);
const en = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'public/locales/en.json'), 'utf8'));
assert.ok(en.servers.desktop_not_changed);
});
test('the server bar uses the shared list and reports what Sync did', () => {
const bar = fs.readFileSync(path.join(__dirname, '..', 'public/js/modules/app-server-bar.js'), 'utf8');
assert.match(bar, /railServers\(window\.location\.origin\)/);
assert.match(bar, /otherServers\(window\.location\.origin\)/);
assert.match(bar, /t\('servers\.sync_updated', \{ added, removed, renamed \}\)/);
assert.match(bar, /t\('servers\.sync_up_to_date'\)/);
assert.match(bar, /addServerHistory\(finalUrl, name, \{ userInitiated: true \}\)/);
assert.match(bar, /add\(name, url, icon, \{ userInitiated: true, customName: true \}\)/);
assert.match(bar, /shareName\(finalUrl\)/);
assert.doesNotMatch(bar, /servers\.sync_success/);
const en = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'public/locales/en.json'), 'utf8'));
for (const key of ['sync_up_to_date', 'sync_updated', 'same_server_as']) assert.ok(en.servers[key], key);
});
test('a server icon loads only from that server, or from where the user chose', async () => {
const health = {
[X]: { name: 'LIT', fingerprint: 'fx', icon: 'https://tracker.example.net/pixel.png' },
[Y]: { name: 'Why', fingerprint: 'fy', icon: '/uploads/icon.png' },
};
const page = openPage(B, { storage: storageWith([X, Y, Z]), health });
await page.manager.checkAll();
const byUrl = new Map(page.manager.getAll().map(s => [s.url, s]));
assert.equal(page.manager.iconFor(byUrl.get(X)), null);
assert.equal(page.manager.iconFor(byUrl.get(Y)), 'https://y.example.com/uploads/icon.png');
assert.equal(page.manager.iconFor({ url: Z, icon: 'https://cdn.example.org/z.png', status: {} }), null);
assert.equal(page.manager.iconFor({ url: Z, icon: 'https://cdn.example.org/z.png', customIcon: true, status: {} }), 'https://cdn.example.org/z.png');
});