<commit_before>var canvasScale = 8;
var calculatedCanvasEdgeSize = 50;
var pieceEdgeSize = 10;
var paddedWidth = pieceEdgeSize - 10;
var playerTurn = true;
var board;
var winner = {
    found: false,
    draw: false,
    piece: ""
};

function setup() {
	frameRate(60);

    calculatedCanvasEdgeSize = windowWidth / 16;
    if (windowHeight / 9 < calculatedCanvasEdgeSize) {
        calculatedCanvasEdgeSize = windowHeight / 9;
    }
    calculatedCanvasEdgeSize *= canvasScale;
    pieceEdgeSize = calculatedCanvasEdgeSize / 3;
    paddedWidth = pieceEdgeSize - 10;
	createCanvas(calculatedCanvasEdgeSize, calculatedCanvasEdgeSize);

    board = new gameBoard();
}

function windowResized() {
    calculatedCanvasEdgeSize = windowWidth / 16;
    if (windowHeight / 9 < calculatedCanvasEdgeSize) {
        calculatedCanvasEdgeSize = windowHeight / 9;
    }
    calculatedCanvasEdgeSize *= canvasScale;
    pieceEdgeSize = calculatedCanvasEdgeSize / 3;
    paddedWidth = pieceEdgeSize - 10;
    resizeCanvas(calculatedCanvasEdgeSize, calculatedCanvasEdgeSize);
}

function hoveringOver(topLeft) {
    return (mouseX > topLeft.x && mouseX < topLeft.x + paddedWidth && mouseY > topLeft.y && mouseY < topLeft.y + paddedWidth);
}

function mouseClicked() {
    for (var y = 0; y < 3; y++) {
        for (var x = 0; x < 3; x++) {
            var topLeft = {
                x: x * pieceEdgeSize + 5,
                y: y * pieceEdgeSize + 5
            };
            if (hoveringOver(topLeft)) {
                if (playerTurn && !winner.found && !winner.draw) {
                    if (board.board[y][x] === "") {
                        playerTurn = false;
                        board.board[y][x] = "o";
                        winner = hasWon();
                        if (!winner.found && !winner.draw) {
                            aiTakeTurn();
                            winner = hasWon();
                        }
                    } else {
                        alert("You can't go there!");
                    }
                } else {
                    alert("You can't play now!");
                }
                break;
            }
        }
    }
}

function aiTakeTurn() {
    console.log("TODO make computer player use MiniMax algorithm");
    var x = floor(random(3));
    var y = floor(random(3));
    while(board.board[y][x] !== "") {
        x = floor(random(3));
        y = floor(random(3));
    }
    board.board[y][x] = "x";
    playerTurn = true;
}

function hasWon() {
    var winnerFound = {
        found: false,
        draw: false,
        piece: ""
    };
    for (var y = 0; y < 3; y++) {
        if (
            board.board[y][0] === board.board[y][1] &&
            board.board[y][1] === board.board[y][2]
        ) {
            if (board.board[y][0] !== "") {
                winnerFound.found = true;
                winnerFound.piece = board.board[y][0];
                break;
            }
        }
    }
    if (!winnerFound.found) {
        for (var x = 0; x < 3; x++) {
            if (
                board.board[0][x] === board.board[1][x] &&
                board.board[1][x] === board.board[2][x]
            ) {
                if (board.board[0][x] !== "") {
                    winnerFound.found = true;
                    winnerFound.piece = board.board[0][x];
                    break;
                }
            }
        }
    }
    if (!winnerFound.found) {
        if (
            board.board[0][0] === board.board[1][1] &&
            board.board[1][1] === board.board[2][2]
            ||
            board.board[0][2] === board.board[1][1] &&
            board.board[1][1] === board.board[2][0]
        ) {
            if (board.board[1][1] !== "") {
                winnerFound.found = true;
                winnerFound.piece = board.board[1][1];
            }
        }
    }
    if (winnerFound.found) {
        console.log("We have a winner", winnerFound.piece);
        return winnerFound;
    } else {
        for (var y = 0; y < 3; y++) {
            for (var x = 0; x < 3; x++) {
                if (board.board[y][x] === "") {
                    // when checking for a draw, if we encounter an empty piece, we haven't drawn yet
                    return winnerFound;
                }
            }
        }
        console.log("It's a draw!");
        winnerFound.draw = true;
        return winnerFound;
    }
}

function draw() {
    background(51);
    board.draw();
}

function gameBoard() {
    this.board = [];
    for (var y = 0; y < 3; y++) {
        this.board[y] = [];
        for (var x = 0; x < 3; x++) {
            //this.board[y].push(random(["","","o","x"]));
            this.board[y].push("");
        }
    }

    this.draw = function() {
        noStroke();
        for (var y = 0; y < 3; y++) {
            for (var x = 0; x < 3; x++) {
                var topLeft = {
                    x: x * pieceEdgeSize + 5,
                    y: y * pieceEdgeSize + 5
                };
                var drawnSquare = false;
                if (hoveringOver(topLeft)) {
                    if (board.board[y][x] === "") {
                        if (playerTurn && !winner.found) {
                            fill(255);
                            rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                            fill(0, 255, 0, 150);
                            rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                            drawnSquare = true;
                        }
                    }
                }
                if (!drawnSquare) {
                    if (board.board[y][x] === "") {
                        fill(255);
                    } else if (board.board[y][x] === "x") {
                        fill(255, 0, 0, 150);
                    } else {
                        fill(0, 255, 0, 150);
                    }
                    rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                }
            }
        }
    }
}<commit_msg>Actual naughts and crosses instead of boxes<commit_after>var canvasScale = 8;
var calculatedCanvasEdgeSize = 50;
var pieceEdgeSize = 10;
var paddedWidth = pieceEdgeSize - 10;
var playerTurn = true;
var board;
var winner = {
    found: false,
    draw: false,
    piece: ""
};

function setup() {
	frameRate(60);

    calculatedCanvasEdgeSize = windowWidth / 16;
    if (windowHeight / 9 < calculatedCanvasEdgeSize) {
        calculatedCanvasEdgeSize = windowHeight / 9;
    }
    calculatedCanvasEdgeSize *= canvasScale;
    pieceEdgeSize = calculatedCanvasEdgeSize / 3;
    paddedWidth = pieceEdgeSize - 10;
	createCanvas(calculatedCanvasEdgeSize, calculatedCanvasEdgeSize);

    board = new gameBoard();
}

function windowResized() {
    calculatedCanvasEdgeSize = windowWidth / 16;
    if (windowHeight / 9 < calculatedCanvasEdgeSize) {
        calculatedCanvasEdgeSize = windowHeight / 9;
    }
    calculatedCanvasEdgeSize *= canvasScale;
    pieceEdgeSize = calculatedCanvasEdgeSize / 3;
    paddedWidth = pieceEdgeSize - 10;
    resizeCanvas(calculatedCanvasEdgeSize, calculatedCanvasEdgeSize);
}

function hoveringOver(topLeft) {
    return (mouseX > topLeft.x && mouseX < topLeft.x + paddedWidth && mouseY > topLeft.y && mouseY < topLeft.y + paddedWidth);
}

function mouseClicked() {
    for (var y = 0; y < 3; y++) {
        for (var x = 0; x < 3; x++) {
            var topLeft = {
                x: x * pieceEdgeSize + 5,
                y: y * pieceEdgeSize + 5
            };
            if (hoveringOver(topLeft)) {
                if (playerTurn && !winner.found && !winner.draw) {
                    if (board.board[y][x] === "") {
                        playerTurn = false;
                        board.board[y][x] = "o";
                        winner = hasWon();
                        if (!winner.found && !winner.draw) {
                            aiTakeTurn();
                            winner = hasWon();
                        }
                    } else {
                        alert("You can't go there!");
                    }
                } else {
                    alert("You can't play now!");
                }
                break;
            }
        }
    }
}

function aiTakeTurn() {
    console.log("TODO make computer player use MiniMax algorithm");
    var x = floor(random(3));
    var y = floor(random(3));
    while(board.board[y][x] !== "") {
        x = floor(random(3));
        y = floor(random(3));
    }
    board.board[y][x] = "x";
    playerTurn = true;
}

function hasWon() {
    var winnerFound = {
        found: false,
        draw: false,
        piece: ""
    };
    for (var y = 0; y < 3; y++) {
        if (
            board.board[y][0] === board.board[y][1] &&
            board.board[y][1] === board.board[y][2]
        ) {
            if (board.board[y][0] !== "") {
                winnerFound.found = true;
                winnerFound.piece = board.board[y][0];
                break;
            }
        }
    }
    if (!winnerFound.found) {
        for (var x = 0; x < 3; x++) {
            if (
                board.board[0][x] === board.board[1][x] &&
                board.board[1][x] === board.board[2][x]
            ) {
                if (board.board[0][x] !== "") {
                    winnerFound.found = true;
                    winnerFound.piece = board.board[0][x];
                    break;
                }
            }
        }
    }
    if (!winnerFound.found) {
        if (
            board.board[0][0] === board.board[1][1] &&
            board.board[1][1] === board.board[2][2]
            ||
            board.board[0][2] === board.board[1][1] &&
            board.board[1][1] === board.board[2][0]
        ) {
            if (board.board[1][1] !== "") {
                winnerFound.found = true;
                winnerFound.piece = board.board[1][1];
            }
        }
    }
    if (winnerFound.found) {
        console.log("We have a winner", winnerFound.piece);
        return winnerFound;
    } else {
        for (var y = 0; y < 3; y++) {
            for (var x = 0; x < 3; x++) {
                if (board.board[y][x] === "") {
                    // when checking for a draw, if we encounter an empty piece, we haven't drawn yet
                    return winnerFound;
                }
            }
        }
        console.log("It's a draw!");
        winnerFound.draw = true;
        return winnerFound;
    }
}

function draw() {
    background(51);
    board.draw();
}

