<commit_before>/* @license
 *
 * Device firmware update with Web Bluetooth
 * Version: 0.0.2
 *
 * Protocol from:
 * http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk52.v0.9.2/bledfu_transport.html?cp=4_0_2_4_2_4
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Rob Moran
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * 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 NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS 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.
 */

// https://github.com/umdjs/umd
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['es6-promise', 'bleat', 'crc16'], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS
        module.exports = factory(Promise, require('bleat').webbluetooth, require('./crc16'));
    } else {
        // Browser globals with support for web workers (root is window)
        root.dfu = factory(Promise, root.navigator.bluetooth, root.crc16);
    }
}(this, function(Promise, bluetooth, crc16) {
    "use strict";

    var LITTLE_ENDIAN = true;

    var packetSize = 20;
    var notifySteps = 40;

    var serviceUUID = "00001530-1212-efde-1523-785feabcd123";
    var controlUUID = "00001531-1212-efde-1523-785feabcd123";
    var packetUUID = "00001532-1212-efde-1523-785feabcd123";
    var versionUUID = "00001534-1212-efde-1523-785feabcd123";

    var ImageType = {
        None: 0,
        SoftDevice: 1,
        Bootloader: 2,
        SoftDevice_Bootloader: 3, // Will not work right now
        Application: 4
    };

    // TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples.
    var initPacket = {
        device_type: 0xFFFF,
        device_rev: 0xFFFF,
        app_version: 0xFFFFFFFF,
        softdevice_len: 0x0001,
        softdevice: 0xFFFE,
        crc: 0x0000
    };

    var OPCODE = {
        RESERVED: 0,
        START_DFU: 1,
        INITIALIZE_DFU_PARAMETERS: 2,
        RECEIVE_FIRMWARE_IMAGE: 3,
        VALIDATE_FIRMWARE: 4,
        ACTIVATE_IMAGE_AND_RESET: 5,
        RESET_SYSTEM: 6,
        REPORT_RECEIVED_IMAGE_SIZE: 7,
        PACKET_RECEIPT_NOTIFICATION_REQUEST: 8,
        RESPONSE_CODE: 16,
        PACKET_RECEIPT_NOTIFICATION: 17
    };

    var loggers = [];
    function addLogger(loggerFn) {
        if (typeof loggerFn === "function") {
            loggers.push(loggerFn);
        }
    }
    function log(message) {
        loggers.forEach(function(logger) {
            logger(message);
        });
    }

    function findDevice(filters) {
        return bluetooth.requestDevice({
            filters: [ filters ],
            optionalServices: [serviceUUID]
        });
    }

    /**
     * Switch to bootloader/DFU mode by writing to the control point of the DFU Service.
     * The DFU Controller is not responsible for disconnecting from the application (DFU Target) after the write.
     * The application (DFU Target) will issue a GAP Disconnect and reset into bootloader/DFU mode.
     *
     * https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v11.0.0/bledfu_appswitching.html?cp=4_0_0_4_1_3_2_2
     */
    function writeMode(device) {
        return new Promise(function(resolve, reject) {

            device.addEventListener("gattserverdisconnected", function() {
                log("DFU target issued GAP disconnect and reset into bootloader/DFU mode");
                resolve(device);
            });

            var characteristics = null;

            connect(device)
            .then(function(chars) {
                log("enabling notifications");
                characteristics = chars;
                return characteristics.controlChar.startNotifications();
            })
            .then(function() {
                log("writing modeData");
                return characteristics.controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, ImageType.Application]));
            })
            .then(function() {
                log("modeData written");
            })
            .catch(function(error) {
                error = "writeMode error: " + error;
                log(error);
                reject(error);
            });
        });
    }

    /**
     * Contains basic functionality for performing safety checks on software updates for nRF5 based devices.
     * Init packet used for pre-checking to ensure the following image is compatible with the device.
     * Contains information on device type, revision, and supported SoftDevices along with a CRC or hash of firmware image.
     *
     * Not used in mbed bootloader (init packet was optional in SDK v6.x).
     */
    function generateInitPacket() {
        var buffer = new ArrayBuffer(14);
        var view = new DataView(buffer);
        view.setUint16(0, initPacket.device_type, LITTLE_ENDIAN);
        view.setUint16(2, initPacket.device_rev, LITTLE_ENDIAN);
        view.setUint32(4, initPacket.app_version, LITTLE_ENDIAN); // Application version for the image software. This field allows for additional checking, for example ensuring that a downgrade is not allowed.
        view.setUint16(8, initPacket.softdevice_len, LITTLE_ENDIAN); // Number of different SoftDevice revisions compatible with this application.
        view.setUint16(10, initPacket.softdevice, LITTLE_ENDIAN); // Variable length array of SoftDevices compatible with this application. The length of the array is specified in the length (softdevice_len) field. 0xFFFE indicates any SoftDevice.
        view.setUint16(12, initPacket.crc, LITTLE_ENDIAN);
        return view;
    }

    function provision(device, arrayBuffer, imageType, crc) {
        return new Promise(function(resolve, reject) {
            imageType = imageType || ImageType.Application;
            initPacket.crc = crc || crc16(arrayBuffer);
            var versionChar = null;

            connect(device)
            .then(function(chars) {
                versionChar = chars.versionChar;
                // Older DFU implementations (from older Nordic SDKs < 7.0) have no DFU Version characteristic.
                if (versionChar) {
                    return versionChar.readValue()
                    .then(function(data) {
                        console.log('read versionChar');
                        var major = data.getUint8(0);
                        var minor = data.getUint8(1);
                        return transfer(chars, arrayBuffer, imageType, major, minor);
                    });
                } else {
                    // Default to version 6.0 (mbed).
                    return transfer(chars, arrayBuffer, imageType, 6, 0);
                }
            })
            .then(function() {
                resolve();
            })
            .catch(function(error) {
                log(error);
                reject(error);
            });
        });
    }

    function connect(device) {
        return new Promise(function(resolve, reject) {
            var server = null;
            var service = null;
            var controlChar = null;
            var packetChar = null;
            var versionChar = null;

            function complete() {
                resolve({
                    server: server,
                    controlChar: controlChar,
                    packetChar: packetChar,
                    versionChar: versionChar
                });
            }

            device.gatt.connect()
            .then(function(gattServer) {
                log("connected to device");
                server = gattServer;
                return server.getPrimaryService(serviceUUID);
            })
            .then(function(primaryService) {
                log("found DFU service");
                service = primaryService;
                return service.getCharacteristic(controlUUID);
            })
            .then(function(characteristic) {
                log("found control characteristic");
                controlChar = characteristic;
                return service.getCharacteristic(packetUUID);
            })
            .then(function(characteristic) {
                log("found packet characteristic");
                packetChar = characteristic;
                service.getCharacteristic(versionUUID)
                // Older DFU implementations (from older Nordic SDKs) have no DFU Version characteristic. So this may fail.
                .then(function(characteristic) {
                    log("found version characteristic");
                    versionChar = characteristic;
                    complete();
                })
                .catch(function(error) {
                    log("info: no version characteristic found");
                    complete();
                });
            })
            .catch(function(error) {
                error = "connect error: " + error;
                log(error);
                reject(error);
            });
        });
    }

    var interval;
    var offset;
    function transfer(chars, arrayBuffer, imageType, majorVersion, minorVersion) {
        return new Promise(function(resolve, reject) {
            var server = chars.server;
            var controlChar = chars.controlChar;
            var packetChar = chars.packetChar;
            log('using dfu version ' + majorVersion + "." + minorVersion);

            // Set up receipts
            interval = Math.floor(arrayBuffer.byteLength / (packetSize * notifySteps));
            offset = 0;

            if (!controlChar.properties.notify) {
                var err = "controlChar missing notify property";
                log(err);
                return reject(err);
            }

            log("enabling notifications");
            controlChar.startNotifications()
            .then(function() {
                controlChar.addEventListener('characteristicvaluechanged', handleControl);
                log("sending imagetype: " + imageType);
                return controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, imageType]));
            })
            .then(function() {
                log("sent start");

                var softLength = (imageType === ImageType.SoftDevice) ? arrayBuffer.byteLength : 0;
                var bootLength = (imageType === ImageType.Bootloader) ? arrayBuffer.byteLength : 0;
                var appLength = (imageType === ImageType.Application) ? arrayBuffer.byteLength : 0;

                var buffer = new ArrayBuffer(12);
                var view = new DataView(buffer);
                view.setUint32(0, softLength, LITTLE_ENDIAN);
                view.setUint32(4, bootLength, LITTLE_ENDIAN);
                view.setUint32(8, appLength, LITTLE_ENDIAN);

                return packetChar.writeValue(view);
            })
            .then(function() {
                log("sent image size: " + arrayBuffer.byteLength);
            })
            .catch(function(error) {
                error = "start error: " + error;
                log(error);
                reject(error);
            });

            function handleControl(event) {
                var view = event.target.value;

                var opCode = view.getUint8(0);
                var req_opcode = view.getUint8(1);
                var resp_code = view.getUint8(2);

                if (opCode === OPCODE.RESPONSE_CODE) {
                    if (resp_code !== 1) {
                        var err = "error from control point notification, resp_code: " + resp_code;
                        log(err);
                        return reject(err);
                    }

                    switch(req_opcode) {
                        case OPCODE.START_DFU:
                        case OPCODE.INITIALIZE_DFU_PARAMETERS:
                            if(req_opcode === OPCODE.START_DFU && majorVersion > 6) { // init packet is not used in SDK v6 (so not used in mbed).
                                log('write init packet');
                                controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 0]))
                                .then(function() {
                                    return packetChar.writeValue(generateInitPacket());
                                })
                                .then(function() {
                                    return controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 1]));
                                })
                                .catch(function(error) {
                                    error = "error writing dfu init parameters: " + error;
                                    log(error);
                                    reject(error);
                                });
                                break;
                            }

                            log('send packet count');

                            var buffer = new ArrayBuffer(3);
                            view = new DataView(buffer);
                            view.setUint8(0, OPCODE.PACKET_RECEIPT_NOTIFICATION_REQUEST);
                            view.setUint16(1, interval, LITTLE_ENDIAN);

                            controlChar.writeValue(view)
                            .then(function() {
                                log("sent packet count: " + interval);
                                return controlChar.writeValue(new Uint8Array([OPCODE.RECEIVE_FIRMWARE_IMAGE]));
                            })
                            .then(function() {
                                log("sent receive");
                                return writePacket(packetChar, arrayBuffer, 0);
                            })
                            .catch(function(error) {
                                error = "error sending packet count: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.RECEIVE_FIRMWARE_IMAGE:
                            log('check length');

                            controlChar.writeValue(new Uint8Array([OPCODE.REPORT_RECEIVED_IMAGE_SIZE]))
                            .catch(function(error) {
                                error = "error checking length: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.REPORT_RECEIVED_IMAGE_SIZE:
                            var bytesReceived = view.getUint32(3, LITTLE_ENDIAN);
                            log('length: ' + bytesReceived);
                            log('validate...');

                            controlChar.writeValue(new Uint8Array([OPCODE.VALIDATE_FIRMWARE]))
                            .catch(function(error) {
                                error = "error validating: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.VALIDATE_FIRMWARE:
                            log('complete, reset...');

                            server.device.addEventListener("gattserverdisconnected", function() {
                                log('disconnected and completed the DFU transfer');
                                resolve();
                            });

                            controlChar.writeValue(new Uint8Array([OPCODE.ACTIVATE_IMAGE_AND_RESET]))
                            .then(function() {
                                log('image activated and dfu target reset');
                            })
                            .catch(function(error) {
                                error = "error resetting: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        default:
                            log('unexpected req opCode - ERROR');
                            break;
                    }

                } else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) {
                    var bytes = view.getUint32(1, LITTLE_ENDIAN);
                    log('transferred: ' + bytes);
                    writePacket(packetChar, arrayBuffer, 0);
                }
            }
        });
    }

    function writePacket(packetChar, arrayBuffer, count) {
        var size = (offset + packetSize > arrayBuffer.byteLength) ? arrayBuffer.byteLength - offset : packetSize;
        var packet = arrayBuffer.slice(offset, offset + size);
        var view = new Uint8Array(packet);

        packetChar.writeValue(view)
        .then(function() {
            count ++;
            offset += packetSize;
            if (count < interval && offset < arrayBuffer.byteLength) {
                writePacket(packetChar, arrayBuffer, count);
            }
        })
        .catch(function(error) {
            error = "writePacket error: " + error;
            log(error);
        });
    }

    return {
        addLogger: addLogger,
        ImageType: ImageType,
        findDevice: findDevice,
        writeMode: writeMode,
        provision: provision
    };
}));
<commit_msg>Add timeouts so entire DFU process works.<commit_after>/* @license
 *
 * Device firmware update with Web Bluetooth
 * Version: 0.0.2
 *
 * Protocol from:
 * http://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk52.v0.9.2/bledfu_transport.html?cp=4_0_2_4_2_4
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Rob Moran
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * 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 NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS 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.
 */

