@@ -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