function gameBoard() {
    this.board = [];
    for (var y = 0; y < 3; y++) {
        this.board[y] = [];
        for (var x = 0; x < 3; x++) {
            //this.board[y].push(random(["","","o","x"]));
            this.board[y].push("");
        }
    }

    this.draw = function() {
        noStroke();
        for (var y = 0; y < 3; y++) {
            for (var x = 0; x < 3; x++) {
                var topLeft = {
                    x: x * pieceEdgeSize + 5,
                    y: y * pieceEdgeSize + 5
                };
                var drawnSquare = false;
                if (hoveringOver(topLeft)) {
                    if (board.board[y][x] === "") {
                        if (playerTurn && !winner.found) {
                            noStroke();
                            fill(255);
                            rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                            fill(0, 255, 0, 150);
                            rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                            drawnSquare = true;
                        }
                    }
                }
                if (!drawnSquare) {
                    noStroke();
                    if (!winner.found && !winner.draw) {
                        fill(255);
                    } else {
                        fill(255, 51);
                    }
                    rect(topLeft.x, topLeft.y, paddedWidth, paddedWidth);
                    if (board.board[y][x] === "x") {
                        noFill();
                        strokeWeight(4);
                        stroke(255, 0, 0, 150);
                        line(topLeft.x, topLeft.y, topLeft.x + paddedWidth, topLeft.y + paddedWidth);
                        line(topLeft.x, topLeft.y + paddedWidth, topLeft.x + paddedWidth, topLeft.y);
                    } else if (board.board[y][x] === "o") {
                        noFill();
                        strokeWeight(4);
                        stroke(0, 255, 0, 150);
                        ellipse(topLeft.x + paddedWidth/2, topLeft.y + paddedWidth/2, paddedWidth, paddedWidth);
                    }
                }
            }
        }
    }
}<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import { FormModal, useNotifications, useApi, useLoading, useModals } from 'react-components';
import { setPaymentMethod } from 'proton-shared/lib/api/payments';
import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants';

import Card from './Card';
import useCard from './useCard';
import toDetails from './toDetails';
import { handlePaymentToken } from './paymentTokenHelper';

const EditCardModal = ({ card: existingCard, onClose, onChange, ...rest }) => {
    const api = useApi();
    const [loading, withLoading] = useLoading();
    const { createNotification } = useNotifications();
    const { createModal } = useModals();
    const title = existingCard ? c('Title').t`Edit credit/debit card` : c('Title').t`Add credit/debit card`;
    const { card, updateCard, errors, isValid } = useCard(existingCard);

    const handleSubmit = async (event) => {
        if (!isValid) {
            event.preventDefault();
            return;
        }
        // 1 CHF to allow card authorizations
        const { Payment } = await handlePaymentToken({
            params: {
                Amount: 100,
                Currency: 'CHF',
                Payment: {
                    Type: PAYMENT_METHOD_TYPES.CARD,
                    Details: toDetails(card)
                }
            },
            api,
            createModal
        });
        await api(setPaymentMethod(Payment));
        await onChange();
        onClose();
        createNotification({ text: c('Success').t`Payment method updated` });
    };

    return (
        <FormModal
            small
            onSubmit={() => withLoading(handleSubmit())}
            onClose={onClose}
            title={title}
            loading={loading}
            submit={c('Action').t`Save`}
            {...rest}
        >
            <Card card={card} errors={errors} onChange={updateCard} loading={loading} />
        </FormModal>
    );
};

EditCardModal.propTypes = {
    card: PropTypes.object,
    onClose: PropTypes.func,
    onChange: PropTypes.func.isRequired
};

