refactor(task-9): extract badgeHtml const and tighten device badge tests

- Extract inline ternary for device badge into `const badgeHtml` variable
  above the return block, consistent with how `bellHtml` is handled
- Tighten data-session-key/data-source-url test assertions to check exact
  attribute values rather than just attribute presence
- Add XSS test: verifies deviceName containing HTML is escaped in the badge

All 159 tests pass.
This commit is contained in:
Brian Krabach
2026-03-30 23:41:50 -07:00
parent 5980a6c080
commit 4084544530
2 changed files with 20 additions and 3 deletions
+6 -1
View File
@@ -463,6 +463,11 @@ function buildTileHTML(session, index, mobile) {
bellHtml = `<span class="tile-bell">${countStr}</span>`; bellHtml = `<span class="tile-bell">${countStr}</span>`;
} }
// Device badge (shown when multiple sources configured and session has a device name)
const badgeHtml = _sources.length > 1 && session.deviceName
? `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`
: '';
// Last 20 lines of snapshot // Last 20 lines of snapshot
const snapshot = session.snapshot || ''; const snapshot = session.snapshot || '';
const lastLines = snapshot.split('\n').slice(-20).join('\n'); const lastLines = snapshot.split('\n').slice(-20).join('\n');
@@ -470,7 +475,7 @@ function buildTileHTML(session, index, mobile) {
return ( return (
`<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem" aria-label="${escapedName}">` + `<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem" aria-label="${escapedName}">` +
`<div class="tile-header">` + `<div class="tile-header">` +
`<span class="tile-name">${escapeHtml(name)}${_sources.length > 1 && session.deviceName ? '<span class="device-badge">' + escapeHtml(session.deviceName) + '</span>' : ''}</span>` + `<span class="tile-name">${escapeHtml(name)}${badgeHtml}</span>` +
`<span class="tile-meta">${bellHtml}<span class="tile-time">${escapeHtml(timeStr)}</span></span>` + `<span class="tile-meta">${bellHtml}<span class="tile-time">${escapeHtml(timeStr)}</span></span>` +
`</div>` + `</div>` +
`<div class="tile-body"><pre>${ansiToHtml(lastLines)}</pre></div>` + `<div class="tile-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
+14 -2
View File
@@ -2371,8 +2371,20 @@ test('buildTileHTML includes data-session-key and data-source-url attributes on
snapshot: '', snapshot: '',
}; };
const html = app.buildTileHTML(session, 0, false); const html = app.buildTileHTML(session, 0, false);
assert.ok(html.includes('data-session-key='), 'article should have data-session-key attribute'); assert.ok(html.includes('data-session-key="https://remote.example.com::work"'), 'article should have data-session-key with correct value');
assert.ok(html.includes('data-source-url='), 'article should have data-source-url attribute'); assert.ok(html.includes('data-source-url="https://remote.example.com"'), 'article should have data-source-url with correct value');
});
test('buildTileHTML escapes HTML in deviceName within device-badge', () => {
app._setSources([
{ url: '', name: 'This device', type: 'local', status: 'authenticated', backoffMs: 2000 },
{ url: 'https://remote.example.com', name: 'Remote', type: 'remote', status: 'authenticated', backoffMs: 2000 },
]);
const session = { name: 'work', deviceName: '<script>alert(1)</script>', sourceUrl: '', sessionKey: '::work', snapshot: '' };
const html = app.buildTileHTML(session, 0, false);
assert.ok(!html.includes('<script>'), 'device-badge should not contain raw <script> tag');
assert.ok(html.includes('&lt;script&gt;'), 'device-badge should escape < and > in deviceName');
app._setSources([]);
}); });
test('pollSessions sets unreachable and applies exponential backoff on network error', async () => { test('pollSessions sets unreachable and applies exponential backoff on network error', async () => {