// https://github.com/umdjs/umd
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['es6-promise', 'bleat', 'crc16'], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS
        module.exports = factory(Promise, require('bleat').webbluetooth, require('./crc16'));
    } else {
        // Browser globals with support for web workers (root is window)
        root.dfu = factory(Promise, root.navigator.bluetooth, root.crc16);
    }
}(this, function(Promise, bluetooth, crc16) {
    "use strict";

    var LITTLE_ENDIAN = true;

    var packetSize = 20;
    var notifySteps = 40;

    var serviceUUID = "00001530-1212-efde-1523-785feabcd123";
    var controlUUID = "00001531-1212-efde-1523-785feabcd123";
    var packetUUID = "00001532-1212-efde-1523-785feabcd123";
    var versionUUID = "00001534-1212-efde-1523-785feabcd123";

    var ImageType = {
        None: 0,
        SoftDevice: 1,
        Bootloader: 2,
        SoftDevice_Bootloader: 3, // Will not work right now
        Application: 4
    };

    // TODO: This should be configurable by the user. For now this will work with any of Nordic's SDK examples.
    var initPacket = {
        device_type: 0xFFFF,
        device_rev: 0xFFFF,
        app_version: 0xFFFFFFFF,
        softdevice_len: 0x0001,
        softdevice: 0xFFFE,
        crc: 0x0000
    };

    var OPCODE = {
        RESERVED: 0,
        START_DFU: 1,
        INITIALIZE_DFU_PARAMETERS: 2,
        RECEIVE_FIRMWARE_IMAGE: 3,
        VALIDATE_FIRMWARE: 4,
        ACTIVATE_IMAGE_AND_RESET: 5,
        RESET_SYSTEM: 6,
        REPORT_RECEIVED_IMAGE_SIZE: 7,
        PACKET_RECEIPT_NOTIFICATION_REQUEST: 8,
        RESPONSE_CODE: 16,
        PACKET_RECEIPT_NOTIFICATION: 17
    };

    var loggers = [];
    function addLogger(loggerFn) {
        if (typeof loggerFn === "function") {
            loggers.push(loggerFn);
        }
    }
    function log(message) {
        loggers.forEach(function(logger) {
            logger(message);
        });
    }

    function findDevice(filters) {
        return bluetooth.requestDevice({
            filters: [ filters ],
            optionalServices: [serviceUUID]
        });
    }

    /**
     * Switch to bootloader/DFU mode by writing to the control point of the DFU Service.
     * The DFU Controller is not responsible for disconnecting from the application (DFU Target) after the write.
     * The application (DFU Target) will issue a GAP Disconnect and reset into bootloader/DFU mode.
     *
     * https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v11.0.0/bledfu_appswitching.html?cp=4_0_0_4_1_3_2_2
     */
    function writeMode(device) {
        return new Promise(function(resolve, reject) {

            device.addEventListener("gattserverdisconnected", function() {
                log("DFU target issued GAP disconnect and reset into bootloader/DFU mode");
                resolve(device);
            });

            var characteristics = null;

            connect(device)
            .then(function(chars) {
                log("enabling notifications");
                characteristics = chars;
                return characteristics.controlChar.startNotifications();
            })
            .then(function() {
                log("writing modeData");
                return characteristics.controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, ImageType.Application]));
            })
            .then(function() {
                log("modeData written");
                setTimeout(function() {  // TODO: Remove this when gattserverdisconnected event is implemented and possibly put a timeout in that event handler before resolving.
                    resolve(device);
                }, 2000);
            })
            .catch(function(error) {
                error = "writeMode error: " + error;
                log(error);
                reject(error);
            });
        });
    }

    /**
     * Contains basic functionality for performing safety checks on software updates for nRF5 based devices.
     * Init packet used for pre-checking to ensure the following image is compatible with the device.
     * Contains information on device type, revision, and supported SoftDevices along with a CRC or hash of firmware image.
     *
     * Not used in mbed bootloader (init packet was optional in SDK v6.x).
     */
    function generateInitPacket() {
        var buffer = new ArrayBuffer(14);
        var view = new DataView(buffer);
        view.setUint16(0, initPacket.device_type, LITTLE_ENDIAN);
        view.setUint16(2, initPacket.device_rev, LITTLE_ENDIAN);
        view.setUint32(4, initPacket.app_version, LITTLE_ENDIAN); // Application version for the image software. This field allows for additional checking, for example ensuring that a downgrade is not allowed.
        view.setUint16(8, initPacket.softdevice_len, LITTLE_ENDIAN); // Number of different SoftDevice revisions compatible with this application.
        view.setUint16(10, initPacket.softdevice, LITTLE_ENDIAN); // Variable length array of SoftDevices compatible with this application. The length of the array is specified in the length (softdevice_len) field. 0xFFFE indicates any SoftDevice.
        view.setUint16(12, initPacket.crc, LITTLE_ENDIAN);
        return view;
    }

    function provision(device, arrayBuffer, imageType, crc) {
        return new Promise(function(resolve, reject) {
            imageType = imageType || ImageType.Application;
            initPacket.crc = crc || crc16(arrayBuffer);
            var versionChar = null;

            connect(device)
            .then(function(chars) {
                versionChar = chars.versionChar;
                // Older DFU implementations (from older Nordic SDKs < 7.0) have no DFU Version characteristic.
                if (versionChar) {
                    return versionChar.readValue()
                    .then(function(data) {
                        console.log('read versionChar');
                        var major = data.getUint8(0);
                        var minor = data.getUint8(1);
                        return transfer(chars, arrayBuffer, imageType, major, minor);
                    });
                } else {
                    // Default to version 6.0 (mbed).
                    return transfer(chars, arrayBuffer, imageType, 6, 0);
                }
            })
            .then(function() {
                resolve();
            })
            .catch(function(error) {
                log(error);
                reject(error);
            });
        });
    }

    function connect(device) {
        return new Promise(function(resolve, reject) {
            var server = null;
            var service = null;
            var controlChar = null;
            var packetChar = null;
            var versionChar = null;

            function complete() {
                resolve({
                    server: server,
                    controlChar: controlChar,
                    packetChar: packetChar,
                    versionChar: versionChar
                });
            }

            device.gatt.connect()
            .then(function(gattServer) {
                log("connected to device");
                return new Promise(function(resolve) {
                    server = gattServer;
                    setTimeout(resolve, 2000); // This delay is needed because BlueZ needs time to update it's cache.
                });
            })
            .then(function() {
                return server.getPrimaryService(serviceUUID);
            })
            .then(function(primaryService) {
                log("found DFU service");
                service = primaryService;
                return service.getCharacteristic(controlUUID);
            })
            .then(function(characteristic) {
                log("found control characteristic");
                controlChar = characteristic;
                return service.getCharacteristic(packetUUID);
            })
            .then(function(characteristic) {
                log("found packet characteristic");
                packetChar = characteristic;
                service.getCharacteristic(versionUUID)
                // Older DFU implementations (from older Nordic SDKs) have no DFU Version characteristic. So this may fail.
                .then(function(characteristic) {
                    log("found version characteristic");
                    versionChar = characteristic;
                    complete();
                })
                .catch(function(error) {
                    log("info: no version characteristic found");
                    complete();
                });
            })
            .catch(function(error) {
                error = "connect error: " + error;
                log(error);
                reject(error);
            });
        });
    }

    var interval;
    var offset;
    function transfer(chars, arrayBuffer, imageType, majorVersion, minorVersion) {
        return new Promise(function(resolve, reject) {
            var server = chars.server;
            var controlChar = chars.controlChar;
            var packetChar = chars.packetChar;
            log('using dfu version ' + majorVersion + "." + minorVersion);

            // Set up receipts
            interval = Math.floor(arrayBuffer.byteLength / (packetSize * notifySteps));
            offset = 0;

            if (!controlChar.properties.notify) {
                var err = "controlChar missing notify property";
                log(err);
                return reject(err);
            }

            log("enabling notifications");
            controlChar.startNotifications()
            .then(function() {
                controlChar.addEventListener('characteristicvaluechanged', handleControl);
                log("sending imagetype: " + imageType);
                return controlChar.writeValue(new Uint8Array([OPCODE.START_DFU, imageType]));
            })
            .then(function() {
                log("sent start");

                var softLength = (imageType === ImageType.SoftDevice) ? arrayBuffer.byteLength : 0;
                var bootLength = (imageType === ImageType.Bootloader) ? arrayBuffer.byteLength : 0;
                var appLength = (imageType === ImageType.Application) ? arrayBuffer.byteLength : 0;

                var buffer = new ArrayBuffer(12);
                var view = new DataView(buffer);
                view.setUint32(0, softLength, LITTLE_ENDIAN);
                view.setUint32(4, bootLength, LITTLE_ENDIAN);
                view.setUint32(8, appLength, LITTLE_ENDIAN);

                return packetChar.writeValue(view);
            })
            .then(function() {
                log("sent image size: " + arrayBuffer.byteLength);
            })
            .catch(function(error) {
                error = "start error: " + error;
                log(error);
                reject(error);
            });

            function handleControl(event) {
                var view = event.target.value;

                var opCode = view.getUint8(0);
                var req_opcode = view.getUint8(1);
                var resp_code = view.getUint8(2);

                if (opCode === OPCODE.RESPONSE_CODE) {
                    if (resp_code !== 1) {
                        var err = "error from control point notification, resp_code: " + resp_code;
                        log(err);
                        return reject(err);
                    }

                    switch(req_opcode) {
                        case OPCODE.START_DFU:
                        case OPCODE.INITIALIZE_DFU_PARAMETERS:
                            if(req_opcode === OPCODE.START_DFU && majorVersion > 6) { // init packet is not used in SDK v6 (so not used in mbed).
                                log('write init packet');
                                controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 0]))
                                .then(function() {
                                    return packetChar.writeValue(generateInitPacket());
                                })
                                .then(function() {
                                    return controlChar.writeValue(new Uint8Array([OPCODE.INITIALIZE_DFU_PARAMETERS, 1]));
                                })
                                .catch(function(error) {
                                    error = "error writing dfu init parameters: " + error;
                                    log(error);
                                    reject(error);
                                });
                                break;
                            }

                            log('send packet count');

                            var buffer = new ArrayBuffer(3);
                            view = new DataView(buffer);
                            view.setUint8(0, OPCODE.PACKET_RECEIPT_NOTIFICATION_REQUEST);
                            view.setUint16(1, interval, LITTLE_ENDIAN);

                            controlChar.writeValue(view)
                            .then(function() {
                                log("sent packet count: " + interval);
                                return controlChar.writeValue(new Uint8Array([OPCODE.RECEIVE_FIRMWARE_IMAGE]));
                            })
                            .then(function() {
                                log("sent receive");
                                return writePacket(packetChar, arrayBuffer, 0);
                            })
                            .catch(function(error) {
                                error = "error sending packet count: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.RECEIVE_FIRMWARE_IMAGE:
                            log('check length');

                            controlChar.writeValue(new Uint8Array([OPCODE.REPORT_RECEIVED_IMAGE_SIZE]))
                            .catch(function(error) {
                                error = "error checking length: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.REPORT_RECEIVED_IMAGE_SIZE:
                            var bytesReceived = view.getUint32(3, LITTLE_ENDIAN);
                            log('length: ' + bytesReceived);
                            log('validate...');

                            controlChar.writeValue(new Uint8Array([OPCODE.VALIDATE_FIRMWARE]))
                            .catch(function(error) {
                                error = "error validating: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        case OPCODE.VALIDATE_FIRMWARE:
                            log('complete, reset...');

                            server.device.addEventListener("gattserverdisconnected", function() {
                                log('disconnected and completed the DFU transfer');
                                resolve();
                            });

                            controlChar.writeValue(new Uint8Array([OPCODE.ACTIVATE_IMAGE_AND_RESET]))
                            .then(function() {
                                log('image activated and dfu target reset');
                            })
                            .catch(function(error) {
                                error = "error resetting: " + error;
                                log(error);
                                reject(error);
                            });
                            break;
                        default:
                            log('unexpected req opCode - ERROR');
                            break;
                    }

                } else if (opCode === OPCODE.PACKET_RECEIPT_NOTIFICATION) {
                    var bytes = view.getUint32(1, LITTLE_ENDIAN);
                    log('transferred: ' + bytes);
                    writePacket(packetChar, arrayBuffer, 0);
                }
            }
        });
    }

    function writePacket(packetChar, arrayBuffer, count) {
        var size = (offset + packetSize > arrayBuffer.byteLength) ? arrayBuffer.byteLength - offset : packetSize;
        var packet = arrayBuffer.slice(offset, offset + size);
        var view = new Uint8Array(packet);

        packetChar.writeValue(view)
        .then(function() {
            count ++;
            offset += packetSize;
            if (count < interval && offset < arrayBuffer.byteLength) {
                writePacket(packetChar, arrayBuffer, count);
            }
        })
        .catch(function(error) {
            error = "writePacket error: " + error;
            log(error);
        });
    }

    return {
        addLogger: addLogger,
        ImageType: ImageType,
        findDevice: findDevice,
        writeMode: writeMode,
        provision: provision
    };
}));
<|endoftext|>


