<commit_before>"use strict";

var fs = require('fs')
  , path = require('path')
  , _ = require('underscore')
  , logger = require('../server/logger.js').get('appium')
  , helpers = require('../helpers.js')
  , copyLocalZip = helpers.copyLocalZip
  , unzipApp = helpers.unzipApp
  , downloadFile = helpers.downloadFile
  , capConversion = require('../server/capabilities.js').capabilityConversions
  , DeviceSettings = require('./device-settings.js')
  , url = require('url');

var Device = function () {
  throw new Error("Cannot instantiate Device directly");
};

Device.prototype.init = function () {
  this.appExt = null;
  this.tempFiles = [];
  this.args = {};
  this.capabilities = {};
  this.settings = new DeviceSettings();
};

Device.prototype.configure = function (args, caps) {
  _.extend(this.args, args);
  _.extend(this.capabilities, caps);
  _.each(caps, function (val, cap) {
    this.setArgFromCap(cap, cap);
  }.bind(this));
  if (this.args.tmpDir === null) {
    this.args.tmpDir = helpers.isWindows() ? "C:\\Windows\\Temp" : "/tmp";
  }
  if (this.args.traceDir === null) {
    this.args.traceDir = path.resolve(this.args.tmpDir , 'appium-instruments');
  }
};

Device.prototype.setArgFromCap = function (arg, cap) {
  if (typeof this.capabilities[cap] !== "undefined") {
    if (_.has(capConversion, cap)) {
      var key = capConversion[cap];
      if (!_.has(this.capabilities, key)) {
        // if we have both 'version' and 'platformVersion' caps being sent,
        // make sure 'platformVersion' isn't overwritten by 'version'
        this.args[key] = this.capabilities[cap];
      }
    } else {
      this.args[arg] = this.capabilities[cap];
    }
  }
};

Device.prototype.appString = function () {
  return this.args.app ? this.args.app.toString() : '';
};

Device.prototype.configureApp = function (cb) {
  if (this.args.app.substring(0, 4).toLowerCase() === "http") {
    this.configureDownloadedApp(cb);
  } else {
    this.configureLocalApp(cb);
  }
};

Device.prototype.configureLocalApp = function (cb) {
  this.args.app = path.resolve(this.args.app);
  var appPath = this.args.app;
  var origin = this.capabilities.app ? "desired caps" : "command line";
  var ext = appPath.substring(appPath.length - 4).toLowerCase();
  if (ext === this.appExt) {
    this.args.app = appPath;
    logger.debug("Using local app from " + origin + ": " + appPath);
    fs.stat(appPath, function (err) {
      if (err) {
        return cb(new Error("Error locating the app: " + err.message));
      }
      cb();
    });
  } else if (ext === ".zip" || ext === ".ipa") {
    logger.debug("Using local " + ext + " from " + origin + ": " + appPath);
    this.unzipLocalApp(appPath, function (zipErr, newAppPath) {
      if (zipErr) return cb(zipErr);
      this.args.app = newAppPath;
      logger.debug("Using locally extracted app: " + this.args.app);
      cb();
    }.bind(this));
  } else {
    var msg = "Using local app, but didn't end in .zip, .ipa or " + this.appExt;
    logger.error(msg);
    cb(new Error(msg));
  }
};

Device.prototype.appIsPackageOrBundle = function (app) {
  return (/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/).test(app);
};

Device.prototype.configureDownloadedApp = function (cb) {
  var origin = this.capabilities.app ? "desired caps" : "command line";
  var appUrl;
  try {
    appUrl = url.parse(this.args.app);
  } catch (err) {
    cb("Invalid App URL (" + this.args.app + ")");
  }
  var ext = path.extname(appUrl.pathname);
  if (ext === ".apk") {
    try {
      downloadFile(url.format(appUrl), ".apk", function (appPath) {
        this.tempFiles.push(appPath);
        this.args.app = appPath;
        cb();
      }.bind(this));
    } catch (e) {
      var err = e.toString();
      logger.error("Failed downloading app from appUrl " + appUrl);
      cb(err);
    }
  } else if (ext === ".zip" || ext === ".ipa") {
    try {
      this.downloadAndUnzipApp(url.format(appUrl), function (zipErr, appPath) {
        if (zipErr) {
          cb(zipErr);
        } else {
          this.args.app = appPath;
          logger.debug("Using extracted app: " + this.args.app);
          cb();
        }
      }.bind(this));
      logger.debug("Using downloadable app from " + origin + ": " + appUrl);
    } catch (e) {
      var err = e.toString();
      logger.error("Failed downloading app from appUrl " + appUrl);
      cb(err);
    }
  } else {
    cb("App URL (" + this.args.app + ") didn't seem to point to a .zip, " +
       ".apk, or .ipa file");
  }
};

Device.prototype.unzipLocalApp = function (localZipPath, cb) {
  try {
    copyLocalZip(localZipPath, function (err, zipPath) {
      if (err) return cb(err);
      this.unzipApp(zipPath, cb);
    }.bind(this));
  } catch (e) {
    logger.error("Failed copying and unzipping local app: " + localZipPath);
    cb(e);
  }
};

Device.prototype.unzipApp = function (zipPath, cb) {
  this.tempFiles.push(zipPath);
  unzipApp(zipPath, this.appExt, function (err, appPath) {
    if (err) {
      cb(err, null);
    } else {
      this.tempFiles.push(appPath);
      cb(null, appPath);
    }
  }.bind(this));
};

Device.prototype.downloadAndUnzipApp = function (appUrl, cb) {
  downloadFile(appUrl, ".zip", function (zipPath) {
    this.unzipApp(zipPath, cb);
  }.bind(this));
};

// get a specific setting
Device.prototype.getSetting = function (str) {
  return this.settings._settings[str];
};

module.exports = Device;
<commit_msg>Fix for installing IPA packages on iOS devices.<commit_after>"use strict";

var fs = require('fs')
  , path = require('path')
  , _ = require('underscore')
  , logger = require('../server/logger.js').get('appium')
  , helpers = require('../helpers.js')
  , copyLocalZip = helpers.copyLocalZip
  , unzipApp = helpers.unzipApp
  , downloadFile = helpers.downloadFile
  , capConversion = require('../server/capabilities.js').capabilityConversions
  , DeviceSettings = require('./device-settings.js')
  , url = require('url');

var Device = function () {
  throw new Error("Cannot instantiate Device directly");
};

Device.prototype.init = function () {
  this.appExt = null;
  this.tempFiles = [];
  this.args = {};
  this.capabilities = {};
  this.settings = new DeviceSettings();
};

Device.prototype.configure = function (args, caps) {
  _.extend(this.args, args);
  _.extend(this.capabilities, caps);
  _.each(caps, function (val, cap) {
    this.setArgFromCap(cap, cap);
  }.bind(this));
  if (this.args.tmpDir === null) {
    this.args.tmpDir = helpers.isWindows() ? "C:\\Windows\\Temp" : "/tmp";
  }
  if (this.args.traceDir === null) {
    this.args.traceDir = path.resolve(this.args.tmpDir , 'appium-instruments');
  }
};

Device.prototype.setArgFromCap = function (arg, cap) {
  if (typeof this.capabilities[cap] !== "undefined") {
    if (_.has(capConversion, cap)) {
      var key = capConversion[cap];
      if (!_.has(this.capabilities, key)) {
        // if we have both 'version' and 'platformVersion' caps being sent,
        // make sure 'platformVersion' isn't overwritten by 'version'
        this.args[key] = this.capabilities[cap];
      }
    } else {
      this.args[arg] = this.capabilities[cap];
    }
  }
};

Device.prototype.appString = function () {
  return this.args.app ? this.args.app.toString() : '';
};

Device.prototype.configureApp = function (cb) {
  if (this.args.app.substring(0, 4).toLowerCase() === "http") {
    this.configureDownloadedApp(cb);
  } else {
    this.configureLocalApp(cb);
  }
};

Device.prototype.configureLocalApp = function (cb) {
  this.args.app = path.resolve(this.args.app);
  var appPath = this.args.app;
  var origin = this.capabilities.app ? "desired caps" : "command line";
  var ext = appPath.substring(appPath.length - 4).toLowerCase();
  if (ext === this.appExt) {
    this.args.app = appPath;
    logger.debug("Using local app from " + origin + ": " + appPath);
    fs.stat(appPath, function (err) {
      if (err) {
        return cb(new Error("Error locating the app: " + err.message));
      }
      cb();
    });
  } else if (ext === ".zip" || ext === ".ipa") {
    logger.debug("Using local " + ext + " from " + origin + ": " + appPath);
    this.unzipLocalApp(appPath, function (zipErr, newAppPath) {
      if (zipErr) return cb(zipErr);
      if (ext === ".ipa") {
        this.args.ipa = this.args.app;
      }
      this.args.app = newAppPath;
      logger.debug("Using locally extracted app: " + this.args.app);
      cb();
    }.bind(this));
  } else {
    var msg = "Using local app, but didn't end in .zip, .ipa or " + this.appExt;
    logger.error(msg);
    cb(new Error(msg));
  }
};

Device.prototype.appIsPackageOrBundle = function (app) {
  return (/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/).test(app);
};

