ok
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { PlayerStateClient } from "../public/player-state.js";
|
||||
|
||||
test("polls reuse tracks and accept edits from another browser", async () => {
|
||||
const tracks = [{ title: "First" }];
|
||||
const changedTracks = [{ title: "Second" }];
|
||||
const responses = [
|
||||
{ playlistVersion: "first", tracks, position: 0 },
|
||||
{ playlistVersion: "first", tracks: null, position: 1 },
|
||||
{ playlistVersion: "changed", tracks: changedTracks, position: 2 },
|
||||
{ playlistVersion: "changed", tracks: null, position: 3 },
|
||||
];
|
||||
const paths = [];
|
||||
const client = new PlayerStateClient(async (path) => {
|
||||
paths.push(path);
|
||||
return responses.shift();
|
||||
});
|
||||
assert.equal((await client.request("/api/state")).tracks, tracks);
|
||||
const unchanged = await client.request("/api/state");
|
||||
assert.equal(unchanged.tracks, tracks);
|
||||
assert.equal(unchanged.position, 1);
|
||||
assert.equal((await client.request("/api/state")).tracks, changedTracks);
|
||||
assert.equal((await client.request("/api/state")).tracks, changedTracks);
|
||||
assert.deepEqual(paths, [
|
||||
"/api/state", "/api/state?playlistVersion=first",
|
||||
"/api/state?playlistVersion=first", "/api/state?playlistVersion=changed",
|
||||
]);
|
||||
});
|
||||
|
||||
test("commands and discovery wait for polling and use its latest version", async () => {
|
||||
let finishPoll;
|
||||
const paths = [];
|
||||
const commandBody = { index: 1 };
|
||||
const client = new PlayerStateClient((path, body) => {
|
||||
paths.push(path);
|
||||
if (paths.length === 1) {
|
||||
return new Promise((resolve) => { finishPoll = resolve; });
|
||||
}
|
||||
if (paths.length === 2) {
|
||||
assert.equal(body, commandBody);
|
||||
return { playlistVersion: "other-tab", tracks: [] };
|
||||
}
|
||||
return { playlistVersion: "other-tab", tracks: null };
|
||||
});
|
||||
const poll = client.request("/api/state");
|
||||
const command = client.request("/api/command/tab-select", commandBody);
|
||||
const discovery = client.request("/api/discover", {});
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(paths, ["/api/state"]);
|
||||
finishPoll({ playlistVersion: "original-tab", tracks: [{ title: "First" }] });
|
||||
await poll;
|
||||
assert.deepEqual((await command).tracks, []);
|
||||
assert.deepEqual((await discovery).tracks, []);
|
||||
assert.deepEqual(paths, [
|
||||
"/api/state",
|
||||
"/api/command/tab-select?playlistVersion=original-tab",
|
||||
"/api/discover?playlistVersion=other-tab",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a failed request leaves the queue usable", async () => {
|
||||
let calls = 0;
|
||||
const client = new PlayerStateClient(async () => {
|
||||
if (++calls === 1) throw new Error("Offline");
|
||||
return { playlistVersion: "recovered", tracks: [] };
|
||||
});
|
||||
await assert.rejects(client.request("/api/state"), /Offline/);
|
||||
assert.deepEqual((await client.request("/api/state")).tracks, []);
|
||||
});
|
||||
|
||||
test("mismatched omitted tracks force a full refresh instead of stale reuse", async () => {
|
||||
const paths = [];
|
||||
const responses = [
|
||||
{ playlistVersion: "before", tracks: [{ title: "Before" }] },
|
||||
{ playlistVersion: "after", tracks: null },
|
||||
{ playlistVersion: "after", tracks: [] },
|
||||
];
|
||||
const client = new PlayerStateClient(async (path) => {
|
||||
paths.push(path);
|
||||
return responses.shift();
|
||||
});
|
||||
await client.request("/api/state");
|
||||
await assert.rejects(client.request("/api/state"), /Playlist cache/);
|
||||
assert.deepEqual((await client.request("/api/state")).tracks, []);
|
||||
assert.equal(paths[2], "/api/state");
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { PlaylistLibrary } from "../public/playlist-library.js";
|
||||
|
||||
// Minimal DOM elements let the view's actions run without a browser dependency.
|
||||
class Element {
|
||||
constructor() {
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this.listeners = {};
|
||||
this.classes = new Set();
|
||||
this.classList = { toggle: (name, active) => active ? this.classes.add(name) : this.classes.delete(name) };
|
||||
}
|
||||
append(...children) { this.children.push(...children); }
|
||||
replaceChildren(...children) { this.children = children; }
|
||||
setAttribute(name, value) { this.attributes[name] = value; }
|
||||
addEventListener(name, callback) { this.listeners[name] = callback; }
|
||||
fire(name, values = {}) { this.listeners[name]({ target: this, stopPropagation() {}, preventDefault() {}, ...values }); }
|
||||
focus() {}
|
||||
closest() { return null; }
|
||||
}
|
||||
|
||||
class Fixture {
|
||||
constructor() {
|
||||
this.nodes = new Map();
|
||||
this.commands = [];
|
||||
this.language = "en";
|
||||
globalThis.window = { RktTranslate: {
|
||||
language: () => this.language,
|
||||
t: (key, values = {}) => `${key} ${values.name || ""}`,
|
||||
} };
|
||||
globalThis.document = { createElement: () => new Element() };
|
||||
this.view = new PlaylistLibrary({ querySelector: (selector) => this.node(selector) },
|
||||
(name, data) => this.commands.push({ name, data }), duration => `${duration} seconds`);
|
||||
}
|
||||
node(selector) {
|
||||
if (!this.nodes.has(selector)) this.nodes.set(selector, new Element());
|
||||
return this.nodes.get(selector);
|
||||
}
|
||||
}
|
||||
|
||||
test("library tabs change only the visible panel", () => {
|
||||
const fixture = new Fixture();
|
||||
fixture.node("#saved-playlists-tab").fire("click");
|
||||
assert.equal(fixture.node("#folders-panel").hidden, true);
|
||||
assert.equal(fixture.node("#saved-playlists-panel").hidden, false);
|
||||
assert.equal(fixture.node("#saved-playlists-tab").attributes["aria-selected"], "true");
|
||||
fixture.node("#saved-playlists-tab").fire("keydown", { key: "ArrowLeft" });
|
||||
assert.equal(fixture.node("#folders-panel").hidden, false);
|
||||
assert.deepEqual(fixture.commands, []);
|
||||
});
|
||||
|
||||
test("plus and play address the saved playlist UUID and never use item-add/play", () => {
|
||||
const fixture = new Fixture();
|
||||
fixture.view.render([{ id: "saved-id", name: "Music", count: 2, duration: 120 }]);
|
||||
const row = fixture.node("#saved-playlists").children[0];
|
||||
row.fire("click");
|
||||
assert.deepEqual(fixture.commands, []);
|
||||
assert.ok(row.classes.has("selected"));
|
||||
row.children[2].children[1].fire("click");
|
||||
row.children[2].children[0].fire("click");
|
||||
assert.deepEqual(fixture.commands, [
|
||||
{ name: "playlist-open", data: { id: "saved-id" } },
|
||||
{ name: "playlist-play", data: { id: "saved-id" } },
|
||||
]);
|
||||
assert.equal(fixture.node("#saved-playlists-empty").hidden, true);
|
||||
});
|
||||
|
||||
test("unchanged summaries reuse rows while metadata and language changes refresh them", () => {
|
||||
const fixture = new Fixture();
|
||||
const playlists = [{ id: "one", name: "Music", count: 0, duration: 0 }];
|
||||
fixture.view.render(playlists);
|
||||
const row = fixture.node("#saved-playlists").children[0];
|
||||
assert.equal(row.children[2].children[0].disabled, true);
|
||||
assert.equal(row.children[2].children[1].disabled, false);
|
||||
fixture.view.render(playlists);
|
||||
assert.equal(fixture.node("#saved-playlists").children[0], row);
|
||||
fixture.language = "nl";
|
||||
fixture.view.render(playlists);
|
||||
assert.notEqual(fixture.node("#saved-playlists").children[0], row);
|
||||
fixture.view.render([{ ...playlists[0], name: "Renamed" }]);
|
||||
assert.equal(fixture.node("#saved-playlists").children[0].children[1].textContent, "Renamed");
|
||||
fixture.view.render([]);
|
||||
assert.equal(fixture.node("#saved-playlists-empty").hidden, false);
|
||||
});
|
||||
|
||||
test("keyboard opening does not intercept a nested action button", () => {
|
||||
const fixture = new Fixture();
|
||||
fixture.view.render([{ id: "one", name: "Music", count: 1, duration: 60 }]);
|
||||
const row = fixture.node("#saved-playlists").children[0];
|
||||
row.fire("keydown", { key: "Enter", target: row.children[2].children[0] });
|
||||
assert.deepEqual(fixture.commands, []);
|
||||
row.fire("keydown", { key: "Enter" });
|
||||
assert.deepEqual(fixture.commands, [{ name: "playlist-open", data: { id: "one" } }]);
|
||||
});
|
||||
Reference in New Issue
Block a user