Skip to content
Draft
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
102 changes: 68 additions & 34 deletions packages/app/cypress/e2e/overview.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ const SCOPE_LINE = `${SCOPE_METRIC} · ${SCOPE_DIRECTION} · ${SOURCE_NOTE}`;
const SCOPE_METRIC_ZH = '超大规模云(hyperscaler)成本';
const SCOPE_DIRECTION_ZH = '↓ 越低越好';
const SCOPE_LINE_ZH = `${SCOPE_METRIC_ZH} · ${SCOPE_DIRECTION_ZH} · ${SOURCE_NOTE_ZH}`;
const OVERVIEW_TIERS = [30, 50, 75, 100, 150, 200] as const;

function selectOverviewTier(tier: (typeof OVERVIEW_TIERS)[number]) {
const targetIndex = OVERVIEW_TIERS.indexOf(tier);
return cy.get<HTMLInputElement>('[data-testid="overview-tier-slider"]').then(([slider]) => {
const view = slider.ownerDocument.defaultView;
if (view === null) throw new Error('Slider has no window');
const nativeValueSetter = Object.getOwnPropertyDescriptor(
view.HTMLInputElement.prototype,
'value',
)?.set;
if (nativeValueSetter === undefined) throw new Error('Range input has no native value setter');
nativeValueSetter.call(slider, String(targetIndex));
slider.dispatchEvent(new view.Event('input', { bubbles: true }));
});
}

