diff --git a/frontend/src/__tests__/recommendations.test.ts b/frontend/src/__tests__/recommendations.test.ts index e0ab8dbf8..650bbb7d1 100644 --- a/frontend/src/__tests__/recommendations.test.ts +++ b/frontend/src/__tests__/recommendations.test.ts @@ -587,6 +587,9 @@ describe('Recommendations Module', () => { }); test('shows empty-state message when no recommendations', async () => { + // Issue #700: zero rows now render an empty
with a hint cell + // rather than replacing the entire table with a. The stays
+ // so column headers remain visible. The hint text lives inside the tbody.
(api.getRecommendations as jest.Mock).mockResolvedValue({
summary: {},
recommendations: [],
@@ -596,7 +599,12 @@ describe('Recommendations Module', () => {
await loadRecommendations();
const list = document.getElementById('recommendations-list');
- expect(list?.innerHTML).toContain('No recommendations match');
+ // The table (including ) must still be rendered.
+ expect(list?.querySelector('thead')).not.toBeNull();
+ // The hint cell must be present inside the tbody.
+ const emptyCell = list?.querySelector('tbody td.empty');
+ expect(emptyCell).not.toBeNull();
+ expect(emptyCell?.textContent).toMatch(/No rows match/);
});
test('stores recommendations in state', async () => {
@@ -2222,6 +2230,64 @@ describe('Bundle B: column header filter triggers', () => {
expect(state.setRecommendationsColumnFilter).toHaveBeenCalledWith('provider', { kind: 'set', values: [] });
});
+ test('Issue #700: Clear resets (All) checkbox to unchecked (not indeterminate)', async () => {
+ // Bug: the old Clear branch set cb.checked = false on individual boxes but
+ // never called updateAllTriState(), so the (All) checkbox kept its prior
+ // state (checked or indeterminate). After the fix, commitAllRef(false) is
+ // used which calls updateAllTriState() and leaves (All) unchecked.
+ //
+ // Simulate real state-store behaviour: setRecommendationsColumnFilter
+ // updates the store so the next getRecommendationsColumnFilters() call
+ // sees the cleared filter. Without this the resyncOpenPopover() call
+ // triggered by the rerender would re-apply the stale filter and overwrite
+ // the tri-state that updateAllTriState() just set.
+ (state.getRecommendationsColumnFilters as jest.Mock).mockReturnValue({
+ provider: { kind: 'set', values: ['aws'] },
+ });
+ (state.setRecommendationsColumnFilter as jest.Mock).mockImplementation(
+ (col: string, val: unknown) => {
+ (state.getRecommendationsColumnFilters as jest.Mock).mockReturnValue(
+ val === null ? {} : { [col]: val },
+ );
+ },
+ );
+ await loadRecommendations();
+ const providerBtn = document.querySelector
+ // when no rows matched, removing . After the fix, the header row
+ // remains visible (with an empty should replace the table.
+ expect(container?.querySelector('p.empty')).toBeNull();
+ });
+
test('Clear button on a numeric column clears the expression filter (null)', async () => {
// Numeric columns still use null on Clear: the empty-set semantic
// only applies to categorical filters because the set-membership
diff --git a/frontend/src/recommendations.ts b/frontend/src/recommendations.ts
index 1ab3c5ae2..657ab933e 100644
--- a/frontend/src/recommendations.ts
+++ b/frontend/src/recommendations.ts
@@ -1636,6 +1636,10 @@ function buildPopoverContent(
const checkboxes = new Map No recommendations match these filters. Try clearing filters or refreshing. . The column headers remain visible so the user
+ // can see which columns are active while they adjust filters.
+ if (emptyResult) {
+ const tbody = container.querySelector('tbody');
+ if (tbody) {
+ // colspan = 1 (checkbox col) + all visible data columns.
+ const colspan = 1 + visibleCols.length;
+ const tr = document.createElement('tr');
+ const td = document.createElement('td');
+ td.setAttribute('colspan', String(colspan));
+ td.className = 'empty';
+ td.textContent = 'No rows match these filters.';
+ tr.appendChild(td);
+ tbody.appendChild(tr);
+ }
+ }
+
+ const visibleCount = recommendations?.length ?? 0;
// Filter status: Clear-filters badge + aria-live count + Expand-All toggle.
// Rendered AFTER buildListMarkup so lastVisibleGroupKeys is populated.
// Mounted as a sibling above the table so it survives the container's
// innerHTML rewrite without losing aria-live announcements.
- renderFilterStatusBar(loadedRecs?.length ?? 0, recommendations.length);
+ renderFilterStatusBar(loadedRecs?.length ?? 0, visibleCount);
- updateBottomActionBox(recommendations.length, loadedRecs?.length ?? recommendations.length);
+ updateBottomActionBox(visibleCount, loadedRecs?.length ?? visibleCount);
// Per-column filter button: trigger opens the popover anchored to the
with a