顯示具有 jQuery 標籤的文章。 顯示所有文章
顯示具有 jQuery 標籤的文章。 顯示所有文章

2019年1月9日 星期三

[Flask] Socket.IO integration : Flask-SocketIO


 Python   Flask    WebSocket    Socket.IO  



Introduction


We will use Flask-SocketIO on Flask (Server-side) and Socket.IO JS on client to establish WebSocket between them.




Environment

Python 3.6.5
Flask 1.0.2
Flask-SocketIO 3.1.2
Socket.IO 2.2.0


Implement (Server-side)


Install Flask-SocketIO, eventlet

$ pip install flask-socketio
$ pip install eventlet

PS. eventlet supports good performance long-polling and WebSocket transports.

Receive message

Lets create two server-side event handlers:
1.  connected_event
2.  broadcast_event

The first event will be emitted and send a message to the original client when the WebSocket connection between server and the client is established.
The second event is used for broadcasting message to all connected clients with setting broadcast=True.

Inside the two above events, they will both emits the client-side’s server_response event with some data (messages).


main.py

from flask import Flask, request
from flask_socketio import SocketIO, emit

app = Flask(__name__)

# Websocket setting
app.config['SECRET_KEY'] = 'XXXXX'
socketio = SocketIO(app)

@socketio.on('connected_event')
def connected(msg):
    emit('server_response', {'data': msg['data']})

@socketio.on('broadcast_event')
def broadcast(msg):
    emit('server_response', {'data': msg['data']}, broadcast=True)

if __name__=='__main__':
    socketio.run(app)



Create a web page
.
Now we are going to create index.html in Flask to interact with the server-side’s events.
The project files’ hierarchy is as following,



We will implement the template: index.html, later.
Now, update main.py to create a new api: index, which will render the template and return the HTML.

main.py

from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
import json

app = Flask(__name__, template_folder='./templates')

# Websocket setting

app.config['SECRET_KEY'] = 'XXXX'
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html', async_mode=socketio.async_mode)


# … Skip connected/broadcast SocketIO events



(Optional) Create a web api to emit client-side event

We can implement an web api to emit the event of client-side as following codes.

@app.route('/send', methods=['POST'])
def send():
    jsonobj_content = request.json
    socketio.emit('server_response',  {'data':str(jsonobj_content)}, broadcast=True)
    return '', 200


Notice that use socket.emit() instead of emit() inside a Flask api.

(Request sample in Postman)



Implement (Client-side)


The following steps will be implemented on index.html.


Install/Embed jquery, Socket.IO

For npm users,

$ npm install jquery
$ npm install socket.io-client


I will just use CDN in my sample codes

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.6/socket.io.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />


Create connection

$(document).ready(function () {
            var socket = io.connect(); //Or given server url: io.connect("http://xxxx:123")
});



Register event handlers on “connect”


The callback will emits the “connected_event” on server-side

$(document).ready(function () {
            var socket = io.connect(); //Or given server url: io.connect("http://xxxx:123")

            socket.on('connect', function () { //The default event: connect, when a connection is fired
                console.log('Start connect!');
                socket.emit('connected_event', { data: 'I\'m connected!' });
            });
 });



Register event handlers on “server_response”

This handler will receive the message: msg, from server-side and display it.

$(document).ready(function () {
            var socket = io.connect(); //Or given server url: io.connect("http://xxxx:123")

            // … skip connect event handler

socket.on('server_response', function (msg) {
                $('#log').append('<br>' + $('<div/>').text('Received #' + ': ' + msg.data).html());
            });
});

And HTML:

<pre id="log"></pre>



Broadcast message to all connected clients

Implement a Textbox and button to send the message to other clients by emit “broadcast_event”.

$(document).ready(function () {
            var socket = io.connect(); //Or given server url: io.connect("http://xxxx:123")

            // … skip connect event handler
            // … skip server_resposne event handler
  
            $('#emitMsg').on('click', function () {
                var msg = $('#msg').val();
                socket.emit('broadcast_event', { data: msg });
            });           
});


<input type="text"  id="msg" />
<button id="emitMsg">Send Message to all</button>


Demo





To stop the Flask-SocketIO

@app.route('/shutdown', methods=['POST'])
def shutdown():
    socketio.stop()






Sample code






Reference






2017年5月23日 星期二

[JS] Unit testing with Karma and Jasmine

 karma    Jasmine    gulp  


Introduction


This is a simple sample for javascript unit-testing with karma, Jasmine and gulp.

karma :
A simple tool that allows you to execute JavaScript code in multiple real browsers.

A Behavior Driven Development testing framework for JavaScript.

Adapter for the Jasmine testing framework.



Sample codes




Implement


Install packages

karma
gulp


Create karma.conf.js

We can use the following command to create the karma.conf.js step by step, for more information, take a look at Karma : Configuration

$> karma init karma.conf.js

In this example, my test folders are as following,




So here is my karma.conf.js

Notice I used jquery in my javascript files, so the jquery library must be included in the files configuration.

