<commit_before>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

module('Path Bounds');

test('path.bounds', function() {
	var path = new Path([
		new Segment(new Point(121, 334), new Point(-19, 38), new Point(30.7666015625, -61.53369140625)),
		new Segment(new Point(248, 320), new Point(-42, -74), new Point(42, 74)),
		new Segment(new Point(205, 420.94482421875), new Point(66.7890625, -12.72802734375), new Point(-79, 15.05517578125))
	]);
	// Test both closed and open paths, as the bounds for them differ
	path.closed = false;
	compareRectangles(path.bounds,
			{ x: 121, y: 275.06796, width: 149.49304, height: 145.87686 },
			'open path.bounds');
	comparePoints(path.position,
			{ x: 195.74652, y: 348.00641 },
			'open path.position');

	// Test both closed and open paths, as the bounds for them differ
	path.closed = true;
	compareRectangles(path.bounds,
			{ x: 114.82726, y: 275.06796, width: 155.66579, height: 148.12778 },
			'closed path.bounds');
	comparePoints(path.position,
			{ x: 192.66016, y: 349.13184 },
			'closed path.position');

	// Scale the path by 0.5 and check bounds
	path.scale(0.5);
	compareRectangles(path.bounds,
			{ x: 153.7437, y: 312.09976, width: 77.8329, height: 74.06381 },
			'scaled path.bounds');

	// Move the path to another position and check bounds
	path.position = [100, 100];
	compareRectangles(path.bounds,
			{ x: 61.08355, y: 62.96797, width: 77.83289, height: 74.06384 },
			'moved path.bounds');

	// Set new bounds and check segment list as result of resizing / positioning
	path.bounds = { x: 100, y: 100, width: 200, height: 200 };
	equals(path.segments.toString(),
			'{ point: { x: 107.93077, y: 179.56917 }, handleIn: { x: -24.41127, y: 51.30707 }, handleOut: { x: 39.52904, y: -83.08194 } },{ point: { x: 271.10084, y: 160.66656 }, handleIn: { x: -53.96176, y: -99.91377 }, handleOut: { x: 53.96176, y: 99.91377 } },{ point: { x: 215.85428, y: 296.96086 }, handleIn: { x: 85.81084, y: -17.18521 }, handleOut: { x: -101.49949, y: 20.32729 } }',
			'resized path.segments');

	// Now rotate by 40 degrees and test bounds and segments again.
	path.rotate(40);
	compareRectangles(path.bounds,
			{ x: 92.38109, y: 106.78957, width: 191.4803, height: 203.66878 },
			'rotated path.bounds');

	equals(path.segments.toString(),
			'{ point: { x: 142.60356, y: 125.16811 }, handleIn: { x: -51.67967, y: 23.61224 }, handleOut: { x: 83.68504, y: -38.23568 } },{ point: { x: 279.74945, y: 215.57158 }, handleIn: { x: 22.88623, y: -111.22434 }, handleOut: { x: -22.88623, y: 111.22434 } },{ point: { x: 149.81984, y: 284.46726 }, handleIn: { x: 76.78135, y: 41.99351 }, handleOut: { x: -90.81925, y: -49.67101 } }',
			'roated path.segments');
});

test('path.strokeBounds on path without stroke', function() {
	var path = new Path([
		new Segment(new Point(121, 334), new Point(-19, 38), new Point(30.7666015625, -61.53369140625)),
		new Segment(new Point(248, 320), new Point(-42, -74), new Point(42, 74)),
		new Segment(new Point(205, 420.94482421875), new Point(66.7890625, -12.72802734375), new Point(-79, 15.05517578125))
	]);
	compareRectangles(path.strokeBounds, { x: 121, y: 275.06796, width: 149.49304, height: 145.87686 });
});

test('path.bounds & path.strokeBounds with stroke styles', function() {
	function makePath() {
		var path = new Path();
		path.moveTo(200, 50);
		path.lineTo(230, 100);
		path.lineTo(250, 50);
		path.lineTo(280, 110);
		path.arcTo([250, 20], false);
		path.rotate(-5);
		path.strokeWidth = 30;
		path.miterLimit = 3;
		return path;
	}

	var path = makePath();
	path.fullySelected = true;
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/round path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 186.87242, y: 1.78419, width: 140.64705, height: 120.96766 },
			'butt/round path.strokeBounds');

	var path = makePath().translate(150, 0);
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 336.87242, y: 1.78419, width: 140.64705, height: 119.73034 },
			'butt/bevel path.strokeBounds');

	var path = makePath().translate(300, 0);
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 486.87242, y: 1.78419, width: 140.64705, height: 133.64882 },
			'butt/miter path.strokeBounds');

	var path = makePath().translate(0, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/round path.bounds');
	compareRectangles(path.strokeBounds,
		 	{ x: 178.06332, y: 150.9807, width: 149.45615, height: 121.77115 },
			'square/strokeBounds path.bounds');

	var path = makePath().translate(150, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 328.06332, y: 150.9807, width: 149.45615, height: 120.53383 },
			'square/bevel path.strokeBounds');

	var path = makePath().translate(300, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 478.06332, y: 150.9807, width: 149.45615, height: 134.45231 },
			'square/miter path.strokeBounds');

	var path = makePath().translate(0, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/round path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 184.01325, y: 301.78419, width: 143.50622, height: 120.96766 },
			'round/round path.strokeBounds');

	var path = makePath().translate(150, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 334.01325, y: 301.78419, width: 143.50622, height: 119.73034 },
			'round/bevel path.strokeBounds');

	var path = makePath().translate(300, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 484.01325, y: 301.78419, width: 143.50622, height: 133.64882 },
			'round/miter path.strokeBounds');
});<commit_msg>Fix tests using applyMatrix() calls after bounds are changed.<commit_after>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

module('Path Bounds');

test('path.bounds', function() {
	var path = new Path([
		new Segment(new Point(121, 334), new Point(-19, 38), new Point(30.7666015625, -61.53369140625)),
		new Segment(new Point(248, 320), new Point(-42, -74), new Point(42, 74)),
		new Segment(new Point(205, 420.94482421875), new Point(66.7890625, -12.72802734375), new Point(-79, 15.05517578125))
	]);
	// Test both closed and open paths, as the bounds for them differ
	path.closed = false;
	compareRectangles(path.bounds,
			{ x: 121, y: 275.06796, width: 149.49304, height: 145.87686 },
			'open path.bounds');
	comparePoints(path.position,
			{ x: 195.74652, y: 348.00641 },
			'open path.position');

	// Test both closed and open paths, as the bounds for them differ
	path.closed = true;
	compareRectangles(path.bounds,
			{ x: 114.82726, y: 275.06796, width: 155.66579, height: 148.12778 },
			'closed path.bounds');
	comparePoints(path.position,
			{ x: 192.66016, y: 349.13184 },
			'closed path.position');

	// Scale the path by 0.5 and check bounds
	path.scale(0.5);
	compareRectangles(path.bounds,
			{ x: 153.7437, y: 312.09976, width: 77.8329, height: 74.06381 },
			'scaled path.bounds');

	// Move the path to another position and check bounds
	path.position = [100, 100];
	compareRectangles(path.bounds,
			{ x: 61.08355, y: 62.96797, width: 77.83289, height: 74.06384 },
			'moved path.bounds');

	// Set new bounds and check segment list as result of resizing / positioning
	path.bounds = { x: 100, y: 100, width: 200, height: 200 };
	path.applyMatrix();
	equals(path.segments.toString(),
			'{ point: { x: 107.93077, y: 179.56917 }, handleIn: { x: -24.41127, y: 51.30707 }, handleOut: { x: 39.52904, y: -83.08194 } },{ point: { x: 271.10084, y: 160.66656 }, handleIn: { x: -53.96176, y: -99.91377 }, handleOut: { x: 53.96176, y: 99.91377 } },{ point: { x: 215.85428, y: 296.96086 }, handleIn: { x: 85.81084, y: -17.18521 }, handleOut: { x: -101.49949, y: 20.32729 } }',
			'resized path.segments');

	// Now rotate by 40 degrees and test bounds and segments again.
	path.rotate(40);
	compareRectangles(path.bounds,
			{ x: 92.38109, y: 106.78957, width: 191.4803, height: 203.66878 },
			'rotated path.bounds');

	path.applyMatrix();
	equals(path.segments.toString(),
			'{ point: { x: 142.60356, y: 125.16811 }, handleIn: { x: -51.67967, y: 23.61224 }, handleOut: { x: 83.68504, y: -38.23568 } },{ point: { x: 279.74945, y: 215.57158 }, handleIn: { x: 22.88623, y: -111.22434 }, handleOut: { x: -22.88623, y: 111.22434 } },{ point: { x: 149.81984, y: 284.46726 }, handleIn: { x: 76.78135, y: 41.99351 }, handleOut: { x: -90.81925, y: -49.67101 } }',
			'roated path.segments');
});

