<commit_before>/*global setImmediate:true, setTimeout*/

if (typeof setImmediate === 'undefined') {
    setImmediate = process.nextTick;
}

var BufferedStream = require('bufferedstream'),
    URL = require('url'),
    http = require('http'),
    stream = require('stream'),
    _ = require('underscore'),
    FormData = require('form-data'),
    messy = require('messy'),
    metadataPropertyNames = ['strictAsync', 'errorPassedToNext', 'isDestroyed', 'nextCalled'];

module.exports = {
    name: 'unexpected-express',
    installInto: function (expect) {
        expect.installPlugin(require('unexpected-messy'));

        expect.addType({
            name: 'IncomingMessage',
            base: 'object',
            identify: function (obj) {
                return obj && obj.constructor && obj.constructor.name === 'IncomingMessage';
            },
            inspect: function (obj, depth, output) {
                output.text(obj.constructor.name, 'jsFunctionName');
            }
        });

        expect.addAssertion('IncomingMessage', 'to have url satisfying', function (expect, subject, value) {
            this.errorMode = 'nested';
            expect(subject.url, 'to satisfy', value);
        });

        expect.addAssertion('function', 'to yield exchange', function (expect, subject, value, unsupportedDone) {
            // capture the current stack
            var stack;
            try {
                throw new Error('<message>');
            } catch (e) {
                stack = e.stack;
            }

            var that = this;

            if (!subject.handle || !subject.set) {
                // This check is from the lib/application file in express @ 4.10.2.
                // If we get inside here, we have something that is not an express app
                // https://github.com/strongloop/express/blob/661435256384165bb656cb7b6046b4138ca24c9e/lib/application.js#L186
                subject = require('express')().use(subject);
                this.subject = expect.output.clone().text('express middleware');
            } else {
                this.subject = expect.output.clone().text('express app');
            }

            if (unsupportedDone) {
                throw new Error('unexpected-express 6 no longer supports a "done" callback, but returns a promise');
            }

            var context = {},
                nextCalls = [];

            value = _.extend({}, value);
            var requestProperties = typeof value.request === 'string' ? {url: value.request} : _.extend({}, value.request),
                httpRequest = new messy.HttpRequest({
                    method: requestProperties.method,
                    url: requestProperties.url || '/',
                    protocolName: 'HTTP',
                    protocolVersion: requestProperties.httpVersion || '1.1',
                    headers: requestProperties.headers
                });
            httpRequest._body = requestProperties.body;
            delete value.request;
            delete requestProperties.method;
            delete requestProperties.url;
            delete requestProperties.httpVersion;
            delete requestProperties.headers;
            delete requestProperties.body;
            if (typeof httpRequest.url === 'string') {
                var matchMethod = httpRequest.url.match(/^([A-Z]+) ([\s\S]*)$/);
                if (matchMethod) {
                    httpRequest.method = httpRequest.method || matchMethod[1];
                    httpRequest.url = matchMethod[2];
                }
            }
            httpRequest.method = httpRequest.method || 'GET';
            if (/^https?:\/\//.test(httpRequest.url)) {
                var urlObj = URL.parse(httpRequest.url);
                if (!httpRequest.headers.has('Host')) {
                    httpRequest.headers.set('Host', urlObj.host);
                }
                if (urlObj.protocol === 'https:' && typeof requestProperties.https === 'undefined') {
                    requestProperties.https = true;
                }
                httpRequest.url = urlObj.path;
            }
            if (requestProperties.formData) {
                if (httpRequest._body) {
                    throw new Error('unexpected-express: The "body" and "formData" options are not supported together');
                }
                httpRequest._body = new FormData();
                Object.keys(requestProperties.formData).forEach(function (name) {
                    var value = requestProperties.formData[name],
                        options;
                    if (typeof value === 'object') {
                        options = _.extend({}, value);
                        value = options.value;
                        delete options.value;
                        if (options.fileName) {
                            options.filename = options.fileName;
                            delete options.fileName;
                        }
                    }
                    httpRequest._body.append(name, value, options);
                });

                delete requestProperties.formData;
            }

            if (typeof requestProperties.query !== 'undefined') {
                if (typeof requestProperties.query === 'object' && requestProperties.query) {
                    Object.keys(requestProperties.query).forEach(function (parameterName) {
                        var valueOrValues = requestProperties.query[parameterName];
                        (Array.isArray(valueOrValues) ? valueOrValues : [String(valueOrValues)]).forEach(function (value) {
                            httpRequest.url += (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
                                encodeURIComponent(parameterName) + '=' + encodeURIComponent(value);
                        });
                    });
                } else {
                    httpRequest.url += (httpRequest.url.indexOf('?') === -1 ? '?' : '&') + String(requestProperties.query);
                }
                delete requestProperties.query;
            }

            var expectedResponseProperties;

            if (typeof value.response === 'number') {
                expectedResponseProperties = {statusCode: value.response};
            } else if (typeof value.response === 'string' || Buffer.isBuffer(value.response)) {
                expectedResponseProperties = {body: value.response};
            } else {
                expectedResponseProperties = _.extend({}, value.response);
            }
            delete value.response;

            var expectedMetadata = _.extend(
                    {},
                    _.pick(expectedResponseProperties, metadataPropertyNames),
                    _.pick(value, metadataPropertyNames)
                );
            expectedResponseProperties = _.omit(expectedResponseProperties, metadataPropertyNames);

            var missingResponseProperties = Object.keys(expectedResponseProperties).filter(function (key) {
                return messy.HttpResponse.propertyNames.indexOf(key) === -1 && metadataPropertyNames.indexOf(key) === -1 && key !== 'url' && key !== 'locals';
            });
            if (missingResponseProperties.length > 0) {
                throw new Error('Property "' + missingResponseProperties[0] + '" does not exist on the response object.');
            }

            var responseProperties = {},
                requestStream,
                req;

            if (typeof httpRequest._body !== 'undefined') {
                httpRequest.headers.set('Transfer-Encoding', 'chunked');
                if (httpRequest._body.pipe) {
                    requestStream = httpRequest._body;
                    if (requestStream.constructor && requestStream.constructor.name === 'FormData') {
                        if (!httpRequest.headers.has('Content-Type')) {
                            httpRequest.headers.set('Content-Type', 'multipart/form-data; boundary=' + requestStream.getBoundary());
                            // form-data pauses its streams by default for some reason:
                            setImmediate(function () {
                                requestStream.resume();
                            });
                        }
                    }
                    var requestBodyChunks = [];
                    requestStream.on('data', function (chunk) {
                        if (!Buffer.isBuffer(chunk)) {
                            chunk = new Buffer(chunk, 'utf-8');
                        }
                        requestBodyChunks.push(chunk);
                        req.emit('data', chunk);
                    }).on('end', function () {
                        httpRequest._body = Buffer.concat(requestBodyChunks);
                        req.emit('end');
                    }).on('error', function (err) {
                        req.emit('error', err);
                    });
                } else {
                    if (typeof httpRequest._body === 'object' && !Buffer.isBuffer(httpRequest._body)) {
                        if (!httpRequest.headers.has('Content-Type')) {
                            httpRequest.headers.set('Content-Type', 'application/json');
                        }
                        httpRequest._body = JSON.stringify(httpRequest._body);
                    }

                    if (!Buffer.isBuffer(httpRequest._body)) {
                        httpRequest._body = new Buffer(String(httpRequest._body), 'utf-8');
                    }
                    if (!httpRequest.headers.has('Content-Length') && !httpRequest.headers.has('Transfer-Encoding')) {
                        httpRequest.headers.set('Content-Length', String(httpRequest._body.length));
                    }
                    requestStream = new BufferedStream();
                    setImmediate(function emitRequestBodyOnceThereIsAListener() {
                        if (req.listeners('data').length > 0 || req.listeners('end').length > 0) {
                            req.emit('data', httpRequest._body);
                            req.emit('end');
                        } else {
                            setTimeout(emitRequestBodyOnceThereIsAListener, 10);
                        }
                    });
                }
            } else {
                requestStream = new BufferedStream();
                requestStream.destroy = function () {
                    responseProperties.requestDestroyed = true;
                };
                setImmediate(function () {
                    requestStream.emit('end');
                    req.emit('end');
                });
            }

            req = new http.IncomingMessage(requestStream);
            req.httpVersion = httpRequest.protocolVersion;
            if (requestProperties.flowMode) {
                delete requestProperties.flowMode;
                req.resume();
            }
            var matchProtocolVersion = String(httpRequest.protocolVersion).match(/^(\d+)(?:\.(\d+))$/);
            if (matchProtocolVersion) {
                req.httpVersionMajor = parseInt(matchProtocolVersion[1], 10);
                req.httpVersionMinor = matchProtocolVersion[2] ? parseInt(matchProtocolVersion[2], 10) : 0;
            }
            req.connection = req.connection || {};
            req.connection.encrypted = !!requestProperties.https;
            delete requestProperties.https;
            req.connection.remoteAddress = requestProperties.remoteAddress || requestProperties.ip || '127.0.0.1';
            delete requestProperties.ip;
            delete requestProperties.remoteAddress;
            req.headers = {};
            httpRequest.headers.getNames().forEach(function (headerName) {
                if (headerName === 'set-cookie') {
                    req.headers[headerName] = [].concat(httpRequest.headers.valuesByName[headerName]);
                } else {
                    req.headers[headerName] = httpRequest.headers.valuesByName[headerName].join(', ');
                }
            });
            req.method = httpRequest.method;
            req.url = httpRequest.url;
            _.extend(req, requestProperties);

            var res = new http.ServerResponse(req);
            _.extend(res, requestProperties.res); // Allows for specifying eg. res.locals
            delete requestProperties.res;
            res.locals = res.locals || {};

            var rawResponseChunks = [];
            res.assignSocket(new stream.Writable());
            res.connection._write = function (chunk, encoding, cb) {
                rawResponseChunks.push(chunk);
                cb();
            };

            var isAsync = false,
                isDestroyed = false,
                done = false,
                errorPassedToNext;

            setImmediate(function () {
                isAsync = true;
            });

            return expect.promise(function (resolve, reject) {
                ['write', 'end', 'destroy'].forEach(function (methodName) {
                    var orig = res[methodName];
                    res[methodName] = function (chunk, encoding) {
                        var returnValue = orig.apply(this, arguments);
                        isDestroyed = isDestroyed || methodName === 'destroy';
                        if (methodName === 'end' || methodName === 'destroy') {
                            resolve();
                        }
                        // Don't attempt to implement backpressure, since we're buffering the entire response anyway.
                        if (methodName !== 'write') {
                            return returnValue;
                        }
                    };
                });
                subject(req, res, function (err, _req, _res, _next) {
                    nextCalls.push(arguments);
                    if (nextCalls.length > 1) {
                        if (done) {
                            if (err) {
                                throw err;
                            } else {
                                throw new Error('next called more than once');
                            }
                        } else {
                            // Will be reported as a failure later
                            return;
                        }
                    }
                    errorPassedToNext = err;
                    if (typeof err === 'number') {
                        var statusCode = err;
                        err = new Error('' + statusCode);
                        err.statusCode = statusCode;
                    }
                    res.statusCode = err && (err.statusCode || err.status) || 404;
                    resolve();
                });
            }).then(function () {
                _.extend(context, {
                    req: req,
                    res: res,
                    httpRequest: httpRequest,
                    metadata: {
                        strictAsync: isAsync,
                        errorPassedToNext: false,
                        isDestroyed: isDestroyed,
                        nextCalled: nextCalls.length > 0
                    }
                });

                if (errorPassedToNext && errorPassedToNext.statusCode) {
                    res.writeHead(errorPassedToNext.statusCode);
                }

                var httpResponse = context.httpResponse = new messy.HttpResponse(
                    rawResponseChunks.length > 0 ? Buffer.concat(rawResponseChunks) : res._header || undefined
                );
                httpResponse.statusCode = httpResponse.statusCode || res.statusCode;

                if (errorPassedToNext) {
                    context.metadata.errorPassedToNext = errorPassedToNext;
                    if (typeof expectedMetadata.errorPassedToNext !== 'undefined') {
                        if (expectedMetadata.errorPassedToNext === true) {
                            context.metadata.errorPassedToNext = true;
                        } else if (typeof expectedMetadata.errorPassedToNext === 'string') {
                            context.metadata.errorPassedToNext = errorPassedToNext.message;
                        } else {
                            context.metadata.errorPassedToNext = errorPassedToNext;
                        }
                    } else if (typeof errorPassedToNext.statusCode === 'number') {
                        // FIXME
                        if (!httpResponse.headers.get('Content-Type')) {
                            httpResponse.headers.set('Content-Type', 'text/plain');
                            httpResponse.body = errorPassedToNext.stack;
                        }
                    } else {
                        throw errorPassedToNext;
                    }
                }
                return expect(context.metadata, 'to satisfy', expectedMetadata);
            }).then(function () {
                if (expectedResponseProperties.locals) {
                    var expectedLocals = expectedResponseProperties.locals;
                    delete expectedResponseProperties.locals;
                    return expect({ res: { locals: res.locals } }, 'to satisfy', { res: { locals: expectedLocals } });
                }
            }).then(function () {
                if (expectedResponseProperties.url) {
                    var expectedUrl = expectedResponseProperties.url;
                    delete expectedResponseProperties.url;
                    that.errorMode = 'diff';
                    return expect(req, 'to have url satisfying', expectedUrl);
                }
            }).then(function () {
                that.errorMode = 'default';
                return expect(new messy.HttpExchange({
                    request: context.httpRequest,
                    response: context.httpResponse
                }), 'to satisfy', {response: expectedResponseProperties});
            }).then(function () {
                if (nextCalls.length > 1) {
                    throw new Error('next called more than once');
                }
                done = true; // Tell the next function that subsequent calls should cause an exception to be thrown
            });
        });
    }
};

module.exports.messy = messy;
<commit_msg>Removed unused stack capture.<commit_after>/*global setImmediate:true, setTimeout*/

if (typeof setImmediate === 'undefined') {
    setImmediate = process.nextTick;
}

var BufferedStream = require('bufferedstream'),
    URL = require('url'),
    http = require('http'),
    stream = require('stream'),
    _ = require('underscore'),
    FormData = require('form-data'),
    messy = require('messy'),
    metadataPropertyNames = ['strictAsync', 'errorPassedToNext', 'isDestroyed', 'nextCalled'];

module.exports = {
    name: 'unexpected-express',
    installInto: function (expect) {
        expect.installPlugin(require('unexpected-messy'));

        expect.addType({
            name: 'IncomingMessage',
            base: 'object',
            identify: function (obj) {
                return obj && obj.constructor && obj.constructor.name === 'IncomingMessage';
            },
            inspect: function (obj, depth, output) {
                output.text(obj.constructor.name, 'jsFunctionName');
            }
        });

        expect.addAssertion('IncomingMessage', 'to have url satisfying', function (expect, subject, value) {
            this.errorMode = 'nested';
            expect(subject.url, 'to satisfy', value);
        });

        expect.addAssertion('function', 'to yield exchange', function (expect, subject, value, unsupportedDone) {
            var that = this;

            if (!subject.handle || !subject.set) {
                // This check is from the lib/application file in express @ 4.10.2.
                // If we get inside here, we have something that is not an express app
                // https://github.com/strongloop/express/blob/661435256384165bb656cb7b6046b4138ca24c9e/lib/application.js#L186
                subject = require('express')().use(subject);
                this.subject = expect.output.clone().text('express middleware');
            } else {
                this.subject = expect.output.clone().text('express app');
            }

            if (unsupportedDone) {
                throw new Error('unexpected-express 6 no longer supports a "done" callback, but returns a promise');
            }

            var context = {},
                nextCalls = [];

            value = _.extend({}, value);
            var requestProperties = typeof value.request === 'string' ? {url: value.request} : _.extend({}, value.request),
                httpRequest = new messy.HttpRequest({
                    method: requestProperties.method,
                    url: requestProperties.url || '/',
                    protocolName: 'HTTP',
                    protocolVersion: requestProperties.httpVersion || '1.1',
                    headers: requestProperties.headers
                });
            httpRequest._body = requestProperties.body;
            delete value.request;
            delete requestProperties.method;
            delete requestProperties.url;
            delete requestProperties.httpVersion;
            delete requestProperties.headers;
            delete requestProperties.body;
            if (typeof httpRequest.url === 'string') {
                var matchMethod = httpRequest.url.match(/^([A-Z]+) ([\s\S]*)$/);
                if (matchMethod) {
                    httpRequest.method = httpRequest.method || matchMethod[1];
                    httpRequest.url = matchMethod[2];
                }
            }
            httpRequest.method = httpRequest.method || 'GET';
            if (/^https?:\/\//.test(httpRequest.url)) {
                var urlObj = URL.parse(httpRequest.url);
                if (!httpRequest.headers.has('Host')) {
                    httpRequest.headers.set('Host', urlObj.host);
                }
                if (urlObj.protocol === 'https:' && typeof requestProperties.https === 'undefined') {
                    requestProperties.https = true;
                }
                httpRequest.url = urlObj.path;
            }
            if (requestProperties.formData) {
                if (httpRequest._body) {
                    throw new Error('unexpected-express: The "body" and "formData" options are not supported together');
                }
                httpRequest._body = new FormData();
                Object.keys(requestProperties.formData).forEach(function (name) {
                    var value = requestProperties.formData[name],
                        options;
                    if (typeof value === 'object') {
                        options = _.extend({}, value);
                        value = options.value;
                        delete options.value;
                        if (options.fileName) {
                            options.filename = options.fileName;
                            delete options.fileName;
                        }
                    }
                    httpRequest._body.append(name, value, options);
                });

                delete requestProperties.formData;
            }

            if (typeof requestProperties.query !== 'undefined') {
                if (typeof requestProperties.query === 'object' && requestProperties.query) {
                    Object.keys(requestProperties.query).forEach(function (parameterName) {
                        var valueOrValues = requestProperties.query[parameterName];
                        (Array.isArray(valueOrValues) ? valueOrValues : [String(valueOrValues)]).forEach(function (value) {
                            httpRequest.url += (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
                                encodeURIComponent(parameterName) + '=' + encodeURIComponent(value);
                        });
                    });
                } else {
                    httpRequest.url += (httpRequest.url.indexOf('?') === -1 ? '?' : '&') + String(requestProperties.query);
                }
                delete requestProperties.query;
            }

            var expectedResponseProperties;

            if (typeof value.response === 'number') {
                expectedResponseProperties = {statusCode: value.response};
            } else if (typeof value.response === 'string' || Buffer.isBuffer(value.response)) {
                expectedResponseProperties = {body: value.response};
            } else {
                expectedResponseProperties = _.extend({}, value.response);
            }
            delete value.response;

            var expectedMetadata = _.extend(
                    {},
                    _.pick(expectedResponseProperties, metadataPropertyNames),
                    _.pick(value, metadataPropertyNames)
                );
            expectedResponseProperties = _.omit(expectedResponseProperties, metadataPropertyNames);

            var missingResponseProperties = Object.keys(expectedResponseProperties).filter(function (key) {
                return messy.HttpResponse.propertyNames.indexOf(key) === -1 && metadataPropertyNames.indexOf(key) === -1 && key !== 'url' && key !== 'locals';
            });
            if (missingResponseProperties.length > 0) {
                throw new Error('Property "' + missingResponseProperties[0] + '" does not exist on the response object.');
            }

            var responseProperties = {},
                requestStream,
                req;

            if (typeof httpRequest._body !== 'undefined') {
                httpRequest.headers.set('Transfer-Encoding', 'chunked');
                if (httpRequest._body.pipe) {
                    requestStream = httpRequest._body;
                    if (requestStream.constructor && requestStream.constructor.name === 'FormData') {
                        if (!httpRequest.headers.has('Content-Type')) {
                            httpRequest.headers.set('Content-Type', 'multipart/form-data; boundary=' + requestStream.getBoundary());
                            // form-data pauses its streams by default for some reason:
                            setImmediate(function () {
                                requestStream.resume();
                            });
                        }
                    }
                    var requestBodyChunks = [];
                    requestStream.on('data', function (chunk) {
                        if (!Buffer.isBuffer(chunk)) {
                            chunk = new Buffer(chunk, 'utf-8');
                        }
                        requestBodyChunks.push(chunk);
                        req.emit('data', chunk);
                    }).on('end', function () {
                        httpRequest._body = Buffer.concat(requestBodyChunks);
                        req.emit('end');
                    }).on('error', function (err) {
                        req.emit('error', err);
                    });
                } else {
                    if (typeof httpRequest._body === 'object' && !Buffer.isBuffer(httpRequest._body)) {
                        if (!httpRequest.headers.has('Content-Type')) {
                            httpRequest.headers.set('Content-Type', 'application/json');
                        }
                        httpRequest._body = JSON.stringify(httpRequest._body);
                    }

                    if (!Buffer.isBuffer(httpRequest._body)) {
                        httpRequest._body = new Buffer(String(httpRequest._body), 'utf-8');
                    }
                    if (!httpRequest.headers.has('Content-Length') && !httpRequest.headers.has('Transfer-Encoding')) {
                        httpRequest.headers.set('Content-Length', String(httpRequest._body.length));
                    }
                    requestStream = new BufferedStream();
                    setImmediate(function emitRequestBodyOnceThereIsAListener() {
                        if (req.listeners('data').length > 0 || req.listeners('end').length > 0) {
                            req.emit('data', httpRequest._body);
                            req.emit('end');
                        } else {
                            setTimeout(emitRequestBodyOnceThereIsAListener, 10);
                        }
                    });
                }
            } else {
                requestStream = new BufferedStream();
                requestStream.destroy = function () {
                    responseProperties.requestDestroyed = true;
                };
                setImmediate(function () {
                    requestStream.emit('end');
                    req.emit('end');
                });
            }

            req = new http.IncomingMessage(requestStream);
            req.httpVersion = httpRequest.protocolVersion;
            if (requestProperties.flowMode) {
                delete requestProperties.flowMode;
                req.resume();
            }
            var matchProtocolVersion = String(httpRequest.protocolVersion).match(/^(\d+)(?:\.(\d+))$/);
            if (matchProtocolVersion) {
                req.httpVersionMajor = parseInt(matchProtocolVersion[1], 10);
                req.httpVersionMinor = matchProtocolVersion[2] ? parseInt(matchProtocolVersion[2], 10) : 0;
            }
            req.connection = req.connection || {};
            req.connection.encrypted = !!requestProperties.https;
            delete requestProperties.https;
            req.connection.remoteAddress = requestProperties.remoteAddress || requestProperties.ip || '127.0.0.1';
            delete requestProperties.ip;
            delete requestProperties.remoteAddress;
            req.headers = {};
            httpRequest.headers.getNames().forEach(function (headerName) {
                if (headerName === 'set-cookie') {
                    req.headers[headerName] = [].concat(httpRequest.headers.valuesByName[headerName]);
                } else {
                    req.headers[headerName] = httpRequest.headers.valuesByName[headerName].join(', ');
                }
            });
            req.method = httpRequest.method;
            req.url = httpRequest.url;
            _.extend(req, requestProperties);

            var res = new http.ServerResponse(req);
            _.extend(res, requestProperties.res); // Allows for specifying eg. res.locals
            delete requestProperties.res;
            res.locals = res.locals || {};

            var rawResponseChunks = [];
            res.assignSocket(new stream.Writable());
            res.connection._write = function (chunk, encoding, cb) {
                rawResponseChunks.push(chunk);
                cb();
            };

            var isAsync = false,
                isDestroyed = false,
                done = false,
                errorPassedToNext;

            setImmediate(function () {
                isAsync = true;
            });

            return expect.promise(function (resolve, reject) {
                ['write', 'end', 'destroy'].forEach(function (methodName) {
                    var orig = res[methodName];
                    res[methodName] = function (chunk, encoding) {
                        var returnValue = orig.apply(this, arguments);
                        isDestroyed = isDestroyed || methodName === 'destroy';
                        if (methodName === 'end' || methodName === 'destroy') {
                            resolve();
                        }
                        // Don't attempt to implement backpressure, since we're buffering the entire response anyway.
                        if (methodName !== 'write') {
                            return returnValue;
                        }
                    };
                });
                subject(req, res, function (err, _req, _res, _next) {
                    nextCalls.push(arguments);
                    if (nextCalls.length > 1) {
                        if (done) {
                            if (err) {
                                throw err;
                            } else {
                                throw new Error('next called more than once');
                            }
                        } else {
                            // Will be reported as a failure later
                            return;
                        }
                    }
                    errorPassedToNext = err;
                    if (typeof err === 'number') {
                        var statusCode = err;
                        err = new Error('' + statusCode);
                        err.statusCode = statusCode;
                    }
                    res.statusCode = err && (err.statusCode || err.status) || 404;
                    resolve();
                });
            }).then(function () {
                _.extend(context, {
                    req: req,
                    res: res,
                    httpRequest: httpRequest,
                    metadata: {
                        strictAsync: isAsync,
                        errorPassedToNext: false,
                        isDestroyed: isDestroyed,
                        nextCalled: nextCalls.length > 0
                    }
                });

                if (errorPassedToNext && errorPassedToNext.statusCode) {
                    res.writeHead(errorPassedToNext.statusCode);
                }

                var httpResponse = context.httpResponse = new messy.HttpResponse(
                    rawResponseChunks.length > 0 ? Buffer.concat(rawResponseChunks) : res._header || undefined
                );
                httpResponse.statusCode = httpResponse.statusCode || res.statusCode;

                if (errorPassedToNext) {
                    context.metadata.errorPassedToNext = errorPassedToNext;
                    if (typeof expectedMetadata.errorPassedToNext !== 'undefined') {
                        if (expectedMetadata.errorPassedToNext === true) {
                            context.metadata.errorPassedToNext = true;
                        } else if (typeof expectedMetadata.errorPassedToNext === 'string') {
                            context.metadata.errorPassedToNext = errorPassedToNext.message;
                        } else {
                            context.metadata.errorPassedToNext = errorPassedToNext;
                        }
                    } else if (typeof errorPassedToNext.statusCode === 'number') {
                        // FIXME
                        if (!httpResponse.headers.get('Content-Type')) {
                            httpResponse.headers.set('Content-Type', 'text/plain');
                            httpResponse.body = errorPassedToNext.stack;
                        }
                    } else {
                        throw errorPassedToNext;
                    }
                }
                return expect(context.metadata, 'to satisfy', expectedMetadata);
            }).then(function () {
                if (expectedResponseProperties.locals) {
                    var expectedLocals = expectedResponseProperties.locals;
                    delete expectedResponseProperties.locals;
                    return expect({ res: { locals: res.locals } }, 'to satisfy', { res: { locals: expectedLocals } });
                }
            }).then(function () {
                if (expectedResponseProperties.url) {
                    var expectedUrl = expectedResponseProperties.url;
                    delete expectedResponseProperties.url;
                    that.errorMode = 'diff';
                    return expect(req, 'to have url satisfying', expectedUrl);
                }
            }).then(function () {
                that.errorMode = 'default';
                return expect(new messy.HttpExchange({
                    request: context.httpRequest,
                    response: context.httpResponse
                }), 'to satisfy', {response: expectedResponseProperties});
            }).then(function () {
                if (nextCalls.length > 1) {
                    throw new Error('next called more than once');
                }
                done = true; // Tell the next function that subsequent calls should cause an exception to be thrown
            });
        });
    }
};

module.exports.messy = messy;
<|endoftext|>


<commit_before>var BufferedStream = require('bufferedstream'),
    http = require('http'),
    _ = require('underscore');

// Convert a header name to its canonical form, eg. "content-length" => "Content-Length".
var headerNameSpecialCases = {
    'content-md5': 'Content-MD5',
    dnt: 'DNT',
    etag: 'ETag',
    p3p: 'P3P',
    te: 'TE',
    'www-authenticate': 'WWW-Authenticate',
    'x-att-deviceid': 'X-ATT-DeviceId',
    'x-cdn': 'X-CDN',
    'x-ua-compatible': 'X-UA-Compatible',
    'x-xss-protection': 'X-XSS-Protection'
};

function formatHeaderName(headerName) {
    var lowerCasedHeaderName = headerName.toLowerCase();
    if (headerNameSpecialCases.hasOwnProperty(lowerCasedHeaderName)) {
        return headerNameSpecialCases[lowerCasedHeaderName];
    } else {
        // Make sure that the first char and all chars following a dash are upper-case:
        return lowerCasedHeaderName.replace(/(^|-)([a-z])/g, function ($0, optionalLeadingDash, ch) {
            return optionalLeadingDash + ch.toUpperCase();
        });
    }
}

function formatHeaderNames(obj) {
    var resultObj = {};
    Object.keys(obj || {}).forEach(function (headerName) {
        resultObj[formatHeaderName(headerName)] = obj[headerName];
    });
    return resultObj;
}

function lowerCaseHeaderNames(obj) {
    var resultObj = {};
    Object.keys(obj || {}).forEach(function (headerName) {
        resultObj[headerName.toLowerCase()] = obj[headerName];
    });
    return resultObj;
}

module.exports = function (expect) {
    return expect.addAssertion('to be middleware that processes', function (expect, subject, value, done) {
        var requestProperties = value.request,
            expectedResponseProperties = value.response;

        requestProperties.headers = lowerCaseHeaderNames(requestProperties.headers || {});

        var responseProperties = {},
            req = new http.IncomingMessage();

        req.socket = {
            destroy: function () {
                responseProperties.requestDestroyed = true;
            }
        };
        _.extend(req, requestProperties);

        var res = new http.ServerResponse(req);
        res.connection = new BufferedStream();
        res.connection._httpMessage = res;

        var nextCallCount = 0,
            next = function (err, _req, _res, _next) {
                res.nextCallCount += 1;
                res.statusCode = err && err.statusCode || 404;
                done();
            };

        var responseBodyChunks = [];
        ['write', 'end', 'destroy'].forEach(function (methodName) {
            var orig = res[methodName];
            res[methodName] = function (chunk, encoding) {
                var returnValue = orig.apply(this, arguments);
                if (methodName !== 'destroy') {
                    if (encoding) {
                        throw new Error('encoding parameter not supported');
                    }
                    if (chunk) {
                        responseBodyChunks.push(chunk);
                    }
                }
                if (methodName === 'end' || methodName === 'destroy') {
                    var responseProperties = {
                        statusCode: res.statusCode,
                        body: Buffer.concat(responseBodyChunks),
                        isDestroyed: methodName === 'destroy',
                        nextCalled: nextCallCount > 0
                    };
                    var formattedExpectedResponseHeaders = formatHeaderNames(expectedResponseProperties.headers);
                    delete expectedResponseProperties.headers;
                    if (typeof expectedResponseProperties.body === 'string') {
                        responseProperties.body = responseProperties.body.toString('utf-8');
                    } else if (expectedResponseProperties.body && !Buffer.isBuffer(expectedResponseProperties.body) && typeof expectedResponseProperties.body === 'object' && /^application\/json\b/.test(res._headers['content-type'])) {
                        responseProperties.body = JSON.parse(responseProperties.body.toString('utf-8'));
                        formattedExpectedResponseHeaders['Content-Type'] = formattedExpectedResponseHeaders['Content-Type'] || 'application/json; charset=utf-8';
                    }
                    expect(formatHeaderNames(res._headers), 'to have properties', formattedExpectedResponseHeaders);
                    try {
                        expect(responseProperties, 'to have properties', expectedResponseProperties);
                    } catch (err) {
                        return done(err);
                    }
                    done();
                }
                return returnValue;
            };
        });
        subject(req, res, next);
    });
};
<commit_msg>Assert that 'done' is a function.<commit_after>var BufferedStream = require('bufferedstream'),
    http = require('http'),
    _ = require('underscore');

// Convert a header name to its canonical form, eg. "content-length" => "Content-Length".
var headerNameSpecialCases = {
    'content-md5': 'Content-MD5',
    dnt: 'DNT',
    etag: 'ETag',
    p3p: 'P3P',
    te: 'TE',
    'www-authenticate': 'WWW-Authenticate',
    'x-att-deviceid': 'X-ATT-DeviceId',
    'x-cdn': 'X-CDN',
    'x-ua-compatible': 'X-UA-Compatible',
    'x-xss-protection': 'X-XSS-Protection'
};

function formatHeaderName(headerName) {
    var lowerCasedHeaderName = headerName.toLowerCase();
    if (headerNameSpecialCases.hasOwnProperty(lowerCasedHeaderName)) {
        return headerNameSpecialCases[lowerCasedHeaderName];
    } else {
        // Make sure that the first char and all chars following a dash are upper-case:
        return lowerCasedHeaderName.replace(/(^|-)([a-z])/g, function ($0, optionalLeadingDash, ch) {
            return optionalLeadingDash + ch.toUpperCase();
        });
    }
}

function formatHeaderNames(obj) {
    var resultObj = {};
    Object.keys(obj || {}).forEach(function (headerName) {
        resultObj[formatHeaderName(headerName)] = obj[headerName];
    });
    return resultObj;
}

function lowerCaseHeaderNames(obj) {
    var resultObj = {};
    Object.keys(obj || {}).forEach(function (headerName) {
        resultObj[headerName.toLowerCase()] = obj[headerName];
    });
    return resultObj;
}

module.exports = function (expect) {
    return expect.addAssertion('to be middleware that processes', function (expect, subject, value, done) {
        expect(done, 'to be a function');
        var requestProperties = value.request,
            expectedResponseProperties = value.response;

        requestProperties.headers = lowerCaseHeaderNames(requestProperties.headers || {});

        var responseProperties = {},
            req = new http.IncomingMessage();

        req.socket = {
            destroy: function () {
                responseProperties.requestDestroyed = true;
            }
        };
        _.extend(req, requestProperties);

        var res = new http.ServerResponse(req);
        res.connection = new BufferedStream();
        res.connection._httpMessage = res;

        var nextCallCount = 0,
            next = function (err, _req, _res, _next) {
                res.nextCallCount += 1;
                res.statusCode = err && err.statusCode || 404;
                done();
            };

        var responseBodyChunks = [];
        ['write', 'end', 'destroy'].forEach(function (methodName) {
            var orig = res[methodName];
            res[methodName] = function (chunk, encoding) {
                var returnValue = orig.apply(this, arguments);
                if (methodName !== 'destroy') {
                    if (encoding) {
                        throw new Error('encoding parameter not supported');
                    }
                    if (chunk) {
                        responseBodyChunks.push(chunk);
                    }
                }
                if (methodName === 'end' || methodName === 'destroy') {
                    var responseProperties = {
                        statusCode: res.statusCode,
                        body: Buffer.concat(responseBodyChunks),
                        isDestroyed: methodName === 'destroy',
                        nextCalled: nextCallCount > 0
                    };
                    var formattedExpectedResponseHeaders = formatHeaderNames(expectedResponseProperties.headers);
                    delete expectedResponseProperties.headers;
                    if (typeof expectedResponseProperties.body === 'string') {
                        responseProperties.body = responseProperties.body.toString('utf-8');
                    } else if (expectedResponseProperties.body && !Buffer.isBuffer(expectedResponseProperties.body) && typeof expectedResponseProperties.body === 'object' && /^application\/json\b/.test(res._headers['content-type'])) {
                        responseProperties.body = JSON.parse(responseProperties.body.toString('utf-8'));
                        formattedExpectedResponseHeaders['Content-Type'] = formattedExpectedResponseHeaders['Content-Type'] || 'application/json; charset=utf-8';
                    }
                    expect(formatHeaderNames(res._headers), 'to have properties', formattedExpectedResponseHeaders);
                    try {
                        expect(responseProperties, 'to have properties', expectedResponseProperties);
                    } catch (err) {
                        return done(err);
                    }
                    done();
                }
                return returnValue;
            };
        });
        subject(req, res, next);
    });
};
<|endoftext|>


<commit_before>/**
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @fileoverview Defines a FailoverUri which handles multiple URLs and handles
 * requests and failures.
 */

goog.provide('shaka.util.FailoverUri');

goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.util.AjaxRequest');

/**
 * Creates a FailoverUri.
 *
 * @param {Array.<!goog.Uri>} urls
 * @param {number=} opt_startByte The start byte of the data, defaults to 0.
 * @param {?number=} opt_endByte The end byte of the data, null means the end;
 *     defaults to null.
 * @constructor
 */
shaka.util.FailoverUri = function(urls, opt_startByte, opt_endByte) {
  shaka.asserts.assert(urls);
  shaka.asserts.assert(urls.length > 0);

  /** @const {!Array.<!goog.Uri>} */
  this.urls = urls;

  /** @const {number} */
  this.startByte = opt_startByte || 0;

  /** @const {?number} */
  this.endByte = opt_endByte != null ? opt_endByte : null;

  /** @private {?Promise} */
  this.requestPromise_ = null;

  /** @private {shaka.util.AjaxRequest} */
  this.request_ = null;
};

/**
 * Resolves a relative url to the given |baseUrl|.
 *
 * @param {Array.<!goog.Uri>} baseUrl
 * @param {!goog.Uri} url
 * @return {!Array.<!goog.Uri>}
 */
shaka.util.FailoverUri.resolve = function(baseUrl, url) {
  if (!baseUrl || baseUrl.length === 0) {
    return [url];
  }

  return baseUrl.map(function(e) { return e.resolve(url); });
};

/**
 * Gets the data specified by the URLs.
 *
 * @param {shaka.util.AjaxRequest.Parameters=} opt_parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.fetch =
    function(opt_parameters, opt_estimator) {
  if (this.requestPromise_) {
    // A fetch has already completed or is in progress.
    return this.requestPromise_;
  }

  var parameters = opt_parameters || new shaka.util.AjaxRequest.Parameters();
  if (this.startByte || this.endByte) {
    var rangeString =
        this.startByte + '-' + (this.endByte != null ? this.endByte : '');
    parameters.requestHeaders['Range'] = 'bytes=' + rangeString;
  }

  shaka.asserts.assert(!this.request_);
  this.requestPromise_ = this.createRequest_(0, parameters, opt_estimator);
  return this.requestPromise_;
};

/**
 * Aborts fetch() if it is pending.
 */
shaka.util.FailoverUri.prototype.abortFetch = function() {
  if (this.request_) {
    // Set the promise first to indicate to the running promise it is an abort.
    this.requestPromise_ = null;
    this.request_.abort();
    this.request_ = null;
  }
};

/**
 * Creates a request using the given url.  This will add the catch block
 * and will recursively call itself to handle failover.
 *
 * @private
 * @param {number} i
 * @param {shaka.util.AjaxRequest.Parameters} parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.createRequest_ =
    function(i, parameters, opt_estimator) {
  shaka.asserts.assert(i < this.urls.length);
  this.request_ = new shaka.util.AjaxRequest(
      this.urls[i].toString(), parameters);
  if (opt_estimator) {
    this.request_.estimator = opt_estimator;
  }

  var p = this.request_.send().then(shaka.util.TypedBind(this,
      /** @param {!XMLHttpRequest} xhr */
      function(xhr) {
        this.request_ = null;
        return Promise.resolve(xhr.response);
      }));

  p = p.catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (this.requestPromise_ && i + 1 < this.urls.length) {
          shaka.log.info('Trying fallack URL...');
          this.requestPromise_ = this.createRequest_(
              i + 1, parameters, opt_estimator);
          return this.requestPromise_;
        } else {
          this.request_ = null;
          this.requestPromise_ = null;
          return Promise.reject(error);
        }
      }));

  return p;
};

