<commit_before>/**
 * jquery.doctest.js beta
 * http://lab.heungsub.net/jquery.doctest.js
 * http://github.com/heungsub/jquery.doctest.js
 *
 * Copyright 2010, Heungsub Lee <lee@heungsub.net>
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * See also MIT-LICENSE.txt and GPL-LICENSE.txt
 */
(function( $, undefined ) {

var doctest = function( scriptUrl ) {
        return new doctest.fn.init( scriptUrl );
    },

    // Check if this line has flags
    flags = /\/\/doctest:\s*(.+)\s*$/,

    blank = /^\s*$/,

    // A tab string
    ____ = "    ",

    th = function( n ) {
        n = parseInt( n );
        if ( n == 1 ) {
            return "1st";
        } else if ( n == 2 ) {
            return "2nd";
        } else if ( n == 3 ) {
            return "3rd";
        } else {
            return n + "th";
        }
    },

    escapeRegExp = function( t ) {
        var specials = [
            "/", ".", "*", "+", "?", "|",
            "(", ")", "[", "]", "{", "}", "\\"
        ];
        var r = new RegExp( "(\\" + specials.join( "|\\" ) + ")", "g" );

        return t.replace( r, "\\$1" );
    };

// Alias for static methods
var self = doctest;

// Make jQuery.doctest
$.extend({
    doctest: self
});

// Methods
doctest.fn = doctest.prototype = {
    // Initialize
    init: function( scriptUrl, options ) {
        this.scriptUrl = scriptUrl;
        this.result = self.testjs( scriptUrl, this.events );
        return this;
    },

    verbose: function() {
        this.pass(function( test ) {
            // Log seccess message
            self.console.log([
                "Trying:",
                ____ + test.code,
                "Expecting:",
                ____ + test.expected,
                "ok"
            ].join( "\n" ) );
        });

        this.complete(function( result ) {
            var message, msg,
                passedMsg = [], failedMsg = [], emptyMsg = [],
                passedItems = [], failedItems = [], emptyItems = [],
                tests = 0, passed = 0, failed = 0,
                item, nl = "\n";

            // for item in result
            for ( var i in result ) {
                item = result[ i ];

                tests += item.tests;
                passed += item.passed;
                failed += item.failed;

                msg = th( parseInt( i ) + 1 ) + " item(line " + item.line + ")";

                // Empty item
                if ( item.tests === 0 ) {
                    emptyItems.push( item );
                    msg = ____ + msg;
                    emptyMsg.push( msg );

                // All tests passed item
                } else if ( item.passed === item.tests ) {
                    passedItems.push( item );
                    msg = ____ + item.tests + " tests in " + msg;
                    passedMsg.push( msg );

                // This item had some failures
                } else {
                    failedItems.push( item );
                    msg = ____ + item.failed + " of " +
                        item.tests + " in " + msg;
                    failedMsg.push( msg );
                }
            }

            if ( i === undefined ) {
                return;
            }

            // Output empty items message
            if ( emptyItems.length ) {
                message = [];
                message.push( emptyItems.length + " items had no tests:" );
                message.push( emptyMsg.join( nl ) );
                self.console.log( message.join( nl ) );
            }

            // Output passed items message
            if ( passedItems.length ) {
                message = [];
                message.push( passedItems.length + " items passed all tests:" );
                message.push( passedMsg.join( nl ) );
                self.console.log( message.join( nl ) );
            }

            // Output failed items message with console.error
            if ( failedItems.length ) {
                message = [];
                message.push( failedItems.length + " items had failures:" );
                message.push( failedMsg.join( nl ) );
                self.console.warn( message.join( nl ) );
            }

            // Summary
            message = [
                tests + " tests in " + i + " items.",
                passed + " passed and " + failed + " failed."
            ];

            // All tests passed?
            if ( !failedItems.length ) {
                message.push( "Test passed." );
            }

            self.console.log( message.join( nl ) );
        });

        return this;
    },

    events: {
        complete: undefined,
        pass: undefined,
        fail: undefined
    },

    toString: function() {
        /**
        >>> ({});
        [object Object]
        >>> $.doctest();
        [object $.doctest]
        >>> $.doctest( "test/nothing-fn.toString.js" );
        [object $.doctest]
        */
        return "[object $.doctest]";
    }
};

// Event setters
for ( var i in doctest.fn.events ) {
    /**
    >>> dt = $.doctest() //doctest: +SKIP
    >>> $.isFunction( dt.events.complete );
    false
    >>> dt.complete(function() {});
    [object $.doctest]
    >>> $.isFunction( dt.events.complete );
    true
    >>> $.isFunction( dt.events.pass );
    false
    >>> $.isFunction( dt.events.fail );
    false
    >>> dt.pass(function() {}).fail(function() {});
    [object $.doctest]
    >>> $.isFunction( dt.events.pass );
    true
    >>> $.isFunction( dt.events.fail );
    true
    */
    doctest.fn[ i ] = eval(
        "(function( fn ) { this.events." + i + " = fn; return this; })"
    );
}

// Give the init function the doctest prototype for later instantiation
doctest.fn.init.prototype = doctest.fn;

doctest.extend = doctest.fn.extend = $.extend;

doctest.extend({
    // Console should contains log(), error() method
    console: {
        log: $.proxy( console, "log" ),
        error: $.proxy( console, $.browser.webkit ? "error" : "warn" ),
        warn: $.proxy( console, "warn" )
    },

    // Default symbols
    symbols: {
        docStart: "/**",
        docEnd: "*/",
        prompt: ">>> ",
        continued: "... "
    },

    // Test the script file
    testjs: function( scriptUrl, events ) {
        /**
        >>> $.doctest.testjs( "test/nothing-testjs.js" );
        [object Object]
        >>> $( "script[src$=test/nothing-testjs.js]" ).length;
        1
        */

        var result = {},
            start = events && events.start || self.events.start,
            complete = events && events.complete || self.events.complete;

        var run = function( code ) {
            var description = self.describe( code );

            // Set result
            $.extend( result, self.run( description, events ) );

            // Call complete event
            return complete( result );
        };

		// Handle $.doctest(), $.doctest( null ) or $.doctest( undefined )
        if ( !scriptUrl ) {
            return result;
            // run( $( "html" ).html() );

		// Handle $.doctest( "example.js" )
        } else {
            // Load the script
            if ( !$( "script[src=" + scriptUrl + "]" ).length ) {
                var markup = '<script class="doctest" type="text/javascript" '
                           + 'src="' + scriptUrl + '"></script>';
                $( markup ).appendTo( document.body );
            }

            // Get code of the script and run tests
            $.getScript( scriptUrl, run );
        }

        // Result is empty yet. It will come later
        return result;
    },

    // Run tests
    run: function( description, events, assert ) {
        /**
        >>> d = [[{ line: 12, code: "1+1;", expected: "2", flags: []}]] //doctest: +SKIP
        >>> $.doctest.run( d )[ 0 ].passed;
        1
        */
        var test, item, line,
            result = [],
            passed, failed;

        pass = events && events.pass || self.events.pass;
        fail = events && events.fail || self.events.fail;
        assert = assert || self.assert;

        // for item in description
        for ( var i in description ) {
            item = description[ i ];
            passed = failed = 0,
            line = 1;

            // for test in item
            for ( var j in item ) {
                test = item[ j ];

                // Line number of the item
                if ( line === 1 ) {
                    line = parseInt( test.line );
                }

                try {
                    assert( test );
                    pass( test );
                    passed++;

                } catch ( error ) {
                    fail( error );
                    failed++;
                }
            }

            // Save result each item
            result.push({
                item: item,
                line: line,
                tests: item.length,
                passed: passed,
                failed: failed
            });
        }

        return result;
    },

    // Parse the code and return a description
    describe: function( code ) {
        /**
        >>> code = "/*" + "*\n>>> 1 + 1;\n2\n*" + "/"; //doctest: +SKIP
        >>> code.length;
        19
        >>> description = $.doctest.describe( code );
        [object Object]
        >>> description.length;
        1
        >>> description[0][0].code;
        1 + 1;
        >>> description[0][0].expected;
        2
        >>> description[0][0].line;
        2
        >>> description[0][0].flags.length;
        0
        */
        var lines = code.split( "\n" ),

            // Modes
            isItem = false, hasFlag = false,

            description = [], items = [], itemLines = [],
            line, finalLine, item, test, indent,

            _docStart = self.symbols.docStart,
            _docEnd = self.symbols.docEnd,
            _prompt = self.symbols.prompt,
            _continued = self.symbols.continued,
            docStart, docEnd, prompt, continued,

            spaces = "^\\s*",
            group = "(.+)$";

        docStart = new RegExp( escapeRegExp( _docStart ) );
        docEnd = new RegExp( escapeRegExp( _docEnd ) );
        prompt = new RegExp( spaces + escapeRegExp( _prompt ) + group );
        continued = new RegExp( spaces + escapeRegExp( _continued ) + group );

        for ( var i in lines ) {
            line = lines[ i ];

            // Validate indentation
            if ( indent !== undefined && !blank.exec( line ) ) {
                if ( line.search( indent ) !== 0 ) {
                    throw new self.errors.IndentationError( i );
                } else {
                    line = line.slice( indent.length );
                }
            }

            // When the start line of a docstring
            if ( docStart.exec( line ) ) {
                isItem = true;
                indent = line.slice( 0, line.search( docStart ) );
                continue;

            // When the end line of a docstring
            } else if ( isItem && docEnd.exec( line ) ) {
                items.push( itemLines );
                itemLines = [];
                isItem = false;
                indent = undefined;
            }

            // When the line in a docstring
            if ( isItem ) {
                finalLine = parseInt( i ) + 1;
                itemLines[ finalLine ] = line;
            }
        }

        var keep = function( test ) {
            // Merge code
            test.code = test.code.join( "\n" );

            // Merge expected value
            if ( test.expected === undefined ) {
                test.expected = undefined;
            } else {
                test.expected = test.expected.join( "\n" );
            }

            item.push( test );
        }

        for ( var j in items ) {
            item = [];

            // for line in items[ j ]
            for ( var i in items[ j ] ) {
                i = parseInt( i );
                line = items[ j ][ i ];

                // When the line contains prompt
                if ( prompt.exec( line ) ) {
                    if ( test !== undefined ) {
                        keep( test );
                    }

                    test = {
                        line: i,
                        code: [line.match( prompt )[ 1 ]]
                    };

                    // Find flags
                    hasFlag = test.code[0].match( flags );

                    if ( hasFlag ) {
                        test.flags = hasFlag[ 1 ].split( /\s+/ );
                    } else {
                        test.flags = [];
                    }

                } else if ( test !== undefined ) {
                    // When the line contains continued prompt
                    if ( continued.exec( line ) ) {
                        test.code.push( line.match( continued )[1] );

                    // Blank line
                    } else if ( blank.exec( line ) ) {
                        keep( test );
                        test = undefined;

                    // When the line means expected value
                    } else {
                        if ( test.expected === undefined ) {
                            test.expected = [];
                        }
                        test.expected.push( line );
                    }
                }

                // The test must be keeped
                if ( test !== undefined && i === finalLine ) {
                    keep( test );
                }
            }

            description.push( item );
        }

        return description;
    },

    // Assert test object
    assert: function( test ) {
        /**
        >>> $.doctest.assert({ code: "1+'0';", expected: "10", flags: [] });
        true
        >>> $.doctest.assert({ code: "1+0;", expected: "1", flags: [] });
        true
        */
        var got, flag;

        // Evaluation!
        try {
            got = self.eval( test );
        } catch ( error ) {
            got = error;
        }

        // Process flags
        for ( var i in test.flags ) {

            // Get flag name
            flag = test.flags[ i ].replace( /^\+/, "" );

            // Check support the flag
            if ( self.flags[ flag ] !== undefined ) {
                var returned = self.flags[ flag ]( test );

                // Just return if flag returns anything
                if ( returned !== undefined ) {
                    return returned;
                }
            }
        }

        // Stringify and compare
        if ( String( test.expected ) !== String( got ) ) {
            throw new self.errors.TestError( test, got );
        }

        // If no problem
        return true;
    },

    // Evaluate test object
    eval: function( test ) {
        return eval( test.code );//$.globalEval( test.code );
    },

    // Default event handlers
    events: {
        complete: function( result ) {
            // Quiet
        },

        // Call when each test passed
        pass: function( test ) {
            // Quiet
        },

        // Call when each test failed
        fail: function( error ) {
            /*
            >>> e = new $.doctest.errors.TestError({
            ...   line: 12,
            ...   code: "13",
            ...   expected: "12"
            ... }, 13 );
            [object $.doctest.errors.TestError]

            >>> $.doctest.events.fail( e );

            Issue: catching the console output is impossible
            */
            var message;
            if ( error instanceof self.errors.TestError ) {
                message = [
                    "Line " + error.test.line,
                    "Failed example:",
                    ____ + error.test.code,
                    "Expected:",
                    ____ + error.test.expected,
                    "Got:",
                    ____ + error.got
                ].join( "\n" );
            } else {
                message = [
                    "Line " + error.test.line,
                    "Failed example:",
                    ____ + error.test.code,
                    "Expected:",
                    ____ + error.test.expected,
                    "Got:",
                    ____ + String( error )
                ].join( "\n" );
            }
            return self.console.error( message );
        }
    },

    flags: {
        SKIP: function() {
            return true;
        }
    },

    errors: {
        TestError: function( test, got ) {
            this.test = test;
            this.got = got;
            this.toString = function() {
                return "[object $.doctest.errors.TestError]";
            }
        },

        IndentationError: function( line ) {
            this.line = line;
            this.toString = function() {
                return "[object $.doctest.errors.IndentaionError]";
            }
        }
    }
});

})( jQuery );
<commit_msg>Implemented options<commit_after>/**
 * jquery.doctest.js beta
 * http://lab.heungsub.net/jquery.doctest.js
 * http://github.com/heungsub/jquery.doctest.js
 *
 * Copyright 2010, Heungsub Lee <lee@heungsub.net>
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * See also MIT-LICENSE.txt and GPL-LICENSE.txt
 */