Device.prototype.configureDownloadedApp = function (cb) {
  var origin = this.capabilities.app ? "desired caps" : "command line";
  var appUrl;
  try {
    appUrl = url.parse(this.args.app);
  } catch (err) {
    cb("Invalid App URL (" + this.args.app + ")");
  }
  var ext = path.extname(appUrl.pathname);
  if (ext === ".apk") {
    try {
      downloadFile(url.format(appUrl), ".apk", function (appPath) {
        this.tempFiles.push(appPath);
        this.args.app = appPath;
        cb();
      }.bind(this));
    } catch (e) {
      var err = e.toString();
      logger.error("Failed downloading app from appUrl " + appUrl);
      cb(err);
    }
  } else if (ext === ".zip" || ext === ".ipa") {
    try {
      this.downloadAndUnzipApp(url.format(appUrl), function (zipErr, appPath) {
        if (zipErr) {
          cb(zipErr);
        } else {
          this.args.app = appPath;
          logger.debug("Using extracted app: " + this.args.app);
          cb();
        }
      }.bind(this));
      logger.debug("Using downloadable app from " + origin + ": " + appUrl);
    } catch (e) {
      var err = e.toString();
      logger.error("Failed downloading app from appUrl " + appUrl);
      cb(err);
    }
  } else {
    cb("App URL (" + this.args.app + ") didn't seem to point to a .zip, " +
       ".apk, or .ipa file");
  }
};

Device.prototype.unzipLocalApp = function (localZipPath, cb) {
  try {
    copyLocalZip(localZipPath, function (err, zipPath) {
      if (err) return cb(err);
      this.unzipApp(zipPath, cb);
    }.bind(this));
  } catch (e) {
    logger.error("Failed copying and unzipping local app: " + localZipPath);
    cb(e);
  }
};

Device.prototype.unzipApp = function (zipPath, cb) {
  this.tempFiles.push(zipPath);
  unzipApp(zipPath, this.appExt, function (err, appPath) {
    if (err) {
      cb(err, null);
    } else {
      this.tempFiles.push(appPath);
      cb(null, appPath);
    }
  }.bind(this));
};

Device.prototype.downloadAndUnzipApp = function (appUrl, cb) {
  downloadFile(appUrl, ".zip", function (zipPath) {
    this.unzipApp(zipPath, cb);
  }.bind(this));
};

// get a specific setting
Device.prototype.getSetting = function (str) {
  return this.settings._settings[str];
};

module.exports = Device;
<|endoftext|>


<commit_before>// JavaScript Document

// Scripts written by Jacob Bearce @ Weblinx, Inc.

import "mdn-polyfills/Array.prototype.forEach";
import "mdn-polyfills/Element.prototype.closest";
import "mdn-polyfills/NodeList.prototype.forEach";
import "mdn-polyfills/Object.assign";

import inViewport from "in-vp";
import transition from "transition-to-from-auto";

/**
 * Create an array of attribtes to target lists for
 */
const ATTRIBUTES = {
    classes:  ["menu-list", "menu-list__container--mega"],
    datasets: ["accordion", "hover", "touch"],
};

const SELECTORS = [];

/**
 * Create an array out of the CLASSES and DATAS in order to reduce duplicate code.
 */
ATTRIBUTES.classes.forEach((CLASS) => {
    ATTRIBUTES.datasets.forEach((DATA) => {
        SELECTORS.push(`.${CLASS}[data-${DATA}=true]`);
    });
});

/**
 * Find all MENU_LISTS which can be hovered or touched
 */
const MENU_LISTS = document.querySelectorAll(SELECTORS.join());

/**
 * Update the label for a MENU_TOGGLE
 *
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 */
const UPDATE_LABEL = (MENU_TOGGLE) => {
    const LABEL = MENU_TOGGLE.querySelector("span.__visuallyhidden");
    const TEXT  = LABEL.innerHTML;
    const NEXT  = LABEL.dataset.alt;

    LABEL.innerHTML   = NEXT;
    LABEL.dataset.alt = TEXT;
};

/**
 * Mark an item active
 *
 * @param {Object} LIST_ITEM - A DOM object to mark as active
 * @param {Object} MENU_LIST - A DOM object to mark as visible and reverse based on viewport
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 * @param {Boolean} EVENT - An event to prevent from firing
 */
const MARK_ACTIVE = (LIST_ITEM, MENU_LIST, MENU_TOGGLE, EVENT = false) => {
    /**
     * Prevent the EVENT from finishing unless the LIST_ITEM is already active
     */
    if (EVENT && !LIST_ITEM.classList.contains("is-active")) {
        EVENT.preventDefault();
    }

    /**
     * Mark the LIST_ITEM as active
     */
    LIST_ITEM.classList.add("is-active");

    /**
     * Transition open the MENU_LIST if it's an accordion
     */
    if (MENU_LIST.dataset.accordion === "true") {
        transition({ element: MENU_LIST, val: "auto" });
    }

    /**
     * Mark the MENU_LIST as aria-hidden="false"
     */
    MENU_LIST.setAttribute("aria-hidden", "false");

    /**
     * Reverse the MENU_LIST if it's not fully within the viewport
     */
    if (inViewport(MENU_LIST).fully === false) {
        MENU_LIST.classList.add("menu-list--reverse");
    }

    /**
     * Update the MENU_TOGGLE label
     */
    UPDATE_LABEL(MENU_TOGGLE);
};

/**
 * Mark an item and its children inactive
 *
 * @param {Object} LIST_ITEM - A DOM object to mark as inactive
 * @param {Object} MENU_LIST - A DOM object to mark as hidden and unreverse
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 */
const MARK_INACTIVE = (LIST_ITEM, MENU_LIST, MENU_TOGGLE) => {
    /**
     * Find any active CHILDREN
     */
    const CHILDREN = LIST_ITEM.querySelectorAll("is-active");

    /**
     * Recursively mark active CHILDREN as inactive
     */
    if (CHILDREN.length > 0) {
        CHILDREN.foreach((CHILD) => {
            MARK_INACTIVE(CHILD);
        });
    }

    /**
     * Mark the LIST_ITEM as inactive
     */
    LIST_ITEM.classList.remove("is-active");

    /**
     * Transition open the MENU_LIST if it's an accordion
     */
    if (MENU_LIST.dataset.accordion === "true") {
        transition({ element: MENU_LIST, val: 0 });
    }

    /**
     * Mark the MENU_LIST as aria-hidden="false"
     */
    MENU_LIST.setAttribute("aria-hidden", "true");

    /**
     * Unreverse the MENU_LIST
     */
    MENU_LIST.classList.remove("menu-list--reverse");

    /**
     * Update the MENU_TOGGLE label
     */
    UPDATE_LABEL(MENU_TOGGLE);
};

/**
 * Compare two sets of coordinates to determine if the user dragged
 *
 * @param {Object} START_COORDS - The coordinates where the user started their touch
 * @param {Object} END_COORDS - The coordinates where the user ended their touch
 * @param {Integer} THRESHOLD - The minimum distance in pixels to mark something as dragged
 */
const DID_USER_DRAG = (START_COORDS, END_COORDS, THRESHOLD = 10) => {
    return Math.abs(END_COORDS.clientX - START_COORDS.clientX) > THRESHOLD || Math.abs(END_COORDS.clientY - START_COORDS.clientY) > THRESHOLD;
};

/**
 * Store various events to listen for
 */
const EVENTS = {
    document:  ["click", "touchstart", "touchend"],
    list_item: {
        activate: {
            accordion: ["touchstart", "touchend"],
            hover:     ["mouseenter"],
            touch:     ["touchstart", "touchend"],
        },
        deactivate: ["focusout", "mouseleave"],
    },
};

/**
 * Set up an object to track touches
 */
let start_coords = { clientX: 0, clientY: 0 };
let end_coords   = { clientX: 0, clientY: 0 };

/**
 * Listen for interactions on each menu
 */
MENU_LISTS.forEach((MENU_LIST) => {
    const LIST_ITEM   = MENU_LIST.closest(".menu-list__item");
    const MENU_TOGGLE = LIST_ITEM.querySelector(".menu-list__toggle");

    /**
     * Mark the LIST_ITEM as active when moused into or touched
     */
    for (const MODE in EVENTS.list_item.activate) {
        if (MENU_LIST.dataset[MODE] === "true") {

            for (const EVENT in EVENTS.list_item.activate[MODE]) {
                LIST_ITEM.addEventListener(EVENTS.list_item.activate[MODE][EVENT], (e) => {
                    /**
                     * Store the touchstart position
                     */
                    if (e.type === "touchstart") {
                        start_coords = e.touches[0];
                    }

                    /**
                     * Store the touchend position
                     */
                    if (e.type === "touchend") {
                        end_coords = e.changedTouches[0];
                    }

                    /**
                     * Mark the item as active if the event isn't touchstart and
                     * the user didn't drag their touch
                     */
                    if (e.type !== "touchstart" && !DID_USER_DRAG(start_coords, end_coords)) {
                        MARK_ACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE, e);
                    }
                });
            }
        }
    }

    /**
     * Mark the LIST_ITEM as inactive when moused away
     */
    EVENTS.list_item.deactivate.forEach((EVENT) => {
        LIST_ITEM.addEventListener(EVENT, (e) => {
            /**
             * Don't close the menu on focusout if the next focused
             * element is within the current list item!
             */
            if (EVENT === "focusout" && LIST_ITEM.contains(e.relatedTarget)) {
                return;
            }

            MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        }, { passive: true });
    });

    /**
     * Mark the LIST_ITEM as active or inactive when MENU_TOGGLE is clicked
     */
    MENU_TOGGLE.addEventListener("click", () => {
        if (!LIST_ITEM.classList.contains("is-active")) {
            MARK_ACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        } else {
            MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        }
    });

    /**
     * Mark the LIST_ITEM as inactive when clicked away or touched away
     */
    EVENTS.document.forEach((EVENT) => {
        document.addEventListener(EVENT, (e) => {
            if (LIST_ITEM.classList.contains("is-active")) {
                /**
                 * Get the event path
                 */
                const PATH = e.path || (e.composedPath && e.composedPath());

                /**
                 * Determine if the LIST_ITEM is in the path of touched elements
                 */
                const LIST_ITEM_TOUCHED = PATH.some((ELEMENT) => {
                    return LIST_ITEM === ELEMENT;
                });

                /**
                 * Store the touchstart position
                 */
                if (e.type === "touchstart") {
                    start_coords = e.touches[0];
                }

                /**
                 * Store the touchend position
                 */
                if (e.type === "touchend") {
                    end_coords = e.changedTouches[0];
                }

                /**
                 * Mark the LIST_ITEM as inactive if it's not in the path of
                 * touched elements, and the user didn't drag their touch
                 */
                if (!LIST_ITEM_TOUCHED && !DID_USER_DRAG(start_coords, end_coords)) {
                    MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
                }
            }
        }, { passive: true });
    });
});
<commit_msg>Mark event listener as non-passive<commit_after>// JavaScript Document

