feat: Playtube Stream Dock Plugin 1.0.0 (Ajazz AKP153E) mit Icon-Pack
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
DEFINITIONS, PREFIX, actionIdFromUuid, buildRequest, normalizeSettings, renderAction, wrapTitle,
|
||||
} = require('../src/actions');
|
||||
|
||||
const STATE = {
|
||||
activeTab: 'music',
|
||||
visibleTab: 'music',
|
||||
tabs: {
|
||||
music: { playing: true, title: 'Ein sehr langer Songtitel', volume: 42.4, muted: false, likeStatus: 'like',
|
||||
repeatMode: 'ALL', hasVideo: true, thumbnail: 'https://example.test/c=w60-h60' },
|
||||
youtube: { playing: false, title: 'Video', volume: 80, muted: true, likeStatus: 'none', repeatMode: 'NONE' },
|
||||
},
|
||||
};
|
||||
|
||||
test('actionIdFromUuid erkennt nur bekannte Aktionen', () => {
|
||||
assert.equal(actionIdFromUuid(`${PREFIX}volumeup`), 'volumeup');
|
||||
assert.equal(actionIdFromUuid(`${PREFIX}gibtesnicht`), null);
|
||||
assert.equal(actionIdFromUuid('com.other.volumeup'), null);
|
||||
assert.equal(actionIdFromUuid(undefined), null);
|
||||
});
|
||||
|
||||
test('normalizeSettings begrenzt Werte und ersetzt ungueltige durch Standardwerte', () => {
|
||||
const settings = normalizeSettings({ step: '999', volume: -5, seconds: 'abc', target: 'x', tab: 'y' });
|
||||
assert.equal(settings.step, 25);
|
||||
assert.equal(settings.volume, 0);
|
||||
assert.equal(settings.seconds, 10);
|
||||
assert.equal(settings.target, 'auto');
|
||||
assert.equal(settings.tab, 'toggle');
|
||||
});
|
||||
|
||||
test('normalizeSettings: showTitle nutzt den Standard der Aktion, wenn nichts gespeichert ist', () => {
|
||||
assert.equal(normalizeSettings({}, true).showTitle, true);
|
||||
assert.equal(normalizeSettings({}, false).showTitle, false);
|
||||
assert.equal(normalizeSettings({ showTitle: false }, true).showTitle, false);
|
||||
assert.equal(normalizeSettings({ showTitle: 'true' }, false).showTitle, true);
|
||||
});
|
||||
|
||||
test('Lauter/Leiser senden volume_change mit Vorzeichen und Schrittweite', () => {
|
||||
assert.deepEqual(buildRequest('volumeup', { step: 7 }), { cmd: 'volume_change', fields: { target: 'auto', value: 7 } });
|
||||
assert.deepEqual(buildRequest('volumedown', { step: 7, target: 'music' }),
|
||||
{ cmd: 'volume_change', fields: { target: 'music', value: -7 } });
|
||||
});
|
||||
|
||||
test('Lautstaerke setzen und Spulen', () => {
|
||||
assert.deepEqual(buildRequest('volumeset', { volume: 55 }), { cmd: 'set_volume', fields: { target: 'auto', value: 55 } });
|
||||
assert.deepEqual(buildRequest('seekback', { seconds: 15 }), { cmd: 'seek', fields: { target: 'auto', value: -15 } });
|
||||
assert.deepEqual(buildRequest('seekforward', {}), { cmd: 'seek', fields: { target: 'auto', value: 10 } });
|
||||
});
|
||||
|
||||
test('Playlist-Taste ohne ID sendet nichts, mit ID play_playlist', () => {
|
||||
assert.equal(buildRequest('playlist', {}), null);
|
||||
assert.deepEqual(buildRequest('playlist', { playlistId: ' PL123 ' }),
|
||||
{ cmd: 'play_playlist', fields: { playlist: 'PL123' } });
|
||||
});
|
||||
|
||||
test('Tab wechseln: "toggle" wird zu auto', () => {
|
||||
assert.equal(buildRequest('switchtab', {}).fields.target, 'auto');
|
||||
assert.equal(buildRequest('switchtab', { tab: 'youtube' }).fields.target, 'youtube');
|
||||
});
|
||||
|
||||
test('jede Aktion hat request() und liefert gueltiges cmd', () => {
|
||||
for (const id of Object.keys(DEFINITIONS)) {
|
||||
const request = buildRequest(id, { playlistId: 'PLx' });
|
||||
assert.ok(request && typeof request.cmd === 'string', id);
|
||||
}
|
||||
});
|
||||
|
||||
test('Play/Pause-Zustand folgt dem aktiven Tab bzw. dem gewaehlten Ziel', () => {
|
||||
assert.equal(renderAction('playpause', {}, STATE, true).state, 1);
|
||||
assert.equal(renderAction('playpause', { target: 'youtube' }, STATE, true).state, 0);
|
||||
});
|
||||
|
||||
test('Lautstaerke-Tasten zeigen die gerundete Lautstaerke', () => {
|
||||
assert.equal(renderAction('volumeup', {}, STATE, true).title, '42%');
|
||||
assert.equal(renderAction('volumedown', { target: 'youtube' }, STATE, true).title, '80%');
|
||||
});
|
||||
|
||||
test('offline: Titel leer bzw. Offline-Hinweis, Zustand 0', () => {
|
||||
assert.equal(renderAction('volumeup', {}, null, false).title, '');
|
||||
const view = renderAction('playpause', { showTitle: true }, null, false);
|
||||
assert.equal(view.state, 0);
|
||||
assert.match(view.title, /offline/);
|
||||
});
|
||||
|
||||
test('Mute, Like und Repeat bilden den Playtube-Zustand auf State-Indizes ab', () => {
|
||||
assert.equal(renderAction('mute', { target: 'youtube' }, STATE, true).state, 1);
|
||||
assert.equal(renderAction('mute', {}, STATE, true).state, 0);
|
||||
assert.equal(renderAction('like', {}, STATE, true).state, 1);
|
||||
assert.equal(renderAction('repeat', {}, STATE, true).state, 1);
|
||||
assert.equal(renderAction('repeat', { target: 'youtube' }, STATE, true).state, 0);
|
||||
const one = { ...STATE, tabs: { ...STATE.tabs, music: { ...STATE.tabs.music, repeatMode: 'ONE' } } };
|
||||
assert.equal(renderAction('repeat', {}, one, true).state, 2);
|
||||
});
|
||||
|
||||
test('Cover-Taste liefert die Thumbnail-URL nur bei vorhandenem Video/Titel', () => {
|
||||
assert.equal(renderAction('nowplaying', {}, STATE, true).coverUrl, 'https://example.test/c=w60-h60');
|
||||
assert.equal(renderAction('nowplaying', { target: 'youtube' }, STATE, true).coverUrl, null);
|
||||
assert.equal(renderAction('nowplaying', {}, null, false).coverUrl, null);
|
||||
});
|
||||
|
||||
test('wrapTitle bricht um, trennt lange Woerter und kuerzt', () => {
|
||||
assert.equal(wrapTitle('Hello World'), 'Hello\nWorld');
|
||||
assert.equal(wrapTitle('Supercalifragilistic'), 'Supercal\nifragili\nstic');
|
||||
const lines = wrapTitle('eins zwei drei vier fuenf sechs sieben acht neun zehn').split('\n');
|
||||
assert.equal(lines.length, 3);
|
||||
assert.ok(lines[2].endsWith('..'));
|
||||
assert.equal(wrapTitle(''), '');
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { Controller, sharpCoverUrl } = require('../src/controller');
|
||||
const { PlaytubeError } = require('../src/playtubeClient');
|
||||
const { PREFIX } = require('../src/actions');
|
||||
|
||||
const silentLogger = { info() {}, warn() {}, error() {} };
|
||||
const tick = (ms = 5) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const STATE = {
|
||||
activeTab: 'music',
|
||||
visibleTab: 'music',
|
||||
tabs: {
|
||||
music: { playing: true, title: 'Song', volume: 40, muted: false, likeStatus: 'none', repeatMode: 'NONE',
|
||||
hasVideo: true, thumbnail: 'https://example.test/x=w60-h60-l90' },
|
||||
youtube: { playing: false, title: '', volume: 100, muted: false },
|
||||
},
|
||||
};
|
||||
|
||||
class FakeHost extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.calls = [];
|
||||
for (const name of ['setTitle', 'setImage', 'setState', 'showAlert', 'showOk', 'sendToPropertyInspector']) {
|
||||
this[name] = (...args) => this.calls.push([name, ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
named(name) { return this.calls.filter((call) => call[0] === name); }
|
||||
}
|
||||
|
||||
class FakeClient extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.connected = true;
|
||||
this.requests = [];
|
||||
this.state = STATE;
|
||||
this.failWith = null;
|
||||
this.playlists = [{ id: 'LM', title: 'Liked' }];
|
||||
}
|
||||
|
||||
start() {}
|
||||
stop() {}
|
||||
async request(cmd, fields) {
|
||||
if (this.failWith) throw this.failWith;
|
||||
this.requests.push({ cmd, ...fields });
|
||||
return { ok: true };
|
||||
}
|
||||
async status() { return this.state; }
|
||||
async listPlaylists() { return this.playlists; }
|
||||
}
|
||||
|
||||
function setup() {
|
||||
const host = new FakeHost();
|
||||
const client = new FakeClient();
|
||||
const covers = [];
|
||||
const controller = new Controller({
|
||||
host, client, logger: silentLogger, pollIntervalMs: 100000,
|
||||
fetchImage: async (url) => { covers.push(url); return `data:image/jpeg;base64,${Buffer.from(url).toString('base64')}`; },
|
||||
});
|
||||
return { host, client, controller, covers };
|
||||
}
|
||||
|
||||
const appear = (controller, id, context = `ctx-${id}`, settings = {}) => controller.handleEvent({
|
||||
event: 'willAppear', action: `${PREFIX}${id}`, context, payload: { settings },
|
||||
});
|
||||
|
||||
test('sharpCoverUrl hebt die Aufloesung von Music-Covern auf Tastengroesse', () => {
|
||||
assert.equal(sharpCoverUrl('https://x/a=w60-h60-l90-rj'), 'https://x/a=w144-h144-l90-rj');
|
||||
assert.equal(sharpCoverUrl('https://i.ytimg.com/vi/abc/hq.jpg'), 'https://i.ytimg.com/vi/abc/hq.jpg');
|
||||
});
|
||||
|
||||
test('Taste erscheint offline: Offline-Titel', () => {
|
||||
const { host, client, controller } = setup();
|
||||
client.connected = false;
|
||||
appear(controller, 'playpause', 'k1', { showTitle: true });
|
||||
assert.match(host.named('setTitle')[0][2], /offline/);
|
||||
});
|
||||
|
||||
test('Lauter: sofort beim Druecken, Wiederholung beim Halten, Stopp beim Loslassen', async () => {
|
||||
const { client, controller } = setup();
|
||||
appear(controller, 'volumeup', 'v1', { step: 3 });
|
||||
controller.handleEvent({ event: 'keyDown', context: 'v1' });
|
||||
await tick();
|
||||
assert.equal(client.requests.length, 1);
|
||||
assert.deepEqual(client.requests[0], { cmd: 'volume_change', target: 'auto', value: 3 });
|
||||
assert.ok(controller.repeaters.has('v1'));
|
||||
controller.handleEvent({ event: 'keyUp', context: 'v1' });
|
||||
assert.equal(controller.repeaters.size, 0);
|
||||
await tick();
|
||||
assert.equal(client.requests.length, 1);
|
||||
});
|
||||
|
||||
test('normale Aktionen laufen beim Loslassen, nicht beim Druecken', async () => {
|
||||
const { client, controller } = setup();
|
||||
appear(controller, 'next', 'n1');
|
||||
controller.handleEvent({ event: 'keyDown', context: 'n1' });
|
||||
await tick();
|
||||
assert.equal(client.requests.length, 0);
|
||||
controller.handleEvent({ event: 'keyUp', context: 'n1' });
|
||||
await tick();
|
||||
assert.deepEqual(client.requests, [{ cmd: 'next', target: 'auto' }]);
|
||||
});
|
||||
|
||||
test('Fehler von Playtube loesen einen Alarm auf der Taste aus', async () => {
|
||||
const { host, client, controller } = setup();
|
||||
client.failWith = new PlaytubeError('not_connected', 'offline');
|
||||
appear(controller, 'next', 'n1');
|
||||
controller.handleEvent({ event: 'keyUp', context: 'n1' });
|
||||
await tick();
|
||||
assert.equal(host.named('showAlert').length, 1);
|
||||
});
|
||||
|
||||
test('Playlist-Taste ohne Auswahl zeigt Alarm und sendet nichts', async () => {
|
||||
const { host, client, controller } = setup();
|
||||
appear(controller, 'playlist', 'p1');
|
||||
controller.handleEvent({ event: 'keyUp', context: 'p1' });
|
||||
await tick();
|
||||
assert.equal(client.requests.length, 0);
|
||||
assert.equal(host.named('showAlert').length, 1);
|
||||
});
|
||||
|
||||
test('Playlist-Taste startet die gewaehlte Playlist und zeigt ihren Namen', async () => {
|
||||
const { host, client, controller } = setup();
|
||||
appear(controller, 'playlist', 'p1', { playlistId: 'PLabc', playlistTitle: 'Mein Mix' });
|
||||
assert.equal(host.named('setTitle').at(-1)[2], 'Mein Mix');
|
||||
controller.handleEvent({ event: 'keyUp', context: 'p1' });
|
||||
await tick();
|
||||
assert.deepEqual(client.requests, [{ cmd: 'play_playlist', playlist: 'PLabc' }]);
|
||||
});
|
||||
|
||||
test('Status aktualisiert Zustand und Lautstaerke-Anzeige, nur bei Aenderung', async () => {
|
||||
const { host, controller } = setup();
|
||||
appear(controller, 'playpause', 'pp');
|
||||
appear(controller, 'volumeup', 'vu');
|
||||
await controller.poll();
|
||||
assert.deepEqual(host.named('setState').filter((c) => c[1] === 'pp').at(-1), ['setState', 'pp', 1]);
|
||||
assert.equal(host.named('setTitle').filter((c) => c[1] === 'vu').at(-1)[2], '40%');
|
||||
const before = host.calls.length;
|
||||
await controller.poll();
|
||||
assert.equal(host.calls.length, before, 'unveraenderter Status sendet nichts erneut');
|
||||
});
|
||||
|
||||
test('Cover wird in hoher Aufloesung geladen und als Bild gesetzt', async () => {
|
||||
const { host, controller, covers } = setup();
|
||||
appear(controller, 'nowplaying', 'np');
|
||||
await controller.poll();
|
||||
await tick(20);
|
||||
assert.deepEqual(covers, ['https://example.test/x=w144-h144-l90']);
|
||||
const image = host.named('setImage').at(-1);
|
||||
assert.equal(image[1], 'np');
|
||||
assert.match(image[2], /^data:image\/jpeg;base64,/);
|
||||
});
|
||||
|
||||
test('Verbindungsverlust stellt Offline-Anzeige her', async () => {
|
||||
const { host, client, controller } = setup();
|
||||
controller.start();
|
||||
appear(controller, 'volumeup', 'vu');
|
||||
await controller.poll();
|
||||
assert.equal(host.named('setTitle').filter((c) => c[1] === 'vu').at(-1)[2], '40%');
|
||||
client.connected = false;
|
||||
client.emit('disconnected');
|
||||
assert.equal(host.named('setTitle').filter((c) => c[1] === 'vu').at(-1)[2], '');
|
||||
controller.stop();
|
||||
});
|
||||
|
||||
test('Property Inspector bekommt die Playlists', async () => {
|
||||
const { host, controller } = setup();
|
||||
controller.handleEvent({ event: 'sendToPlugin', action: `${PREFIX}playlist`, context: 'p1',
|
||||
payload: { command: 'requestPlaylists' } });
|
||||
await tick();
|
||||
const [, action, context, payload] = host.named('sendToPropertyInspector')[0];
|
||||
assert.equal(action, `${PREFIX}playlist`);
|
||||
assert.equal(context, 'p1');
|
||||
assert.deepEqual(payload, { command: 'playlists', ok: true, playlists: [{ id: 'LM', title: 'Liked' }] });
|
||||
});
|
||||
|
||||
test('Property Inspector: offline und Fehler werden gemeldet', async () => {
|
||||
const { host, client, controller } = setup();
|
||||
client.connected = false;
|
||||
controller.handleEvent({ event: 'sendToPlugin', action: 'a', context: 'c', payload: { command: 'requestPlaylists' } });
|
||||
await tick();
|
||||
assert.equal(host.named('sendToPropertyInspector')[0][3].error, 'offline');
|
||||
|
||||
client.connected = true;
|
||||
client.listPlaylists = async () => { throw new PlaytubeError('remote', 'unbekannter Befehl'); };
|
||||
controller.handleEvent({ event: 'sendToPlugin', action: 'a', context: 'c', payload: { command: 'requestPlaylists' } });
|
||||
await tick();
|
||||
const payload = host.named('sendToPropertyInspector')[1][3];
|
||||
assert.equal(payload.ok, false);
|
||||
assert.equal(payload.error, 'remote');
|
||||
});
|
||||
|
||||
test('willDisappear raeumt Taste und Wiederholung auf', () => {
|
||||
const { controller } = setup();
|
||||
appear(controller, 'volumeup', 'v1');
|
||||
controller.handleEvent({ event: 'keyDown', context: 'v1' });
|
||||
controller.handleEvent({ event: 'willDisappear', context: 'v1' });
|
||||
assert.equal(controller.keys.size, 0);
|
||||
assert.equal(controller.repeaters.size, 0);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { buildInfoSvg, formatTime, svgToDataUri } = require('../src/infoPanel');
|
||||
const { Controller } = require('../src/controller');
|
||||
const { PREFIX } = require('../src/actions');
|
||||
|
||||
const VIEW = { title: 'Tom & <Live>', subtitle: 'Interpret', playing: true, currentTime: 65, duration: 200, volume: 42.4 };
|
||||
|
||||
test('formatTime formatiert Sekunden als m:ss', () => {
|
||||
assert.equal(formatTime(65), '1:05');
|
||||
assert.equal(formatTime(0), '0:00');
|
||||
assert.equal(formatTime(NaN), '0:00');
|
||||
assert.equal(formatTime(-3), '0:00');
|
||||
});
|
||||
|
||||
test('SVG enthaelt Titel (XML-escaped) und Interpret, quadratisch', () => {
|
||||
const svg = buildInfoSvg(VIEW);
|
||||
assert.match(svg, /^<svg /);
|
||||
assert.ok(svg.includes('Tom &'));
|
||||
assert.ok(svg.includes('<Live>'));
|
||||
assert.ok(svg.includes('Interpret'));
|
||||
assert.ok(!svg.includes('<Live>'));
|
||||
assert.ok(svg.includes('width="144" height="144"'));
|
||||
});
|
||||
|
||||
test('Cover wird eingebettet, sonst Platzhalter', () => {
|
||||
assert.ok(buildInfoSvg({ ...VIEW, coverDataUri: 'data:image/jpeg;base64,AAAA' }).includes('href="data:image/jpeg;base64,AAAA"'));
|
||||
assert.ok(!buildInfoSvg(VIEW).includes('<image'));
|
||||
});
|
||||
|
||||
test('Fortschrittsbalken ist auf 0..100% begrenzt', () => {
|
||||
assert.ok(buildInfoSvg({ ...VIEW, currentTime: 999 }).includes('width="124.0"'));
|
||||
assert.ok(buildInfoSvg({ ...VIEW, duration: 0 }).includes('width="0.0"'));
|
||||
});
|
||||
|
||||
test('offline und "nichts spielt" haben eigene Darstellungen', () => {
|
||||
assert.ok(buildInfoSvg(null).includes('offline'));
|
||||
assert.ok(buildInfoSvg({ ...VIEW, title: '' }).includes('spielt'));
|
||||
});
|
||||
|
||||
test('svgToDataUri erzeugt eine dekodierbare Data-URI', () => {
|
||||
const uri = svgToDataUri('<svg/>');
|
||||
assert.equal(Buffer.from(uri.split(',')[1], 'base64').toString(), '<svg/>');
|
||||
});
|
||||
|
||||
test('Controller: nowplaying auf dem Info-Display sendet ein SVG-Bild', () => {
|
||||
const calls = [];
|
||||
const host = new EventEmitter();
|
||||
host.setTitle = (...a) => calls.push(['setTitle', ...a]);
|
||||
host.setImage = (...a) => calls.push(['setImage', ...a]);
|
||||
host.setState = () => {};
|
||||
const client = new EventEmitter();
|
||||
client.connected = true;
|
||||
client.status = async () => ({ activeTab: 'music', tabs: { music: { ...VIEW } } });
|
||||
const controller = new Controller({
|
||||
host, client, logger: { info() {}, warn() {}, error() {} }, fetchImage: async () => 'data:image/jpeg;base64,AA',
|
||||
});
|
||||
controller.handleEvent({ event: 'willAppear', action: `${PREFIX}nowplaying`, context: 'INFO',
|
||||
payload: { controller: 'Information', settings: {} } });
|
||||
controller.state = { activeTab: 'music', tabs: { music: { ...VIEW } } };
|
||||
controller.renderAll();
|
||||
const image = calls.filter((c) => c[0] === 'setImage').at(-1);
|
||||
assert.match(image[2], /^data:image\/svg\+xml;base64,/);
|
||||
const before = calls.length;
|
||||
controller.renderAll();
|
||||
assert.equal(calls.length, before, 'unveraendert sendet nichts erneut');
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const net = require('node:net');
|
||||
const { PlaytubeClient, PlaytubeError, pipePath } = require('../src/playtubeClient');
|
||||
|
||||
const uniqueName = () => `Test${process.pid}${Date.now()}${Math.floor(Math.random() * 1e6)}`;
|
||||
const until = async (condition, timeoutMs = 3000) => {
|
||||
const start = Date.now();
|
||||
while (!condition()) {
|
||||
if (Date.now() - start > timeoutMs) throw new Error('Zeitueberschreitung im Test');
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
};
|
||||
|
||||
/** Fake-Playtube: beantwortet zeilenweise JSON-Befehle wie playtube/remote_control.py. */
|
||||
function startServer(name, handler) {
|
||||
const sockets = new Set();
|
||||
const server = net.createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.setEncoding('utf8');
|
||||
let buffer = '';
|
||||
socket.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
let index;
|
||||
while ((index = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, index);
|
||||
buffer = buffer.slice(index + 1);
|
||||
const request = JSON.parse(line);
|
||||
const reply = handler(request);
|
||||
if (reply) socket.write(`${JSON.stringify({ id: request.id, ...reply })}\n`);
|
||||
}
|
||||
});
|
||||
socket.on('close', () => sockets.delete(socket));
|
||||
socket.on('error', () => {});
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
server.listen(pipePath(name), () => resolve({
|
||||
dropClients: () => sockets.forEach((socket) => socket.destroy()),
|
||||
close: () => new Promise((done) => { sockets.forEach((s) => s.destroy()); server.close(done); }),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
test('pipePath bildet den Windows-Pipe-Namen', () => {
|
||||
assert.equal(pipePath('Playtube', 'win32'), '\\\\.\\pipe\\Playtube.Remote.Playtube');
|
||||
});
|
||||
|
||||
test('verbindet sich, sendet Befehle und liefert Antworten', async () => {
|
||||
const name = uniqueName();
|
||||
const fake = await startServer(name, (request) => {
|
||||
if (request.cmd === 'status') return { ok: true, state: { activeTab: 'music' } };
|
||||
if (request.cmd === 'list_playlists') return { ok: true, playlists: [{ id: 'LM', title: 'Liked' }] };
|
||||
if (request.cmd === 'boom') return { ok: false, error: 'kaputt' };
|
||||
return { ok: true, echo: request };
|
||||
});
|
||||
const client = new PlaytubeClient({ pipeNames: [name] });
|
||||
client.start();
|
||||
try {
|
||||
await until(() => client.connected);
|
||||
assert.deepEqual(await client.status(), { activeTab: 'music' });
|
||||
assert.deepEqual(await client.listPlaylists(), [{ id: 'LM', title: 'Liked' }]);
|
||||
const echoed = await client.request('volume_change', { target: 'auto', value: 5, skip: undefined });
|
||||
assert.deepEqual(echoed.echo, { id: echoed.id, cmd: 'volume_change', target: 'auto', value: 5 });
|
||||
await assert.rejects(client.request('boom'),
|
||||
(error) => error instanceof PlaytubeError && error.code === 'remote' && /kaputt/.test(error.message));
|
||||
} finally {
|
||||
client.stop();
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('ohne Verbindung wird sofort abgelehnt', async () => {
|
||||
const client = new PlaytubeClient({ pipeNames: [uniqueName()] });
|
||||
await assert.rejects(client.request('status'), (error) => error.code === 'not_connected');
|
||||
});
|
||||
|
||||
test('Timeout, wenn Playtube nicht antwortet', async () => {
|
||||
const name = uniqueName();
|
||||
const fake = await startServer(name, () => null);
|
||||
const client = new PlaytubeClient({ pipeNames: [name], requestTimeoutMs: 80 });
|
||||
client.start();
|
||||
try {
|
||||
await until(() => client.connected);
|
||||
await assert.rejects(client.request('status'), (error) => error.code === 'timeout');
|
||||
} finally {
|
||||
client.stop();
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Verbindungsabbruch: offene Anfragen scheitern, danach automatische Wiederverbindung', async () => {
|
||||
const name = uniqueName();
|
||||
const fake = await startServer(name, () => null);
|
||||
const client = new PlaytubeClient({ pipeNames: [name], backoffMs: [20] });
|
||||
const events = [];
|
||||
client.on('connected', () => events.push('connected'));
|
||||
client.on('disconnected', () => events.push('disconnected'));
|
||||
client.start();
|
||||
try {
|
||||
await until(() => client.connected);
|
||||
const pending = client.request('status');
|
||||
fake.dropClients();
|
||||
await assert.rejects(pending, (error) => error.code === 'disconnected');
|
||||
await until(() => events.filter((e) => e === 'connected').length === 2);
|
||||
assert.deepEqual(events, ['connected', 'disconnected', 'connected']);
|
||||
} finally {
|
||||
client.stop();
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('probiert mehrere Pipe-Namen der Reihe nach und verbindet Playtube spaeter', async () => {
|
||||
const missing = uniqueName();
|
||||
const present = uniqueName();
|
||||
const client = new PlaytubeClient({ pipeNames: [missing, present], backoffMs: [20] });
|
||||
client.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
assert.equal(client.connected, false);
|
||||
const fake = await startServer(present, () => ({ ok: true }));
|
||||
try {
|
||||
await until(() => client.connected);
|
||||
assert.equal(client.pipeName, present);
|
||||
} finally {
|
||||
client.stop();
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user