(function( $, undefined ) {

var doctest = function( scriptUrl ) {
        return new doctest.fn.init( scriptUrl );
    },

    // Check if this line has flags
    flags = /\/\/doctest:\s*(.+)\s*$/,

    blank = /^\s*$/,

    // A tab string
    ____ = "    ",

    th = function( n ) {
        n = parseInt( n );
        if ( n == 1 ) {
            return "1st";
        } else if ( n == 2 ) {
            return "2nd";
        } else if ( n == 3 ) {
            return "3rd";
        } else {
            return n + "th";
        }
    },

    escapeRegExp = function( t ) {
        var specials = [
            "/", ".", "*", "+", "?", "|",
            "(", ")", "[", "]", "{", "}", "\\"
        ];
        var r = new RegExp( "(\\" + specials.join( "|\\" ) + ")", "g" );

        return t.replace( r, "\\$1" );
    };

// Alias for static methods
var self = doctest;

// Make jQuery.doctest
$.extend({ doctest: self });

// Methods
doctest.fn = doctest.prototype = {
    // Initialize
    init: function( scriptUrl, options ) {
        this.scriptUrl = scriptUrl;
        $.extend( this.options, options );
        this.result = self.testjs( scriptUrl, this.options );
        return this;
    },

    verbose: function() {
        this.pass(function( test ) {
            // Log seccess message
            self.console.log([
                "Trying:",
                ____ + test.code,
                "Expecting:",
                ____ + test.expected,
                "ok"
            ].join( "\n" ) );
        });

        this.complete(function( result ) {
            var message, msg,
                passedMsg = [], failedMsg = [], emptyMsg = [],
                passedItems = [], failedItems = [], emptyItems = [],
                tests = 0, passed = 0, failed = 0,
                item, nl = "\n";

            // for item in result
            for ( var i in result ) {
                item = result[ i ];

                tests += item.tests;
                passed += item.passed;
                failed += item.failed;

                msg = th( parseInt( i ) + 1 ) + " item(line " + item.line + ")";

                // Empty item
                if ( item.tests === 0 ) {
                    emptyItems.push( item );
                    msg = ____ + msg;
                    emptyMsg.push( msg );

                // All tests passed item
                } else if ( item.passed === item.tests ) {
                    passedItems.push( item );
                    msg = ____ + item.tests + " tests in " + msg;
                    passedMsg.push( msg );

                // This item had some failures
                } else {
                    failedItems.push( item );
                    msg = ____ + item.failed + " of " +
                        item.tests + " in " + msg;
                    failedMsg.push( msg );
                }
            }

            if ( i === undefined ) {
                return;
            }

            // Output empty items message
            if ( emptyItems.length ) {
                message = [];
                message.push( emptyItems.length + " items had no tests:" );
                message.push( emptyMsg.join( nl ) );
                self.console.log( message.join( nl ) );
            }

            // Output passed items message
            if ( passedItems.length ) {
                message = [];
                message.push( passedItems.length + " items passed all tests:" );
                message.push( passedMsg.join( nl ) );
                self.console.log( message.join( nl ) );
            }

            // Output failed items message with console.error
            if ( failedItems.length ) {
                message = [];
                message.push( failedItems.length + " items had failures:" );
                message.push( failedMsg.join( nl ) );
                self.console.warn( message.join( nl ) );
            }

            // Summary
            message = [
                tests + " tests in " + i + " items.",
                passed + " passed and " + failed + " failed."
            ];

            // All tests passed?
            if ( !failedItems.length ) {
                message.push( "Test passed." );
            }

            self.console.log( message.join( nl ) );
        });

        return this;
    },

    toString: function() {
        /**
        >>> ({});
        [object Object]
        >>> $.doctest();
        [object $.doctest]
        >>> $.doctest( "test/nothing-fn.toString.js" );
        [object $.doctest]
        */
        return "[object $.doctest]";
    }
};

// Give the init function the doctest prototype for later instantiation
doctest.fn.init.prototype = doctest.fn;

doctest.extend = doctest.fn.extend = $.extend;

doctest.extend({
    // Console should contains log(), error() method
    console: {
        log: $.proxy( console, "log" ),
        error: $.proxy( console, $.browser.webkit ? "error" : "warn" ),
        warn: $.proxy( console, "warn" )
    },

    // Default symbols
    symbols: {
        docStart: "/**",
        docEnd: "*/",
        prompt: ">>> ",
        continued: "... "
    },

    // Test the script file
    testjs: function( scriptUrl, options ) {
        /**
        >>> $.doctest.testjs( "test/nothing-testjs.js" );
        [object Object]
        >>> $( "script[src$=test/nothing-testjs.js]" ).length;
        1
        */

        options = self.options( options );

        var result = {},
            start = options.start,
            complete = options.complete;

        var run = function( code ) {
            var description = self.describe( code );

            // Set result
            $.extend( result, self.run( description, options ) );

            // Call complete event
            return complete( result );
        };

		// Handle $.doctest(), $.doctest( null ) or $.doctest( undefined )
        if ( !scriptUrl ) {
            return result;
            // run( $( "html" ).html() );

		// Handle $.doctest( "example.js" )
        } else {
            // Load the script
            if ( !$( "script[src=" + scriptUrl + "]" ).length ) {
                var markup = '<script class="doctest" type="text/javascript" '
                           + 'src="' + scriptUrl + '"></script>';
                $( markup ).appendTo( document.body );
            }

            // Get code of the script and run tests
            $.getScript( scriptUrl, run );
        }

        // Result is empty yet. It will come later
        return result;
    },

    // Run tests
    run: function( description, options ) {
        /**
        >>> d = [[{ line: 12, code: "1+1;", expected: "2", flags: []}]] //doctest: +SKIP
        >>> $.doctest.run( d )[ 0 ].passed;
        1
        */
        options = self.options( options );

        var result = [],
            test, item, line,
            passed, failed,

            // Events
            pass = options.pass,
            fail = options.fail;

        // for item in description
        for ( var i in description ) {
            item = description[ i ];
            passed = failed = 0,
            line = 1;

            // for test in item
            for ( var j in item ) {
                test = item[ j ];

                // Line number of the item
                if ( line === 1 ) {
                    line = parseInt( test.line );
                }

                try {
                    self.assert( test );
                    pass( test );
                    passed++;

                } catch ( error ) {
                    fail( error );
                    failed++;
                }
            }

            // Save result each item
            result.push({
                item: item,
                line: line,
                tests: item.length,
                passed: passed,
                failed: failed
            });
        }

        return result;
    },

    // Parse the code and return a description
    describe: function( code, options ) {
        /**
        >>> code = "/*" + "*\n>>> 1 + 1;\n2\n*" + "/"; //doctest: +SKIP
        >>> code.length;
        19
        >>> description = $.doctest.describe( code );
        [object Object]
        >>> description.length;
        1
        >>> description[0][0].code;
        1 + 1;
        >>> description[0][0].expected;
        2
        >>> description[0][0].line;
        2
        >>> description[0][0].flags.length;
        0
        */
        options = self.options( options );

        var lines = code.split( "\n" ),

            // Modes
            isItem = false, hasFlag = false,

            description = [], items = [], itemLines = [],
            line, finalLine, item, test, indent,

            _docStart = options.docStart,
            _docEnd = options.docEnd,
            _prompt = options.prompt,
            _continued = options.continued,
            docStart, docEnd, prompt, continued,

            spaces = "^\\s*",
            input = "(.+)$";

        docStart = new RegExp( spaces + escapeRegExp( _docStart ) );
        docEnd = new RegExp( spaces + escapeRegExp( _docEnd ) );
        prompt = new RegExp( spaces + escapeRegExp( _prompt ) + input );
        continued = new RegExp( spaces + escapeRegExp( _continued ) + input );

        for ( var i in lines ) {
            line = lines[ i ];

            // Validate indentation
            if ( indent !== undefined && !blank.exec( line ) ) {
                if ( line.search( indent ) !== 0 ) {
                    throw new self.errors.IndentationError( i );
                } else {
                    line = line.slice( indent.length );
                }
            }

            // When the start line of a docstring
            if ( docStart.exec( line ) ) {
                isItem = true;
                indent = line.slice( 0, line.match( spaces )[ 0 ].length );
                continue;

            // When the end line of a docstring
            } else if ( isItem && docEnd.exec( line ) ) {
                items.push( itemLines );
                itemLines = [];
                isItem = false;
                indent = undefined;
            }

            // When the line in a docstring
            if ( isItem ) {
                finalLine = parseInt( i ) + 1;
                itemLines[ finalLine ] = line;
            }
        }

        var keep = function( test ) {
            // Merge code
            test.code = test.code.join( "\n" );

            // Merge expected value
            if ( test.expected === undefined ) {
                test.expected = undefined;
            } else {
                test.expected = test.expected.join( "\n" );
            }

            item.push( test );
        }

        for ( var j in items ) {
            item = [];

            // for line in items[ j ]
            for ( var i in items[ j ] ) {
                i = parseInt( i );
                line = items[ j ][ i ];

                // When the line contains prompt
                if ( prompt.exec( line ) ) {
                    if ( test !== undefined ) {
                        keep( test );
                    }

                    test = {
                        line: i,
                        code: [line.match( prompt )[ 1 ]]
                    };

                    // Find flags
                    hasFlag = test.code[0].match( flags );

                    if ( hasFlag ) {
                        test.flags = hasFlag[ 1 ].split( /\s+/ );
                    } else {
                        test.flags = [];
                    }

                } else if ( test !== undefined ) {
                    // When the line contains continued prompt
                    if ( continued.exec( line ) ) {
                        test.code.push( line.match( continued )[1] );

                    // Blank line
                    } else if ( blank.exec( line ) ) {
                        keep( test );
                        test = undefined;

                    // When the line means expected value
                    } else {
                        if ( test.expected === undefined ) {
                            test.expected = [];
                        }
                        test.expected.push( line );
                    }
                }

                // The test must be keeped
                if ( test !== undefined && i === finalLine ) {
                    keep( test );
                }
            }

            description.push( item );
        }

        return description;
    },

    // Assert test object
    assert: function( test ) {
        /**
        >>> $.doctest.assert({ code: "1+'0';", expected: "10", flags: [] });
        true
        >>> $.doctest.assert({ code: "1+0;", expected: "1", flags: [] });
        true
        */
        var got, flag;

        // Evaluation!
        try {
            got = self.eval( test );
        } catch ( error ) {
            got = error;
        }

        // Process flags
        for ( var i in test.flags ) {

            // Get flag name
            flag = test.flags[ i ].replace( /^\+/, "" );

            // Check support the flag
            if ( self.flags[ flag ] !== undefined ) {
                var returned = self.flags[ flag ]( test );

                // Just return if flag returns anything
                if ( returned !== undefined ) {
                    return returned;
                }
            }
        }

        // Stringify and compare
        if ( String( test.expected ) !== String( got ) ) {
            throw new self.errors.TestError( test, got );
        }

        // If no problem
        return true;
    },

    // Evaluate test object
    eval: function( test ) {
        return eval( test.code );//$.globalEval( test.code );
    },

    // Default event handlers
    events: {
        complete: function( result ) {
            // Quiet
        },

        // Call when each test passed
        pass: function( test ) {
            // Quiet
        },

        // Call when each test failed
        fail: function( error ) {
            /*
            >>> e = new $.doctest.errors.TestError({
            ...   line: 12,
            ...   code: "13",
            ...   expected: "12"
            ... }, 13 );
            [object $.doctest.errors.TestError]

            >>> $.doctest.events.fail( e );

            Issue: catching the console output is impossible
            */
            var message;
            if ( error instanceof self.errors.TestError ) {
                message = [
                    "Line " + error.test.line,
                    "Failed example:",
                    ____ + error.test.code,
                    "Expected:",
                    ____ + error.test.expected,
                    "Got:",
                    ____ + error.got
                ].join( "\n" );
            } else {
                message = [
                    "Line " + error.test.line,
                    "Failed example:",
                    ____ + error.test.code,
                    "Expected:",
                    ____ + error.test.expected,
                    "Got:",
                    ____ + String( error )
                ].join( "\n" );
            }
            return self.console.error( message );
        }
    },

    flags: {
        SKIP: function() {
            return true;
        }
    },

    errors: {
        TestError: function( test, got ) {
            this.test = test;
            this.got = got;
            this.toString = function() {
                return "[object $.doctest.errors.TestError]";
            }
        },

        IndentationError: function( line ) {
            this.line = line;
            this.toString = function() {
                return "[object $.doctest.errors.IndentaionError]";
            }
        }
    },

    options: function( options ) {
        return $.extend( {}, self.events, self.symbols, options );
    }
});

})( jQuery );
<|endoftext|>