export default EditCardModal;
<commit_msg>Remove 1 CHF for card authorization<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import { FormModal, useNotifications, useApi, useLoading, useModals } from 'react-components';
import { setPaymentMethod } from 'proton-shared/lib/api/payments';
import { PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants';

import Card from './Card';
import useCard from './useCard';
import toDetails from './toDetails';
import { handlePaymentToken } from './paymentTokenHelper';

const EditCardModal = ({ card: existingCard, onClose, onChange, ...rest }) => {
    const api = useApi();
    const [loading, withLoading] = useLoading();
    const { createNotification } = useNotifications();
    const { createModal } = useModals();
    const title = existingCard ? c('Title').t`Edit credit/debit card` : c('Title').t`Add credit/debit card`;
    const { card, updateCard, errors, isValid } = useCard(existingCard);

    const handleSubmit = async (event) => {
        if (!isValid) {
            event.preventDefault();
            return;
        }

        const { Payment } = await handlePaymentToken({
            params: {
                Payment: {
                    Type: PAYMENT_METHOD_TYPES.CARD,
                    Details: toDetails(card)
                }
            },
            api,
            createModal
        });
        await api(setPaymentMethod(Payment));
        await onChange();
        onClose();
        createNotification({ text: c('Success').t`Payment method updated` });
    };

    return (
        <FormModal
            small
            onSubmit={() => withLoading(handleSubmit())}
            onClose={onClose}
            title={title}
            loading={loading}
            submit={c('Action').t`Save`}
            {...rest}
        >
            <Card card={card} errors={errors} onChange={updateCard} loading={loading} />
        </FormModal>
    );
};

EditCardModal.propTypes = {
    card: PropTypes.object,
    onClose: PropTypes.func,
    onChange: PropTypes.func.isRequired
};

export default EditCardModal;
<|endoftext|>


<commit_before>(function (root, factory) {
    if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        factory(exports, require('angular'));
    } else {
        factory(root, root.angular);
    }
}(this, function (exports, angular) {

    const hasOwnProperty = Object.prototype.hasOwnProperty

    class ScopeController {
        constructor (...args) {
            const {$inject: inject, prototype} = this.constructor
            if (inject) {
                inject.forEach((key, index) => this[key] = args[index])
            }

            const state = this.$onState && this.$onState(...args)
            const scope = this.$scope
            if (state) {
                if (scope) {
                    proxyState(this, state)
                } else {
                    assignState(this, state)
                }
            }

            if (!scope) {
                return
            }

            const toWatch = prototype.$$toWatch

            if (process.env.NODE_ENV === 'development') {
                Object.defineProperty(scope, '$$scopeController', {value: this})
            }

            const descriptors = Object.getOwnPropertyDescriptors(prototype)
            const toAssignDescriptors = {}
            for (const [key, descriptor] of Object.entries(descriptors)) {
                if (key === 'constructor' || key.startsWith('$')) {
                    continue
                }
                if (angular.isFunction(descriptor.value)) {
                    descriptor.value = this::descriptor.value
                } else {
                    if (toWatch && toWatch[key]) { // computed property
                        continue
                    }
                    if (angular.isFunction(descriptor.get)) {
                        descriptor.get = this::descriptor.get
                    }
                    if (angular.isFunction(descriptor.set)) {
                        descriptor.set = this::descriptor.set
                    }
                }
                toAssignDescriptors[key] = descriptor
            }
            assign(scope, toAssignDescriptors)

            if (!toWatch) {
                return
            }
            const unwatches = {}
            const toAssignComputedDescriptors = {}
            const self = this
            for (const [key, options] of Object.entries(toWatch)) {
                if (angular.isFunction(options)) {
                    unwatches[key] = this.$watch(...options.expressions, options)
                    continue
                }
                // computed property
                const {get: getter, set: setter} = options.callback
                const commputed = makeComputed(this, getter, options)
                options.callback = commputed
                this.$watch(...options.expressions, options)
                Object.defineProperty(this, key, {
                    get: commputed,
                    set: setter,
                    enumerable: true
                })
                toAssignComputedDescriptors[key] = {
                    get: commputed,
                    set: angular.isFunction(setter) ? this::setter : undefined,
                    enumerable: true
                }
            }
            Object.defineProperty(this, '$$unwatches', {
                value: unwatches
            })
            assign(scope, toAssignComputedDescriptors)
        }

        $watch (...args) {
            const options = args.pop()
            const expressions = args
            let callback, deep, strict
            if (angular.isFunction(options)) {
                callback = this::options
            } else {
                callback = this::options.callback
                deep = options.deep
                strict = options.strict
            }

            const scope = this.$scope
            if (expressions.length > 1) {
                return scope.$watchGroup(expressions, callback)
            }
            const [expression] = expressions
            if (deep) {
                return scope.$watchCollection(expression, callback)
            } else if (strict ) {
                return scope.$watch(expression, callback, true)
            } else {
                return scope.$watch(expression, callback)
            }
        }

        $unwatch (...names) {
            const unwatches = this.$$unwatches
            if (!unwatches) {
                return
            }
            for (const name of names) {
                const unwatch = unwatches[name]
                if (unwatch) {
                    unwatch()
                    delete unwatches[name]
                }
            }
        }
    }

    function makeGetter (key) {
        return function getter () {
            if (process.env.NODE_ENV === 'development') {
                if (!this.$scope) {
                    throw new Error('Should inject $scope to Controller!')
                }
            }
            return this.$scope[key]
        }
    }
    function makeSetter (key) {
        return function setter (value) {
            if (process.env.NODE_ENV === 'development') {
                if (!this.$scope) {
                    throw new Error('Should inject $scope to Controller!')
                }
            }
            this.$scope[key] = value
        }
    }

    function write (target, key, value) {
        const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if (process.env.NODE_ENV === 'development') {
            if (descriptor && !descriptor.writable && !descriptor.set) {
                console.warn(`Can not assign ${value} to ${target}.${key}!`)
                return
            }
        }
        target[key] = value
    }

    function proxyState (target, state) {
        const {$scope} = target
        const descriptors = Object.getOwnPropertyDescriptors(state)
        for (const [key, descriptor] of Object.entries(descriptors)) {
            const targetDescriptor = Object.getOwnPropertyDescriptor(target, key)
            if (targetDescriptor && !targetDescriptor.configurable) {
                continue
            }
            let getter, setter
            if (descriptor::hasOwnProperty('value')) {
                getter = makeGetter(key)
                setter = makeSetter(key)
            } else {
                getter = descriptor.get && makeGetter(key)
                setter = descriptor.set && makeSetter(key)
            }
            Object.defineProperty(target, key, {
                get: getter,
                set: setter,
                enumerable: true
            })

            if ($scope::hasOwnProperty(key)) {
                if ($scope[key] === undefined) {
                    write($scope, key, target[key])
                } else {
                    write(target, key, $scope[key])
                }
            } else {
                Object.defineProperty(target.$scope, key, descriptor)
            }
        }
    }

    function assign (target, descriptors) {
        for (const [key, descriptor] of Object.entries(descriptors)) {
            const targetDescriptor = Object.getOwnPropertyDescriptor(target, key)
            if (targetDescriptor && !targetDescriptor.configurable) {
                continue
            }
            Object.defineProperty(target, key, descriptor)
        }
    }

    function assignState (target, state) {
        const descriptors = Object.getOwnPropertyDescriptors(state)
        assign(target, descriptors)
    }

    function inject (...args) {
        return function doInject (target) {
            target.$inject = args
            return target
        }
    }

    function makeComputed (target, getter, {sync}) {
        let value
        const evaluate = () => value = target::getter()
        if (sync) {
            return function commputed () {
                if (arguments.length === 0) {
                    return value
                }
                evaluate()
            }
        }
        let dirty = true
        return function commputed () {
            if (arguments.length === 0) {
                if (dirty) {
                    evaluate()
                    dirty = false
                }
                return value
            }
            dirty = true
        }
    }

    function watch (...args) {
        const expressions = args
        const options = {
            expressions,
            callback: null,
            deep: false,
            strict: false,
            sync: false,
        }
        return function registerWatch (prototype, property, descriptor) {
            const lastArg = expressions[args.length - 1] || false
            const toWatch = descriptor::hasOwnProperty('value')
            if (lastArg === true || lastArg === false) {
                if (toWatch) {
                    options.deep = lastArg
                } else {
                    options.sync = lastArg
                }
                expressions.pop()
            } else if (angular.isObject(lastArg)) {
                angular.merge(options, lastArg)
                expressions.pop
            }
            if (toWatch) {
                options.callback = descriptor.value
            } else {
                options.callback = {
                    get: descriptor.get,
                    set: descriptor.set,
                }
            }
            if (!prototype['$$toWatch']) {
                Object.defineProperty(prototype, '$$toWatch', {
                    value: Object.create(null),
                    configurable: true
                })
            }
            prototype['$$toWatch'][property] = options
        }
    }

    exports.ScopeController = angular.ScopeController = ScopeController
    exports.inject = ScopeController.inject = inject
    exports.watch = ScopeController.watch = watch
}));
<commit_msg>Fix @watch<commit_after>(function (root, factory) {
    if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        factory(exports, require('angular'));
    } else {
        factory(root, root.angular);
    }
}(this, function (exports, angular) {

    const hasOwnProperty = Object.prototype.hasOwnProperty

    class ScopeController {
        constructor (...args) {
            const {$inject: inject, prototype} = this.constructor
            if (inject) {
                inject.forEach((key, index) => this[key] = args[index])
            }

            const state = this.$onState && this.$onState(...args)
            const scope = this.$scope
            if (state) {
                if (scope) {
                    proxyState(this, state)
                } else {
                    assignState(this, state)
                }
            }

            if (!scope) {
                return
            }

            const toWatch = prototype.$$toWatch

            if (process.env.NODE_ENV === 'development') {
                Object.defineProperty(scope, '$$scopeController', {value: this})
            }

            const descriptors = Object.getOwnPropertyDescriptors(prototype)
            const toAssignDescriptors = {}
            for (const [key, descriptor] of Object.entries(descriptors)) {
                if (key === 'constructor' || key.startsWith('$')) {
                    continue
                }
                if (angular.isFunction(descriptor.value)) {
                    descriptor.value = this::descriptor.value
                } else {
                    if (toWatch && toWatch[key]) { // computed property
                        continue
                    }
                    if (angular.isFunction(descriptor.get)) {
                        descriptor.get = this::descriptor.get
                    }
                    if (angular.isFunction(descriptor.set)) {
                        descriptor.set = this::descriptor.set
                    }
                }
                toAssignDescriptors[key] = descriptor
            }
            assign(scope, toAssignDescriptors)

            if (!toWatch) {
                return
            }
            const unwatches = {}
            const toAssignComputedDescriptors = {}
            for (const [key, options] of Object.entries(toWatch)) {
                const {callback} = options
                if (angular.isFunction(callback)) {
                    unwatches[key] = this.$watch(...options.expressions, options)
                    continue
                }
                // computed property
                const {get: getter, set: setter} = callback
                const commputed = makeComputed(this, getter, options)
                options.callback = commputed
                this.$watch(...options.expressions, options)
                Object.defineProperty(this, key, {
                    get: commputed,
                    set: setter,
                    enumerable: true
                })
                toAssignComputedDescriptors[key] = {
                    get: commputed,
                    set: angular.isFunction(setter) ? this::setter : undefined,
                    enumerable: true
                }
            }
            Object.defineProperty(this, '$$unwatches', {
                value: unwatches
            })
            assign(scope, toAssignComputedDescriptors)
        }

        $watch (...args) {
            const options = args.pop()
            const expressions = args
            let callback, deep, strict
            if (angular.isFunction(options)) {
                callback = this::options
            } else {
                callback = this::options.callback
                deep = options.deep
                strict = options.strict
            }

            const scope = this.$scope
            if (expressions.length > 1) {
                return scope.$watchGroup(expressions, callback)
            }
            const [expression] = expressions
            if (deep) {
                return scope.$watchCollection(expression, callback)
            } else if (strict ) {
                return scope.$watch(expression, callback, true)
            } else {
                return scope.$watch(expression, callback)
            }
        }

        $unwatch (...names) {
            const unwatches = this.$$unwatches
            if (!unwatches) {
                return
            }
            for (const name of names) {
                const unwatch = unwatches[name]
                if (unwatch) {
                    unwatch()
                    delete unwatches[name]
                }
            }
        }
    }

    function makeGetter (key) {
        return function getter () {
            if (process.env.NODE_ENV === 'development') {
                if (!this.$scope) {
                    throw new Error('Should inject $scope to Controller!')
                }
            }
            return this.$scope[key]
        }
    }
    function makeSetter (key) {
        return function setter (value) {
            if (process.env.NODE_ENV === 'development') {
                if (!this.$scope) {
                    throw new Error('Should inject $scope to Controller!')
                }
            }
            this.$scope[key] = value
        }
    }

    function write (target, key, value) {
        const descriptor = Object.getOwnPropertyDescriptor(target, key)
        if (process.env.NODE_ENV === 'development') {
            if (descriptor && !descriptor.writable && !descriptor.set) {
                console.warn(`Can not assign ${value} to ${target}.${key}!`)
                return
            }
        }
        target[key] = value
    }

    function proxyState (target, state) {
        const {$scope} = target
        const descriptors = Object.getOwnPropertyDescriptors(state)
        for (const [key, descriptor] of Object.entries(descriptors)) {
            const targetDescriptor = Object.getOwnPropertyDescriptor(target, key)
            if (targetDescriptor && !targetDescriptor.configurable) {
                continue
            }
            let getter, setter
            if (descriptor::hasOwnProperty('value')) {
                getter = makeGetter(key)
                setter = makeSetter(key)
            } else {
                getter = descriptor.get && makeGetter(key)
                setter = descriptor.set && makeSetter(key)
            }
            Object.defineProperty(target, key, {
                get: getter,
                set: setter,
                enumerable: true
            })

            if ($scope::hasOwnProperty(key)) {
                if ($scope[key] === undefined) {
                    write($scope, key, target[key])
                } else {
                    write(target, key, $scope[key])
                }
            } else {
                Object.defineProperty(target.$scope, key, descriptor)
            }
        }
    }

    function assign (target, descriptors) {
        for (const [key, descriptor] of Object.entries(descriptors)) {
            const targetDescriptor = Object.getOwnPropertyDescriptor(target, key)
            if (targetDescriptor && !targetDescriptor.configurable) {
                continue
            }
            Object.defineProperty(target, key, descriptor)
        }
    }

    function assignState (target, state) {
        const descriptors = Object.getOwnPropertyDescriptors(state)
        assign(target, descriptors)
    }

    function inject (...args) {
        return function doInject (target) {
            target.$inject = args
            return target
        }
    }

    function makeComputed (target, getter, {sync}) {
        let value
        const evaluate = () => value = target::getter()
        if (sync) {
            return function commputed () {
                if (arguments.length === 0) {
                    return value
                }
                evaluate()
            }
        }
        let dirty = true
        return function commputed () {
            if (arguments.length === 0) {
                if (dirty) {
                    evaluate()
                    dirty = false
                }
                return value
            }
            dirty = true
        }
    }

    function watch (...args) {
        const expressions = args
        const options = {
            expressions,
            callback: null,
            deep: false,
            strict: false,
            sync: false,
        }
        return function registerWatch (prototype, property, descriptor) {
            const lastArg = expressions[args.length - 1] || false
            const toWatch = descriptor::hasOwnProperty('value')
            if (lastArg === true || lastArg === false) {
                if (toWatch) {
                    options.deep = lastArg
                } else {
                    options.sync = lastArg
                }
                expressions.pop()
            } else if (angular.isObject(lastArg)) {
                angular.merge(options, lastArg)
                expressions.pop
            }
            if (toWatch) {
                options.callback = descriptor.value
            } else {
                options.callback = {
                    get: descriptor.get,
                    set: descriptor.set,
                }
            }
            if (!prototype['$$toWatch']) {
                Object.defineProperty(prototype, '$$toWatch', {
                    value: Object.create(null),
                    configurable: true
                })
            }
            prototype['$$toWatch'][property] = options
        }
    }

    exports.ScopeController = angular.ScopeController = ScopeController
    exports.inject = ScopeController.inject = inject
    exports.watch = ScopeController.watch = watch
}));
<|endoftext|>


<commit_before>/**
 * The unit tests for the exported functions from `audio-over-socket.js`.
 */

import autobahn from 'autobahn';
import * as aos from './audio-over-socket';
import { settings } from '../communication';
import * as communication from '../communication/websocket';
import * as utils from '.';
import broadcaster from '../broadcaster';

const rpcName = 'rpcName';

function setupStubsSimple() {
  const recorderStub = jasmine.createSpyObj('Recorder', [
    'addEventListener',
    'removeEventListener',
    'dispatchEvent',
    'getAudioSpecs',
  ]);

  const makeWebsocketCallSpy = spyOn(communication, 'makeWebsocketCall');
  const dataToBase64Spy = spyOn(utils, 'dataToBase64');
  const broadcasterSpy = spyOn(broadcaster, 'emit');

  return {
    recorderStub,
    makeWebsocketCallSpy,
    dataToBase64Spy,
    broadcasterSpy,
  };
}