/**
 * Creates a deep-copy of the object.
 *
 * @return {!shaka.util.FailoverUri}
 */
shaka.util.FailoverUri.prototype.clone = function() {
  return new shaka.util.FailoverUri(
      this.urls.map(function(a) { return a.clone(); }),
      this.startByte,
      this.endByte
  );
};

if (!COMPILED) {
  /**
   * Gets the url of the first element.  Should only be used for testing.
   *
   * @return {!string}
   */
  shaka.util.FailoverUri.prototype.toString = function() {
    return this.urls[0].toString();
  };
}
<commit_msg>Fix typo in failover_uri.js<commit_after>/**
 * Copyright 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @fileoverview Defines a FailoverUri which handles multiple URLs and handles
 * requests and failures.
 */

goog.provide('shaka.util.FailoverUri');

goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.util.AjaxRequest');

/**
 * Creates a FailoverUri.
 *
 * @param {Array.<!goog.Uri>} urls
 * @param {number=} opt_startByte The start byte of the data, defaults to 0.
 * @param {?number=} opt_endByte The end byte of the data, null means the end;
 *     defaults to null.
 * @constructor
 */
shaka.util.FailoverUri = function(urls, opt_startByte, opt_endByte) {
  shaka.asserts.assert(urls);
  shaka.asserts.assert(urls.length > 0);

  /** @const {!Array.<!goog.Uri>} */
  this.urls = urls;

  /** @const {number} */
  this.startByte = opt_startByte || 0;

  /** @const {?number} */
  this.endByte = opt_endByte != null ? opt_endByte : null;

  /** @private {?Promise} */
  this.requestPromise_ = null;

  /** @private {shaka.util.AjaxRequest} */
  this.request_ = null;
};