<commit_before>/*global window, document, jQuery */

/*
 * jQuery Form Example Plugin 1.6.1
 * Populate form inputs with example text that disappears on focus.
 *
 * e.g.
 *    $('input#name').example('Bob Smith');
 *    $('input[@title]').example(function () {
 *        return $(this).attr('title');
 *    });
 *    $('textarea#message').example('Type your message here', {
 *        className: 'example_text'
 *    });
 *
 * Copyright (c) Paul Mucur (http://mudge.name), 2007-2012.
 * Dual-licensed under the BSD (BSD-LICENSE.txt) and GPL (GPL-LICENSE.txt)
 * licenses.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
 * GNU General Public License for more details.
 */
(function ($) {
    'use strict';

    /* Define a helper function to attach event handlers in the best possible way
     * while remaining compatible with older versions of jQuery.
     */
    var attach;

    if ($.fn.on) {

        /* jQuery 1.7's `on` is the preferred way of attaching event handlers. */
        attach = function (selector, events, callback) {
            $('body').on(events, selector, callback);
        };
    } else if ($.fn.delegate) {

        /* Followed by jQuery 1.4.2's `delegate`. */
        attach = function (selector, events, callback) {
            $('body').delegate(selector, events, callback);
        };
    } else if ($.fn.live) {

        /* Then jQuery 1.3's `live`. */
        attach = function (selector, events, callback) {
            $(selector).live(events, callback);
        };
    } else {

        /* Fall back to jQuery 1.0's `bind`. */
        attach = function (selector, events, callback) {
            $(selector).bind(events, callback);
        };
    }

    $.fn.example = function (text, args) {

        var isCallback = $.isFunction(text),
            options    = $.extend({}, args, {example: text});

        return this.each(function () {

            var o, $this = $(this);

            /* Merge the plugin defaults with the given options and, if present,
             * any metadata.
             */
            if ($.metadata) {
                o = $.extend({}, $.fn.example.defaults, $this.metadata(), options);
            } else {
                o = $.extend({}, $.fn.example.defaults, options);
            }

            /* The following event handlers only need to be bound once
             * per class name. In order to do this, an array of used
             * class names is stored and checked on each use of the plugin.
             * If the class name is in the array then this whole section
             * is skipped. If not, the events are bound and the class name
             * added to the array.
             */
            if (!$.fn.example.boundClassNames[o.className]) {

                /* Because Gecko-based browsers cache form values
                 * but ignore all other attributes such as class, all example
                 * values must be cleared on page unload to prevent them from
                 * being saved.
                 */
                $(window).bind('unload.example', function () {
                    $('.' + o.className).val('').removeClass(o.className);
                });

                /* Clear fields that are still examples before any form is submitted
                 * otherwise those examples will be sent along as well.
                 *
                 * Where possible, attempt to delegate this event handler so that
                 * all forms are bound now and in the future.
                 */
                attach('form', 'submit.example example:resetForm', function () {
                    $(this).find('.' + o.className).val('').removeClass(o.className);
                });

                /* Add the class name to the array. */
                $.fn.example.boundClassNames[o.className] = true;
            }

            /* Several browsers will cache form values even if they are cleared
             * on unload, so this will clear any value that matches the example
             * text and hasn't been specified in the value attribute.
             *
             * If a callback is used, it is not possible or safe to predict
             * what the example text is going to be so all non-default values
             * are cleared. This means that caching is effectively disabled for
             * that field.
             *
             * Many thanks to Klaus Hartl for helping resolve this issue.
             */
            if ($this.val() !== this.defaultValue && (isCallback || $this.val() === o.example)) {
                $this.val(this.defaultValue);
            }

            /* Initially place the example text in the field if it is empty
             * and doesn't have focus yet.
             */
            if ($this.val() === '' && !$this.is(':focus')) {

                /* The text argument can now be a function; if this is the case,
                 * call it, passing the current element as `this`.
                 */
                $this.addClass(o.className).val(isCallback ? o.example.call(this) : o.example);
            }

            /* Make the example text disappear when someone focuses.
             *
             * To determine whether the value of the field is an example or not,
             * check for the example class name only; comparing the actual value
             * seems wasteful and can stop people from using example values as real
             * input.
             */
            $this.bind('focus.example', function () {

                if ($(this).is('.' + o.className)) {
                    $(this).val('').removeClass(o.className);
                }

            }).bind('change.example', function () {

                if ($(this).is('.' + o.className)) {
                    $(this).removeClass(o.className);
                }

            }).bind('blur.example', function () {

                if ($(this).val() === '') {

                    /* Re-evaluate the callback function every time the user
                     * blurs the field without entering anything. While this
                     * is not as efficient as caching the value, it allows for
                     * more dynamic applications of the plugin.
                     */
                    $(this).addClass(o.className).val(isCallback ? o.example.call(this) : o.example);
                }

            });
        });
    };

    /* Users can override the defaults for the plugin like so:
     *
     *     $.fn.example.defaults.className = 'not_example';
     */
    $.fn.example.defaults = {
        className: 'example'
    };

    /* All the class names used are stored as keys in the following array. */
    $.fn.example.boundClassNames = [];

}(jQuery));
<commit_msg>onChange: do not check class name<commit_after>/*global window, document, jQuery */

