fix(ui): suppress empty device status tiles in all view modes (not just grouped)
v0.6.4 added `&& _gridViewMode !== 'grouped'` to the status:empty branch in
renderGrid, suppressing the "No sessions" tile only in grouped grid view.
User reported the alienware-r13 "No sessions" tile still appearing in flat view
(gridViewMode: "flat"), which is the muxplex default. The original request was
unambiguous: "don't need a block for those that don't have anything to show" —
no view-mode qualifier was implied.
Changes:
- Both renderGrid call sites (early-return branch for visible.length === 0, and
the normal rendering branch) now simply omit the status:empty else-if entirely.
The status:empty sentinel may still arrive from the server; the renderer silently
ignores it in every view mode.
- Updated surrounding comments to reflect the new unconditional suppression.
- Updated/replaced tests that asserted the old (wrong) flat-mode behaviour:
- Replaced 'renderGrid shows "No sessions" status tile for status=empty devices'
with a v0.6.5 variant that asserts NO tile is emitted.
- Removed 'v0.6.4: status:empty block IS still rendered in flat grid mode'
(superseded; kept as a tombstone comment).
- Added three new tests under 'v0.6.5 empty tile suppressed in all view modes':
1. Flat view: empty sentinel → no source-tile--empty, no 'No sessions' text.
2. Grouped view: same (regression guard for the v0.6.4 suppression).
3. Mixed input: real session renders, empty sentinel is discarded, both modes.
No other rendering paths that produce a per-device block for empty devices were
found: renderSidebar and renderGroupedGrid both operate on getVisibleSessions()
output which already excludes status-bearing sentinels.
Test count: 402 (v0.6.4) → 404 (v0.6.5).
This commit is contained in:
@@ -1659,13 +1659,12 @@ function renderGrid(sessions) {
|
|||||||
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
// Build status tiles for auth_failed/unreachable sessions even when no regular sessions exist.
|
// Build status tiles for auth_failed/unreachable sessions even when no regular sessions exist.
|
||||||
// In grouped grid mode, skip status:empty tiles — empty devices are silently omitted so that
|
// status:empty sentinels are intentionally ignored — a remote with zero tmux sessions
|
||||||
// a remote with zero tmux sessions doesn't produce a visible device block in the grouped view.
|
// produces no visible tile in any view mode (flat, grouped, or otherwise).
|
||||||
var statusTilesHtml = '';
|
var statusTilesHtml = '';
|
||||||
(sessions || []).forEach(function(session) {
|
(sessions || []).forEach(function(session) {
|
||||||
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
||||||
else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
||||||
else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
|
||||||
});
|
});
|
||||||
if (grid) grid.innerHTML = statusTilesHtml;
|
if (grid) grid.innerHTML = statusTilesHtml;
|
||||||
// Only show empty-state when there are truly no tiles at all
|
// Only show empty-state when there are truly no tiles at all
|
||||||
@@ -1697,15 +1696,13 @@ function renderGrid(sessions) {
|
|||||||
html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join('');
|
html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append status tiles for auth_failed, unreachable, and empty sessions.
|
// Append status tiles for auth_failed and unreachable sessions. status:empty sentinels are
|
||||||
// In grouped grid mode, skip status:empty tiles — a remote with zero tmux sessions is simply
|
// intentionally ignored in all view modes — a remote with zero tmux sessions produces no
|
||||||
// omitted from the grouped view. auth_failed and unreachable are always shown because those
|
// visible tile. auth_failed and unreachable are actionable error states and are always shown.
|
||||||
// are actionable error states regardless of view mode.
|
|
||||||
var statusTilesHtml = '';
|
var statusTilesHtml = '';
|
||||||
(sessions || []).forEach(function(session) {
|
(sessions || []).forEach(function(session) {
|
||||||
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
if (session.status === 'auth_failed') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Auth required', 'auth');
|
||||||
else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
else if (session.status === 'unreachable') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'Offline', 'offline');
|
||||||
else if (session.status === 'empty' && _gridViewMode !== 'grouped') statusTilesHtml += buildStatusTileHTML(session.deviceName, 'No sessions', 'empty');
|
|
||||||
});
|
});
|
||||||
if (grid) grid.innerHTML = html + statusTilesHtml;
|
if (grid) grid.innerHTML = html + statusTilesHtml;
|
||||||
|
|
||||||
|
|||||||
@@ -4829,13 +4829,14 @@ test('renderGrid status tiles use session.deviceName not session.name for offlin
|
|||||||
|
|
||||||
// --- renderGrid: status=empty shows "No sessions" tile ---
|
// --- renderGrid: status=empty shows "No sessions" tile ---
|
||||||
|
|
||||||
test('renderGrid shows "No sessions" status tile for status=empty devices', () => {
|
test('renderGrid silently drops status=empty devices (no tile emitted) [v0.6.5]', () => {
|
||||||
// A device that is online but has zero tmux sessions returns
|
// v0.6.5: a device that is online but has zero tmux sessions returns
|
||||||
// {status: 'empty', deviceName: '...'} from the federation endpoint.
|
// {status: 'empty', deviceName: '...'} from the federation endpoint.
|
||||||
// renderGrid must render a status tile with the text "No sessions" (not blank).
|
// renderGrid must NOT render any tile for it — the user asked not to see
|
||||||
|
// blocks for devices that have nothing to show (flat OR grouped mode).
|
||||||
//
|
//
|
||||||
// Before implementation: fails because neither status loop handles status === 'empty',
|
// Previously (pre-v0.6.5) a "No sessions" status tile was emitted; that
|
||||||
// so the tile is never rendered and grid.innerHTML stays empty.
|
// behaviour is intentionally removed.
|
||||||
const grid = { innerHTML: '' };
|
const grid = { innerHTML: '' };
|
||||||
const emptyState = { style: {}, classList: { add() {}, remove() {} } };
|
const emptyState = { style: {}, classList: { add() {}, remove() {} } };
|
||||||
const origGetById = globalThis.document.getElementById;
|
const origGetById = globalThis.document.getElementById;
|
||||||
@@ -4848,12 +4849,16 @@ test('renderGrid shows "No sessions" status tile for status=empty devices', () =
|
|||||||
app.renderGrid([{ status: 'empty', deviceName: 'quiet-box', remoteId: 3 }]);
|
app.renderGrid([{ status: 'empty', deviceName: 'quiet-box', remoteId: 3 }]);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
grid.innerHTML.includes('No sessions'),
|
!grid.innerHTML.includes('No sessions'),
|
||||||
`renderGrid must include "No sessions" text for status=empty device, got: ${grid.innerHTML}`
|
`renderGrid must NOT include "No sessions" text for status=empty device, got: ${grid.innerHTML}`
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
grid.innerHTML.includes('quiet-box'),
|
!grid.innerHTML.includes('quiet-box'),
|
||||||
`renderGrid must include the deviceName "quiet-box" in the status tile, got: ${grid.innerHTML}`
|
`renderGrid must NOT include the deviceName "quiet-box" for a status=empty device, got: ${grid.innerHTML}`
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!grid.innerHTML.includes('source-tile--empty'),
|
||||||
|
`renderGrid must NOT emit source-tile--empty class for status=empty device, got: ${grid.innerHTML}`
|
||||||
);
|
);
|
||||||
|
|
||||||
globalThis.document.getElementById = origGetById;
|
globalThis.document.getElementById = origGetById;
|
||||||
@@ -5635,46 +5640,9 @@ test('v0.6.4: status:empty block is NOT rendered in grouped grid mode for a remo
|
|||||||
app._setActiveView('all');
|
app._setActiveView('all');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('v0.6.4: status:empty block IS still rendered in flat grid mode', () => {
|
// NOTE: v0.6.4 tested that status:empty blocks appeared in flat mode. That behaviour was
|
||||||
// In flat mode the "No sessions" tile for an empty remote is intentional — only
|
// wrong and is superseded by v0.6.5, which silently drops status:empty in ALL view modes.
|
||||||
// the grouped view should suppress it.
|
// See the v0.6.5 section below for the authoritative tests.
|
||||||
const sessions = [
|
|
||||||
{ name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' },
|
|
||||||
{ status: 'empty', deviceName: 'alienware-r13', deviceId: 'aw-uuid', remoteId: 'aw-uuid' },
|
|
||||||
];
|
|
||||||
app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] });
|
|
||||||
app._setGridViewMode('flat');
|
|
||||||
app._setActiveView('all');
|
|
||||||
|
|
||||||
let capturedHTML = '';
|
|
||||||
const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } };
|
|
||||||
const mockEmpty = { classList: { add: () => {}, remove: () => {} } };
|
|
||||||
const origGetById = globalThis.document.getElementById;
|
|
||||||
const origQSA = globalThis.document.querySelectorAll;
|
|
||||||
globalThis.document.getElementById = (id) => {
|
|
||||||
if (id === 'session-grid') return mockGrid;
|
|
||||||
if (id === 'empty-state') return mockEmpty;
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
globalThis.document.querySelectorAll = () => [];
|
|
||||||
|
|
||||||
app.renderGrid(sessions);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
capturedHTML.includes('source-tile--empty'),
|
|
||||||
'source-tile--empty must still appear for an empty remote in flat mode; got: ' + capturedHTML
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
capturedHTML.includes('alienware-r13'),
|
|
||||||
'alienware-r13 name must appear in the status tile in flat mode; got: ' + capturedHTML
|
|
||||||
);
|
|
||||||
|
|
||||||
globalThis.document.getElementById = origGetById;
|
|
||||||
globalThis.document.querySelectorAll = origQSA;
|
|
||||||
app._setGridViewMode('flat');
|
|
||||||
app._setServerSettings(null);
|
|
||||||
app._setActiveView('all');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('v0.6.4: auth_failed and unreachable tiles still appear in grouped mode', () => {
|
test('v0.6.4: auth_failed and unreachable tiles still appear in grouped mode', () => {
|
||||||
// Only status:empty is suppressed in grouped mode; auth_failed and unreachable
|
// Only status:empty is suppressed in grouped mode; auth_failed and unreachable
|
||||||
@@ -5718,6 +5686,145 @@ test('v0.6.4: auth_failed and unreachable tiles still appear in grouped mode', (
|
|||||||
app._setActiveView('all');
|
app._setActiveView('all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── v0.6.5 empty tile suppressed in all view modes ──────────────────────────
|
||||||
|
|
||||||
|
test('v0.6.5: flat view — status:empty sentinel produces NO tile', () => {
|
||||||
|
// User-reported bug: alienware-r13 showed a "No sessions" tile in flat view
|
||||||
|
// (the muxplex default). v0.6.4 only suppressed it in grouped mode.
|
||||||
|
// v0.6.5 drops status:empty tiles unconditionally regardless of view mode.
|
||||||
|
const sessions = [
|
||||||
|
{ name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' },
|
||||||
|
{ status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' },
|
||||||
|
];
|
||||||
|
app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] });
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setActiveView('all');
|
||||||
|
|
||||||
|
let capturedHTML = '';
|
||||||
|
const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } };
|
||||||
|
const mockEmpty = { classList: { add: () => {}, remove: () => {} } };
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQSA = globalThis.document.querySelectorAll;
|
||||||
|
globalThis.document.getElementById = (id) => {
|
||||||
|
if (id === 'session-grid') return mockGrid;
|
||||||
|
if (id === 'empty-state') return mockEmpty;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
globalThis.document.querySelectorAll = () => [];
|
||||||
|
|
||||||
|
app.renderGrid(sessions);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('source-tile--empty'),
|
||||||
|
'source-tile--empty must NOT appear in flat mode for an empty remote; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('No sessions'),
|
||||||
|
'"No sessions" text must NOT appear in flat mode for an empty remote; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
capturedHTML.includes('local-sess') || capturedHTML.includes('spark-1'),
|
||||||
|
'the real local session must still render; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelectorAll = origQSA;
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setServerSettings(null);
|
||||||
|
app._setActiveView('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('v0.6.5: grouped view — status:empty sentinel still produces NO tile (regression)', () => {
|
||||||
|
// v0.6.4 suppressed empty tiles only in grouped mode; v0.6.5 keeps that suppression
|
||||||
|
// intact. This is a regression guard: grouped suppression must not have been broken
|
||||||
|
// while fixing flat mode.
|
||||||
|
const sessions = [
|
||||||
|
{ name: 'local-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:local-sess' },
|
||||||
|
{ status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' },
|
||||||
|
];
|
||||||
|
app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] });
|
||||||
|
app._setGridViewMode('grouped');
|
||||||
|
app._setActiveView('all');
|
||||||
|
|
||||||
|
let capturedHTML = '';
|
||||||
|
const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } };
|
||||||
|
const mockEmpty = { classList: { add: () => {}, remove: () => {} } };
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQSA = globalThis.document.querySelectorAll;
|
||||||
|
globalThis.document.getElementById = (id) => {
|
||||||
|
if (id === 'session-grid') return mockGrid;
|
||||||
|
if (id === 'empty-state') return mockEmpty;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
globalThis.document.querySelectorAll = () => [];
|
||||||
|
|
||||||
|
app.renderGrid(sessions);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('source-tile--empty'),
|
||||||
|
'source-tile--empty must NOT appear in grouped mode for an empty remote; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('No sessions'),
|
||||||
|
'"No sessions" text must NOT appear in grouped mode for an empty remote; got: ' + capturedHTML
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelectorAll = origQSA;
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setServerSettings(null);
|
||||||
|
app._setActiveView('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('v0.6.5: real sessions still render alongside an empty sentinel', () => {
|
||||||
|
// A mix of a real session and a status:empty sentinel. The real session must
|
||||||
|
// appear; the empty sentinel must be silently discarded in both flat and grouped modes.
|
||||||
|
const sessions = [
|
||||||
|
{ name: 'my-sess', deviceName: 'spark-1', snapshot: '', sessionKey: 'spark-1:my-sess' },
|
||||||
|
{ status: 'empty', deviceName: 'alienware-r13', sessionKey: 'devX:_empty' },
|
||||||
|
];
|
||||||
|
app._setServerSettings({ multi_device_enabled: true, hidden_sessions: [] });
|
||||||
|
app._setActiveView('all');
|
||||||
|
|
||||||
|
for (const mode of ['flat', 'grouped']) {
|
||||||
|
app._setGridViewMode(mode);
|
||||||
|
|
||||||
|
let capturedHTML = '';
|
||||||
|
const mockGrid = { get innerHTML() { return capturedHTML; }, set innerHTML(v) { capturedHTML = v; } };
|
||||||
|
const mockEmpty = { classList: { add: () => {}, remove: () => {} } };
|
||||||
|
const origGetById = globalThis.document.getElementById;
|
||||||
|
const origQSA = globalThis.document.querySelectorAll;
|
||||||
|
globalThis.document.getElementById = (id) => {
|
||||||
|
if (id === 'session-grid') return mockGrid;
|
||||||
|
if (id === 'empty-state') return mockEmpty;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
globalThis.document.querySelectorAll = () => [];
|
||||||
|
|
||||||
|
app.renderGrid(sessions);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
capturedHTML.includes('my-sess') || capturedHTML.includes('spark-1'),
|
||||||
|
`[${mode}] real session must render; got: ` + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('source-tile--empty'),
|
||||||
|
`[${mode}] source-tile--empty must NOT appear; got: ` + capturedHTML
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!capturedHTML.includes('No sessions'),
|
||||||
|
`[${mode}] "No sessions" text must NOT appear; got: ` + capturedHTML
|
||||||
|
);
|
||||||
|
|
||||||
|
globalThis.document.getElementById = origGetById;
|
||||||
|
globalThis.document.querySelectorAll = origQSA;
|
||||||
|
}
|
||||||
|
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setServerSettings(null);
|
||||||
|
app._setActiveView('all');
|
||||||
|
});
|
||||||
|
|
||||||
test('v0.6.3: empty-state still appears when every device has zero visible sessions', () => {
|
test('v0.6.3: empty-state still appears when every device has zero visible sessions', () => {
|
||||||
// When ALL sessions across ALL devices are hidden, visible.length === 0.
|
// When ALL sessions across ALL devices are hidden, visible.length === 0.
|
||||||
// renderGrid() must still reach its early-return branch and show empty-state,
|
// renderGrid() must still reach its early-return branch and show empty-state,
|
||||||
|
|||||||
Reference in New Issue
Block a user