1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// Run: node desktop/modules/sonix/test_sonix.js
// Sonix.js is a QML library, so its .pragma line is stripped before node
// evaluates it.
const fs = require("fs");
const assert = require("assert");
const src = fs.readFileSync(__dirname + "/Sonix.js", "utf8")
.replace(/^\.pragma library$/m, "");
const S = new Function(src +
"\nreturn { parseBeacon, qualityLine, nextMode, modeGlyph, clock, cover, centred, zoomAt," +
" stamp, query, crumbs, parentOf, colour, offsetXY };")();
// Beacon: "SONIXLINK1 <ip> <port> <name with spaces>".
assert.deepStrictEqual(S.parseBeacon("SONIXLINK1 10.0.0.5 7800 HiBy R1\n"),
{ host: "10.0.0.5", port: 7800, name: "HiBy R1" });
assert.strictEqual(S.parseBeacon("SONIXLINK2 10.0.0.5 7800 x"), null);
assert.strictEqual(S.parseBeacon("SONIXLINK1 nope 7800 x"), null);
assert.strictEqual(S.parseBeacon("SONIXLINK1 10.0.0.5 0 x"), null);
assert.strictEqual(S.parseBeacon("SONIXLINK1 10.0.0.5 7800"), null);
// Quality line from /api/state fields.
assert.strictEqual(S.qualityLine({ path: "/mnt/sd_0/a/b.flac", sample_rate: 96000, bits: 24, bitrate: 2304000 }),
"FLAC · 24/96 · 2304 kbps");
assert.strictEqual(S.qualityLine({ path: "x.mp3", sample_rate: 44100, bits: 0, bitrate: 320000 }),
"MP3 · 44.1 kHz · 320 kbps");
assert.strictEqual(S.qualityLine({ path: "", sample_rate: 0, bits: 0, bitrate: 0 }), "");
// Mode cycle, unknown restarts at normal.
assert.strictEqual(S.nextMode("normal"), "repeat_all");
assert.strictEqual(S.nextMode("shuffle_repeat"), "normal");
assert.strictEqual(S.nextMode("???"), "normal");
assert.strictEqual(S.modeGlyph("repeat_one"), 0xF0458);
assert.strictEqual(S.modeGlyph("???"), 0xF0457);
assert.strictEqual(S.clock(0), "0:00");
assert.strictEqual(S.clock(61.9), "1:01");
assert.strictEqual(S.clock(-3), "0:00");
// Cover: a 2000x1000 picture in 480x800 scales to height (0.8), 1600 wide.
let c = S.cover(2000, 1000, 1, 0, 0, 480, 800);
assert.strictEqual(c.scale, 0.8);
assert.strictEqual(c.w, 1600);
assert.strictEqual(c.h, 800);
// Panning is clamped so the frame stays covered.
c = S.cover(2000, 1000, 1, 100, -50, 480, 800);
assert.strictEqual(c.x, 0);
assert.strictEqual(c.y, 0);
c = S.cover(2000, 1000, 1, -5000, 0, 480, 800);
assert.strictEqual(c.x, 480 - 1600);
// Zoom is kept in 1..4.
assert.strictEqual(S.cover(2000, 1000, 0.2, 0, 0, 480, 800).zoom, 1);
assert.strictEqual(S.cover(2000, 1000, 9, 0, 0, 480, 800).zoom, 4);
// Centred: the crop the player itself makes.
c = S.centred(2000, 1000, 480, 800);
assert.strictEqual(c.x, (480 - 1600) / 2);
assert.strictEqual(c.y, 0);
// Zooming at a point keeps that point under the cursor.
c = S.centred(2000, 1000, 480, 800);
const z = S.zoomAt(2000, 1000, c, 2, 240, 400, 480, 800);
assert.strictEqual(z.zoom, 2);
assert.ok(Math.abs((240 - z.x) / z.scale - (240 - c.x) / c.scale) < 1e-9);
assert.strictEqual(S.stamp(new Date(2026, 10, 7, 9, 5, 3)), "bootlogo-20261107-090503");
assert.strictEqual(S.query({ do: "play_path", path: "/mnt/sd_0/a b&c.flac" }),
"do=play_path&path=%2Fmnt%2Fsd_0%2Fa%20b%26c.flac");
assert.deepStrictEqual(S.crumbs("/mnt/sd_0", "/mnt/sd_0"), [{ name: "Card", path: "/mnt/sd_0" }]);
assert.deepStrictEqual(S.crumbs("/mnt/sd_0/A/B", "/mnt/sd_0"), [
{ name: "Card", path: "/mnt/sd_0" },
{ name: "A", path: "/mnt/sd_0/A" },
{ name: "B", path: "/mnt/sd_0/A/B" },
]);
assert.deepStrictEqual(S.crumbs("/elsewhere", "/mnt/sd_0"), [{ name: "Card", path: "/mnt/sd_0" }]);
assert.strictEqual(S.parentOf("/mnt/sd_0/A/B", "/mnt/sd_0"), "/mnt/sd_0/A");
assert.strictEqual(S.parentOf("/mnt/sd_0/A", "/mnt/sd_0"), "/mnt/sd_0");
assert.strictEqual(S.parentOf("/mnt/sd_0", "/mnt/sd_0"), "/mnt/sd_0");
assert.strictEqual(S.colour("udt", "#123456"), "#123456");
assert.strictEqual(S.colour("#e5a50a", "#123456"), "#e5a50a");
assert.deepStrictEqual(S.offsetXY("se", 8), { x: 8, y: 8 });
assert.deepStrictEqual(S.offsetXY("nw", 3), { x: -3, y: -3 });
assert.deepStrictEqual(S.offsetXY("bogus", 5), { x: 5, y: 5 });
console.log("ok");
|