// Scripts written by Jacob Bearce @ Weblinx, Inc.

import "mdn-polyfills/Array.prototype.forEach";
import "mdn-polyfills/Element.prototype.closest";
import "mdn-polyfills/NodeList.prototype.forEach";
import "mdn-polyfills/Object.assign";

import inViewport from "in-vp";
import transition from "transition-to-from-auto";

/**
 * Create an array of attribtes to target lists for
 */
const ATTRIBUTES = {
    classes:  ["menu-list", "menu-list__container--mega"],
    datasets: ["accordion", "hover", "touch"],
};

const SELECTORS = [];

/**
 * Create an array out of the CLASSES and DATAS in order to reduce duplicate code.
 */
ATTRIBUTES.classes.forEach((CLASS) => {
    ATTRIBUTES.datasets.forEach((DATA) => {
        SELECTORS.push(`.${CLASS}[data-${DATA}=true]`);
    });
});

/**
 * Find all MENU_LISTS which can be hovered or touched
 */
const MENU_LISTS = document.querySelectorAll(SELECTORS.join());

/**
 * Update the label for a MENU_TOGGLE
 *
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 */
const UPDATE_LABEL = (MENU_TOGGLE) => {
    const LABEL = MENU_TOGGLE.querySelector("span.__visuallyhidden");
    const TEXT  = LABEL.innerHTML;
    const NEXT  = LABEL.dataset.alt;

    LABEL.innerHTML   = NEXT;
    LABEL.dataset.alt = TEXT;
};

/**
 * Mark an item active
 *
 * @param {Object} LIST_ITEM - A DOM object to mark as active
 * @param {Object} MENU_LIST - A DOM object to mark as visible and reverse based on viewport
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 * @param {Boolean} EVENT - An event to prevent from firing
 */
const MARK_ACTIVE = (LIST_ITEM, MENU_LIST, MENU_TOGGLE, EVENT = false) => {
    /**
     * Prevent the EVENT from finishing unless the LIST_ITEM is already active
     */
    if (EVENT && !LIST_ITEM.classList.contains("is-active")) {
        EVENT.preventDefault();
    }

    /**
     * Mark the LIST_ITEM as active
     */
    LIST_ITEM.classList.add("is-active");

    /**
     * Transition open the MENU_LIST if it's an accordion
     */
    if (MENU_LIST.dataset.accordion === "true") {
        transition({ element: MENU_LIST, val: "auto" });
    }

    /**
     * Mark the MENU_LIST as aria-hidden="false"
     */
    MENU_LIST.setAttribute("aria-hidden", "false");

    /**
     * Reverse the MENU_LIST if it's not fully within the viewport
     */
    if (inViewport(MENU_LIST).fully === false) {
        MENU_LIST.classList.add("menu-list--reverse");
    }

    /**
     * Update the MENU_TOGGLE label
     */
    UPDATE_LABEL(MENU_TOGGLE);
};

/**
 * Mark an item and its children inactive
 *
 * @param {Object} LIST_ITEM - A DOM object to mark as inactive
 * @param {Object} MENU_LIST - A DOM object to mark as hidden and unreverse
 * @param {Object} MENU_TOGGLE - A DOM object to update the label on
 */
const MARK_INACTIVE = (LIST_ITEM, MENU_LIST, MENU_TOGGLE) => {
    /**
     * Find any active CHILDREN
     */
    const CHILDREN = LIST_ITEM.querySelectorAll("is-active");

    /**
     * Recursively mark active CHILDREN as inactive
     */
    if (CHILDREN.length > 0) {
        CHILDREN.foreach((CHILD) => {
            MARK_INACTIVE(CHILD);
        });
    }

    /**
     * Mark the LIST_ITEM as inactive
     */
    LIST_ITEM.classList.remove("is-active");

    /**
     * Transition open the MENU_LIST if it's an accordion
     */
    if (MENU_LIST.dataset.accordion === "true") {
        transition({ element: MENU_LIST, val: 0 });
    }

    /**
     * Mark the MENU_LIST as aria-hidden="false"
     */
    MENU_LIST.setAttribute("aria-hidden", "true");

    /**
     * Unreverse the MENU_LIST
     */
    MENU_LIST.classList.remove("menu-list--reverse");

    /**
     * Update the MENU_TOGGLE label
     */
    UPDATE_LABEL(MENU_TOGGLE);
};

/**
 * Compare two sets of coordinates to determine if the user dragged
 *
 * @param {Object} START_COORDS - The coordinates where the user started their touch
 * @param {Object} END_COORDS - The coordinates where the user ended their touch
 * @param {Integer} THRESHOLD - The minimum distance in pixels to mark something as dragged
 */
const DID_USER_DRAG = (START_COORDS, END_COORDS, THRESHOLD = 10) => {
    return Math.abs(END_COORDS.clientX - START_COORDS.clientX) > THRESHOLD || Math.abs(END_COORDS.clientY - START_COORDS.clientY) > THRESHOLD;
};

/**
 * Store various events to listen for
 */
const EVENTS = {
    document:  ["click", "touchstart", "touchend"],
    list_item: {
        activate: {
            accordion: ["touchstart", "touchend"],
            hover:     ["mouseenter"],
            touch:     ["touchstart", "touchend"],
        },
        deactivate: ["focusout", "mouseleave"],
    },
};

/**
 * Set up an object to track touches
 */
let start_coords = { clientX: 0, clientY: 0 };
let end_coords   = { clientX: 0, clientY: 0 };

/**
 * Listen for interactions on each menu
 */
MENU_LISTS.forEach((MENU_LIST) => {
    const LIST_ITEM   = MENU_LIST.closest(".menu-list__item");
    const MENU_TOGGLE = LIST_ITEM.querySelector(".menu-list__toggle");

    /**
     * Mark the LIST_ITEM as active when moused into or touched
     */
    for (const MODE in EVENTS.list_item.activate) {
        if (MENU_LIST.dataset[MODE] === "true") {

            for (const EVENT in EVENTS.list_item.activate[MODE]) {
                LIST_ITEM.addEventListener(EVENTS.list_item.activate[MODE][EVENT], (e) => {
                    /**
                     * Store the touchstart position
                     */
                    if (e.type === "touchstart") {
                        start_coords = e.touches[0];
                    }

                    /**
                     * Store the touchend position
                     */
                    if (e.type === "touchend") {
                        end_coords = e.changedTouches[0];
                    }

                    /**
                     * Mark the item as active if the event isn't touchstart and
                     * the user didn't drag their touch
                     */
                    if (e.type !== "touchstart" && !DID_USER_DRAG(start_coords, end_coords)) {
                        MARK_ACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE, e);
                    }
                }, { passive: false });
            }
        }
    }

    /**
     * Mark the LIST_ITEM as inactive when moused away
     */
    EVENTS.list_item.deactivate.forEach((EVENT) => {
        LIST_ITEM.addEventListener(EVENT, (e) => {
            /**
             * Don't close the menu on focusout if the next focused
             * element is within the current list item!
             */
            if (EVENT === "focusout" && LIST_ITEM.contains(e.relatedTarget)) {
                return;
            }

            MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        }, { passive: true });
    });

    /**
     * Mark the LIST_ITEM as active or inactive when MENU_TOGGLE is clicked
     */
    MENU_TOGGLE.addEventListener("click", () => {
        if (!LIST_ITEM.classList.contains("is-active")) {
            MARK_ACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        } else {
            MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
        }
    });

    /**
     * Mark the LIST_ITEM as inactive when clicked away or touched away
     */
    EVENTS.document.forEach((EVENT) => {
        document.addEventListener(EVENT, (e) => {
            if (LIST_ITEM.classList.contains("is-active")) {
                /**
                 * Get the event path
                 */
                const PATH = e.path || (e.composedPath && e.composedPath());

                /**
                 * Determine if the LIST_ITEM is in the path of touched elements
                 */
                const LIST_ITEM_TOUCHED = PATH.some((ELEMENT) => {
                    return LIST_ITEM === ELEMENT;
                });

                /**
                 * Store the touchstart position
                 */
                if (e.type === "touchstart") {
                    start_coords = e.touches[0];
                }

                /**
                 * Store the touchend position
                 */
                if (e.type === "touchend") {
                    end_coords = e.changedTouches[0];
                }

                /**
                 * Mark the LIST_ITEM as inactive if it's not in the path of
                 * touched elements, and the user didn't drag their touch
                 */
                if (!LIST_ITEM_TOUCHED && !DID_USER_DRAG(start_coords, end_coords)) {
                    MARK_INACTIVE(LIST_ITEM, MENU_LIST, MENU_TOGGLE);
                }
            }
        }, { passive: true });
    });
});
<|endoftext|>