/**
 * Resolves a relative url to the given |baseUrl|.
 *
 * @param {Array.<!goog.Uri>} baseUrl
 * @param {!goog.Uri} url
 * @return {!Array.<!goog.Uri>}
 */
shaka.util.FailoverUri.resolve = function(baseUrl, url) {
  if (!baseUrl || baseUrl.length === 0) {
    return [url];
  }

  return baseUrl.map(function(e) { return e.resolve(url); });
};

/**
 * Gets the data specified by the URLs.
 *
 * @param {shaka.util.AjaxRequest.Parameters=} opt_parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.fetch =
    function(opt_parameters, opt_estimator) {
  if (this.requestPromise_) {
    // A fetch has already completed or is in progress.
    return this.requestPromise_;
  }

  var parameters = opt_parameters || new shaka.util.AjaxRequest.Parameters();
  if (this.startByte || this.endByte) {
    var rangeString =
        this.startByte + '-' + (this.endByte != null ? this.endByte : '');
    parameters.requestHeaders['Range'] = 'bytes=' + rangeString;
  }

  shaka.asserts.assert(!this.request_);
  this.requestPromise_ = this.createRequest_(0, parameters, opt_estimator);
  return this.requestPromise_;
};

/**
 * Aborts fetch() if it is pending.
 */