function setupStubs() {
  const recorderStub = jasmine.createSpyObj('Recorder', [
    'addEventListener',
    'removeEventListener',
    'dispatchEvent',
  ]);

  spyOn(autobahn.Connection.prototype, 'close');
  const connectionOpenSpy = spyOn(autobahn.Connection.prototype, 'open');
  const connectionSessionStub = jasmine.createSpyObj('Session', [
    'call',
    'register',
    'unregister',
  ]);
  connectionSessionStub.call.and.callFake(() => {
    // eslint-disable-next-line new-cap
    const defer = new autobahn.when.defer();
    defer.resolve();
    return defer.promise;
  });

  connectionSessionStub.registrations = [];

  connectionSessionStub.register.and.callFake((...args) => {
    const [rpc, callback] = args;
    const registration = {
      id: '123',
      rpc,
      callback,
    };
    connectionSessionStub.registrations.push(registration);
    return Promise.resolve(registration);
  });

  connectionSessionStub.unregister.and.callFake((...args) => {
    const [registration] = args;
    const { registrations } = connectionSessionStub;
    registrations.splice(
      registrations.findIndex(reg => reg.id === registration.id),
      1,
    );
    return Promise.resolve();
  });

  // We cannot use arrow functions because of `this` scope.
  // eslint-disable-next-line func-names
  connectionOpenSpy.and.callFake(function() {
    // This property is returned through the session "property" of a
    // connection instance. Sadly only the get is defined with the
    // `Object.defineProperty` which forces us to mock the internals.
    this._session = connectionSessionStub; // eslint-disable-line no-underscore-dangle
    this.onopen();
  });

  return {
    recorderStub,
    connectionOpenSpy,
    connectionSessionStub,
  };
}

describe('Audio Over socket', () => {
  beforeEach(() => {
    // Make sure we have enough time to complete some tests.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
  });

  beforeAll(() => {
    settings.wsUrl = 'wss://fake.ws.url';
  });

  describe('registerStreamForRecorder', () => {
    it('should register an RPC call named nl.itslanguage.rpcName', async () => {
      const { recorderStub } = setupStubs();
      await expectAsync(
        aos.registerStreamForRecorder(recorderStub, rpcName),
      ).toBeResolvedTo({
        id: '123',
        rpc: `nl.itslanguage.${rpcName}`,
        callback: jasmine.any(Function),
      });
    });

    it('should remove a previously registered RCP', async () => {
      const { recorderStub, connectionSessionStub } = setupStubs();
      const reg = {
        id: '456',
        rpc: `nl.itslanguage.${rpcName}`,
        callback: Function,
      };

      connectionSessionStub.registrations = [reg];
      await aos.registerStreamForRecorder(recorderStub, rpcName);

      expect(connectionSessionStub.registrations.length).toBe(1);
    });

    it('should emit websocketserverreadyforaudio when ready to receive audio', async () => {
      const { recorderStub } = setupStubs();
      const broadcastSpy = spyOn(broadcaster, 'emit');

      await aos.registerStreamForRecorder(recorderStub, rpcName);

      expect(broadcastSpy).toHaveBeenCalledWith('websocketserverreadyforaudio');
    });

    it('should stream audio to the backend', async () => {
      const { recorderStub } = setupStubs();
      const data = {
        data: new Blob(['Knees weak, arms are heavy.'], { type: 'text/plain' }),
      };

      // Make sure to convert the data to what is actually being sent to the backend: an array
      // with integers.
      const arrayBuffer = await utils.asyncBlobToArrayBuffer(data.data);
      const intArray = Array.from(new Uint8Array(arrayBuffer));

      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback(data);
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);

      const detailsSpy = jasmine.createSpyObj('details', ['progress']);
      await result.callback([], {}, detailsSpy);

      expect(detailsSpy.progress).toHaveBeenCalledWith([intArray]);
    });

    it('should not stream audio if the progress function does not exist', async () => {
      const { recorderStub } = setupStubs();

      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback({
          data: new Blob(['Knees weak, arms are heavy.'], {
            type: 'text/plain',
          }),
        });
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);

      await expectAsync(result.callback([], {}, {})).toBeRejectedWith(
        'no progress function registered',
      );
    });

    it('should only resolve on the last chunk', async () => {
      const { recorderStub } = setupStubs();
      let dataavailableCallback = null;
      let stopCallback = null;
      const blob = {
        data: new Blob(['Knees weak, arms are heavy.'], { type: 'text/plain' }),
      };

      recorderStub.addEventListener.and.callFake((event, callback) => {
        switch (event) {
          case 'dataavailable':
            dataavailableCallback = callback;
            break;
          case 'stop':
            stopCallback = callback;
            break;
          default:
            break;
        }
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);
      const detailsSpy = jasmine.createSpyObj('details', ['progress']);
      result.callback([], {}, detailsSpy);

      dataavailableCallback(blob);

      // We need to wait a bit before we can send the rest.
      setTimeout(async () => {
        // Send the stop event
        stopCallback();

        // Send the final chunk!
        dataavailableCallback(blob);
      }, 1000);

      await result.callback([], {}, detailsSpy);

      expect(detailsSpy.progress).toHaveBeenCalledTimes(2);
    });
  });

  describe('encodeAndSendAudioOnDataAvailable', () => {
    it('should send the data when the recorder fires the event', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        dataToBase64Spy,
      } = setupStubsSimple();

      dataToBase64Spy.and.returnValue(
        "There's vomit on his sweater already, mom's spaghetti",
      );
      makeWebsocketCallSpy.and.returnValue(
        Promise.resolve(
          "He's nervous, but on the surface he looks calm and ready",
        ),
      );
      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback({
          data: new Blob(['Knees weak, arms are heavy.'], {
            type: 'text/plain',
          }),
        });
      });

      const result = await aos.encodeAndSendAudioOnDataAvailable(
        'r353rv3d1d',
        recorderStub,
        'his.palms.are.sweaty',
      );

      expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
        'his.palms.are.sweaty',
        {
          args: [
            'r353rv3d1d',
            "There's vomit on his sweater already, mom's spaghetti",
            'base64',
          ],
        },
      );

      expect(result).toEqual(
        "He's nervous, but on the surface he looks calm and ready",
      );
    });

    it('should reject if the `makeWebsocketCall` rejected', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        dataToBase64Spy,
      } = setupStubsSimple();

      dataToBase64Spy.and.returnValue(
        "There's vomit on his sweater already, mom's spaghetti",
      );
      makeWebsocketCallSpy.and.returnValue(
        Promise.reject(
          new Error('Websocket server has received and rejected the call.'),
        ),
      );
      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback({
          data: new Blob(['Knees weak, arms are heavy.'], {
            type: 'text/plain',
          }),
        });
      });

      try {
        await aos.encodeAndSendAudioOnDataAvailable(
          'r353rv3d1d',
          recorderStub,
          'his.palms.are.sweaty',
        );
        fail('encodeAndSendAudioOnDataAvailable should have failed');
      } catch ({ message }) {
        expect(message).toBe(
          'Websocket server has received and rejected the call.',
        );

        expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
          'his.palms.are.sweaty',
          {
            args: [
              'r353rv3d1d',
              "There's vomit on his sweater already, mom's spaghetti",
              'base64',
            ],
          },
        );
      }
    });
  });

  describe('prepareServerForAudio', () => {
    it('should broadcast when the websocket server has successfully been prepped and resolve in the reserved ID', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        broadcasterSpy,
      } = setupStubsSimple();

      recorderStub.getAudioSpecs.and.returnValue({
        audioFormat: 'audio/ogg',
        audioParameters: {
          bitrate: 9001,
        },
      });
      makeWebsocketCallSpy.and.returnValue(
        Promise.resolve('Websocket server has received and handled the call.'),
      );

      const result = await aos.prepareServerForAudio(
        'r353rv3d1d',
        recorderStub,
        'write.this.down.kiddo',
      );

      expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
        'write.this.down.kiddo',
        {
          args: ['r353rv3d1d', 'audio/ogg'],
          kwargs: {
            bitrate: 9001,
          },
        },
      );

      expect(result).toEqual('r353rv3d1d');

      expect(broadcasterSpy).toHaveBeenCalledWith(
        'websocketserverreadyforaudio',
      );
    });

    it('should reject if the `makeWebsocketCall` rejected', async () => {
      const { recorderStub, makeWebsocketCallSpy } = setupStubsSimple();

      recorderStub.getAudioSpecs.and.returnValue({
        audioFormat: 'audio/ogg',
        audioParameters: {
          bitrate: 9001,
        },
      });
      makeWebsocketCallSpy.and.returnValue(
        Promise.reject(
          new Error('Websocket server has received and rejected the call.'),
        ),
      );

      try {
        await aos.prepareServerForAudio(
          'r353rv3d1d',
          recorderStub,
          'write.this.down.kiddo',
        );
        fail('prepareServerForAudio should have failed');
      } catch (error) {
        expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
          'write.this.down.kiddo',
          {
            args: ['r353rv3d1d', 'audio/ogg'],
            kwargs: {
              bitrate: 9001,
            },
          },
        );
      }
    });
  });
});
<commit_msg>test: refactor tests to work with new stop<commit_after>/**
 * The unit tests for the exported functions from `audio-over-socket.js`.
 */

import autobahn from 'autobahn';
import * as aos from './audio-over-socket';
import { settings } from '../communication';
import * as communication from '../communication/websocket';
import * as utils from '.';
import broadcaster from '../broadcaster';

const rpcName = 'rpcName';

function wait(seconds = 2) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, seconds * 1000);
  });
}

function setupStubsSimple() {
  const recorderStub = jasmine.createSpyObj('Recorder', [
    'addEventListener',
    'removeEventListener',
    'dispatchEvent',
    'getAudioSpecs',
  ]);

  const makeWebsocketCallSpy = spyOn(communication, 'makeWebsocketCall');
  const dataToBase64Spy = spyOn(utils, 'dataToBase64');
  const broadcasterSpy = spyOn(broadcaster, 'emit');

  return {
    recorderStub,
    makeWebsocketCallSpy,
    dataToBase64Spy,
    broadcasterSpy,
  };
}