<commit_before>function Shape(id) {
    this.id = id;
    this.paper_shape = undefined;

    this.fill_color = undefined;
    this.stroke_color = undefined;
    this.stroke_width = undefined;

    this._configJSON = function() {
        var fill_color_json = ColorsAdapter.paperColorToHex(this.fill_color);
        var stroke_color_json = ColorsAdapter.paperColorToHex(this.stroke_color);
        return {
            'shape_id': this.id,
            'fill_color': fill_color_json.hex_color,
            'fill_alpha': fill_color_json.alpha,
            'stroke_color': stroke_color_json.hex_color,
            'stroke_alpha': stroke_color_json.alpha,
            'stroke_width': this.stroke_width
        }
    };

    this.getCenter = function() {
        if (typeof this.paper_shape !== 'undefined') {
            var bbox = this.paper_shape.bounds;
            return {
                'x': bbox.center.x,
                'y': bbox.center.y
            }
        } else {
            console.info('Shape not initialized');
        }
    };

    this.configure = function(shape_config) {
        if (typeof this.paper_shape !== 'undefined') {
            this.fill_color = ColorsAdapter.hexToPaperColor(shape_config.fill_color, shape_config.fill_alpha);
            this.paper_shape.setFillColor(this.fill_color);
            this.stroke_color = ColorsAdapter.hexToPaperColor(shape_config.stroke_color, shape_config.stroke_alpha);
            this.paper_shape.setStrokeColor(this.stroke_color);
            this.stroke_width = shape_config.stroke_width;
            this.paper_shape.setStrokeWidth(this.stroke_width);
        }
    };

    this.enableEvents = function(events_list) {
        if (typeof events_list === 'undefined') {
            for (var ev in this.default_events) {
                this.paper_shape[this.default_events[ev]] = true;
            }
        } else {
            for (var ev in events_list) {
                if (this.default_events.indexOf(events_list[ev]) !== -1) {
                    this.paper_shape[events_list[ev]] = true;
                } else {
                    console.warn('Unknown event ' + events_list[ev]);
                }
            }
        }
    };

    this.disableEvents = function(events_list) {
        if (typeof events_list === 'undefined') {
            for (var ev in this.default_events) {
                this.paper_shape[this.default_events[ev]] = false;
            }
        } else {
            for (var ev in events_list) {
                if (this.default_events.indexOf(events_list[ev]) !== -1) {
                    this.paper_shape[events_list[ev]] = false;
                } else {
                    console.warn('Unknown event ' + events_list[ev]);
                }
            }
        }
    };

    this._buildEvents = function() {
        this.paper_shape.on({
            mousedrag: function(event) {
                if (this[Shape.MOUSE_DRAG_EVENT] === true) {
                    document.body.style.cursor = 'move';
                    this.position = new paper.Point(
                        this.position.x + event.delta.x,
                        this.position.y + event.delta.y
                    );
                }
            },
            mouseup:  function(event) {
                document.body.style.cursor = 'auto';
            }
        });
    };

    this.initializeEvents = function(activate_events) {
        var activate = (typeof activate_events === 'undefined') ? false : activate_events;
        this._buildEvents();
        if (activate) {
            this.enableEvents();
        } else {
            this.disableEvents();
        }
    };

    this.select = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setSelected(true);
        }
    };

    this.deselect = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setSelected(false);
        }
    };

    this.isSelected = function() {
        if (typeof this.paper_shape !== 'undefined') {
            return this.paper_shape.selected;
        } else {
            return undefined;
        }
    };

    this.show = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setVisible(true);
        }
    };

    this.hide = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setVisible(false);
        }
    };

    this.delete = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.remove();
        }
    };
}

