diff --git a/.gitignore b/.gitignore
index d7b8f1bd7..56b53c457 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ yarn-debug.log*
yarn-error.log*
.eslintcache
+CLAUDE.md
diff --git a/src/__tests__/CompareResults/FilteredRowsNotice.test.tsx b/src/__tests__/CompareResults/FilteredRowsNotice.test.tsx
new file mode 100644
index 000000000..9d9e2308f
--- /dev/null
+++ b/src/__tests__/CompareResults/FilteredRowsNotice.test.tsx
@@ -0,0 +1,43 @@
+import FilteredRowsNotice from '../../components/CompareResults/FilteredRowsNotice';
+import type { ActiveColumnFilter } from '../../hooks/useTableFilters';
+import { render, screen } from '../utils/test-utils';
+
+describe('FilteredRowsNotice', () => {
+ const filters: ActiveColumnFilter[] = [
+ { name: 'Significance', excludedLabels: ['Noise'] },
+ ];
+
+ it('renders nothing when no rows are hidden', () => {
+ const { container } = render(
+ ,
+ );
+ expect(container).toBeEmptyDOMElement();
+ expect(
+ screen.queryByTestId('filtered-rows-notice'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('reports the count (singular) and the active filter reasons', () => {
+ render();
+ const notice = screen.getByTestId('filtered-rows-notice');
+ expect(notice).toHaveTextContent('1 row hidden by filters');
+ expect(notice).toHaveTextContent('Significance: Noise');
+ });
+
+ it('pluralizes the count and joins multiple filters and values', () => {
+ render(
+ ,
+ );
+ const notice = screen.getByTestId('filtered-rows-notice');
+ expect(notice).toHaveTextContent('3 rows hidden by filters');
+ expect(notice).toHaveTextContent(
+ 'Significance: Noise • Status: No changes, Improvement',
+ );
+ });
+});
diff --git a/src/__tests__/CompareResults/ResultsTable.test.tsx b/src/__tests__/CompareResults/ResultsTable.test.tsx
index 18605c806..96ce88ade 100644
--- a/src/__tests__/CompareResults/ResultsTable.test.tsx
+++ b/src/__tests__/CompareResults/ResultsTable.test.tsx
@@ -1308,7 +1308,7 @@ describe('Advanced-columns toggle for mann-whitney-u testVersion', () => {
// Open the dropdown and enable Cliff's Delta only.
await user.click(
- screen.getByRole('combobox', { name: 'Advanced columns' }),
+ screen.getByRole('combobox', { name: 'Advanced options' }),
);
await user.click(screen.getByRole('option', { name: "Cliff's Delta" }));
expect(header().querySelector('.delta-header')).toBeTruthy();
@@ -1349,7 +1349,7 @@ describe('Advanced-columns toggle for mann-whitney-u testVersion', () => {
expect(advancedParam()).toBeNull();
await user.click(
- screen.getByRole('combobox', { name: 'Advanced columns' }),
+ screen.getByRole('combobox', { name: 'Advanced options' }),
);
await user.click(screen.getByRole('option', { name: "Cliff's Delta" }));
expect(advancedParam()).toBe('cliffs_delta');
@@ -1364,6 +1364,76 @@ describe('Advanced-columns toggle for mann-whitney-u testVersion', () => {
await user.click(screen.getByRole('option', { name: 'CLES' }));
expect(advancedParam()).toBeNull();
});
+
+ it('groups the dropdown into Columns and Expanded row sections', async () => {
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+ const { testCompareMannWhitneyData } = getTestData();
+ setupAndRender(testCompareMannWhitneyData, 'test_version=mann-whitney-u');
+ await screen.findByText('a11yr');
+
+ await user.click(
+ screen.getByRole('combobox', { name: 'Advanced options' }),
+ );
+
+ // Both group headers and one option from each group are present.
+ expect(screen.getByText('Columns')).toBeInTheDocument();
+ expect(screen.getByText('Expanded row')).toBeInTheDocument();
+ expect(
+ screen.getByRole('option', { name: "Cliff's Delta" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('option', { name: 'Statistics table' }),
+ ).toBeInTheDocument();
+ });
+
+ it('seeds the expanded-row selection from the advanced_expanded URL param', async () => {
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+ const { testCompareMannWhitneyData } = getTestData();
+ setupAndRender(
+ testCompareMannWhitneyData,
+ 'test_version=mann-whitney-u&advanced_expanded=stats_table',
+ );
+ await screen.findByText('a11yr');
+
+ await user.click(
+ screen.getByRole('combobox', { name: 'Advanced options' }),
+ );
+
+ // The seeded option is checked; an unseeded one is not.
+ expect(
+ screen.getByRole('option', { name: 'Statistics table' }),
+ ).toHaveAttribute('aria-selected', 'true');
+ expect(
+ screen.getByRole('option', { name: 'Data warnings' }),
+ ).toHaveAttribute('aria-selected', 'false');
+ });
+
+ it('persists the expanded-row selection to the advanced_expanded URL param', async () => {
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+ const { testCompareMannWhitneyData } = getTestData();
+ setupAndRender(testCompareMannWhitneyData, 'test_version=mann-whitney-u');
+ await screen.findByText('a11yr');
+
+ const expandedParam = () =>
+ new URLSearchParams(window.location.search).get('advanced_expanded');
+ expect(expandedParam()).toBeNull();
+
+ await user.click(
+ screen.getByRole('combobox', { name: 'Advanced options' }),
+ );
+ await user.click(screen.getByRole('option', { name: 'Statistics table' }));
+ expect(expandedParam()).toBe('stats_table');
+
+ await user.click(screen.getByRole('option', { name: 'Data warnings' }));
+ expect(expandedParam()).toBe('stats_table,warnings');
+
+ // Turning the columns off leaves the expanded-row param untouched.
+ await user.click(screen.getByRole('option', { name: 'Statistics table' }));
+ expect(expandedParam()).toBe('warnings');
+
+ await user.click(screen.getByRole('option', { name: 'Data warnings' }));
+ expect(expandedParam()).toBeNull();
+ });
});
describe('cookie persistence vs. shareable URLs', () => {
@@ -1483,3 +1553,40 @@ describe('cookie persistence vs. shareable URLs', () => {
});
});
});
+
+// Placed at the end of the file on purpose: this test performs extra renders /
+// menu interactions, and React's useId counter is global across renders in a
+// jest run, so running it before the snapshot tests above would shift their
+// generated ids. Keeping it last avoids perturbing those snapshots.
+describe('Filtered-rows notice', () => {
+ it('shows how many rows the active filters hide, and why', async () => {
+ const { testCompareData } = getTestData();
+ setupAndRender(testCompareData, 'test_version=student-t');
+
+ await screen.findByText('a11yr');
+ // No active filters → no notice.
+ expect(
+ screen.queryByTestId('filtered-rows-notice'),
+ ).not.toBeInTheDocument();
+
+ const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+
+ // Hide the two "No changes" rows; the notice reports the count and reason.
+ await clickMenuItem(user, 'Status', /No changes/);
+ const notice = await screen.findByTestId('filtered-rows-notice');
+ expect(notice).toHaveTextContent('2 rows hidden by filters');
+ expect(notice).toHaveTextContent('Status: No changes');
+
+ // Narrowing to a single status hides more rows and the count updates.
+ await clickMenuItem(user, 'Status', /Select only.*Regression/);
+ expect(await screen.findByTestId('filtered-rows-notice')).toHaveTextContent(
+ '3 rows hidden by filters',
+ );
+
+ // Clearing the filter removes the notice again.
+ await clickMenuItem(user, 'Status', /Select all values/);
+ expect(
+ screen.queryByTestId('filtered-rows-notice'),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/src/__tests__/CompareResults/RevisionRow.test.tsx b/src/__tests__/CompareResults/RevisionRow.test.tsx
index 6f542eab5..93ddd8e2a 100644
--- a/src/__tests__/CompareResults/RevisionRow.test.tsx
+++ b/src/__tests__/CompareResults/RevisionRow.test.tsx
@@ -15,6 +15,7 @@ import {
screen,
renderWithRouter,
enableAdvancedColumns,
+ enableExpandedRowOptions,
} from '../utils/test-utils';
jest.mock('../../hooks/useSubtestRegressionCount');
@@ -167,6 +168,10 @@ describe('Subtest count pills', () => {
});
describe('Expanded row', () => {
+ // Several tests assert the (advanced) MWU expanded-row components, which are
+ // hidden by default; enable them. No-op for Student-T expanded rows.
+ beforeEach(() => enableExpandedRowOptions());
+
it('should display "Show 39 more" and "Show less" for base runs when row is expanded', async () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const { testCompareDataWithReplicatesMultipleValues: rowData } =
diff --git a/src/__tests__/CompareResults/RevisionRowExpandable.test.tsx b/src/__tests__/CompareResults/RevisionRowExpandable.test.tsx
index 5c002d62a..c1fef150c 100644
--- a/src/__tests__/CompareResults/RevisionRowExpandable.test.tsx
+++ b/src/__tests__/CompareResults/RevisionRowExpandable.test.tsx
@@ -5,7 +5,11 @@ import fetchMock from '@fetch-mock/jest';
import { loader } from '../../components/CompareResults/loader';
import RevisionRowExpandable from '../../components/CompareResults/RevisionRowExpandable';
import getTestData from '../utils/fixtures';
-import { screen, renderWithRouter } from '../utils/test-utils';
+import {
+ screen,
+ renderWithRouter,
+ enableExpandedRowOptions,
+} from '../utils/test-utils';
function renderWithRoute(component: ReactElement) {
fetchMock
@@ -62,6 +66,9 @@ describe('RevisionRowExpandable for student-t testVersion', () => {
});
describe('RevisionRowExpandable for mann-whitney-u testVersion', () => {
+ // These assert the (advanced) expanded-row components, hidden by default.
+ beforeEach(() => enableExpandedRowOptions());
+
it('should display warnings', async () => {
const { mockMannWhitneyResultData } = getTestData();
@@ -147,3 +154,106 @@ describe('RevisionRowExpandable for mann-whitney-u testVersion', () => {
expect(goodFit).toBeInTheDocument();
});
});
+
+describe('RevisionRowExpandable simplified Mann-Whitney-U view', () => {
+ // Text unique to each advanced expanded-row component, used to assert whether
+ // that component is currently rendered.
+ const EFFECT_SIZE_TEXT = /Effect Size:/;
+ const STATS_TABLE_TEXT = /Normality Test/;
+ const WARNINGS_TEXT = /Shapiro-Wilk test cannot be run/;
+ const MODES_LABEL = 'Mode-by-mode breakdown';
+
+ function renderMwuRow() {
+ const { mockMannWhitneyResultData } = getTestData();
+ renderWithRoute(
+ ,
+ );
+ }
+
+ it('shows the graph blurb and hides every advanced component by default', async () => {
+ renderMwuRow();
+
+ // The how-to-read blurb is always present in the simplified view.
+ expect(
+ await screen.findByText(/how the Base and New results are distributed/i),
+ ).toBeInTheDocument();
+
+ // None of the advanced (checkbox-gated) components render by default.
+ expect(screen.queryByText(EFFECT_SIZE_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByText(STATS_TABLE_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByText(WARNINGS_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(MODES_LABEL)).not.toBeInTheDocument();
+ });
+
+ it('reveals only the effect size component when that option is on', async () => {
+ enableExpandedRowOptions({ effectSize: true });
+ renderMwuRow();
+
+ expect(await screen.findByText(EFFECT_SIZE_TEXT)).toBeInTheDocument();
+ expect(screen.queryByText(STATS_TABLE_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByText(WARNINGS_TEXT)).not.toBeInTheDocument();
+ });
+
+ it('reveals only the statistics table when that option is on', async () => {
+ enableExpandedRowOptions({ statsTable: true });
+ renderMwuRow();
+
+ expect(await screen.findByText(STATS_TABLE_TEXT)).toBeInTheDocument();
+ expect(screen.queryByText(EFFECT_SIZE_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByText(WARNINGS_TEXT)).not.toBeInTheDocument();
+ });
+
+ it('reveals only the data warnings when that option is on', async () => {
+ enableExpandedRowOptions({ warnings: true });
+ renderMwuRow();
+
+ // Base and New each contribute a Shapiro-Wilk warning.
+ expect((await screen.findAllByText(WARNINGS_TEXT)).length).toBeGreaterThan(
+ 0,
+ );
+ expect(screen.queryByText(EFFECT_SIZE_TEXT)).not.toBeInTheDocument();
+ expect(screen.queryByText(STATS_TABLE_TEXT)).not.toBeInTheDocument();
+ });
+
+ it('hides the mode-analysis controls (valley-depth slider + Show modes) by default', async () => {
+ renderMwuRow();
+ await screen.findByText(/how the Base and New results are distributed/i);
+
+ expect(
+ screen.queryByRole('slider', { name: /valley depth threshold/i }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('checkbox', { name: /show modes/i }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('reveals the mode-analysis controls when Mode analysis is enabled', async () => {
+ enableExpandedRowOptions({ modes: true });
+ renderMwuRow();
+
+ expect(
+ await screen.findByRole('slider', { name: /valley depth threshold/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('checkbox', { name: /show modes/i }),
+ ).toBeInTheDocument();
+ });
+
+ it('shows a placeholder when Mode analysis is on but no breakdown is available', async () => {
+ // The mock is a sparse/unimodal comparison, so no mode-by-mode breakdown is
+ // produced; the cell should surface a placeholder rather than stay empty.
+ enableExpandedRowOptions({ modes: true });
+ renderMwuRow();
+
+ expect(
+ await screen.findByText(/No mode analysis available/i),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByLabelText('Mode-by-mode breakdown'),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/src/__tests__/CompareResults/SubtestsResultsView.test.tsx b/src/__tests__/CompareResults/SubtestsResultsView.test.tsx
index 531ea6d6f..1607c4b5c 100644
--- a/src/__tests__/CompareResults/SubtestsResultsView.test.tsx
+++ b/src/__tests__/CompareResults/SubtestsResultsView.test.tsx
@@ -14,11 +14,18 @@ import {
renderWithRouter,
screen,
enableAdvancedColumns,
+ enableExpandedRowOptions,
} from '../utils/test-utils';
jest.mock('../../utils/location');
const mockedGetLocationOrigin = getLocationOrigin as jest.Mock;
+// The Mann-Whitney-U expanded row hides its statistical components (stats
+// table, warnings, effect size, modes) behind "Advanced options" by default.
+// These tests assert on those components, so enable them everywhere here. This
+// is a no-op for rows that aren't expanded and for the Student-T layout.
+beforeEach(() => enableExpandedRowOptions());
+
const setup = ({
element,
route,
diff --git a/src/__tests__/CompareResults/SubtestsRevisionRow.test.tsx b/src/__tests__/CompareResults/SubtestsRevisionRow.test.tsx
index 1b328e56a..9cfef3274 100644
--- a/src/__tests__/CompareResults/SubtestsRevisionRow.test.tsx
+++ b/src/__tests__/CompareResults/SubtestsRevisionRow.test.tsx
@@ -11,6 +11,7 @@ import {
screen,
renderWithRouter,
enableAdvancedColumns,
+ enableExpandedRowOptions,
} from '../utils/test-utils';
function renderWithRoute(component: ReactElement) {
@@ -124,6 +125,7 @@ describe('SubtestsRevisionRow Component', () => {
});
it('renders subtests results with mann-whitney-u testVersion', async () => {
+ enableExpandedRowOptions();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const { subtestsResult } = getTestData();
const mockGridTemplateColumns = '1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr';
@@ -145,6 +147,7 @@ describe('SubtestsRevisionRow Component', () => {
});
it('renders subtests results defaulting to mann-whitney-u with no testVersion', async () => {
+ enableExpandedRowOptions();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const { subtestsMannWhitneyResult } = getTestData();
const mockGridTemplateColumns = '1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr';
diff --git a/src/__tests__/CompareResults/__snapshots__/OverTimeResultsView.test.tsx.snap b/src/__tests__/CompareResults/__snapshots__/OverTimeResultsView.test.tsx.snap
index 5a06c4746..d580ae35a 100644
--- a/src/__tests__/CompareResults/__snapshots__/OverTimeResultsView.test.tsx.snap
+++ b/src/__tests__/CompareResults/__snapshots__/OverTimeResultsView.test.tsx.snap
@@ -373,23 +373,23 @@ exports[`Results View The table should match snapshot and other elements should
class="MuiGrid-root MuiGrid-direction-xs-row MuiGrid-grid-xs-auto css-zh2j38-MuiGrid-root"
>