Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions frontend/security-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,35 @@
);
}

/** Shared empty-state renderer — consistent messaging across all result lists. */
function buildEmptyStateHtml(message, { tag = 'ok' } = {}) {
return `<span class="result-tag tag-${safeCssToken(tag, 'ok')}">${escHtml(message)}</span>`;
}

/** 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 `<p class="history-empty">${escHtml(emptyMessage)}</p>`;
}
return entries.map((e) => buildStoredListItemHtml(e, loaderOpts)).join('');
}

global.QyverixSecurity = {
ALLOWED_PRIORITIES,
ALLOWED_SEVERITIES,
Expand All @@ -256,5 +285,9 @@
bindKeyboardShortcuts,
buildIssueCardHtml,
buildSuggestCardHtml,
buildEmptyStateHtml,
buildIssuesListHtml,
buildSuggestionsListHtml,
buildStoredListHtml,
};
})(typeof window !== 'undefined' ? window : globalThis);
57 changes: 57 additions & 0 deletions frontend/tests/security-utils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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('<script>alert(1)</script>');
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: '<img onerror=alert(1)>' });
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');
});
}
});
Loading