205 lines
7.9 KiB
JavaScript
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);
|
|
});
|