Add responseType as a concept to RCTNetworking, send binary data as base64
Summary: In preparation for Blob support (wherein binary XHR and WebSocket responses can be retained as native data blobs on the native side and JS receives a web-like opaque Blob object), this change makes RCTNetworking aware of the responseType that JS requests. A `xhr.responseType` of `''` or `'text'` translates to a native response type of `'text'`. A `xhr.responseType` of `arraybuffer` translates to a native response type of `base64`, as we currently lack an API to transmit TypedArrays directly to JS. This is analogous to how the WebSocket module already works, and it's a lot more versatile and much less brittle than converting a JS *string* back to a TypedArray, which is what's currently going on. Now that we don't always send text down to JS, JS consumers might still want to get progress updates about a binary download. This is what the `'progress'` event is designed for, so this change also implements that. This change also follows the XHR spec with regards to `xhr.response` and `xhr.responseText`: - if the response type is `'text'`, `xhr.responseText` can be peeked at by the JS consumer. It will be updated periodically as the download progresses, so long as there's either an `onreadystatechange` or `onprogress` handler on the XHR. - if the response type is not `'text'`, `xhr.responseText` can't be accessed and `xhr.response` remains `null` until the response is fully received. `'progress'` events containing response details (total bytes, downloaded so far) are dispatched if there's an `onprogress` handler. Once Blobs are landed, `xhr.responseType` of `'blob'` will correspond to the same native response type, which will cause RCTNetworking to only send a blob ID down to JS, which can then create a `Blob` object from that for consumers. Closes https://github.com/facebook/react-native/pull/8324 Reviewed By: javache Differential Revision: D3508822 Pulled By: davidaurelio fbshipit-source-id: 441b2d4d40265b6036559c3ccb9fa962999fa5df
This commit is contained in:
Родитель
c65eb4ef19
Коммит
08c375f828
|
@ -25,14 +25,15 @@
|
|||
var React = require('react');
|
||||
var ReactNative = require('react-native');
|
||||
var {
|
||||
CameraRoll,
|
||||
Image,
|
||||
ProgressBarAndroid,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
Image,
|
||||
CameraRoll
|
||||
} = ReactNative;
|
||||
|
||||
var XHRExampleHeaders = require('./XHRExampleHeaders');
|
||||
|
@ -40,6 +41,13 @@ var XHRExampleCookies = require('./XHRExampleCookies');
|
|||
var XHRExampleFetch = require('./XHRExampleFetch');
|
||||
var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');
|
||||
|
||||
/**
|
||||
* Convert number of bytes to MB and round to the nearest 0.1 MB.
|
||||
*/
|
||||
function roundKilo(value: number): number {
|
||||
return Math.round(value / 1000);
|
||||
}
|
||||
|
||||
// TODO t7093728 This is a simplified XHRExample.ios.js.
|
||||
// Once we have Camera roll, Toast, Intent (for opening URLs)
|
||||
// we should make this consistent with iOS.
|
||||
|
@ -54,8 +62,18 @@ class Downloader extends React.Component {
|
|||
this.cancelled = false;
|
||||
this.state = {
|
||||
status: '',
|
||||
contentSize: 1,
|
||||
downloaded: 0,
|
||||
downloading: false,
|
||||
|
||||
// set by onreadystatechange
|
||||
contentLength: 1,
|
||||
responseLength: 0,
|
||||
// set by onprogress
|
||||
progressTotal: 1,
|
||||
progressLoaded: 0,
|
||||
|
||||
readystateHandler: false,
|
||||
progressHandler: true,
|
||||
arraybuffer: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -63,44 +81,66 @@ class Downloader extends React.Component {
|
|||
this.xhr && this.xhr.abort();
|
||||
|
||||
var xhr = this.xhr || new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
const onreadystatechange = () => {
|
||||
if (xhr.readyState === xhr.HEADERS_RECEIVED) {
|
||||
var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10);
|
||||
const contentLength = parseInt(xhr.getResponseHeader('Content-Length'), 10);
|
||||
this.setState({
|
||||
contentSize: contentSize,
|
||||
downloaded: 0,
|
||||
contentLength,
|
||||
responseLength: 0,
|
||||
});
|
||||
} else if (xhr.readyState === xhr.LOADING) {
|
||||
} else if (xhr.readyState === xhr.LOADING && xhr.response) {
|
||||
this.setState({
|
||||
downloaded: xhr.responseText.length,
|
||||
responseLength: xhr.response.length,
|
||||
});
|
||||
} else if (xhr.readyState === xhr.DONE) {
|
||||
if (this.cancelled) {
|
||||
this.cancelled = false;
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
this.setState({
|
||||
status: 'Download complete!',
|
||||
});
|
||||
} else if (xhr.status !== 0) {
|
||||
this.setState({
|
||||
status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
status: 'Error: ' + xhr.responseText,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt');
|
||||
const onprogress = (event) => {
|
||||
this.setState({
|
||||
progressTotal: event.total,
|
||||
progressLoaded: event.loaded,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.state.readystateHandler) {
|
||||
xhr.onreadystatechange = onreadystatechange;
|
||||
}
|
||||
if (this.state.progressHandler) {
|
||||
xhr.onprogress = onprogress;
|
||||
}
|
||||
if (this.state.arraybuffer) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
xhr.onload = () => {
|
||||
this.setState({downloading: false});
|
||||
if (this.cancelled) {
|
||||
this.cancelled = false;
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
let responseType = `Response is a string, ${xhr.response.length} characters long.`;
|
||||
if (typeof ArrayBuffer !== 'undefined' &&
|
||||
xhr.response instanceof ArrayBuffer) {
|
||||
responseType = `Response is an ArrayBuffer, ${xhr.response.byteLength} bytes long.`;
|
||||
}
|
||||
this.setState({status: `Download complete! ${responseType}`});
|
||||
} else if (xhr.status !== 0) {
|
||||
this.setState({
|
||||
status: 'Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText
|
||||
});
|
||||
} else {
|
||||
this.setState({status: 'Error: ' + xhr.responseText});
|
||||
}
|
||||
};
|
||||
xhr.open('GET', 'http://aleph.gutenberg.org/cache/epub/100/pg100.txt.utf8');
|
||||
// Avoid gzip so we can actually show progress
|
||||
xhr.setRequestHeader('Accept-Encoding', '');
|
||||
xhr.send();
|
||||
this.xhr = xhr;
|
||||
|
||||
this.setState({status: 'Downloading...'});
|
||||
this.setState({
|
||||
downloading: true,
|
||||
status: 'Downloading...',
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -109,7 +149,7 @@ class Downloader extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
var button = this.state.status === 'Downloading...' ? (
|
||||
var button = this.state.downloading ? (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.button}>
|
||||
<Text>...</Text>
|
||||
|
@ -125,11 +165,67 @@ class Downloader extends React.Component {
|
|||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
let readystate = null;
|
||||
let progress = null;
|
||||
if (this.state.readystateHandler && !this.state.arraybuffer) {
|
||||
const { responseLength, contentLength } = this.state;
|
||||
readystate = (
|
||||
<View>
|
||||
<Text style={styles.progressBarLabel}>
|
||||
responseText:{' '}
|
||||
{roundKilo(responseLength)}/{roundKilo(contentLength)}k chars
|
||||
</Text>
|
||||
<ProgressBarAndroid
|
||||
progress={(responseLength / contentLength)}
|
||||
styleAttr="Horizontal"
|
||||
indeterminate={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (this.state.progressHandler) {
|
||||
const { progressLoaded, progressTotal } = this.state;
|
||||
progress = (
|
||||
<View>
|
||||
<Text style={styles.progressBarLabel}>
|
||||
onprogress:{' '}
|
||||
{roundKilo(progressLoaded)}/{roundKilo(progressTotal)} KB
|
||||
</Text>
|
||||
<ProgressBarAndroid
|
||||
progress={(progressLoaded / progressTotal)}
|
||||
styleAttr="Horizontal"
|
||||
indeterminate={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>onreadystatechange handler</Text>
|
||||
<Switch
|
||||
value={this.state.readystateHandler}
|
||||
onValueChange={(readystateHandler => this.setState({readystateHandler}))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>onprogress handler</Text>
|
||||
<Switch
|
||||
value={this.state.progressHandler}
|
||||
onValueChange={(progressHandler => this.setState({progressHandler}))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>download as arraybuffer</Text>
|
||||
<Switch
|
||||
value={this.state.arraybuffer}
|
||||
onValueChange={(arraybuffer => this.setState({arraybuffer}))}
|
||||
/>
|
||||
</View>
|
||||
{button}
|
||||
<ProgressBarAndroid progress={(this.state.downloaded / this.state.contentSize)}
|
||||
styleAttr="Horizontal" indeterminate={false} />
|
||||
{readystate}
|
||||
{progress}
|
||||
<Text>{this.state.status}</Text>
|
||||
</View>
|
||||
);
|
||||
|
@ -364,6 +460,16 @@ var styles = StyleSheet.create({
|
|||
backgroundColor: '#eeeeee',
|
||||
padding: 8,
|
||||
},
|
||||
progressBarLabel: {
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
configRow: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
paramRow: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 8,
|
||||
|
|
|
@ -31,6 +31,7 @@ var {
|
|||
Linking,
|
||||
ProgressViewIOS,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableHighlight,
|
||||
|
@ -41,6 +42,13 @@ var XHRExampleHeaders = require('./XHRExampleHeaders');
|
|||
var XHRExampleFetch = require('./XHRExampleFetch');
|
||||
var XHRExampleOnTimeOut = require('./XHRExampleOnTimeOut');
|
||||
|
||||
/**
|
||||
* Convert number of bytes to MB and round to the nearest 0.1 MB.
|
||||
*/
|
||||
function roundKilo(value: number): number {
|
||||
return Math.round(value / 1000);
|
||||
}
|
||||
|
||||
class Downloader extends React.Component {
|
||||
state: any;
|
||||
|
||||
|
@ -52,8 +60,16 @@ class Downloader extends React.Component {
|
|||
this.cancelled = false;
|
||||
this.state = {
|
||||
downloading: false,
|
||||
contentSize: 1,
|
||||
downloaded: 0,
|
||||
// set by onreadystatechange
|
||||
contentLength: 1,
|
||||
responseLength: 0,
|
||||
// set by onprogress
|
||||
progressTotal: 1,
|
||||
progressLoaded: 0,
|
||||
|
||||
readystateHandler: false,
|
||||
progressHandler: true,
|
||||
arraybuffer: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -61,41 +77,62 @@ class Downloader extends React.Component {
|
|||
this.xhr && this.xhr.abort();
|
||||
|
||||
var xhr = this.xhr || new XMLHttpRequest();
|
||||
xhr.onreadystatechange = () => {
|
||||
const onreadystatechange = () => {
|
||||
if (xhr.readyState === xhr.HEADERS_RECEIVED) {
|
||||
var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10);
|
||||
const contentLength = parseInt(xhr.getResponseHeader('Content-Length'), 10);
|
||||
this.setState({
|
||||
contentSize: contentSize,
|
||||
downloaded: 0,
|
||||
contentLength,
|
||||
responseLength: 0,
|
||||
});
|
||||
} else if (xhr.readyState === xhr.LOADING) {
|
||||
this.setState({
|
||||
downloaded: xhr.responseText.length,
|
||||
responseLength: xhr.responseText.length,
|
||||
});
|
||||
} else if (xhr.readyState === xhr.DONE) {
|
||||
this.setState({
|
||||
downloading: false,
|
||||
});
|
||||
if (this.cancelled) {
|
||||
this.cancelled = false;
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
alert('Download complete!');
|
||||
} else if (xhr.status !== 0) {
|
||||
alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText);
|
||||
} else {
|
||||
alert('Error: ' + xhr.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt');
|
||||
const onprogress = (event) => {
|
||||
this.setState({
|
||||
progressTotal: event.total,
|
||||
progressLoaded: event.loaded,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.state.readystateHandler) {
|
||||
xhr.onreadystatechange = onreadystatechange;
|
||||
}
|
||||
if (this.state.progressHandler) {
|
||||
xhr.onprogress = onprogress;
|
||||
}
|
||||
if (this.state.arraybuffer) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
xhr.onload = () => {
|
||||
this.setState({downloading: false});
|
||||
if (this.cancelled) {
|
||||
this.cancelled = false;
|
||||
return;
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
let responseType = `Response is a string, ${xhr.response.length} characters long.`;
|
||||
if (typeof ArrayBuffer !== 'undefined' &&
|
||||
xhr.response instanceof ArrayBuffer) {
|
||||
responseType = `Response is an ArrayBuffer, ${xhr.response.byteLength} bytes long.`;
|
||||
}
|
||||
alert(`Download complete! ${responseType}`);
|
||||
} else if (xhr.status !== 0) {
|
||||
alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText);
|
||||
} else {
|
||||
alert('Error: ' + xhr.responseText);
|
||||
}
|
||||
};
|
||||
xhr.open('GET', 'http://aleph.gutenberg.org/cache/epub/100/pg100.txt.utf8');
|
||||
xhr.send();
|
||||
this.xhr = xhr;
|
||||
|
||||
this.setState({downloading: true});
|
||||
}
|
||||
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cancelled = true;
|
||||
this.xhr && this.xhr.abort();
|
||||
|
@ -113,15 +150,68 @@ class Downloader extends React.Component {
|
|||
style={styles.wrapper}
|
||||
onPress={this.download.bind(this)}>
|
||||
<View style={styles.button}>
|
||||
<Text>Download 5MB Text File</Text>
|
||||
<Text>Download 5MB Text File</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
let readystate = null;
|
||||
let progress = null;
|
||||
if (this.state.readystateHandler && !this.state.arraybuffer) {
|
||||
const { responseLength, contentLength } = this.state;
|
||||
readystate = (
|
||||
<View>
|
||||
<Text style={styles.progressBarLabel}>
|
||||
responseText:{' '}
|
||||
{roundKilo(responseLength)}/{roundKilo(contentLength)}k chars
|
||||
</Text>
|
||||
<ProgressViewIOS
|
||||
progress={(responseLength / contentLength)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (this.state.progressHandler) {
|
||||
const { progressLoaded, progressTotal } = this.state;
|
||||
progress = (
|
||||
<View>
|
||||
<Text style={styles.progressBarLabel}>
|
||||
onprogress:{' '}
|
||||
{roundKilo(progressLoaded)}/{roundKilo(progressTotal)} KB
|
||||
</Text>
|
||||
<ProgressViewIOS
|
||||
progress={(progressLoaded / progressTotal)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>onreadystatechange handler</Text>
|
||||
<Switch
|
||||
value={this.state.readystateHandler}
|
||||
onValueChange={(readystateHandler => this.setState({readystateHandler}))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>onprogress handler</Text>
|
||||
<Switch
|
||||
value={this.state.progressHandler}
|
||||
onValueChange={(progressHandler => this.setState({progressHandler}))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.configRow}>
|
||||
<Text>download as arraybuffer</Text>
|
||||
<Switch
|
||||
value={this.state.arraybuffer}
|
||||
onValueChange={(arraybuffer => this.setState({arraybuffer}))}
|
||||
/>
|
||||
</View>
|
||||
{button}
|
||||
<ProgressViewIOS progress={(this.state.downloaded / this.state.contentSize)}/>
|
||||
{readystate}
|
||||
{progress}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -232,7 +322,6 @@ class FormUploader extends React.Component {
|
|||
(param) => formdata.append(param.name, param.value)
|
||||
);
|
||||
xhr.upload.onprogress = (event) => {
|
||||
console.log('upload onprogress', event);
|
||||
if (event.lengthComputable) {
|
||||
this.setState({uploadProgress: event.loaded / event.total});
|
||||
}
|
||||
|
@ -354,6 +443,16 @@ var styles = StyleSheet.create({
|
|||
backgroundColor: '#eeeeee',
|
||||
padding: 8,
|
||||
},
|
||||
progressBarLabel: {
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
configRow: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
paramRow: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 8,
|
||||
|
|
|
@ -76,7 +76,7 @@ class XHRExampleCookies extends React.Component {
|
|||
|
||||
clearCookies() {
|
||||
RCTNetworking.clearCookies((cleared) => {
|
||||
this.setStatus('Cookies cleared, had cookies=' + cleared);
|
||||
this.setStatus('Cookies cleared, had cookies=' + cleared.toString());
|
||||
this.refreshWebview();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
|
||||
typedef void (^RCTURLRequestCancellationBlock)(void);
|
||||
typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
|
||||
typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data, int64_t progress, int64_t total);
|
||||
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
|
||||
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
}
|
||||
[_data appendData:data];
|
||||
if (_incrementalDataBlock) {
|
||||
_incrementalDataBlock(data);
|
||||
_incrementalDataBlock(data, _data.length, _response.expectedContentLength);
|
||||
}
|
||||
if (_downloadProgressBlock && _response.expectedContentLength > 0) {
|
||||
_downloadProgressBlock(_data.length, _response.expectedContentLength);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule RCTNetworking
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -42,33 +43,28 @@ class RCTNetworking extends NativeEventEmitter {
|
|||
}
|
||||
|
||||
sendRequest(
|
||||
method: ?string,
|
||||
method: string,
|
||||
trackingName: string,
|
||||
url: ?string,
|
||||
url: string,
|
||||
headers: Object,
|
||||
data: string | FormData | Object,
|
||||
data: string | FormData | {uri: string},
|
||||
responseType: 'text' | 'base64',
|
||||
incrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
callback: (requestId: number) => void,
|
||||
callback: (requestId: number) => any
|
||||
) {
|
||||
if (typeof data === 'string') {
|
||||
data = {string: data};
|
||||
} else if (data instanceof FormData) {
|
||||
data = {
|
||||
formData: data.getParts().map((part) => {
|
||||
part.headers = convertHeadersMapToArray(part.headers);
|
||||
return part;
|
||||
}),
|
||||
};
|
||||
}
|
||||
data = {...data, trackingName};
|
||||
const body =
|
||||
typeof data === 'string' ? {string: data} :
|
||||
data instanceof FormData ? {formData: getParts(data)} :
|
||||
data;
|
||||
const requestId = generateRequestId();
|
||||
RCTNetworkingNative.sendRequest(
|
||||
method,
|
||||
url,
|
||||
requestId,
|
||||
convertHeadersMapToArray(headers),
|
||||
data,
|
||||
{...body, trackingName},
|
||||
responseType,
|
||||
incrementalUpdates,
|
||||
timeout
|
||||
);
|
||||
|
@ -79,9 +75,16 @@ class RCTNetworking extends NativeEventEmitter {
|
|||
RCTNetworkingNative.abortRequest(requestId);
|
||||
}
|
||||
|
||||
clearCookies(callback: number) {
|
||||
clearCookies(callback: (result: boolean) => any) {
|
||||
RCTNetworkingNative.clearCookies(callback);
|
||||
}
|
||||
}
|
||||
|
||||
function getParts(data) {
|
||||
return data.getParts().map((part) => {
|
||||
part.headers = convertHeadersMapToArray(part.headers);
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = new RCTNetworking();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule RCTNetworking
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -21,26 +22,26 @@ class RCTNetworking extends NativeEventEmitter {
|
|||
}
|
||||
|
||||
sendRequest(
|
||||
method: ?string,
|
||||
method: string,
|
||||
trackingName: string,
|
||||
url: ?string,
|
||||
url: string,
|
||||
headers: Object,
|
||||
data: string | FormData | Object,
|
||||
data: string | FormData | {uri: string},
|
||||
responseType: 'text' | 'base64',
|
||||
incrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
callback: (requestId: number) => void,
|
||||
callback: (requestId: number) => any
|
||||
) {
|
||||
if (typeof data === 'string') {
|
||||
data = {string: data};
|
||||
} else if (data instanceof FormData) {
|
||||
data = {formData: data.getParts()};
|
||||
}
|
||||
data = {...data, trackingName};
|
||||
const body =
|
||||
typeof data === 'string' ? {string: data} :
|
||||
data instanceof FormData ? {formData: data.getParts()} :
|
||||
data;
|
||||
RCTNetworkingNative.sendRequest({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
data: {...body, trackingName},
|
||||
headers,
|
||||
responseType,
|
||||
incrementalUpdates,
|
||||
timeout
|
||||
}, callback);
|
||||
|
@ -50,7 +51,7 @@ class RCTNetworking extends NativeEventEmitter {
|
|||
RCTNetworkingNative.abortRequest(requestId);
|
||||
}
|
||||
|
||||
clearCookies(callback: number) {
|
||||
clearCookies(callback: (result: boolean) => any) {
|
||||
console.warn('RCTNetworking.clearCookies is not supported on iOS');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,6 +138,8 @@ RCT_EXPORT_MODULE()
|
|||
return @[@"didCompleteNetworkResponse",
|
||||
@"didReceiveNetworkResponse",
|
||||
@"didSendNetworkData",
|
||||
@"didReceiveNetworkIncrementalData",
|
||||
@"didReceiveNetworkDataProgress",
|
||||
@"didReceiveNetworkData"];
|
||||
}
|
||||
|
||||
|
@ -313,26 +315,16 @@ RCT_EXPORT_MODULE()
|
|||
return callback(nil, nil);
|
||||
}
|
||||
|
||||
- (void)sendData:(NSData *)data forTask:(RCTNetworkTask *)task
|
||||
+ (NSString *)decodeTextData:(NSData *)data fromResponse:(NSURLResponse *)response
|
||||
{
|
||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||
|
||||
if (data.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get text encoding
|
||||
NSURLResponse *response = task.response;
|
||||
NSStringEncoding encoding = NSUTF8StringEncoding;
|
||||
if (response.textEncodingName) {
|
||||
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
|
||||
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
|
||||
}
|
||||
|
||||
// Attempt to decode text
|
||||
NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
|
||||
if (!responseText && data.length) {
|
||||
|
||||
NSString *encodedResponse = [[NSString alloc] initWithData:data encoding:encoding];
|
||||
if (!encodedResponse && data.length) {
|
||||
// We don't have an encoding, or the encoding is incorrect, so now we
|
||||
// try to guess (unfortunately, this feature is available in iOS 8+ only)
|
||||
if ([NSString respondsToSelector:@selector(stringEncodingForData:
|
||||
|
@ -341,22 +333,43 @@ RCT_EXPORT_MODULE()
|
|||
usedLossyConversion:)]) {
|
||||
[NSString stringEncodingForData:data
|
||||
encodingOptions:nil
|
||||
convertedString:&responseText
|
||||
convertedString:&encodedResponse
|
||||
usedLossyConversion:NULL];
|
||||
}
|
||||
}
|
||||
return encodedResponse;
|
||||
}
|
||||
|
||||
// If we still can't decode it, bail out
|
||||
if (!responseText) {
|
||||
- (void)sendData:(NSData *)data
|
||||
responseType:(NSString *)responseType
|
||||
forTask:(RCTNetworkTask *)task
|
||||
{
|
||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||
|
||||
if (data.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *responseString;
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
responseString = [RCTNetworking decodeTextData:data fromResponse:task.response];
|
||||
if (!responseString) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseString = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseText ?: @""];
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseString];
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:responseJSON];
|
||||
}
|
||||
|
||||
- (void)sendRequest:(NSURLRequest *)request
|
||||
responseType:(NSString *)responseType
|
||||
incrementalUpdates:(BOOL)incrementalUpdates
|
||||
responseSender:(RCTResponseSenderBlock)responseSender
|
||||
{
|
||||
|
@ -371,7 +384,7 @@ RCT_EXPORT_MODULE()
|
|||
});
|
||||
};
|
||||
|
||||
void (^responseBlock)(NSURLResponse *) = ^(NSURLResponse *response) {
|
||||
RCTURLRequestResponseBlock responseBlock = ^(NSURLResponse *response) {
|
||||
dispatch_async(self->_methodQueue, ^{
|
||||
NSDictionary<NSString *, NSString *> *headers;
|
||||
NSInteger status;
|
||||
|
@ -389,17 +402,44 @@ RCT_EXPORT_MODULE()
|
|||
});
|
||||
};
|
||||
|
||||
void (^incrementalDataBlock)(NSData *) = incrementalUpdates ? ^(NSData *data) {
|
||||
dispatch_async(self->_methodQueue, ^{
|
||||
[self sendData:data forTask:task];
|
||||
});
|
||||
} : nil;
|
||||
// XHR does not allow you to peek at xhr.response before the response is
|
||||
// finished. Only when xhr.responseType is set to ''/'text', consumers may
|
||||
// peek at xhr.responseText. So unless the requested responseType is 'text',
|
||||
// we only send progress updates and not incremental data updates to JS here.
|
||||
RCTURLRequestIncrementalDataBlock incrementalDataBlock = nil;
|
||||
RCTURLRequestProgressBlock downloadProgressBlock = nil;
|
||||
if (incrementalUpdates) {
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
incrementalDataBlock = ^(NSData *data, int64_t progress, int64_t total) {
|
||||
dispatch_async(self->_methodQueue, ^{
|
||||
NSString *responseString = [RCTNetworking decodeTextData:data fromResponse:task.response];
|
||||
if (!responseString) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
}
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseString, @(progress), @(total)];
|
||||
[self sendEventWithName:@"didReceiveNetworkIncrementalData" body:responseJSON];
|
||||
});
|
||||
};
|
||||
} else {
|
||||
downloadProgressBlock = ^(int64_t progress, int64_t total) {
|
||||
dispatch_async(self->_methodQueue, ^{
|
||||
NSArray<id> *responseJSON = @[task.requestID, @(progress), @(total)];
|
||||
[self sendEventWithName:@"didReceiveNetworkDataProgress" body:responseJSON];
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
RCTURLRequestCompletionBlock completionBlock =
|
||||
^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||
dispatch_async(self->_methodQueue, ^{
|
||||
if (!incrementalUpdates) {
|
||||
[self sendData:data forTask:task];
|
||||
// Unless we were sending incremental (text) chunks to JS, all along, now
|
||||
// is the time to send the request body to JS.
|
||||
if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) {
|
||||
[self sendData:data
|
||||
responseType:responseType
|
||||
forTask:task];
|
||||
}
|
||||
NSArray *responseJSON = @[task.requestID,
|
||||
RCTNullIfNil(error.localizedDescription),
|
||||
|
@ -412,6 +452,7 @@ RCT_EXPORT_MODULE()
|
|||
};
|
||||
|
||||
task = [self networkTaskWithRequest:request completionBlock:completionBlock];
|
||||
task.downloadProgressBlock = downloadProgressBlock;
|
||||
task.incrementalDataBlock = incrementalDataBlock;
|
||||
task.responseBlock = responseBlock;
|
||||
task.uploadProgressBlock = uploadProgressBlock;
|
||||
|
@ -453,8 +494,10 @@ RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
|
|||
// loading a large file to build the request body
|
||||
[self buildRequest:query completionBlock:^(NSURLRequest *request) {
|
||||
|
||||
NSString *responseType = [RCTConvert NSString:query[@"responseType"]];
|
||||
BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]];
|
||||
[self sendRequest:request
|
||||
responseType:responseType
|
||||
incrementalUpdates:incrementalUpdates
|
||||
responseSender:responseSender];
|
||||
}];
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
const RCTNetworking = require('RCTNetworking');
|
||||
|
||||
const EventTarget = require('event-target-shim');
|
||||
const base64 = require('base64-js');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const utf8 = require('utf8');
|
||||
const warning = require('fbjs/lib/warning');
|
||||
|
||||
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
|
@ -102,7 +102,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
_method: ?string = null;
|
||||
_response: string | ?Object;
|
||||
_responseType: ResponseType;
|
||||
_responseText: string = '';
|
||||
_response: string = '';
|
||||
_sent: boolean;
|
||||
_url: ?string = null;
|
||||
_timedOut: boolean = false;
|
||||
|
@ -125,7 +125,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
this._cachedResponse = undefined;
|
||||
this._hasError = false;
|
||||
this._headers = {};
|
||||
this._responseText = '';
|
||||
this._response = '';
|
||||
this._responseType = '';
|
||||
this._sent = false;
|
||||
this._lowerCaseResponseHeaders = {};
|
||||
|
@ -141,10 +141,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
|
||||
// $FlowIssue #10784535
|
||||
set responseType(responseType: ResponseType): void {
|
||||
if (this.readyState > HEADERS_RECEIVED) {
|
||||
if (this._sent) {
|
||||
throw new Error(
|
||||
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
|
||||
"response type cannot be set if the object's state is LOADING or DONE"
|
||||
'Failed to set the \'responseType\' property on \'XMLHttpRequest\': The ' +
|
||||
'response type cannot be set after the request has been sent.'
|
||||
);
|
||||
}
|
||||
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
|
||||
|
@ -174,7 +174,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
if (this.readyState < LOADING) {
|
||||
return '';
|
||||
}
|
||||
return this._responseText;
|
||||
return this._response;
|
||||
}
|
||||
|
||||
// $FlowIssue #10784535
|
||||
|
@ -183,7 +183,7 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
if (responseType === '' || responseType === 'text') {
|
||||
return this.readyState < LOADING || this._hasError
|
||||
? ''
|
||||
: this._responseText;
|
||||
: this._response;
|
||||
}
|
||||
|
||||
if (this.readyState !== DONE) {
|
||||
|
@ -194,26 +194,25 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
return this._cachedResponse;
|
||||
}
|
||||
|
||||
switch (this._responseType) {
|
||||
switch (responseType) {
|
||||
case 'document':
|
||||
this._cachedResponse = null;
|
||||
break;
|
||||
|
||||
case 'arraybuffer':
|
||||
this._cachedResponse = toArrayBuffer(
|
||||
this._responseText, this.getResponseHeader('content-type') || '');
|
||||
this._cachedResponse = base64.toByteArray(this._response).buffer;
|
||||
break;
|
||||
|
||||
case 'blob':
|
||||
this._cachedResponse = new global.Blob(
|
||||
[this._responseText],
|
||||
[base64.toByteArray(this._response).buffer],
|
||||
{type: this.getResponseHeader('content-type') || ''}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
try {
|
||||
this._cachedResponse = JSON.parse(this._responseText);
|
||||
this._cachedResponse = JSON.parse(this._response);
|
||||
} catch (_) {
|
||||
this._cachedResponse = null;
|
||||
}
|
||||
|
@ -232,7 +231,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
}
|
||||
|
||||
// exposed for testing
|
||||
__didUploadProgress(requestId: number, progress: number, total: number): void {
|
||||
__didUploadProgress(
|
||||
requestId: number,
|
||||
progress: number,
|
||||
total: number
|
||||
): void {
|
||||
if (requestId === this._requestId) {
|
||||
this.upload.dispatchEvent({
|
||||
type: 'progress',
|
||||
|
@ -261,16 +264,47 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
}
|
||||
}
|
||||
|
||||
__didReceiveData(requestId: number, responseText: string): void {
|
||||
if (requestId === this._requestId) {
|
||||
if (!this._responseText) {
|
||||
this._responseText = responseText;
|
||||
} else {
|
||||
this._responseText += responseText;
|
||||
}
|
||||
this._cachedResponse = undefined; // force lazy recomputation
|
||||
this.setReadyState(this.LOADING);
|
||||
__didReceiveData(requestId: number, response: string): void {
|
||||
if (requestId !== this._requestId) {
|
||||
return;
|
||||
}
|
||||
this._response = response;
|
||||
this._cachedResponse = undefined; // force lazy recomputation
|
||||
this.setReadyState(this.LOADING);
|
||||
}
|
||||
|
||||
__didReceiveIncrementalData(
|
||||
requestId: number,
|
||||
responseText: string,
|
||||
progress: number,
|
||||
total: number
|
||||
) {
|
||||
if (requestId !== this._requestId) {
|
||||
return;
|
||||
}
|
||||
if (!this._response) {
|
||||
this._response = responseText;
|
||||
} else {
|
||||
this._response += responseText;
|
||||
}
|
||||
this.setReadyState(this.LOADING);
|
||||
this.__didReceiveDataProgress(requestId, progress, total);
|
||||
}
|
||||
|
||||
__didReceiveDataProgress(
|
||||
requestId: number,
|
||||
loaded: number,
|
||||
total: number
|
||||
): void {
|
||||
if (requestId !== this._requestId) {
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent({
|
||||
type: 'progress',
|
||||
lengthComputable: total >= 0,
|
||||
loaded,
|
||||
total,
|
||||
});
|
||||
}
|
||||
|
||||
// exposed for testing
|
||||
|
@ -281,7 +315,9 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
): void {
|
||||
if (requestId === this._requestId) {
|
||||
if (error) {
|
||||
this._responseText = error;
|
||||
if (this._responseType === '' || this._responseType === 'text') {
|
||||
this._response = error;
|
||||
}
|
||||
this._hasError = true;
|
||||
if (timeOutError) {
|
||||
this._timedOut = true;
|
||||
|
@ -343,49 +379,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
if (!url) {
|
||||
throw new Error('Cannot load an empty url');
|
||||
}
|
||||
this._reset();
|
||||
this._method = method.toUpperCase();
|
||||
this._url = url;
|
||||
this._aborted = false;
|
||||
this.setReadyState(this.OPENED);
|
||||
}
|
||||
|
||||
sendImpl(
|
||||
method: ?string,
|
||||
url: ?string,
|
||||
headers: Object,
|
||||
data: any,
|
||||
useIncrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
): void {
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didSendNetworkData',
|
||||
(args) => this.__didUploadProgress(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkResponse',
|
||||
(args) => this.__didReceiveResponse(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkData',
|
||||
(args) => this.__didReceiveData(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didCompleteNetworkResponse',
|
||||
(args) => this.__didCompleteResponse(...args)
|
||||
));
|
||||
RCTNetworking.sendRequest(
|
||||
method,
|
||||
this._trackingName,
|
||||
url,
|
||||
headers,
|
||||
data,
|
||||
useIncrementalUpdates,
|
||||
timeout,
|
||||
this.__didCreateRequest.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
send(data: any): void {
|
||||
if (this.readyState !== this.OPENED) {
|
||||
throw new Error('Request has not been opened');
|
||||
|
@ -394,14 +393,52 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
throw new Error('Request has already been sent');
|
||||
}
|
||||
this._sent = true;
|
||||
const incrementalEvents = this._incrementalEvents || !!this.onreadystatechange;
|
||||
this.sendImpl(
|
||||
const incrementalEvents = this._incrementalEvents ||
|
||||
!!this.onreadystatechange ||
|
||||
!!this.onprogress;
|
||||
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didSendNetworkData',
|
||||
(args) => this.__didUploadProgress(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkResponse',
|
||||
(args) => this.__didReceiveResponse(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkData',
|
||||
(args) => this.__didReceiveData(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkIncrementalData',
|
||||
(args) => this.__didReceiveIncrementalData(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didReceiveNetworkDataProgress',
|
||||
(args) => this.__didReceiveDataProgress(...args)
|
||||
));
|
||||
this._subscriptions.push(RCTNetworking.addListener(
|
||||
'didCompleteNetworkResponse',
|
||||
(args) => this.__didCompleteResponse(...args)
|
||||
));
|
||||
|
||||
let nativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
|
||||
nativeResponseType = 'base64';
|
||||
}
|
||||
|
||||
invariant(this._method, 'Request method needs to be defined.');
|
||||
invariant(this._url, 'Request URL needs to be defined.');
|
||||
RCTNetworking.sendRequest(
|
||||
this._method,
|
||||
this._trackingName,
|
||||
this._url,
|
||||
this._headers,
|
||||
data,
|
||||
nativeResponseType,
|
||||
incrementalEvents,
|
||||
this.timeout
|
||||
this.timeout,
|
||||
this.__didCreateRequest.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -454,32 +491,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
|||
// have to send repeated LOADING events with incremental updates
|
||||
// to responseText, which will avoid a bunch of native -> JS
|
||||
// bridge traffic.
|
||||
if (type === 'readystatechange') {
|
||||
if (type === 'readystatechange' || type === 'progress') {
|
||||
this._incrementalEvents = true;
|
||||
}
|
||||
super.addEventListener(type, listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toArrayBuffer(text: string, contentType: string): ArrayBuffer {
|
||||
const {length} = text;
|
||||
if (length === 0) {
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
|
||||
const charsetMatch = contentType.match(/;\s*charset=([^;]*)/i);
|
||||
const charset = charsetMatch ? charsetMatch[1].trim() : 'utf-8';
|
||||
|
||||
if (/^utf-?8$/i.test(charset)) {
|
||||
return utf8.encode(text);
|
||||
} else { //TODO: utf16 / ucs2 / utf32
|
||||
const array = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
array[i] = text.charCodeAt(i); // Uint8Array automatically masks with 0xff
|
||||
}
|
||||
return array.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = XMLHttpRequest;
|
||||
|
|
|
@ -10,160 +10,174 @@
|
|||
'use strict';
|
||||
|
||||
jest
|
||||
.disableAutomock()
|
||||
.dontMock('event-target-shim')
|
||||
.setMock('NativeModules', {
|
||||
.disableAutomock()
|
||||
.dontMock('event-target-shim')
|
||||
.setMock('NativeModules', {
|
||||
Networking: {
|
||||
addListener: function(){},
|
||||
removeListeners: function(){},
|
||||
addListener: function() {},
|
||||
removeListeners: function() {},
|
||||
sendRequest: (options, callback) => {
|
||||
callback(1);
|
||||
},
|
||||
abortRequest: function() {},
|
||||
}
|
||||
});
|
||||
|
||||
const XMLHttpRequest = require('XMLHttpRequest');
|
||||
|
||||
describe('XMLHttpRequest', function(){
|
||||
var xhr;
|
||||
var handleTimeout;
|
||||
var handleError;
|
||||
var handleLoad;
|
||||
var handleReadyStateChange;
|
||||
describe('XMLHttpRequest', function() {
|
||||
var xhr;
|
||||
var handleTimeout;
|
||||
var handleError;
|
||||
var handleLoad;
|
||||
var handleReadyStateChange;
|
||||
|
||||
beforeEach(() => {
|
||||
xhr = new XMLHttpRequest();
|
||||
beforeEach(() => {
|
||||
xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.ontimeout = jest.fn();
|
||||
xhr.onerror = jest.fn();
|
||||
xhr.onload = jest.fn();
|
||||
xhr.onreadystatechange = jest.fn();
|
||||
xhr.ontimeout = jest.fn();
|
||||
xhr.onerror = jest.fn();
|
||||
xhr.onload = jest.fn();
|
||||
xhr.onreadystatechange = jest.fn();
|
||||
|
||||
handleTimeout = jest.fn();
|
||||
handleError = jest.fn();
|
||||
handleLoad = jest.fn();
|
||||
handleReadyStateChange = jest.fn();
|
||||
handleTimeout = jest.fn();
|
||||
handleError = jest.fn();
|
||||
handleLoad = jest.fn();
|
||||
handleReadyStateChange = jest.fn();
|
||||
|
||||
xhr.addEventListener('timeout', handleTimeout);
|
||||
xhr.addEventListener('error', handleError);
|
||||
xhr.addEventListener('load', handleLoad);
|
||||
xhr.addEventListener('readystatechange', handleReadyStateChange);
|
||||
xhr.addEventListener('timeout', handleTimeout);
|
||||
xhr.addEventListener('error', handleError);
|
||||
xhr.addEventListener('load', handleLoad);
|
||||
xhr.addEventListener('readystatechange', handleReadyStateChange);
|
||||
});
|
||||
|
||||
xhr.__didCreateRequest(1);
|
||||
});
|
||||
afterEach(() => {
|
||||
xhr = null;
|
||||
handleTimeout = null;
|
||||
handleError = null;
|
||||
handleLoad = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
xhr = null;
|
||||
handleTimeout = null;
|
||||
handleError = null;
|
||||
handleLoad = null;
|
||||
});
|
||||
it('should transition readyState correctly', function() {
|
||||
expect(xhr.readyState).toBe(xhr.UNSENT);
|
||||
|
||||
it('should transition readyState correctly', function() {
|
||||
xhr.open('GET', 'blabla');
|
||||
|
||||
expect(xhr.readyState).toBe(xhr.UNSENT);
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(1);
|
||||
expect(xhr.readyState).toBe(xhr.OPENED);
|
||||
});
|
||||
|
||||
xhr.open('GET', 'blabla');
|
||||
it('should expose responseType correctly', function() {
|
||||
expect(xhr.responseType).toBe('');
|
||||
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(1);
|
||||
expect(xhr.readyState).toBe(xhr.OPENED);
|
||||
});
|
||||
// Setting responseType to an unsupported value has no effect.
|
||||
xhr.responseType = 'arrayblobbuffertextfile';
|
||||
expect(xhr.responseType).toBe('');
|
||||
|
||||
it('should expose responseType correctly', function() {
|
||||
expect(xhr.responseType).toBe('');
|
||||
xhr.responseType = 'arraybuffer';
|
||||
expect(xhr.responseType).toBe('arraybuffer');
|
||||
|
||||
// Setting responseType to an unsupported value has no effect.
|
||||
xhr.responseType = 'arrayblobbuffertextfile';
|
||||
expect(xhr.responseType).toBe('');
|
||||
// Can't change responseType after first data has been received.
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
expect(() => { xhr.responseType = 'text'; }).toThrow();
|
||||
});
|
||||
|
||||
xhr.responseType = 'arraybuffer';
|
||||
expect(xhr.responseType).toBe('arraybuffer');
|
||||
it('should expose responseText correctly', function() {
|
||||
xhr.responseType = '';
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
|
||||
// Can't change responseType after first data has been received.
|
||||
xhr.__didReceiveData(1, 'Some data');
|
||||
expect(() => { xhr.responseType = 'text'; }).toThrow();
|
||||
});
|
||||
xhr.responseType = 'arraybuffer';
|
||||
expect(() => xhr.responseText).toThrow();
|
||||
expect(xhr.response).toBe(null);
|
||||
|
||||
it('should expose responseText correctly', function() {
|
||||
xhr.responseType = '';
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
xhr.responseType = 'text';
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
|
||||
xhr.responseType = 'arraybuffer';
|
||||
expect(() => xhr.responseText).toThrow();
|
||||
expect(xhr.response).toBe(null);
|
||||
// responseText is read-only.
|
||||
expect(() => { xhr.responseText = 'hi'; }).toThrow();
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
|
||||
xhr.responseType = 'text';
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
xhr.__didReceiveData(1, 'Some data');
|
||||
expect(xhr.responseText).toBe('Some data');
|
||||
});
|
||||
|
||||
// responseText is read-only.
|
||||
expect(() => { xhr.responseText = 'hi'; }).toThrow();
|
||||
expect(xhr.responseText).toBe('');
|
||||
expect(xhr.response).toBe('');
|
||||
it('should call ontimeout function when the request times out', function() {
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
xhr.__didCompleteResponse(1, 'Timeout', true);
|
||||
xhr.__didCompleteResponse(1, 'Timeout', true);
|
||||
|
||||
xhr.__didReceiveData(1, 'Some data');
|
||||
expect(xhr.responseText).toBe('Some data');
|
||||
});
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
|
||||
it('should call ontimeout function when the request times out', function(){
|
||||
xhr.__didCompleteResponse(1, 'Timeout', true);
|
||||
expect(xhr.ontimeout.mock.calls.length).toBe(1);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
expect(handleTimeout.mock.calls.length).toBe(1);
|
||||
expect(handleError).not.toBeCalled();
|
||||
expect(handleLoad).not.toBeCalled();
|
||||
});
|
||||
|
||||
expect(xhr.ontimeout.mock.calls.length).toBe(1);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
it('should call onerror function when the request times out', function() {
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
xhr.__didCompleteResponse(1, 'Generic error');
|
||||
|
||||
expect(handleTimeout.mock.calls.length).toBe(1);
|
||||
expect(handleError).not.toBeCalled();
|
||||
expect(handleLoad).not.toBeCalled();
|
||||
});
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
|
||||
it('should call onerror function when the request times out', function(){
|
||||
xhr.__didCompleteResponse(1, 'Generic error');
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
|
||||
expect(xhr.onerror.mock.calls.length).toBe(1);
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(2);
|
||||
expect(handleError.mock.calls.length).toBe(1);
|
||||
expect(handleTimeout).not.toBeCalled();
|
||||
expect(handleLoad).not.toBeCalled();
|
||||
});
|
||||
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
|
||||
expect(xhr.onerror.mock.calls.length).toBe(1);
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
expect(xhr.onload).not.toBeCalled();
|
||||
it('should call onload function when there is no error', function() {
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
xhr.__didCompleteResponse(1, null);
|
||||
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(1);
|
||||
expect(handleError.mock.calls.length).toBe(1);
|
||||
expect(handleTimeout).not.toBeCalled();
|
||||
expect(handleLoad).not.toBeCalled();
|
||||
});
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
|
||||
it('should call onload function when there is no error', function(){
|
||||
xhr.__didCompleteResponse(1, null);
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(2);
|
||||
expect(xhr.onload.mock.calls.length).toBe(1);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
|
||||
expect(xhr.readyState).toBe(xhr.DONE);
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(2);
|
||||
expect(handleLoad.mock.calls.length).toBe(1);
|
||||
expect(handleError).not.toBeCalled();
|
||||
expect(handleTimeout).not.toBeCalled();
|
||||
});
|
||||
|
||||
expect(xhr.onreadystatechange.mock.calls.length).toBe(1);
|
||||
expect(xhr.onload.mock.calls.length).toBe(1);
|
||||
expect(xhr.onerror).not.toBeCalled();
|
||||
expect(xhr.ontimeout).not.toBeCalled();
|
||||
it('should call onload function when there is no error', function() {
|
||||
xhr.open('GET', 'blabla');
|
||||
xhr.send();
|
||||
|
||||
expect(handleReadyStateChange.mock.calls.length).toBe(1);
|
||||
expect(handleLoad.mock.calls.length).toBe(1);
|
||||
expect(handleError).not.toBeCalled();
|
||||
expect(handleTimeout).not.toBeCalled();
|
||||
});
|
||||
xhr.upload.onprogress = jest.fn();
|
||||
var handleProgress = jest.fn();
|
||||
xhr.upload.addEventListener('progress', handleProgress);
|
||||
|
||||
it('should call onload function when there is no error', function() {
|
||||
xhr.upload.onprogress = jest.fn();
|
||||
var handleProgress = jest.fn();
|
||||
xhr.upload.addEventListener('progress', handleProgress);
|
||||
xhr.__didUploadProgress(1, 42, 100);
|
||||
|
||||
xhr.__didUploadProgress(1, 42, 100);
|
||||
expect(xhr.upload.onprogress.mock.calls.length).toBe(1);
|
||||
expect(handleProgress.mock.calls.length).toBe(1);
|
||||
|
||||
expect(xhr.upload.onprogress.mock.calls.length).toBe(1);
|
||||
expect(handleProgress.mock.calls.length).toBe(1);
|
||||
|
||||
expect(xhr.upload.onprogress.mock.calls[0][0].loaded).toBe(42);
|
||||
expect(xhr.upload.onprogress.mock.calls[0][0].total).toBe(100);
|
||||
expect(handleProgress.mock.calls[0][0].loaded).toBe(42);
|
||||
expect(handleProgress.mock.calls[0][0].total).toBe(100);
|
||||
});
|
||||
expect(xhr.upload.onprogress.mock.calls[0][0].loaded).toBe(42);
|
||||
expect(xhr.upload.onprogress.mock.calls[0][0].total).toBe(100);
|
||||
expect(handleProgress.mock.calls[0][0].loaded).toBe(42);
|
||||
expect(handleProgress.mock.calls[0][0].total).toBe(100);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -83,6 +83,7 @@ public class NetworkRecordingModuleMock extends ReactContextBaseJavaModule {
|
|||
int requestId,
|
||||
ReadableArray headers,
|
||||
ReadableMap data,
|
||||
final String responseType,
|
||||
boolean incrementalUpdates,
|
||||
int timeout) {
|
||||
mLastRequestId = requestId;
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -34,6 +36,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm
|
|||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.JavaNetCookieJar;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
|
@ -157,6 +160,7 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
final int requestId,
|
||||
ReadableArray headers,
|
||||
ReadableMap data,
|
||||
final String responseType,
|
||||
final boolean useIncrementalUpdates,
|
||||
int timeout) {
|
||||
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||
|
@ -165,18 +169,54 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
requestBuilder.tag(requestId);
|
||||
}
|
||||
|
||||
OkHttpClient client = mClient;
|
||||
final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken);
|
||||
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
|
||||
|
||||
// If JS is listening for progress updates, install a ProgressResponseBody that intercepts the
|
||||
// response and counts bytes received.
|
||||
if (useIncrementalUpdates) {
|
||||
clientBuilder.addNetworkInterceptor(new Interceptor() {
|
||||
@Override
|
||||
public Response intercept(Interceptor.Chain chain) throws IOException {
|
||||
Response originalResponse = chain.proceed(chain.request());
|
||||
ProgressResponseBody responseBody = new ProgressResponseBody(
|
||||
originalResponse.body(),
|
||||
new ProgressListener() {
|
||||
long last = System.nanoTime();
|
||||
|
||||
@Override
|
||||
public void onProgress(long bytesWritten, long contentLength, boolean done) {
|
||||
long now = System.nanoTime();
|
||||
if (!done && !shouldDispatch(now, last)) {
|
||||
return;
|
||||
}
|
||||
if (responseType.equals("text")) {
|
||||
// For 'text' responses we continuously send response data with progress info to
|
||||
// JS below, so no need to do anything here.
|
||||
return;
|
||||
}
|
||||
ResponseUtil.onDataReceivedProgress(
|
||||
eventEmitter,
|
||||
requestId,
|
||||
bytesWritten,
|
||||
contentLength);
|
||||
last = now;
|
||||
}
|
||||
});
|
||||
return originalResponse.newBuilder().body(responseBody).build();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the current timeout does not equal the passed in timeout, we need to clone the existing
|
||||
// client and set the timeout explicitly on the clone. This is cheap as everything else is
|
||||
// shared under the hood.
|
||||
// See https://github.com/square/okhttp/wiki/Recipes#per-call-configuration for more information
|
||||
if (timeout != mClient.connectTimeoutMillis()) {
|
||||
client = mClient.newBuilder()
|
||||
.readTimeout(timeout, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
clientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
OkHttpClient client = clientBuilder.build();
|
||||
|
||||
final RCTDeviceEventEmitter eventEmitter = getEventEmitter(executorToken);
|
||||
Headers requestHeaders = extractHeaders(headers, data);
|
||||
if (requestHeaders == null) {
|
||||
ResponseUtil.onRequestError(eventEmitter, requestId, "Unrecognized headers format", null);
|
||||
|
@ -247,11 +287,11 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
method,
|
||||
RequestBodyUtil.createProgressRequest(
|
||||
multipartBuilder.build(),
|
||||
new ProgressRequestListener() {
|
||||
new ProgressListener() {
|
||||
long last = System.nanoTime();
|
||||
|
||||
@Override
|
||||
public void onRequestProgress(long bytesWritten, long contentLength, boolean done) {
|
||||
public void onProgress(long bytesWritten, long contentLength, boolean done) {
|
||||
long now = System.nanoTime();
|
||||
if (done || shouldDispatch(now, last)) {
|
||||
ResponseUtil.onDataSend(eventEmitter, requestId, bytesWritten, contentLength);
|
||||
|
@ -292,13 +332,23 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
|
||||
ResponseBody responseBody = response.body();
|
||||
try {
|
||||
if (useIncrementalUpdates) {
|
||||
// If JS wants progress updates during the download, and it requested a text response,
|
||||
// periodically send response data updates to JS.
|
||||
if (useIncrementalUpdates && responseType.equals("text")) {
|
||||
readWithProgress(eventEmitter, requestId, responseBody);
|
||||
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||
} else {
|
||||
ResponseUtil.onDataReceived(eventEmitter, requestId, responseBody.string());
|
||||
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise send the data in one big chunk, in the format that JS requested.
|
||||
String responseString = "";
|
||||
if (responseType.equals("text")) {
|
||||
responseString = responseBody.string();
|
||||
} else if (responseType.equals("base64")) {
|
||||
responseString = Base64.encodeToString(responseBody.bytes(), Base64.NO_WRAP);
|
||||
}
|
||||
ResponseUtil.onDataReceived(eventEmitter, requestId, responseString);
|
||||
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||
} catch (IOException e) {
|
||||
ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e);
|
||||
}
|
||||
|
@ -310,12 +360,27 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
|||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
ResponseBody responseBody) throws IOException {
|
||||
long totalBytesRead = -1;
|
||||
long contentLength = -1;
|
||||
try {
|
||||
ProgressResponseBody progressResponseBody = (ProgressResponseBody) responseBody;
|
||||
totalBytesRead = progressResponseBody.totalBytesRead();
|
||||
contentLength = progressResponseBody.contentLength();
|
||||
} catch (ClassCastException e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
Reader reader = responseBody.charStream();
|
||||
try {
|
||||
char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
|
||||
int read;
|
||||
while ((read = reader.read(buffer)) != -1) {
|
||||
ResponseUtil.onDataReceived(eventEmitter, requestId, new String(buffer, 0, read));
|
||||
ResponseUtil.onIncrementalDataReceived(
|
||||
eventEmitter,
|
||||
requestId,
|
||||
new String(buffer, 0, read),
|
||||
totalBytesRead,
|
||||
contentLength);
|
||||
}
|
||||
} finally {
|
||||
reader.close();
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
|
||||
public interface ProgressRequestListener {
|
||||
void onRequestProgress(long bytesWritten, long contentLength, boolean done);
|
||||
public interface ProgressListener {
|
||||
void onProgress(long bytesWritten, long contentLength, boolean done);
|
||||
}
|
|
@ -12,22 +12,19 @@ package com.facebook.react.modules.network;
|
|||
import java.io.IOException;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.internal.Util;
|
||||
import okio.BufferedSink;
|
||||
import okio.Buffer;
|
||||
import okio.Sink;
|
||||
import okio.ForwardingSink;
|
||||
import okio.ByteString;
|
||||
import okio.Okio;
|
||||
import okio.Source;
|
||||
|
||||
public class ProgressRequestBody extends RequestBody {
|
||||
|
||||
private final RequestBody mRequestBody;
|
||||
private final ProgressRequestListener mProgressListener;
|
||||
private final ProgressListener mProgressListener;
|
||||
private BufferedSink mBufferedSink;
|
||||
|
||||
public ProgressRequestBody(RequestBody requestBody, ProgressRequestListener progressListener) {
|
||||
public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
|
||||
mRequestBody = requestBody;
|
||||
mProgressListener = progressListener;
|
||||
}
|
||||
|
@ -63,7 +60,8 @@ public class ProgressRequestBody extends RequestBody {
|
|||
contentLength = contentLength();
|
||||
}
|
||||
bytesWritten += byteCount;
|
||||
mProgressListener.onRequestProgress(bytesWritten, contentLength, bytesWritten == contentLength);
|
||||
mProgressListener.onProgress(
|
||||
bytesWritten, contentLength, bytesWritten == contentLength);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ForwardingSource;
|
||||
import okio.Okio;
|
||||
import okio.Source;
|
||||
|
||||
public class ProgressResponseBody extends ResponseBody {
|
||||
|
||||
private final ResponseBody mResponseBody;
|
||||
private final ProgressListener mProgressListener;
|
||||
private @Nullable BufferedSource mBufferedSource;
|
||||
private long mTotalBytesRead;
|
||||
|
||||
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
|
||||
this.mResponseBody = responseBody;
|
||||
this.mProgressListener = progressListener;
|
||||
mTotalBytesRead = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return mResponseBody.contentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return mResponseBody.contentLength();
|
||||
}
|
||||
|
||||
public long totalBytesRead() {
|
||||
return mTotalBytesRead;
|
||||
}
|
||||
|
||||
@Override public BufferedSource source() {
|
||||
if (mBufferedSource == null) {
|
||||
mBufferedSource = Okio.buffer(source(mResponseBody.source()));
|
||||
}
|
||||
return mBufferedSource;
|
||||
}
|
||||
|
||||
private Source source(Source source) {
|
||||
return new ForwardingSource(source) {
|
||||
@Override public long read(Buffer sink, long byteCount) throws IOException {
|
||||
long bytesRead = super.read(sink, byteCount);
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
mTotalBytesRead += bytesRead != -1 ? bytesRead : 0;
|
||||
mProgressListener.onProgress(
|
||||
mTotalBytesRead, mResponseBody.contentLength(), bytesRead == -1);
|
||||
return bytesRead;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -117,7 +117,9 @@ import okio.Source;
|
|||
/**
|
||||
* Creates a ProgressRequestBody that can be used for showing uploading progress
|
||||
*/
|
||||
public static ProgressRequestBody createProgressRequest(RequestBody requestBody, ProgressRequestListener listener) {
|
||||
public static ProgressRequestBody createProgressRequest(
|
||||
RequestBody requestBody,
|
||||
ProgressListener listener) {
|
||||
return new ProgressRequestBody(requestBody, listener);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,34 @@ public class ResponseUtil {
|
|||
eventEmitter.emit("didSendNetworkData", args);
|
||||
}
|
||||
|
||||
public static void onIncrementalDataReceived(
|
||||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
String data,
|
||||
long progress,
|
||||
long total) {
|
||||
WritableArray args = Arguments.createArray();
|
||||
args.pushInt(requestId);
|
||||
args.pushString(data);
|
||||
args.pushInt((int) progress);
|
||||
args.pushInt((int) total);
|
||||
|
||||
eventEmitter.emit("didReceiveNetworkIncrementalData", args);
|
||||
}
|
||||
|
||||
public static void onDataReceivedProgress(
|
||||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
long progress,
|
||||
long total) {
|
||||
WritableArray args = Arguments.createArray();
|
||||
args.pushInt(requestId);
|
||||
args.pushInt((int) progress);
|
||||
args.pushInt((int) total);
|
||||
|
||||
eventEmitter.emit("didReceiveNetworkDataProgress", args);
|
||||
}
|
||||
|
||||
public static void onDataReceived(
|
||||
RCTDeviceEventEmitter eventEmitter,
|
||||
int requestId,
|
||||
|
|
|
@ -61,11 +61,12 @@ import static org.mockito.Mockito.when;
|
|||
Call.class,
|
||||
RequestBodyUtil.class,
|
||||
ProgressRequestBody.class,
|
||||
ProgressRequestListener.class,
|
||||
ProgressListener.class,
|
||||
MultipartBody.class,
|
||||
MultipartBody.Builder.class,
|
||||
NetworkingModule.class,
|
||||
OkHttpClient.class,
|
||||
OkHttpClient.Builder.class,
|
||||
OkHttpCallUtil.class})
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
|
||||
|
@ -84,6 +85,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
|
||||
|
@ -91,11 +95,12 @@ public class NetworkingModuleTest {
|
|||
mock(ExecutorToken.class),
|
||||
"GET",
|
||||
"http://somedomain/foo",
|
||||
0,
|
||||
JavaOnlyArray.of(),
|
||||
null,
|
||||
true,
|
||||
0);
|
||||
/* requestId */ 0,
|
||||
/* headers */ JavaOnlyArray.of(),
|
||||
/* body */ null,
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(httpClient).newCall(argumentCaptor.capture());
|
||||
|
@ -112,6 +117,9 @@ public class NetworkingModuleTest {
|
|||
when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
|
||||
|
||||
OkHttpClient httpClient = mock(OkHttpClient.class);
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
|
||||
|
||||
List<JavaOnlyArray> invalidHeaders = Arrays.asList(JavaOnlyArray.of("foo"));
|
||||
|
@ -122,11 +130,12 @@ public class NetworkingModuleTest {
|
|||
mock(ExecutorToken.class),
|
||||
"GET",
|
||||
"http://somedoman/foo",
|
||||
0,
|
||||
JavaOnlyArray.from(invalidHeaders),
|
||||
null,
|
||||
true,
|
||||
0);
|
||||
/* requestId */ 0,
|
||||
/* headers */ JavaOnlyArray.from(invalidHeaders),
|
||||
/* body */ null,
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
verifyErrorEmit(emitter, 0);
|
||||
}
|
||||
|
@ -138,6 +147,9 @@ public class NetworkingModuleTest {
|
|||
when(context.getJSModule(any(ExecutorToken.class), any(Class.class))).thenReturn(emitter);
|
||||
|
||||
OkHttpClient httpClient = mock(OkHttpClient.class);
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient);
|
||||
|
||||
JavaOnlyMap body = new JavaOnlyMap();
|
||||
|
@ -152,8 +164,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
JavaOnlyArray.of(),
|
||||
body,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
verifyErrorEmit(emitter, 0);
|
||||
}
|
||||
|
@ -196,6 +209,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
|
||||
|
@ -209,8 +225,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")),
|
||||
body,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(httpClient).newCall(argumentCaptor.capture());
|
||||
|
@ -234,6 +251,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
|
||||
|
@ -248,8 +268,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
JavaOnlyArray.from(headers),
|
||||
null,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
verify(httpClient).newCall(argumentCaptor.capture());
|
||||
Headers requestHeaders = argumentCaptor.getValue().headers();
|
||||
|
@ -265,7 +286,8 @@ public class NetworkingModuleTest {
|
|||
.thenReturn(mock(InputStream.class));
|
||||
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class)))
|
||||
.thenReturn(mock(RequestBody.class));
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
|
||||
.thenCallRealMethod();
|
||||
|
||||
JavaOnlyMap body = new JavaOnlyMap();
|
||||
JavaOnlyArray formData = new JavaOnlyArray();
|
||||
|
@ -288,6 +310,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
networkingModule.sendRequest(
|
||||
|
@ -297,8 +322,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
new JavaOnlyArray(),
|
||||
body,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
// verify url, method, headers
|
||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
|
@ -320,7 +346,8 @@ public class NetworkingModuleTest {
|
|||
.thenReturn(mock(InputStream.class));
|
||||
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class)))
|
||||
.thenReturn(mock(RequestBody.class));
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
|
||||
.thenCallRealMethod();
|
||||
|
||||
List<JavaOnlyArray> headers = Arrays.asList(
|
||||
JavaOnlyArray.of("Accept", "text/plain"),
|
||||
|
@ -348,6 +375,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
networkingModule.sendRequest(
|
||||
|
@ -357,8 +387,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
JavaOnlyArray.from(headers),
|
||||
body,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
// verify url, method, headers
|
||||
ArgumentCaptor<Request> argumentCaptor = ArgumentCaptor.forClass(Request.class);
|
||||
|
@ -383,7 +414,8 @@ public class NetworkingModuleTest {
|
|||
when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class)))
|
||||
.thenReturn(inputStream);
|
||||
when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod();
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressRequestListener.class))).thenCallRealMethod();
|
||||
when(RequestBodyUtil.createProgressRequest(any(RequestBody.class), any(ProgressListener.class)))
|
||||
.thenCallRealMethod();
|
||||
when(inputStream.available()).thenReturn("imageUri".length());
|
||||
|
||||
final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class);
|
||||
|
@ -445,6 +477,9 @@ public class NetworkingModuleTest {
|
|||
return callMock;
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
|
@ -455,8 +490,9 @@ public class NetworkingModuleTest {
|
|||
0,
|
||||
JavaOnlyArray.from(headers),
|
||||
body,
|
||||
true,
|
||||
0);
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
/* timeout */ 0);
|
||||
|
||||
// verify RequestBodyPart for image
|
||||
PowerMockito.verifyStatic(times(1));
|
||||
|
@ -503,6 +539,9 @@ public class NetworkingModuleTest {
|
|||
return calls[(Integer) request.tag() - 1];
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
networkingModule.initialize();
|
||||
|
@ -515,7 +554,8 @@ public class NetworkingModuleTest {
|
|||
idx + 1,
|
||||
JavaOnlyArray.of(),
|
||||
null,
|
||||
true,
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
0);
|
||||
}
|
||||
verify(httpClient, times(3)).newCall(any(Request.class));
|
||||
|
@ -550,6 +590,9 @@ public class NetworkingModuleTest {
|
|||
return calls[(Integer) request.tag() - 1];
|
||||
}
|
||||
});
|
||||
OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class);
|
||||
when(clientBuilder.build()).thenReturn(httpClient);
|
||||
when(httpClient.newBuilder()).thenReturn(clientBuilder);
|
||||
NetworkingModule networkingModule =
|
||||
new NetworkingModule(mock(ReactApplicationContext.class), "", httpClient);
|
||||
|
||||
|
@ -561,7 +604,8 @@ public class NetworkingModuleTest {
|
|||
idx + 1,
|
||||
JavaOnlyArray.of(),
|
||||
null,
|
||||
true,
|
||||
/* responseType */ "text",
|
||||
/* useIncrementalUpdates*/ true,
|
||||
0);
|
||||
}
|
||||
verify(httpClient, times(3)).newCall(any(Request.class));
|
||||
|
|
Загрузка…
Ссылка в новой задаче