@@ -20,6 +20,7 @@ import {
2020    stringToBuffer , 
2121    bufferToString 
2222}  from  '../../util/buffer' ; 
23+ import  {  formatDuration  }  from  '../../util/text' ; 
2324import  { 
2425    getHeaderValue , 
2526    headersToRawHeaders , 
@@ -50,10 +51,13 @@ import {
5051    RequestBreakpointStep , 
5152    ResponseBreakpointStep , 
5253    RequestAndResponseBreakpointStep , 
54+     DelayStep , 
5355    TimeoutStep , 
5456    CloseConnectionStep , 
5557    ResetConnectionStep , 
56-     FromFileResponseStep 
58+     FromFileResponseStep , 
59+     WebhookStep , 
60+     RequestWebhookEvents 
5761}  from  '../../model/rules/definitions/http-rule-definitions' ; 
5862import  { 
5963    WebSocketPassThroughStep , 
@@ -177,12 +181,16 @@ export function StepConfiguration(props: {
177181            return  < ResponseBreakpointStepConfig  { ...configProps }  /> ; 
178182        case  'request-and-response-breakpoint' :
179183            return  < RequestAndResponseBreakpointStepConfig  { ...configProps }  /> ; 
184+         case  'delay' :
185+             return  < WaitForDurationConfig  { ...configProps }  /> ; 
180186        case  'timeout' :
181187            return  < TimeoutStepConfig  { ...configProps }  /> ; 
182188        case  'close-connection' :
183189            return  < CloseConnectionStepConfig  { ...configProps }  /> ; 
184190        case  'reset-connection' :
185191            return  < ResetConnectionStepConfig  { ...configProps }  /> ; 
192+         case  'webhook' :
193+             return  < WebhookStepConfig  { ...configProps }  /> ; 
186194
187195        case  'ws-echo' :
188196            return  < WebSocketEchoStepConfig  { ...configProps }  /> ; 
@@ -226,7 +234,7 @@ export function StepConfiguration(props: {
226234        case  'wait-for-rtc-media' :
227235            return  < RTCWaitForMediaConfig  { ...configProps }  /> ; 
228236        case  'wait-for-duration' :
229-             return  < RTCWaitForDurationConfig  { ...configProps }  /> ; 
237+             return  < WaitForDurationConfig  { ...configProps }  /> ; 
230238        case  'wait-for-rtc-data-channel' :
231239            return  < RTCWaitForChannelConfig  { ...configProps }  /> ; 
232240        case  'wait-for-rtc-message' :
@@ -1725,6 +1733,83 @@ const validateRegexMatcher = (value: string): true | string => {
17251733    } 
17261734} 
17271735
1736+ @observer 
1737+ class  WebhookStepConfig  extends  StepConfig < WebhookStep >  { 
1738+ 
1739+     @observable 
1740+     private  error : Error  |  undefined ; 
1741+ 
1742+     @observable 
1743+     webhookUrl : string  =  this . props . step . url ; 
1744+ 
1745+     @observable 
1746+     events : Array < RequestWebhookEvents >  =  this . props . step . events ; 
1747+ 
1748+     render ( )  { 
1749+         return  < ConfigContainer > 
1750+             < SectionLabel > Webhook target URL</ SectionLabel > 
1751+             < UrlInput 
1752+                 type = "url" 
1753+                 value = { this . webhookUrl } 
1754+                 invalid = { ! ! this . error } 
1755+                 spellCheck = { false } 
1756+                 onChange = { this . onUrlChange } 
1757+             /> 
1758+ 
1759+             < SectionLabel > Webhook events</ SectionLabel > 
1760+ 
1761+             < label > 
1762+                 < input  type = "checkbox" 
1763+                     checked = { this . events . includes ( 'request' ) } 
1764+                     onChange = { ( e )  =>  this . setEvent ( 'request' ,  e . target . checked ) } 
1765+                 /> 
1766+                 Request received
1767+             </ label > 
1768+             < br  /> 
1769+             < label > 
1770+                 < input 
1771+                     type = "checkbox" 
1772+                     checked = { this . events . includes ( 'response' ) } 
1773+                     onChange = { ( e )  =>  this . setEvent ( 'response' ,  e . target . checked ) } 
1774+                 /> 
1775+                 Response sent
1776+             </ label > 
1777+         </ ConfigContainer > ; 
1778+     } 
1779+ 
1780+     @action . bound 
1781+     onUrlChange ( event : React . ChangeEvent < HTMLInputElement > )  { 
1782+         this . webhookUrl  =  event . target . value ; 
1783+         this . updateStep ( event . target ) ; 
1784+     } 
1785+ 
1786+     @action . bound 
1787+     setEvent ( event : RequestWebhookEvents ,  enabled : boolean )  { 
1788+         if  ( enabled  &&  ! this . events . includes ( event ) )  { 
1789+             this . events . push ( event ) ; 
1790+         }  else  if  ( ! enabled  &&  this . events . includes ( event ) )  { 
1791+             this . events . splice ( this . events . indexOf ( event ) ,  1 ) ; 
1792+         } 
1793+         this . updateStep ( ) ; 
1794+     } 
1795+ 
1796+     updateStep ( target ?: HTMLInputElement )  { 
1797+         try  { 
1798+             if  ( ! this . webhookUrl )  throw  new  Error ( 'A webhook URL is required' ) ; 
1799+ 
1800+             this . props . onChange ( new  WebhookStep ( this . webhookUrl ,  this . events ) ) ; 
1801+ 
1802+             this . error  =  undefined ; 
1803+             target ?. setCustomValidity ( '' ) ; 
1804+         }  catch  ( e )  { 
1805+             this . error  =  asError ( e ) ; 
1806+             target ?. setCustomValidity ( this . error . message ) ; 
1807+             if  ( this . props . onInvalidState )  this . props . onInvalidState ( ) ; 
1808+         } 
1809+         target ?. reportValidity ( ) ; 
1810+     } 
1811+ } 
1812+ 
17281813@observer 
17291814class  PassThroughStepConfig  extends  StepConfig < 
17301815    |  PassThroughStep 
@@ -1809,15 +1894,15 @@ class TimeoutStepConfig extends StepConfig<TimeoutStep> {
18091894    render ( )  { 
18101895        return  < ConfigContainer > 
18111896            < ConfigExplanation > 
1812-                 When a matching  { 
1897+                 The  { 
18131898                    isHttpCompatibleType ( this . props . ruleType ) 
18141899                        ? 'request' 
18151900                    : this . props . ruleType  ===  'websocket' 
18161901                        ? 'WebSocket' 
18171902                    : this . props . ruleType  ===  'webrtc' 
18181903                        ? ( ( )  =>  {  throw  new  Error ( 'Not compatible with WebRTC rules' )  } ) ( ) 
18191904                    : unreachableCheck ( this . props . ruleType ) 
1820-                 }  is received, the server will keep  the connection open but do  nothing.
1905+                 }  will receive no response, keeping  the connection open but doing  nothing.
18211906                With no data or response, most clients will time out and abort the
18221907                request after sufficient time has passed.
18231908            </ ConfigExplanation > 
@@ -1830,15 +1915,15 @@ class CloseConnectionStepConfig extends StepConfig<CloseConnectionStep> {
18301915    render ( )  { 
18311916        return  < ConfigContainer > 
18321917            < ConfigExplanation > 
1833-                 As soon as a matching  { 
1918+                 The  { 
18341919                    isHttpCompatibleType ( this . props . ruleType ) 
18351920                        ? 'request' 
18361921                    : this . props . ruleType  ===  'websocket' 
18371922                        ? 'WebSocket' 
18381923                    : this . props . ruleType  ===  'webrtc' 
18391924                        ? ( ( )  =>  {  throw  new  Error ( 'Not compatible with WebRTC rules' )  } ) ( ) 
18401925                    : unreachableCheck ( this . props . ruleType ) 
1841-                 }  is received, the  connection will be cleanly closed, with no response.
1926+                 } 's  connection will be cleanly closed, with no response.
18421927            </ ConfigExplanation > 
18431928        </ ConfigContainer > ; 
18441929    } 
@@ -1849,16 +1934,16 @@ class ResetConnectionStepConfig extends StepConfig<ResetConnectionStep> {
18491934    render ( )  { 
18501935        return  < ConfigContainer > 
18511936            < ConfigExplanation > 
1852-                 As soon as a matching  { 
1937+                 The  { 
18531938                    isHttpCompatibleType ( this . props . ruleType ) 
18541939                        ? 'request' 
18551940                    : this . props . ruleType  ===  'websocket' 
18561941                        ? 'WebSocket' 
18571942                    : this . props . ruleType  ===  'webrtc' 
18581943                        ? ( ( )  =>  {  throw  new  Error ( 'Not compatible with WebRTC rules' )  } ) ( ) 
18591944                    : unreachableCheck ( this . props . ruleType ) 
1860-                 }  is received, the  connection will be killed with a TCP RST packet (or a
1861-                 RST_STREAM frame, for HTTP/2 requests ).
1945+                 } 's  connection will be abruptly  killed with a TCP RST packet (or a
1946+                 RST_STREAM frame, for HTTP/2).
18621947            </ ConfigExplanation > 
18631948        </ ConfigContainer > ; 
18641949    } 
@@ -2692,24 +2777,28 @@ class RTCWaitForMediaConfig extends StepConfig<WaitForMediaStep> {
26922777} 
26932778
26942779@observer 
2695- class  RTCWaitForDurationConfig  extends  StepConfig < WaitForDurationStep >  { 
2780+ class  WaitForDurationConfig  extends  StepConfig < WaitForDurationStep  |  DelayStep >  { 
2781+ 
2782+     @computed 
2783+     get  stepDuration ( )  { 
2784+         return  'durationMs'  in  this . props . step 
2785+             ? this . props . step . durationMs 
2786+             : this . props . step . delayMs ; 
2787+     } 
26962788
26972789    @observable 
2698-     duration : number  |  ''  =  this . props . step . durationMs ; 
2790+     inputDuration : number  |  ''  =  this . stepDuration ; 
26992791
27002792    componentDidMount ( )  { 
27012793        // If the step changes (or when its set initially), update our data fields 
27022794        disposeOnUnmount ( this ,  autorun ( ( )  =>  { 
2703-             const  {  durationMs }  =  this . props . step ; 
2704-             runInAction ( ( )  =>  { 
2705-                 if  ( durationMs  ===  0  &&  this . duration  ===  '' )  return ;  // Allows clearing the input, making it *implicitly* 0 
2706-                 this . duration  =  durationMs ; 
2707-             } ) ; 
2795+             if  ( this . stepDuration  ===  0  &&  this . inputDuration  ===  '' )  return ;  // Allows clearing the input, making it *implicitly* 0 
2796+             this . inputDuration  =  this . stepDuration ; 
27082797        } ) ) ; 
27092798    } 
27102799
27112800    render ( )  { 
2712-         const  {  duration }  =  this ; 
2801+         const  {  inputDuration :  duration  }  =  this ; 
27132802
27142803        return  < ConfigContainer > 
27152804            Wait for < TextInput 
@@ -2718,7 +2807,10 @@ class RTCWaitForDurationConfig extends StepConfig<WaitForDurationStep> {
27182807                placeholder = 'Duration (ms)' 
27192808                value = { duration } 
27202809                onChange = { this . onChange } 
2721-             />  milliseconds.
2810+             />  milliseconds{ 
2811+                 duration  !==  ''  &&  ! formatDuration ( duration ) . endsWith ( 'ms' )  && 
2812+                     ` (${  formatDuration ( duration )  }  
2813+             } .
27222814        </ ConfigContainer > 
27232815    } 
27242816
@@ -2728,12 +2820,16 @@ class RTCWaitForDurationConfig extends StepConfig<WaitForDurationStep> {
27282820
27292821        const  newValue  =  inputValue  ===  '' 
27302822            ? '' 
2731-             : parseInt ( inputValue ,  10 ) ; 
2823+             : Math . max ( parseInt ( inputValue ,  10 )   ||   0 ,   0 ) ; 
27322824
27332825        if  ( _ . isNaN ( newValue ) )  return ;  // I.e. reject the edit 
27342826
2735-         this . duration  =  newValue ; 
2736-         this . props . onChange ( new  WaitForDurationStep ( newValue  ||  0 ) ) ; 
2827+         this . inputDuration  =  newValue ; 
2828+ 
2829+         const  step  =  this . props . ruleType  ===  'webrtc' 
2830+             ? new  WaitForDurationStep ( newValue  ||  0 ) 
2831+             : new  DelayStep ( newValue  ||  0 ) ; 
2832+         this . props . onChange ( step ) ; 
27372833    } 
27382834
27392835} 
0 commit comments