Files
Haven/test/mediaGroupLayout.integration.test.js
ancsemi 5039ac95bd Add a server default and a personal choice for the picture grid (#5799)
media_group_layout is 'grid' or 'stacked': a server setting the admin sets, carried in templates, and a member preference that overrides it, saved on the account.
2026-10-16 12:36:35 -04:00

127 lines
5.8 KiB
JavaScript

'use strict';
/**
* Pictures sent together show as a grid or full size, one under another
* (#5799). The admin sets the server's default (media_group_layout) and each
* member may pick their own, saved on their account. Both are checked here
* against a real server on a scratch port and data dir:
* node --test test/mediaGroupLayout.integration.test.js
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const { spawn } = require('node:child_process');
const { io } = require('socket.io-client');
const { checkSetting } = require('../src/serverTemplate');
const { MEDIA_GROUP_LAYOUTS } = require('../src/mediaGroup');
const PORT = 3463;
const BASE = `http://localhost:${PORT}`;
const DATA = path.join(os.tmpdir(), `haven-media-layout-${Date.now()}`);
let server;
const clients = [];
const post = (p, body) => new Promise((res, rej) => {
const d = JSON.stringify(body);
const r = http.request({ host: 'localhost', port: PORT, path: p, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(d) } }, (resp) => {
let out = ''; resp.on('data', (c) => { out += c; }); resp.on('end', () => res(JSON.parse(out || '{}')));
});
r.on('error', rej); r.end(d);
});
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const register = (username) => post('/api/auth/register', { username, password: 'layout12345', eulaVersion: '2.0', ageVerified: true });
function connect(token) {
const s = io(BASE, { auth: { token }, transports: ['websocket'], forceNew: true });
clients.push(s);
return new Promise((res, rej) => { s.on('connect', () => res(s)); s.on('connect_error', rej); });
}
// Sends `event` and resolves with the first `reply` event, or null after a while.
function ask(sock, event, data, reply, ms = 1500) {
return new Promise((res) => {
const t = setTimeout(() => { sock.off(reply, on); res(null); }, ms);
function on(payload) { clearTimeout(t); sock.off(reply, on); res(payload); }
sock.on(reply, on);
sock.emit(event, data);
});
}
const settings = (sock) => ask(sock, 'get-server-settings', undefined, 'server-settings');
const prefs = (sock) => ask(sock, 'get-preferences', undefined, 'preferences');
test.before(async () => {
fs.mkdirSync(DATA, { recursive: true });
server = spawn(process.execPath, ['server.js'], {
cwd: path.join(__dirname, '..'),
env: { ...process.env, PORT: String(PORT), HAVEN_DATA_DIR: DATA, ADMIN_USERNAME: 'admin', FORCE_HTTP: 'true' },
stdio: 'ignore',
});
for (let i = 0; i < 60; i++) {
try {
await new Promise((res, rej) => http.get(`${BASE}/api/health`, (r) => (r.statusCode === 200 ? res() : rej())).on('error', rej));
return;
} catch { await wait(500); } // not listening yet: try again
}
throw new Error('server did not start');
});
test.after(async () => {
clients.forEach((s) => s.close());
if (server && server.exitCode === null) {
const exited = new Promise((r) => server.once('exit', r));
server.kill();
await exited;
}
fs.rmSync(DATA, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
});
test('the layouts are grid and stacked, and templates carry only those', () => {
assert.deepEqual(MEDIA_GROUP_LAYOUTS, ['grid', 'stacked']);
assert.equal(checkSetting('media_group_layout', 'stacked'), 'stacked');
assert.equal(checkSetting('media_group_layout', ' grid '), 'grid');
assert.equal(checkSetting('media_group_layout', 'mosaic'), null);
assert.equal(checkSetting('media_group_layout', ''), null);
});
test('the admin sets the server default; members see it change and cannot set it', async () => {
const admin = await register('admin');
const member = await register('layoutmember');
const A = await connect(admin.token);
const M = await connect(member.token);
assert.equal((await settings(M)).media_group_layout, 'grid', 'a new server shows the grid');
// Anything but the two layouts is refused without a word.
assert.equal(await ask(A, 'update-server-setting', { key: 'media_group_layout', value: 'mosaic' }, 'server-setting-changed', 800), null);
assert.equal((await settings(M)).media_group_layout, 'grid');
const changed = await ask(A, 'update-server-setting', { key: 'media_group_layout', value: 'stacked' }, 'server-setting-changed');
assert.deepEqual(changed, { key: 'media_group_layout', value: 'stacked' });
await wait(200);
assert.equal((await settings(M)).media_group_layout, 'stacked', 'members are sent the new default');
const refused = await ask(M, 'update-server-setting', { key: 'media_group_layout', value: 'grid' }, 'error-msg');
assert.match(String(refused), /admins/i);
assert.equal((await settings(M)).media_group_layout, 'stacked');
});
test('a member saves their own layout on the account and can go back to the default', async () => {
const login = await post('/api/auth/login', { username: 'layoutmember', password: 'layout12345', eulaVersion: '2.0', ageVerified: true });
const M = await connect(login.token);
assert.equal(await ask(M, 'set-preference', { key: 'media_group_layout', value: 'mosaic' }, 'preference-saved', 800), null);
assert.equal((await prefs(M)).media_group_layout, undefined, 'no row means the server default');
assert.deepEqual(await ask(M, 'set-preference', { key: 'media_group_layout', value: 'grid' }, 'preference-saved'), { key: 'media_group_layout', value: 'grid' });
// Another device of the same account gets it with the rest of the preferences.
const other = await connect(login.token);
assert.equal((await prefs(other)).media_group_layout, 'grid');
assert.deepEqual(await ask(M, 'delete-preference', { key: 'media_group_layout' }, 'preference-deleted'), { key: 'media_group_layout' });
assert.equal((await prefs(M)).media_group_layout, undefined);
});