function setupStubs() {
  const recorderStub = jasmine.createSpyObj('Recorder', [
    'addEventListener',
    'removeEventListener',
    'dispatchEvent',
  ]);

  spyOn(autobahn.Connection.prototype, 'close');
  const connectionOpenSpy = spyOn(autobahn.Connection.prototype, 'open');
  const connectionSessionStub = jasmine.createSpyObj('Session', [
    'call',
    'register',
    'unregister',
  ]);
  connectionSessionStub.call.and.callFake(() => {
    // eslint-disable-next-line new-cap
    const defer = new autobahn.when.defer();
    defer.resolve();
    return defer.promise;
  });

  connectionSessionStub.registrations = [];

  connectionSessionStub.register.and.callFake((...args) => {
    const [rpc, callback] = args;
    const registration = {
      id: '123',
      rpc,
      callback,
    };
    connectionSessionStub.registrations.push(registration);
    return Promise.resolve(registration);
  });

  connectionSessionStub.unregister.and.callFake((...args) => {
    const [registration] = args;
    const { registrations } = connectionSessionStub;
    registrations.splice(
      registrations.findIndex(reg => reg.id === registration.id),
      1,
    );
    return Promise.resolve();
  });

  // We cannot use arrow functions because of `this` scope.
  // eslint-disable-next-line func-names
  connectionOpenSpy.and.callFake(function() {
    // This property is returned through the session "property" of a
    // connection instance. Sadly only the get is defined with the
    // `Object.defineProperty` which forces us to mock the internals.
    this._session = connectionSessionStub; // eslint-disable-line no-underscore-dangle
    this.onopen();
  });

  return {
    recorderStub,
    connectionOpenSpy,
    connectionSessionStub,
  };
}

describe('Audio Over socket', () => {
  beforeEach(() => {
    // Make sure we have enough time to complete some tests.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
  });

  beforeAll(() => {
    settings.wsUrl = 'wss://fake.ws.url';
  });

  describe('registerStreamForRecorder', () => {
    it('should register an RPC call named nl.itslanguage.rpcName', async () => {
      const { recorderStub } = setupStubs();
      await expectAsync(
        aos.registerStreamForRecorder(recorderStub, rpcName),
      ).toBeResolvedTo({
        id: '123',
        rpc: `nl.itslanguage.${rpcName}`,
        callback: jasmine.any(Function),
      });
    });

    it('should remove a previously registered RCP', async () => {
      const { recorderStub, connectionSessionStub } = setupStubs();
      const reg = {
        id: '456',
        rpc: `nl.itslanguage.${rpcName}`,
        callback: Function,
      };

      connectionSessionStub.registrations = [reg];
      await aos.registerStreamForRecorder(recorderStub, rpcName);

      expect(connectionSessionStub.registrations.length).toBe(1);
    });

    it('should emit websocketserverreadyforaudio when ready to receive audio', async () => {
      const { recorderStub } = setupStubs();
      const broadcastSpy = spyOn(broadcaster, 'emit');

      await aos.registerStreamForRecorder(recorderStub, rpcName);

      expect(broadcastSpy).toHaveBeenCalledWith('websocketserverreadyforaudio');
    });

    it('should stream audio to the backend', async () => {
      const { recorderStub } = setupStubs();
      let dataavailableCallback = null;
      let stopCallback = null;
      const data = {
        data: new Blob(['Knees weak, arms are heavy.'], { type: 'text/plain' }),
      };

      // Make sure to convert the data to what is actually being sent to the backend: an array
      // with integers.
      const arrayBuffer = await utils.asyncBlobToArrayBuffer(data.data);
      const intArray = Array.from(new Uint8Array(arrayBuffer));

      recorderStub.addEventListener.and.callFake((event, callback) => {
        switch (event) {
          case 'dataavailable':
            dataavailableCallback = callback;
            break;
          case 'stop':
            stopCallback = callback;
            break;
          default:
            break;
        }
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);
      const detailsSpy = jasmine.createSpyObj('details', ['progress']);

      result.callback([], {}, detailsSpy);

      dataavailableCallback(data);

      await wait(1); // Wait a second before sending stop;
      stopCallback();

      // Add a wait to make sure the events are properly processed.
      await wait();

      expect(detailsSpy.progress).toHaveBeenCalledWith([intArray]);
    });

    it('should not stream audio if the progress function does not exist', async () => {
      const { recorderStub } = setupStubs();
      const data = {
        data: new Blob(['Knees weak, arms are heavy.'], { type: 'text/plain' }),
      };

      recorderStub.addEventListener.and.callFake((event, callback) => {
        if (event === 'dataavailable') {
          callback(data);
        }
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);
      await expectAsync(result.callback([], {}, {})).toBeRejectedWith(
        'no progress function registered',
      );
    });

    it('should only resolve on the last chunk', async () => {
      const { recorderStub } = setupStubs();
      let dataavailableCallback = null;
      let stopCallback = null;
      const blob = {
        data: new Blob(['Knees weak, arms are heavy.'], { type: 'text/plain' }),
      };

      recorderStub.addEventListener.and.callFake((event, callback) => {
        switch (event) {
          case 'dataavailable':
            dataavailableCallback = callback;
            break;
          case 'stop':
            stopCallback = callback;
            break;
          default:
            break;
        }
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);
      const detailsSpy = jasmine.createSpyObj('details', ['progress']);
      const resultCB = result.callback([], {}, detailsSpy);

      dataavailableCallback(blob);

      await wait(1); // Wait a second before sending stop;
      stopCallback();

      await wait(1); // Wait a second before sending stop;
      dataavailableCallback(blob);

      await expectAsync(resultCB).toBeResolved();
    });

    it('should resolve if stopped without sending data', async () => {
      const { recorderStub } = setupStubs();
      let stopCallback = null;

      recorderStub.addEventListener.and.callFake((event, callback) => {
        if (event === 'stop') {
          stopCallback = callback;
        }
      });

      const result = await aos.registerStreamForRecorder(recorderStub, rpcName);
      const detailsSpy = jasmine.createSpyObj('details', ['progress']);
      const resultCB = result.callback([], {}, detailsSpy);

      stopCallback();
      await expectAsync(resultCB).toBeResolved();
    });
  });

  describe('encodeAndSendAudioOnDataAvailable', () => {
    it('should send the data when the recorder fires the event', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        dataToBase64Spy,
      } = setupStubsSimple();

      dataToBase64Spy.and.returnValue(
        "There's vomit on his sweater already, mom's spaghetti",
      );
      makeWebsocketCallSpy.and.returnValue(
        Promise.resolve(
          "He's nervous, but on the surface he looks calm and ready",
        ),
      );
      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback({
          data: new Blob(['Knees weak, arms are heavy.'], {
            type: 'text/plain',
          }),
        });
      });

      const result = await aos.encodeAndSendAudioOnDataAvailable(
        'r353rv3d1d',
        recorderStub,
        'his.palms.are.sweaty',
      );

      expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
        'his.palms.are.sweaty',
        {
          args: [
            'r353rv3d1d',
            "There's vomit on his sweater already, mom's spaghetti",
            'base64',
          ],
        },
      );

      expect(result).toEqual(
        "He's nervous, but on the surface he looks calm and ready",
      );
    });

    it('should reject if the `makeWebsocketCall` rejected', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        dataToBase64Spy,
      } = setupStubsSimple();

      dataToBase64Spy.and.returnValue(
        "There's vomit on his sweater already, mom's spaghetti",
      );
      makeWebsocketCallSpy.and.returnValue(
        Promise.reject(
          new Error('Websocket server has received and rejected the call.'),
        ),
      );
      recorderStub.addEventListener.and.callFake((event, callback) => {
        // Pretend as if the event has been fired and thus call the callback.
        callback({
          data: new Blob(['Knees weak, arms are heavy.'], {
            type: 'text/plain',
          }),
        });
      });

      try {
        await aos.encodeAndSendAudioOnDataAvailable(
          'r353rv3d1d',
          recorderStub,
          'his.palms.are.sweaty',
        );
        fail('encodeAndSendAudioOnDataAvailable should have failed');
      } catch ({ message }) {
        expect(message).toBe(
          'Websocket server has received and rejected the call.',
        );

        expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
          'his.palms.are.sweaty',
          {
            args: [
              'r353rv3d1d',
              "There's vomit on his sweater already, mom's spaghetti",
              'base64',
            ],
          },
        );
      }
    });
  });

  describe('prepareServerForAudio', () => {
    it('should broadcast when the websocket server has successfully been prepped and resolve in the reserved ID', async () => {
      const {
        recorderStub,
        makeWebsocketCallSpy,
        broadcasterSpy,
      } = setupStubsSimple();

      recorderStub.getAudioSpecs.and.returnValue({
        audioFormat: 'audio/ogg',
        audioParameters: {
          bitrate: 9001,
        },
      });
      makeWebsocketCallSpy.and.returnValue(
        Promise.resolve('Websocket server has received and handled the call.'),
      );

      const result = await aos.prepareServerForAudio(
        'r353rv3d1d',
        recorderStub,
        'write.this.down.kiddo',
      );

      expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
        'write.this.down.kiddo',
        {
          args: ['r353rv3d1d', 'audio/ogg'],
          kwargs: {
            bitrate: 9001,
          },
        },
      );

      expect(result).toEqual('r353rv3d1d');

      expect(broadcasterSpy).toHaveBeenCalledWith(
        'websocketserverreadyforaudio',
      );
    });

    it('should reject if the `makeWebsocketCall` rejected', async () => {
      const { recorderStub, makeWebsocketCallSpy } = setupStubsSimple();

      recorderStub.getAudioSpecs.and.returnValue({
        audioFormat: 'audio/ogg',
        audioParameters: {
          bitrate: 9001,
        },
      });
      makeWebsocketCallSpy.and.returnValue(
        Promise.reject(
          new Error('Websocket server has received and rejected the call.'),
        ),
      );

      try {
        await aos.prepareServerForAudio(
          'r353rv3d1d',
          recorderStub,
          'write.this.down.kiddo',
        );
        fail('prepareServerForAudio should have failed');
      } catch (error) {
        expect(makeWebsocketCallSpy).toHaveBeenCalledWith(
          'write.this.down.kiddo',
          {
            args: ['r353rv3d1d', 'audio/ogg'],
            kwargs: {
              bitrate: 9001,
            },
          },
        );
      }
    });
  });
});
<|endoftext|>


