Tester = function Tester(casper, options) {
    "use strict";
    /*eslint max-statements:0*/
    if (!utils.isCasperObject(casper)) {
        throw new CasperError("Tester needs a Casper instance");
    }

    // self reference
    var self = this;

    // casper reference
    this.casper = casper;

    // public properties
    this._setUp = undefined;
    this._tearDown = undefined;
    this.aborted = false;
    this.executed = 0;
    this.currentTestFile = null;
    this.currentTestStartTime = new Date();
    this.currentSuite = undefined;
    this.currentSuiteNum = 0;
    this.lastAssertTime = 0;
    this.loadIncludes = {
        includes: [],
        pre:      [],
        post:     []
    };
    this.options = utils.mergeObjects({
        concise:  false,  // concise output?
        failFast: false,  // terminates a suite as soon as a test fails?
        failText: "FAIL", // text to use for a failed test
        passText: "PASS", // text to use for a succesful test
        skipText: "SKIP", // text to use for a skipped test
        save:     false,  // false to not save 
        pad:      80    , // maximum number of chars for a result line
        warnText: "WARN"  // text to use for a dubious test
    }, options);
    this.queue = [];
    this.running = false;
    this.started = false;
    this.suiteResults = new TestSuiteResult();

    this.on('success', function onSuccess(success) {
        var timeElapsed = new Date() - this.currentTestStartTime;
        this.currentSuite.addSuccess(success, timeElapsed - this.lastAssertTime);
        this.lastAssertTime = timeElapsed;
    });

    this.on('skipped', function onSkipped(skipped) {
        var timeElapsed = new Date() - this.currentTestStartTime;
        this.currentSuite.addSkip(skipped, timeElapsed - this.lastAssertTime);
        this.lastAssertTime = timeElapsed;
    });

    this.on('fail', function onFail(failure) {
        // export
        var valueKeys = Object.keys(failure.values),
            timeElapsed = new Date() - this.currentTestStartTime;
        this.currentSuite.addFailure(failure, timeElapsed - this.lastAssertTime);
        this.lastAssertTime = timeElapsed;
        // special printing
        if (failure.type) {
            this.comment('   type: ' + failure.type);
        }
        if (failure.file) {
            this.comment('   file: ' + failure.file + (failure.line ? ':' + failure.line : ''));
        }
        if (failure.lineContents) {
            this.comment('   code: ' + failure.lineContents);
        }
        if (!failure.values || valueKeys.length === 0) {
            return;
        }
        valueKeys.forEach(function(name) {
            this.comment(f('   %s: %s', name, utils.formatTestValue(failure.values[name], name)));
        }.bind(this));
        // check for fast failing
        if (this.options.failFast) {
            return this.terminate('--fail-fast: aborted all remaining tests');
        }
    });

    function errorHandler(error, backtrace) {
        self.casper.unwait();
        if (error instanceof Error) {
            self.processError(error);
            return;
        }
        if (utils.isString(error) && /^(Assertion|Termination|TimedOut)Error/.test(error)) {
            return;
        }
        var line = 0;
        try {
            line = (backtrace || []).filter(function(entry) {
                return self.currentTestFile === entry.file;
            })[0].line;
        } catch (e) {}
        self.uncaughtError(error, self.currentTestFile, line, backtrace);
    }

    function errorHandlerAndDone(error, backtrace) {
        errorHandler(error, backtrace);
        self.done();
    }

    // casper events
    this.casper.on('error', function onCasperError(msg, backtrace) {
        self.processPhantomError(msg, backtrace);
    });

    [
        'wait.error',
        'waitFor.timeout.error',
        'event.error',
        'complete.error'
    ].forEach(function(event) {
        self.casper.on(event, errorHandlerAndDone);
    });

    self.casper.on('step.error', errorHandler);

    this.casper.on('warn', function(warning) {
        if (self.currentSuite) {
            self.currentSuite.addWarning(warning);
        }
    });

    // Do not hook casper if we're not testing
    if (!phantom.casperTest) {
        return;
    }

    // specific timeout callbacks
    this.casper.options.onStepTimeout = function test_onStepTimeout(timeout, step) {
        throw new TimedOutError(f("Step timeout occured at step %s (%dms)", step, timeout));
    };

    this.casper.options.onTimeout = function test_onTimeout(timeout) {
        throw new TimedOutError(f("Timeout occured (%dms)", timeout));
    };

    this.casper.options.onWaitTimeout = function test_onWaitTimeout(timeout, details) {
        /*eslint complexity:0*/
        var message = f("Wait timeout occured (%dms)", timeout);
        details = details || {};

        if (details.selector) {
            message = f(details.waitWhile ? '"%s" never went away in %dms' : '"%s" still did not exist in %dms', details.selector, timeout);
        }
        else if (details.visible) {
            message = f(details.waitWhile ? '"%s" never disappeared in %dms' : '"%s" never appeared in %dms', details.visible, timeout);
        }
        else if (details.url || details.resource) {
            message = f('%s did not load in %dms', details.url || details.resource, timeout);
        }
        else if (details.popup) {
            message = f('%s did not pop up in %dms', details.popup, timeout);
        }
        else if (details.text) {
            message = f('"%s" did not appear in the page in %dms', details.text, timeout);
        }
        else if (details.selectorTextChange) {
            message = f('"%s" did not have a text change in %dms', details.selectorTextChange, timeout);
        }
        else if (utils.isFunction(details.testFx)) {
            message = f('"%s" did not evaluate to something truthy in %dms', details.testFx.toString(), timeout);
        }

        errorHandlerAndDone(new TimedOutError(message));
    };
}
Casper tester: makes assertions, stores test results and display then.

@param  Casper       casper   A valid Casper instance
@param  Object|null  options  Options object