/*
 * jQuery Form Example Plugin 1.6.1
 * Populate form inputs with example text that disappears on focus.
 *
 * e.g.
 *    $('input#name').example('Bob Smith');
 *    $('input[@title]').example(function () {
 *        return $(this).attr('title');
 *    });
 *    $('textarea#message').example('Type your message here', {
 *        className: 'example_text'
 *    });
 *
 * Copyright (c) Paul Mucur (http://mudge.name), 2007-2012.
 * Dual-licensed under the BSD (BSD-LICENSE.txt) and GPL (GPL-LICENSE.txt)
 * licenses.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
 * GNU General Public License for more details.
 */
(function ($) {
    'use strict';

    /* Define a helper function to attach event handlers in the best possible way
     * while remaining compatible with older versions of jQuery.
     */
    var attach;

    if ($.fn.on) {

        /* jQuery 1.7's `on` is the preferred way of attaching event handlers. */
        attach = function (selector, events, callback) {
            $('body').on(events, selector, callback);
        };
    } else if ($.fn.delegate) {

        /* Followed by jQuery 1.4.2's `delegate`. */
        attach = function (selector, events, callback) {
            $('body').delegate(selector, events, callback);
        };
    } else if ($.fn.live) {

        /* Then jQuery 1.3's `live`. */
        attach = function (selector, events, callback) {
            $(selector).live(events, callback);
        };
    } else {

        /* Fall back to jQuery 1.0's `bind`. */
        attach = function (selector, events, callback) {
            $(selector).bind(events, callback);
        };
    }

    $.fn.example = function (text, args) {

        var isCallback = $.isFunction(text),
            options    = $.extend({}, args, {example: text});

        return this.each(function () {

            var o, $this = $(this);

            /* Merge the plugin defaults with the given options and, if present,
             * any metadata.
             */
            if ($.metadata) {
                o = $.extend({}, $.fn.example.defaults, $this.metadata(), options);
            } else {
                o = $.extend({}, $.fn.example.defaults, options);
            }

            /* The following event handlers only need to be bound once
             * per class name. In order to do this, an array of used
             * class names is stored and checked on each use of the plugin.
             * If the class name is in the array then this whole section
             * is skipped. If not, the events are bound and the class name
             * added to the array.
             */
            if (!$.fn.example.boundClassNames[o.className]) {

                /* Because Gecko-based browsers cache form values
                 * but ignore all other attributes such as class, all example
                 * values must be cleared on page unload to prevent them from
                 * being saved.
                 */
                $(window).bind('unload.example', function () {
                    $('.' + o.className).val('').removeClass(o.className);
                });

                /* Clear fields that are still examples before any form is submitted
                 * otherwise those examples will be sent along as well.
                 *
                 * Where possible, attempt to delegate this event handler so that
                 * all forms are bound now and in the future.
                 */
                attach('form', 'submit.example example:resetForm', function () {
                    $(this).find('.' + o.className).val('').removeClass(o.className);
                });

                /* Add the class name to the array. */
                $.fn.example.boundClassNames[o.className] = true;
            }

            /* Several browsers will cache form values even if they are cleared
             * on unload, so this will clear any value that matches the example
             * text and hasn't been specified in the value attribute.
             *
             * If a callback is used, it is not possible or safe to predict
             * what the example text is going to be so all non-default values
             * are cleared. This means that caching is effectively disabled for
             * that field.
             *
             * Many thanks to Klaus Hartl for helping resolve this issue.
             */
            if ($this.val() !== this.defaultValue && (isCallback || $this.val() === o.example)) {
                $this.val(this.defaultValue);
            }

            /* Initially place the example text in the field if it is empty
             * and doesn't have focus yet.
             */
            if ($this.val() === '' && !$this.is(':focus')) {

                /* The text argument can now be a function; if this is the case,
                 * call it, passing the current element as `this`.
                 */
                $this.addClass(o.className).val(isCallback ? o.example.call(this) : o.example);
            }

            /* Make the example text disappear when someone focuses.
             *
             * To determine whether the value of the field is an example or not,
             * check for the example class name only; comparing the actual value
             * seems wasteful and can stop people from using example values as real
             * input.
             */
            $this.bind('focus.example', function () {

                if ($(this).is('.' + o.className)) {
                    $(this).val('').removeClass(o.className);
                }

            }).bind('change.example', function () {

                $(this).removeClass(o.className);

            }).bind('blur.example', function () {

                if ($(this).val() === '') {

                    /* Re-evaluate the callback function every time the user
                     * blurs the field without entering anything. While this
                     * is not as efficient as caching the value, it allows for
                     * more dynamic applications of the plugin.
                     */
                    $(this).addClass(o.className).val(isCallback ? o.example.call(this) : o.example);
                }

            });
        });
    };

    /* Users can override the defaults for the plugin like so:
     *
     *     $.fn.example.defaults.className = 'not_example';
     */
    $.fn.example.defaults = {
        className: 'example'
    };

    /* All the class names used are stored as keys in the following array. */
    $.fn.example.boundClassNames = [];

}(jQuery));
<|endoftext|>