<commit_before>
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import moment from 'moment';
import Markdown from 'markdown-to-jsx';
import { getItems, getItem } from '../../actions';
import Loading from '../../components/Loading';
import Stored from '../../components/Stored';
import Button from '../../components/Button';
import AddIcon from '../../icons/Add';
import FormAdd from './FormAdd';
import FormEdit from './FormEdit';
import PaymentPay from '../payment/PaymentPay';
import SessionSection from '../session/SessionSection';
import { calculateTotal } from './FormUtils';

const LABEL = {
  Register: 'Registered',
  'Sign Up': 'Signed up',
  Submit: 'Submitted',
  Subscribe: 'Subscribed',
};

const LOADING = 'loading';
const AUTHENTICATION_NEEDED = 'authenticationNeeded';
const SESSION = 'session';
const ADDING = 'adding';
const EDITING = 'editing';
const PAYING = 'paying';
const SUMMARY = 'summary';

const FormItem = (props) => {
  const { className, distinguish, item: form, onClick, verb } = props;
  const classNames = ['item__container', className];
  const timestamp = moment(form.modified).format('MMM Do YYYY');
  let message;
  if (distinguish) {
    message = `${verb} for ${form.name} ${timestamp}`;
  } else {
    message = `${verb} ${timestamp}`;
  }

  return (

        <button className="button button-plain" onClick={onClick}>
          {message}
        </button>

  );
};

FormItem.propTypes = {
  className: PropTypes.string,
  distinguish: PropTypes.bool,
  item: PropTypes.object.isRequired,
  onClick: PropTypes.func.isRequired,
  verb: PropTypes.string.isRequired,
};

FormItem.defaultProps = {
  className: undefined,
  distinguish: false,
};

class FormSection extends Component {

  constructor(props) {
    super(props);
    this._nextState = this._nextState.bind(this);
    this._onCancel = this._onCancel.bind(this);
    this._onDone = this._onDone.bind(this);
    // this._onResize = this._onResize.bind(this);
    // this._layout = this._layout.bind(this);
    this.state = { forms: [], state: LOADING };
  }

  componentDidMount() {
    this._load(this.props);
    // window.addEventListener('resize', this._onResize);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.formTemplateId !== this.props.formTemplateId) {
      this._load(nextProps);
    }
    if ((nextProps.session && !this.props.session) ||
      (!nextProps.session && this.props.session)) {
      this._resetState(nextProps);
    }
  }

  // componentDidUpdate() {
  //   if (this.state.layoutNeeded) {
  //     this.setState({ layoutNeeded: undefined });
  //     this._onResize();
  //   }
  // }

  // componentWillUnmount() {
  //   window.removeEventListener('resize', this._onResize);
  //   clearTimeout(this._layoutTimer);
  // }

  // _onResize() {
  //   clearTimeout(this._layoutTimer);
  //   this._layoutTimer = setTimeout(this._layout, 300);
  // }

  _load(props) {
    const { formTemplate, formTemplateId } = props;
    if (formTemplateId && !formTemplate) {
      getItem('form-templates', formTemplateId._id || formTemplateId)
      .then(formTemplateResponse => this._formTemplateReady(formTemplateResponse))
      .catch(error => console.error('!!! FormSummary formTemplate catch', error));
    } else if (formTemplate) {
      this._formTemplateReady(formTemplate);
    }
  }

  _formTemplateReady(formTemplate) {
    const { session } = this.props;
    this.setState({ formTemplate });
    if (!session && formTemplate.authenticate) {
      this.setState({ state: AUTHENTICATION_NEEDED });
    } else {
      this._loadForms(formTemplate);
    }
  }

  _loadForms(formTemplate) {
    const { session } = this.props;
    if (session) {
      getItems('forms', {
        filter: { formTemplateId: formTemplate._id, userId: session.userId },
        populate: true,
      })
      .then((forms) => {
        let paymentFormId;
        if (formTemplate.payable) {
          // see if any require payment
          forms.forEach((form) => {
            const formTotal = calculateTotal(formTemplate, form);
            let paymentTotal = 0;
            (form.paymentIds || []).forEach((payment) => {
              paymentTotal += payment.amount;
            });
            if (formTotal > paymentTotal) {
              form.needsPayment = true;
              if (!paymentFormId) {
                paymentFormId = form._id;
              }
            }
          });
        }
        this.setState({ forms, paymentFormId });
      })
      .catch(error => console.error('!!! FormSummary forms catch', error));
    }
  }

  // _layout() {
  //   const contents = this.refs.contents;
  //   const height = contents.offsetHeight;
  //   if (this.state.height !== height) {
  //     this.setState({ height });
  //     // unset height after a while, this allows forms to be dynamic
  //     clearTimeout(this._layoutTimer);
  //     this._layoutTimer = setTimeout(
  //       () => this.setState({ height: undefined }), 600);
  //   }
  // }

  _edit(id) {
    return () => {
      this.setState({ editId: id, state: EDITING });
    };
  }

  _onCancel() {
    this.setState({
      adding: undefined,
      editId: undefined,
      paymentFormId: undefined,
      // layoutNeeded: true,
    });
  }

  _resetState(props) {
    const { session } = props;
    const { formTemplate, forms, editId, paymentFormId } = this.state;
    let state;
    if (!formTemplate || !forms) {
      state = LOADING;
    } else if (!session && formTemplate.authenticate) {
      state = AUTHENTICATION_NEEDED;
    } else if (forms.length === 0) {
      state = ADDING;
    } else if (editId) {
      state = EDITING;
    } else if (paymentFormId) {
      state = PAYING;
    } else {
      state = SUMMARY;
    }
    this.setState({ state });
  }

  _nextState(state) {
    return () => {
      this.setState({ state, editId: undefined, paymentFormId: undefined });
    };
  }

  _onDone() {
    this._resetState(this.props);
    this._loadForms();
  }

  render() {
    const { className, formTemplateId, session } = this.props;
    const {
      formTemplate, forms, editId, height, paymentFormId, state,
    } = this.state;

    const classes = ['form-summary__container'];
    if (className) {
      classes.push(className);
    }

    let formTemplateLink;
    if (session && session.administrator && formTemplate) {
      const formTemplatePath = `/form-templates/${formTemplate._id}`;
      formTemplateLink = (
        <Link className="form-summary__template" to={formTemplatePath}>
          {formTemplate.name}
        </Link>
      );
    }

    let contents;
    switch (state) {

      case LOADING: {
        contents = <Loading />;
        break;
      }

      case AUTHENTICATION_NEEDED: {
        contents = (

{formTemplate.name}

You must sign in to fill out this form.

            <Button label="Sign In"
              onClick={this._nextState(SESSION)} />

        );
        break;
      }

      case SESSION: {
        contents = (
          <SessionSection onCancel={this._nextState(AUTHENTICATION_NEEDED)} />
        );
        break;
      }

      case ADDING: {
        const onCancel = forms.length > 0 ? this._nextState(SUMMARY) : undefined;
        contents = (
          <FormAdd formTemplateId={formTemplateId._id || formTemplateId}
            full={false} inline={true}
            onDone={this._onDone} onCancel={onCancel} />
        );
        break;
      }

      case EDITING: {
        contents = (
          <FormEdit id={editId} full={false} inline={true}
            onDone={this._onDone} onCancel={this._nextState(SUMMARY)} />
        );
        break;
      }

      case PAYING: {
        contents = (
          <PaymentPay formId={paymentFormId} formTemplateId={formTemplate._id}
            onDone={this._onDone} onCancel={this._nextState(SUMMARY)} />
        );
        break;
      }

      case SUMMARY: {
        const items = forms.map(form => (

            <FormItem item={form} onClick={this._edit(form._id)}
              verb={LABEL[formTemplate.submitLabel] || LABEL.Submit}
              distinguish={forms.length > 1} />

        ));

        contents = (

            <Button className="form-summary__add" plain={true}
              icon={<AddIcon />} onClick={this._nextState(ADDING)} />

              <Markdown>
                {formTemplate.postSubmitMessage || `## ${formTemplate.name}`}
              </Markdown>

              {items}

        );
        break;
      }

      default:
        contents = <Loading />;
    }

    return (

          {contents}

        {formTemplateLink}

    );
  }
}

FormSection.propTypes = {
  className: PropTypes.string,
  formTemplateId: PropTypes.oneOfType([
    PropTypes.string, PropTypes.object]),
  session: PropTypes.shape({
    administrator: PropTypes.bool,
    administratorDomainId: PropTypes.string,
    userId: PropTypes.string,
  }),
};

FormSection.defaultProps = {
  className: undefined,
  formTemplateId: undefined,
  session: undefined,
};

const select = state => ({
  session: state.session,
});

export default Stored(FormSection, select);
<commit_msg>Fixed an issue with FormSection remaining in Loading.<commit_after>
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import moment from 'moment';
import Markdown from 'markdown-to-jsx';
import { getItems, getItem } from '../../actions';
import Loading from '../../components/Loading';
import Stored from '../../components/Stored';
import Button from '../../components/Button';
import AddIcon from '../../icons/Add';
import FormAdd from './FormAdd';
import FormEdit from './FormEdit';
import PaymentPay from '../payment/PaymentPay';
import SessionSection from '../session/SessionSection';
import { calculateTotal } from './FormUtils';

const LABEL = {
  Register: 'Registered',
  'Sign Up': 'Signed up',
  Submit: 'Submitted',
  Subscribe: 'Subscribed',
};

const LOADING = 'loading';
const AUTHENTICATION_NEEDED = 'authenticationNeeded';
const SESSION = 'session';
const ADDING = 'adding';
const EDITING = 'editing';
const PAYING = 'paying';
const SUMMARY = 'summary';

