Files

205 lines
7.9 KiB
JavaScript

'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);
});