<commit_before>// ==ClosureCompiler==
// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/jquery-1.9.js
// ==/ClosureCompiler==
/** @preserve jquery.numeric
 * git://github.com/danielgindi/jquery.numeric.git
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('jquery.numeric', ['jquery'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('jquery'));
    } else {
        /*root.numeric = */factory(root.jQuery);
    }
}(this, function ($) {
    'use strict';

    /** @const */
    var DECIMAL_SEPARATOR = (1.1).toLocaleString().match(/\d(.*?)\d/)[1];

    /** @const */
    var DECIMAL_SEPARATOR_REGEX = new RegExp('\\' + regexEscape(DECIMAL_SEPARATOR), 'g');

    function regexEscape (str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    }

    // Edge has a bug from 2014:
    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/669685/
    // So we need to use deep feature detection.
    var isSupported = (function() {
        var mem = null;
        return function () {
            if (mem === null) {
                var input = document.createElement('input');
                input.type = 'number';
                input.value = '1';
                mem = input.valueAsNumber === 1;
            }
            return mem;
        };
    })();

    function cleanupInput (input, decimal, negative) {
        var value = input.value,
            strippedValue = '',
            hasDecimal = false;

        if (input.type !== 'number') {
            var selectionStart = input.selectionStart;
            var selectionEnd = input.selectionEnd;
        }

        var rgx = new RegExp((negative ? '[-+]?' : '[+]?') + '(?:[0-9]+(?:' +
                             (decimal ? regexEscape(decimal) : '') + '[0-9]*)?|' +
                             (decimal ? regexEscape(decimal) : '') + '[0-9]*)', '');

        for (var i = 0, c; i < value.length; i++) {
            c = value.charAt(i);
            if ((c >= '0' && c <= '9') || (negative && c === '-' && !strippedValue.length)) {
                strippedValue += c;
            } else if (decimal && value.substr(i, decimal.length) === decimal && !hasDecimal) {
                hasDecimal = true;
                strippedValue += decimal;
                i += decimal.length - 1;
            } else {
                if (i <= selectionStart) {
                    selectionStart--
                }
                if (i <= selectionEnd) {
                    selectionEnd--
                }
            }
        }

        if (value !== strippedValue) {
            input.value = strippedValue;
            if (input.type !== 'number') {
                input.selectionStart = selectionStart;
                input.selectionEnd = selectionEnd;
            }
            return true;
        }

        return false;
    };

    /** @expose */
    $.fn.numeric = function (config) {
        this.each(function(index,item) {
            var isInput = item.tagName === 'INPUT', attr;
            //if (isInput && item.type === 'number' && 'valueAsNumber' in item) return;

            if (typeof config === 'boolean') {
                config = { /** @expose */ decimal: config };
            }
            config = config || {};

            if (typeof config.negative === 'undefined') {
                // If the min attribute does not allow negatives, then disable the negative feature
                /** @expose */ config.negative = isInput ? (item.getAttribute('min') ? parseFloat(item.getAttribute('min')) < 0 : true) : true;
            }

            var decimal = (config.decimal === false)
                ? ''
                : ((typeof config.decimal === 'string' && config.decimal) ? config.decimal : DECIMAL_SEPARATOR);
            var negative = !!config.negative;

            // If we're going to use the real 'valueAsNumber', force using the native decimal separator
            if (decimal && this.tagName === 'INPUT' && this.type === 'number' && 'valueAsNumber' in this && isSupported()) {
                decimal = DECIMAL_SEPARATOR;
            }

            // If the step attribute does not allow decimals
            if (typeof config.decimal === 'undefined' && isInput) {
                attr = item.getAttribute('step');
                if (attr && attr !== 'any' && attr.indexOf('.') === -1) {
                    decimal = '';
                }
            }

            return $(item)
                    .data('numeric.decimal', decimal)
                    .data('numeric.negative', negative)
                    .on('keypress.numericValue', $.fn.numeric._event)
                    .on('keyup.numericValue', $.fn.numeric._event)
                    .on('blur.numericValue', $.fn.numeric._event)
                    .on('input.numericValue', $.fn.numeric._event);
        });
        return this;
    };

    /** @expose */
    $.fn.valueAsNumber = function () {
        if (!this.length) return null;
        var args = arguments;
        if (args.length) {
            return this.each(function(){
                if (this.tagName === 'INPUT' && this.type === 'number' && 'valueAsNumber' in this && isSupported()) {
                    this.valueAsNumber = args[0];
                } else {
                    this.value = args[0].toString().replace(/\./, $.data(this, 'numeric.decimal') || DECIMAL_SEPARATOR);
                }
            });
        } else {
            if (this[0].tagName === 'INPUT' && this[0].type === 'number' && 'valueAsNumber' in this[0] && isSupported()) {
                return this[0].valueAsNumber;
            } else {
                var decimal = this.data('numeric.decimal');
                if (decimal) {
                    decimal = new RegExp(regexEscape(decimal), 'g');
                } else {
                    decimal = DECIMAL_SEPARATOR_REGEX;
                }
                return this[0].value === '' ? null : parseFloat(this[0].value.replace(decimal, '.'));
            }
        }
    };

    $.fn.numeric._event = function (e) {
        var decimal = $.data(this, 'numeric.decimal'),
            negative = $.data(this, 'numeric.negative');

        cleanupInput(this, decimal, negative);
    };

    /** @expose */
    $.fn.removeNumeric = function () {
        return this
               .removeData('numeric.decimal')
               .removeData('numeric.negative')
               .off('.numericValue');
    };

}));
<commit_msg>Support valueAsNumber(null)<commit_after>// ==ClosureCompiler==
// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/jquery-1.9.js
// ==/ClosureCompiler==
/** @preserve jquery.numeric
 * git://github.com/danielgindi/jquery.numeric.git
 */
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('jquery.numeric', ['jquery'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('jquery'));
    } else {
        /*root.numeric = */factory(root.jQuery);
    }
}(this, function ($) {
    'use strict';

    /** @const */
    var DECIMAL_SEPARATOR = (1.1).toLocaleString().match(/\d(.*?)\d/)[1];

    /** @const */
    var DECIMAL_SEPARATOR_REGEX = new RegExp('\\' + regexEscape(DECIMAL_SEPARATOR), 'g');

    function regexEscape (str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
    }

    // Edge has a bug from 2014:
    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/669685/
    // So we need to use deep feature detection.
    var isSupported = (function() {
        var mem = null;
        return function () {
            if (mem === null) {
                var input = document.createElement('input');
                input.type = 'number';
                input.value = '1';
                mem = input.valueAsNumber === 1;
            }
            return mem;
        };
    })();

    function cleanupInput (input, decimal, negative) {
        var value = input.value,
            strippedValue = '',
            hasDecimal = false;

        if (input.type !== 'number') {
            var selectionStart = input.selectionStart;
            var selectionEnd = input.selectionEnd;
        }

        var rgx = new RegExp((negative ? '[-+]?' : '[+]?') + '(?:[0-9]+(?:' +
                             (decimal ? regexEscape(decimal) : '') + '[0-9]*)?|' +
                             (decimal ? regexEscape(decimal) : '') + '[0-9]*)', '');

        for (var i = 0, c; i < value.length; i++) {
            c = value.charAt(i);
            if ((c >= '0' && c <= '9') || (negative && c === '-' && !strippedValue.length)) {
                strippedValue += c;
            } else if (decimal && value.substr(i, decimal.length) === decimal && !hasDecimal) {
                hasDecimal = true;
                strippedValue += decimal;
                i += decimal.length - 1;
            } else {
                if (i <= selectionStart) {
                    selectionStart--
                }
                if (i <= selectionEnd) {
                    selectionEnd--
                }
            }
        }

        if (value !== strippedValue) {
            input.value = strippedValue;
            if (input.type !== 'number') {
                input.selectionStart = selectionStart;
                input.selectionEnd = selectionEnd;
            }
            return true;
        }

        return false;
    };

    /** @expose */
    $.fn.numeric = function (config) {
        this.each(function(index,item) {
            var isInput = item.tagName === 'INPUT', attr;
            //if (isInput && item.type === 'number' && 'valueAsNumber' in item) return;

            if (typeof config === 'boolean') {
                config = { /** @expose */ decimal: config };
            }
            config = config || {};

            if (typeof config.negative === 'undefined') {
                // If the min attribute does not allow negatives, then disable the negative feature
                /** @expose */ config.negative = isInput ? (item.getAttribute('min') ? parseFloat(item.getAttribute('min')) < 0 : true) : true;
            }

            var decimal = (config.decimal === false)
                ? ''
                : ((typeof config.decimal === 'string' && config.decimal) ? config.decimal : DECIMAL_SEPARATOR);
            var negative = !!config.negative;

            // If we're going to use the real 'valueAsNumber', force using the native decimal separator
            if (decimal && this.tagName === 'INPUT' && this.type === 'number' && 'valueAsNumber' in this && isSupported()) {
                decimal = DECIMAL_SEPARATOR;
            }

            // If the step attribute does not allow decimals
            if (typeof config.decimal === 'undefined' && isInput) {
                attr = item.getAttribute('step');
                if (attr && attr !== 'any' && attr.indexOf('.') === -1) {
                    decimal = '';
                }
            }

            return $(item)
                    .data('numeric.decimal', decimal)
                    .data('numeric.negative', negative)
                    .on('keypress.numericValue', $.fn.numeric._event)
                    .on('keyup.numericValue', $.fn.numeric._event)
                    .on('blur.numericValue', $.fn.numeric._event)
                    .on('input.numericValue', $.fn.numeric._event);
        });
        return this;
    };

    /** @expose */
    $.fn.valueAsNumber = function () {
        if (!this.length) return null;
        var args = arguments;
        if (args.length) {
            return this.each(function(){
                if (this.tagName === 'INPUT' && this.type === 'number' && 'valueAsNumber' in this && isSupported()) {
                    this.valueAsNumber = args[0];
                } else {
                    this.value = (args[0] === null || args[0] === undefined)
                        ? ''
                        : args[0].toString().replace(/\./, $.data(this, 'numeric.decimal') || DECIMAL_SEPARATOR);
                }
            });
        } else {
            if (this[0].tagName === 'INPUT' && this[0].type === 'number' && 'valueAsNumber' in this[0] && isSupported()) {
                return this[0].valueAsNumber;
            } else {
                var decimal = this.data('numeric.decimal');
                if (decimal) {
                    decimal = new RegExp(regexEscape(decimal), 'g');
                } else {
                    decimal = DECIMAL_SEPARATOR_REGEX;
                }
                return this[0].value === '' ? null : parseFloat(this[0].value.replace(decimal, '.'));
            }
        }
    };

    $.fn.numeric._event = function (e) {
        var decimal = $.data(this, 'numeric.decimal'),
            negative = $.data(this, 'numeric.negative');

        cleanupInput(this, decimal, negative);
    };

    /** @expose */
    $.fn.removeNumeric = function () {
        return this
               .removeData('numeric.decimal')
               .removeData('numeric.negative')
               .off('.numericValue');
    };

}));
<|endoftext|>