shaka.util.FailoverUri.prototype.abortFetch = function() {
  if (this.request_) {
    // Set the promise first to indicate to the running promise it is an abort.
    this.requestPromise_ = null;
    this.request_.abort();
    this.request_ = null;
  }
};

/**
 * Creates a request using the given url.  This will add the catch block
 * and will recursively call itself to handle failover.
 *
 * @private
 * @param {number} i
 * @param {shaka.util.AjaxRequest.Parameters} parameters
 * @param {shaka.util.IBandwidthEstimator=} opt_estimator
 * @return {!Promise.<!ArrayBuffer|string>}
 */
shaka.util.FailoverUri.prototype.createRequest_ =
    function(i, parameters, opt_estimator) {
  shaka.asserts.assert(i < this.urls.length);
  this.request_ = new shaka.util.AjaxRequest(
      this.urls[i].toString(), parameters);
  if (opt_estimator) {
    this.request_.estimator = opt_estimator;
  }

  var p = this.request_.send().then(shaka.util.TypedBind(this,
      /** @param {!XMLHttpRequest} xhr */
      function(xhr) {
        this.request_ = null;
        return Promise.resolve(xhr.response);
      }));

  p = p.catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (this.requestPromise_ && i + 1 < this.urls.length) {
          shaka.log.info('Trying fallback URL...');
          this.requestPromise_ = this.createRequest_(
              i + 1, parameters, opt_estimator);
          return this.requestPromise_;
        } else {
          this.request_ = null;
          this.requestPromise_ = null;
          return Promise.reject(error);
        }
      }));

  return p;
};