test('path.strokeBounds on path without stroke', function() {
	var path = new Path([
		new Segment(new Point(121, 334), new Point(-19, 38), new Point(30.7666015625, -61.53369140625)),
		new Segment(new Point(248, 320), new Point(-42, -74), new Point(42, 74)),
		new Segment(new Point(205, 420.94482421875), new Point(66.7890625, -12.72802734375), new Point(-79, 15.05517578125))
	]);
	compareRectangles(path.strokeBounds, { x: 121, y: 275.06796, width: 149.49304, height: 145.87686 });
});

test('path.bounds & path.strokeBounds with stroke styles', function() {
	function makePath() {
		var path = new Path();
		path.moveTo(200, 50);
		path.lineTo(230, 100);
		path.lineTo(250, 50);
		path.lineTo(280, 110);
		path.arcTo([250, 20], false);
		path.rotate(-5);
		path.strokeWidth = 30;
		path.miterLimit = 3;
		return path;
	}

	var path = makePath();
	path.fullySelected = true;
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/round path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 186.87242, y: 1.78419, width: 140.64705, height: 120.96766 },
			'butt/round path.strokeBounds');

	var path = makePath().translate(150, 0);
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 336.87242, y: 1.78419, width: 140.64705, height: 119.73034 },
			'butt/bevel path.strokeBounds');

	var path = makePath().translate(300, 0);
	path.strokeColor = 'black';
	path.strokeCap = 'butt';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 16.78419, width: 113.50622, height: 90.96766 },
			'butt/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 486.87242, y: 1.78419, width: 140.64705, height: 133.64882 },
			'butt/miter path.strokeBounds');

	var path = makePath().translate(0, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/round path.bounds');
	compareRectangles(path.strokeBounds,
		 	{ x: 178.06332, y: 150.9807, width: 149.45615, height: 121.77115 },
			'square/strokeBounds path.bounds');

	var path = makePath().translate(150, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 328.06332, y: 150.9807, width: 149.45615, height: 120.53383 },
			'square/bevel path.strokeBounds');

	var path = makePath().translate(300, 150);
	path.strokeColor = 'black';
	path.strokeCap = 'square';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 166.78419, width: 113.50622, height: 90.96766 },
			'square/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 478.06332, y: 150.9807, width: 149.45615, height: 134.45231 },
			'square/miter path.strokeBounds');

	var path = makePath().translate(0, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'round';
	compareRectangles(path.bounds,
			{ x: 199.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/round path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 184.01325, y: 301.78419, width: 143.50622, height: 120.96766 },
			'round/round path.strokeBounds');

	var path = makePath().translate(150, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'bevel';
	compareRectangles(path.bounds,
			{ x: 349.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/bevel path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 334.01325, y: 301.78419, width: 143.50622, height: 119.73034 },
			'round/bevel path.strokeBounds');

	var path = makePath().translate(300, 300);
	path.strokeColor = 'black';
	path.strokeCap = 'round';
	path.strokeJoin = 'miter';
	compareRectangles(path.bounds,
			{ x: 499.01325, y: 316.78419, width: 113.50622, height: 90.96766 },
			'round/miter path.bounds');
	compareRectangles(path.strokeBounds,
			{ x: 484.01325, y: 301.78419, width: 143.50622, height: 133.64882 },
			'round/miter path.strokeBounds');
});<|endoftext|>


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

describe("StatusFile", function() {
  var NodeGit = require("../../");
  var Status = NodeGit.Status;
  var StatusFile = NodeGit.StatusFile;

  var pathName = "README.md";
  var statusCode = Status.STATUS.WT_NEW;

  before(function() {
    this.status = new StatusFile({path: pathName, status: statusCode});
  });

  it("passes the path to the working function", function() {
    assert.equal(this.status.path(), pathName);
  });

  it("identifies the proper statuses", function() {
    assert.ok(this.status.isNew());
    assert.ok(!this.status.isModified());
  });
});
<commit_msg>add test for new convenience methods<commit_after>var assert = require("assert");

describe("StatusFile", function() {
  var NodeGit = require("../../");
  var Status = NodeGit.Status;
  var StatusFile = NodeGit.StatusFile;

  var pathName = "README.md";
  var statusCode = Status.STATUS.WT_NEW;

  before(function() {
    this.status = new StatusFile({path: pathName, status: statusCode});
  });

  it("passes the path to the working function", function() {
    assert.equal(this.status.path(), pathName);
  });

  it("identifies the proper statuses", function() {
    assert.ok(this.status.isNew());
    assert.ok(!this.status.isModified());
  });

  it("detects working tree and index statuses", function() {
    assert.ok(this.status.inWorkingTree());
    assert.ok(!this.status.inIndex());
  });
});
<|endoftext|>


<commit_before>/**
 * Built-in Log Configuration
 *
 * Configure the log level for your app
 */

var path = require('path');

module.exports.log = {
    /**
     * The order of precedence for log levels from lowest to highest is:
     * silly, verbose, info, debug, warn, error, silent
     * @type {String}
     */
    level: 'silly',

    /**
     * Boolean flag indicating whether to suppress output
     * @type {Boolean}
     */
    silent: false,

    /**
     * Boolean flag indicating if we should colorize output
     * @type {Boolean}
     */
    colorize: true,

    /**
     * Boolean flag indicating if we should prepend output with timestamps.
     * If function is specified, its return value will be used instead of timestamps.
     * @type {Boolean|Function}
     */
    timestamp: true,

    /**
     * Boolean flag indicating if we should util.inspect the meta.
     * If function is specified, its return value will be the string representing the meta.
     * @type {Boolean|Function}
     */
    prettyPrint: true,

    /**
     * Numeric indicating how many times to recurse while formatting the object with util.inspect.
     * Only used with prettyPrint: true.
     * @type {Number}
     */
    depth: 1,

    /**
     * Boolean flag indicating if we should prepend output with level
     * @type {Boolean}
     */
    showLevel: true,

    /**
     * If function is specified, its return value will be used instead of default output.
     * @type {Function|Object}
     */
    formatter: function (options) {
        var message = this.timestamp ? (new Date().toUTCString() + ' ') : '';

        message += (options.message !== 'undefined' ? options.message : '');
        message += (options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : '' );

        return message;
    },

    /**
     * Add daily rotate transport
     * @description Pipes logs to files sorted by dates
     * @type {Object}
     */
    dailyRotate: {
        level: 'silly',
        timestamp: true,
        filename: 'app-',
        prettyPrint: true,
        dirname: path.resolve('logs'),
        datePattern: 'yyyy-MM-dd.log'
    }
};
<commit_msg>Replace formatter func with array join<commit_after>/**
 * Built-in Log Configuration
 *
 * Configure the log level for your app
 */

var path = require('path');

module.exports.log = {
    /**
     * The order of precedence for log levels from lowest to highest is:
     * silly, verbose, info, debug, warn, error, silent
     * @type {String}
     */
    level: 'silly',

    /**
     * Boolean flag indicating whether to suppress output
     * @type {Boolean}
     */
    silent: false,

    /**
     * Boolean flag indicating if we should colorize output
     * @type {Boolean}
     */
    colorize: true,

    /**
     * Boolean flag indicating if we should prepend output with timestamps.
     * If function is specified, its return value will be used instead of timestamps.
     * @type {Boolean|Function}
     */
    timestamp: true,

    /**
     * Boolean flag indicating if we should util.inspect the meta.
     * If function is specified, its return value will be the string representing the meta.
     * @type {Boolean|Function}
     */
    prettyPrint: true,

    /**
     * Numeric indicating how many times to recurse while formatting the object with util.inspect.
     * Only used with prettyPrint: true.
     * @type {Number}
     */
    depth: 1,

    /**
     * Boolean flag indicating if we should prepend output with level
     * @type {Boolean}
     */
    showLevel: true,

    /**
     * If function is specified, its return value will be used instead of default output.
     * @type {Function|Object}
     */
    formatter: function (options) {
        return [
            this.timestamp ? (new Date().toUTCString() + ': ') : '',
            typeof options.message !== 'undefined' ? options.message : '',
            options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : ''
        ].join('');
    },

    /**
     * Add daily rotate transport
     * @description Pipes logs to files sorted by dates
     * @type {Object}
     */
    dailyRotate: {
        level: 'silly',
        timestamp: true,
        filename: 'app-',
        prettyPrint: true,
        dirname: path.resolve('logs'),
        datePattern: 'yyyy-MM-dd.log'
    }
};
<|endoftext|>