<commit_before>/*!
 * Muster v1.7.2
 * http://apps.education.ucsb.edu/redmine/projects/muster
 *
 * Copyright 2011, Justin Force
 * Licensed under the BSD 3-Clause License
 *
 * Includes "Merge Sort in JavaScript"
 */

/*jslint browser: true, indent: 2 */
/*global jQuery */

// Merge Sort

/*!
 * Merge Sort in JavaScript v1.0
 * http://github.com/sidewaysmilk/merge-sort
 *
 * Copyright (c) 2011, Justin Force
 * Licensed under the BSD 3-Clause License
 */

/*jslint browser: true, indent: 2 */
/*global jQuery */

(function () {

  'use strict';

  // Add stable merge sort method to Array prototype
  if (!Array.mergeSort) {
    Array.prototype.mergeSort = function (compare) {

      var length = this.length,
        middle = Math.floor(length / 2);

      // define default comparison function if none is defined
      if (!compare) {
        compare = function (left, right) {
          if (left  <  right) {
            return -1;
          } else if (left === right) {
            return 0;
          } else {
            return 1;
          }
        };
      }

      if (length < 2) {
        return this;
      }

      function merge(left, right, compare) {

        var result = [];

        while (left.length > 0 || right.length > 0) {
          if (left.length > 0 && right.length > 0) {
            if (compare(left[0], right[0]) <= 0) {
              result.push(left[0]);
              left = left.slice(1);
            } else {
              result.push(right[0]);
              right = right.slice(1);
            }
          } else if (left.length > 0) {
            result.push(left[0]);
            left = left.slice(1);
          } else if (right.length > 0) {
            result.push(right[0]);
            right = right.slice(1);
          }
        }
        return result;
      }

      return merge(
        this.slice(0, middle).mergeSort(compare),
        this.slice(middle, length).mergeSort(compare),
        compare
      );
    };
  }

  // Add merge sort to jQuery if it's present
  if (window.jQuery !== undefined) {
    jQuery.fn.mergeSort = function (compare) {
      return jQuery(Array.prototype.mergeSort.call(this, compare));
    };
    jQuery.mergeSort = function (array, compare) {
      return Array.prototype.mergeSort.call(array, compare);
    };
  }

}());