/**
 * Creates a deep-copy of the object.
 *
 * @return {!shaka.util.FailoverUri}
 */
shaka.util.FailoverUri.prototype.clone = function() {
  return new shaka.util.FailoverUri(
      this.urls.map(function(a) { return a.clone(); }),
      this.startByte,
      this.endByte
  );
};

if (!COMPILED) {
  /**
   * Gets the url of the first element.  Should only be used for testing.
   *
   * @return {!string}
   */
  shaka.util.FailoverUri.prototype.toString = function() {
    return this.urls[0].toString();
  };
}
<|endoftext|>


<commit_before>/* eslint no-underscore-dangle:0 */

'use strict';

var Readable = require('stream').Readable;
var inspect = require('../inspect');
var list = require('../list');

// ---------------------------------------------------------
// SYNC
// ---------------------------------------------------------

var walkSync = function (path, options, callback, currentLevel) {
  var item = inspect.sync(path, options.inspectOptions);

  if (options.maxLevelsDeep === undefined) {
    options.maxLevelsDeep = Infinity;
  }
  if (currentLevel === undefined) {
    currentLevel = 0;
  }

  callback(path, item);
  if (item && item.type === 'dir' && currentLevel < options.maxLevelsDeep) {
    list.sync(path).forEach(function (child) {
      walkSync(path + '/' + child, options, callback, currentLevel + 1);
    });
  }
};

// ---------------------------------------------------------
// STREAM
// ---------------------------------------------------------

var walkStream = function (path, options) {
  var rs = new Readable({ objectMode: true });
  var nextTreeNode = {
    path: path,
    parent: undefined,
    level: 0
  };
  var running = false;
  var readSome;

  var error = function (err) {
    rs.emit('error', err);
  };

  var findNextUnprocessedNode = function (node) {
    if (node.nextSibling) {
      return node.nextSibling;
    } else if (node.parent && node.parent.nextSibling) {
      return node.parent.nextSibling;
    }
    return undefined;
  };

  var pushAndContinueMaybe = function (data) {
    var theyWantMore = rs.push(data);
    running = false;
    if (!nextTreeNode) {
      // Previous was the last node. The job is done.
      rs.push(null);
    } else if (theyWantMore) {
      readSome();
    }
  };

  if (options.maxLevelsDeep === undefined) {
    options.maxLevelsDeep = Infinity;
  }

  readSome = function () {
    var theNode = nextTreeNode;

    running = true;

    inspect.async(theNode.path, options.inspectOptions)
    .then(function (inspected) {
      theNode.inspected = inspected;
      if (inspected && inspected.type === 'dir' && theNode.level < options.maxLevelsDeep) {
        list.async(theNode.path)
        .then(function (childrenNames) {
          var children = childrenNames.map(function (name) {
            return {
              name: name,
              path: theNode.path + '/' + name,
              parent: theNode,
              level: theNode.level + 1
            };
          });
          children.forEach(function (child, index) {
            child.nextSibling = children[index + 1];
          });

          nextTreeNode = children[0] || findNextUnprocessedNode(theNode);
          pushAndContinueMaybe({ path: theNode.path, item: inspected });
        })
        .catch(error);
      } else {
        nextTreeNode = findNextUnprocessedNode(theNode);
        pushAndContinueMaybe({ path: theNode.path, item: inspected });
      }
    })
    .catch(error);
  };

  rs._read = function () {
    if (!running) {
      readSome();
    }
  };

  return rs;
};

// ---------------------------------------------------------
// API
// ---------------------------------------------------------

exports.sync = walkSync;
exports.stream = walkStream;
<commit_msg>Fix for windows<commit_after>/* eslint no-underscore-dangle:0 */

'use strict';

var Readable = require('stream').Readable;
var pathUtil = require('path');
var inspect = require('../inspect');
var list = require('../list');

// ---------------------------------------------------------
// SYNC
// ---------------------------------------------------------

var walkSync = function (path, options, callback, currentLevel) {
  var item = inspect.sync(path, options.inspectOptions);

  if (options.maxLevelsDeep === undefined) {
    options.maxLevelsDeep = Infinity;
  }
  if (currentLevel === undefined) {
    currentLevel = 0;
  }

  callback(path, item);
  if (item && item.type === 'dir' && currentLevel < options.maxLevelsDeep) {
    list.sync(path).forEach(function (child) {
      walkSync(path + pathUtil.sep + child, options, callback, currentLevel + 1);
    });
  }
};

// ---------------------------------------------------------
// STREAM
// ---------------------------------------------------------