Shape.MOUSE_DRAG_EVENT = 'mouse_drag_event';
Shape.prototype.default_events = [
    Shape.MOUSE_DRAG_EVENT
];

function Rectangle(id, origin_x, origin_y, width, height) {
    Shape.call(this, id);

    this.origin_x = origin_x;
    this.origin_y = origin_y;
    this.width = width;
    this.height = height;

    this.toPaperShape = function(activate_events) {
        var rect = new paper.Shape.Rectangle({
            point: [this.origin_x, this.origin_y],
            size: [this.width, this.height]
        });
        this.paper_shape = rect;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'origin_x': this.origin_x + x_offset,
                'origin_y': this.origin_y + y_offset,
                'width': this.width,
                'height': this.height
            });
        return shape_json;
    };
}

Rectangle.prototype = new Shape();

function Ellipse(id, center_x, center_y, radius_x, radius_y) {
    Shape.call(this, id);

    this.center_x = center_x;
    this.center_y = center_y;
    this.radius_x = radius_x;
    this.radius_y = radius_y;

    this.toPaperShape = function(activate_events) {
        var ellipse = new paper.Shape.Ellipse({
            center: [this.center_x, this.center_y],
            radius: [this.radius_x, this.radius_y]
        });
        this.paper_shape = ellipse;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'center_x': this.center_x + x_offset,
                'center_y': this.center_y + y_offset,
                'radius_x': this.radius_x,
                'radius_y': this.radius_y
            });
        return shape_json;
    };
}

Ellipse.prototype = new Shape();

function Circle(id, center_x, center_y, radius) {
    Shape.call(this, id);

    this.center_x = center_x;
    this.center_y = center_y;
    this.radius = radius;

    this.toPaperShape = function(activate_events) {
        var circle = new paper.Shape.Circle({
            center: [this.center_x, this.center_y],
            radius: this.radius
        });
        this.paper_shape = circle;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'center_x': this.center_x + x_offset,
                'center_y': this.center_y + y_offset,
                'radius': this.radius
            });
        return shape_json;
    };
}

Circle.prototype = new Shape();

function Line(id, from_x, from_y, to_x, to_y) {
    Shape.call(this, id);

    this.from_x = from_x;
    this.from_y = from_y;
    this.to_x = to_x;
    this.to_y = to_y;

    this.toPaperShape = function(activate_events) {
        var line = new paper.Path.Line({
            from: [this.from_x, this.from_y],
            to: [this.to_x, this.to_y]
        });
        this.paper_shape = line;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'from_x': this.from_x + x_offset,
                'from_y': this.from_y + y_offset,
                'to_x': this.to_x + x_offset,
                'to_y': this.to_y + y_offset
            });
        return shape_json;
    };
}

Line.prototype = new Shape();<commit_msg>added type to JSON export of shapes<commit_after>function Shape(id) {
    this.id = id;
    this.paper_shape = undefined;

    this.fill_color = undefined;
    this.stroke_color = undefined;
    this.stroke_width = undefined;

    this._configJSON = function() {
        var fill_color_json = ColorsAdapter.paperColorToHex(this.fill_color);
        var stroke_color_json = ColorsAdapter.paperColorToHex(this.stroke_color);
        return {
            'shape_id': this.id,
            'fill_color': fill_color_json.hex_color,
            'fill_alpha': fill_color_json.alpha,
            'stroke_color': stroke_color_json.hex_color,
            'stroke_alpha': stroke_color_json.alpha,
            'stroke_width': this.stroke_width
        }
    };

    this.getCenter = function() {
        if (typeof this.paper_shape !== 'undefined') {
            var bbox = this.paper_shape.bounds;
            return {
                'x': bbox.center.x,
                'y': bbox.center.y
            }
        } else {
            console.info('Shape not initialized');
        }
    };

    this.configure = function(shape_config) {
        if (typeof this.paper_shape !== 'undefined') {
            this.fill_color = ColorsAdapter.hexToPaperColor(shape_config.fill_color, shape_config.fill_alpha);
            this.paper_shape.setFillColor(this.fill_color);
            this.stroke_color = ColorsAdapter.hexToPaperColor(shape_config.stroke_color, shape_config.stroke_alpha);
            this.paper_shape.setStrokeColor(this.stroke_color);
            this.stroke_width = shape_config.stroke_width;
            this.paper_shape.setStrokeWidth(this.stroke_width);
        }
    };

    this.enableEvents = function(events_list) {
        if (typeof events_list === 'undefined') {
            for (var ev in this.default_events) {
                this.paper_shape[this.default_events[ev]] = true;
            }
        } else {
            for (var ev in events_list) {
                if (this.default_events.indexOf(events_list[ev]) !== -1) {
                    this.paper_shape[events_list[ev]] = true;
                } else {
                    console.warn('Unknown event ' + events_list[ev]);
                }
            }
        }
    };

    this.disableEvents = function(events_list) {
        if (typeof events_list === 'undefined') {
            for (var ev in this.default_events) {
                this.paper_shape[this.default_events[ev]] = false;
            }
        } else {
            for (var ev in events_list) {
                if (this.default_events.indexOf(events_list[ev]) !== -1) {
                    this.paper_shape[events_list[ev]] = false;
                } else {
                    console.warn('Unknown event ' + events_list[ev]);
                }
            }
        }
    };

    this._buildEvents = function() {
        this.paper_shape.on({
            mousedrag: function(event) {
                if (this[Shape.MOUSE_DRAG_EVENT] === true) {
                    document.body.style.cursor = 'move';
                    this.position = new paper.Point(
                        this.position.x + event.delta.x,
                        this.position.y + event.delta.y
                    );
                }
            },
            mouseup:  function(event) {
                document.body.style.cursor = 'auto';
            }
        });
    };

    this.initializeEvents = function(activate_events) {
        var activate = (typeof activate_events === 'undefined') ? false : activate_events;
        this._buildEvents();
        if (activate) {
            this.enableEvents();
        } else {
            this.disableEvents();
        }
    };

    this.select = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setSelected(true);
        }
    };

    this.deselect = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setSelected(false);
        }
    };

    this.isSelected = function() {
        if (typeof this.paper_shape !== 'undefined') {
            return this.paper_shape.selected;
        } else {
            return undefined;
        }
    };

    this.show = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setVisible(true);
        }
    };

    this.hide = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.setVisible(false);
        }
    };

    this.delete = function() {
        if (typeof this.paper_shape !== 'undefined') {
            this.paper_shape.remove();
        }
    };
}

Shape.MOUSE_DRAG_EVENT = 'mouse_drag_event';
Shape.prototype.default_events = [
    Shape.MOUSE_DRAG_EVENT
];

function Rectangle(id, origin_x, origin_y, width, height) {
    Shape.call(this, id);

    this.origin_x = origin_x;
    this.origin_y = origin_y;
    this.width = width;
    this.height = height;

    this.toPaperShape = function(activate_events) {
        var rect = new paper.Shape.Rectangle({
            point: [this.origin_x, this.origin_y],
            size: [this.width, this.height]
        });
        this.paper_shape = rect;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'origin_x': this.origin_x + x_offset,
                'origin_y': this.origin_y + y_offset,
                'width': this.width,
                'height': this.height,
                'type': 'rectangle'
            });
        return shape_json;
    };
}

Rectangle.prototype = new Shape();

function Ellipse(id, center_x, center_y, radius_x, radius_y) {
    Shape.call(this, id);

    this.center_x = center_x;
    this.center_y = center_y;
    this.radius_x = radius_x;
    this.radius_y = radius_y;

    this.toPaperShape = function(activate_events) {
        var ellipse = new paper.Shape.Ellipse({
            center: [this.center_x, this.center_y],
            radius: [this.radius_x, this.radius_y]
        });
        this.paper_shape = ellipse;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'center_x': this.center_x + x_offset,
                'center_y': this.center_y + y_offset,
                'radius_x': this.radius_x,
                'radius_y': this.radius_y,
                'type': 'ellipse'
            });
        return shape_json;
    };
}

Ellipse.prototype = new Shape();

function Circle(id, center_x, center_y, radius) {
    Shape.call(this, id);

    this.center_x = center_x;
    this.center_y = center_y;
    this.radius = radius;

    this.toPaperShape = function(activate_events) {
        var circle = new paper.Shape.Circle({
            center: [this.center_x, this.center_y],
            radius: this.radius
        });
        this.paper_shape = circle;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'center_x': this.center_x + x_offset,
                'center_y': this.center_y + y_offset,
                'radius': this.radius,
                'type': 'circle'
            });
        return shape_json;
    };
}

Circle.prototype = new Shape();

function Line(id, from_x, from_y, to_x, to_y) {
    Shape.call(this, id);

    this.from_x = from_x;
    this.from_y = from_y;
    this.to_x = to_x;
    this.to_y = to_y;

    this.toPaperShape = function(activate_events) {
        var line = new paper.Path.Line({
            from: [this.from_x, this.from_y],
            to: [this.to_x, this.to_y]
        });
        this.paper_shape = line;
        this.initializeEvents(activate_events);
    };

    this.toJSON = function(x_offset, y_offset) {
        var shape_json = this._configJSON();
        $.extend(
            shape_json,
            {
                'from_x': this.from_x + x_offset,
                'from_y': this.from_y + y_offset,
                'to_x': this.to_x + x_offset,
                'to_y': this.to_y + y_offset,
                'type': 'line'
            });
        return shape_json;
    };
}