<commit_before>'use strict';

var chai = require('chai');
var api = require('../index.js');

require('dotenv').load();
chai.should();

describe('Unsplash API public endpoints', function() {
  api.init(process.env.CLIENT_ID);

  describe('User', function() {
    describe('getUserPhotos', function() {
      it('should return without err and with an empty array', function(done) {
        api.getUserPhotos('fletcher_hills', function(err, photos) {
          if (err) return done(err);

          photos.should.be.instanceOf(Array);

          done();
        });
      });

      it('should return with invalid username error', function(done) {
        api.getUserPhotos('01234', function(err, photos) {

          err.should.exist;
          chai.expect(photos).to.not.exist;

          done();
        });
      })
    });

    describe('getUserByName', function() {
      it('should return without err and with a user', function(done) {
        api.getUserByName('fletcher_hills', function(err, user) {
          if (err) return done(err);

          user.should.be.ok;

          done();
        });
      });

      it('should return with invalid username error', function(done) {
        api.getUserByName('01234', function(err, user) {

          err.should.exist;
          chai.expect(user).to.not.exist;

          done();
        });
      });
    });
  });

  describe('Photos', function() {
     describe('getPhotos', function() {
        it('should return without err and with the first 10 pictures', function(done) {
           api.getPhotos(null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              done();
           });
        });

        it('should return without err and with the first 20 pictures', function(done) {
           api.getPhotos(null, 20, function(err, photos, link) {
               if (err) return done(err);

               photos.should.be.instanceOf(Array);
               photos.should.have.length(20);

               done();
           });
        });

        it('should return without err and with the second 10 pictures', function(done) {
           api.getPhotos(2, null, function(err, photos, link) {
               if (err) return done(err);

               photos.should.be.instanceOf(Array);
               photos.should.have.length(10);

               link.should.contain('<https://api.unsplash.com/photos?page=1>; rel="prev",');

               done();
           });
        });
     });

     describe('searchPhotos', function() {
        it('should return without err and with the first 10 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return first 10 pictures from query "beach" with "nature" category applied', function(done) {
           api.searchPhotos('beach', [4], null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');
              link.should.contain('category=4');

              done();
           });
        });

        it('should return first 10 pictures from query "beach" with "nature" and "people" categories applied', function(done) {
           api.searchPhotos('beach', [4, 6], null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return without err and the second 10 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, 2, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('<https://api.unsplash.com/photos/search?page=1&query=beach>; rel="prev",');

              done();
           });
        });

        it('should return without err and with the first 20 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, null, 20, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(20);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return with invalid query error', function(done) {
           api.searchPhotos(null, null, null, null, function(err, photos, link) {
              err.should.exist;
              chai.expect(photos).to.not.exist;
              chai.expect(link).to.not.exist;

              done();
           });
        });
     });

     describe('getPhoto', function() {
        it('should return without err and single photo from account "cacobjopus"', function(done) {
           api.getPhoto('9gkKZO-xezM', null, null, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');

              done();
           });
        });

        it('should return without err and single photo with custom width', function(done) {
           api.getPhoto('9gkKZO-xezM', 500, null, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return without err and single photo with custom height', function(done) {
           api.getPhoto('9gkKZO-xezM', null, 250, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return without err and single photo with custom dimensions', function(done) {
           api.getPhoto('9gkKZO-xezM', null, null, [10, 10, 500, 250], function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return invalid id err', function(done) {
           api.getPhoto('1', null, null, null, function(err, photo) {
              err.should.exist;
              chai.expect(photo).to.not.exist;

              done();
           });
        });
     });
  });

  describe('Categories', function() {
    describe('getAllCategories', function() {
      it('should return without err and an array of categories', function(done) {
        api.getAllCategories(function(err, cats) {
          if (err) return done(err);

          cats.should.be.instanceOf(Array);

          done();
        });
      })
    });

    describe('getCategory', function() {
      it('should return without err and category #2 info', function(done) {
        api.getCategory('2', function(err, category) {
          if (err) return done(err);

          category.should.be.ok;
          category.id.should.equal(2);

          done();
        });
      });

      it('should return with invalid ID error', function(done) {
        api.getCategory('-1', function(err, category) {
          err.should.exist;
          chai.expect(category).to.not.exist;

          done();
        });
      });
    });

    describe('getCategoryPhotos', function() {
      it('should return without err and an array of the first 10 photos', function(done) {
        api.getCategoryPhotos('2', null, null, function(err, photos, link) {
          if (err) return done(err);

          photos.should.be.instanceOf(Array);
          photos.should.have.length(10);
          link.should.be.ok;

          done();
        });
      });

      it('should return without err and an array of the second 10 photos', function(done) {
         api.getCategoryPhotos('2', 2, null, function(err, photos, link) {
            if (err) return done(err);

            photos.should.be.instanceOf(Array);
            photos.should.have.length(10);
            link.should.be.ok;
            link.should.contain('<https://api.unsplash.com/categories/2/photos?page=3>; rel="next"');

            done();
         });
      });

      it('should return without err and an array of the first 20 photos', function(done) {
         api.getCategoryPhotos('2', null, 20, function(err, photos, link) {
            if (err) return done(err);

            photos.should.be.instanceOf(Array);
            photos.should.have.length(20);
            link.should.be.ok;

            done();
         });
      });

      it('should return with an invalid ID error', function(done) {
        api.getCategoryPhotos('-1', null, null, function(err, photos, link) {
          err.should.exist;

          chai.expect(photos).to.not.exist;
          chai.expect(link).to.not.exist;

          done();
        });
      });
    });
  });

  describe('Curated Batches', function() {
     describe('getCuratedBatches', function() {
        it('should return without err and the first page of 10 curated batches', function(done) {
           api.getCuratedBatches(null, null, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(10);

              done();
           });
        });

        it('should return without err and the second page of 10 curated batches', function(done) {
           api.getCuratedBatches(2, null, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(10);
              link.should.contain('<https://api.unsplash.com/curated_batches?page=3>; rel="next"');

              done();
           });
        });

        it('should return without err and the first page of 20 curated batches', function(done) {
           api.getCuratedBatches(null, 20, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(20);

              done();
           });
        });
     });

     describe('getCuratedBatch', function() {
        it('should return without err and a curated batch object', function(done) {
           api.getCuratedBatch(1, function(err, batch) {
              if (err) return done(err);

              batch.should.be.ok;
              batch.id.should.equal(1);

              done();
           });
        });

        it('should return with an invalid ID error', function(done) {
           api.getCuratedBatch(-1, function(err, batch) {
              err.should.exist;

              chai.expect(batch).to.not.exist;

              done();
           });
        });
     });
  });
});<commit_msg>add getCuratedBatchPhotos tests<commit_after>'use strict';