var walkStream = function (path, options) {
  var rs = new Readable({ objectMode: true });
  var nextTreeNode = {
    path: path,
    parent: undefined,
    level: 0
  };
  var running = false;
  var readSome;

  var error = function (err) {
    rs.emit('error', err);
  };

  var findNextUnprocessedNode = function (node) {
    if (node.nextSibling) {
      return node.nextSibling;
    } else if (node.parent && node.parent.nextSibling) {
      return node.parent.nextSibling;
    }
    return undefined;
  };

  var pushAndContinueMaybe = function (data) {
    var theyWantMore = rs.push(data);
    running = false;
    if (!nextTreeNode) {
      // Previous was the last node. The job is done.
      rs.push(null);
    } else if (theyWantMore) {
      readSome();
    }
  };

  if (options.maxLevelsDeep === undefined) {
    options.maxLevelsDeep = Infinity;
  }

  readSome = function () {
    var theNode = nextTreeNode;

    running = true;

    inspect.async(theNode.path, options.inspectOptions)
    .then(function (inspected) {
      theNode.inspected = inspected;
      if (inspected && inspected.type === 'dir' && theNode.level < options.maxLevelsDeep) {
        list.async(theNode.path)
        .then(function (childrenNames) {
          var children = childrenNames.map(function (name) {
            return {
              name: name,
              path: theNode.path + pathUtil.sep + name,
              parent: theNode,
              level: theNode.level + 1
            };
          });
          children.forEach(function (child, index) {
            child.nextSibling = children[index + 1];
          });

          nextTreeNode = children[0] || findNextUnprocessedNode(theNode);
          pushAndContinueMaybe({ path: theNode.path, item: inspected });
        })
        .catch(error);
      } else {
        nextTreeNode = findNextUnprocessedNode(theNode);
        pushAndContinueMaybe({ path: theNode.path, item: inspected });
      }
    })
    .catch(error);
  };

  rs._read = function () {
    if (!running) {
      readSome();
    }
  };

  return rs;
};

// ---------------------------------------------------------
// API
// ---------------------------------------------------------

exports.sync = walkSync;
exports.stream = walkStream;
<|endoftext|>


<commit_before>function Player(name) {
  this.name = name;
};

Player.prototype.picks = function(pick) {
  this.pick = pick;
};

function Game(player1, player2) {
  this.player1 = player1;
  this.player2 = player2;
};

Game.prototype.PAIRS = {
  rock:     { beats: 'scissors' },
  paper:    { beats: 'rock' },
  scissors: { beats: 'paper' }
};

Game.prototype.winner = function() {
  if(this.samePick()) { return null; }
  else if(this.PAIRS[player1.pick]['beats'] === this.player2.pick) {
    return this.player1;
  } else {
    return this.player2;
  }
};

Game.prototype.samePick = function() {
  if(this.player1.pick === this.player2.pick) {
    return 'draw'
  };
};

<commit_msg>all tests passing<commit_after>function Player(name) {
  this.name = name;
};

Player.prototype.picks = function(pick) {
  this.pick = pick;
};

function Game(player1, player2) {
  this.player1 = player1;
  this.player2 = player2;
};

Game.prototype.PAIRS = {
  rock:     { beats: 'scissors' },
  paper:    { beats: 'rock' },
  scissors: { beats: 'paper' }
};

Game.prototype.winner = function() {
  if(this.samePick()) return null;
  if(this.PAIRS[this.player1.pick]['beats'] === this.player2.pick) {
    return this.player1;
  } else {
    return this.player2;
  }
};

Game.prototype.samePick = function() {
  return this.player1.pick === this.player2.pick
};

<|endoftext|>


<commit_before>/**
 * @file TimeLine component
 * @author wendy(wendy.wei@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TimeLineItem from '../_TimelineItem';
import Theme from '../Theme';

import Util from '../_vendors/Util';

class TimeLine extends Component {

    static Type = TimeLineItem.Type;
    static Theme = Theme;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    render() {

        const {data, style, className} = this.props,

            wrapperClassName = classNames('time-line', {
                [className]: className
            });

        return (

                {
                    data && data.map((item, index) =>
                        <TimeLineItem key={index}
                                      theme={item.theme}
                                      type={item.type}
                                      title={item.title}
                                      date={item.date}
                                      hasBorder={item.hasBorder}
                                      contentText={item.contentText}
                                      headerText={item.headerText}/>
                    )
                }

        );

    }
}

TimeLine.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The items data.
     */
    data: PropTypes.arrayOf(PropTypes.shape({

        /**
         * The TimeLine theme.Can be primary,highlight,success,warning,error.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The TimeLine type.Can be header or title.
         */
        type: PropTypes.string,

        /**
         * The text value of TimeLine header.
         */
        headerText: PropTypes.string,

        /**
         * The date value of TimeLine.
         */
        date: PropTypes.string,

        /**
         * If true,the right content will have border style.
         */
        hasBorder: PropTypes.bool,

        /**
         * The text value of right content.
         */
        contentText: PropTypes.string

    })).isRequired

};

TimeLine.defaultProps = {
    hasBorder: true,
    title: '',
    type: ''
};

export default TimeLine;<commit_msg>update TimeLine<commit_after>/**
 * @file TimeLine component
 * @author wendy(wendy.wei@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TimeLineItem from '../_TimelineItem';

import Theme from '../Theme';
import Util from '../_vendors/Util';

class TimeLine extends Component {

    static Type = TimeLineItem.Type;
    static Theme = Theme;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    render() {

        const {data, style, className} = this.props;

        return (

                {
                    data && data.map((item, index) =>
                        <TimeLineItem key={index}
                                      theme={item.theme}
                                      type={item.type}
                                      title={item.title}
                                      date={item.date}
                                      hasBorder={item.hasBorder}
                                      contentText={item.contentText}
                                      headerText={item.headerText}/>
                    )
                }

        );

    }
}

TimeLine.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The items data.
     */
    data: PropTypes.arrayOf(PropTypes.shape({

        /**
         * The TimeLine theme.Can be primary,highlight,success,warning,error.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The TimeLine type.Can be header or title.
         */
        type: PropTypes.string,

        /**
         * The text value of TimeLine header.
         */
        headerText: PropTypes.string,

        /**
         * The date value of TimeLine.
         */
        date: PropTypes.string,

        /**
         * If true,the right content will have border style.
         */
        hasBorder: PropTypes.bool,

        /**
         * The text value of right content.
         */
        contentText: PropTypes.string

    })).isRequired

};

TimeLine.defaultProps = {
    hasBorder: true,
    title: '',
    type: ''
};

export default TimeLine;
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import Firebase from '../Firebase';
import {createStyleSheet, withStyles} from 'material-ui/styles';
import {Button, Grid} from 'material-ui';
import PropTypes from 'prop-types';
import AddIcon from 'material-ui-icons/Add';
import TrackMessage from './TrackMessage';
import TrackCardView from './TrackCardView';
import TrackCardEdit from './TrackCardEdit';
import UUID from 'uuid/v4';

const

  styleSheet = createStyleSheet('Tracks', theme => ({
    root:         {
      flexGrow:  1,
      marginTop: 30
    },
    button:       {
      margin: theme.spacing.unit,
    },
    grid:         theme.mixins.gutters({
      flexGrow:  1,
      marginTop: 30
    }),
    flashmessage: {
      justify: 'center',
    }
  }))
;

function getNewTracker() {
  return {
    id:                       UUID(),
    name:                     '',
    description:              '',
    editing:                  true,
    sessionId:                '',
    _sessionElapsed:          0,
    // the total duration BEFORE the session if any
    completedSessionDuration: 0
  }
}

function getStartedSession(trackerId) {
  return {
    id:            UUID(),
    trackerId:     trackerId,
      // TODO: use Firebase.ServerValue.TIMESTAMP here
    startedAt:     Date.now(),
    _elapsed:      0,
    // will be zero until finished
    finalDuration: 0
  }
}

function getFinishedSession(sessionObj) {
  return {
    ...sessionObj,
    _elapsed:      0,
    finalDuration: Date.now() - sessionObj.startedAt
  }
}

function getAddTrackerAction() {
  return function({trackers}, props) {
    const newTracker = getNewTracker();
    return {
      trackers: {
        ...trackers,
        [newTracker.id]: newTracker
      }
    }
  };
}

function getSaveTrackerAction(tracker) {
  return function({trackers}, props) {
    return {
      trackers: {
        // all the existing tracks
        ...trackers,
        // plus the new saved tracker
        [tracker.id]: tracker
      }
    };
  };
}

function getTrackerDeleteAction(trackerId) {
  return function({trackers, sessions}, props) {
    const
      {[trackerId]: deleted, ...newTrackers} = trackers,
      newSessions                            = filterAndAssignObj(
        sessions, (session) => !(session.trackerId === trackerId))
    ;
    return {
      trackers: {
        // all of the existing trackers except for the removed one
        ...newTrackers
      },
      sessions: {
        ...newSessions
      }
    };
  }
}

function getTrackerPlayPauseToggleAction(trackerId) {
  return function({trackers, sessions}, props) {
    const
      {[trackerId]: tracker}   = trackers,

      session                  = tracker.sessionId
        ? getFinishedSession(sessions[tracker.sessionId])
        : getStartedSession(trackerId),

      newSessions              = {
        ...sessions,
        [session.id]: session
      },

      // iterate through new sessions. if session id matches tracker.id,
      // matches given then add the session duration to the total.
      completedSessionDuration = reduceObj(
        (total, sess) => (sess.trackerId === tracker.id)
          ? total + sess.finalDuration : total,
        0, newSessions),

      newState                 = {
        trackers: {
          // all of the other trackers
          ...trackers,
          [trackerId]: {
            // all of the existing props of the tracker
            ...tracker,
            // but with the sessionId toggled
            sessionId:                tracker.sessionId ? '' : session.id,
            _sessionElapsed:          session._elapsed,
            completedSessionDuration: completedSessionDuration
          }
        },
        sessions: newSessions
      };

    console.log(newState);

    return newState;
  };
}

function reduceObj(reducer, accumulator, object) {
  return Object.keys(object).reduce(function(accumulator, key) {
    return reducer(accumulator, object[key], key, object)
  }, accumulator);
}

function filterAndAssignObj(object, filterer, assigner = (k, v) => ({[k]: v})) {
  return reduceObj(
    function(accumulator, objValue, objKey, reduceObject) {
      return filterer(objValue, reduceObject)
        ? Object.assign(accumulator, assigner(objKey, objValue))
        : accumulator
    },
    {}, object
  );
}