Line.prototype = new Shape();<|endoftext|>


<commit_before>/*!
 * kind-of <https://github.com/jonschlinkert/kind-of>
 *
 * Copyright (c) 2014-2015, Jon Schlinkert.
 * Licensed under the MIT License
 */

'use strict';

var should = require('should');
var kindOf = require('./');

describe('kindOf', function () {
  describe('nulls', function () {
    it('should work for undefined', function () {
      kindOf(undefined).should.equal('undefined');
    });

    it('should work for null', function () {
      kindOf(null).should.equal('null');
    });
  });

  describe('primitives', function () {
    it('should work for booleans', function () {
      kindOf(true).should.equal('boolean');
      kindOf(false).should.equal('boolean');
      kindOf(new Boolean(true)).should.equal('boolean');
    });

    it('should work for numbers', function () {
      kindOf(42).should.equal('number');
      kindOf(new Number(42)).should.equal('number');
    });

    it('should work for strings', function () {
      kindOf("string").should.equal('string');
    });
  });

  describe('objects', function () {
    it('should work for arguments', function () {
      (function () {
        kindOf(arguments).should.equal('arguments');
        return;
      })();
    });

    it('should work for objects', function () {
      function Test() {}
      kindOf({}).should.equal('object');
      kindOf(new Test()).should.equal('object');
    });

    it('should work for dates', function () {
      kindOf(new Date()).should.equal('date');
    });

    it('should work for arrays', function () {
      kindOf([]).should.equal('array');
      kindOf([1, 2, 3]).should.equal('array');
      kindOf(new Array()).should.equal('array');
    });

    it('should work for regular expressions', function () {
      kindOf(/[\s\S]+/).should.equal('regexp');
      kindOf(new RegExp('^' + 'foo$')).should.equal('regexp');
    });

    it('should work for functions', function () {
      kindOf(function () {}).should.equal('function');
      kindOf(new Function()).should.equal('function');
    });
  });
});<commit_msg>adds tests for `buffer`<commit_after>/*!
 * kind-of <https://github.com/jonschlinkert/kind-of>
 *
 * Copyright (c) 2014-2015, Jon Schlinkert.
 * Licensed under the MIT License
 */

'use strict';

var should = require('should');
var kindOf = require('./');

describe('kindOf', function () {
  describe('nulls', function () {
    it('should work for undefined', function () {
      kindOf(undefined).should.equal('undefined');
    });

    it('should work for null', function () {
      kindOf(null).should.equal('null');
    });
  });

  describe('primitives', function () {
    it('should work for booleans', function () {
      kindOf(true).should.equal('boolean');
      kindOf(false).should.equal('boolean');
      kindOf(new Boolean(true)).should.equal('boolean');
    });

    it('should work for numbers', function () {
      kindOf(42).should.equal('number');
      kindOf(new Number(42)).should.equal('number');
    });

    it('should work for strings', function () {
      kindOf("string").should.equal('string');
    });
  });

  describe('objects', function () {
    it('should work for arguments', function () {
      (function () {
        kindOf(arguments).should.equal('arguments');
        return;
      })();
    });

    it('should work for buffers', function () {
      kindOf(new Buffer('')).should.equal('buffer');
    });

    it('should work for objects', function () {
      function Test() {}
      kindOf({}).should.equal('object');
      kindOf(new Test()).should.equal('object');
    });

    it('should work for dates', function () {
      kindOf(new Date()).should.equal('date');
    });

    it('should work for arrays', function () {
      kindOf([]).should.equal('array');
      kindOf([1, 2, 3]).should.equal('array');
      kindOf(new Array()).should.equal('array');
    });

    it('should work for regular expressions', function () {
      kindOf(/[\s\S]+/).should.equal('regexp');
      kindOf(new RegExp('^' + 'foo$')).should.equal('regexp');
    });

    it('should work for functions', function () {
      kindOf(function () {}).should.equal('function');
      kindOf(new Function()).should.equal('function');
    });
  });
});<|endoftext|>


<commit_before>const playwright = require('playwright');
const webpack = require('webpack');

const CI = Boolean(process.env.CI);

let build = `material-ui local ${new Date().toISOString()}`;

if (process.env.CIRCLE_BUILD_URL) {
  build = process.env.CIRCLE_BUILD_URL;
}

const browserStack = {
  username: process.env.BROWSERSTACK_USERNAME,
  accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
  build,
};

process.env.CHROME_BIN = playwright.chromium.executablePath();

// BrowserStack rate limit after 1600 calls every 5 minutes.
// Per second, https://www.browserstack.com/docs/automate/api-reference/selenium/introduction#rest-api-projects
const MAX_REQUEST_PER_SECOND_BROWSERSTACK = 1600 / (60 * 5);
// Estimate the max number of concurrent karma builds
// For each PR, 6 concurrent builds are used, only one is usng BrowserStack.
const AVERAGE_KARMA_BUILD = 1 / 6;
// CircleCI accepts up to 83 concurrent builds.
const MAX_CIRCLE_CI_CONCURRENCY = 83;

// Karma configuration
module.exports = function setKarmaConfig(config) {
  const baseConfig = {
    basePath: '../',
    browsers: ['chromeHeadless'],
    browserDisconnectTimeout: 120000, // default 2000
    browserDisconnectTolerance: 1, // default 0
    browserNoActivityTimeout: 6 * 60 * 1000, // default 10000
    colors: true,
    client: {
      mocha: {
        // Some BrowserStack browsers can be slow.
        timeout: (process.env.CIRCLECI === 'true' ? 4 : 2) * 1000,
      },
    },
    frameworks: ['mocha'],
    files: [
      {
        pattern: 'test/karma.tests.js',
        watched: true,
        served: true,
        included: true,
      },
      {
        pattern: 'test/assets/*.png',
        watched: false,
        included: false,
        served: true,
      },
    ],
    plugins: ['karma-mocha', 'karma-chrome-launcher', 'karma-sourcemap-loader', 'karma-webpack'],
    /**
     * possible values:
     * - config.LOG_DISABLE
     * - config.LOG_ERROR
     * - config.LOG_WARN
     * - config.LOG_INFO
     * - config.LOG_DEBUG
     */
    logLevel: config.LOG_INFO,
    port: 9876,
    preprocessors: {
      'test/karma.tests.js': ['webpack', 'sourcemap'],
    },
    proxies: {
      '/fake.png': '/base/test/assets/fake.png',
      '/fake2.png': '/base/test/assets/fake2.png',
    },
    reporters: ['dots'],
    webpack: {
      mode: 'development',
      devtool: CI ? 'inline-source-map' : 'eval-source-map',
      plugins: [
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify('test'),
          'process.env.CI': JSON.stringify(process.env.CI),
          'process.env.KARMA': JSON.stringify(true),
          'process.env.TEST_GATE': JSON.stringify(process.env.TEST_GATE),
        }),
      ],
      module: {
        rules: [
          {
            test: /\.(js|ts|tsx)$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            options: {
              envName: 'stable',
            },
          },
        ],
      },
      node: {
        // Some tests import fs
        fs: 'empty',
      },
      resolve: {
        extensions: ['.js', '.ts', '.tsx'],
      },
    },
    webpackMiddleware: {
      noInfo: true,
      writeToDisk: CI,
    },
    customLaunchers: {
      chromeHeadless: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox'],
      },
    },
    singleRun: CI,
  };

  let newConfig = baseConfig;

  if (browserStack.accessKey) {
    newConfig = {
      ...baseConfig,
      browserStack,
      browsers: baseConfig.browsers.concat(['chrome', 'firefox', 'safari', 'edge']),
      plugins: baseConfig.plugins.concat(['karma-browserstack-launcher']),
      customLaunchers: {
        ...baseConfig.customLaunchers,
        chrome: {
          base: 'BrowserStack',
          os: 'OS X',
          os_version: 'Catalina',
          browser: 'chrome',
          browser_version: '84.0',
        },
        firefox: {
          base: 'BrowserStack',
          os: 'Windows',
          os_version: '10',
          browser: 'firefox',
          browser_version: '78.0',
        },
        safari: {
          base: 'BrowserStack',
          os: 'OS X',
          os_version: 'Catalina',
          browser: 'safari',
          // We support 12.2 on iOS.
          // However, 12.1 is very flaky on desktop (mobile is always flaky).
          browser_version: '13.0',
        },
        edge: {
          base: 'BrowserStack',
          os: 'Windows',
          os_version: '10',
          browser: 'edge',
          browser_version: '85.0',
        },
      },
    };

    // -1 because chrome headless runs in the local machine
    const browserstackBrowsersUsed = newConfig.browsers.length - 1;

    // default 1000, Avoid Rate Limit Exceeded
    newConfig.pollingTimeout =
      ((MAX_CIRCLE_CI_CONCURRENCY * AVERAGE_KARMA_BUILD * browserstackBrowsersUsed) /
        MAX_REQUEST_PER_SECOND_BROWSERSTACK) *
      1000;
  }

  config.set(newConfig);
};
<commit_msg>[test] Improve BrowserStack configuration (#25049)<commit_after>const playwright = require('playwright');
const webpack = require('webpack');

const CI = Boolean(process.env.CI);

let build = `material-ui local ${new Date().toISOString()}`;

if (process.env.CIRCLE_BUILD_URL) {
  build = process.env.CIRCLE_BUILD_URL;
}

const browserStack = {
  username: process.env.BROWSERSTACK_USERNAME,
  accessKey: process.env.BROWSERSTACK_ACCESS_KEY,
  build,
  // https://github.com/browserstack/api#timeout300
  timeout: 5.5 * 60, // Maximum time before a worker is terminated. Default 5 minutes.
};

process.env.CHROME_BIN = playwright.chromium.executablePath();

// BrowserStack rate limit after 1600 calls every 5 minutes.
// Per second, https://www.browserstack.com/docs/automate/api-reference/selenium/introduction#rest-api-projects
const MAX_REQUEST_PER_SECOND_BROWSERSTACK = 1600 / (60 * 5);
// Estimate the max number of concurrent karma builds
// For each PR, 6 concurrent builds are used, only one is usng BrowserStack.
const AVERAGE_KARMA_BUILD = 1 / 6;
// CircleCI accepts up to 83 concurrent builds.
const MAX_CIRCLE_CI_CONCURRENCY = 83;

// Karma configuration
module.exports = function setKarmaConfig(config) {
  const baseConfig = {
    basePath: '../',
    browsers: ['chromeHeadless'],
    browserDisconnectTimeout: 3 * 60 * 1000, // default 2000
    browserDisconnectTolerance: 1, // default 0
    browserNoActivityTimeout: 3 * 60 * 1000, // default 30000
    colors: true,
    client: {
      mocha: {
        // Some BrowserStack browsers can be slow.
        timeout: (process.env.CIRCLECI === 'true' ? 4 : 2) * 1000,
      },
    },
    frameworks: ['mocha'],
    files: [
      {
        pattern: 'test/karma.tests.js',
        watched: true,
        served: true,
        included: true,
      },
      {
        pattern: 'test/assets/*.png',
        watched: false,
        included: false,
        served: true,
      },
    ],
    plugins: ['karma-mocha', 'karma-chrome-launcher', 'karma-sourcemap-loader', 'karma-webpack'],
    /**
     * possible values:
     * - config.LOG_DISABLE
     * - config.LOG_ERROR
     * - config.LOG_WARN
     * - config.LOG_INFO
     * - config.LOG_DEBUG
     */
    logLevel: config.LOG_INFO,
    port: 9876,
    preprocessors: {
      'test/karma.tests.js': ['webpack', 'sourcemap'],
    },
    proxies: {
      '/fake.png': '/base/test/assets/fake.png',
      '/fake2.png': '/base/test/assets/fake2.png',
    },
    reporters: ['dots'],
    webpack: {
      mode: 'development',
      devtool: CI ? 'inline-source-map' : 'eval-source-map',
      plugins: [
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify('test'),
          'process.env.CI': JSON.stringify(process.env.CI),
          'process.env.KARMA': JSON.stringify(true),
          'process.env.TEST_GATE': JSON.stringify(process.env.TEST_GATE),
        }),
      ],
      module: {
        rules: [
          {
            test: /\.(js|ts|tsx)$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
            options: {
              envName: 'stable',
            },
          },
        ],
      },
      node: {
        // Some tests import fs
        fs: 'empty',
      },
      resolve: {
        extensions: ['.js', '.ts', '.tsx'],
      },
    },
    webpackMiddleware: {
      noInfo: true,
      writeToDisk: CI,
    },
    customLaunchers: {
      chromeHeadless: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox'],
      },
    },
    singleRun: CI,
  };

  let newConfig = baseConfig;

  if (browserStack.accessKey) {
    newConfig = {
      ...baseConfig,
      browserStack,
      browsers: baseConfig.browsers.concat(['chrome', 'firefox', 'safari', 'edge']),
      plugins: baseConfig.plugins.concat(['karma-browserstack-launcher']),
      customLaunchers: {
        ...baseConfig.customLaunchers,
        chrome: {
          base: 'BrowserStack',
          os: 'OS X',
          os_version: 'Catalina',
          browser: 'chrome',
          browser_version: '84.0',
        },
        firefox: {
          base: 'BrowserStack',
          os: 'Windows',
          os_version: '10',
          browser: 'firefox',
          browser_version: '78.0',
        },
        safari: {
          base: 'BrowserStack',
          os: 'OS X',
          os_version: 'Catalina',
          browser: 'safari',
          // We support 12.2 on iOS.
          // However, 12.1 is very flaky on desktop (mobile is always flaky).
          browser_version: '13.0',
        },
        edge: {
          base: 'BrowserStack',
          os: 'Windows',
          os_version: '10',
          browser: 'edge',
          browser_version: '85.0',
        },
      },
    };

    // -1 because chrome headless runs in the local machine
    const browserstackBrowsersUsed = newConfig.browsers.length - 1;

    // default 1000, Avoid Rate Limit Exceeded
    newConfig.browserStack.pollingTimeout =
      ((MAX_CIRCLE_CI_CONCURRENCY * AVERAGE_KARMA_BUILD * browserstackBrowsersUsed) /
        MAX_REQUEST_PER_SECOND_BROWSERSTACK) *
      1000;
  }

  config.set(newConfig);
};
<|endoftext|>