const FormItem = (props) => {
  const { className, distinguish, item: form, onClick, verb } = props;
  const classNames = ['item__container', className];
  const timestamp = moment(form.modified).format('MMM Do YYYY');
  let message;
  if (distinguish) {
    message = `${verb} for ${form.name} ${timestamp}`;
  } else {
    message = `${verb} ${timestamp}`;
  }

  return (

        <button className="button button-plain" onClick={onClick}>
          {message}
        </button>

  );
};

FormItem.propTypes = {
  className: PropTypes.string,
  distinguish: PropTypes.bool,
  item: PropTypes.object.isRequired,
  onClick: PropTypes.func.isRequired,
  verb: PropTypes.string.isRequired,
};

FormItem.defaultProps = {
  className: undefined,
  distinguish: false,
};

class FormSection extends Component {

  constructor(props) {
    super(props);
    this._nextState = this._nextState.bind(this);
    this._onCancel = this._onCancel.bind(this);
    this._onDone = this._onDone.bind(this);
    // this._onResize = this._onResize.bind(this);
    // this._layout = this._layout.bind(this);
    this.state = { forms: [], state: LOADING };
  }

  componentDidMount() {
    this._load(this.props);
    // window.addEventListener('resize', this._onResize);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.formTemplateId !== this.props.formTemplateId) {
      this._load(nextProps);
    }
    if ((nextProps.session && !this.props.session) ||
      (!nextProps.session && this.props.session)) {
      this._resetState(nextProps);
    }
  }

  // componentDidUpdate() {
  //   if (this.state.layoutNeeded) {
  //     this.setState({ layoutNeeded: undefined });
  //     this._onResize();
  //   }
  // }

  // componentWillUnmount() {
  //   window.removeEventListener('resize', this._onResize);
  //   clearTimeout(this._layoutTimer);
  // }

  // _onResize() {
  //   clearTimeout(this._layoutTimer);
  //   this._layoutTimer = setTimeout(this._layout, 300);
  // }

  _load(props) {
    const { formTemplate, formTemplateId } = props;
    if (formTemplateId && !formTemplate) {
      getItem('form-templates', formTemplateId._id || formTemplateId)
      .then(formTemplateResponse => this._formTemplateReady(formTemplateResponse))
      .catch(error => console.error('!!! FormSummary formTemplate catch', error));
    } else if (formTemplate) {
      this._formTemplateReady(formTemplate);
    }
  }

  _formTemplateReady(formTemplate) {
    const { session } = this.props;
    this.setState({ formTemplate });
    if (!session && formTemplate.authenticate) {
      this.setState({ state: AUTHENTICATION_NEEDED });
    } else {
      this._loadForms(formTemplate);
    }
  }

  _loadForms(formTemplate) {
    const { session } = this.props;
    if (session) {
      getItems('forms', {
        filter: { formTemplateId: formTemplate._id, userId: session.userId },
        populate: true,
      })
      .then((forms) => {
        let paymentFormId;
        if (formTemplate.payable) {
          // see if any require payment
          forms.forEach((form) => {
            const formTotal = calculateTotal(formTemplate, form);
            let paymentTotal = 0;
            (form.paymentIds || []).forEach((payment) => {
              paymentTotal += payment.amount;
            });
            if (formTotal > paymentTotal) {
              form.needsPayment = true;
              if (!paymentFormId) {
                paymentFormId = form._id;
              }
            }
          });
        }
        this.setState({ forms, paymentFormId },
          () => this._resetState(this.props));
      })
      .catch(error => console.error('!!! FormSummary forms catch', error));
    }
  }

  // _layout() {
  //   const contents = this.refs.contents;
  //   const height = contents.offsetHeight;
  //   if (this.state.height !== height) {
  //     this.setState({ height });
  //     // unset height after a while, this allows forms to be dynamic
  //     clearTimeout(this._layoutTimer);
  //     this._layoutTimer = setTimeout(
  //       () => this.setState({ height: undefined }), 600);
  //   }
  // }

  _edit(id) {
    return () => {
      this.setState({ editId: id, state: EDITING });
    };
  }

  _onCancel() {
    this.setState({
      adding: undefined,
      editId: undefined,
      paymentFormId: undefined,
      // layoutNeeded: true,
    });
  }

  _resetState(props) {
    const { session } = props;
    const { formTemplate, forms, editId, paymentFormId } = this.state;
    let state;
    if (!formTemplate || !forms) {
      state = LOADING;
    } else if (!session && formTemplate.authenticate) {
      state = AUTHENTICATION_NEEDED;
    } else if (forms.length === 0) {
      state = ADDING;
    } else if (editId) {
      state = EDITING;
    } else if (paymentFormId) {
      state = PAYING;
    } else {
      state = SUMMARY;
    }
    this.setState({ state });
  }

  _nextState(state) {
    return () => {
      this.setState({ state, editId: undefined, paymentFormId: undefined });
    };
  }

  _onDone() {
    this._resetState(this.props);
    this._loadForms();
  }

  render() {
    const { className, formTemplateId, session } = this.props;
    const {
      formTemplate, forms, editId, height, paymentFormId, state,
    } = this.state;

    const classes = ['form-summary__container'];
    if (className) {
      classes.push(className);
    }

    let formTemplateLink;
    if (session && session.administrator && formTemplate) {
      const formTemplatePath = `/form-templates/${formTemplate._id}`;
      formTemplateLink = (
        <Link className="form-summary__template" to={formTemplatePath}>
          {formTemplate.name}
        </Link>
      );
    }

    let contents;
    switch (state) {

      case LOADING: {
        contents = <Loading />;
        break;
      }

      case AUTHENTICATION_NEEDED: {
        contents = (

{formTemplate.name}

You must sign in to fill out this form.

            <Button label="Sign In"
              onClick={this._nextState(SESSION)} />

        );
        break;
      }

      case SESSION: {
        contents = (
          <SessionSection onCancel={this._nextState(AUTHENTICATION_NEEDED)} />
        );
        break;
      }

      case ADDING: {
        const onCancel = forms.length > 0 ? this._nextState(SUMMARY) : undefined;
        contents = (
          <FormAdd formTemplateId={formTemplateId._id || formTemplateId}
            full={false} inline={true}
            onDone={this._onDone} onCancel={onCancel} />
        );
        break;
      }

      case EDITING: {
        contents = (
          <FormEdit id={editId} full={false} inline={true}
            onDone={this._onDone} onCancel={this._nextState(SUMMARY)} />
        );
        break;
      }

      case PAYING: {
        contents = (
          <PaymentPay formId={paymentFormId} formTemplateId={formTemplate._id}
            onDone={this._onDone} onCancel={this._nextState(SUMMARY)} />
        );
        break;
      }

      case SUMMARY: {
        const items = forms.map(form => (

            <FormItem item={form} onClick={this._edit(form._id)}
              verb={LABEL[formTemplate.submitLabel] || LABEL.Submit}
              distinguish={forms.length > 1} />

        ));

        contents = (

            <Button className="form-summary__add" plain={true}
              icon={<AddIcon />} onClick={this._nextState(ADDING)} />

              <Markdown>
                {formTemplate.postSubmitMessage || `## ${formTemplate.name}`}
              </Markdown>

              {items}

        );
        break;
      }

      default:
        contents = <Loading />;
    }

    return (

          {contents}

        {formTemplateLink}

    );
  }
}

FormSection.propTypes = {
  className: PropTypes.string,
  formTemplateId: PropTypes.oneOfType([
    PropTypes.string, PropTypes.object]),
  session: PropTypes.shape({
    administrator: PropTypes.bool,
    administratorDomainId: PropTypes.string,
    userId: PropTypes.string,
  }),
};

FormSection.defaultProps = {
  className: undefined,
  formTemplateId: undefined,
  session: undefined,
};

const select = state => ({
  session: state.session,
});

export default Stored(FormSection, select);
<|endoftext|>


<commit_before>const test = require('tape');
const mockery = require('mockery');
const mocks = require('./mocks');

mockery.registerAllowable('./index.js');
mockery.registerAllowable('../lib/command/development.js');
mockery.registerMock('../../package.json', {
	name: 'testname',
	version: '1.3.4'
});
mockery.registerMock('../../change-log.json', {
	'1.3.4': [
		'change 2',
		'change 1'
	],
	'1.3.3': [
		'fix 2',
		'fix 1'
	]
});
mockery.enable();

const dev = require('../lib/command/development.js');

mockery.disable();
mockery.deregisterAll();

const callChanges = dev.changes.fn.bind(mocks.commands);
const callVersion = dev.version.fn.bind(mocks.commands);

test('development.changes', t => {
	t.deepEqual(dev.changes.fn.options, {
		argCount: 1,
		argSplit: ' ',
		description: 'Get the update notes from the author (defaults to the current version).',
		passRecord: false,
		signatures: ['@BOTNAME changes [version]']
	}, 'verify options');

	t.equal(dev.changes.name, 'changes', 'verify Commands method name');

	t.test('no version argument', st => {
		callChanges(mocks.createMessage(null, 'bill32')).then(result => {
			st.deepEqual(result, {
				sentMsg: [
					'@bill32, Change Log for `testname`, version **1.3.4**.',
					'change 2',
					'change 1'
				].join('\n')
			}, 'sent message about current version');

			st.end();
		}, error => { st.fail(error); st.end(); });
	});

	t.test('valid version argument', st => {
		callChanges(mocks.createMessage(null, 'greg14'), '1.3.3').then(result => {
			st.deepEqual(result, {
				sentMsg: [
					'@greg14, Change Log for `testname`, version **1.3.3**.',
					'fix 2',
					'fix 1'
				].join('\n')
			}, 'sent message about specifed version');

			st.end();
		}, error => { st.fail(error); st.end(); });
	});

	t.test('invalid version argument', st => {
		callChanges(mocks.createMessage(null, 'jack81'), '1.3.5').then(result => {
			st.notOk(result, 'no response without error');
			st.end();
		}, error => { st.fail(error); st.end(); });
	});

	t.end();
});

