@@ -57,4 +57,95 @@ describe('NotificationTestPanel', () => {
5757 expect ( screen . getByText ( 'HTTP 200' ) ) . toBeInTheDocument ( ) ;
5858 expect ( screen . getByText ( 'https://example.com/hook?token=***' ) ) . toBeInTheDocument ( ) ;
5959 } ) ;
60+
61+ it ( 'renders custom webhook partial failure attempts' , async ( ) => {
62+ testNotificationChannel . mockResolvedValueOnce ( {
63+ success : true ,
64+ message : '自定义 Webhook 通知测试部分成功(1/2)' ,
65+ errorCode : null ,
66+ stage : 'notification_send' ,
67+ retryable : true ,
68+ latencyMs : 35 ,
69+ attempts : [
70+ {
71+ channel : 'custom' ,
72+ success : false ,
73+ message : 'HTTP 500' ,
74+ target : 'https://example.com/hook?token=***' ,
75+ errorCode : 'http_500' ,
76+ stage : 'notification_send' ,
77+ retryable : true ,
78+ latencyMs : 12 ,
79+ httpStatus : 500 ,
80+ } ,
81+ {
82+ channel : 'custom' ,
83+ success : true ,
84+ message : 'sent' ,
85+ target : 'https://example.com/second/***' ,
86+ errorCode : null ,
87+ stage : 'notification_send' ,
88+ retryable : false ,
89+ latencyMs : 23 ,
90+ httpStatus : 200 ,
91+ } ,
92+ ] ,
93+ } ) ;
94+
95+ render (
96+ < NotificationTestPanel
97+ items = { [ { key : 'CUSTOM_WEBHOOK_URLS' , value : 'https://example.com/hook?token=secret' } ] }
98+ maskToken = "******"
99+ /> ,
100+ ) ;
101+
102+ fireEvent . change ( screen . getByLabelText ( '渠道' ) , { target : { value : 'custom' } } ) ;
103+ fireEvent . click ( screen . getByRole ( 'button' , { name : / 发 送 测 试 / } ) ) ;
104+
105+ expect ( await screen . findByText ( '测试成功' ) ) . toBeInTheDocument ( ) ;
106+ expect ( screen . getByText ( / 部 分 成 功 / ) ) . toBeInTheDocument ( ) ;
107+ expect ( screen . getAllByText ( 'HTTP 500' ) . length ) . toBeGreaterThanOrEqual ( 1 ) ;
108+ expect ( screen . getByText ( 'HTTP 200' ) ) . toBeInTheDocument ( ) ;
109+ expect ( screen . getByText ( 'http_500' ) ) . toHaveClass ( 'text-warning' ) ;
110+ expect ( screen . getByText ( 'https://example.com/hook?token=***' ) ) . toBeInTheDocument ( ) ;
111+ } ) ;
112+
113+ it ( 'renders retryable timeout diagnostics' , async ( ) => {
114+ testNotificationChannel . mockResolvedValueOnce ( {
115+ success : false ,
116+ message : '通知测试异常: timeout' ,
117+ errorCode : 'timeout' ,
118+ stage : 'notification_send' ,
119+ retryable : true ,
120+ latencyMs : null ,
121+ attempts : [
122+ {
123+ channel : 'wechat' ,
124+ success : false ,
125+ message : 'timeout' ,
126+ target : 'https://qyapi.example.com/cgi-bin/webhook/send?key=***' ,
127+ errorCode : 'timeout' ,
128+ stage : 'notification_send' ,
129+ retryable : true ,
130+ latencyMs : null ,
131+ httpStatus : null ,
132+ } ,
133+ ] ,
134+ } ) ;
135+
136+ render (
137+ < NotificationTestPanel
138+ items = { [ { key : 'WECHAT_WEBHOOK_URL' , value : 'https://qyapi.example.com/cgi-bin/webhook/send?key=secret' } ] }
139+ maskToken = "******"
140+ /> ,
141+ ) ;
142+
143+ fireEvent . click ( screen . getByRole ( 'button' , { name : / 发 送 测 试 / } ) ) ;
144+
145+ expect ( await screen . findByText ( '测试失败' ) ) . toBeInTheDocument ( ) ;
146+ const timeoutEntries = screen . getAllByText ( 'timeout' ) ;
147+ expect ( timeoutEntries [ 0 ] ) . toBeInTheDocument ( ) ;
148+ expect ( screen . getByText ( 'https://qyapi.example.com/cgi-bin/webhook/send?key=***' ) ) . toBeInTheDocument ( ) ;
149+ expect ( timeoutEntries [ 0 ] ) . toHaveClass ( 'text-warning' ) ;
150+ } ) ;
60151} ) ;
0 commit comments