<commit_before>import test from 'ava';

const devices = require( './index' );

test( 'deviceList()', t => {
  t.is( typeof devices.deviceList, 'function' );
  t.ok( Array.isArray( devices.deviceList() ) );
} );

test( 'brandList()', t => {
  t.is( typeof devices.brandList, 'function' );
  t.ok( Array.isArray( devices.brandList() ) );
} );

test( 'getDevicesByBrand()', t => {
  t.is( typeof devices.getDevicesByBrand, 'function' );
  t.throws( function () { devices.getDevicesByBrand(); } );
  t.ok( Array.isArray( devices.getDevicesByBrand( '' ) ) );
} );

test( 'getDevicesByName()', t => {
  t.is( typeof devices.getDevicesByName, 'function' );
  t.throws( function () { devices.getDevicesByName(); } );
  t.ok( Array.isArray( devices.getDevicesByName( '' ) ) );
} );

test( 'getDevicesByDeviceId()', t => {
  t.is( typeof devices.getDevicesByDeviceId, 'function' );
  t.throws( function () { devices.getDevicesByDeviceId(); } );
  t.ok( Array.isArray( devices.getDevicesByDeviceId( '' ) ) );
} );

test( 'getDevicesByModel()', t => {
  t.is( typeof devices.getDevicesByModel, 'function' );
  t.throws( function () { devices.getDevicesByModel(); } );
  t.ok( Array.isArray( devices.getDevicesByModel( '' ) ) );
} );
<commit_msg>ok() is deprecated<commit_after>import test from 'ava';

const devices = require( './index' );

test( 'deviceList()', t => {
  t.is( typeof devices.deviceList, 'function' );
  t.truthy( Array.isArray( devices.deviceList() ) );
} );

test( 'brandList()', t => {
  t.is( typeof devices.brandList, 'function' );
  t.truthy( Array.isArray( devices.brandList() ) );
} );

test( 'getDevicesByBrand()', t => {
  t.is( typeof devices.getDevicesByBrand, 'function' );
  t.throws( function () { devices.getDevicesByBrand(); } );
  t.truthy( Array.isArray( devices.getDevicesByBrand( '' ) ) );
} );

test( 'getDevicesByName()', t => {
  t.is( typeof devices.getDevicesByName, 'function' );
  t.throws( function () { devices.getDevicesByName(); } );
  t.truthy( Array.isArray( devices.getDevicesByName( '' ) ) );
} );

test( 'getDevicesByDeviceId()', t => {
  t.is( typeof devices.getDevicesByDeviceId, 'function' );
  t.throws( function () { devices.getDevicesByDeviceId(); } );
  t.truthy( Array.isArray( devices.getDevicesByDeviceId( '' ) ) );
} );

test( 'getDevicesByModel()', t => {
  t.is( typeof devices.getDevicesByModel, 'function' );
  t.throws( function () { devices.getDevicesByModel(); } );
  t.truthy( Array.isArray( devices.getDevicesByModel( '' ) ) );
} );
<|endoftext|>


<commit_before>'use strict';

/* global _, $, moment, PNotify */
var CartUtility = function() {
    this.spinTasksCount = 0;
    this.pnotifyStack = {'dir1': 'up', 'dir2': 'right', 'push': 'bottom', 'spacing1': 0, 'spacing2': 0};
};

CartUtility.prototype.post = function($http, $q, url, data, onSuccess, onError) {
    var self = this;
    var deferred = $q.defer();

    var options = {
        method: "POST",
        url: url,
        headers: {"Content-type": "application/x-www-form-urlencoded"}
    };
    if (_.isObject(data) && !_.isEmpty(data)) {
        options.data = $.param(data);
    }

    $http(options)
    .success(function(data) {
        if (self.handleResponse(data)) {
            deferred.resolve(onSuccess(data));
        } else {
            deferred.reject();
        }
    })
    .error(function(data) {
        if (_.isFunction(onError)) {
            onError(data);
        }
        deferred.reject();
    });

    return self.spinShow(deferred.promise);
};

CartUtility.prototype.spinShow = function(promise) {
    var self = this;
    self.spinTasksCount++;
    $('.spinner-mask').show();
    if (_.isObject(promise) && promise.hasOwnProperty('finally')) {
        // this is a promise
        promise['finally'](function() {
            self.spinHide();
        });
    } else {
        setTimeout(function() {
            self.spinHide();
        }, 300);
    }
    return promise;
};

CartUtility.prototype.spinHide = function() {
    this.spinTasksCount--;
    if (this.spinTasksCount > 0) {
        return; // still spin tasks left
    }
    $('.spinner-mask').hide();
};

CartUtility.prototype.notify = function(title, text, type, duration) {
    // type: "notice", "info", "success", or "error"
    // duration: milliseconds
    if (_.isUndefined(title)) {
        title = 'title';
    }
    if (_.isUndefined(text)) {
        text = '';
    }
    if (_.isUndefined(type) || _.isEmpty(type) || _.isNull(type)) {
        type = 'info';
    }
    if (_.isUndefined(duration) || _.isEmpty(duration) || _.isNull(duration)) {
        duration = 3000; // 3s
    }

    var options = {
        title: title,
        text: text,
        type: type,
        delay: duration,
        addclass: 'stack-bar-bottom',
        width: '70%',
        stack: this.pnotifyStack
    };
    if (type === 'error') {
        options.hide = false;
    }

    new PNotify(options);
};

