Skip to content
Merged
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
70 changes: 67 additions & 3 deletions packages/app/cypress/component/feedback-modal.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,19 @@ describe('FeedbackForm', () => {
it('POSTs to /api/v1/feedback, dispatches FEEDBACK_SUBMITTED_EVENT, then dismisses', () => {
cy.intercept('POST', '/api/v1/feedback', { statusCode: 204 }).as('post');
const onDismiss = cy.stub().as('onDismiss');
cy.mount(<FeedbackForm onDismiss={onDismiss} />);
cy.mount(
<FeedbackForm
onDismiss={onDismiss}
titleId="feedback-modal-title"
descriptionId="feedback-modal-description"
/>,
);

cy.get('#feedback-modal-title').should('have.text', 'Help us improve InferenceX');
cy.get('#feedback-modal-description').should(
'have.text',
"We'd love to hear what's working and what isn't.",
);

let submittedFired = false;
cy.window().then((win) => {
Expand All @@ -40,10 +52,11 @@ describe('FeedbackForm', () => {
cy.get('[data-testid="feedback-doing-well"]').type('useful chart!');
cy.get('[data-testid="feedback-modal-submit"]').click();
cy.wait('@post');
cy.contains('Thanks for your feedback!').should('be.visible');
cy.get('#feedback-modal-title').should('have.text', 'Thanks for your feedback!');
cy.get('#feedback-modal-description').should('have.text', 'We read every response.');
cy.then(() => expect(submittedFired).to.be.true);
// Success-hold is 2s; onDismiss fires after.
cy.get('@onDismiss', { timeout: 3000 }).should('have.been.calledOnce');
cy.get('@onDismiss').should('have.been.calledOnce');
});

it('surfaces a 429 as a user-readable error', () => {
Expand All @@ -54,4 +67,55 @@ describe('FeedbackForm', () => {
cy.wait('@post');
cy.contains('Too many submissions').should('be.visible');
});

it('renders validation, privacy, controls, and success in Chinese', () => {
cy.viewport(1440, 900);
cy.intercept('POST', '/api/v1/feedback', { statusCode: 204 }).as('postZh');
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);

cy.contains('h2', '帮助我们改进 InferenceX').should('be.visible');
cy.contains('您的反馈会加密保存').should('be.visible');
cy.get('[data-testid="feedback-modal-submit"]').click();
cy.contains('[role="alert"]', '请至少填写一项。').should('be.visible');

cy.get('[data-testid="feedback-doing-well"]').type('图表很清晰');
cy.get('[data-testid="feedback-modal-submit"]').click();
cy.wait('@postZh');
cy.contains('感谢您的反馈!').should('be.visible');
});

for (const [statusCode, expected] of [
[400, '提交未通过校验,请检查填写内容后重试。'],
[429, '提交次数过多,请稍后再试。'],
[500, '反馈保存失败,请重试。'],
] as const) {
it(`localizes a ${statusCode} response on Chinese routes`, () => {
cy.intercept('POST', '/api/v1/feedback', { statusCode }).as('failedSubmission');
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
cy.get('[data-testid="feedback-doing-well"]').type('反馈');
cy.get('[data-testid="feedback-modal-submit"]').click();
cy.wait('@failedSubmission');
cy.contains('[role="alert"]', expected).should('be.visible');
});
}

it('does not expose a raw network error on Chinese routes', () => {
cy.intercept('POST', '/api/v1/feedback', { forceNetworkError: true }).as('networkFailure');
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
cy.get('[data-testid="feedback-doing-well"]').type('反馈');
cy.get('[data-testid="feedback-modal-submit"]').click();
cy.wait('@networkFailure');
cy.contains('[role="alert"]', '出现意外错误,请重试。').should('be.visible');
cy.contains('Failed to fetch').should('not.exist');
});

it('keeps the Chinese form inside a 375px viewport', () => {
cy.viewport(375, 667);
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);

cy.get('[data-testid="feedback-modal-submit"]').should('be.visible');
cy.document().then((doc) => {
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
});
});
});
19 changes: 16 additions & 3 deletions packages/app/cypress/component/reliability-bar-chart.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ import ReliabilityBarChartD3 from '@/components/reliability/ui/BarChartD3';
import { mountWithProviders } from '../support/test-utils';
import { createMockReliabilityData } from '../support/mock-data';
import { Model } from '@/lib/data-mappings';
import { registerAnalyticsClient } from '@/lib/analytics';

describe('ReliabilityBarChartD3', () => {
it('shows error message when error is set', () => {
it('tracks retry and requests reliability data again', () => {
const capture = cy.stub();
const refetch = cy.stub().resolves();
registerAnalyticsClient({ capture });
mountWithProviders(<ReliabilityBarChartD3 />, {
reliability: { error: 'Server error', chartData: [] },
reliability: { error: 'Server error', chartData: [], refetch },
});
cy.get('[data-testid="reliability-error"]')
.should('contain.text', 'Failed to load reliability data.')
.find('button')
.should('have.text', 'Retry')
.click();

cy.then(() => {
expect(capture).to.have.been.calledWith('reliability_retry_clicked');
expect(refetch.callCount).to.eq(1);
});
cy.contains('Failed to load reliability data.').should('be.visible');
});

it('shows "No reliability data" when chartData is empty', () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/app/cypress/component/tab-nav.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ describe('TabNav — Hidden popover for gated tabs', () => {
cy.get('[data-testid="tab-trigger-collectivex"]').should('not.exist');
});

it('still names the current gated page in the locked mobile selector', () => {
cy.viewport(390, 844);
cy.window().then((win) => win.localStorage.removeItem('inferencex-feature-gate'));
mountTabNav({ pathname: '/zh/ai-chart' });
cy.get('[data-testid="mobile-chart-select"]')
.should('be.visible')
.and('contain.text', 'AI 图表');
});

it('renders the Hidden trigger when unlocked; popover reveals gated links', () => {
cy.window().then((win) => win.localStorage.setItem('inferencex-feature-gate', '1'));
mountTabNav({});
Expand Down
168 changes: 168 additions & 0 deletions packages/app/cypress/e2e/ai-chart.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,171 @@ describe('AI chart metric semantics', () => {
});
});
});

describe('AI chart Chinese workflow', () => {
it('requests Chinese presentation copy and renders the returned chart and summary', () => {
cy.viewport(1440, 900);
cy.fixture<FixtureRow[]>('api/benchmarks.json').then((fixtureRows) => {
const rows = [
fixtureRow(fixtureRows, 'b200', 40, 12_000),
fixtureRow(fixtureRows, 'mi355x', 40, 10_000),
];
const spec = benchmarkSpec({
title: '每芯片吞吐量对比',
description: '在目标交互性下对比两种芯片。',
yAxisLabel: '每芯片吞吐量',
});

cy.intercept('GET', '**/api/v1/benchmarks?*', rows).as('zhBenchmarks');
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', (request) => {
const systemPrompt = request.body.messages?.[0]?.content ?? '';
if (systemPrompt.includes('chart generation assistant')) {
expect(systemPrompt).to.contain('natural Simplified Chinese');
request.reply({ choices: [{ message: { content: JSON.stringify(spec) } }] });
return;
}

expect(systemPrompt).to.contain('用自然、准确的简体中文回答');
request.reply({ choices: [{ message: { content: 'B200 在该配置下吞吐量更高。' } }] });
}).as('zhOpenAi');

cy.visit('/zh/ai-chart');
cy.get('input[placeholder="OpenAI API Key"]').type('test-api-key', { log: false });
cy.get('textarea[placeholder="描述想查看的图表……"]').type(
'对比 B200 和 MI355X 的每芯片吞吐量',
);
cy.contains('button', '生成图表').click();

cy.wait('@zhBenchmarks');
cy.get('#ai-chart-bar-export')
.should('contain.text', '每芯片吞吐量对比')
.and('contain.text', '在目标交互性下对比两种芯片。')
.and('contain.text', '每芯片吞吐量');
cy.get('[role="img"][aria-label="AI 生成的条形图"]').should('be.visible');
cy.contains('[data-slot="card-title"]', 'AI 总结').should('be.visible');
cy.contains('B200 在该配置下吞吐量更高。').should('be.visible');
});
});

it('keeps interactive scatter and line charts exposed as accessible groups', () => {
cy.fixture<FixtureRow[]>('api/benchmarks.json').then((fixtureRows) => {
const rows = [
fixtureRow(fixtureRows, 'b200', 20, 8_000),
fixtureRow(fixtureRows, 'b200', 40, 12_000),
fixtureRow(fixtureRows, 'mi355x', 20, 7_000),
fixtureRow(fixtureRows, 'mi355x', 40, 10_000),
];
const specs = [
benchmarkSpec({ chartType: 'scatter', title: '交互式散点图' }),
benchmarkSpec({ chartType: 'line', title: '交互式折线图' }),
];

cy.intercept('GET', '**/api/v1/benchmarks?*', rows).as('interactiveBenchmarks');
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', (request) => {
const systemPrompt = request.body.messages?.[0]?.content ?? '';
request.reply({
choices: [
{
message: {
content: systemPrompt.includes('chart generation assistant')
? JSON.stringify(specs)
: '已生成两张交互式图表。',
},
},
],
});
});

cy.visit('/zh/ai-chart');
cy.get('input[placeholder="OpenAI API Key"]').type('test-api-key', { log: false });
cy.get('textarea[placeholder="描述想查看的图表……"]').type('生成散点图和折线图');
cy.contains('button', '生成图表').click();
cy.wait('@interactiveBenchmarks');

cy.get('[role="group"][aria-label="AI 生成的散点图"]').should('be.visible');
cy.get('[role="group"][aria-label="AI 生成的折线图"]').should('be.visible');
cy.get('[role="img"][aria-label="AI 生成的散点图"]').should('not.exist');
cy.get('[role="img"][aria-label="AI 生成的折线图"]').should('not.exist');
});
});

it('shows a localized empty state for one unmatched chart in a multi-chart result', () => {
cy.fixture<FixtureRow[]>('api/benchmarks.json').then((fixtureRows) => {
const rows = [fixtureRow(fixtureRows, 'b200', 40, 12_000)];
const specs = [
benchmarkSpec({
title: 'B200 吞吐量',
description: '实测配置。',
hardwareKeys: ['b200'],
yAxisLabel: '每芯片吞吐量',
}),
benchmarkSpec({
title: 'H100 吞吐量',
description: '当前数据集中没有匹配项。',
hardwareKeys: ['h100'],
yAxisLabel: '每芯片吞吐量',
}),
];

cy.intercept('GET', '**/api/v1/benchmarks?*', rows).as('multiChartBenchmarks');
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', (request) => {
const systemPrompt = request.body.messages?.[0]?.content ?? '';
request.reply({
choices: [
{
message: {
content: systemPrompt.includes('chart generation assistant')
? JSON.stringify(specs)
: '已对比可用数据。',
},
},
],
});
});

cy.visit('/zh/ai-chart');
cy.get('input[placeholder="OpenAI API Key"]').type('test-api-key', { log: false });
cy.get('textarea').type('生成两张吞吐量图表');
cy.contains('button', '生成图表').click();

cy.wait('@multiChartBenchmarks');
cy.contains('H100 吞吐量')
.closest('[data-slot="card"]')
.should('contain.text', '当前图表配置没有匹配的数据。');
});
});

it('does not expose a provider error body or API key on the Chinese route', () => {
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', {
statusCode: 401,
body: { error: { message: 'provider-internal-error sk-sensitive-example-key' } },
}).as('failedProvider');
cy.visit('/zh/ai-chart');
cy.get('input[placeholder="OpenAI API Key"]').type('sk-sensitive-example-key', { log: false });
cy.get('textarea').type('对比吞吐量');
cy.contains('button', '生成图表').click();
cy.wait('@failedProvider');

cy.contains('图表请求失败。请检查 API 密钥和服务商设置后重试。').should('be.visible');
cy.contains('provider-internal-error').should('not.exist');
cy.get('[data-testid="ai-chart-error"]').should('not.contain.text', 'sk-sensitive-example-key');
cy.contains('button', '返回修改').click();
cy.get('[data-testid="ai-chart-error"]').should('not.exist');
cy.get('input[placeholder="OpenAI API Key"]').should('have.value', 'sk-sensitive-example-key');
cy.get('textarea').should('have.value', '对比吞吐量');
});

it('keeps Chinese provider controls and examples within 375px', () => {
cy.viewport(375, 844);
cy.visit('/zh/ai-chart');
cy.get('input[placeholder="OpenAI API Key"]').should('be.visible');
cy.get('textarea[placeholder="描述想查看的图表……"]').should('be.visible');
cy.contains('提示词示例').should('be.visible');
cy.contains(`${Cypress.platform === 'darwin' ? '⌘' : 'Ctrl'}+Enter 生成图表`).should(
'be.visible',
);
cy.document().then((doc) => {
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
});
});
});
Loading