Skip to content

Commit 918dbae

Browse files
authored
component test migration (#59)
* extract IntroSection component, test imports real component * extract ChartTabs into own component * tabs E2E: cover all tab URL updates * remove intro-section E2E test, replaced by component test * add component tests for chart-selectors, chart-buttons, date-range-picker * remove tautological tests (renders/exists checks with no behavioral value) * remove E2E tests redundant with component tests * add data-testid to legend switches, sidebar-legend, calculator selectors * fix NoFallbackError in CI: allow dynamic params for tab routes * Revert "fix NoFallbackError in CI: allow dynamic params for tab routes" This reverts commit b4aa8cb. * allow dynamic params on tab routes to suppress NoFallbackError in CI
1 parent 2872bdb commit 918dbae

18 files changed

Lines changed: 596 additions & 652 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ChartButtons } from '@/components/ui/chart-buttons';
2+
3+
describe('ChartButtons', () => {
4+
describe('without CSV export', () => {
5+
beforeEach(() => {
6+
cy.mount(
7+
<div style={{ position: 'relative', width: 400, height: 200 }}>
8+
<div id="test-chart">Chart content</div>
9+
<ChartButtons chartId="test-chart" analyticsPrefix="test" />
10+
</div>,
11+
);
12+
});
13+
14+
it('zoom reset dispatches custom event', () => {
15+
cy.window().then((win) => {
16+
const handler = cy.stub().as('zoomReset');
17+
win.addEventListener('test_zoom_reset_test-chart', handler);
18+
});
19+
cy.get('[data-testid="zoom-reset-button"]').click();
20+
cy.get('@zoomReset').should('have.been.calledOnce');
21+
});
22+
});
23+
24+
describe('with CSV export', () => {
25+
it('shows dropdown with PNG and CSV options', () => {
26+
const onExportCsv = cy.stub().as('csvExport');
27+
cy.mount(
28+
<div style={{ position: 'relative', width: 400, height: 200 }}>
29+
<div id="test-chart">Chart content</div>
30+
<ChartButtons chartId="test-chart" analyticsPrefix="test" onExportCsv={onExportCsv} />
31+
</div>,
32+
);
33+
cy.get('[data-testid="export-button"]').click();
34+
cy.get('[data-testid="export-png-button"]').should('be.visible');
35+
cy.get('[data-testid="export-csv-button"]').should('be.visible');
36+
});
37+
38+
it('clicking CSV calls onExportCsv', () => {
39+
const onExportCsv = cy.stub().as('csvExport');
40+
cy.mount(
41+
<div style={{ position: 'relative', width: 400, height: 200 }}>
42+
<div id="test-chart">Chart content</div>
43+
<ChartButtons chartId="test-chart" analyticsPrefix="test" onExportCsv={onExportCsv} />
44+
</div>,
45+
);
46+
cy.get('[data-testid="export-button"]').click();
47+
cy.get('[data-testid="export-csv-button"]').click();
48+
cy.get('@csvExport').should('have.been.calledOnce');
49+
});
50+
});
51+
52+
describe('hideZoomReset', () => {
53+
it('hides zoom reset button when hideZoomReset is true', () => {
54+
cy.mount(
55+
<div style={{ position: 'relative', width: 400, height: 200 }}>
56+
<div id="test-chart">Chart content</div>
57+
<ChartButtons chartId="test-chart" analyticsPrefix="test" hideZoomReset />
58+
</div>,
59+
);
60+
cy.get('[data-testid="zoom-reset-button"]').should('not.exist');
61+
cy.get('[data-testid="export-button"]').should('be.visible');
62+
});
63+
});
64+
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useState } from 'react';
2+
3+
import {
4+
ModelSelector,
5+
SequenceSelector,
6+
PrecisionSelector,
7+
} from '@/components/ui/chart-selectors';
8+
import { TooltipProvider } from '@/components/ui/tooltip';
9+
10+
function ModelSelectorHarness() {
11+
const [value, setValue] = useState('DeepSeek-R1-0528');
12+
return (
13+
<TooltipProvider>
14+
<ModelSelector
15+
value={value}
16+
onChange={setValue}
17+
availableModels={['DeepSeek-R1-0528', 'Llama-4-Maverick', 'Qwen3-235B']}
18+
data-testid="model-selector"
19+
/>
20+
</TooltipProvider>
21+
);
22+
}
23+
24+
function SequenceSelectorHarness() {
25+
const [value, setValue] = useState('1024_128');
26+
return (
27+
<TooltipProvider>
28+
<SequenceSelector
29+
value={value}
30+
onChange={setValue}
31+
availableSequences={['1024_128', '1024_8192', '8192_1024']}
32+
data-testid="sequence-selector"
33+
/>
34+
</TooltipProvider>
35+
);
36+
}
37+
38+
function PrecisionSelectorHarness() {
39+
const [value, setValue] = useState(['FP8']);
40+
return (
41+
<TooltipProvider>
42+
<PrecisionSelector
43+
value={value}
44+
onChange={setValue}
45+
availablePrecisions={['FP8', 'FP4', 'BF16']}
46+
data-testid="precision-multiselect"
47+
/>
48+
</TooltipProvider>
49+
);
50+
}
51+
52+
describe('Chart Selectors', () => {
53+
describe('ModelSelector', () => {
54+
beforeEach(() => {
55+
cy.mount(<ModelSelectorHarness />);
56+
});
57+
58+
it('shows options when clicked', () => {
59+
cy.get('[data-testid="model-selector"]').click();
60+
cy.get('[role="option"]').should('have.length.greaterThan', 0);
61+
});
62+
63+
it('selecting an option updates the displayed value', () => {
64+
cy.get('[data-testid="model-selector"]').click();
65+
cy.get('[role="option"]').contains('Qwen3-235B').click();
66+
cy.get('[data-testid="model-selector"]').should('contain', 'Qwen3-235B');
67+
});
68+
});
69+
70+
describe('SequenceSelector', () => {
71+
beforeEach(() => {
72+
cy.mount(<SequenceSelectorHarness />);
73+
});
74+
75+
it('shows options when clicked', () => {
76+
cy.get('[data-testid="sequence-selector"]').click();
77+
cy.get('[role="option"]').should('have.length', 3);
78+
});
79+
80+
it('selecting an option updates the displayed value', () => {
81+
cy.get('[data-testid="sequence-selector"]').click();
82+
cy.get('[role="option"]').last().click();
83+
cy.get('[data-testid="sequence-selector"]').should('not.contain', '1K / 128');
84+
});
85+
});
86+
87+
describe('PrecisionSelector', () => {
88+
beforeEach(() => {
89+
cy.mount(<PrecisionSelectorHarness />);
90+
});
91+
92+
it('shows current selection', () => {
93+
cy.get('[data-testid="precision-multiselect"]').should('contain', 'FP8');
94+
});
95+
});
96+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useState } from 'react';
2+
3+
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker';
4+
5+
function DateRangePickerHarness({
6+
initialRange = { startDate: '', endDate: '' },
7+
availableDates,
8+
}: {
9+
initialRange?: DateRange;
10+
availableDates?: string[];
11+
}) {
12+
const [range, setRange] = useState<DateRange>(initialRange);
13+
return (
14+
<div data-testid="date-range-wrapper">
15+
<DateRangePicker
16+
dateRange={range}
17+
onChange={setRange}
18+
availableDates={availableDates}
19+
placeholder="Select date range"
20+
/>
21+
<div data-testid="date-range-output">
22+
{range.startDate && range.endDate ? `${range.startDate} to ${range.endDate}` : 'no range'}
23+
</div>
24+
</div>
25+
);
26+
}
27+
28+
describe('DateRangePicker', () => {
29+
it('renders with placeholder text', () => {
30+
cy.mount(<DateRangePickerHarness />);
31+
cy.get('[data-testid="date-range-wrapper"]').should('contain', 'Select date range');
32+
});
33+
34+
it('opens dialog when clicked', () => {
35+
cy.mount(<DateRangePickerHarness />);
36+
cy.contains('Select date range').click();
37+
cy.get('[role="dialog"]').should('be.visible');
38+
cy.contains('Select Date Range').should('be.visible');
39+
});
40+
41+
it('apply is disabled when no dates selected', () => {
42+
cy.mount(<DateRangePickerHarness />);
43+
cy.contains('Select date range').click();
44+
cy.contains('button', 'Apply').should('be.disabled');
45+
});
46+
47+
it('cancel closes the dialog', () => {
48+
cy.mount(<DateRangePickerHarness />);
49+
cy.contains('Select date range').click();
50+
cy.get('[role="dialog"]').should('be.visible');
51+
cy.contains('button', 'Cancel').click();
52+
cy.get('[role="dialog"]').should('not.exist');
53+
});
54+
55+
it('displays formatted range when dates are provided', () => {
56+
cy.mount(
57+
<DateRangePickerHarness initialRange={{ startDate: '2026-01-15', endDate: '2026-02-20' }} />,
58+
);
59+
cy.get('[data-testid="date-range-wrapper"]').should('contain', 'Jan 15, 2026');
60+
cy.get('[data-testid="date-range-wrapper"]').should('contain', 'Feb 20, 2026');
61+
});
62+
63+
it('shows quick select buttons when availableDates provided', () => {
64+
const dates = ['2025-12-01', '2025-12-15', '2026-01-01', '2026-02-01', '2026-03-01'];
65+
cy.mount(<DateRangePickerHarness availableDates={dates} />);
66+
cy.contains('Select date range').click();
67+
cy.contains('button', 'Max Range').should('be.visible');
68+
});
69+
70+
it('shows overlay when no available dates', () => {
71+
cy.mount(<DateRangePickerHarness availableDates={[]} />);
72+
cy.contains('Select date range').click();
73+
cy.contains('No available dates').should('be.visible');
74+
});
75+
76+
it('shows overlay when only 1 available date', () => {
77+
cy.mount(<DateRangePickerHarness availableDates={['2026-01-01']} />);
78+
cy.contains('Select date range').click();
79+
cy.contains('Only 1 date available').should('be.visible');
80+
});
81+
});

packages/app/cypress/component/footer.cy.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ describe('Footer', () => {
1515
);
1616
});
1717

18-
it('renders the footer element', () => {
19-
cy.get('[data-testid="footer"]').should('exist');
20-
});
21-
2218
it('displays copyright notice with semianalysis.com and current year', () => {
2319
const year = new Date().getFullYear().toString();
2420
cy.get('[data-testid="footer-copyright"]').should('contain', 'semianalysis.com');
@@ -58,10 +54,6 @@ describe('Footer', () => {
5854
.and('include', 'github.com/SemiAnalysisAI/InferenceX-app');
5955
});
6056

61-
it('shows the SemiAnalysis logo', () => {
62-
cy.get('[data-testid="footer-brand"]').find('img[alt="SemiAnalysis logo"]').should('exist');
63-
});
64-
6557
it('all external links open in a new tab', () => {
6658
cy.get('[data-testid="footer-links"]')
6759
.find('a[target="_blank"]')

packages/app/cypress/component/header.cy.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ describe('Header', () => {
1818
);
1919
});
2020

21-
it('renders the header element', () => {
22-
cy.get('[data-testid="header"]').should('be.visible');
23-
});
24-
2521
it('displays the InferenceX title', () => {
2622
cy.get('[data-testid="header"]').contains('InferenceX').should('be.visible');
2723
});
@@ -52,10 +48,6 @@ describe('Header', () => {
5248
.and('include', 'github.com/SemiAnalysisAI/InferenceX');
5349
});
5450

55-
it('GitHub stars button has glow animation class', () => {
56-
cy.get('[data-testid="header-star-button"]').should('have.class', 'star-button-glow');
57-
});
58-
5951
it('shows the theme toggle button', () => {
6052
cy.get('[data-testid="theme-toggle"]').should('be.visible');
6153
});

packages/app/cypress/component/mode-toggle.cy.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ describe('ModeToggle', () => {
1010
);
1111
});
1212

13-
it('renders the theme toggle button', () => {
14-
cy.get('[data-testid="theme-toggle"]').should('be.visible');
15-
});
16-
17-
it('has accessible aria-label', () => {
18-
cy.get('[data-testid="theme-toggle"]').should('have.attr', 'aria-label');
19-
});
20-
2113
it('clicking toggle switches to dark mode', () => {
2214
cy.get('html').should('not.have.class', 'dark');
2315
cy.get('[data-testid="theme-toggle"]').click();

packages/app/cypress/component/social-share.cy.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/app/cypress/e2e/chart-export.cy.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)