CartUtility.prototype.handleResponse = function(result, notifySuccess, notifyError) {
    if (!_.isBoolean(notifySuccess)) {
        notifySuccess = false; // default not display the succeed message
    }
    if (!_.isBoolean(notifyError)) {
        notifyError = true; // default display the failure message
    }

    if (_.isObject(result) && result.hasOwnProperty('valid') && result.valid === false) {
        // request failed with normal framework exception
        if (notifyError) { // show failure msg box
            this.notify('REQ Failed!', result.message.error, 'error');
        }
        return false;
    } else if (!_.isObject(result) || (_.isObject(result) && !result.hasOwnProperty('valid'))) {
        // request failed without normal framework exception
        if (notifyError) { // show failure msg box
            this.notify('REQ Failed!');
        }
        return false;
    } else {
        // request done
        if (notifySuccess) { // show success msg box, and hide automatically
            this.notify('REQ Succeed!', JSON.stringify(result.message));
        }
        return true;
    }
};

CartUtility.prototype.buildToc = function(html, baseUrl) {
    if (_.isUndefined(baseUrl) || _.isNull(baseUrl)) {
        baseUrl = ''; // shall be 'http://...'
    }
    var rootNodeName = 'root';
    /**
     * [
     *     { // index of this node is 5
     *         "level": 3,
     *         "text": "headerTextForLv3",
     *         "anchor": "CartUtility.escapeAnchorName(headerTextForLv3)"
     *         "parent": 4, // index of node in this array, only root parent is string 'root'
     *         "html": $('text')
     *     },
     *     ...
     * ]
     */
    var headers = [];

    if (_.isUndefined(html) || _.isEmpty(html) || _.isNull(html)) {
        return '';
    }

    // find all headers, and collect basic info
    var headerInfos = $(html).filter(':header');
    if (headerInfos.length <= 0) {
        return ''; // no headers found
    }
    _.forEach(headerInfos, function(header) {
        // build basic headers info
        var hElement = $(header);
        var headerLevel = parseInt(hElement.prop("tagName").substring(1));
        var headerText = hElement.text();
        var anchorName = hElement.find('a').attr('name');
        headers.push({
            "level": headerLevel,
            "text": headerText,
            "anchor": anchorName,
            "parent": null,
            "html": null
        });
    });
    var headersCloned = _.clone(headers);
    // loop to build html code & find parent levels
    _.forEach(headers, function(header, index) {
        header.html = $('
' + header.text + '
');
        if (index === 0) {
            header.parent = rootNodeName;
        } else {
            var reversedBeforeHeaders = headersCloned.slice(0, index).reverse();
            _.forEach(reversedBeforeHeaders, function(bfHeader, bfIndex) {
                if (header.parent !== null) {
                    // means parent already determined, skip this loop
                    return;
                }
                if (bfHeader.level === header.level) {
                    // same level, shall share the same parent node
                    header.parent = bfHeader.parent;
                } else if (bfHeader.level  header.level) {
                    // current level is lower than target, dismiss it
                }
            });
            if (header.parent === null) {
                // parent not found, root node
                header.parent = rootNodeName;
            }
        }
    });

    // build final html codes
    var root = $('

');
    // build non root nodes first, and have to reverse headers first
    // since we need to handle sub headers from the bottom to top
    _.forEach(headers, function(header) {
        if (header.parent === rootNodeName) {
            return; // dismiss root nodes, since the content in it has not been built yet
        }
        var parentNode = headers[header.parent].html;
        if (parentNode.find('ul').length <= 0) {
            // parent li element does not contain the ul element, add it
            parentNode.append('

');
        }
        $(parentNode.find('ul')[0]).append(header.html);
    });
    // build root nodes
    _.forEach(headers, function(header) {
        if (header.parent === rootNodeName) {
            root.append(header.html);
        }
    });

    return '
' + root.html() + '
'; // since html() only returned the innerHtml of the element
};

CartUtility.prototype.toggleToc = function(icon, toc) { /* all params are jquery elements: $('#id') */
    if (toc.html() !== '') {
        toc.slideToggle('fast', function() {
            icon.toggleClass('glyphicon-plus').toggleClass('glyphicon-minus');
        });
    }
};

CartUtility.prototype.escapeAnchorName = function(anchorName) {
    return encodeURIComponent(anchorName);
};

CartUtility.prototype.getPureRootUrlFromLocation = function($location) {
    return $location.protocol() + '://' + $location.host() + '/';
};

CartUtility.prototype.getPureAbsUrlFromLocation = function($location) {
    return $location.protocol() + '://' + $location.host() + '/' + $location.path();
};

CartUtility.prototype.log = function(msg, facility) {
    if (_.isUndefined(msg) || _.isEmpty(msg) || _.isNull(msg)) {
        return;
    }
    if (_.isObject(console)) {
        if (_.isObject(msg)) {
            msg = JSON.stringify(msg);
        }
        msg = '[' + this.getTimeString() + ']' + ((_.isString(facility)) ? (' ' + facility) : '') + ' ' + msg;
        console.log(msg);
    }
};

CartUtility.prototype.isDndSupported = function() {
    var div = document.createElement('div');
    return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
};

CartUtility.prototype.generateMdHTMLHeader = function(post) {
    return '

' + JSON.stringify({
        "uuid": post.uuid,
        "title": post.title,
        "category": post.category,
        "tags": post.tags,
        "attachments": post.attachments
    }) + '

';
};

CartUtility.prototype.parseMdHTMLHeader = function(markdown) {
    return JSON.parse($(markdown).find('.mdHeader').text());
};

CartUtility.prototype.parseMdPureContent = function(markdown) {
    return markdown.substr(markdown.indexOf('

') + '

'.length);
};

CartUtility.prototype.focusEditor = function(editor) {
    editor.focus(); // focus the ace editor
    editor.resize(true); // resize the window

    var editorValues = editor.getSession().getValue();
    if (_.isEmpty(editorValues)) {
        return; // no content, no need to redirect to end of page
    }

    editorValues = editorValues.split("\n");
    var lineNo = editorValues.length; // count total number of lines
    var columnNo = editorValues[lineNo - 1].split('').length;
    editor.gotoLine(lineNo, columnNo); // go to end of document
};

CartUtility.prototype.getTime = function() {
    return moment().unix(); // return current timestamp
};

CartUtility.prototype.getTimeString = function(timestamp) {
    var instance = _.isUndefined(timestamp) ? moment() : this.parseUnixTime(timestamp);
    return instance.format('YYYY-MM-DD HH:mm:ss');
};

CartUtility.prototype.parseUnixTime = function(timestamp) {
    return moment.unix(timestamp); // return moment instance
};

CartUtility.prototype.parseUnixYear = function(timestamp, needFull) {
    if (_.isUndefined(needFull) || !_.isBoolean(needFull)) {
        needFull = false;
    }
    // return 'YYYY' or 'YYYY-01-01 00:00:00'
    return this.parseUnixTime(timestamp).format('YYYY') + (needFull ? '-01-01 00:00:00' : '');
};

CartUtility.prototype.parseUnixYearEndTimestamp = function(timestamp) {
    // return 'NEXT_YYYY-01-01 00:00:00'.timestamp - 1
    return moment((parseInt(this.parseUnixYear(timestamp)) + 1) + '-01-01 00:00:00').unix() - 1;
};

CartUtility.prototype.parseUnixDate = function(timestamp, needFull) {
    if (_.isUndefined(needFull) || !_.isBoolean(needFull)) {
        needFull = false;
    }
    // return 'YYYY-MM' or 'YYYY-MM-01 00:00:00'
    return this.parseUnixTime(timestamp).format('YYYY-MM') + (needFull ? '-01 00:00:00' : '');
};

CartUtility.prototype.parseUnixMonth = function(timestamp) {
    // return 'MM'
    return this.parseUnixTime(timestamp).format('MM');
};

CartUtility.prototype.parseUnixDateEndTimestamp = function(timestamp) {
    var year = parseInt(this.parseUnixYear(timestamp));
    var month = parseInt(this.parseUnixMonth(timestamp));
    if (month === 12) {
        year++;
        month = '01';
    } else {
        month++;
    }
    if ((month + '').length === 1) {
        month = '0' + month;
    }
    // month is 12 => return 'NEXT_YYYY-01-01 00:00:00'.timestamp - 1
    // month lower than 12 => return 'YYYY-NEXT_MM-01 00:00:00'.timestamp - 1
    return moment(year + '-' + month + '-01 00:00:00').unix() - 1;
};

module.exports = new CartUtility();<commit_msg>Fix default param validation.<commit_after>'use strict';

/* global _, $, moment, PNotify */
var CartUtility = function() {
    this.spinTasksCount = 0;
    this.pnotifyStack = {'dir1': 'up', 'dir2': 'right', 'push': 'bottom', 'spacing1': 0, 'spacing2': 0};
};

CartUtility.prototype.post = function($http, $q, url, data, onSuccess, onError) {
    var self = this;
    var deferred = $q.defer();

    var options = {
        method: "POST",
        url: url,
        headers: {"Content-type": "application/x-www-form-urlencoded"}
    };
    if (_.isObject(data) && !_.isEmpty(data)) {
        options.data = $.param(data);
    }

    $http(options)
    .success(function(data) {
        if (self.handleResponse(data)) {
            deferred.resolve(onSuccess(data));
        } else {
            deferred.reject();
        }
    })
    .error(function(data) {
        if (_.isFunction(onError)) {
            onError(data);
        }
        deferred.reject();
    });

    return self.spinShow(deferred.promise);
};

CartUtility.prototype.spinShow = function(promise) {
    var self = this;
    self.spinTasksCount++;
    $('.spinner-mask').show();
    if (_.isObject(promise) && promise.hasOwnProperty('finally')) {
        // this is a promise
        promise['finally'](function() {
            self.spinHide();
        });
    } else {
        setTimeout(function() {
            self.spinHide();
        }, 300);
    }
    return promise;
};

CartUtility.prototype.spinHide = function() {
    this.spinTasksCount--;
    if (this.spinTasksCount > 0) {
        return; // still spin tasks left
    }
    $('.spinner-mask').hide();
};

CartUtility.prototype.notify = function(title, text, type, duration) {
    // type: "notice", "info", "success", or "error"
    // duration: milliseconds
    if (_.isUndefined(title)) {
        title = 'title';
    }
    if (_.isUndefined(text)) {
        text = '';
    }
    if (_.isUndefined(type) || _.isEmpty(type) || _.isNull(type)) {
        type = 'info';
    }
    if (_.isUndefined(duration) || _.isEmpty(duration) || _.isNull(duration)) {
        duration = 3000; // 3s
    }

    var options = {
        title: title,
        text: text,
        type: type,
        delay: duration,
        addclass: 'stack-bar-bottom',
        width: '70%',
        stack: this.pnotifyStack
    };
    if (type === 'error') {
        options.hide = false;
    }

    new PNotify(options);
};

CartUtility.prototype.handleResponse = function(result, notifySuccess, notifyError) {
    if (!_.isBoolean(notifySuccess)) {
        notifySuccess = false; // default not display the succeed message
    }
    if (!_.isBoolean(notifyError)) {
        notifyError = true; // default display the failure message
    }

    if (_.isObject(result) && result.hasOwnProperty('valid') && result.valid === false) {
        // request failed with normal framework exception
        if (notifyError) { // show failure msg box
            this.notify('REQ Failed!', result.message.error, 'error');
        }
        return false;
    } else if (!_.isObject(result) || (_.isObject(result) && !result.hasOwnProperty('valid'))) {
        // request failed without normal framework exception
        if (notifyError) { // show failure msg box
            this.notify('REQ Failed!');
        }
        return false;
    } else {
        // request done
        if (notifySuccess) { // show success msg box, and hide automatically
            this.notify('REQ Succeed!', JSON.stringify(result.message));
        }
        return true;
    }
};

CartUtility.prototype.buildToc = function(html, baseUrl) {
    if (_.isUndefined(baseUrl) || _.isNull(baseUrl)) {
        baseUrl = ''; // shall be 'http://...'
    }
    var rootNodeName = 'root';
    /**
     * [
     *     { // index of this node is 5
     *         "level": 3,
     *         "text": "headerTextForLv3",
     *         "anchor": "CartUtility.escapeAnchorName(headerTextForLv3)"
     *         "parent": 4, // index of node in this array, only root parent is string 'root'
     *         "html": $('text')
     *     },
     *     ...
     * ]
     */
    var headers = [];

    if (_.isUndefined(html) || _.isEmpty(html) || _.isNull(html)) {
        return '';
    }

    // find all headers, and collect basic info
    var headerInfos = $(html).filter(':header');
    if (headerInfos.length <= 0) {
        return ''; // no headers found
    }
    _.forEach(headerInfos, function(header) {
        // build basic headers info
        var hElement = $(header);
        var headerLevel = parseInt(hElement.prop("tagName").substring(1));
        var headerText = hElement.text();
        var anchorName = hElement.find('a').attr('name');
        headers.push({
            "level": headerLevel,
            "text": headerText,
            "anchor": anchorName,
            "parent": null,
            "html": null
        });
    });
    var headersCloned = _.clone(headers);
    // loop to build html code & find parent levels
    _.forEach(headers, function(header, index) {
        header.html = $('
' + header.text + '
');
        if (index === 0) {
            header.parent = rootNodeName;
        } else {
            var reversedBeforeHeaders = headersCloned.slice(0, index).reverse();
            _.forEach(reversedBeforeHeaders, function(bfHeader, bfIndex) {
                if (header.parent !== null) {
                    // means parent already determined, skip this loop
                    return;
                }
                if (bfHeader.level === header.level) {
                    // same level, shall share the same parent node
                    header.parent = bfHeader.parent;
                } else if (bfHeader.level  header.level) {
                    // current level is lower than target, dismiss it
                }
            });
            if (header.parent === null) {
                // parent not found, root node
                header.parent = rootNodeName;
            }
        }
    });

    // build final html codes
    var root = $('

');
    // build non root nodes first, and have to reverse headers first
    // since we need to handle sub headers from the bottom to top
    _.forEach(headers, function(header) {
        if (header.parent === rootNodeName) {
            return; // dismiss root nodes, since the content in it has not been built yet
        }
        var parentNode = headers[header.parent].html;
        if (parentNode.find('ul').length <= 0) {
            // parent li element does not contain the ul element, add it
            parentNode.append('

');
        }
        $(parentNode.find('ul')[0]).append(header.html);
    });
    // build root nodes
    _.forEach(headers, function(header) {
        if (header.parent === rootNodeName) {
            root.append(header.html);
        }
    });

    return '
' + root.html() + '
'; // since html() only returned the innerHtml of the element
};

CartUtility.prototype.toggleToc = function(icon, toc) { /* all params are jquery elements: $('#id') */
    if (toc.html() !== '') {
        toc.slideToggle('fast', function() {
            icon.toggleClass('glyphicon-plus').toggleClass('glyphicon-minus');
        });
    }
};

CartUtility.prototype.escapeAnchorName = function(anchorName) {
    return encodeURIComponent(anchorName);
};

CartUtility.prototype.getPureRootUrlFromLocation = function($location) {
    return $location.protocol() + '://' + $location.host() + '/';
};

CartUtility.prototype.getPureAbsUrlFromLocation = function($location) {
    return $location.protocol() + '://' + $location.host() + '/' + $location.path();
};

CartUtility.prototype.log = function(msg, facility) {
    if (_.isUndefined(msg) || _.isEmpty(msg) || _.isNull(msg)) {
        return;
    }
    if (_.isObject(console)) {
        if (_.isObject(msg)) {
            msg = JSON.stringify(msg);
        }
        msg = '[' + this.getTimeString() + ']' + ((_.isString(facility)) ? (' ' + facility) : '') + ' ' + msg;
        console.log(msg);
    }
};

CartUtility.prototype.isDndSupported = function() {
    var div = document.createElement('div');
    return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
};

CartUtility.prototype.generateMdHTMLHeader = function(post) {
    return '

' + JSON.stringify({
        "uuid": post.uuid,
        "title": post.title,
        "category": post.category,
        "tags": post.tags,
        "attachments": post.attachments
    }) + '

';
};

CartUtility.prototype.parseMdHTMLHeader = function(markdown) {
    return JSON.parse($(markdown).find('.mdHeader').text());
};

CartUtility.prototype.parseMdPureContent = function(markdown) {
    return markdown.substr(markdown.indexOf('

') + '

'.length);
};

CartUtility.prototype.focusEditor = function(editor) {
    editor.focus(); // focus the ace editor
    editor.resize(true); // resize the window

    var editorValues = editor.getSession().getValue();
    if (_.isEmpty(editorValues)) {
        return; // no content, no need to redirect to end of page
    }

    editorValues = editorValues.split("\n");
    var lineNo = editorValues.length; // count total number of lines
    var columnNo = editorValues[lineNo - 1].split('').length;
    editor.gotoLine(lineNo, columnNo); // go to end of document
};

CartUtility.prototype.getTime = function() {
    return moment().unix(); // return current timestamp
};

CartUtility.prototype.getTimeString = function(timestamp) {
    var instance = _.isUndefined(timestamp) ? moment() : this.parseUnixTime(timestamp);
    return instance.format('YYYY-MM-DD HH:mm:ss');
};

CartUtility.prototype.parseUnixTime = function(timestamp) {
    return moment.unix(timestamp); // return moment instance
};

CartUtility.prototype.parseUnixYear = function(timestamp, needFull) {
    if (!_.isBoolean(needFull)) {
        needFull = false;
    }
    // return 'YYYY' or 'YYYY-01-01 00:00:00'
    return this.parseUnixTime(timestamp).format('YYYY') + (needFull ? '-01-01 00:00:00' : '');
};

CartUtility.prototype.parseUnixYearEndTimestamp = function(timestamp) {
    // return 'NEXT_YYYY-01-01 00:00:00'.timestamp - 1
    return moment((parseInt(this.parseUnixYear(timestamp)) + 1) + '-01-01 00:00:00').unix() - 1;
};

CartUtility.prototype.parseUnixDate = function(timestamp, needFull) {
    if (!_.isBoolean(needFull)) {
        needFull = false;
    }
    // return 'YYYY-MM' or 'YYYY-MM-01 00:00:00'
    return this.parseUnixTime(timestamp).format('YYYY-MM') + (needFull ? '-01 00:00:00' : '');
};

CartUtility.prototype.parseUnixMonth = function(timestamp) {
    // return 'MM'
    return this.parseUnixTime(timestamp).format('MM');
};

CartUtility.prototype.parseUnixDateEndTimestamp = function(timestamp) {
    var year = parseInt(this.parseUnixYear(timestamp));
    var month = parseInt(this.parseUnixMonth(timestamp));
    if (month === 12) {
        year++;
        month = '01';
    } else {
        month++;
    }
    if ((month + '').length === 1) {
        month = '0' + month;
    }
    // month is 12 => return 'NEXT_YYYY-01-01 00:00:00'.timestamp - 1
    // month lower than 12 => return 'YYYY-NEXT_MM-01 00:00:00'.timestamp - 1
    return moment(year + '-' + month + '-01 00:00:00').unix() - 1;
};

module.exports = new CartUtility();<|endoftext|>