Files
open-design/apps/web/tests/components/chat-reconnect.test.tsx
eca7c7ab98 fix(web): align ChatPanel failures with approved product copy (#8140)
* fix(web): align runtime error messages with approved product copy

* test(chat): give retry deduplication scenario a Cloud owner

* test(chat): use the supported Cloud model preference shape

* test(chat): align CLI recovery scenarios with fixed Cloud actions

* test(e2e): align Cloud error layout with fixed recovery actions

* test(e2e): use ordinary Cloud failure for error-card layout

* test(web): preserve ChatPane helpers and align recovery fixtures

* test(e2e): resize error-card layout through the native separator

* test(e2e): reject missing error-card action geometry

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-09-16 03:28:29 +00:00

117 lines
5.6 KiB
TypeScript

// @vitest-environment jsdom
/**
* 组件 22 · 重连(84 格状态矩阵第 82–84 格)。
*
* 这一族的验收点不在「字对不对」,在**形状与边界**:
* · 82 / 83 是同一副形状,只有计数不同 —— 陈列页两格并排就是为了让设计确认这件事
* · 恢复后整行消失,不留「已恢复」
* · 84 停止自动重连,换成一颗交回给人的按钮
* · 计数不能超过预算(传输层的预算会被 keepalive 续上,读数可能大过 max)
*
* 文案按 `components/chat/AGENTS.md` §0 逐字对设计稿(zh-CN 是原文,不改写),
* 其余断言只碰行为 / ARIA / 稳定的 `data-testid` —— §5 明令不断 CSS 类名。
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render as rtlRender, screen } from '@testing-library/react';
import type { ReactElement } from 'react';
import { I18nProvider } from '../../src/i18n';
import { Reconnect } from '../../src/components/chat/Reconnect';
afterEach(() => { cleanup(); });
const render = (ui: ReactElement) => rtlRender(<I18nProvider initial="zh-CN">{ui}</I18nProvider>);
const row = () => screen.getByTestId('chat-reconnect');
/** 那颗球 —— Orb 自己挂的 data-orb,是这一行「还在等」的稳定标记 */
const orb = () => row().querySelector('[data-orb]');
describe('Reconnect · 82 重连中', () => {
it('shows how many attempts in, out of the transport budget', () => {
render(<Reconnect attempt={2} max={5} />);
// 保留 next 标题计数,并补齐 S29 授权的当前连接尝试正文。
expect(row().textContent).toBe('正在恢复网络连接2/5正在进行第 2/5 次连接尝试,请稍候。');
expect(orb()).not.toBeNull();
});
it('drops the fraction when nothing is counting behind it', () => {
/*
* 浏览器自己报「这一屏没网了」时推的就是这一档(`ChatReconnectSignal` 的
* `network`,读数固定 1/1)。背后**没有梯子在数** —— 那种断法里传输层的
* 重连预算一次都没上膛 —— 所以写「1/5」是假话,写「1/1」像倒计时且一个
* 信息都没给。判据是组件自己的 `showCount = max > 1`,这里钉住它的外观。
*/
render(<Reconnect attempt={1} max={1} />);
expect(row().textContent).toBe('正在恢复网络连接');
expect(orb()).not.toBeNull();
});
it('renders nothing once the connection is no longer dropping', () => {
// 传输层恢复时发的是 { attempt: 0, phase: 'cleared' } —— 调用方可以直接铺进来,
// 由组件自己判「这一行该没了」。设计稿:恢复后整行消失,不留「已恢复」。
render(<Reconnect attempt={0} max={5} />);
expect(screen.queryByTestId('chat-reconnect')).toBeNull();
});
it('keeps the detail chevron out until someone can answer what broke', () => {
// 今天的传输层把 fetch 抛错 / 流提前关 / 只收到 keepalive 走同一条路径,
// 分不出断因;不传 onShowDetail 就不该出这颗点开什么都没有的箭头。
render(<Reconnect attempt={2} max={5} />);
expect(screen.queryByRole('button')).toBeNull();
});
it('offers the detail chevron with an accessible name when a handler is wired', () => {
const onShowDetail = vi.fn();
render(<Reconnect attempt={2} max={5} onShowDetail={onShowDetail} />);
const chevron = screen.getByRole('button', { name: '查看详情' });
fireEvent.click(chevron);
expect(onShowDetail).toHaveBeenCalledTimes(1);
});
});
describe('Reconnect · 83 最后一次', () => {
it('is the same shape as any other attempt — only the count moved', () => {
const { unmount } = render(<Reconnect attempt={2} max={5} />);
const mid = { orb: orb() !== null, buttons: screen.queryAllByRole('button').length };
unmount();
render(<Reconnect attempt={5} max={5} />);
expect(row().textContent).toContain('5/5');
expect({ orb: orb() !== null, buttons: screen.queryAllByRole('button').length }).toEqual(mid);
});
it('never shows a count past the budget', () => {
// 预算会被 keepalive 空转续上,读数可能走到 7 —— 但屏幕上不该出现「7/5」。
render(<Reconnect attempt={7} max={5} />);
expect(row().textContent).toContain('5/5');
expect(row().textContent).not.toContain('7');
});
});
describe('Reconnect · 84 重连失败', () => {
it('stops counting and hands the retry back to the user', () => {
const onReconnect = vi.fn();
render(<Reconnect attempt={5} max={5} exhausted onReconnect={onReconnect} />);
// 不再是「还在等」:球撤了,计数也不再报
expect(orb()).toBeNull();
expect(row().textContent).not.toContain('5/5');
expect(screen.getByText('网络连接未能恢复')).toBeTruthy();
// 稿子给的是「重新连接」不是「重试」—— 语义是接回同一个 run 的流,不是新建 run
fireEvent.click(screen.getByRole('button', { name: '重新连接' }));
expect(onReconnect).toHaveBeenCalledTimes(1);
});
it('still states the failure when nobody wired a reconnect action', () => {
render(<Reconnect attempt={5} max={5} exhausted />);
expect(screen.queryByTestId('chat-reconnect')).not.toBeNull();
expect(screen.queryByRole('button')).toBeNull();
});
it('shows the failed row even after the counter was cleared', () => {
// exhausted 优先于 attempt:传输层用尽预算时发的是 { attempt: 5, phase: 'exhausted' },
// 但调用方若先把读数清了再置失败,这一行仍然要在。
render(<Reconnect attempt={0} max={5} exhausted />);
expect(screen.queryByTestId('chat-reconnect')).not.toBeNull();
});
});