Skip to content

Commit 58c8d9f

Browse files
edwingao28wenyao
authored andcommitted
fix(zh): restore localized data workflow recovery
Propagate secondary query retry state, preserve feedback modal ARIA labels, format date-only tooltips in UTC, and make reliability failures recoverable. 中文:补齐中文数据页面的错误恢复流程,修复反馈弹窗无障碍标签、UTC 日期格式与可靠性重试。
1 parent 56f56e9 commit 58c8d9f

20 files changed

Lines changed: 410 additions & 33 deletions

‎packages/app/cypress/component/feedback-modal.cy.tsx‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ describe('FeedbackForm', () => {
4646
cy.get('@onDismiss').should('have.been.calledOnce');
4747
});
4848

49+
it('keeps engine-provided English accessible labels valid after success', () => {
50+
cy.intercept('POST', '/api/v1/feedback', { statusCode: 204 }).as('postAccessibleEn');
51+
cy.mount(
52+
<FeedbackForm
53+
onDismiss={cy.stub()}
54+
titleId="feedback-modal-title"
55+
descriptionId="feedback-modal-description"
56+
/>,
57+
);
58+
59+
cy.get('#feedback-modal-title').should('have.text', 'Help us improve InferenceX');
60+
cy.get('#feedback-modal-description').should(
61+
'have.text',
62+
"We'd love to hear what's working and what isn't.",
63+
);
64+
cy.get('[data-testid="feedback-doing-well"]').type('Clear charts');
65+
cy.get('[data-testid="feedback-modal-submit"]').click();
66+
cy.wait('@postAccessibleEn');
67+
cy.get('#feedback-modal-title').should('have.text', 'Thanks for your feedback!');
68+
cy.get('#feedback-modal-description').should('have.text', 'We read every response.');
69+
});
70+
4971
it('surfaces a 429 as a user-readable error', () => {
5072
cy.intercept('POST', '/api/v1/feedback', { statusCode: 429 }).as('post');
5173
cy.mount(<FeedbackForm onDismiss={cy.stub()} />);
@@ -71,6 +93,29 @@ describe('FeedbackForm', () => {
7193
cy.contains('感谢您的反馈!').should('be.visible');
7294
});
7395

96+
it('keeps engine-provided Chinese accessible labels valid after success', () => {
97+
cy.intercept('POST', '/api/v1/feedback', { statusCode: 204 }).as('postAccessibleZh');
98+
cy.mount(
99+
<FeedbackForm
100+
locale="zh"
101+
onDismiss={cy.stub()}
102+
titleId="feedback-modal-title"
103+
descriptionId="feedback-modal-description"
104+
/>,
105+
);
106+
107+
cy.get('#feedback-modal-title').should('have.text', '帮助我们改进 InferenceX');
108+
cy.get('#feedback-modal-description').should(
109+
'have.text',
110+
'欢迎告诉我们哪些体验不错,以及哪些地方需要改进。',
111+
);
112+
cy.get('[data-testid="feedback-doing-well"]').type('图表很清晰');
113+
cy.get('[data-testid="feedback-modal-submit"]').click();
114+
cy.wait('@postAccessibleZh');
115+
cy.get('#feedback-modal-title').should('have.text', '感谢您的反馈!');
116+
cy.get('#feedback-modal-description').should('have.text', '我们会认真阅读每一条反馈。');
117+
});
118+
74119
it('localizes rate-limit and server errors on Chinese routes', () => {
75120
cy.intercept('POST', '/api/v1/feedback', { statusCode: 429 }).as('rateLimited');
76121
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);

‎packages/app/cypress/component/reliability-bar-chart.cy.tsx‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,27 @@ import ReliabilityBarChartD3 from '@/components/reliability/ui/BarChartD3';
22
import { mountWithProviders } from '../support/test-utils';
33
import { createMockReliabilityData } from '../support/mock-data';
44
import { Model } from '@/lib/data-mappings';
5+
import { registerAnalyticsClient } from '@/lib/analytics';
56

67
describe('ReliabilityBarChartD3', () => {
7-
it('shows error message when error is set', () => {
8+
it('tracks retry before requesting reliability data again', () => {
9+
const capture = cy.stub();
10+
const refetch = cy.stub().resolves();
11+
registerAnalyticsClient({ capture });
812
mountWithProviders(<ReliabilityBarChartD3 />, {
9-
reliability: { error: 'Server error', chartData: [] },
13+
reliability: { error: 'Server error', chartData: [], refetch },
14+
});
15+
cy.get('[data-testid="reliability-error"]')
16+
.should('contain.text', 'Failed to load reliability data.')
17+
.find('button')
18+
.should('have.text', 'Retry')
19+
.click();
20+
21+
cy.then(() => {
22+
expect(capture).to.have.been.calledWith('reliability_retry_clicked');
23+
expect(refetch.callCount).to.eq(1);
24+
expect(capture).to.have.been.calledBefore(refetch);
1025
});
11-
cy.contains('Failed to load reliability data.').should('be.visible');
1226
});
1327

1428
it('shows "No reliability data" when chartData is empty', () => {

‎packages/app/cypress/e2e/historical-trends.cy.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,31 @@ describe('Historical Trends — Chinese route', () => {
225225
cy.contains('historical-database-internal-detail').should('not.exist');
226226
});
227227

228+
it('shows a distinct secondary-history error and recovers through the tracked retry', () => {
229+
cy.fixture('api/benchmarks-history.json').then((historyRows) => {
230+
let attempts = 0;
231+
cy.intercept('GET', '**/api/v1/benchmarks/history?*', (request) => {
232+
attempts += 1;
233+
request.reply(
234+
attempts <= 2
235+
? { statusCode: 500, body: { error: 'secondary-history-internal-detail' } }
236+
: { body: historyRows },
237+
);
238+
}).as('secondaryHistory');
239+
240+
cy.reload();
241+
cy.wait('@secondaryHistory');
242+
cy.wait('@secondaryHistory');
243+
cy.get('[data-testid="historical-trend-error"]')
244+
.should('contain.text', '历史趋势数据加载失败。')
245+
.and('not.contain.text', '历史基准测试数据加载失败。')
246+
.and('not.contain.text', 'secondary-history-internal-detail');
247+
cy.contains('button', '重试加载趋势数据').click();
248+
cy.wait('@secondaryHistory');
249+
cy.get('[data-testid="historical-trend-figure"]').should('be.visible');
250+
});
251+
});
252+
228253
for (const width of [375, 390]) {
229254
it(`keeps the target controls and chart reachable at ${width}px`, () => {
230255
cy.viewport(width, 844);

‎packages/app/cypress/e2e/nudge-system.cy.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,34 @@ function clearAllNudgeStorage(win: Cypress.AUTWindow) {
2121
'inferencex-gradient-nudge-shown',
2222
'inferencex-eval-samples-nudge-dismissed',
2323
'inferencex-filter-hint-nudge-dismissed',
24+
'inferencex-feedback-modal-snoozed',
25+
'inferencex-feedback-modal-submitted',
2426
];
2527
for (const key of keys) {
2628
win.localStorage.removeItem(key);
2729
win.sessionStorage.removeItem(key);
2830
}
2931
}
3032

33+
describe('Dashboard feedback modal accessibility', () => {
34+
it('has a valid English accessible name and description', () => {
35+
cy.visit('/inference', {
36+
onBeforeLoad: clearAllNudgeStorage,
37+
});
38+
39+
cy.get('[data-testid="feedback-modal"]')
40+
.should('be.visible')
41+
.and('have.attr', 'role', 'dialog')
42+
.and('have.attr', 'aria-labelledby', 'feedback-modal-title')
43+
.and('have.attr', 'aria-describedby', 'feedback-modal-description');
44+
cy.get('#feedback-modal-title').should('have.text', 'Help us improve InferenceX');
45+
cy.get('#feedback-modal-description').should(
46+
'have.text',
47+
"We'd love to hear what's working and what isn't.",
48+
);
49+
});
50+
});
51+
3152
// `cypress.config.ts` runs with `testIsolation: false` — the browser context
3253
// (incl. localStorage / sessionStorage) survives across tests in this spec.
3354
// Defensively clear before each test so a missed `onBeforeLoad` in any test

‎packages/app/cypress/e2e/reliability-chart.cy.ts‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,28 @@ describe('Reliability Chart — Chinese route and settled states', () => {
176176
cy.contains('正在加载可靠性数据……').should('not.exist');
177177
});
178178

179-
it('shows a safe Chinese error instead of a raw API response', () => {
180-
cy.intercept('GET', '**/api/v1/reliability', {
181-
statusCode: 500,
182-
body: { error: 'database-internal-detail' },
183-
}).as('failedReliability');
179+
it('shows a safe Chinese error and recovers through the tracked retry control', () => {
180+
let attempts = 0;
181+
cy.fixture('api/reliability.json').then((fixture) => {
182+
cy.intercept('GET', '**/api/v1/reliability', (req) => {
183+
attempts += 1;
184+
req.reply(
185+
attempts <= 2
186+
? { statusCode: 500, body: { error: 'database-internal-detail' } }
187+
: { statusCode: 200, body: fixture },
188+
);
189+
}).as('retryReliability');
190+
});
184191
cy.visit('/zh/reliability');
185-
cy.wait('@failedReliability');
186-
cy.contains('可靠性数据加载失败。').should('be.visible');
192+
cy.wait('@retryReliability');
193+
cy.wait('@retryReliability');
194+
cy.get('[data-testid="reliability-error"]')
195+
.should('be.visible')
196+
.and('contain.text', '可靠性数据加载失败。');
187197
cy.contains('database-internal-detail').should('not.exist');
198+
cy.contains('[data-testid="reliability-error"] button', '重试').click();
199+
cy.wait('@retryReliability');
200+
cy.get('#reliability-chart svg rect.bar').should('have.length.greaterThan', 0);
188201
});
189202

190203
for (const width of [375, 390]) {

‎packages/app/cypress/e2e/zh-pages.cy.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,16 @@ describe('Chinese (/zh) pages', () => {
283283

284284
cy.get('[data-testid="feedback-modal"]')
285285
.should('be.visible')
286+
.and('have.attr', 'role', 'dialog')
287+
.and('have.attr', 'aria-labelledby', 'feedback-modal-title')
288+
.and('have.attr', 'aria-describedby', 'feedback-modal-description')
286289
.and('contain.text', '帮助我们改进 InferenceX')
287290
.and('contain.text', '您的反馈会加密保存');
291+
cy.get('#feedback-modal-title').should('have.text', '帮助我们改进 InferenceX');
292+
cy.get('#feedback-modal-description').should(
293+
'have.text',
294+
'欢迎告诉我们哪些体验不错,以及哪些地方需要改进。',
295+
);
288296
cy.get('[data-testid="feedback-modal-dismiss"]').click();
289297
cy.get('[data-testid="feedback-modal"]').should('not.exist');
290298
});

‎packages/app/cypress/support/mock-data.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ export function createMockReliabilityContext(
405405
return {
406406
loading: false,
407407
error: null,
408+
refetch: namedStub('refetchReliability').resolves(),
408409
dateRangeSuccessRateData: {
409410
'last-7-days': {
410411
[Model.DeepSeek_R1]: {

‎packages/app/src/components/feedback-modal.tsx‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface FeedbackFormProps {
2222
onDismiss: () => void;
2323
/** Test/embedded-surface override. Production defaults to the current route locale. */
2424
locale?: Locale;
25+
/** Engine-owned IDs referenced by the containing dialog. */
26+
titleId?: string;
27+
descriptionId?: string;
2528
}
2629

2730
const STRINGS = {
@@ -63,7 +66,12 @@ const STRINGS = {
6366
},
6467
} as const;
6568

66-
export function FeedbackForm({ onDismiss, locale: localeOverride }: FeedbackFormProps) {
69+
export function FeedbackForm({
70+
onDismiss,
71+
locale: localeOverride,
72+
titleId: titleIdOverride,
73+
descriptionId: descriptionIdOverride,
74+
}: FeedbackFormProps) {
6775
const [doingWell, setDoingWell] = useState('');
6876
const [doingPoorly, setDoingPoorly] = useState('');
6977
const [wantToSee, setWantToSee] = useState('');
@@ -74,8 +82,10 @@ export function FeedbackForm({ onDismiss, locale: localeOverride }: FeedbackForm
7482
const routeLocale = useLocale();
7583
const locale = localeOverride ?? routeLocale;
7684
const t = STRINGS[locale];
77-
const titleId = useId();
78-
const descId = useId();
85+
const generatedTitleId = useId();
86+
const generatedDescriptionId = useId();
87+
const titleId = titleIdOverride ?? generatedTitleId;
88+
const descriptionId = descriptionIdOverride ?? generatedDescriptionId;
7989

8090
const handleSubmit = useCallback(async () => {
8191
if (status === 'submitting') return;
@@ -149,7 +159,7 @@ export function FeedbackForm({ onDismiss, locale: localeOverride }: FeedbackForm
149159
<h2 id={titleId} className="text-lg font-semibold">
150160
{t.successTitle}
151161
</h2>
152-
<p id={descId} className="text-sm text-muted-foreground">
162+
<p id={descriptionId} className="text-sm text-muted-foreground">
153163
{t.successBody}
154164
</p>
155165
</div>
@@ -163,7 +173,7 @@ export function FeedbackForm({ onDismiss, locale: localeOverride }: FeedbackForm
163173
<MessageSquareText className="size-5 text-brand" />
164174
{t.title}
165175
</h2>
166-
<p id={descId} className="text-sm text-muted-foreground">
176+
<p id={descriptionId} className="text-sm text-muted-foreground">
167177
{t.description}
168178
</p>
169179
</div>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// @vitest-environment jsdom
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3+
import { act, createElement } from 'react';
4+
import { createRoot, type Root } from 'react-dom/client';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
7+
import { Model, Sequence } from '@/lib/data-mappings';
8+
9+
const { mockFetchBenchmarkHistory } = vi.hoisted(() => ({
10+
mockFetchBenchmarkHistory: vi.fn(),
11+
}));
12+
vi.mock('@/lib/api', () => ({ fetchBenchmarkHistory: mockFetchBenchmarkHistory }));
13+
14+
import { useInterpolatedTrendData } from './useInterpolatedTrendData';
15+
16+
let observed:
17+
| {
18+
error?: Error | null;
19+
refetch?: () => Promise<unknown>;
20+
}
21+
| undefined;
22+
23+
function Probe() {
24+
observed = useInterpolatedTrendData({
25+
selectedModel: Model.DeepSeek_R1,
26+
selectedSequence: Sequence.OneK_OneK,
27+
selectedPrecisions: ['fp8'],
28+
selectedYAxisMetric: 'y_tpPerGpu',
29+
targetInteractivity: 40,
30+
availableDates: [],
31+
enabled: true,
32+
});
33+
return null;
34+
}
35+
36+
describe('useInterpolatedTrendData query state', () => {
37+
let root: Root | undefined;
38+
39+
afterEach(() => {
40+
if (root) act(() => root?.unmount());
41+
root = undefined;
42+
observed = undefined;
43+
vi.clearAllMocks();
44+
});
45+
46+
it('propagates a history failure and exposes a refetch that can recover', async () => {
47+
mockFetchBenchmarkHistory.mockRejectedValueOnce(new Error('secondary history failed'));
48+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
49+
root = createRoot(document.createElement('div'));
50+
51+
await act(() => {
52+
root?.render(createElement(QueryClientProvider, { client }, createElement(Probe)));
53+
});
54+
55+
await vi.waitFor(() => expect(observed?.error?.message).toBe('secondary history failed'));
56+
expect(observed?.refetch).toBeTypeOf('function');
57+
58+
mockFetchBenchmarkHistory.mockResolvedValueOnce([]);
59+
await act(async () => {
60+
await observed?.refetch?.();
61+
});
62+
63+
await vi.waitFor(() => expect(observed?.error).toBeNull());
64+
});
65+
});

‎packages/app/src/components/inference/hooks/useInterpolatedTrendData.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ interface UseInterpolatedTrendDataResult {
294294
hwKeysWithData: string[];
295295
loading: boolean;
296296
progress: number;
297+
error: Error | null;
298+
refetch: () => Promise<unknown>;
297299
}
298300

299301
/**
@@ -314,7 +316,12 @@ export function useInterpolatedTrendData({
314316
}: UseInterpolatedTrendDataParams): UseInterpolatedTrendDataResult {
315317
const seqIslOsl = useMemo(() => sequenceToIslOsl(selectedSequence), [selectedSequence]);
316318

317-
const { data: allRows, isLoading } = useBenchmarkHistory(
319+
const {
320+
data: allRows,
321+
isLoading,
322+
error,
323+
refetch,
324+
} = useBenchmarkHistory(
318325
enabled ? selectedModel : '',
319326
seqIslOsl?.isl ?? 0,
320327
seqIslOsl?.osl ?? 0,
@@ -437,8 +444,10 @@ export function useInterpolatedTrendData({
437444
hwKeysWithData: [],
438445
loading: false,
439446
progress: 0,
447+
error: null,
448+
refetch,
440449
};
441450
}
442451

443-
return { trendLines, hwKeysWithData, loading: isLoading, progress };
452+
return { trendLines, hwKeysWithData, loading: isLoading, progress, error, refetch };
444453
}

0 commit comments

Comments
 (0)