module.exports = function (config) {
    config.set({
        browsers: ['Chrome'],
        frameworks: ['jasmine'],

        basePath: '',
        files: [
            // dependencies
            '../lib/jquery/dist/jquery.min.js',

            //Target js
            '../scripts/*.js',     

            //Test files
            'spec/*.spec.js',
        ],
        autoWatch: false, //Watching files and executing the tests if the files changes.
        singleRun: true //If true, Karma will run tests and then exit.
    });
};


Make a sample for testing

Assume that we would like to test the following html and script.

HTML

<div class="row">
    <div class="col-md-2">
        <input type="text" class="form-control" id="numA" />
    </div>
    <div class="col-md-2">
        <input type="text" class="form-control" id="numB" />
    </div>
    <div class="col-md-3">
        <input type="button" class="form-control" id="add" value="Add" />
    </div>
    <div class="col-md-3">
        <input type="button" class="form-control" id="minus" value="Minus" />
    </div>
    <div class="col-md-2">
        Result : <label id="result"></label>
    </div>
</div>



JS

'use strict';

$(function () {
    $('#add').click(function () {
        a = $('#numA').val();
        b = $('#numB').val();
        $('#result').text(add(+a, +b));
    });

    $('#minus').click(function () {
        a = $('#numA').val();
        b = $('#numB').val();
        $('#result').text(minus(+a, +b));
    });
})


function add(a, b) {
    return a + b;
}

function minus(a, b) {
    return a - b;
}





Where I put the html and js file to.




Create jasmine tests

We are going to write our first unit tests on functions, add and minus, to ensure that they works correctly.

First, add a new test file, *.spec.js




demo.index.spec.js

describe('Demo: function test', function () {

    it('should return 6 for 1 + 5', function () {
        expect(add(1, 5)).toEqual(6);
    });

    it('should return 2 for 6 - 4', function () {
        expect(minus(6, 4)).toEqual(2);
    });
});


For more Jasmine tutorials, go to jasmine.github.io.


Use gulp to start the test

Open gulpfile.js, add the following scripts to enable running test with gulp.

var gulp = require('gulp');
var server = require('karma').Server;

gulp.task('test', function () {
    new server({
        configFile: __dirname + '/wwwroot/test/karma.conf.js',
        singleRun: true
    }).start();
});


Then start the test by the following command,

$> gulp test

Succeed result:




Or fail result:



Handling HTML fixtures


Install packages

Preprocessor for converting HTML files into JS strings.

A plugin for the Karma test runner that loads .html and .json fixtures.



Update karma.conf.js

module.exports = function (config) {
    config.set({
        browsers: ['Chrome'],
        frameworks: ['jasmine','fixture'],

        basePath: '',
        files: [
            // dependencies
            '../lib/jquery/dist/jquery.min.js',

            //Target js
            '../scripts/*.js',     

            //Test files
            'spec/*.spec.js',

            //Inject html
            'html/*.html'
        ],
        preprocessors: {
            'html/*.html': ['html2js']
        },
        autoWatch: false,
        singleRun: true
    });
};


Refactor javascript

Before we create Jasmine test, we have to know how the preprocessor works.

This preprocessor converts HTML files into JS strings and publishes them in the global window.__html__, so that you can use these for testing DOM operations.

If we would like to simulate the click event on the Add or Minusbutton in the tests, we need to re-add event listener on the DOM.

So let’s refactor the javascript and create an event handler to isolate the handler codes from event listener.


JS

'use strict';
$(function () {
    $('#add').click(function () {
        eventHandler(handler.add);
    });

    $('#minus').click(function () {
        eventHandler(handler.minus);
    });

})

let handler = {
    get add() {
        return "add";
    },
    get minus() {
        return "minus";
    }
};

function eventHandler(handler) {
    let a = $('#numA').val();
    let b = $('#numB').val();
    let rslt = 0;
    switch (handler) {
        case 'add':
            rslt = add(+a, +b);
            break;
        case 'minus':
            rslt = minus(+a, +b);
            break;
        default:
            break;
    }
    $('#result').text(rslt);
};
//Skip...




Create Jasmine tests

describe('Demo: Html test', function () {

    // inject the HTML fixture for the tests
    beforeEach(function () {
        fixture.base = 'html';
        fixture.load('demo.index.html');

        //Register events
        document.getElementById('add').addEventListener('click', function () { eventHandler(handler.add); });
        document.getElementById('minus').addEventListener('click', function () { eventHandler(handler.minus); });
    });

    // remove the html fixture from the DOM
    afterEach(function () {
        fixture.cleanup();
    });

    it('should return 10 for 4 + 6 on html', function () {
        document.getElementById('numA').value = 4;
        document.getElementById('numB').value = 6;

        document.getElementById('add').click();

        let actual = document.getElementById('result').innerHTML;
        expect(+actual).toEqual(10);
    });

    it('should return 2 for 6 - 4 on html', function () {
        document.getElementById('numA').value = 6;
        document.getElementById('numB').value = 4;

        document.getElementById('minus').click();

        let actual = document.getElementById('result').innerHTML;
        expect(+actual).toEqual(2);
    });

});


Run test again …





Reference