From a531efe26e9921454b0962fb3107c5b359414932 Mon Sep 17 00:00:00 2001 From: Douglas Lowder Date: Mon, 16 Jan 2017 13:23:33 -0800 Subject: [PATCH] Add an integration test for WebSocket Summary: **Motivation** See if we can safely run a WebSocket test in Travis CI Closes https://github.com/facebook/react-native/pull/11433 Differential Revision: D4342024 Pulled By: ericvicenti fbshipit-source-id: 137fb0c39ed7ea3726e2778d5c0bdac4cef6ab89 --- .../UIExplorerIntegrationTests.m | 9 + IntegrationTests/IntegrationTestsApp.js | 1 + IntegrationTests/WebSocketTest.js | 167 ++++++++++++++++++ .../launchWebSocketServer.command | 20 +++ .../websocket_integration_test_server.js | 52 ++++++ scripts/objc-test.sh | 5 +- 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 IntegrationTests/WebSocketTest.js create mode 100755 IntegrationTests/launchWebSocketServer.command create mode 100755 IntegrationTests/websocket_integration_test_server.js diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m index 91a0c8157..d508c5016 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerIntegrationTests.m @@ -18,6 +18,14 @@ [_runner runTest:_cmd module:@#name]; \ } +#define RCT_TEST_ONLY_WITH_PACKAGER(name) \ +- (void)test##name \ +{ \ + if (getenv("CI_USE_PACKAGER")) { \ + [_runner runTest:_cmd module:@#name]; \ + } \ +} + @interface UIExplorerIntegrationTests : XCTestCase @end @@ -63,6 +71,7 @@ RCT_TEST(AppEventsTest) //RCT_TEST(LayoutEventsTest) // Disabled due to flakiness: #8686784 RCT_TEST(SimpleSnapshotTest) RCT_TEST(PromiseTest) +RCT_TEST_ONLY_WITH_PACKAGER(WebSocketTest) @end diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index 90fbe446d..1a8a27caa 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -31,6 +31,7 @@ var TESTS = [ require('./SimpleSnapshotTest'), require('./ImageSnapshotTest'), require('./PromiseTest'), + require('./WebSocketTest'), ]; TESTS.forEach( diff --git a/IntegrationTests/WebSocketTest.js b/IntegrationTests/WebSocketTest.js new file mode 100644 index 000000000..bae6b602b --- /dev/null +++ b/IntegrationTests/WebSocketTest.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * 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. + * + * @flow + */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { View } = ReactNative; +var { TestModule } = ReactNative.NativeModules; + +const DEFAULT_WS_URL = 'ws://localhost:5555/'; + +const WS_EVENTS = [ + 'close', + 'error', + 'message', + 'open', +]; +const WS_STATES = [ + /* 0 */ 'CONNECTING', + /* 1 */ 'OPEN', + /* 2 */ 'CLOSING', + /* 3 */ 'CLOSED', +]; + +type State = { + url: string; + fetchStatus: ?string; + socket: ?WebSocket; + socketState: ?number; + lastSocketEvent: ?string; + lastMessage: ?string | ?ArrayBuffer; + testMessage: string; + testExpectedResponse: string; +}; + +class WebSocketTest extends React.Component { + state: State = { + url: DEFAULT_WS_URL, + fetchStatus: null, + socket: null, + socketState: null, + lastSocketEvent: null, + lastMessage: null, + testMessage: 'testMessage', + testExpectedResponse: 'testMessage_response' + }; + + _waitFor = (condition: any, timeout: any, callback: any) => { + var remaining = timeout; + var t; + var timeoutFunction = function() { + if (condition()) { + callback(true); + return; + } + remaining--; + if (remaining === 0) { + callback(false); + } else { + t = setTimeout(timeoutFunction,1000); + } + }; + t = setTimeout(timeoutFunction,1000); + } + + _connect = () => { + const socket = new WebSocket(this.state.url); + WS_EVENTS.forEach(ev => socket.addEventListener(ev, this._onSocketEvent)); + this.setState({ + socket, + socketState: socket.readyState, + }); + }; + + _socketIsConnected = () => { + return this.state.socketState === 1; //'OPEN' + } + + _socketIsDisconnected = () => { + return this.state.socketState === 3; //'CLOSED' + } + + _disconnect = () => { + if (!this.state.socket) { + return; + } + this.state.socket.close(); + }; + + _onSocketEvent = (event: any) => { + const state: any = { + socketState: event.target.readyState, + lastSocketEvent: event.type, + }; + if (event.type === 'message') { + state.lastMessage = event.data; + } + this.setState(state); + }; + + _sendText = (text: string) => { + if (!this.state.socket) { + return; + } + this.state.socket.send(text); + }; + + _sendTestMessage = () => { + this._sendText(this.state.testMessage); + }; + + _receivedTestExpectedResponse = () => { + return (this.state.lastMessage === this.state.testExpectedResponse); + }; + + componentDidMount() { + this.testConnect(); + } + + testConnect = () => { + var component = this; + component._connect(); + component._waitFor(component._socketIsConnected, 5, function(connectSucceeded) { + if (!connectSucceeded) { + TestModule.markTestPassed(false); + return; + } + component.testSendAndReceive(); + }); + } + + testSendAndReceive = () => { + var component = this; + component._sendTestMessage(); + component._waitFor(component._receivedTestExpectedResponse, 5, function(messageReceived) { + if (!messageReceived) { + TestModule.markTestPassed(false); + return; + } + component.testDisconnect(); + }); + } + + testDisconnect = () => { + var component = this; + component._disconnect(); + component._waitFor(component._socketIsDisconnected, 5, function(disconnectSucceeded) { + TestModule.markTestPassed(disconnectSucceeded); + }); + } + + render(): React.Element { + return ; + } +} + +WebSocketTest.displayName = 'WebSocketTest'; + +module.exports = WebSocketTest; diff --git a/IntegrationTests/launchWebSocketServer.command b/IntegrationTests/launchWebSocketServer.command new file mode 100755 index 000000000..ebb305dc1 --- /dev/null +++ b/IntegrationTests/launchWebSocketServer.command @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Copyright (c) 2015-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# 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. + +# Set terminal title +echo -en "\033]0;Web Socket Test Server\a" +clear + +THIS_DIR=$(dirname "$0") +pushd "$THIS_DIR" +./websocket_integration_test_server.js +popd + +echo "Process terminated. Press to close the window" +read diff --git a/IntegrationTests/websocket_integration_test_server.js b/IntegrationTests/websocket_integration_test_server.js new file mode 100755 index 000000000..fa66fc983 --- /dev/null +++ b/IntegrationTests/websocket_integration_test_server.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * 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. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +/* eslint-env node */ + +const WebSocket = require('ws'); + +console.log(`\ +WebSocket integration test server + +This will send each incoming message back, with the string '_response' appended. +An incoming message of 'exit' will shut down the server. + +`); + +const server = new WebSocket.Server({port: 5555}); +server.on('connection', (ws) => { + ws.on('message', (message) => { + console.log('Received message:', message); + if (message === 'exit') { + console.log('WebSocket integration test server exit'); + process.exit(0); + } + console.log('Cookie:', ws.upgradeReq.headers.cookie); + ws.send(message + '_response'); + }); + + ws.send('hello'); +}); diff --git a/scripts/objc-test.sh b/scripts/objc-test.sh index d9dda1a7f..571efc842 100755 --- a/scripts/objc-test.sh +++ b/scripts/objc-test.sh @@ -4,6 +4,7 @@ # Start the packager and preload the UIExplorerApp bundle for better performance in integration tests open "./packager/launchPackager.command" || echo "Can't start packager automatically" +open "./IntegrationTests/launchWebSocketServer.command" || echo "Can't start web socket server automatically" sleep 20 curl 'http://localhost:8081/Examples/UIExplorer/js/UIExplorerApp.ios.bundle?platform=ios&dev=true' -o temp.bundle rm temp.bundle @@ -23,8 +24,10 @@ function cleanup { WATCHMAN_LOGS=/usr/local/Cellar/watchman/3.1/var/run/watchman/$USER.log [ -f $WATCHMAN_LOGS ] && cat $WATCHMAN_LOGS fi - # kill whatever is occupying port 8081 + # kill whatever is occupying port 8081 (packager) lsof -i tcp:8081 | awk 'NR!=1 {print $2}' | xargs kill + # kill whatever is occupying port 5555 (web socket server) + lsof -i tcp:5555 | awk 'NR!=1 {print $2}' | xargs kill } trap cleanup EXIT