var chai = require('chai');
var api = require('../index.js');

require('dotenv').load();
chai.should();

describe('Unsplash API public endpoints', function() {
  api.init(process.env.CLIENT_ID);

  describe('User', function() {
    describe('getUserPhotos', function() {
      it('should return without err and with an empty array', function(done) {
        api.getUserPhotos('fletcher_hills', function(err, photos) {
          if (err) return done(err);

          photos.should.be.instanceOf(Array);

          done();
        });
      });

      it('should return with invalid username error', function(done) {
        api.getUserPhotos('01234', function(err, photos) {

          err.should.exist;
          chai.expect(photos).to.not.exist;

          done();
        });
      })
    });

    describe('getUserByName', function() {
      it('should return without err and with a user', function(done) {
        api.getUserByName('fletcher_hills', function(err, user) {
          if (err) return done(err);

          user.should.be.ok;

          done();
        });
      });

      it('should return with invalid username error', function(done) {
        api.getUserByName('01234', function(err, user) {

          err.should.exist;
          chai.expect(user).to.not.exist;

          done();
        });
      });
    });
  });

  describe('Photos', function() {
     describe('getPhotos', function() {
        it('should return without err and with the first 10 pictures', function(done) {
           api.getPhotos(null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              done();
           });
        });

        it('should return without err and with the first 20 pictures', function(done) {
           api.getPhotos(null, 20, function(err, photos, link) {
               if (err) return done(err);

               photos.should.be.instanceOf(Array);
               photos.should.have.length(20);

               done();
           });
        });

        it('should return without err and with the second 10 pictures', function(done) {
           api.getPhotos(2, null, function(err, photos, link) {
               if (err) return done(err);

               photos.should.be.instanceOf(Array);
               photos.should.have.length(10);

               link.should.contain('<https://api.unsplash.com/photos?page=1>; rel="prev",');

               done();
           });
        });
     });

     describe('searchPhotos', function() {
        it('should return without err and with the first 10 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return first 10 pictures from query "beach" with "nature" category applied', function(done) {
           api.searchPhotos('beach', [4], null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');
              link.should.contain('category=4');

              done();
           });
        });

        it('should return 10 pictures from query "beach" with "nature" and "people" categories applied', function(done) {
           api.searchPhotos('beach', [4, 6], null, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return without err and the second 10 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, 2, null, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              link.should.contain('<https://api.unsplash.com/photos/search?page=1&query=beach>; rel="prev",');

              done();
           });
        });

        it('should return without err and with the first 20 pictures from query "beach"', function(done) {
           api.searchPhotos('beach', null, null, 20, function(err, photos, link) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(20);

              link.should.contain('query=beach');

              done();
           });
        });

        it('should return with invalid query error', function(done) {
           api.searchPhotos(null, null, null, null, function(err, photos, link) {
              err.should.exist;
              chai.expect(photos).to.not.exist;
              chai.expect(link).to.not.exist;

              done();
           });
        });
     });

     describe('getPhoto', function() {
        it('should return without err and single photo from account "cacobjopus"', function(done) {
           api.getPhoto('9gkKZO-xezM', null, null, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');

              done();
           });
        });

        it('should return without err and single photo with custom width', function(done) {
           api.getPhoto('9gkKZO-xezM', 500, null, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return without err and single photo with custom height', function(done) {
           api.getPhoto('9gkKZO-xezM', null, 250, null, function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return without err and single photo with custom dimensions', function(done) {
           api.getPhoto('9gkKZO-xezM', null, null, [10, 10, 500, 250], function(err, photo) {
              if (err) return done(err);

              photo.should.be.ok;
              photo.id.should.equal('9gkKZO-xezM');
              photo.urls.custom.should.be.ok;

              done();
           });
        });

        it('should return invalid id err', function(done) {
           api.getPhoto('1', null, null, null, function(err, photo) {
              err.should.exist;
              chai.expect(photo).to.not.exist;

              done();
           });
        });
     });
  });

  describe('Categories', function() {
    describe('getAllCategories', function() {
      it('should return without err and an array of categories', function(done) {
        api.getAllCategories(function(err, cats) {
          if (err) return done(err);

          cats.should.be.instanceOf(Array);

          done();
        });
      })
    });

    describe('getCategory', function() {
      it('should return without err and category #2 info', function(done) {
        api.getCategory('2', function(err, category) {
          if (err) return done(err);

          category.should.be.ok;
          category.id.should.equal(2);

          done();
        });
      });

      it('should return with invalid ID error', function(done) {
        api.getCategory('-1', function(err, category) {
          err.should.exist;
          chai.expect(category).to.not.exist;

          done();
        });
      });
    });

    describe('getCategoryPhotos', function() {
      it('should return without err and an array of the first 10 photos', function(done) {
        api.getCategoryPhotos('2', null, null, function(err, photos, link) {
          if (err) return done(err);

          photos.should.be.instanceOf(Array);
          photos.should.have.length(10);
          link.should.be.ok;

          done();
        });
      });

      it('should return without err and an array of the second 10 photos', function(done) {
         api.getCategoryPhotos('2', 2, null, function(err, photos, link) {
            if (err) return done(err);

            photos.should.be.instanceOf(Array);
            photos.should.have.length(10);
            link.should.be.ok;
            link.should.contain('<https://api.unsplash.com/categories/2/photos?page=3>; rel="next"');

            done();
         });
      });

      it('should return without err and an array of the first 20 photos', function(done) {
         api.getCategoryPhotos('2', null, 20, function(err, photos, link) {
            if (err) return done(err);

            photos.should.be.instanceOf(Array);
            photos.should.have.length(20);
            link.should.be.ok;

            done();
         });
      });

      it('should return with an invalid ID error', function(done) {
        api.getCategoryPhotos('-1', null, null, function(err, photos, link) {
          err.should.exist;

          chai.expect(photos).to.not.exist;
          chai.expect(link).to.not.exist;

          done();
        });
      });
    });
  });

  describe('Curated Batches', function() {
     describe('getCuratedBatches', function() {
        it('should return without err and the first page of 10 curated batches', function(done) {
           api.getCuratedBatches(null, null, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(10);

              done();
           });
        });

        it('should return without err and the second page of 10 curated batches', function(done) {
           api.getCuratedBatches(2, null, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(10);
              link.should.contain('<https://api.unsplash.com/curated_batches?page=3>; rel="next"');

              done();
           });
        });

        it('should return without err and the first page of 20 curated batches', function(done) {
           api.getCuratedBatches(null, 20, function(err, batches, link) {
              if (err) return done(err);

              batches.should.be.instanceOf(Array);
              batches.should.have.length(20);

              done();
           });
        });
     });

     describe('getCuratedBatch', function() {
        it('should return without err and a curated batch object', function(done) {
           api.getCuratedBatch(1, function(err, batch) {
              if (err) return done(err);

              batch.should.be.ok;
              batch.id.should.equal(1);

              done();
           });
        });

        it('should return with an invalid ID error', function(done) {
           api.getCuratedBatch(-1, function(err, batch) {
              err.should.exist;

              chai.expect(batch).to.not.exist;

              done();
           });
        });
     });

     describe('getCuratedBatchPhotos', function() {
        it('should return without err and an array of 10 photos', function(done) {
           api.getCuratedBatchPhotos(1, function(err, photos) {
              if (err) return done(err);

              photos.should.be.instanceOf(Array);
              photos.should.have.length(10);

              done();
           });
        });

        it('should return with an invalid ID error', function(done) {
           api.getCuratedBatchPhotos(-1, function(err, photos) {
              err.should.exist;

              chai.expect(photos).to.not.exist;

              done();
           });
        });
     });
  });
});<|endoftext|>