Skip to content

Commit 6252482

Browse files
committed
Merge branch 'master' into feat/blog-infra
2 parents 58bf72f + 918dbae commit 6252482

50 files changed

Lines changed: 935 additions & 900 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/adding-entities.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Present what you inferred and ask about anything not visible in artifacts:
8989
4. What are the **cost rates** in $/GPU/hr? (hyperscaler, neocloud, 3-year rental)
9090
5. Which **framework variants** will run on it? (e.g. `sglang`, `trt`, `dynamo-trt`, `vllm`, with/without `mtp`)
9191
6. Where should it **sort** relative to existing GPUs in legends?
92-
7. Are there any **new artifact suffixes** for this GPU beyond the existing ones (`-trt`, `-nv`, `-amds`, `-amd`, `-nvd`, `-nvs`, `-disagg`, `-multinode-slurm`, `-dgxc`, `-nb`)?
92+
7. Are there any **new artifact suffixes** for this GPU beyond the existing ones (`-trt`, `-nv`, `-amds`, `-amd`, `-nvd`, `-nvs`, `-disagg`, `-multinode-slurm`, `-dgxc-slurm`, `-dgxc`, `-nb`)?
9393
8. Do you have the **full hardware specs** for the GPU Specs tab? (memory GB, memory bandwidth TB/s, FP4/FP8/BF16 TFLOPS, interconnect tech, scale-up bandwidth, NIC model, scale-out topology)
9494

9595
### Then apply
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: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,47 @@ 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');
2521
cy.get('[data-testid="footer-copyright"]').should('contain', year);
2622
});
2723

28-
it('shows the GitHub star CTA linking to GitHub repo', () => {
24+
it('shows the GitHub star CTA', () => {
2925
cy.get('[data-testid="footer-star-cta"]').should('be.visible');
30-
cy.get('[data-testid="footer-star-cta"]')
31-
.should('have.attr', 'href')
32-
.and('include', 'github.com/SemiAnalysisAI/InferenceX');
33-
});
34-
35-
it('footer star CTA opens in new tab', () => {
36-
cy.get('[data-testid="footer-star-cta"]').should('have.attr', 'target', '_blank');
26+
cy.get('[data-testid="footer-star-cta"]').should('contain', 'Star');
3727
});
3828

3929
it('shows social share buttons', () => {
40-
cy.get('[data-testid="social-share-buttons"]').should('be.visible');
30+
cy.get('[data-testid="footer-social-buttons"]').should('be.visible');
31+
cy.get('[data-testid="footer-social-buttons"]')
32+
.find('button')
33+
.should('have.length.greaterThan', 1);
4134
});
4235

4336
it('has Privacy Policy link', () => {
44-
cy.get('[data-testid="footer"]')
45-
.contains('Privacy Policy')
37+
cy.get('[data-testid="footer-link-privacy"]')
4638
.should('have.attr', 'href')
4739
.and('include', 'semianalysis.com/privacy-policy');
4840
});
4941

5042
it('has Cookie Policy link', () => {
51-
cy.get('[data-testid="footer"]')
52-
.contains('Cookie Policy')
43+
cy.get('[data-testid="footer-link-cookies"]')
5344
.should('have.attr', 'href')
5445
.and('include', 'semianalysis.com/cookie-policy');
5546
});
5647

57-
it('has Contribute link pointing to GitHub', () => {
58-
cy.get('[data-testid="footer"]')
59-
.contains('Contribute')
48+
it('has Contribute section with GitHub links', () => {
49+
cy.get('[data-testid="footer-link-benchmarks"]')
6050
.should('have.attr', 'href')
6151
.and('include', 'github.com/SemiAnalysisAI/InferenceX');
62-
});
63-
64-
it('shows the SemiAnalysis logo', () => {
65-
cy.get('[data-testid="footer"]').find('img[alt="SemiAnalysis logo"]').should('exist');
52+
cy.get('[data-testid="footer-link-frontend"]')
53+
.should('have.attr', 'href')
54+
.and('include', 'github.com/SemiAnalysisAI/InferenceX-app');
6655
});
6756

6857
it('all external links open in a new tab', () => {
69-
cy.get('[data-testid="footer"]')
58+
cy.get('[data-testid="footer-links"]')
7059
.find('a[target="_blank"]')
7160
.should('have.length.greaterThan', 0);
7261
});

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();

0 commit comments

Comments
 (0)