// Muster
(function (context, $) {

  'use strict'; // strict ECMAScript interpretation

  // Constants
  var POSSIBLE_PARAMETERS = [ 'database', 'select', 'from', 'where', 'order' ],
    DEFAULT_URL = 'https://apps.education.ucsb.edu/muster/';

  ///////////////////////////////////////////////////////////////////////////////
  // Constructors and utility functions. Utility functions are specifically meant
  // NOT to be methods of the Muster object.
  ///////////////////////////////////////////////////////////////////////////////

  // Constructor
  function Muster(args) {

    if (typeof args === 'string' || args instanceof String) {
      this.database = args;
    } else if (args !== undefined) {
      this.url = args.url;
      this.database = args.database;
    }

    if (this.url === undefined) {
      this.url = DEFAULT_URL;
    }
  }

  // Constructor wrapper.
  // Whether called as a function or a constructor, it always returns an instance
  // of Muster, i.e. `Muster` and `new Muster()` are equivalent.
  function constructorWrapper(args) {
    return new Muster(args);
  }

  // Assemble the request URI
  function getRequestUri(url, database, params) {

    // each value is [ 'key', 'value' ] and will become 'key=value'
    var parameterPairs = [];

    // Add database to parameters
    params.database = database;

    // assemble the parameters
    $.each(POSSIBLE_PARAMETERS, function () {
      if (params[this] !== undefined && params[this].length > 0) {
        parameterPairs.push([this, window.escape(params[this])].join('='));
      }
    });
    parameterPairs.push('callback=?'); // jQuery JSONP support

    return [url, '?', parameterPairs.join('&')].join('');
  }

  /* Return a copy of the table which supports stable sorting when the table's
   * column headings are clicked.
   *
   * Triggers the $(window).bind('muster_sorted') event after sorting.
   *
   * N.B. If table contents are modified after the table is sorted, sorting and
   *      reverse sorting by clicking the same heading multiple times WILL NOT
   *      correctly sort based on the new content. Sorting again by the same
   *      column just reverses the rows. This has the dual benefits of being
   *      efficient and maintaining a stable sort. If, in the future, Muster
   *      needs to handle sorting a table after data has been modified
   *      dynamically, all of the headings should be stripped of their 'sort'
   *      and 'rsort' classes (i.e.
   *      th.removeClass('sorted').removeClass('rsorted')) BEFORE sorting is
   *      performed to ensure a new sort is performed and that the order isn't
   *      simply reversed.
   */
  function getSortableTable(table) {
    table.find('th').css({cursor: 'pointer'}).click(function (event) {

      var sortedRows,
        th = $(event.target), // the heading that was clicked
        table = th.closest('table'),
        tbody = table.find('tbody'),
        index = th.index() + 1, // the numerical position of the clicked heading
        rows = table.find('tbody tr'),
        sorted = th.hasClass('sorted'), // is the column already sorted?
        rsorted = th.hasClass('rsorted'); // is the column already reverse sorted?

      // Remove sort statuses from all other headings
      th.siblings().removeClass('sorted').removeClass('rsorted');

      // If it's already sorted, the quickest solution is to just reverse it.
      // Otherwise, do a stable merge sort of the unsorted column and mark it
      // as sorted.
      if (sorted || rsorted) {
        th.toggleClass('sorted').toggleClass('rsorted');
        sortedRows = Array.prototype.reverse.apply(rows);
      } else {
        sortedRows = rows.mergeSort(function (left, right) {

          // compare the text of each cell, case insensitive
          left  =  $(left).find('td:nth-child(' + index + ')').text().toLowerCase();
          right = $(right).find('td:nth-child(' + index + ')').text().toLowerCase();

          if (left  <  right) {
            return -1;
          } else if (left === right) {
            return 0;
          } else {
            return 1;
          }
        });

        th.toggleClass('sorted');
      }

      tbody.append(sortedRows);

      $(window).trigger('muster_sorted');

    });
    return table;
  }

  // Expose Muster constructor as method of 'context' (i.e. window.Muster). It's
  // not capitalized because it's a method of 'context' that returns an instance
  // of Muster. The Muster constructor should never be called directly.
  context.muster = constructorWrapper;

  ///////////////////////////////////////////////////////////////////////////////
  // Muster's prototype
  // All public methods are defined here.
  ///////////////////////////////////////////////////////////////////////////////
  Muster.prototype = {

    query: function (query, callback) {
      var muster = this;
      $.getJSON(getRequestUri(this.url, this.database, query), function (data) {
        muster.columns = data.columns;
        muster.results = data.results;
        callback.apply(muster);
      });
    },

    isEmpty: function () {
      return this.results.length < 1;
    },

    clone: function () {

      var property,
        oldMuster = this,
        newMuster = constructorWrapper();

      for (property in oldMuster) {
        if (oldMuster.hasOwnProperty(property)) {
          newMuster[property] = oldMuster[property];
        }
      }
      return newMuster;
    },

    filter: function (column, value) {

      var clone = this.clone();

      clone.results = $.grep(this.results, function (row) {
        return row[column] === value;
      });

      return clone;
    },

    get: function (column) {
      var ret = [];
      $.each(this.results, function () {
        ret.push(this[column]);
      });
      return ret;
    },

    getUnique: function (column) {
      return $.unique(this.get(column));
    },

    getFirst: function (column) {
      return this.results[0][column];
    },

    groupBy: function (column) {

      // ret is an array of arrays. Each array in ret shares the same value for
      // row[column]. uniq keeps track of whether we've encountered a value
      // before so we only traverse the results once.
      var ret = [],
        uniq = [],
        muster = this;

      $.each(this.results, function () {
        var i = uniq.indexOf(this[column]);
        if (i < 0) {
          uniq.push(this[column]);
          i = uniq.length - 1;
          ret[i] = muster.clone();
          ret[i].results = [];
        }
        ret[i].results.push(this);
      });
      return ret;
    },

   /*
    * TODO: document customProperties arg and usage
    *
    * Return a modified Muster which joins together similar rows based on
    * `uniqueColumn` (usually something like "id"). Columns with multiple
    * values become nested.
    *
    * { "id": 2, "friend": "Bob" }
    * { "id": 2, "friend": "Doug" }
    * { "id": 3, "friend": "Sue" }
    * { "id": 3, "friend": "Daisy" }
    *
    * becomes
    *
    * { "id": 2, "friend": [ "Bob", "Doug" ] }
    * { "id": 3, "friend": [ "Sue", "Daisy" ] }
    */
    serializeBy: function (uniqueColumn, customProperties) {

      var columns,
        grouped = this.groupBy(uniqueColumn),
        clone = this.clone();

      clone.results = [];
      if (grouped.length === 0) {
        return clone;
      }

      columns = grouped[0].columns;

      // add labels from custom properties
      $.each(customProperties, function () {
        var prop;
        for (prop in this) {
          if (this.hasOwnProperty(prop)) {
            columns.push(prop);
          }
        }
      });

      /*
       * TODO: This has become significantly more complicated with the
       * implementaiton of #93. Document this stuff well since it looks very
       * gnarly.
       *
       * For each row in each group, examine the values one at a time.
       *
       *   - If the value isn't yet defined in the output, just copy the
       *     incoming value to the output
       *
       *   - If the value in the output is already defined and is a string, it
       *     is a single value. We convert it to an array consisting of the
       *     original value plus the incoming value.
       *
       *   - Otherwise, the output is already an array. Just push the new value
       *     onto it unless it already exists.
       *
       * Once we figure out the row contents, we push it into the clone and
       * return the clone at the end.
       */
      $.each(grouped, function () {
        var mergedRow = {};

        $.each(this.results, function () {
          var row = this;

          // add any custom properties
          $.each(customProperties, function () {
            var prop, attr, obj;
            for (prop in this) {
              if (this.hasOwnProperty(prop)) {
                obj = {};
                for (attr in this[prop]) {
                  if (this[prop].hasOwnProperty(attr)) {
                    obj[attr] = row[this[prop][attr]];
                  }
                }
                row[prop] = obj;
              }
            }
          });

          $.each(columns, function () {

            /* Set the value for this column. We check for and avoid creating
             * duplicates as we insert because of the nature of SQL queries.
             * Identical values will appear in adjacent cells when it's a join
             * query.
             *
             * If the merged cell isn't set, simply set it to the new value.
             *
             * If the merged cell contains an array, simply push the new value
             * onto the array (as long as it doesn't already exist).
             *
             * If the merged cell is set and is not an array, set the merged
             * cell to an array containing both values (as long as the new value
             * isn't the same as the old one).
             */
            if (mergedRow[this] === undefined) {
              mergedRow[this] = row[this];
            } else if (mergedRow[this] instanceof Array) {
              if (mergedRow[this].indexOf(row[this]) < 0) {
                mergedRow[this].push(row[this]);
              }
            } else {
              if (mergedRow[this] !== row[this]) {
                mergedRow[this] = [mergedRow[this], row[this]];
              }
            }
          });
        });
        clone.results.push(mergedRow);
      });

      return clone;
    },

    /*
     * Legacy support for old syntax
     */
    serializeJoinedResults: function (uniqueColumn) {
      return this.serializeBy(uniqueColumn);
    },

    toTable: function (columnSpec, parent, callback) {

      var columns, columnLabels,
        table = $('

'),
        thead = table.find('thead tr'),
        tbody = table.find('tbody');

      if (!columnSpec) {
        columns = columnLabels = this.columns;
      } else {
        columns = [];
        columnLabels = [];
        $.each(columnSpec, function () {
          if (this instanceof Array) {
            columnLabels.push(this[0]);
            columns.push(this[1]);
          } else if (typeof this === 'string' || this instanceof String) {

            //XXX We use both typeof and instance of to cover this weird bug in
            //IE and Safari where reloading the page makes these things Strings
            //instead of strings. O_o
            //
            columns.push(this.toString());
            columnLabels.push(this.toString());
          }
        });
      }

      $.each(columnLabels, function () {
        thead.append('' + this);
      });

      $.each(this.results, function () {

        var row = this,
          tr = $('
');

        tbody.append(tr);
        $.each(columns, function () {
          var value,
            td = $('');

          if (typeof this === 'function') {

            // formatting function
            value = this.apply(row);

          } else if (row[this] instanceof Array) {

            // multiple values
            value = row[this].join('

');
            value = '

' + value + '

';

          } else {

            // just a string
            value = row[this];
          }
          tr.append(td.append(value));
        });
      });

      table = getSortableTable(table);

      if (parent) {
        $($(parent).html(table));
      }

      if (callback) {
        callback.apply(table);
      }

      return table;
    }
  };

  // Add Array.indexOf to browsers that don't have it (i.e. IE)
  (function () {
    if (Array.indexOf === undefined) {
      Array.prototype.indexOf = function (obj) {
        var i, len;
        for (i = 0, len = this.length; i < len; i += 1) {
          if (this[i] === obj) {
            return i;
          }
        }
        return -1;
      };
    }
  }());

}(window, jQuery));

<commit_msg>Comments; white space; remove useless methods from Muster<commit_after>/*!
 * Muster v1.7.2
 * http://apps.education.ucsb.edu/redmine/projects/muster
 *
 * Copyright 2011, Justin Force
 * Licensed under the BSD 3-Clause License
 *
 * Includes "Merge Sort in JavaScript"
 */

/*jslint browser: true, indent: 2 */
/*global jQuery */

// Merge Sort

/*!
 * Merge Sort in JavaScript v1.0
 * http://github.com/sidewaysmilk/merge-sort
 *
 * Copyright (c) 2011, Justin Force
 * Licensed under the BSD 3-Clause License
 */

/*jslint browser: true, indent: 2 */
/*global jQuery */

(function () {

  'use strict';

  // Add stable merge sort method to Array prototype
  //
  if (!Array.mergeSort) {
    Array.prototype.mergeSort = function (compare) {

      var length = this.length,
        middle = Math.floor(length / 2);

      // define default comparison function if none is defined
      //
      if (!compare) {
        compare = function (left, right) {
          if (left  <  right) {
            return -1;
          } else if (left === right) {
            return 0;
          } else {
            return 1;
          }
        };
      }

      if (length < 2) {
        return this;
      }

      // merge left and right arrays using comparison function `compare`
      //
      function merge(left, right, compare) {

        var result = [];

        while (left.length > 0 || right.length > 0) {
          if (left.length > 0 && right.length > 0) {
            if (compare(left[0], right[0]) <= 0) {
              result.push(left[0]);
              left = left.slice(1);
            } else {
              result.push(right[0]);
              right = right.slice(1);
            }
          } else if (left.length > 0) {
            result.push(left[0]);
            left = left.slice(1);
          } else if (right.length > 0) {
            result.push(right[0]);
            right = right.slice(1);
          }
        }
        return result;
      }

      return merge(
        this.slice(0, middle).mergeSort(compare),
        this.slice(middle, length).mergeSort(compare),
        compare
      );
    };
  }

  // Add merge sort to jQuery if it's present
  //
  if (window.jQuery !== undefined) {
    jQuery.fn.mergeSort = function (compare) {
      return jQuery(Array.prototype.mergeSort.call(this, compare));
    };
    jQuery.mergeSort = function (array, compare) {
      return Array.prototype.mergeSort.call(array, compare);
    };
  }

}());

// Muster
//
(function (context, $) {

  'use strict'; // strict ECMAScript interpretation

  // Constants
  //
  var POSSIBLE_PARAMETERS = [ 'database', 'select', 'from', 'where', 'order' ],
    DEFAULT_URL = 'https://apps.education.ucsb.edu/muster/';

  ///////////////////////////////////////////////////////////////////////////////
  // Constructors and utility functions. Utility functions are specifically meant
  // NOT to be methods of the Muster object.
  ///////////////////////////////////////////////////////////////////////////////

  // Constructor
  //
  function Muster(args) {

    if (isString(args)) {
      this.database = args;
    } else if (args !== undefined) {
      this.url = args.url;
      this.database = args.database;
    }

    if (this.url === undefined) {
      this.url = DEFAULT_URL;
    }
  }

  // Constructor wrapper.
  // Whether called as a function or a constructor, it always returns an instance
  // of Muster, i.e. `Muster` and `new Muster()` are equivalent.
  //
  function constructorWrapper(args) {
    return new Muster(args);
  }

  // Assemble the request URI
  //
  function getRequestUri(url, database, params) {

    // each value is [ 'key', 'value' ] and will become 'key=value'
    var parameterPairs = [];

    // Add database to parameters
    params.database = database;

    // assemble the parameters
    $.each(POSSIBLE_PARAMETERS, function () {
      if (params[this] !== undefined && params[this].length > 0) {
        parameterPairs.push([this, window.escape(params[this])].join('='));
      }
    });
    parameterPairs.push('callback=?'); // jQuery JSONP support

    return [url, '?', parameterPairs.join('&')].join('');
  }

  // Return true if obj is a String
  //
  function isString(obj) {

    // XXX We use both typeof and instance of to cover this weird bug
    // in IE and Safari where reloading the page makes these things
    // Strings instead of strings. O_o
    //
    return (typeof obj === 'string' || obj instanceof String);
  }

  /* Return a copy of the table which supports stable sorting when the table's
   * column headings are clicked.
   *
   * Triggers the $(window).bind('muster_sorted') event after sorting.
   *
   * N.B. If table contents are modified after the table is sorted, sorting and
   *      reverse sorting by clicking the same heading multiple times WILL NOT
   *      correctly sort based on the new content. Sorting again by the same
   *      column just reverses the rows. This has the dual benefits of being
   *      efficient and maintaining a stable sort. If, in the future, Muster
   *      needs to handle sorting a table after data has been modified
   *      dynamically, all of the headings should be stripped of their 'sort'
   *      and 'rsort' classes (i.e.
   *      th.removeClass('sorted').removeClass('rsorted')) BEFORE sorting is
   *      performed to ensure a new sort is performed and that the order isn't
   *      simply reversed.
   */
  function getSortableTable(table) {
    table.find('th').css({cursor: 'pointer'}).click(function (event) {

      var sortedRows,
        th = $(event.target),             // the heading that was clicked
        table = th.closest('table'),
        tbody = table.find('tbody'),
        index = th.index() + 1,           // the numerical position of the clicked heading
        rows = table.find('tbody tr'),
        sorted = th.hasClass('sorted'),   // is the column already sorted?
        rsorted = th.hasClass('rsorted'); // is the column already reverse sorted?

      // Remove sort statuses from all other headings
      //
      th.siblings().removeClass('sorted').removeClass('rsorted');

      // If it's already sorted, the quickest solution is to just reverse it.
      // Otherwise, do a stable merge sort of the unsorted column and mark it
      // as sorted.
      //
      if (sorted || rsorted) {
        th.toggleClass('sorted').toggleClass('rsorted');
        sortedRows = Array.prototype.reverse.apply(rows);
      } else {
        sortedRows = rows.mergeSort(function (left, right) {

          // compare the text of each cell, case insensitive
          //
          left  =  $(left).find('td:nth-child(' + index + ')').text().toLowerCase();
          right = $(right).find('td:nth-child(' + index + ')').text().toLowerCase();

          if (left  <  right) {
            return -1;
          } else if (left === right) {
            return 0;
          } else {
            return 1;
          }
        });

        th.toggleClass('sorted');
      }

      tbody.append(sortedRows);

      $(window).trigger('muster_sorted');

    });
    return table;
  }

  // Expose Muster constructor as method of 'context' (i.e. window.Muster). It's
  // not capitalized because it's a method of 'context' that returns an instance
  // of Muster. The Muster constructor should never be called directly.
  //
  context.muster = constructorWrapper;

  ///////////////////////////////////////////////////////////////////////////////
  // Muster's prototype
  // All public methods are defined here.
  ///////////////////////////////////////////////////////////////////////////////
  //
  Muster.prototype = {

    query: function (query, callback) {
      var muster = this;
      $.getJSON(getRequestUri(this.url, this.database, query), function (data) {
        muster.columns = data.columns;
        muster.results = data.results;
        callback.apply(muster);
      });
    },

    // Return true if there are no results
    //
    isEmpty: function () {
      return this.results.length < 1;
    },

    // Return a new copy of this Muster
    //
    clone: function () {

      var property,
        oldMuster = this,
        newMuster = constructorWrapper();

      for (property in oldMuster) {
        if (oldMuster.hasOwnProperty(property)) {
          newMuster[property] = oldMuster[property];
        }
      }
      return newMuster;
    },

    /*
     * Return a copy of this Muster containing only the results for which
     * `filterFunction` returns true.
     *
     * In `filterFunction`, `this` refers to the current row, so to return all
     * of the results for which the first_name field starts with "R", you might
     * do
     *
     *    myResults.filter(function () {
     *      return this.first_name.indexOf('R') === 0;
     *    });
     */
    filter: function (filterFunction) {

      var clone = this.clone();

      clone.results = $.grep(this.results, function (row) {
        return filterFunction.call(row);
      });

      return clone;
    },

    /*
     * Return an array of Musters where each row shares the same value for the
     * `column` passed in.
     *
     * e.g. if your current Muster.results looked like
     *
     *   myResults = [
     *     {id: 7, name: 'Bob'},
     *     {id: 7, name: 'Sue'},
     *     {id: 9, name: 'Fred'},
     *     {id: 9, name: 'Bob'},
     *   ];
     *
     * then `myResults.groupBy('id');` would yield
     *
     *   [
     *     [
     *       {id: 7, name: 'Bob'},
     *       {id: 7, name: 'Sue'}
     *     ],
     *     [
     *       {id: 9, name: 'Fred'},
     *       {id: 9, name: 'Bob'}
     *     ]
     *   ]
     */
    groupBy: function (column) {

      // XXX ret is an array of Musters. Each Muster shares the same value for
      // row[column]. uniq keeps track of whether we've encountered a value
      // before so we only traverse the results once.
      //
      var ret = [],
        uniq = [],
        muster = this;

      $.each(this.results, function () {
        var i = uniq.indexOf(this[column]);
        if (i < 0) {
          uniq.push(this[column]);
          i = uniq.length - 1;
          ret[i] = muster.clone();
          ret[i].results = [];
        }
        ret[i].results.push(this);
      });
      return ret;
    },

    /*
     * TODO: Document customProperties arg and usage.
     *
     * Return a modified Muster which joins together similar rows based on
     * `uniqueColumn` (usually something like "id"). Columns with multiple
     * values become nested.
     *
     * { "id": 2, "friend": "Bob" }
     * { "id": 2, "friend": "Doug" }
     * { "id": 3, "friend": "Sue" }
     * { "id": 3, "friend": "Daisy" }
     *
     * becomes
     *
     * { "id": 2, "friend": [ "Bob", "Doug" ] }
     * { "id": 3, "friend": [ "Sue", "Daisy" ] }
     */
    serializeBy: function (uniqueColumn, customProperties) {

      var columns,
        grouped = this.groupBy(uniqueColumn),
        clone = this.clone();

      clone.results = [];
      if (grouped.length === 0) {
        return clone;
      }

      columns = grouped[0].columns;

      // add labels from custom properties
      //
      $.each(customProperties, function () {
        var prop;
        for (prop in this) {
          if (this.hasOwnProperty(prop)) {
            columns.push(prop);
          }
        }
      });

      /*
       * TODO: This has become significantly more complicated with the
       * implementaiton of #93. Document this stuff well since it looks very
       * gnarly.
       *
       * For each row in each group, examine the values one at a time.
       *
       *   - If the value isn't yet defined in the output, just copy the
       *     incoming value to the output
       *
       *   - If the value in the output is already defined and is a string, it
       *     is a single value. We convert it to an array consisting of the
       *     original value plus the incoming value.
       *
       *   - Otherwise, the output is already an array. Just push the new value
       *     onto it unless it already exists.
       *
       * Once we figure out the row contents, we push it into the clone and
       * return the clone at the end.
       */
      $.each(grouped, function () {
        var mergedRow = {};

        $.each(this.results, function () {
          var row = this;

          // add any custom properties
          $.each(customProperties, function () {
            var prop, attr, obj;
            for (prop in this) {
              if (this.hasOwnProperty(prop)) {
                obj = {};
                for (attr in this[prop]) {
                  if (this[prop].hasOwnProperty(attr)) {
                    obj[attr] = row[this[prop][attr]];
                  }
                }
                row[prop] = obj;
              }
            }
          });

          $.each(columns, function () {

            /* Set the value for this column. We check for and avoid creating
             * duplicates as we insert because of the nature of SQL queries.
             * Identical values will appear in adjacent cells when it's a join
             * query.
             *
             * If the merged cell isn't set, simply set it to the new value.
             *
             * If the merged cell contains an array, simply push the new value
             * onto the array (as long as it doesn't already exist).
             *
             * If the merged cell is set and is not an array, set the merged
             * cell to an array containing both values (as long as the new value
             * isn't the same as the old one).
             */
            if (mergedRow[this] === undefined) {
              mergedRow[this] = row[this];
            } else if (mergedRow[this] instanceof Array) {
              if (mergedRow[this].indexOf(row[this]) < 0) {
                mergedRow[this].push(row[this]);
              }
            } else {
              if (mergedRow[this] !== row[this]) {
                mergedRow[this] = [mergedRow[this], row[this]];
              }
            }
          });
        });
        clone.results.push(mergedRow);
      });

      return clone;
    },

    // Legacy support for old syntax
    //
    serializeJoinedResults: function (uniqueColumn) {
      return this.serializeBy(uniqueColumn);
    },

    toTable: function (columnSpec, parent, callback) {

      var columns, columnLabels,
        table = $('

'),
        thead = table.find('thead tr'),
        tbody = table.find('tbody');

      if (!columnSpec) {
        columns = columnLabels = this.columns;
      } else {
        columns = [];
        columnLabels = [];
        $.each(columnSpec, function () {
          if (this instanceof Array) {
            columnLabels.push(this[0]);
            columns.push(this[1]);
          } else if (isString(this)) {

            columns.push(this.toString());
            columnLabels.push(this.toString());
          }
        });
      }

      $.each(columnLabels, function () {
        thead.append('' + this);
      });

      $.each(this.results, function () {

        var row = this,
          tr = $('
');

        tbody.append(tr);
        $.each(columns, function () {
          var value,
            td = $('');

          if (typeof this === 'function') {

            // formatting function
            //
            value = this.apply(row);

          } else if (row[this] instanceof Array) {

            // multiple values
            //
            value = row[this].join('

');
            value = '

' + value + '

';

          } else {

            // just a string
            //
            value = row[this];
          }
          tr.append(td.append(value));
        });
      });

      table = getSortableTable(table);

      if (parent) {
        $($(parent).html(table));
      }

      if (callback) {
        callback.apply(table);
      }

      return table;
    }
  };

  // Add Array.indexOf to browsers that don't have it (i.e. IE)
  //
  (function () {
    if (Array.indexOf === undefined) {
      Array.prototype.indexOf = function (obj) {
        var i, len;
        for (i = 0, len = this.length; i < len; i += 1) {
          if (this[i] === obj) {
            return i;
          }
        }
        return -1;
      };
    }
  }());

}(window, jQuery));

<|endoftext|>


<commit_before>// Copyright (C) 2011,2013,2014 Colin Walters <walters@verbum.org>
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the
// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
// Boston, MA 02111-1307, USA.

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Format = imports.format;

const GSystem = imports.gi.GSystem;
const OSTree = imports.gi.OSTree;

const Task = imports.task;
const Params = imports.params;
const FileUtil = imports.fileutil;
const ProcUtil = imports.procutil;
const JSUtil = imports.jsutil;
const StreamUtil = imports.streamutil;
const JsonUtil = imports.jsonutil;
const Snapshot = imports.snapshot;
const BuildUtil = imports.buildutil;
const Vcs = imports.vcs;

const TaskBuild = new Lang.Class({
    Name: "TaskBuild",
    Extends: Task.Task,

    TaskDef: {
        TaskName: "build",
    },

    DefaultParameters: {onlyTreesMatching: null},

    BuildState: { 'failed': 'failed',
		  'successful': 'successful',
		  'unchanged': 'unchanged' },

    _composeProduct: function(ref, productName, treeName, treeData, release, architecture, cancellable) {
	let repos = ['fedora-' + release];
	if (release != 'rawhide')
	    repos.push('fedora-' + release + '-updates');

	let addRepos = this._productData['add-repos'];
	if (addRepos)
	    repos.push.apply(repos, addRepos);

	let packages = treeData['packages'];
	let bootstrapBase = this._productData['bootstrap_base'];
	let baseRequired = this._productData['base_required_packages'];
	packages.push.apply(packages, baseRequired);

	let postprocessSteps = this._productData['postprocess'];
	if (postprocessSteps === undefined)
	    postprocessSteps = [];
	else
	    print("Using master postprocessing " + JSON.stringify(postprocessSteps));

	let treePostprocess = treeData['postprocess'];
	if (treePostprocess) {
	    print("Appending postprocessing " + JSON.stringify(treePostprocess));
	    postprocessSteps.push.apply(postprocessSteps, treePostprocess);
	}

	let [,origRevision] = this.ostreeRepo.resolve_rev(ref, true);
	if (origRevision == null)
	    print("Starting new build of " + ref);
	else
	    print("Starting build of " + ref + " previous: " + origRevision);

	let argv = ['rpm-ostree',
		    '--workdir=' + this.workdir.get_path()];
	argv.push.apply(argv, bootstrapBase.map(function (a) { return '--bootstrap-package=' + a; }));
	argv.push.apply(argv, repos.map(function (a) { return '--enablerepo=' + a; }));
	argv.push.apply(argv, postprocessSteps.map(function (a) { return '--post=' + a; }));
	argv.push.apply(argv, ['create', ref]);
	argv.push.apply(argv, packages);
	let productNameUnix = ref.replace(/\//g, '_');
	let buildOutputPath = Gio.File.new_for_path('log-' + productNameUnix + '.txt');

	let procContext = new GSystem.SubprocessContext({ argv: argv });
	GSystem.shutil_rm_rf(buildOutputPath, cancellable);
	procContext.set_stdout_file_path(buildOutputPath.get_path());
	procContext.set_stderr_disposition(GSystem.SubprocessStreamDisposition.STDERR_MERGE);
	let proc = new GSystem.Subprocess({ context: procContext });
	proc.init(cancellable);
	try {
	    proc.wait_sync_check(cancellable);
	} catch (e) {
	    print("Build of " + productName + " failed");
	    return [this.BuildState.failed, origRevision];
	}
	let [,newRevision] = this.ostreeRepo.resolve_rev(ref, true);
	if (origRevision == newRevision)
	    return [this.BuildState.unchanged, newRevision];
	return [this.BuildState.successful, newRevision];
    },

    execute: function(cancellable) {
	let productPath = this.workdir.get_child('products.json');
	let productData = JsonUtil.loadJson(productPath, cancellable);
	this._productData = productData;

	let releases = productData['releases'];
	let architectures = productData['architectures'];
	let products = productData['products'];
	let successful = [];
	let failed = [];
	let unchanged = [];
	let productTrees = {};
	for (let i = 0; i < releases.length; i++) {
	    for (let j = 0; j < architectures.length; j++) {
		for (let productName in products) {
		    for (let treeName in products[productName]) {
			let release = releases[i];
			let architecture = architectures[j];
			let ref = [this._productData['osname'], release, architecture, productName, treeName].join('/');
			if (this.parameters.onlyTreesMatching &&
			    ref.indexOf(this.parameters.onlyTreesMatching) == -1) {
			    log("Skipping " + ref + " which does not match " + this.parameters.onlyTreesMatching);
			    continue;
			}
			let [result, revision] =
			    this._composeProduct(ref, productName, treeName, products[productName][treeName],
						 release, architecture,
						 cancellable);
			productTrees[ref] = revision;
			switch (result) {
			    case this.BuildState.successful: {
				successful.push(ref);
			    }
			    break;
			    case this.BuildState.failed: {
				failed.push(ref);
			    }
			    break;
			    case this.BuildState.unchanged: {
				unchanged.push(ref);
			    }
			    break;
			    default:
			    throw new Error("Invalid result from composeProduct: " + result);
			}
		    }
		}
	    }
	}
	let productsBuilt = { successful: successful,
			      failed: failed,
			      unchanged: unchanged,
			      trees: productTrees };
	let productsBuiltPath = this.builddir.get_child('products-built.json');
	JsonUtil.writeJsonFileAtomic(productsBuiltPath, productsBuilt, cancellable);
	print("Successful: " + successful.join(' '));
	print("Failed: " + failed.join(' '));
	print("Unchanged: " + unchanged.join(' '));

        let modifiedPath = Gio.File.new_for_path('modified.json');
        JsonUtil.writeJsonFileAtomic(modifiedPath, { modified: successful.length > 0 }, cancellable);
    }
});
<commit_msg>build: Don't accumulate postprocessing steps<commit_after>// Copyright (C) 2011,2013,2014 Colin Walters <walters@verbum.org>
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the
// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
// Boston, MA 02111-1307, USA.

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Format = imports.format;

const GSystem = imports.gi.GSystem;
const OSTree = imports.gi.OSTree;

const Task = imports.task;
const Params = imports.params;
const FileUtil = imports.fileutil;
const ProcUtil = imports.procutil;
const JSUtil = imports.jsutil;
const StreamUtil = imports.streamutil;
const JsonUtil = imports.jsonutil;
const Snapshot = imports.snapshot;
const BuildUtil = imports.buildutil;
const Vcs = imports.vcs;

const TaskBuild = new Lang.Class({
    Name: "TaskBuild",
    Extends: Task.Task,

    TaskDef: {
        TaskName: "build",
    },

    DefaultParameters: {onlyTreesMatching: null},

    BuildState: { 'failed': 'failed',
		  'successful': 'successful',
		  'unchanged': 'unchanged' },

    _composeProduct: function(ref, productName, treeName, treeData, release, architecture, cancellable) {
	let repos = ['fedora-' + release];
	if (release != 'rawhide')
	    repos.push('fedora-' + release + '-updates');

	let addRepos = this._productData['add-repos'];
	if (addRepos)
	    repos.push.apply(repos, addRepos);

	let packages = treeData['packages'];
	let bootstrapBase = this._productData['bootstrap_base'];
	let baseRequired = this._productData['base_required_packages'];
	packages.push.apply(packages, baseRequired);

	let postprocessSteps = [];
	let masterPostprocessSteps = this._productData['postprocess'];
	if (masterPostprocessSteps) {
	    postprocessSteps.push.apply(postprocessSteps, masterPostprocessSteps);
	    print("Using master postprocessing " + JSON.stringify(masterPostprocessSteps));
	}

	let treePostprocess = treeData['postprocess'];
	if (treePostprocess) {
	    print("Appending postprocessing " + JSON.stringify(treePostprocess));
	    postprocessSteps.push.apply(postprocessSteps, treePostprocess);
	}

	let [,origRevision] = this.ostreeRepo.resolve_rev(ref, true);
	if (origRevision == null)
	    print("Starting new build of " + ref);
	else
	    print("Starting build of " + ref + " previous: " + origRevision);

	let argv = ['rpm-ostree',
		    '--workdir=' + this.workdir.get_path()];
	argv.push.apply(argv, bootstrapBase.map(function (a) { return '--bootstrap-package=' + a; }));
	argv.push.apply(argv, repos.map(function (a) { return '--enablerepo=' + a; }));
	argv.push.apply(argv, postprocessSteps.map(function (a) { return '--post=' + a; }));
	argv.push.apply(argv, ['create', ref]);
	argv.push.apply(argv, packages);
	let productNameUnix = ref.replace(/\//g, '_');
	let buildOutputPath = Gio.File.new_for_path('log-' + productNameUnix + '.txt');

	let procContext = new GSystem.SubprocessContext({ argv: argv });
	GSystem.shutil_rm_rf(buildOutputPath, cancellable);
	procContext.set_stdout_file_path(buildOutputPath.get_path());
	procContext.set_stderr_disposition(GSystem.SubprocessStreamDisposition.STDERR_MERGE);
	let proc = new GSystem.Subprocess({ context: procContext });
	proc.init(cancellable);
	try {
	    proc.wait_sync_check(cancellable);
	} catch (e) {
	    print("Build of " + productName + " failed");
	    return [this.BuildState.failed, origRevision];
	}
	let [,newRevision] = this.ostreeRepo.resolve_rev(ref, true);
	if (origRevision == newRevision)
	    return [this.BuildState.unchanged, newRevision];
	return [this.BuildState.successful, newRevision];
    },

    execute: function(cancellable) {
	let productPath = this.workdir.get_child('products.json');
	let productData = JsonUtil.loadJson(productPath, cancellable);
	this._productData = productData;

	let releases = productData['releases'];
	let architectures = productData['architectures'];
	let products = productData['products'];
	let successful = [];
	let failed = [];
	let unchanged = [];
	let productTrees = {};
	for (let i = 0; i < releases.length; i++) {
	    for (let j = 0; j < architectures.length; j++) {
		for (let productName in products) {
		    for (let treeName in products[productName]) {
			let release = releases[i];
			let architecture = architectures[j];
			let ref = [this._productData['osname'], release, architecture, productName, treeName].join('/');
			if (this.parameters.onlyTreesMatching &&
			    ref.indexOf(this.parameters.onlyTreesMatching) == -1) {
			    log("Skipping " + ref + " which does not match " + this.parameters.onlyTreesMatching);
			    continue;
			}
			let [result, revision] =
			    this._composeProduct(ref, productName, treeName, products[productName][treeName],
						 release, architecture,
						 cancellable);
			productTrees[ref] = revision;
			switch (result) {
			    case this.BuildState.successful: {
				successful.push(ref);
			    }
			    break;
			    case this.BuildState.failed: {
				failed.push(ref);
			    }
			    break;
			    case this.BuildState.unchanged: {
				unchanged.push(ref);
			    }
			    break;
			    default:
			    throw new Error("Invalid result from composeProduct: " + result);
			}
		    }
		}
	    }
	}
	let productsBuilt = { successful: successful,
			      failed: failed,
			      unchanged: unchanged,
			      trees: productTrees };
	let productsBuiltPath = this.builddir.get_child('products-built.json');
	JsonUtil.writeJsonFileAtomic(productsBuiltPath, productsBuilt, cancellable);
	print("Successful: " + successful.join(' '));
	print("Failed: " + failed.join(' '));
	print("Unchanged: " + unchanged.join(' '));

        let modifiedPath = Gio.File.new_for_path('modified.json');
        JsonUtil.writeJsonFileAtomic(modifiedPath, { modified: successful.length > 0 }, cancellable);
    }
});
<|endoftext|>


<commit_before>/* @flow */
import { describe, it } from 'mocha'
import _ from 'lodash'
import assert from 'assert'
import co from 'co'
import promisify from 'es6-promisify'

describe('lib/margaux', () => {
  const utils = require('../lib/utils')
  const margaux = require('../lib/margaux')

  const createTmpServer = promisify(utils.createTmpServer)
  const create = promisify(margaux.create)
  const navigate = promisify(margaux.navigate)
  const deleteCookie = promisify(margaux.deleteCookie)
  const setCookie = promisify(margaux.setCookie)
  const getCookies = promisify(margaux.getCookies)
  const getOuterHTML = promisify(margaux.getOuterHTML)

  it('evaluate', function (done) {
    margaux.create('localhost', 9223, (err, chrome) => {
      assert(err === null)
      chrome.ws.on('close', (result) => {
        // 本来は1000の正常終了だったが、いつのバージョンからかChromeがwindow.closeだと異常終了を返すようになった。
        // closeのテストではなく、evaluateのテストなので異常終了を正とする。
        // 正常系では影響がないが、延々終わらない異常系でこの方式を使ってchromeを閉じているので問題がないわけではない。
        assert.ok(result === 1006, '異常終了')
        done()
      })

      const expression = 'setTimeout(window.close,  5)'
      margaux.evaluate(chrome, expression, (err, result) => {
        assert.ok(err === null)
      })
    })
  })

  it('(get|set|delete)Cookies', function (done) {
    const script = `
      if (document.cookie.indexOf('test=1') > -1) {
        var span = document.createElement('span');
        document.body.appendChild(span);
      }
    `

    const html = require('util').format(
      '',
      script
    )

    co(function * () {
      // テストサーバーへ画面遷移
      const server = yield createTmpServer(html, {})
      const chrome = yield create('localhost', 9223)
      yield navigate(chrome, 'http://localhost:' + server.address().port)

      let outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML === html, 'まだ cookie が存在していない')

      // set and get
      yield setCookie(chrome, {cookieName: 'test', value: '12345'})
      const result = yield getCookies(chrome, {})

      // 取得した連想配列にセットした cookie が存在するかを確認
      assert.ok(_.filter(result.cookies, {
        domain: 'localhost', name: 'test', value: '12345'
      }).length === 1)

      yield navigate(chrome, 'http://localhost:' + server.address().port)
      outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML !== html, '要素が追加された')

      // cookie を綺麗にしておく
      yield deleteCookie(chrome, { cookieName: 'test', url: 'http://localhost/' })

      yield navigate(chrome, 'http://localhost:' + server.address().port)
      outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML === html, 'cookie が削除されて')
    }).then(done).catch((e) => { throw e })
  })
})
<commit_msg>chore(test_lib_margaux) add tests : create, setCookie, and deleteCookie<commit_after>/* @flow */
import { describe, it } from 'mocha'
import _ from 'lodash'
import assert from 'assert'
import co from 'co'
import promisify from 'es6-promisify'
import errors from 'common-errors'

describe('lib/margaux', () => {
  const utils = require('../lib/utils')
  const margaux = require('../lib/margaux')

  const createTmpServer = promisify(utils.createTmpServer)
  const create = promisify(margaux.create)
  const navigate = promisify(margaux.navigate)
  const deleteCookie = promisify(margaux.deleteCookie)
  const setCookie = promisify(margaux.setCookie)
  const getCookies = promisify(margaux.getCookies)
  const getOuterHTML = promisify(margaux.getOuterHTML)

  it('evaluate', function (done) {
    margaux.create('localhost', 9223, (err, chrome) => {
      assert(err === null)
      chrome.ws.on('close', (result) => {
        // 本来は1000の正常終了だったが、いつのバージョンからかChromeがwindow.closeだと異常終了を返すようになった。
        // closeのテストではなく、evaluateのテストなので異常終了を正とする。
        // 正常系では影響がないが、延々終わらない異常系でこの方式を使ってchromeを閉じているので問題がないわけではない。
        assert.ok(result === 1006, '異常終了')
        done()
      })

      const expression = 'setTimeout(window.close,  5)'
      margaux.evaluate(chrome, expression, (err, result) => {
        assert.ok(err === null)
      })
    })
  })

  it('should not create without host and port', function (done) {
    margaux.create('', 0, (err) => {
      if (!err) {
        assert(false)
      }
      done()
    })
  })

  it('should not set cookie without cookie name', function (done) {
    const chrome = createTmpServer('', {})
    const cookieOpts = {
      cookieName: '',
      value: 12345
    }
    margaux.setCookie(chrome, cookieOpts, (err) => {
      assert(err instanceof errors.ArgumentNullError)
      assert(err.message === 'Missing argument: cookieName')
    })
    done()
  })

  it('should not set cookie without value name', function (done) {
    const chrome = createTmpServer('', {})
    const cookieOpts = {
      cookieName: 'test',
      value: ''
    }
    margaux.setCookie(chrome, cookieOpts, (err) => {
      assert(err instanceof errors.ArgumentNullError)
      assert(err.message === 'Missing argument: value')
    })
    done()
  })

  it('should not delete cookie without cookieName', function (done) {
    const chrome = createTmpServer('', {})
    const cookieOpts = {
      cookieName: '',
      url: 'http://localhost/'
    }
    margaux.deleteCookie(chrome, cookieOpts, (err) => {
      assert(err instanceof errors.ArgumentNullError)
      assert(err.message === 'Missing argument: cookieName')
    })
    done()
  })

  it('should not delete cookie without url', function (done) {
    const chrome = createTmpServer('', {})
    const cookieOpts = {
      cookieName: 'test',
      url: ''
    }
    margaux.deleteCookie(chrome, cookieOpts, (err) => {
      assert(err instanceof errors.ArgumentNullError)
      assert(err.message === 'Missing argument: url')
    })
    done()
  })

  it('(get|set|delete)Cookies', function (done) {
    const script = `
      if (document.cookie.indexOf('test=1') > -1) {
        var span = document.createElement('span');
        document.body.appendChild(span);
      }
    `

    const html = require('util').format(
      '',
      script
    )

    co(function * () {
      // テストサーバーへ画面遷移
      const server = yield createTmpServer(html, {})
      const chrome = yield create('localhost', 9223)
      yield navigate(chrome, 'http://localhost:' + server.address().port)

      let outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML === html, 'まだ cookie が存在していない')

      // set and get
      yield setCookie(chrome, {cookieName: 'test', value: '12345'})
      const result = yield getCookies(chrome, {})

      // 取得した連想配列にセットした cookie が存在するかを確認
      assert.ok(_.filter(result.cookies, {
        domain: 'localhost', name: 'test', value: '12345'
      }).length === 1)

      yield navigate(chrome, 'http://localhost:' + server.address().port)
      outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML !== html, '要素が追加された')

      // cookie を綺麗にしておく
      yield deleteCookie(chrome, { cookieName: 'test', url: 'http://localhost/' })

      yield navigate(chrome, 'http://localhost:' + server.address().port)
      outerHTML = yield getOuterHTML(chrome)
      assert.ok(outerHTML === html, 'cookie が削除されて')
    }).then(done).catch((e) => { throw e })
  })
})
<|endoftext|>