/**
 * TracksList.onComponentDidMount() gives this function an opportunity to
 * change state every second. It calculates the local time elapsed of each
 * in-progress timer, calculating it in session._elapsed and
 * tracker._sessionElapsed. These properties are not persisted to firebase;
 * they are for local display only.
 * @returns {Function}
 */
function getAdvanceTimeAction() {
  return function({trackers, sessions}, props) {
    const
      // only the sessions with final duration equal to zero are in progress
      sessionFilter      = (session) => session.finalDuration === 0,
      sessionAssign      = (sessionId, session) => ({
        [sessionId]: {
          ...session,
          _elapsed: Date.now() - session.startedAt
        }
      }),
      sessionsInProgress = sessions
        ? filterAndAssignObj(sessions, sessionFilter, sessionAssign) : {},

      trackerFilter      = (tracker) => tracker.sessionId,
      trackerAssign      = (trackerId, tracker) => {
        const
          // get the elapsed duration for this in progress tracker
          _sessionElapsed = reduceObj(
            (total, sess) => (sess.trackerId === tracker.id)
              ? total + sess._elapsed : total,
            0, sessionsInProgress);

        return {
          [trackerId]: {
            ...tracker,
            _sessionElapsed: _sessionElapsed
          }
        }
      },
      trackersInProgress = trackers
        ? filterAndAssignObj(trackers, trackerFilter, trackerAssign) : {},

      newState           = {
        trackers: {
          ...trackers,
          ...trackersInProgress
        },
        sessions: {
          ...sessions,
          ...sessionsInProgress
        }
      };

    // console.log('getAdvanceTimeAction: after state:');
    // console.log(newState);
    return newState;
  };
}

/**
 * TracksList.componentDidMound() subscribes to updates from our Firebase
 * adaptor, which uses this mutator to alter state.
 * @param firebaseState to update
 * @returns new local state
 */
function getUpdateFromFirebaseAction(firebaseState) {

  return function({trackers, sessions}, props) {
    const

      newState                           = {
        trackers: {
          ...trackers,
          ...firebaseState.trackers,
        },
        sessions: {
          ...sessions,
          ...firebaseState.sessions,
        }
      };

    // console.log('getAdvanceTimeAction: after state:');
    // console.log(newState);
    return newState
  };
}

function getLogoutAction() {
  return function(state, props) {

    const newState = {
      trackers: {},
      sessions: {},
      loggedIn: false
    };

    return newState
  };
}

class Tracks extends Component {

  constructor(props) {
    super(props);

    this.onAddTrackClicked     = this.onAddTrackClicked.bind(this);
    this.onSaveTrack           = this.onSaveTrack.bind(this);
    this.onStartEditTrackId    = this.onStartEditTrackId.bind(this);
    this.onDeleteExistingTrack = this.onDeleteExistingTrack.bind(this);
    this.onPlayPause           = this.onPlayPause.bind(this);
    this.persistToDatabase     = this.persistToDatabase.bind(this);
    this.handleDatabaseUpdate  = this.handleDatabaseUpdate.bind(this);

    this.state = {
      trackers: {},
      sessions: {},
      loggedIn: false
    };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.onAdvanceTime(),
      1000
    );
    Firebase.subscribe(this);
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
    Firebase.unsubscribe(this);
  }

  /**
   * Our custom Firebase adaptor has subscribe() and unsubscribe()
   * methods called from componentDidMount() and componentWillMount().
   *
   * When subscribed, listeners' get notified of database updates
   * with this method.
   * @param firebaseState
   * @param firebaseUser
   */
  handleDatabaseUpdate(firebaseState = {}, firebaseUser) {
    if (firebaseUser) {
      console.log('caught TracksList.handleDatabaseUpdate WITH user',
        firebaseState, firebaseUser);
      this.setState(getUpdateFromFirebaseAction({...firebaseState}))
    } else {
      console.log('caught Tracks.onAuthStateChanged WITHOUT user');
      this.setState(getLogoutAction())
    }
  }

  onAddTrackClicked() {
    this.setState(getAddTrackerAction());
    console.log('caught Tracks.onAddTrackClicked');
    console.log(this.state);
  }

  onSaveTrack(tracker) {
    const trackerToSave = {
      ...tracker,
      editing: false
    };
    console.log('caught TrackList.onSaveTrack:');
    console.log(trackerToSave);
    this.setState(getSaveTrackerAction(trackerToSave), this.persistToDatabase);
  }

    /**
     * Remove private properties starting with an underscore from
     * the state to persist to firebase. This is necessary so that
     * multiple devices don't get into a race condition when updating firebase.
     * @param state
     */
  persistToDatabase(state = this.state) {
    const
      {trackers = {}, sessions = {}} = state,
      noopFilter                     = (instance) => instance.id,
      localPropsCleaner              = (id, instance) => {
        const // eslint-disable-next-line
          {_elapsed, _sessionElapsed, cleanInstance} = instance;
        return {
          [id]: {
            ...instance,
          }

        }
      },
      cleanTrackers                  = trackers
        ? filterAndAssignObj(trackers, noopFilter, localPropsCleaner) : {},
      cleanSessions                  = sessions
        ? filterAndAssignObj(sessions, noopFilter, localPropsCleaner) : {}
    ;
    debugger;
    Firebase.set({
      trackers: cleanTrackers,
      sessions: cleanSessions
    });
  }

  onAdvanceTime() {
    this.setState(getAdvanceTimeAction())
  }

  onStartEditTrackId(trackerId) {
    const trackerToEdit = {
      ...this.state.trackers[trackerId],
      editing: true
    };
    console.log('caught TrackList.onStartEditTrackId:');
    this.setState(getSaveTrackerAction(trackerToEdit), state => console.log(state));
  }

  onDeleteExistingTrack(trackerId) {
    console.log('caught TrackList.onDeleteExistingTrack:');
    this.setState(getTrackerDeleteAction(trackerId), this.persistToDatabase);
  }

  onPlayPause(trackerId) {
    console.log('caught TrackList.onPlayPause:');
    this.setState(getTrackerPlayPauseToggleAction(trackerId), this.persistToDatabase);
  }

  render() {
    const
      classes     = this.props.classes,

      renderTrack = (trackerId, index) => {
        const
          tracker         = this.state.trackers[trackerId],
          session         = tracker.sessionId ? this.state.sessions[tracker.sessionId] : null,
          sessionDuration = session ? session._elapsed + session.finalDuration : 0,
          totalDuration   = tracker.completedSessionDuration + tracker._sessionElapsed
        ;
        return (
          <Grid key={index} item sm={6} xs={12}>
            {
              tracker.editing
                ? (<TrackCardEdit key={trackerId} id={trackerId} tracker={tracker}
                                  onSave={this.onSaveTrack} onCancel={this.onDeleteExistingTrack}/>)
                : (<TrackCardView key={trackerId} id={trackerId} name={tracker.name}
                                  sessionId={tracker.sessionId}
                                  onEdit={this.onStartEditTrackId} onPlayPause={this.onPlayPause}
                                  totalDuration={totalDuration}
                                  sessionDuration={sessionDuration}
                                  description={tracker.description}/>)
            }
          </Grid>
        )
      },

      trackerKeys = this.state.trackers ? Object.keys(this.state.trackers) : [],

      tracks      = trackerKeys.length > 0 ? trackerKeys.map(renderTrack) : (
        <Grid item key={'flashmessage-center'} sm={12} xs={12}>
          <Grid container className={classes.flashmessage} justify="center">
            <Grid item sm={8} xs={8}>
              <TrackMessage title="No time trackers found!" body="Click the button below to create one."/>
            </Grid>
          </Grid>
        </Grid>
      );

    return (
      <Grid container className={classes.grid}>
        {tracks}
        <Grid item xs={12}>
          <Button fab accent className={classes.button} onClick={this.onAddTrackClicked}>
            <AddIcon/>
          </Button>
        </Grid>
      </Grid>
    )
  }
}

Tracks.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styleSheet)(Tracks);<commit_msg>fix #40 - Delete does not go to second tab until refresh<commit_after>import React, {Component} from 'react';
import Firebase from '../Firebase';
import {createStyleSheet, withStyles} from 'material-ui/styles';
import {Button, Grid} from 'material-ui';
import PropTypes from 'prop-types';
import AddIcon from 'material-ui-icons/Add';
import TrackMessage from './TrackMessage';
import TrackCardView from './TrackCardView';
import TrackCardEdit from './TrackCardEdit';
import UUID from 'uuid/v4';

const

  styleSheet = createStyleSheet('Tracks', theme => ({
    root:         {
      flexGrow:  1,
      marginTop: 30
    },
    button:       {
      margin: theme.spacing.unit,
    },
    grid:         theme.mixins.gutters({
      flexGrow:  1,
      marginTop: 30
    }),
    flashmessage: {
      justify: 'center',
    }
  }))
;

function getNewTracker() {
  return {
    id:                       UUID(),
    name:                     '',
    description:              '',
    editing:                  true,
    sessionId:                '',
    _sessionElapsed:          0,
    // the total duration BEFORE the session if any
    completedSessionDuration: 0
  }
}

function getStartedSession(trackerId) {
  return {
    id:            UUID(),
    trackerId:     trackerId,
      // TODO: use Firebase.ServerValue.TIMESTAMP here
    startedAt:     Date.now(),
    _elapsed:      0,
    // will be zero until finished
    finalDuration: 0
  }
}

function getFinishedSession(sessionObj) {
  return {
    ...sessionObj,
    _elapsed:      0,
    finalDuration: Date.now() - sessionObj.startedAt
  }
}

function getAddTrackerAction() {
  return function({trackers}, props) {
    const newTracker = getNewTracker();
    return {
      trackers: {
        ...trackers,
        [newTracker.id]: newTracker
      }
    }
  };
}

function getSaveTrackerAction(tracker) {
  return function({trackers}, props) {
    return {
      trackers: {
        // all the existing tracks
        ...trackers,
        // plus the new saved tracker
        [tracker.id]: tracker
      }
    };
  };
}

