feat: add device select dropdown to showNewSessionInput
- Add _createDeviceSelect() helper function after _createSessionInput() - Returns null when multi_device_enabled is false or remote_instances is empty - Creates <select class='new-session-device-select'> with local + remote options - Pre-selects based on _activeFilterDevice match - Update showNewSessionInput() to call _createDeviceSelect() and insert select - cleanup() removes both select and input; restores btn display - Enter handler reads remoteId from select.value, calls createNewSession(name, remoteId) - Export _createDeviceSelect for testing - Add 3 tests covering select creation, integration, and argument passing
This commit is contained in:
+46
-4
@@ -1910,17 +1910,56 @@ function _createSessionInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the header + button with an inline text input for session naming.
|
* Create an optional device <select> for multi-device session creation.
|
||||||
* Hides the button, inserts the input before it, and focuses it.
|
* Returns null when multi_device_enabled is false or remote_instances is empty.
|
||||||
* On Enter: if name is non-empty after trim, calls createNewSession(name).
|
* @returns {HTMLSelectElement|null}
|
||||||
|
*/
|
||||||
|
function _createDeviceSelect() {
|
||||||
|
const ss = _serverSettings || {};
|
||||||
|
const remotes = ss.remote_instances;
|
||||||
|
if (!ss.multi_device_enabled || !remotes || remotes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.className = 'new-session-device-select';
|
||||||
|
|
||||||
|
// Local device option
|
||||||
|
const localOpt = document.createElement('option');
|
||||||
|
localOpt.value = '';
|
||||||
|
localOpt.textContent = ss.device_name || 'Local';
|
||||||
|
select.appendChild(localOpt);
|
||||||
|
|
||||||
|
// Remote instance options
|
||||||
|
for (var i = 0; i < remotes.length; i++) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = String(i);
|
||||||
|
opt.textContent = remotes[i].name || remotes[i].url || 'Remote ' + i;
|
||||||
|
if (_activeFilterDevice === remotes[i].name || _activeFilterDevice === remotes[i].url) {
|
||||||
|
opt.selected = true;
|
||||||
|
select.value = String(i);
|
||||||
|
}
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return select;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the header + button with an inline text input (and optional device
|
||||||
|
* select) for session naming. Hides the button, inserts controls before it,
|
||||||
|
* and focuses the input.
|
||||||
|
* On Enter: if name is non-empty after trim, calls createNewSession(name, remoteId).
|
||||||
* On Escape: restores the button (cleanup only).
|
* On Escape: restores the button (cleanup only).
|
||||||
* On blur: delayed cleanup (150ms) to allow click handlers.
|
* On blur: delayed cleanup (150ms) to allow click handlers.
|
||||||
* @param {HTMLElement} btn - The button element to replace temporarily.
|
* @param {HTMLElement} btn - The button element to replace temporarily.
|
||||||
*/
|
*/
|
||||||
function showNewSessionInput(btn) {
|
function showNewSessionInput(btn) {
|
||||||
|
const select = _createDeviceSelect();
|
||||||
const input = _createSessionInput();
|
const input = _createSessionInput();
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
|
if (select && select.parentNode) select.parentNode.removeChild(select);
|
||||||
if (input.parentNode) input.parentNode.removeChild(input);
|
if (input.parentNode) input.parentNode.removeChild(input);
|
||||||
btn.style.display = '';
|
btn.style.display = '';
|
||||||
}
|
}
|
||||||
@@ -1928,8 +1967,9 @@ function showNewSessionInput(btn) {
|
|||||||
input.addEventListener('keydown', function (e) {
|
input.addEventListener('keydown', function (e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const name = input.value.trim();
|
const name = input.value.trim();
|
||||||
|
const remoteId = select ? select.value : '';
|
||||||
cleanup();
|
cleanup();
|
||||||
if (name) createNewSession(name);
|
if (name) createNewSession(name, remoteId);
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
@@ -1940,6 +1980,7 @@ function showNewSessionInput(btn) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
btn.style.display = 'none';
|
btn.style.display = 'none';
|
||||||
|
if (select) btn.parentNode.insertBefore(select, btn);
|
||||||
btn.parentNode.insertBefore(input, btn);
|
btn.parentNode.insertBefore(input, btn);
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
@@ -2483,6 +2524,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
// Fetch wrapper
|
// Fetch wrapper
|
||||||
api,
|
api,
|
||||||
// Header + button with inline name input
|
// Header + button with inline name input
|
||||||
|
_createDeviceSelect,
|
||||||
showNewSessionInput,
|
showNewSessionInput,
|
||||||
showFabSessionInput,
|
showFabSessionInput,
|
||||||
createNewSession,
|
createNewSession,
|
||||||
|
|||||||
@@ -4103,3 +4103,111 @@ test('updatePageTitle is called from pollSessions', () => {
|
|||||||
const pollFn = source.substring(source.indexOf('async function pollSessions'), source.indexOf('async function pollSessions') + 700);
|
const pollFn = source.substring(source.indexOf('async function pollSessions'), source.indexOf('async function pollSessions') + 700);
|
||||||
assert.ok(pollFn.includes('updatePageTitle'), 'pollSessions must call updatePageTitle');
|
assert.ok(pollFn.includes('updatePageTitle'), 'pollSessions must call updatePageTitle');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- _createDeviceSelect and showNewSessionInput multi-device tests ---
|
||||||
|
|
||||||
|
test('_createDeviceSelect builds a <select> with Local + remote options', () => {
|
||||||
|
// Verify function is exported
|
||||||
|
assert.strictEqual(typeof app._createDeviceSelect, 'function', '_createDeviceSelect must be exported');
|
||||||
|
|
||||||
|
// Set up server settings with multi_device_enabled + remote_instances + device_name
|
||||||
|
app._setServerSettings({
|
||||||
|
multi_device_enabled: true,
|
||||||
|
remote_instances: [{ name: 'Remote A', url: 'http://a' }],
|
||||||
|
device_name: 'MyDevice',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock document.createElement to capture created elements
|
||||||
|
const origCE = globalThis.document.createElement;
|
||||||
|
const builtOptions = [];
|
||||||
|
let selectEl = null;
|
||||||
|
|
||||||
|
globalThis.document.createElement = (tag) => {
|
||||||
|
if (tag === 'select') {
|
||||||
|
selectEl = {
|
||||||
|
tagName: 'SELECT',
|
||||||
|
className: '',
|
||||||
|
value: '',
|
||||||
|
options: builtOptions,
|
||||||
|
appendChild: (child) => { builtOptions.push(child); },
|
||||||
|
addEventListener: () => {},
|
||||||
|
};
|
||||||
|
return selectEl;
|
||||||
|
}
|
||||||
|
// option elements
|
||||||
|
return { tagName: tag.toUpperCase(), value: '', textContent: '', selected: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = app._createDeviceSelect();
|
||||||
|
globalThis.document.createElement = origCE;
|
||||||
|
|
||||||
|
assert.ok(result !== null, '_createDeviceSelect must return non-null when multi_device_enabled + remotes present');
|
||||||
|
assert.strictEqual(result.className, 'new-session-device-select', 'select must have className new-session-device-select');
|
||||||
|
assert.strictEqual(builtOptions.length, 2, 'must have 2 options: local + 1 remote');
|
||||||
|
assert.strictEqual(builtOptions[0].value, '', 'first option value must be empty string (local)');
|
||||||
|
assert.strictEqual(builtOptions[0].textContent, 'MyDevice', 'first option text must use device_name');
|
||||||
|
assert.strictEqual(builtOptions[1].value, '0', 'remote option value must be "0" (String(index))');
|
||||||
|
assert.strictEqual(builtOptions[1].textContent, 'Remote A', 'remote option text must use remote.name');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('showNewSessionInput creates device select when multi_device_enabled with remotes', () => {
|
||||||
|
app._setServerSettings({
|
||||||
|
multi_device_enabled: true,
|
||||||
|
remote_instances: [{ name: 'Remote B', url: 'http://b' }],
|
||||||
|
device_name: 'LocalDev',
|
||||||
|
});
|
||||||
|
|
||||||
|
const origCE = globalThis.document.createElement;
|
||||||
|
const createdTags = [];
|
||||||
|
const insertedEls = [];
|
||||||
|
|
||||||
|
globalThis.document.createElement = (tag) => {
|
||||||
|
createdTags.push(tag);
|
||||||
|
return {
|
||||||
|
tagName: tag.toUpperCase(),
|
||||||
|
className: '',
|
||||||
|
type: '',
|
||||||
|
placeholder: '',
|
||||||
|
autocomplete: '',
|
||||||
|
spellcheck: false,
|
||||||
|
value: '',
|
||||||
|
style: {},
|
||||||
|
options: [],
|
||||||
|
appendChild: () => {},
|
||||||
|
addEventListener: () => {},
|
||||||
|
focus: () => {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const btn = {
|
||||||
|
style: {},
|
||||||
|
parentNode: {
|
||||||
|
insertBefore: (el) => { insertedEls.push(el); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
app.showNewSessionInput(btn);
|
||||||
|
globalThis.document.createElement = origCE;
|
||||||
|
|
||||||
|
assert.ok(createdTags.includes('select'), 'showNewSessionInput must create a <select> element when multi_device_enabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('showNewSessionInput passes remoteId from device select to createNewSession', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
// Extract showNewSessionInput function body
|
||||||
|
const fnStart = source.indexOf('function showNewSessionInput(');
|
||||||
|
assert.ok(fnStart !== -1, 'showNewSessionInput function must exist');
|
||||||
|
const fnBody = source.substring(fnStart, fnStart + 1200);
|
||||||
|
// Must call _createDeviceSelect
|
||||||
|
assert.ok(fnBody.includes('_createDeviceSelect'), 'showNewSessionInput must call _createDeviceSelect');
|
||||||
|
// Must read remoteId from select.value (or equivalent)
|
||||||
|
assert.ok(
|
||||||
|
fnBody.includes('remoteId') && (fnBody.includes('select.value') || fnBody.includes('sel.value')),
|
||||||
|
'showNewSessionInput Enter handler must read remoteId from select element value',
|
||||||
|
);
|
||||||
|
// Must call createNewSession with two arguments (name and remoteId)
|
||||||
|
assert.ok(
|
||||||
|
fnBody.includes('createNewSession(name') && fnBody.includes('remoteId'),
|
||||||
|
'showNewSessionInput must call createNewSession with name and remoteId arguments',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user