feat: add buildSidebarHTML — session card HTML builder for sidebar
This commit is contained in:
@@ -301,6 +301,44 @@ function buildTileHTML(session, index, mobile) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the HTML string for a single session sidebar card.
|
||||||
|
* @param {object} session
|
||||||
|
* @param {string} currentSession - name of the currently active session
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function buildSidebarHTML(session, currentSession) {
|
||||||
|
const name = session.name || '';
|
||||||
|
const escapedName = escapeHtml(name);
|
||||||
|
const isActive = name === currentSession;
|
||||||
|
|
||||||
|
let classes = 'sidebar-item';
|
||||||
|
if (isActive) classes += ' sidebar-item--active';
|
||||||
|
|
||||||
|
const unseen = session.bell && session.bell.unseen_count;
|
||||||
|
|
||||||
|
// Bell indicator
|
||||||
|
let bellHtml = '';
|
||||||
|
if (unseen && unseen > 0) {
|
||||||
|
const countStr = unseen > 9 ? '9+' : String(unseen);
|
||||||
|
bellHtml = `<span class="tile-bell">${countStr}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last 20 lines of snapshot
|
||||||
|
const snapshot = session.snapshot || '';
|
||||||
|
const lastLines = snapshot.split('\n').slice(-20).join('\n');
|
||||||
|
|
||||||
|
return (
|
||||||
|
`<article class="${classes}" data-session="${escapedName}" tabindex="0" role="listitem">` +
|
||||||
|
`<div class="sidebar-item-header">` +
|
||||||
|
`<span class="sidebar-item-name">${escapedName}</span>` +
|
||||||
|
`${bellHtml}` +
|
||||||
|
`</div>` +
|
||||||
|
`<div class="sidebar-item-body"><pre>${escapeHtml(lastLines)}</pre></div>` +
|
||||||
|
`</article>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the session grid. Shows empty state when no sessions exist.
|
* Render the session grid. Shows empty state when no sessions exist.
|
||||||
* On mobile, sorts sessions by priority before rendering.
|
* On mobile, sorts sessions by priority before rendering.
|
||||||
@@ -898,6 +936,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
startPolling,
|
startPolling,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
buildTileHTML,
|
buildTileHTML,
|
||||||
|
buildSidebarHTML,
|
||||||
renderGrid,
|
renderGrid,
|
||||||
requestNotificationPermission,
|
requestNotificationPermission,
|
||||||
handleBellTransitions,
|
handleBellTransitions,
|
||||||
|
|||||||
@@ -1646,3 +1646,49 @@ test('closeBottomSheet does not call removeEventListener on sheet-backdrop', ()
|
|||||||
assert.strictEqual(backdropRemoveCalled, false, 'closeBottomSheet must not call removeEventListener on sheet-backdrop');
|
assert.strictEqual(backdropRemoveCalled, false, 'closeBottomSheet must not call removeEventListener on sheet-backdrop');
|
||||||
globalThis.document.getElementById = origGetById;
|
globalThis.document.getElementById = origGetById;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- buildSidebarHTML ---
|
||||||
|
|
||||||
|
test('buildSidebarHTML adds sidebar-item--active class for active session', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, 'my-session');
|
||||||
|
assert.ok(html.includes('sidebar-item--active'), 'active session should have sidebar-item--active class');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML does not add sidebar-item--active class for inactive session', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, 'other-session');
|
||||||
|
assert.ok(!html.includes('sidebar-item--active'), 'inactive session should not have sidebar-item--active class');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML renders bell badge with tile-bell class and count when unseen_count > 0', () => {
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 3 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('tile-bell'), 'should contain tile-bell class when unseen_count > 0');
|
||||||
|
assert.ok(html.includes('>3<'), 'should contain unseen count text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML omits bell badge when unseen_count is 0', () => {
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(!html.includes('tile-bell'), 'should not contain tile-bell class when unseen_count is 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML HTML-escapes session name to prevent XSS', () => {
|
||||||
|
const session = { name: '<script>alert(1)</script>', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(!html.includes('<script>'), 'raw <script> tag must not appear in output');
|
||||||
|
assert.ok(html.includes('<script>'), 'name must be HTML-escaped');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML article element has data-session attribute', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('data-session="my-session"'), 'article must have data-session attribute');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML includes snapshot preview in a pre element', () => {
|
||||||
|
const session = { name: 's', snapshot: 'line1\nline2\nline3', bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(/<pre>[\s\S]*line1[\s\S]*<\/pre>/.test(html), 'snapshot content should be inside a <pre> element');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user