test('development.version', t => {
	t.deepEqual(dev.version.fn.options, {
		argCount: 0,
		argSplit: ' ',
		description: 'Replies the current blitzbot version.',
		passRecord: false,
		signatures: ['@BOTNAME version']
	}, 'verify options');

	t.equal(dev.changes.name, 'changes', 'verify Commands method name');

	t.test('valid call', st => {
		callVersion(mocks.createMessage(null, 'joe65')).then(result => {
			st.deepEqual(result, {
				sentMsg: '@joe65, testname version 1.3.4, written by <@86558039594774528>'
			}, 'valid response.');

			st.end();
		}, error => { st.fail(error); st.end(); });
	});

	t.end();
});
<commit_msg>Use async patterns to test development commands.<commit_after>const test = require('tape');
const mockery = require('mockery');
const {autoEndTest} = require('./.utility.js');
const mocks = require('./mocks');

mockery.registerAllowable('./index.js');
mockery.registerAllowable('../lib/command/development.js');
mockery.registerMock('../../package.json', {
	name: 'testname',
	version: '1.3.4'
});
mockery.registerMock('../../change-log.json', {
	'1.3.4': [
		'change 2',
		'change 1'
	],
	'1.3.3': [
		'fix 2',
		'fix 1'
	]
});
mockery.enable();

const dev = require('../lib/command/development.js');

mockery.disable();
mockery.deregisterAll();

const callChanges = dev.changes.fn.bind(mocks.commands);
const callVersion = dev.version.fn.bind(mocks.commands);

test('development.changes', t => {
	t.deepEqual(dev.changes.fn.options, {
		argCount: 1,
		argSplit: ' ',
		description: 'Get the update notes from the author (defaults to the current version).',
		passRecord: false,
		signatures: ['@BOTNAME changes [version]']
	}, 'verify options');

	t.equal(dev.changes.name, 'changes', 'verify Commands method name');

	t.test('no version argument', autoEndTest(async st => {
		const result = await callChanges(mocks.createMessage(null, 'bill32'));

		st.deepEqual(result, {
			sentMsg: [
				'@bill32, Change Log for `testname`, version **1.3.4**.',
				'change 2',
				'change 1'
			].join('\n')
		}, 'sent message about current version');
	}));

	t.test('valid version argument', autoEndTest(async st => {
		const result = await callChanges(mocks.createMessage(null, 'greg14'), '1.3.3');

		st.deepEqual(result, {
			sentMsg: [
				'@greg14, Change Log for `testname`, version **1.3.3**.',
				'fix 2',
				'fix 1'
			].join('\n')
		}, 'sent message about specifed version');
	}));

	t.test('invalid version argument', autoEndTest(async st => {
		const result = await callChanges(mocks.createMessage(null, 'jack81'), '1.3.5');

		st.notOk(result, 'no response without error');
	}));

	t.end();
});

test('development.version', t => {
	t.deepEqual(dev.version.fn.options, {
		argCount: 0,
		argSplit: ' ',
		description: 'Replies the current blitzbot version.',
		passRecord: false,
		signatures: ['@BOTNAME version']
	}, 'verify options');

	t.equal(dev.changes.name, 'changes', 'verify Commands method name');

	t.test('valid call', autoEndTest(async st => {
		const result = await callVersion(mocks.createMessage(null, 'joe65'));

		st.deepEqual(result, {
			sentMsg: '@joe65, testname version 1.3.4, written by <@86558039594774528>'
		}, 'valid response.');
	}));

	t.end();
});
<|endoftext|>


<commit_before>var assert = require('assert');

var _ = require('underscore');
var PSQL = require('cartodb-psql');
var step = require('step');

var RendererParams = require('../renderers/renderer_params');
var Timer = require('../stats/timer');
var SubstitutionTokens = require('../utils/substitution_tokens');

function AttributesBackend() {
}

module.exports = AttributesBackend;

/// Gets attributes for a given layer feature
//
/// Calls req2params, then expects parameters:
///
/// * token - MapConfig identifier
/// * layer - Layer number
/// * fid   - Feature identifier
///
/// The referenced layer must have been configured
/// to allow for attributes fetching.
/// See https://github.com/CartoDB/Windshaft/wiki/MapConfig-1.1.0
///
/// @param testMode if true generates a call returning requested
///                 columns plus the fid column of the first record
///                 it is only meant to check validity of configuration
///
AttributesBackend.prototype.getFeatureAttributes = function (mapConfigProvider, params, testMode, callback) {
    var timer = new Timer();

    var mapConfig;
    var fid_col;
    step(
        function getMapConfig() {
            mapConfigProvider.getMapConfig(this);
        },
        function getPGClient(err, _mapConfig) {
            assert.ifError(err);

            mapConfig = _mapConfig;

            var dbParams = RendererParams.dbParamsFromReqParams(params);
            _.extend(dbParams, mapConfig.getLayerDatasource(params.layer));
            return new PSQL(dbParams);
        },
        function getAttributes(err, pg) {
            timer.start('getAttributes');

            assert.ifError(err);

            var layer = mapConfig.getLayer(params.layer);
            if ( ! layer ) {
                throw new Error("Map " + params.token + " has no layer number " + params.layer);
            }
            var attributes = layer.options.attributes;
            if ( ! attributes ) {
                throw new Error("Layer " + params.layer + " has no exposed attributes");
            }

            // NOTE: we're assuming that the presence of "attributes"
            //       means it is well-formed (should be checked at
            //       MapConfig construction/validation time).

            fid_col = attributes.id;
            var att_cols = attributes.columns;

            // prepare columns with double quotes
            var quoted_att_cols = _.map(att_cols, function(n) {
                return pg.quoteIdentifier(n);
            }).join(',');

            if ( testMode ) {
                quoted_att_cols += ',' + pg.quoteIdentifier(fid_col);
            }

            var layerSql = SubstitutionTokens.replace(layer.options.sql, {
                bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
                scale_denominator: '0',
                pixel_width: '1',
                pixel_height: '1'
            });

            var sql = 'select ' + quoted_att_cols + ' from ( ' + layerSql + ' ) as _windshaft_subquery ';
            if ( ! testMode ) {
                sql += ' WHERE ' + pg.quoteIdentifier(fid_col) + ' = ' + params.fid;
            } else {
                sql += ' LIMIT 1';
            }

            pg.query(sql, this, true); // use read-only transaction
        },
        function formatAttributes(err, data) {
            timer.end('getAttributes');
            assert.ifError(err);

            if ( data.rows.length !== 1 ) {
                if ( testMode ) {
                    return null;
                }
                else {
                    var rowsLengthError = new Error("Multiple features (" + data.rows.length + ") identified by '" +
                            fid_col + "' = " + params.fid + " in layer " + params.layer);
                    if ( ! data.rows.length ) {
                        rowsLengthError.http_status = 404;
                    }
                    throw rowsLengthError;
                }
            }
            return data.rows[0];
        },
        function returnCallback(err, attributes) {
            return callback(err, attributes, timer.getTimes());
        }
    );
};
<commit_msg>AttributesBackend: Use DISTINCT to join points where all the requested attributes are the same<commit_after>var assert = require('assert');

var _ = require('underscore');
var PSQL = require('cartodb-psql');
var step = require('step');

var RendererParams = require('../renderers/renderer_params');
var Timer = require('../stats/timer');
var SubstitutionTokens = require('../utils/substitution_tokens');

function AttributesBackend() {
}

module.exports = AttributesBackend;

/// Gets attributes for a given layer feature
//
/// Calls req2params, then expects parameters:
///
/// * token - MapConfig identifier
/// * layer - Layer number
/// * fid   - Feature identifier
///
/// The referenced layer must have been configured
/// to allow for attributes fetching.
/// See https://github.com/CartoDB/Windshaft/wiki/MapConfig-1.1.0
///
/// @param testMode if true generates a call returning requested
///                 columns plus the fid column of the first record
///                 it is only meant to check validity of configuration
///
AttributesBackend.prototype.getFeatureAttributes = function (mapConfigProvider, params, testMode, callback) {
    var timer = new Timer();

    var mapConfig;
    var fid_col;
    step(
        function getMapConfig() {
            mapConfigProvider.getMapConfig(this);
        },
        function getPGClient(err, _mapConfig) {
            assert.ifError(err);

            mapConfig = _mapConfig;

            var dbParams = RendererParams.dbParamsFromReqParams(params);
            _.extend(dbParams, mapConfig.getLayerDatasource(params.layer));
            return new PSQL(dbParams);
        },
        function getAttributes(err, pg) {
            timer.start('getAttributes');

            assert.ifError(err);

            var layer = mapConfig.getLayer(params.layer);
            if ( ! layer ) {
                throw new Error("Map " + params.token + " has no layer number " + params.layer);
            }
            var attributes = layer.options.attributes;
            if ( ! attributes ) {
                throw new Error("Layer " + params.layer + " has no exposed attributes");
            }

            // NOTE: we're assuming that the presence of "attributes"
            //       means it is well-formed (should be checked at
            //       MapConfig construction/validation time).

            fid_col = attributes.id;
            var att_cols = attributes.columns;

            // prepare columns with double quotes
            var quoted_att_cols = _.map(att_cols, function(n) {
                return pg.quoteIdentifier(n);
            }).join(',');

            if ( testMode ) {
                quoted_att_cols += ',' + pg.quoteIdentifier(fid_col);
            }

            var layerSql = SubstitutionTokens.replace(layer.options.sql, {
                bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
                scale_denominator: '0',
                pixel_width: '1',
                pixel_height: '1'
            });

            var sql = 'SELECT DISTINCT ' + quoted_att_cols + ' FROM ( ' + layerSql + ' ) AS _windshaft_subquery';
            if ( ! testMode ) {
                sql += ' WHERE ' + pg.quoteIdentifier(fid_col) + ' = ' + params.fid;
            } else {
                sql += ' LIMIT 1';
            }

            pg.query(sql, this, true); // use read-only transaction
        },
        function formatAttributes(err, data) {
            timer.end('getAttributes');
            assert.ifError(err);

            if ( data.rows.length !== 1 ) {
                if ( testMode ) {
                    return null;
                }
                else {
                    var rowsLengthError = new Error("Multiple features (" + data.rows.length + ") identified by '" +
                            fid_col + "' = " + params.fid + " in layer " + params.layer);
                    if ( ! data.rows.length ) {
                        rowsLengthError.http_status = 404;
                    }
                    throw rowsLengthError;
                }
            }
            return data.rows[0];
        },
        function returnCallback(err, attributes) {
            return callback(err, attributes, timer.getTimes());
        }
    );
};
<|endoftext|>