<commit_before>(function(Ractive, CodeMirror, _, tungsten) {
  'use strict';

  Ractive.DEBUG = false;

  var topViews = [];
  tungsten.View.prototype._initialize = tungsten.View.prototype.initialize;
  tungsten.View.prototype.initialize = function(opts) {
    if (!opts.parentView && !this.tutorialView) {
      topViews.push(this);
    }
    this._initialize(opts);
  };

  var View = tungsten.View.extend({
    tutorialView: true,
    initDebug: _.noop
  });
  var Model = tungsten.Model.extend({
    initDebug: _.noop
  });
  var Collection = tungsten.Collection.extend({
    initDebug: _.noop
  });
  var tungstenCode;
  var templateCode;

  // Code Mirror Extensions and options
  CodeMirror.defineMode('mustache', function(config, parserConfig) {
    var mustacheOverlay = {
      token: function(stream) {
        var ch;
        if (stream.match('{{')) {
          while ((ch = stream.next()) != null)
            if (ch == '}' && stream.next() == '}') {
              stream.eat('}');
              return 'mustache';
            }
        }
        while (stream.next() != null && !stream.match('{{', false)) {
        }
        return null;
      }
    };
    return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || 'text/html'), mustacheOverlay);
  });

  var codeOptions = {
    lineNumbers: true,
    lineWrapping: true,
    extraKeys: {
      'Ctrl-Alt-Q': function(cm) { cm.foldCode(cm.getCursor()); },
      'Ctrl-Alt-F': function(cm) {
        cm.operation(function() {
          for (var l = cm.firstLine(); l <= cm.lastLine(); ++l) cm.foldCode({
            line: l,
            ch: 0
          }, null, 'fold');
        });
      },
      'Ctrl-Alt-U': function(cm) {
        cm.operation(function() {
          for (var l = cm.firstLine(); l <= cm.lastLine(); ++l) cm.foldCode({
            line: l,
            ch: 0
          }, null, 'unfold');
        });
      }
    },
    foldGutter: true,
    gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
  };
  // Views and Template
  var rawTemplates = {
    app: '
 {{#tutorial}}

 {{#steps}}
{{{index}}}
 {{/steps}}
 <button class="pure-button js-run run" id="run">Run</button>{{#step}}

{{name}}

{{{description_html}}}
 {{/step}}
 {{#step}}
<textarea class="js-editor editor html" cols="30" rows="25" id="template" value="{{template}}">{{template}}</textarea>

<textarea class="js-editor editor" id="tungsten" cols="30" rows="25" value="{{js}}">{{js}}</textarea>
 {{/step}} {{/tutorial}}
'
  };
  var compiledTemplates = tungsten._template.compileTemplates(rawTemplates);
  var StepSelectView = View.extend({
    events: {
      click: function() {
        this.model.trigger('selectStep', this.model);
      }
    }
  });
  var AppView = View.extend({
    events: {
      'click .js-run': 'run',
      'keydown': 'handleKeydown'
    },
    handleKeydown: function(e) {
      // shift + enter
      if (e.shiftKey && e.which === 13) {
        this.run();
        return false;
      }
      // ctrl + s
      if (e.ctrlKey && e.which === 83) {
        this.run();
        return false;
      }
      // meta + s
      if (e.metaKey && e.which === 83) {
        this.run();
        return false;
      }
    },
    run: function() {
      var newLines = /\n/g;
      var boilerplate = 'var rawTemplates = { app_view: \'' + templateCode.getValue().replace(newLines, '') + '\' };var compiledTemplates = tungsten._template.compileTemplates(rawTemplates);';
      var evalNoContext = eval.bind(null);
      // Destroy any views that were created in the last run to ensure a fresh runtime
      _.invoke(topViews, 'destroy');
      topViews = [];
      document.querySelector('#app').innerHTML = '';
      try {
        evalNoContext(boilerplate + '\n;' + tungstenCode.getValue());
      } catch (e) {
        document.querySelector('#app').innerHTML = '
ERROR: ' + e.toString() + '';
      }
    },
    toggleFold: function(cm, fold) {
      cm.operation(function() {
        for (var l = cm.firstLine(); l <= cm.lastLine(); ++l)
          cm.foldCode({line: l, ch: 0}, null, fold ? 'fold' : unfold);
      });
    },
    postInitialize: function() {
      var self = this;
      var debouncedRun = _.debounce(this.run, 200);
      this.listenTo(this.model.get('tutorials'), 'selectTutorial', _.debounce(function(tutorialName) {
        var tutorial = self.model.get('tutorials').where({name: tutorialName})[0];
        if (tungstenCode && self.model.get('runOnKeyup')) {
          tungstenCode.off('change', debouncedRun);
          templateCode.off('change', debouncedRun);
        }
        self.model.unset('step');
        self.model.unset('tutorial');

        self.model.get('tutorials').each(function(tutorials) {
          tutorials.set('selected', false);
        });
        tutorial.set('selected', true);

        self.model.set('tutorial', tutorial.toJSON());
        self.listenTo(self.model.get('tutorial').get('steps'), 'selectStep', function(s) {

          self.model.get('tutorial').get('steps').each(function(step) {
            step.set('selected', false);
          });
          s.set('selected', true);
          self.model.get('tutorial').set('step', s);
          _.each(document.querySelectorAll('.CodeMirror'), function(editor) {
            editor.remove();
          });
          if (document.querySelector('#app')) {
            document.querySelector('#app').innerHTML = '';
          }
          // Initialize Code Mirror in setTimeout
          window.setTimeout(function() {
            tungstenCode = CodeMirror.fromTextArea(document.getElementById('tungsten'), _.extend({
              mode: 'javascript'
            }, codeOptions));
            templateCode = CodeMirror.fromTextArea(document.getElementById('template'), _.extend({
              mode: 'mustache'
            }, codeOptions));

            if (self.model.get('tutorial').get('step').get('template_highlights')) {
              _.each(self.model.get('tutorial').get('step').get('template_highlights'), function(highlight) {
                templateCode.markText(highlight.start, highlight.end, {
                  className: 'styled-background',
                  clearOnEnter: true
                });
              });
            }
            if (self.model.get('tutorial').get('step').get('js_highlights')) {
              _.each(self.model.get('tutorial').get('step').get('js_highlights'), function(highlight) {
                tungstenCode.markText(highlight.start, highlight.end, {
                  className: 'styled-background',
                  clearOnEnter: true
                });
              });
            }
            self.run();
            if (self.model.get('runOnKeyup')) {
              tungstenCode.on('change', debouncedRun);
              templateCode.on('change', debouncedRun);
            }
          }, 200);
        });
        if (self.model.get('tutorial').get('steps')) {
          // Start at the first step
          self.model.get('tutorial').get('steps').trigger('selectStep', self.model.get('tutorial').get('steps').at(0));
        } else {
          console.log('tutorial has no steps set');
        }

      }, 0));
      // Start with the first tutorial open
      this.model.get('tutorials').trigger('selectTutorial', this.model.get('tutorials').at(0).get('name'));
    },
    childViews: {
      'js-step-select': StepSelectView
    }
  });

  // Models and Collections
  var TutorialModel = Model.extend({
    derived: {
      url_name: {
        deps: ['name'],
        fn: function() {
          return encodeURI(this.get('name'));
        }
      }
    },
    relations: {
      steps: Collection.extend({
        model: Model.extend({
          defaults: {
            selected: false
          }
        })
      })
    }
  });
  var TutorialCollection = Collection.extend({
    model: TutorialModel
  });
  var AppModel = Model.extend({
    relations: {tutorials: TutorialCollection, tutorial: TutorialModel}
  });
  var appModel = new AppModel(window.data);

  // Start app
  var app = new AppView({
    el: document.getElementById('appwrapper'),
    template: compiledTemplates.app,
    model: appModel,
    dynamicInitialize: true
  });

  if (!tungsten.Backbone.history.started) {
    var router = tungsten.Backbone.Router.extend({
      routes: {
        '*tutorialName': 'selectTutorial'
      },
      selectTutorial: function(tutorialName) {
        if (tutorialName) {
          appModel.get('tutorials').trigger('selectTutorial', tutorialName);
        }
      }
    });
    new router();
    tungsten.Backbone.history.start();
  }

})(Ractive, CodeMirror, _, tungsten);
<commit_msg>Change CodeMirrors to Components to improve visual performance<commit_after>(function(Ractive, CodeMirror, _, tungsten) {
  'use strict';

  Ractive.DEBUG = false;

  var topViews = [];
  tungsten.View.prototype._initialize = tungsten.View.prototype.initialize;
  tungsten.View.prototype.initialize = function(opts) {
    if (!opts.parentView && !this.tutorialView) {
      topViews.push(this);
    }
    this._initialize(opts);
  };

  var View = tungsten.View.extend({
    initDebug: _.noop,
    tutorialView: true
  });
  var Model = tungsten.Model.extend({
    initDebug: _.noop
  });
  var Collection = tungsten.Collection.extend({
    initDebug: _.noop
  });
  var ComponentWidget = tungsten.ComponentWidget;

  // Code Mirror Extensions and options
  CodeMirror.defineMode('mustache', function(config, parserConfig) {
    var mustacheOverlay = {
      token: function(stream) {
        var ch;
        if (stream.match('{{')) {
          while ((ch = stream.next()) != null) {
            if (ch == '}' && stream.next() == '}') {
              stream.eat('}');
              return 'mustache';
            }
          }
        }
        while (stream.next() != null && !stream.match('{{', false)) {
        }
        return null;
      }
    };
    return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || 'text/html'), mustacheOverlay);
  });

  var codeOptions = {
    lineNumbers: true,
    lineWrapping: true,
    extraKeys: {
      'Ctrl-Alt-Q': function(cm) { cm.foldCode(cm.getCursor()); },
      'Ctrl-Alt-F': function(cm) {
        cm.operation(function() {
          for (var l = cm.firstLine(); l <= cm.lastLine(); ++l) {
            cm.foldCode({
              line: l,
              ch: 0
            }, null, 'fold');
          }
        });
      },
      'Ctrl-Alt-U': function(cm) {
        cm.operation(function() {
          for (var l = cm.firstLine(); l <= cm.lastLine(); ++l) {
            cm.foldCode({
              line: l,
              ch: 0
            }, null, 'unfold');
          }
        });
      }
    },
    foldGutter: true,
    gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']
  };

  // Views and Template
  var rawTemplates = {
    app: '
 {{#tutorial}}

 {{#steps}}
{{{index}}}
 {{/steps}}
 <button class="pure-button js-run run" id="run">Run</button>{{#step}}

{{name}}

{{{description_html}}}
 {{/step}}

{{{template}}}

{{{js}}}
 {{/tutorial}}
',
    codeMirror: ''
  };
  var compiledTemplates = tungsten._template.compileTemplates(rawTemplates);

  var CodeMirrorComponent = {
    View: View.extend({
      events: {
        'keyup': 'update',
        'input': 'update'
      },
      postInitialize: function() {
        this.listenTo(this.model, 'change:value', function(model, value) {
          this.codeMirror.setValue(value);
        });
        this.listenTo(this.model, 'change:highlights', function(model, value) {
          var codeMirror = this.codeMirror;
          _.each(value, function(highlight) {
            codeMirror.markText(highlight.start, highlight.end, {
              className: 'styled-background',
              clearOnEnter: true
            });
          });
        });
      },
      update: function() {
        this.model.updateValue(this.codeMirror.getValue());
      },
      postRender: function() {
        var self = this;
        if (!this.codeMirror) {
          setTimeout(function() {
            self.codeMirror = CodeMirror(self.el, self.model.attributes);
          });
        }
      }
    }, {debugName: 'CodeMirrorComponentView'}),
    Model: Model.extend({
      exposedEvents: ['change'],
      updateValue: function(val) {
        this.set('value', val, {silent: true});
        this.trigger('change', this);
      },
      serialize: function(data) {
        return data.value;
      }
    }, {debugName: 'CodeMirrorComponentModel'}),
    template: compiledTemplates.codeMirror,
    constructor: function(data, options) {
      if (data && data.constructor === ComponentWidget) {
        return data;
      }
      var component = new ComponentWidget(
        CodeMirrorComponent.View,
        new CodeMirrorComponent.Model(data),
        CodeMirrorComponent.template,
        options
      );

      return component;
    }
  };

  var StepSelectView = View.extend({
    events: {
      click: function() {
        this.model.trigger('selectStep', this.model);
      }
    }
  });
  var AppView = View.extend({
    events: {
      'click .js-run': 'run',
      'keydown': 'handleKeydown'
    },
    handleKeydown: function(e) {
      // shift + enter
      if (e.shiftKey && e.which === 13) {
        this.run();
        return false;
      }
      // ctrl + s
      if (e.ctrlKey && e.which === 83) {
        this.run();
        return false;
      }
      // meta + s
      if (e.metaKey && e.which === 83) {
        this.run();
        return false;
      }
    },
    run: function() {
      var newLines = /\n/g;
      var boilerplate = 'var rawTemplates = { app_view: \'' + this.model.get('template').doSerialize().replace(newLines, '') + '\' };var compiledTemplates = tungsten._template.compileTemplates(rawTemplates);';
      var evalNoContext = eval.bind(null);
      // Destroy any views that were created in the last run to ensure a fresh runtime
      _.invoke(topViews, 'destroy');
      topViews = [];
      document.getElementById('app').innerHTML = '';
      try {
        evalNoContext(boilerplate + '\n;' + this.model.get('js').doSerialize());
      } catch (e) {
        document.getElementById('app').innerHTML = '
ERROR: ' + e.toString() + '';
      }
    },
    toggleFold: function(cm, fold) {
      cm.operation(function() {
        for (var l = cm.firstLine(); l <= cm.lastLine(); ++l) {
          cm.foldCode({line: l, ch: 0}, null, fold ? 'fold' : unfold);
        }
      });
    },
    postInitialize: function() {
      var self = this;
      var debouncedRun = _.debounce(this.run, 200);
      this.listenTo(this.model, 'change:js change:template', debouncedRun);
      this.listenTo(this.model.get('tutorials'), 'selectTutorial', _.debounce(function(tutorialName) {
        var tutorial = self.model.get('tutorials').where({name: tutorialName})[0];
        self.model.unset('step');
        self.model.unset('tutorial');

        self.model.get('tutorials').each(function(tutorials) {
          tutorials.set('selected', false);
        });
        tutorial.set('selected', true);

        self.model.set('tutorial', tutorial.toJSON());
        self.listenTo(self.model.get('tutorial').get('steps'), 'selectStep', function(s) {
          self.model.get('tutorial').get('steps').each(function(step) {
            step.set('selected', false);
          });
          s.set('selected', true);
          self.model.get('tutorial').set('step', s);
          var appEl = document.getElementById('app');
          if (appEl) {
            appEl.innerHTML = '';
          }
        });
        if (self.model.get('tutorial').get('steps')) {
          // Start at the first step
          self.model.get('tutorial').get('steps').trigger('selectStep', self.model.get('tutorial').get('steps').at(0));
        } else {
          console.log('tutorial has no steps set');
        }

      }, 0));
      // Start with the first tutorial open
      this.model.get('tutorials').trigger('selectTutorial', this.model.get('tutorials').at(0).get('name'));
    },
    childViews: {
      'js-step-select': StepSelectView
    }
  });

  // Models and Collections
  var TutorialModel = Model.extend({
    derived: {
      url_name: {
        deps: ['name'],
        fn: function() {
          return encodeURI(this.get('name'));
        }
      }
    },
    relations: {
      steps: Collection.extend({
        model: Model.extend({
          defaults: {
            selected: false
          }
        })
      })
    }
  });
  var TutorialCollection = Collection.extend({
    model: TutorialModel
  });
  var AppModel = Model.extend({
    defaults: {
      js: _.extend({ mode: 'javascript', value: '' }, codeOptions),
      template: _.extend({ mode: 'html', value: '' }, codeOptions)
    },
    relations: {
      js: CodeMirrorComponent.constructor,
      template: CodeMirrorComponent.constructor,
      tutorials: TutorialCollection,
      tutorial: TutorialModel
    },
    postInitialize: function() {
      this.listenTo(this, 'change:tutorial change:tutorial:step', function() {
        this.get('js').set({
          value: this.getDeep('tutorial:step:js'),
          highlights: this.getDeep('tutorial:step:js_highlights')
        });
        this.get('template').set({
          value: this.getDeep('tutorial:step:template'),
          highlights: this.getDeep('tutorial:step:template_highlights')
        });
      });
    }
  });
  var appModel = new AppModel(window.data);

  // Start app
  var app = new AppView({
    el: document.getElementById('appwrapper'),
    template: compiledTemplates.app,
    model: appModel,
    dynamicInitialize: true
  });
  window.app = app;

  if (!tungsten.Backbone.history.started) {
    var Router = tungsten.Backbone.Router.extend({
      routes: {
        '*tutorialName': 'selectTutorial'
      },
      selectTutorial: function(tutorialName) {
        if (tutorialName) {
          appModel.get('tutorials').trigger('selectTutorial', tutorialName);
        }
      }
    });
    new Router();
    tungsten.Backbone.history.start();
  }

})(Ractive, CodeMirror, _, tungsten);
<|endoftext|>


<commit_before>/*
 * Dartmouth Course Checker
 *
 * Displays Dartmouth course data, including enrollment. Easily extendible to
 * provide email notification on course availability detection.
 *
 * Return values:
 *   0 if class has room
 *   1 if class is full
 *  -1 if the program has an error
 *
 * Usage:   phantomjs check_class.js [DEPARMENT] [COURSE NUMBER]
 * Example: phantomjs check_class.js EARS 6
 *
 * Created by Daniel Chen '16
 * Github: cheniel
 */

// called when the class has space. Fill this out if you like!
// send an email, modify a database, etc...
function classHasSpaceHandler(results, page) {
  console.log('\u0007'); // terminal beep

  // // MANDRILL EMAIL TEMPLATE
  // // If you would like to have an email sent when course availabilities is
  // // detected, uncomment the code below, register for a free account, and
  // // fill in YOUR_API_KEY, YOUR@EMAIL.HERE, and RECIPIENT@EMAIL.HERE
  // page.evaluate(function(title, enrl, lim) {
  //   console.log("sending email");
  //   $.ajax({
  //     async: false,
  //     type: 'POST',
  //     url: 'https://mandrillapp.com/api/1.0/messages/send.json',
  //     data: {
  //       'key': 'YOUR_API_KEY',
  //       'message': {
  //         'from_email': 'YOUR@EMAIL.HERE',
  //         'to': [
  //             {
  //               'email': 'RECIPIENT@EMAIL.HERE',
  //               'type': 'to'
  //             }
  //           ],
  //         'autotext': 'true',
  //         'subject': 'Sign up for ' + title + ' now!!',
  //         'html': 'The time has come! The capacity is ' + enrl + '/' + lim + ". Banner."
  //       }
  //     }
  //   }).done(function(response) {
  //     console.log("response: " + response);
  //   });
  // }, results.title, results.enrl, results.lim);
  // console.log("finished");

}

// constants
var NUM_DATA_POINTS = 17,
    SERVER = 'https://banner.dartmouth.edu/dart/groucho/timetable.course_quicksearch',
    JQUERY = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js';

// get command line args
var system = require('system');
var args = system.args;

// terminate if no arguments
if (args.length !== 3) {
  console.log("Usage:   phantomjs" + args[0] + " [DEPARMENT] [COURSE NUMBER]");
  console.log("Example: phantomjs" + args[0] + " EARS 6");
  phantom.exit(-1);
}

var page = require('webpage').create(),
    data = 'classyear=2008&subj=' + args[1] + '&crsenum=' + args[2];

// ERROR HANDLERS
phantom.onError = function(msg, trace) {
  var msgStack = ['PHANTOM ERROR: ' + msg];
  if (trace && trace.length) {
    msgStack.push('TRACE:');
    trace.forEach(function(t) {
      msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : ''));
    });
  }
  console.error(msgStack.join('\n'));
  phantom.exit(-1);
};

page.onError = function(msg, trace) {
  var msgStack = ['ERROR: ' + msg];
  if (trace && trace.length) {
    msgStack.push('TRACE:');
    trace.forEach(function(t) {
      msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
    });
  }
  console.error(msgStack.join('\n'));
};

// // Uncomment if you would like to see console.log() messages from inside
// // page.evaluate(function() {...})
// page.onConsoleMessage = function(msg) {
//   console.log(msg);
// };

page.open(SERVER, 'post', data, function(status) {

  if (status !== 'success') {
    console.log('Unable to post!');
    phantom.exit(-1);
  }

  // include jquery
  page.includeJs(JQUERY, function() {

    var results = [];
    for (var i = 0; i < NUM_DATA_POINTS; i++) {

      // get data type from header row
      var key = page.evaluate(function(idx) {
        var tableRows = $(".data-table > table > tbody > tr");
        return tableRows.eq(0).children("th").eq(idx).text().toLowerCase();
      }, i);

      // get data from data row
      var data = page.evaluate(function(idx) {
        var tableRows = $(".data-table > table > tbody > tr");
        return tableRows.eq(2).children("td").eq(idx).text();
      }, i);

      results[key] = data;
    }

    // print data
    console.log("Course Information:");
    for(var dataType in results) {
      console.log(dataType + ": " + results[dataType]);
    }

    // check if class has space
    if (parseInt(results.lim) > parseInt(results.enrl)) {
      console.log("\nTHERE IS ROOM IN THE CLASS!!! " + results.enrl
          + "/" + results.lim);
      classHasSpaceHandler(results, page);
      phantom.exit(0);
    } else {
      console.log("\nThe class is full. " + results.enrl + "/" + results.lim);
      phantom.exit(1);
    }

  });
});
<commit_msg>add timestamp to result<commit_after>/*
 * Dartmouth Course Checker
 *
 * Displays Dartmouth course data, including enrollment. Easily extendible to
 * provide email notification on course availability detection.
 *
 * Return values:
 *   0 if class has room
 *   1 if class is full
 *  -1 if the program has an error
 *
 * Usage:   phantomjs check_class.js [DEPARMENT] [COURSE NUMBER]
 * Example: phantomjs check_class.js EARS 6
 *
 * Created by Daniel Chen '16
 * Github: cheniel
 */

// called when the class has space. Fill this out if you like!
// send an email, modify a database, etc...
function classHasSpaceHandler(results, page) {
  console.log('\u0007'); // terminal beep

  // // MANDRILL EMAIL TEMPLATE
  // // If you would like to have an email sent when course availabilities is
  // // detected, uncomment the code below, register for a free account, and
  // // fill in YOUR_API_KEY, YOUR@EMAIL.HERE, and RECIPIENT@EMAIL.HERE
  // page.evaluate(function(title, enrl, lim) {
  //   console.log("sending email");
  //   $.ajax({
  //     async: false,
  //     type: 'POST',
  //     url: 'https://mandrillapp.com/api/1.0/messages/send.json',
  //     data: {
  //       'key': 'YOUR_API_KEY',
  //       'message': {
  //         'from_email': 'YOUR@EMAIL.HERE',
  //         'to': [
  //             {
  //               'email': 'RECIPIENT@EMAIL.HERE',
  //               'type': 'to'
  //             }
  //           ],
  //         'autotext': 'true',
  //         'subject': 'Sign up for ' + title + ' now!!',
  //         'html': 'The time has come! The capacity is ' + enrl + '/' + lim + ". Banner."
  //       }
  //     }
  //   }).done(function(response) {
  //     console.log("response: " + response);
  //   });
  // }, results.title, results.enrl, results.lim);
  // console.log("finished");

}

// constants
var NUM_DATA_POINTS = 17,
    SERVER = 'https://banner.dartmouth.edu/dart/groucho/timetable.course_quicksearch',
    JQUERY = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js';

// get command line args
var system = require('system');
var args = system.args;

// terminate if no arguments
if (args.length !== 3) {
  console.log("Usage:   phantomjs" + args[0] + " [DEPARMENT] [COURSE NUMBER]");
  console.log("Example: phantomjs" + args[0] + " EARS 6");
  phantom.exit(-1);
}

var page = require('webpage').create(),
    data = 'classyear=2008&subj=' + args[1] + '&crsenum=' + args[2];

// ERROR HANDLERS
phantom.onError = function(msg, trace) {
  var msgStack = ['PHANTOM ERROR: ' + msg];
  if (trace && trace.length) {
    msgStack.push('TRACE:');
    trace.forEach(function(t) {
      msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : ''));
    });
  }
  console.error(msgStack.join('\n'));
  phantom.exit(-1);
};

page.onError = function(msg, trace) {
  var msgStack = ['ERROR: ' + msg];
  if (trace && trace.length) {
    msgStack.push('TRACE:');
    trace.forEach(function(t) {
      msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
    });
  }
  console.error(msgStack.join('\n'));
};

// // Uncomment if you would like to see console.log() messages from inside
// // page.evaluate(function() {...})
// page.onConsoleMessage = function(msg) {
//   console.log(msg);
// };

page.open(SERVER, 'post', data, function(status) {

  if (status !== 'success') {
    console.log('Unable to post!');
    phantom.exit(-1);
  }

  // include jquery
  page.includeJs(JQUERY, function() {

    var results = [];
    for (var i = 0; i < NUM_DATA_POINTS; i++) {

      // get data type from header row
      var key = page.evaluate(function(idx) {
        var tableRows = $(".data-table > table > tbody > tr");
        return tableRows.eq(0).children("th").eq(idx).text().toLowerCase();
      }, i);

      // get data from data row
      var data = page.evaluate(function(idx) {
        var tableRows = $(".data-table > table > tbody > tr");
        return tableRows.eq(2).children("td").eq(idx).text();
      }, i);

      results[key] = data;
    }

    // print data
    console.log("Course Information:");
    for(var dataType in results) {
      console.log(dataType + ": " + results[dataType]);
    }

    // check if class has space
    if (parseInt(results.lim) > parseInt(results.enrl)) {
      console.log((new Date()).toLocaleTimeString() + " | THERE IS ROOM IN THE CLASS!!! " + results.enrl + "/" + results.lim);
      classHasSpaceHandler(results, page);
      phantom.exit(0);
    } else {
      console.log((new Date()).toLocaleTimeString() + " | The class is full. " + results.enrl + "/" + results.lim);
      phantom.exit(1);
    }

  });
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import ApplicationSerializer from 'ghost/serializers/application';

var SettingSerializer = ApplicationSerializer.extend({
    serializeIntoHash: function (hash, type, record, options) {
        // Settings API does not want ids
        options = options || {};
        options.includeId = false;

        var root = Ember.String.pluralize(type.typeKey),
            data = this.serialize(record, options),
            payload = [];

        delete data.id;

        Object.keys(data).forEach(function (k) {
            payload.push({key: k, value: data[k]});
        });

        hash[root] = payload;
    },

    extractArray: function (store, type, _payload) {
        var payload = {id: '0'};

        _payload.settings.forEach(function (setting) {
            payload[setting.key] = setting.value;
        });

        return [payload];
    },

    extractSingle: function (store, type, payload) {
        return this.extractArray(store, type, payload).pop();
    }
});

export default SettingSerializer;
<commit_msg>Normalize settings payloads in client serializer Closes #5117<commit_after>import Ember from 'ember';
import ApplicationSerializer from 'ghost/serializers/application';

var SettingSerializer = ApplicationSerializer.extend({
    serializeIntoHash: function (hash, type, record, options) {
        // Settings API does not want ids
        options = options || {};
        options.includeId = false;

        var root = Ember.String.pluralize(type.typeKey),
            data = this.serialize(record, options),
            payload = [];

        delete data.id;

        Object.keys(data).forEach(function (k) {
            payload.push({key: k, value: data[k]});
        });

        hash[root] = payload;
    },

    extractArray: function (store, type, _payload) {
        var payload = {id: '0'};

        _payload.settings.forEach(function (setting) {
            payload[setting.key] = setting.value;
        });

        payload = this.normalize(type, payload);

        return [payload];
    },

    extractSingle: function (store, type, payload) {
        return this.extractArray(store, type, payload).pop();
    }
});

export default SettingSerializer;
<|endoftext|>


<commit_before>
define(
[
    'chai',
    'fixtures',
    'argumenta/widgets/Argument',
    'argumenta/widgets/Base'
],
function(chai, fixtures, Argument, Base) {

    var assert = chai.assert;

    describe('Argument', function() {

        it('should be a function', function() {
            assert.isFunction(Argument);
        });

        it('should include a moduleID', function() {
            assert.equal(Argument.prototype.moduleID, 'Argument');
        });

        describe('new Argument( options, element )', function() {
            it('should return a new Argument widget', function() {
                var argData = fixtures.validArgumentData();
                var argument = new Argument(argData);
                assert.instanceOf(argument, Argument);
                assert.instanceOf(argument, Base);
            });
        });

        describe('getType()', function() {
            it('should return the object type', function() {
                var argument = fixtures.validArgument();
                assert.equal(argument.getType(), 'argument');
            });
        });

        describe('getSha1()', function() {
            it('should return the object sha1', function() {
                var argument = fixtures.validArgument();
                assert.equal(argument.getSha1(), '7077e1ce31bc8e9d2a88479aa2d159f2f9de4856');
            });
        });
    });
});
<commit_msg>Add test for Argument setter.<commit_after>
define(
[
    'chai',
    'fixtures',
    'argumenta/widgets/Argument',
    'argumenta/widgets/Base'
],
function(chai, fixtures, Argument, Base) {

    var assert = chai.assert;

    describe('Argument', function() {

        it('should be a function', function() {
            assert.isFunction(Argument);
        });

        it('should include a moduleID', function() {
            assert.equal(Argument.prototype.moduleID, 'Argument');
        });

        describe('new Argument( options, element )', function() {
            it('should return a new Argument widget', function() {
                var argData = fixtures.validArgumentData();
                var argument = new Argument(argData);
                assert.instanceOf(argument, Argument);
                assert.instanceOf(argument, Base);
            });
        });

        describe('getType()', function() {
            it('should return the object type', function() {
                var argument = fixtures.validArgument();
                assert.equal(argument.getType(), 'argument');
            });
        });

        describe('getSha1()', function() {
            it('should return the object sha1', function() {
                var argument = fixtures.validArgument();
                assert.equal(
                    argument.getSha1(),
                    '7077e1ce31bc8e9d2a88479aa2d159f2f9de4856',
                    'Check SHA1.'
                );
            });
        });

        describe('setPropositions( props )', function() {
            it('should load the given propositions data', function() {
                var data = fixtures.validArgumentData();
                var argument = new Argument(data);
                argument.setPropositions([]);
                assert.deepEqual(
                    argument.propositions, [],
                    'Clear propositions.'
                );
                argument.setPropositions(data.propositions);
                assert.deepEqual(
                    argument.propositions, data.propositions,
                    'Set propositions.'
                );
            });
        });
    });
});
<|endoftext|>


<commit_before>// Set of common function that should be main building blocks for e2e tests.
// The e2e tests usually consist of following building blocks:
// - request agent
// - state builder
// - output state checker (in case we don't get jest snapshots working)
//
// The request agent is responsible for making HTTP-like requests to an application (express app in case of Ghost).
// Note there's no actual need to make an HTTP request to an actual server, bypassing HTTP and hooking into the application
// directly is enough and reduces dependence on blocking a port (allows to run tests in parallel).
//
// The state builder is responsible for building the state of the application. Usually it's done by using pre-defined fixtures.
// Can include building a DB state, file system state (themes, config files), building configuration state (config files) etc.
//
// The output state checker is responsible for checking the response from the app after performing a request.
const _ = require('lodash');
const {sequence} = require('@tryghost/promise');
const {any, stringMatching} = require('@tryghost/express-test').snapshot;
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const uuid = require('uuid');

const fixtureUtils = require('./fixture-utils');
const redirectsUtils = require('./redirects');
const configUtils = require('./configUtils');
const urlServiceUtils = require('./url-service-utils');
const mockManager = require('./e2e-framework-mock-manager');

const boot = require('../../core/boot');
const {AdminAPITestAgent, ContentAPITestAgent, GhostAPITestAgent, MembersAPITestAgent} = require('./agents');
const db = require('./db-utils');

// Services that need resetting
const settingsService = require('../../core/server/services/settings/settings-service');
const supertest = require('supertest');

/**
 * @param {Object} [options={}]
 * @param {Boolean} [options.backend] Boot the backend
 * @param {Boolean} [options.frontend] Boot the frontend
 * @param {Boolean} [options.server] Start a server
 * @returns {Promise<Express.Application>} ghost
 */
const startGhost = async (options = {}) => {
    /**
     * We never use the root content folder for testing!
     * We use a tmp folder.
     */
    const contentFolder = path.join(os.tmpdir(), uuid.v4(), 'ghost-test');
    await prepareContentFolder({contentFolder});

    // NOTE: need to pass this config to the server instance
    configUtils.set('paths:contentPath', contentFolder);

    const defaults = {
        backend: true,
        frontend: false,
        server: false
    };

    // Ensure the state of all data, including DB and caches
    await resetData();

    const bootOptions = Object.assign({}, defaults, options);

    const ghostServer = await boot(bootOptions);

    if (bootOptions.frontend) {
        await urlServiceUtils.isFinished();
    }

    return ghostServer;
};

/**
 * Slightly simplified copy-paste from e2e-utils.
 * @param {Object} options
 */
const prepareContentFolder = async ({contentFolder, redirectsFile = true, routesFile = true}) => {
    const contentFolderForTests = contentFolder;

    await fs.ensureDir(contentFolderForTests);
    await fs.ensureDir(path.join(contentFolderForTests, 'data'));
    await fs.ensureDir(path.join(contentFolderForTests, 'themes'));
    await fs.ensureDir(path.join(contentFolderForTests, 'images'));
    await fs.ensureDir(path.join(contentFolderForTests, 'logs'));
    await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
    await fs.ensureDir(path.join(contentFolderForTests, 'settings'));

    // Copy all themes into the new test content folder. Default active theme is always casper.
    // If you want to use a different theme, you have to set the active theme (e.g. stub)
    await fs.copy(
        path.join(__dirname, 'fixtures', 'themes'),
        path.join(contentFolderForTests, 'themes')
    );

    if (redirectsFile) {
        redirectsUtils.setupFile(contentFolderForTests, '.yaml');
    }

    if (routesFile) {
        await fs.copy(
            path.join(__dirname, 'fixtures', 'settings', 'routes.yaml'),
            path.join(contentFolderForTests, 'settings', 'routes.yaml')
        );
    }
};

/**
 * Database state builder. By default inserts an owner user into the database.
 * @param  {...any} [options]
 * @returns {Promise<void>}
 */
const initFixtures = async (...options) => {
    // No DB setup, but override the owner
    options = _.merge({'owner:post': true}, _.transform(options, function (result, val) {
        if (val) {
            result[val] = true;
        }
    }));

    const fixtureOps = fixtureUtils.getFixtureOps(options);

    return sequence(fixtureOps);
};

const getFixture = (type, index = 0) => {
    return fixtureUtils.DataGenerator.forKnex[type][index];
};

/**
 * Reset rate limit instances (not the brute table)
 */
const resetRateLimits = async () => {
    // Reset rate limiting instances
    const {spamPrevention} = require('../../core/server/web/shared/middleware/api');
    spamPrevention.reset();
};

/**
 * This function ensures that Ghost's data is reset back to "factory settings"
 *
 */
const resetData = async () => {
    // Calling reset on the database also causes the fixtures to be re-run
    // We need to unhook the settings events and restore the cache before we do this
    // Otherwise, the fixtures being restored will refer to the old settings cache data
    settingsService.reset();

    // Clear out the database
    await db.reset({truncate: true});

    // Reset rate limiting instances (resetting the table is not enough!)
    await resetRateLimits();
};

/**
 * Creates a ContentAPITestAgent which is a drop-in substitution for supertest.
 * It is automatically hooked up to the Content API so you can make requests to e.g.
 * agent.get('/posts/') without having to worry about URL paths
 * @returns {Promise<InstanceType<ContentAPITestAgent>>} agent
 */
const getContentAPIAgent = async () => {
    try {
        const app = await startGhost();
        const originURL = configUtils.config.get('url');

        return new ContentAPITestAgent(app, {
            apiURL: '/ghost/api/content/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a AdminAPITestAgent which is a drop-in substitution for supertest.
 * It is automatically hooked up to the Admin API so you can make requests to e.g.
 * agent.get('/posts/') without having to worry about URL paths
 *
 * @param {Object} [options={}]
 * @param {Boolean} [options.members] Include members in the boot process
 * @returns {Promise<InstanceType<AdminAPITestAgent>>} agent
 */
const getAdminAPIAgent = async (options = {}) => {
    const bootOptions = {};

    if (options.members) {
        bootOptions.frontend = true;
    }

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a MembersAPITestAgent which is a drop-in substitution for supertest
 * It is automatically hooked up to the Members API so you can make requests to e.g.
 * agent.get('/webhooks/stripe/') without having to worry about URL paths
 *
 * @returns {Promise<InstanceType<MembersAPITestAgent>>} agent
 */
const getMembersAPIAgent = async () => {
    const bootOptions = {
        frontend: true
    };
    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a GhostAPITestAgent, which is a drop-in substitution for supertest
 * It is automatically hooked up to the Ghost API so you can make requests to e.g.
 * agent.get('/well-known/jwks.json') without having to worry about URL paths
 *
 * @returns {Promise<InstanceType<GhostAPITestAgent>>} agent
 */
const getGhostAPIAgent = async () => {
    const bootOptions = {
        frontend: false
    };

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new GhostAPITestAgent(app, {
            apiURL: '/ghost/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 *
 * @returns {Promise<{adminAgent: InstanceType<AdminAPITestAgent>, membersAgent: InstanceType<MembersAPITestAgent>}>} agents
 */
const getAgentsForMembers = async () => {
    let membersAgent;
    let adminAgent;

    const bootOptions = {
        frontend: true
    };

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        membersAgent = new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
        adminAgent = new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }

    return {
        adminAgent,
        membersAgent
    };
};

/**
 * TODO: for now this agent returns a supertest agent instead of a proper test agent.
 * We need to add support for this.
 */
const getAgentsWithFrontend = async () => {
    let membersAgent;
    let adminAgent;
    let frontendAgent;

    const bootOptions = {
        frontend: true,
        server: true
    };
    try {
        const app = (await startGhost(bootOptions)).rootApp;
        const originURL = configUtils.config.get('url');

        membersAgent = new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
        adminAgent = new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
        frontendAgent = supertest.agent(originURL);
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }

    return {
        adminAgent,
        membersAgent,
        frontendAgent
    };
};

const insertWebhook = ({event, url}) => {
    return fixtureUtils.fixtures.insertWebhook({
        event: event,
        target_url: url
    });
};

module.exports = {
    // request agent
    agentProvider: {
        getAdminAPIAgent,
        getMembersAPIAgent,
        getContentAPIAgent,
        getAgentsForMembers,
        getGhostAPIAgent,
        getAgentsWithFrontend
    },

    // Mocks and Stubs
    mockManager,

    // DB State Manipulation
    fixtureManager: {
        get: getFixture,
        insertWebhook: insertWebhook,
        getCurrentOwnerUser: fixtureUtils.getCurrentOwnerUser,
        init: initFixtures,
        restore: resetData,
        getPathForFixture: (fixturePath) => {
            return path.join(__dirname, 'fixtures', fixturePath);
        }
    },
    matchers: {
        anyBoolean: any(Boolean),
        anyString: any(String),
        anyArray: any(Array),
        anyObject: any(Object),
        anyNumber: any(Number),
        anyStringNumber: stringMatching(/\d+/),
        anyISODateTime: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/),
        anyISODate: stringMatching(/\d{4}-\d{2}-\d{2}/),
        anyISODateTimeWithTZ: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000\+\d{2}:\d{2}/),
        anyEtag: stringMatching(/(?:W\/)?"(?:[ !#-\x7E\x80-\xFF]*|\r\n[\t ]|\\.)*"/),
        anyContentLength: stringMatching(/\d+/),
        anyContentVersion: stringMatching(/v\d+\.\d+/),
        anyObjectId: stringMatching(/[a-f0-9]{24}/),
        anyErrorId: stringMatching(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/),
        anyUuid: stringMatching(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/),
        anyLocationFor: (resource) => {
            return stringMatching(new RegExp(`https?://.*?/${resource}/[a-f0-9]{24}/`));
        },
        anyGhostAgent: stringMatching(/Ghost\/\d+\.\d+\.\d+\s\(https:\/\/github.com\/TryGhost\/Ghost\)/),
        // @NOTE: hack here! it's due to https://github.com/TryGhost/Toolbox/issues/341
        //        this matcher should be removed once the issue is solved - routing is redesigned
        //        An ideal solution would be removal of this matcher altogether.
        anyLocalURL: stringMatching(/http:\/\/127.0.0.1:2369\/[A-Za-z0-9_-]+\//),
        stringMatching
    },

    // utilities
    configUtils: require('./configUtils'),
    dbUtils: require('./db-utils'),
    urlUtils: require('./urlUtils'),
    resetRateLimits
};
<commit_msg>Exposed Content API agent in e2e test utils<commit_after>// Set of common function that should be main building blocks for e2e tests.
// The e2e tests usually consist of following building blocks:
// - request agent
// - state builder
// - output state checker (in case we don't get jest snapshots working)
//
// The request agent is responsible for making HTTP-like requests to an application (express app in case of Ghost).
// Note there's no actual need to make an HTTP request to an actual server, bypassing HTTP and hooking into the application
// directly is enough and reduces dependence on blocking a port (allows to run tests in parallel).
//
// The state builder is responsible for building the state of the application. Usually it's done by using pre-defined fixtures.
// Can include building a DB state, file system state (themes, config files), building configuration state (config files) etc.
//
// The output state checker is responsible for checking the response from the app after performing a request.
const _ = require('lodash');
const {sequence} = require('@tryghost/promise');
const {any, stringMatching} = require('@tryghost/express-test').snapshot;
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const uuid = require('uuid');

const fixtureUtils = require('./fixture-utils');
const redirectsUtils = require('./redirects');
const configUtils = require('./configUtils');
const urlServiceUtils = require('./url-service-utils');
const mockManager = require('./e2e-framework-mock-manager');

const boot = require('../../core/boot');
const {AdminAPITestAgent, ContentAPITestAgent, GhostAPITestAgent, MembersAPITestAgent} = require('./agents');
const db = require('./db-utils');

// Services that need resetting
const settingsService = require('../../core/server/services/settings/settings-service');
const supertest = require('supertest');

/**
 * @param {Object} [options={}]
 * @param {Boolean} [options.backend] Boot the backend
 * @param {Boolean} [options.frontend] Boot the frontend
 * @param {Boolean} [options.server] Start a server
 * @returns {Promise<Express.Application>} ghost
 */
const startGhost = async (options = {}) => {
    /**
     * We never use the root content folder for testing!
     * We use a tmp folder.
     */
    const contentFolder = path.join(os.tmpdir(), uuid.v4(), 'ghost-test');
    await prepareContentFolder({contentFolder});

    // NOTE: need to pass this config to the server instance
    configUtils.set('paths:contentPath', contentFolder);

    const defaults = {
        backend: true,
        frontend: false,
        server: false
    };

    // Ensure the state of all data, including DB and caches
    await resetData();

    const bootOptions = Object.assign({}, defaults, options);

    const ghostServer = await boot(bootOptions);

    if (bootOptions.frontend) {
        await urlServiceUtils.isFinished();
    }

    return ghostServer;
};

/**
 * Slightly simplified copy-paste from e2e-utils.
 * @param {Object} options
 */
const prepareContentFolder = async ({contentFolder, redirectsFile = true, routesFile = true}) => {
    const contentFolderForTests = contentFolder;

    await fs.ensureDir(contentFolderForTests);
    await fs.ensureDir(path.join(contentFolderForTests, 'data'));
    await fs.ensureDir(path.join(contentFolderForTests, 'themes'));
    await fs.ensureDir(path.join(contentFolderForTests, 'images'));
    await fs.ensureDir(path.join(contentFolderForTests, 'logs'));
    await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
    await fs.ensureDir(path.join(contentFolderForTests, 'settings'));

    // Copy all themes into the new test content folder. Default active theme is always casper.
    // If you want to use a different theme, you have to set the active theme (e.g. stub)
    await fs.copy(
        path.join(__dirname, 'fixtures', 'themes'),
        path.join(contentFolderForTests, 'themes')
    );

    if (redirectsFile) {
        redirectsUtils.setupFile(contentFolderForTests, '.yaml');
    }

    if (routesFile) {
        await fs.copy(
            path.join(__dirname, 'fixtures', 'settings', 'routes.yaml'),
            path.join(contentFolderForTests, 'settings', 'routes.yaml')
        );
    }
};

/**
 * Database state builder. By default inserts an owner user into the database.
 * @param  {...any} [options]
 * @returns {Promise<void>}
 */
const initFixtures = async (...options) => {
    // No DB setup, but override the owner
    options = _.merge({'owner:post': true}, _.transform(options, function (result, val) {
        if (val) {
            result[val] = true;
        }
    }));

    const fixtureOps = fixtureUtils.getFixtureOps(options);

    return sequence(fixtureOps);
};

const getFixture = (type, index = 0) => {
    return fixtureUtils.DataGenerator.forKnex[type][index];
};

/**
 * Reset rate limit instances (not the brute table)
 */
const resetRateLimits = async () => {
    // Reset rate limiting instances
    const {spamPrevention} = require('../../core/server/web/shared/middleware/api');
    spamPrevention.reset();
};

/**
 * This function ensures that Ghost's data is reset back to "factory settings"
 *
 */
const resetData = async () => {
    // Calling reset on the database also causes the fixtures to be re-run
    // We need to unhook the settings events and restore the cache before we do this
    // Otherwise, the fixtures being restored will refer to the old settings cache data
    settingsService.reset();

    // Clear out the database
    await db.reset({truncate: true});

    // Reset rate limiting instances (resetting the table is not enough!)
    await resetRateLimits();
};

/**
 * Creates a ContentAPITestAgent which is a drop-in substitution for supertest.
 * It is automatically hooked up to the Content API so you can make requests to e.g.
 * agent.get('/posts/') without having to worry about URL paths
 * @returns {Promise<InstanceType<ContentAPITestAgent>>} agent
 */
const getContentAPIAgent = async () => {
    try {
        const app = await startGhost();
        const originURL = configUtils.config.get('url');

        return new ContentAPITestAgent(app, {
            apiURL: '/ghost/api/content/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a AdminAPITestAgent which is a drop-in substitution for supertest.
 * It is automatically hooked up to the Admin API so you can make requests to e.g.
 * agent.get('/posts/') without having to worry about URL paths
 *
 * @param {Object} [options={}]
 * @param {Boolean} [options.members] Include members in the boot process
 * @returns {Promise<InstanceType<AdminAPITestAgent>>} agent
 */
const getAdminAPIAgent = async (options = {}) => {
    const bootOptions = {};

    if (options.members) {
        bootOptions.frontend = true;
    }

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a MembersAPITestAgent which is a drop-in substitution for supertest
 * It is automatically hooked up to the Members API so you can make requests to e.g.
 * agent.get('/webhooks/stripe/') without having to worry about URL paths
 *
 * @returns {Promise<InstanceType<MembersAPITestAgent>>} agent
 */
const getMembersAPIAgent = async () => {
    const bootOptions = {
        frontend: true
    };
    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 * Creates a GhostAPITestAgent, which is a drop-in substitution for supertest
 * It is automatically hooked up to the Ghost API so you can make requests to e.g.
 * agent.get('/well-known/jwks.json') without having to worry about URL paths
 *
 * @returns {Promise<InstanceType<GhostAPITestAgent>>} agent
 */
const getGhostAPIAgent = async () => {
    const bootOptions = {
        frontend: false
    };

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        return new GhostAPITestAgent(app, {
            apiURL: '/ghost/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }
};

/**
 *
 * @returns {Promise<{adminAgent: InstanceType<AdminAPITestAgent>, membersAgent: InstanceType<MembersAPITestAgent>}>} agents
 */
const getAgentsForMembers = async () => {
    let membersAgent;
    let adminAgent;

    const bootOptions = {
        frontend: true
    };

    try {
        const app = await startGhost(bootOptions);
        const originURL = configUtils.config.get('url');

        membersAgent = new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
        adminAgent = new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }

    return {
        adminAgent,
        membersAgent
    };
};

/**
 * @NOTE: for now method returns a supertest agent for Frontend instead of test agent with snapshot support.
 *        frontendAgent should be returning an instance of TestAgent.
 *  @returns {Promise<{adminAgent: InstanceType<AdminAPITestAgent>, membersAgent: InstanceType<MembersAPITestAgent>, frontendAgent: InstanceType<supertest.SuperAgentTest>, contentAPIAgent: InstanceType<ContentAPITestAgent>}>} agents
 */
const getAgentsWithFrontend = async () => {
    let membersAgent;
    let adminAgent;
    let frontendAgent;
    let contentAPIAgent;

    const bootOptions = {
        frontend: true,
        server: true
    };
    try {
        const app = (await startGhost(bootOptions)).rootApp;
        const originURL = configUtils.config.get('url');

        membersAgent = new MembersAPITestAgent(app, {
            apiURL: '/members/',
            originURL
        });
        adminAgent = new AdminAPITestAgent(app, {
            apiURL: '/ghost/api/admin/',
            originURL
        });
        contentAPIAgent = new ContentAPITestAgent(app, {
            apiURL: '/ghost/api/content/',
            originURL
        });
        frontendAgent = supertest.agent(originURL);
    } catch (error) {
        error.message = `Unable to create test agent. ${error.message}`;
        throw error;
    }

    return {
        adminAgent,
        membersAgent,
        frontendAgent,
        contentAPIAgent
    };
};

const insertWebhook = ({event, url}) => {
    return fixtureUtils.fixtures.insertWebhook({
        event: event,
        target_url: url
    });
};

module.exports = {
    // request agent
    agentProvider: {
        getAdminAPIAgent,
        getMembersAPIAgent,
        getContentAPIAgent,
        getAgentsForMembers,
        getGhostAPIAgent,
        getAgentsWithFrontend
    },

    // Mocks and Stubs
    mockManager,

    // DB State Manipulation
    fixtureManager: {
        get: getFixture,
        insertWebhook: insertWebhook,
        getCurrentOwnerUser: fixtureUtils.getCurrentOwnerUser,
        init: initFixtures,
        restore: resetData,
        getPathForFixture: (fixturePath) => {
            return path.join(__dirname, 'fixtures', fixturePath);
        }
    },
    matchers: {
        anyBoolean: any(Boolean),
        anyString: any(String),
        anyArray: any(Array),
        anyObject: any(Object),
        anyNumber: any(Number),
        anyStringNumber: stringMatching(/\d+/),
        anyISODateTime: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z/),
        anyISODate: stringMatching(/\d{4}-\d{2}-\d{2}/),
        anyISODateTimeWithTZ: stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000\+\d{2}:\d{2}/),
        anyEtag: stringMatching(/(?:W\/)?"(?:[ !#-\x7E\x80-\xFF]*|\r\n[\t ]|\\.)*"/),
        anyContentLength: stringMatching(/\d+/),
        anyContentVersion: stringMatching(/v\d+\.\d+/),
        anyObjectId: stringMatching(/[a-f0-9]{24}/),
        anyErrorId: stringMatching(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/),
        anyUuid: stringMatching(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/),
        anyLocationFor: (resource) => {
            return stringMatching(new RegExp(`https?://.*?/${resource}/[a-f0-9]{24}/`));
        },
        anyGhostAgent: stringMatching(/Ghost\/\d+\.\d+\.\d+\s\(https:\/\/github.com\/TryGhost\/Ghost\)/),
        // @NOTE: hack here! it's due to https://github.com/TryGhost/Toolbox/issues/341
        //        this matcher should be removed once the issue is solved - routing is redesigned
        //        An ideal solution would be removal of this matcher altogether.
        anyLocalURL: stringMatching(/http:\/\/127.0.0.1:2369\/[A-Za-z0-9_-]+\//),
        stringMatching
    },

    // utilities
    configUtils: require('./configUtils'),
    dbUtils: require('./db-utils'),
    urlUtils: require('./urlUtils'),
    resetRateLimits
};
<|endoftext|>


<commit_before><commit_msg>get heatmap visualization / av_tests.js<commit_after><|endoftext|>