function expectNoHorizontalOverflow() {
cy.document().then((doc) => {
Expand Down Expand Up @@ -137,14 +153,14 @@ describe('Overview page', () => {
'preserved';
});

cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click();
selectOverviewTier(75);
cy.wait('@overviewJson');
cy.location('search', { timeout: 15_000 }).should('eq', '?tier=75');
cy.window().its('__overviewNavigationSentinel').should('eq', 'preserved');

cy.get('[data-testid="overview-tier-switcher"]').contains('a', '50').click();
selectOverviewTier(50);
cy.location('search').should('eq', '');
cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click();
selectOverviewTier(75);
cy.location('search').should('eq', '?tier=75');
cy.then(() => {
expect(jsonRequests, 'one request; both visited selections are cached').to.equal(1);
Expand Down Expand Up @@ -179,7 +195,7 @@ describe('Overview page', () => {
cy.viewport(1280, 900);
cy.visit('/overview');
cy.intercept('GET', '**/api/v1/overview*').as('overviewJson');
cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click();
selectOverviewTier(75);
cy.wait('@overviewJson');
cy.location('search').should('eq', '?tier=75');

Expand All @@ -205,7 +221,7 @@ describe('Overview page', () => {
cy.viewport(1280, 900);
cy.visit('/overview');

cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click();
selectOverviewTier(75);
cy.get('[data-testid="overview-engine-scope-switcher"]')
.find('[data-overview-engine-scope="all"]')
.click();
Expand Down Expand Up @@ -240,7 +256,7 @@ describe('Overview page', () => {
);
});

cy.get('[data-testid="overview-tier-switcher"]').contains('a', '75').click();
selectOverviewTier(75);
cy.location('search').should('eq', '?tier=75&models=all');
cy.get('[data-testid="overview-desktop-model"][data-model="gpt-oss-120b"]').should('exist');

Expand Down Expand Up @@ -284,22 +300,21 @@ describe('Overview page', () => {
platform('b200').find('[data-testid="overview-cost-delta"]').should('exist');
});

cy.get('[data-testid="overview-tier-switcher"]')
.contains('a', '100')
.should('have.attr', 'href', '/overview?tier=100&ref=b300');
selectOverviewTier(100);
cy.location('search').should('eq', '?tier=100&ref=b300');
cy.get('[data-testid="overview-engine-scope-switcher"]')
.find('[data-overview-engine-scope="all"]')
.should('have.attr', 'href', '/overview?engine=all&ref=b300');
.should('have.attr', 'href', '/overview?tier=100&engine=all&ref=b300');
cy.get('[data-overview-comparison="history"]').should(
'have.attr',
'href',
'/overview?ref=b300&compare=30d',
'/overview?tier=100&ref=b300&compare=30d',
);
cy.get('[data-testid="language-toggle"]')
.should('have.attr', 'href', '/zh/overview?ref=b300')
.should('have.attr', 'href', '/zh/overview?tier=100&ref=b300')
.click();
cy.location('pathname').should('eq', '/zh/overview');
cy.location('search').should('eq', '?ref=b300');
cy.location('search').should('eq', '?tier=100&ref=b300');
cy.get('[data-overview-comparison="hardware"]').should('contain.text', '对比 B300');
});

Expand Down Expand Up @@ -345,17 +360,16 @@ describe('Overview page', () => {
cy.get('[data-testid="overview-desktop-matrix"] thead').should('contain.text', 'B200');
cy.get('[data-testid="overview-desktop-matrix"] thead').should('not.contain.text', 'Reference');

cy.get('[data-testid="overview-tier-switcher"]')
.contains('a', '100')
.should('have.attr', 'href', '/overview?tier=100&compare=30d');
selectOverviewTier(100);
cy.location('search').should('eq', '?tier=100&compare=30d');
cy.get('[data-testid="overview-engine-scope-switcher"]')
.find('[data-overview-engine-scope="all"]')
.should('have.attr', 'href', '/overview?engine=all&compare=30d');
.should('have.attr', 'href', '/overview?tier=100&engine=all&compare=30d');
cy.get('[data-testid="language-toggle"]')
.should('have.attr', 'href', '/zh/overview?compare=30d')
.should('have.attr', 'href', '/zh/overview?tier=100&compare=30d')
.click();
cy.location('pathname').should('eq', '/zh/overview');
cy.location('search').should('eq', '?compare=30d');
cy.location('search').should('eq', '?tier=100&compare=30d');
cy.get('[data-testid="overview-comparison-switcher"]')
.should('have.attr', 'aria-label', '对比方式')
.within(() => {
Expand Down Expand Up @@ -533,10 +547,7 @@ describe('Overview page', () => {
).click();
cy.location('search').should('eq', '?engine=all');
desktopModel('GLM-5.2').find('[data-testid="overview-pair-missing"]').should('have.length', 5);
cy.get('[data-testid="overview-tier-switcher"]')
.contains('a', '100')
.should('have.attr', 'href', '/overview?tier=100&engine=all')
.click();
selectOverviewTier(100);
cy.location('search').should('eq', '?tier=100&engine=all');

cy.get('[data-testid="overview-engine-scope-switcher"]')
Expand Down Expand Up @@ -956,7 +967,7 @@ describe('Overview page', () => {
.should('not.match', /∞\s*%/);
});

it('re-renders the whole matrix at the service level the URL names, via plain links', () => {
it('re-renders the whole matrix from the six-stop SLO slider', () => {
cy.viewport(1280, 900);
cy.visit('/overview');

Expand All @@ -966,22 +977,42 @@ describe('Overview page', () => {
.and('contain.text', 'SLO');
cy.get('body').should('not.contain.text', 'Service level');
cy.get('[data-testid="overview-tier-switcher"]').within(() => {
cy.get('[aria-current="page"]').should('have.text', '50');
// 30 / 75 / 100 / 150 / 200 link out; the active 50 is inert text.
cy.get('a').should('have.length', 5);
cy.contains('a', '30').should('have.attr', 'href', '/overview?tier=30');
cy.contains('a', '150').should('have.attr', 'href', '/overview?tier=150');
cy.contains('a', '200').should('have.attr', 'href', '/overview?tier=200');
cy.contains('a', '100').should('have.attr', 'href', '/overview?tier=100').click();
cy.get('a, button').should('not.exist');
cy.get('[data-tier-option]').should('have.length', 6);
cy.get('[data-tier-option="50"]').should('have.attr', 'data-selected', 'true');
cy.get('[data-testid="overview-tier-ridge"]').should('have.length', 6);
cy.get('[data-tier-ridge="30"]').should('have.attr', 'data-state', 'filled');
cy.get('[data-tier-ridge="50"]').should('have.attr', 'data-state', 'selected');
cy.get('[data-tier-ridge="75"]').should('have.attr', 'data-state', 'unfilled');
cy.get('[data-testid="overview-tier-slider"]')
.should('have.attr', 'min', '0')
.and('have.attr', 'max', '5')
.and('have.attr', 'step', '1')
.and('have.value', '1')
.and('have.attr', 'data-tier', '50')
.and('have.attr', 'aria-valuetext', '50 tok/s/user');
});

selectOverviewTier(100);

cy.location('search').should('eq', '?tier=100');
// The metric line never repeats the tier — the switcher states it.
cy.get('[data-testid="overview-scope"]').should('have.text', SCOPE_LINE);
cy.get('[data-testid="overview-tier-switcher"]').within(() => {
cy.get('[aria-current="page"]').should('have.text', '100');
cy.contains('a', '50').should('have.attr', 'href', '/overview');
cy.get('[data-tier-option="100"]').should('have.attr', 'data-selected', 'true');
cy.get('[data-tier-ridge="75"]').should('have.attr', 'data-state', 'filled');
cy.get('[data-tier-ridge="100"]').should('have.attr', 'data-state', 'selected');
cy.get('[data-tier-ridge="150"]').should('have.attr', 'data-state', 'unfilled');
cy.get('[data-testid="overview-tier-slider"]')
.should('have.attr', 'type', 'range')
.should('have.value', '3')
.and('have.attr', 'data-tier', '100')
.and('have.attr', 'aria-valuetext', '100 tok/s/user');
});
selectOverviewTier(75);
cy.location('search').should('eq', '?tier=75');
selectOverviewTier(100);
cy.location('search').should('eq', '?tier=100');

desktopModel('Qwen-3.5-397B-A17B', SINGLE_TURN).within(() => {
platform('b200').should('contain.text', '$0.124').and('contain.text', 'FP8');
Expand Down Expand Up @@ -1328,7 +1359,10 @@ describe('Overview page', () => {
.and('contain.text', 'SLO');
cy.get('body').should('not.contain.text', '服务档位');
cy.get('[data-testid="overview-tier-switcher"]').within(() => {
cy.contains('a', '50').should('have.attr', 'href', '/zh/overview');
cy.get('[data-testid="overview-tier-slider"]')
.should('have.attr', 'data-tier', '100')
.and('have.attr', 'aria-valuetext', '100 tok/s/用户');
cy.get('[data-tier-option]').should('have.length', 6);
});
});
});
55 changes: 12 additions & 43 deletions packages/app/src/components/overview/overview-scorecard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
OVERVIEW_DEFAULT_REFERENCE_HARDWARE,
OVERVIEW_HARDWARE,
OVERVIEW_TIERS,
overviewHardwareLabel,
type OverviewComparisonMode,
type OverviewEngineScope,
Expand All @@ -18,13 +17,13 @@ import {
detailHref,
overviewEngineScopeHref,
overviewHref,
overviewTierHref,
} from '@/lib/overview-links';

import { OverviewDetailLink } from './overview-detail-link';
import { OverviewHistoryDetailLink } from './overview-history-detail-link';
import { OverviewNavLink } from './overview-nav-link';
import { OverviewReferenceSelect } from './overview-reference-select';
import { OverviewTierSlider } from './overview-tier-slider';

export type OverviewLocale = 'en' | 'zh';

Expand Down Expand Up @@ -803,8 +802,7 @@ export function MobileOverviewList({
);
}

/** Every option remains a copyable server-rendered URL; ordinary clicks use a
* soft App Router transition and the displayed tier is never a self-link. */
/** The six benchmarked service levels are exposed as discrete slider stops. */
export function OverviewTierSwitcher({
tier,
engineScope,
Expand All @@ -822,46 +820,17 @@ export function OverviewTierSwitcher({
locale: OverviewLocale;
strings: OverviewStrings;
}) {
const optionClass = 'inline-flex min-h-11 items-center px-3 tabular-nums';
return (
<nav
data-testid="overview-tier-switcher"
aria-label={strings.tierNavLabel}
className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs"
>
<span className="text-muted-foreground">{strings.tierNavLabel}</span>
<div className="flex divide-x divide-border/60 overflow-hidden rounded-md border border-border/60">
{OVERVIEW_TIERS.map((option) =>
option === tier ? (
<span
key={option}
aria-current="page"
className={`${optionClass} bg-foreground font-semibold text-background`}
>
{option}
</span>
) : (
<OverviewNavLink
key={option}
href={overviewTierHref(
locale,
option,
engineScope,
comparisonMode,
referenceHardware,
modelScope,
)}
analytics={{ control: 'tier', value: String(option) }}
searchKeys={['tier']}
className={`${optionClass} text-muted-foreground transition-colors hover:bg-muted hover:text-foreground`}
>
{option}
</OverviewNavLink>
),
)}
</div>
<span className="text-muted-foreground">{strings.tierUnit}</span>
</nav>
<OverviewTierSlider
tier={tier}
engineScope={engineScope}
comparisonMode={comparisonMode}
referenceHardware={referenceHardware}
modelScope={modelScope}
locale={locale}
label={strings.tierNavLabel}
unit={strings.tierUnit}
/>
);
}

Expand Down
Loading