function getTrackerDeleteAction(trackerId) {
  return function({trackers, sessions}, props) {
    const
      {[trackerId]: deleted, ...newTrackers} = trackers,
      newSessions                            = filterAndAssignObj(
        sessions, (session) => !(session.trackerId === trackerId))
    ;
    return {
      trackers: {
        // all of the existing trackers except for the removed one
        ...newTrackers
      },
      sessions: {
        ...newSessions
      }
    };
  }
}

function getTrackerPlayPauseToggleAction(trackerId) {
  return function({trackers, sessions}, props) {
    const
      {[trackerId]: tracker}   = trackers,

      session                  = tracker.sessionId
        ? getFinishedSession(sessions[tracker.sessionId])
        : getStartedSession(trackerId),

      newSessions              = {
        ...sessions,
        [session.id]: session
      },

      // iterate through new sessions. if session id matches tracker.id,
      // matches given then add the session duration to the total.
      completedSessionDuration = reduceObj(
        (total, sess) => (sess.trackerId === tracker.id)
          ? total + sess.finalDuration : total,
        0, newSessions),

      newState                 = {
        trackers: {
          // all of the other trackers
          ...trackers,
          [trackerId]: {
            // all of the existing props of the tracker
            ...tracker,
            // but with the sessionId toggled
            sessionId:                tracker.sessionId ? '' : session.id,
            _sessionElapsed:          session._elapsed,
            completedSessionDuration: completedSessionDuration
          }
        },
        sessions: newSessions
      };

    console.log(newState);

    return newState;
  };
}

function reduceObj(reducer, accumulator, object) {
  return Object.keys(object).reduce(function(accumulator, key) {
    return reducer(accumulator, object[key], key, object)
  }, accumulator);
}

function filterAndAssignObj(object, filterer, assigner = (k, v) => ({[k]: v})) {
  return reduceObj(
    function(accumulator, objValue, objKey, reduceObject) {
      return filterer(objValue, reduceObject)
        ? Object.assign(accumulator, assigner(objKey, objValue))
        : accumulator
    },
    {}, object
  );
}

/**
 * TracksList.onComponentDidMount() gives this function an opportunity to
 * change state every second. It calculates the local time elapsed of each
 * in-progress timer, calculating it in session._elapsed and
 * tracker._sessionElapsed. These properties are not persisted to firebase;
 * they are for local display only.
 * @returns {Function}
 */
function getAdvanceTimeAction() {
  return function({trackers, sessions}, props) {
    const
      // only the sessions with final duration equal to zero are in progress
      sessionFilter      = (session) => session.finalDuration === 0,
      sessionAssign      = (sessionId, session) => ({
        [sessionId]: {
          ...session,
          _elapsed: Date.now() - session.startedAt
        }
      }),
      sessionsInProgress = sessions
        ? filterAndAssignObj(sessions, sessionFilter, sessionAssign) : {},

      trackerFilter      = (tracker) => tracker.sessionId,
      trackerAssign      = (trackerId, tracker) => {
        const
          // get the elapsed duration for this in progress tracker
          _sessionElapsed = reduceObj(
            (total, sess) => (sess.trackerId === tracker.id)
              ? total + sess._elapsed : total,
            0, sessionsInProgress);

        return {
          [trackerId]: {
            ...tracker,
            _sessionElapsed: _sessionElapsed
          }
        }
      },
      trackersInProgress = trackers
        ? filterAndAssignObj(trackers, trackerFilter, trackerAssign) : {},

      newState           = {
        trackers: {
          ...trackers,
          ...trackersInProgress
        },
        sessions: {
          ...sessions,
          ...sessionsInProgress
        }
      };

    // console.log('getAdvanceTimeAction: after state:');
    // console.log(newState);
    return newState;
  };
}

/**
 * TracksList.componentDidMound() subscribes to updates from our Firebase
 * adaptor, which uses this mutator to alter state. This needs to support
 * creating, updating, and deleting trackers.
 * @param firebaseState to update
 * @returns new local state
 */
function getUpdateFromFirebaseAction(firebaseState) {

  return function({trackers, sessions}, props) {
    const

      newState                           = {
        // we need to support updates as well as deletes
        trackers: {
          ...firebaseState.trackers,
        },
        sessions: {
          ...firebaseState.sessions,
        }
      };

    // console.log('getAdvanceTimeAction: after state:');
    // console.log(newState);
    return newState
  };
}

function getLogoutAction() {
  return function(state, props) {

    const newState = {
      trackers: {},
      sessions: {},
      loggedIn: false
    };

    return newState
  };
}

class Tracks extends Component {

  constructor(props) {
    super(props);

    this.onAddTrackClicked     = this.onAddTrackClicked.bind(this);
    this.onSaveTrack           = this.onSaveTrack.bind(this);
    this.onStartEditTrackId    = this.onStartEditTrackId.bind(this);
    this.onDeleteExistingTrack = this.onDeleteExistingTrack.bind(this);
    this.onPlayPause           = this.onPlayPause.bind(this);
    this.persistToDatabase     = this.persistToDatabase.bind(this);
    this.handleDatabaseUpdate  = this.handleDatabaseUpdate.bind(this);

    this.state = {
      trackers: {},
      sessions: {},
      loggedIn: false
    };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.onAdvanceTime(),
      1000
    );
    Firebase.subscribe(this);
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
    Firebase.unsubscribe(this);
  }

  /**
   * Our custom Firebase adaptor has subscribe() and unsubscribe()
   * methods called from componentDidMount() and componentWillMount().
   *
   * When subscribed, listeners' get notified of database updates
   * with this method.
   * @param firebaseState
   * @param firebaseUser
   */
  handleDatabaseUpdate(firebaseState = {}, firebaseUser) {
    if (firebaseUser) {
      console.log('caught TracksList.handleDatabaseUpdate WITH user',
        firebaseState, firebaseUser);
      this.setState(getUpdateFromFirebaseAction({...firebaseState}))
    } else {
      console.log('caught Tracks.onAuthStateChanged WITHOUT user');
      this.setState(getLogoutAction())
    }
  }

  onAddTrackClicked() {
    this.setState(getAddTrackerAction());
    console.log('caught Tracks.onAddTrackClicked');
    console.log(this.state);
  }

  onSaveTrack(tracker) {
    const trackerToSave = {
      ...tracker,
      editing: false
    };
    console.log('caught TrackList.onSaveTrack:');
    console.log(trackerToSave);
    this.setState(getSaveTrackerAction(trackerToSave), this.persistToDatabase);
  }

    /**
     * Remove private properties starting with an underscore from
     * the state to persist to firebase. This is necessary so that
     * multiple devices don't get into a race condition when updating firebase.
     * @param state
     */
  persistToDatabase(state = this.state) {
    const
      {trackers = {}, sessions = {}} = state,
      noopFilter                     = (instance) => instance.id,
      localPropsCleaner              = (id, instance) => {
        const // eslint-disable-next-line
          {_elapsed, _sessionElapsed, cleanInstance} = instance;
        return {
          [id]: {
            ...instance,
          }

        }
      },
      cleanTrackers                  = trackers
        ? filterAndAssignObj(trackers, noopFilter, localPropsCleaner) : {},
      cleanSessions                  = sessions
        ? filterAndAssignObj(sessions, noopFilter, localPropsCleaner) : {}
    ;
    Firebase.set({
      trackers: cleanTrackers,
      sessions: cleanSessions
    });
  }

  onAdvanceTime() {
    this.setState(getAdvanceTimeAction())
  }

  onStartEditTrackId(trackerId) {
    const trackerToEdit = {
      ...this.state.trackers[trackerId],
      editing: true
    };
    console.log('caught TrackList.onStartEditTrackId:');
    this.setState(getSaveTrackerAction(trackerToEdit), state => console.log(state));
  }

  onDeleteExistingTrack(trackerId) {
    console.log('caught TrackList.onDeleteExistingTrack:');
    this.setState(getTrackerDeleteAction(trackerId), this.persistToDatabase);
  }

  onPlayPause(trackerId) {
    console.log('caught TrackList.onPlayPause:');
    this.setState(getTrackerPlayPauseToggleAction(trackerId), this.persistToDatabase);
  }

  render() {
    const
      classes     = this.props.classes,

      renderTrack = (trackerId, index) => {
        const
          tracker         = this.state.trackers[trackerId],
          session         = tracker.sessionId ? this.state.sessions[tracker.sessionId] : null,
          sessionDuration = session ? session._elapsed + session.finalDuration : 0,
          totalDuration   = tracker.completedSessionDuration + tracker._sessionElapsed
        ;
        return (
          <Grid key={index} item sm={6} xs={12}>
            {
              tracker.editing
                ? (<TrackCardEdit key={trackerId} id={trackerId} tracker={tracker}
                                  onSave={this.onSaveTrack} onCancel={this.onDeleteExistingTrack}/>)
                : (<TrackCardView key={trackerId} id={trackerId} name={tracker.name}
                                  sessionId={tracker.sessionId}
                                  onEdit={this.onStartEditTrackId} onPlayPause={this.onPlayPause}
                                  totalDuration={totalDuration}
                                  sessionDuration={sessionDuration}
                                  description={tracker.description}/>)
            }
          </Grid>
        )
      },

      trackerKeys = this.state.trackers ? Object.keys(this.state.trackers) : [],

      tracks      = trackerKeys.length > 0 ? trackerKeys.map(renderTrack) : (
        <Grid item key={'flashmessage-center'} sm={12} xs={12}>
          <Grid container className={classes.flashmessage} justify="center">
            <Grid item sm={8} xs={8}>
              <TrackMessage title="No time trackers found!" body="Click the button below to create one."/>
            </Grid>
          </Grid>
        </Grid>
      );

    return (
      <Grid container className={classes.grid}>
        {tracks}
        <Grid item xs={12}>
          <Button fab accent className={classes.button} onClick={this.onAddTrackClicked}>
            <AddIcon/>
          </Button>
        </Grid>
      </Grid>
    )
  }
}

Tracks.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styleSheet)(Tracks);<|endoftext|>