mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-26 06:42:32 +00:00
* fix(library): Edit Metadata modal — editable Year + no close on drag-release Two fixes to the Songs -> Edit Metadata modal (openEditModal/saveEditModal in static/app.js), both reported on macOS for 0.3.0. 1) Year is now editable. A year can be set when authoring a pak but the modal had no Year field, so it could never be changed. The backend (POST /api/song/<f>/meta) already accepts + normalizes `year` and writes it into the file via songmeta (survives a rescan) -- only the UI omitted it. Add a Year input (populated from the song's current year) and include `year` in the save POST body. Both the v3 card menu and the legacy edit button already pass the year through, so both surfaces get the field. 2) The modal no longer closes when a click-drag is released on the backdrop. Selecting text inside a field and releasing the mouse past the modal edge dismissed the form without warning (the `click` event's target resolves to the backdrop, the common ancestor) -- discarding the edit. Backdrop dismissal now also requires the mousedown to have STARTED on the backdrop, tracked per-modal and decided by a new pure helper _editModalShouldClose(clickTarget, modalEl, downOnBackdrop). Cancel / X still close on a normal click. Tests: tests/js/edit_metadata_modal.test.js extracts the real functions from app.js and asserts (a) openEditModal renders #edit-year, (b) saveEditModal's meta POST body carries `year`, and (c) the backdrop-close decision table (Cancel always closes; backdrop needs down+up on the backdrop; a drag from a field released on the backdrop does NOT close). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(library): wire Edit Metadata Save via listener, not an inline onclick encodeURIComponent does not escape "'", so embedding the filename in the single-quoted inline onclick="saveEditModal('…')" handler produced a malformed handler for any song whose filename contains an apostrophe (e.g. Bob's Song.sloppak) — clicking Save threw a syntax error and the edit silently failed. Replace the inline onclick with a data-edit-save hook wired in JS from the closure filename (mirrors the existing Delete button pattern), so the filename never has to survive attribute-string embedding. Pre-existing bug surfaced during review of this modal. Adds a regression assertion (no inline saveEditModal onclick; Save wired via data-edit-save). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
112 lines
5.4 KiB
JavaScript
112 lines
5.4 KiB
JavaScript
// Regression guards for two Edit-Metadata modal fixes (static/app.js):
|
|
//
|
|
// 1. Year is editable — the modal renders an `edit-year` field and
|
|
// saveEditModal() includes `year` in the POST /api/song/<f>/meta body.
|
|
// (Backend already accepts/normalizes year; only the UI omitted it.)
|
|
//
|
|
// 2. A click-drag that starts inside a field and is released on the backdrop
|
|
// must NOT dismiss the modal. _editModalShouldClose() gates backdrop
|
|
// dismissal on the mousedown having started on the backdrop too.
|
|
//
|
|
// Functions are extracted from the real shipped source and run in a vm — no
|
|
// mirror copies.
|
|
|
|
'use strict';
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const { extractFunction } = require('./test_utils');
|
|
|
|
const APP_JS = path.join(__dirname, '..', '..', 'static', 'app.js');
|
|
const readApp = () => fs.readFileSync(APP_JS, 'utf8');
|
|
|
|
function loadFn(signature, sandbox, exportAs) {
|
|
const fnSrc = extractFunction(readApp(), signature);
|
|
const ctx = vm.createContext(sandbox);
|
|
vm.runInContext(`${fnSrc}\nglobalThis.${exportAs} = ${exportAs};`, ctx);
|
|
return sandbox[exportAs];
|
|
}
|
|
|
|
// ── Issue: Edit Metadata does not allow changing Year ────────────────────────
|
|
|
|
test('openEditModal renders a Year field bound to songData.y', () => {
|
|
const src = extractFunction(readApp(), 'function openEditModal');
|
|
assert.match(src, /id="edit-year"/, 'modal must render an #edit-year input');
|
|
assert.match(src, /_escAttr\(songData\.y\)/, 'year input must be populated from songData.y');
|
|
});
|
|
|
|
test('Save button wires via data-edit-save, not an inline onclick that embeds the filename', () => {
|
|
// encodeURIComponent does NOT escape `'`, so embedding the filename in a
|
|
// single-quoted inline `saveEditModal('…')` handler breaks the save for a
|
|
// song whose filename contains an apostrophe (e.g. `Bob's Song.sloppak`).
|
|
// The Save button must use the data-attr + JS-listener pattern instead.
|
|
const src = extractFunction(readApp(), 'function openEditModal');
|
|
assert.doesNotMatch(src, /onclick="saveEditModal\('/, 'Save must not embed the filename in an inline onclick');
|
|
assert.match(src, /data-edit-save/, 'Save button must carry the data-edit-save hook');
|
|
assert.match(src, /querySelector\('\[data-edit-save\]'\)/, 'Save must be wired via addEventListener');
|
|
});
|
|
|
|
test('saveEditModal includes year in the metadata POST body', async () => {
|
|
const calls = [];
|
|
const values = {
|
|
'edit-title': 'My Title', 'edit-artist': 'My Artist',
|
|
'edit-album': 'My Album', 'edit-year': '1998',
|
|
'edit-art-file': null, // signals the file branch via .files below
|
|
'edit-modal': null,
|
|
};
|
|
const sandbox = {
|
|
decodeURIComponent, encodeURIComponent, JSON, Promise,
|
|
_lastLibSelected: null,
|
|
loadLibrary: () => {}, loadFavorites: () => {},
|
|
fetch: (url, opts) => { calls.push({ url, opts }); return Promise.resolve({ ok: true }); },
|
|
document: {
|
|
getElementById: (id) => {
|
|
if (id === 'edit-art-file') return { files: null };
|
|
if (id === 'edit-modal') return null;
|
|
return id in values ? { value: values[id] } : null;
|
|
},
|
|
querySelector: () => null, // no active screen
|
|
body: { contains: () => false },
|
|
},
|
|
};
|
|
const saveEditModal = loadFn('async function saveEditModal', sandbox, 'saveEditModal');
|
|
|
|
await saveEditModal(encodeURIComponent('Song With Spaces.sloppak'));
|
|
|
|
const metaCall = calls.find((c) => /\/api\/song\/.+\/meta$/.test(c.url));
|
|
assert.ok(metaCall, 'expected a POST to /api/song/<filename>/meta');
|
|
const body = JSON.parse(metaCall.opts.body);
|
|
assert.equal(body.year, '1998', 'meta POST body must carry the edited year');
|
|
assert.deepEqual(
|
|
body,
|
|
{ title: 'My Title', artist: 'My Artist', album: 'My Album', year: '1998' },
|
|
'meta POST body shape',
|
|
);
|
|
});
|
|
|
|
// ── Issue: Renaming Metadata Closes Modal (click-drag release on backdrop) ────
|
|
|
|
test('_editModalShouldClose: backdrop needs mousedown to have started there', () => {
|
|
const fn = loadFn('function _editModalShouldClose', {}, '_editModalShouldClose');
|
|
|
|
const modalEl = { closest: () => null }; // the backdrop element
|
|
const innerEl = { closest: () => null }; // a field inside the modal
|
|
const cancelBtn = { closest: (s) => (s === '[data-edit-close]' ? { tag: 'button' } : null) };
|
|
|
|
// Cancel / ✕ always closes, regardless of where the mousedown began.
|
|
assert.equal(fn(cancelBtn, modalEl, false), true, 'Cancel/✕ closes');
|
|
assert.equal(fn(cancelBtn, modalEl, true), true, 'Cancel/✕ closes (down-on-backdrop irrelevant)');
|
|
|
|
// Genuine backdrop click: down AND up on the backdrop.
|
|
assert.equal(fn(modalEl, modalEl, true), true, 'backdrop down+up closes');
|
|
|
|
// The reported bug: drag began inside a field (down NOT on backdrop), click
|
|
// resolves to the backdrop on release — must NOT close.
|
|
assert.equal(fn(modalEl, modalEl, false), false, 'drag-from-field release on backdrop does NOT close');
|
|
|
|
// A click that lands on inner content never closes via the backdrop path.
|
|
assert.equal(fn(innerEl, modalEl, true), false, 'click on inner content does not close');
|
|
});
|