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" } }]); });