diff --git a/frontend/security-utils.js b/frontend/security-utils.js index f3019175..6f9e6a8d 100644 --- a/frontend/security-utils.js +++ b/frontend/security-utils.js @@ -233,6 +233,35 @@ ); } + /** Shared empty-state renderer — consistent messaging across all result lists. */ + function buildEmptyStateHtml(message, { tag = 'ok' } = {}) { + return `${escHtml(message)}`; + } + + /** Renders a list of issue cards, or a consistent empty state if none exist. */ + function buildIssuesListHtml(issues, { emptyMessage = '✓ No issues found. Code looks clean!' } = {}) { + if (!Array.isArray(issues) || issues.length === 0) { + return buildEmptyStateHtml(emptyMessage); + } + return issues.map(buildIssueCardHtml).join(''); + } + + /** Renders a list of suggestion cards, or a consistent empty state if none exist. */ + function buildSuggestionsListHtml(suggestions, { emptyMessage = 'No suggestions available.' } = {}) { + if (!Array.isArray(suggestions) || suggestions.length === 0) { + return buildEmptyStateHtml(emptyMessage); + } + return suggestions.map(buildSuggestCardHtml).join(''); + } + + /** Renders a stored history/favorites list, or a consistent empty state if none exist. */ + function buildStoredListHtml(entries, loaderOpts, { emptyMessage = 'No entries yet.' } = {}) { + if (!Array.isArray(entries) || entries.length === 0) { + return `

${escHtml(emptyMessage)}

`; + } + return entries.map((e) => buildStoredListItemHtml(e, loaderOpts)).join(''); + } + global.QyverixSecurity = { ALLOWED_PRIORITIES, ALLOWED_SEVERITIES, @@ -256,5 +285,9 @@ bindKeyboardShortcuts, buildIssueCardHtml, buildSuggestCardHtml, + buildEmptyStateHtml, + buildIssuesListHtml, + buildSuggestionsListHtml, + buildStoredListHtml, }; })(typeof window !== 'undefined' ? window : globalThis); diff --git a/frontend/tests/security-utils.test.mjs b/frontend/tests/security-utils.test.mjs index a0863976..25ee4c5f 100644 --- a/frontend/tests/security-utils.test.mjs +++ b/frontend/tests/security-utils.test.mjs @@ -301,3 +301,60 @@ describe('normal code preserved', () => { }); } }); + +describe('empty-state rendering', () => { + it('buildEmptyStateHtml escapes the message and applies a safe tag', () => { + const html = SEC.buildEmptyStateHtml(''); + assertPlainTextHtml(html, 'empty state'); + assert.ok(html.includes('tag-ok')); + }); + + it('buildEmptyStateHtml falls back to "ok" tag for unsafe tag input', () => { + const html = SEC.buildEmptyStateHtml('done', { tag: '' }); + assert.ok(html.includes('tag-ok')); + }); + + it('buildIssuesListHtml renders empty state for empty array', () => { + const html = SEC.buildIssuesListHtml([]); + assertPlainTextHtml(html, 'issues empty state'); + assert.ok(html.includes('No issues found')); + }); + + it('buildIssuesListHtml renders empty state for non-array input', () => { + const html = SEC.buildIssuesListHtml(null); + assert.ok(html.includes('No issues found')); + }); + + it('buildIssuesListHtml renders cards when issues exist', () => { + const html = SEC.buildIssuesListHtml([ + { type: 'Bug', description: 'x', severity: 'error' }, + ]); + assert.ok(html.includes('issue-card')); + }); + + it('buildSuggestionsListHtml renders empty state for empty array', () => { + const html = SEC.buildSuggestionsListHtml([]); + assertPlainTextHtml(html, 'suggestions empty state'); + assert.ok(html.includes('No suggestions available')); + }); + + it('buildStoredListHtml renders empty state for empty history', () => { + const html = SEC.buildStoredListHtml([]); + assert.ok(html.includes('No entries yet')); + assert.ok(html.includes('history-empty')); + }); + + it('buildStoredListHtml renders items when entries exist', () => { + const html = SEC.buildStoredListHtml([ + { id: 1, lang: 'Python', ts: 'now', preview: 'x=1' }, + ]); + assert.ok(html.includes('history-item') || html.includes('fav-item')); + }); + + for (const payload of XSS_PAYLOADS) { + it(`buildEmptyStateHtml is XSS-safe for: ${payload.slice(0, 30)}`, () => { + const html = SEC.buildEmptyStateHtml(payload); + assertPlainTextHtml(html, 'empty state payload'); + }); + } +});