<commit_before>import { Action, registerAction } from '../utils';

const VERSION = 53; // Increase when changed.
const LAST_SHOWN_DELAY = 1000 * 60 * 60 * 24 * 7; // 7 days

export class HeartbeatFlow {
  constructor(action) {
    this.action = action;
    const { normandy, recipe, client, location } = action;

    const flashPlugin = client.plugins['Shockwave Flash'];
    const plugins = {};
    for (const pluginName in client.plugins) {
      if (!client.plugins.hasOwnProperty(pluginName)) {
        continue;
      }
      const plugin = client.plugins[pluginName];
      plugins[plugin.name] = plugin.version;
    }

    this.data = {
      // Required fields
      response_version: 2,
      experiment_version: '-',
      person_id: 'NA',
      survey_id: recipe.arguments.surveyId,
      flow_id: normandy.uuid(),
      question_id: recipe.arguments.message.slice(0, 50),
      updated_ts: Date.now(),
      question_text: recipe.arguments.message,
      variation_id: recipe.revision_id.toString(),

      // Optional fields
      score: null,
      max_score: 5,
      flow_began_ts: Date.now(),
      flow_offered_ts: 0,
      flow_voted_ts: 0,
      flow_engaged_ts: 0,
      platform: 'UNK',
      channel: client.channel.slice(0, 50),
      version: client.version.slice(0, 50),
      locale: normandy.locale.slice(0, 50),
      country: (location.countryCode || 'unk').toLowerCase().slice(0, 4),
      build_id: '-',
      partner_id: '-',
      profile_age: 0,
      profile_usage: {},
      addons: {
        addons: [],
      },
      extra: {
        crashes: {},
        engage: [],
        numflows: 0,
        searchEngine: client.searchEngine,
        syncSetup: client.syncSetup,
        defaultBrowser: client.isDefaultBrowser,
        plugins,
        flashVersion: flashPlugin ? flashPlugin.version : undefined,
      },
      is_test: normandy.testing,
    };
  }

  get id() {
    return this.data.flow_id;
  }

  save() {
    this.data.updated_ts = Date.now();

    const { normandy } = this.action;
    normandy.saveHeartbeatFlow(this.data);
  }

  addLink(href, source) {
    this.data.extra.engage.push([Date.now(), href, source]);
  }

  setPhaseTimestamp(phase, timestamp) {
    const key = `flow_${phase}_ts`;
    if (key in this.data && this.data[key] === 0) {
      this.data[key] = timestamp;
    }
  }

  setScore(score) {
    this.data.score = score;
  }
}

export default class ShowHeartbeatAction extends Action {
  constructor(normandy, recipe) {
    super(normandy, recipe);
    this.storage = normandy.createStorage(recipe.id);
  }

  async execute() {
    const {
      surveyId,
      message,
      engagementButtonLabel,
      thanksMessage,
      postAnswerUrl,
      learnMoreMessage,
      learnMoreUrl,
    } = this.recipe.arguments;

    const lastShown = await this.getLastShownDate();
    const shouldShowSurvey = (
      this.normandy.testing
      || lastShown === null
      || Date.now() - lastShown > LAST_SHOWN_DELAY
    );
    if (!shouldShowSurvey) {
      return;
    }

    this.location = await this.normandy.location();
    this.client = await this.normandy.client();

    const flow = new HeartbeatFlow(this);
    flow.save();

    // A bit redundant but the action argument names shouldn't necessarily rely
    // on the argument names showHeartbeat takes.
    const heartbeatData = {
      surveyId: `${surveyId}::${flow.id}`,
      message,
      engagementButtonLabel,
      thanksMessage,
      postAnswerUrl: this.annotatePostAnswerUrl({ url: postAnswerUrl, userId: flow.id }),
      learnMoreMessage,
      learnMoreUrl,
      flowId: flow.id,
      surveyVersion: this.recipe.revision_id,
    };

    if (this.normandy.testing) {
      heartbeatData.testing = 1;
    }

    const heartbeat = await this.normandy.showHeartbeat(heartbeatData);

    heartbeat.on('NotificationOffered', data => {
      flow.setPhaseTimestamp('offered', data.timestamp);
      flow.save();
    });

    heartbeat.on('LearnMore', () => {
      flow.addLink(this.recipe.arguments.learnMoreUrl, 'notice');
      flow.save();
    });

    heartbeat.on('Voted', data => {
      flow.setScore(data.score);
      flow.setPhaseTimestamp('voted', data.timestamp);
      flow.save();
    });

    heartbeat.on('Engaged', data => {
      flow.setPhaseTimestamp('engaged', data.timestamp);
      flow.save();
    });

    this.setLastShownDate();
  }

  setLastShownDate() {
    // Returns a promise, but there's nothing to do if it fails.
    this.storage.setItem('lastShown', Date.now());
  }

  async getLastShownDate() {
    const lastShown = Number.parseInt(await this.storage.getItem('lastShown'), 10);
    return Number.isNaN(lastShown) ? null : lastShown;
  }

  annotatePostAnswerUrl({ url, userId }) {
    // Don't bother with empty URLs.
    if (!url) {
      return url;
    }

    const args = {
      source: 'heartbeat',
      surveyversion: VERSION,
      updateChannel: this.client.channel,
      fxVersion: this.client.version,
      isDefaultBrowser: this.client.isDefaultBrowser ? 1 : 0,
      searchEngine: this.client.searchEngine,
      syncSetup: this.client.syncSetup ? 1 : 0,
      userId,
    };

    // Append testing parameter if in testing mode.
    if (this.normandy.testing) {
      args.testing = 1;
    }

    const annotatedUrl = new URL(url);
    for (const key in args) {
      if (!args.hasOwnProperty(key)) {
        continue;
      }
      annotatedUrl.searchParams.set(key, args[key]);
    }

    return annotatedUrl.href;
  }
}

registerAction('show-heartbeat', ShowHeartbeatAction);
<commit_msg>Add telemetry handling to heartbeat action<commit_after>import { Action, registerAction } from '../utils';

const VERSION = 54; // Increase when changed.
const LAST_SHOWN_DELAY = 1000 * 60 * 60 * 24 * 7; // 7 days

export class HeartbeatFlow {
  constructor(action) {
    this.action = action;
    const { normandy, recipe, client, location } = action;

    const flashPlugin = client.plugins['Shockwave Flash'];
    const plugins = {};
    for (const pluginName in client.plugins) {
      if (!client.plugins.hasOwnProperty(pluginName)) {
        continue;
      }
      const plugin = client.plugins[pluginName];
      plugins[plugin.name] = plugin.version;
    }

    this.data = {
      // Required fields
      response_version: 2,
      experiment_version: '-',
      person_id: 'NA',
      survey_id: recipe.arguments.surveyId,
      flow_id: normandy.uuid(),
      question_id: recipe.arguments.message.slice(0, 50),
      updated_ts: Date.now(),
      question_text: recipe.arguments.message,
      variation_id: recipe.revision_id.toString(),

      // Optional fields
      score: null,
      max_score: 5,
      flow_began_ts: Date.now(),
      flow_offered_ts: 0,
      flow_voted_ts: 0,
      flow_engaged_ts: 0,
      platform: 'UNK',
      channel: client.channel.slice(0, 50),
      version: client.version.slice(0, 50),
      locale: normandy.locale.slice(0, 50),
      country: (location.countryCode || 'unk').toLowerCase().slice(0, 4),
      build_id: '-',
      partner_id: '-',
      profile_age: 0,
      profile_usage: {},
      addons: {
        addons: [],
      },
      extra: {
        crashes: {},
        engage: [],
        numflows: 0,
        searchEngine: client.searchEngine,
        syncSetup: client.syncSetup,
        defaultBrowser: client.isDefaultBrowser,
        plugins,
        flashVersion: flashPlugin ? flashPlugin.version : undefined,
      },
      is_test: normandy.testing,
    };
  }

  get id() {
    return this.data.flow_id;
  }

  save() {
    this.data.updated_ts = Date.now();

    const { normandy } = this.action;
    normandy.saveHeartbeatFlow(this.data);
  }

  addLink(href, source) {
    this.data.extra.engage.push([Date.now(), href, source]);
  }

  setPhaseTimestamp(phase, timestamp) {
    const key = `flow_${phase}_ts`;
    if (key in this.data && this.data[key] === 0) {
      this.data[key] = timestamp;
    }
  }

  setScore(score) {
    this.data.score = score;
  }
}

export default class ShowHeartbeatAction extends Action {
  constructor(normandy, recipe) {
    super(normandy, recipe);
    this.storage = normandy.createStorage(recipe.id);
  }

  async execute() {
    const {
      surveyId,
      message,
      engagementButtonLabel,
      thanksMessage,
      postAnswerUrl,
      learnMoreMessage,
      learnMoreUrl,
    } = this.recipe.arguments;

    const lastShown = await this.getLastShownDate();
    const shouldShowSurvey = (
      this.normandy.testing
      || lastShown === null
      || Date.now() - lastShown > LAST_SHOWN_DELAY
    );
    if (!shouldShowSurvey) {
      return;
    }

    this.location = await this.normandy.location();
    this.client = await this.normandy.client();

    const flow = new HeartbeatFlow(this);
    flow.save();

    let userId;
    let heartbeatSurveyId = surveyId;

    // should user ID stuff be sent to telemetry?
    const { includeTelemetryUUID } = this.recipe.arguments;

    if (includeTelemetryUUID) {
      // get the already-defined UUID from normandy
      // #todo normandy.userId is undefined
      userId = this.normandy.userId;

      // if a userId exists,
      if (userId) {
        // alter the survey ID to include that UUID
        heartbeatSurveyId = `${surveyId}::${userId}`;
      }
    }

    // A bit redundant but the action argument names shouldn't necessarily rely
    // on the argument names showHeartbeat takes.
    const heartbeatData = {
      surveyId: heartbeatSurveyId,
      message,
      engagementButtonLabel,
      thanksMessage,
      postAnswerUrl: this.annotatePostAnswerUrl({ url: postAnswerUrl, userId }),
      learnMoreMessage,
      learnMoreUrl,
      flowId: flow.id,
      surveyVersion: this.recipe.revision_id,
    };

    if (this.normandy.testing) {
      heartbeatData.testing = 1;
    }

    const heartbeat = await this.normandy.showHeartbeat(heartbeatData);

    heartbeat.on('NotificationOffered', data => {
      flow.setPhaseTimestamp('offered', data.timestamp);
      flow.save();
    });

    heartbeat.on('LearnMore', () => {
      flow.addLink(this.recipe.arguments.learnMoreUrl, 'notice');
      flow.save();
    });

    heartbeat.on('Voted', data => {
      flow.setScore(data.score);
      flow.setPhaseTimestamp('voted', data.timestamp);
      flow.save();
    });

    heartbeat.on('Engaged', data => {
      flow.setPhaseTimestamp('engaged', data.timestamp);
      flow.save();
    });

    this.setLastShownDate();
  }

  setLastShownDate() {
    // Returns a promise, but there's nothing to do if it fails.
    this.storage.setItem('lastShown', Date.now());
  }

  async getLastShownDate() {
    const lastShown = Number.parseInt(await this.storage.getItem('lastShown'), 10);
    return Number.isNaN(lastShown) ? null : lastShown;
  }

  annotatePostAnswerUrl({ url, userId }) {
    // Don't bother with empty URLs.
    if (!url) {
      return url;
    }

    const args = {
      source: 'heartbeat',
      surveyversion: VERSION,
      updateChannel: this.client.channel,
      fxVersion: this.client.version,
      isDefaultBrowser: this.client.isDefaultBrowser ? 1 : 0,
      searchEngine: this.client.searchEngine,
      syncSetup: this.client.syncSetup ? 1 : 0,
    };

    // if a userId is given,
    // we'll include it with the data passed through
    // to SurveyGizmo (via query params)
    if (userId) {
      args.userId = userId;
    }

    // Append testing parameter if in testing mode.
    if (this.normandy.testing) {
      args.testing = 1;
    }

    const annotatedUrl = new URL(url);
    for (const key in args) {
      if (!args.hasOwnProperty(key)) {
        continue;
      }
      annotatedUrl.searchParams.set(key, args[key]);
    }

    return annotatedUrl.href;
  }
}

registerAction('show-heartbeat', ShowHeartbeatAction);
<|endoftext|>


<commit_before>// Logic of the usergroup editor interface. It's used by "new" and "edit" pages.
//

'use strict';

var _  = require('lodash');
var ko = require('knockout');

// Single setting of a usergroup.
//
// form (Form): Root form object. See below for Form class.
// name (String): Unique identofier of the setting.
// schema (Object): N.config.setting_schemas.usergroup[name]
// config (Object): Must contain: `group`, `overriden`, `forced`, and `value`.
//
function Setting(form, name, schema, config) {
  var tName = 'admin.setting.' + name
    , tHelp = 'admin.setting.' + name + '_help';

  // Read-only slots.
  //
  this.elementId     = 'setting_' + name; // HTML id attribute.
  this.name          = name;
  this.defaultValue  = schema['default'];
  this.valueType     = schema.type;
  this.categoryKey   = schema.category_key;
  this.priority      = schema.priority;
  this.ownerGroup    = config.group;
  this.localizedName = N.runtime.t(tName);
  this.localizedHelp = N.runtime.t.exists(tHelp) ? N.runtime.t(tHelp) : null;

  // Setting model of `ownerGroup` from which the current setting should inherit
  // the value when it isn't overriden.
  this.parentSetting = ko.computed(function () {
    if (this.ownerGroup && this.ownerGroup.parentGroup()) {
      return this.ownerGroup.parentGroup().settingsByName[this.name];
    } else {
      return null;
    }
  }, this);

  // Private writable slots.
  //
  this._overriden = ko.observable(Boolean(config.overriden));
  this._forced    = ko.observable(Boolean(config.forced));
  this._value     = ko.observable('number' === schema.type ? Number(config.value) : config.value);

  // Public proxy slots. These allows dynamically recompute _shown_ data on
  // parent group change or overriden flag unmark AND keep user's own input
  // at the same time.
  //
  this.overriden = ko.computed({
    read:  function () { return this._overriden() || !this.ownerGroup.parentGroup(); }
  , write: this._overriden
  , owner: this
  }).extend({ dirty: false });

  this.inherited = ko.computed(function () { return !this.overriden(); }, this);

  this.forced = ko.computed({
    read:  function () { return this.overriden() && this._forced(); }
  , write: this._forced
  , owner: this
  }).extend({ dirty: false });

  this.value = ko.computed({
    read: function () {
      if (this.overriden()) {
        return 'number' === this.valueType ? Number(this._value()) : this._value();

      } else if (this.parentSetting()) {
        return this.parentSetting().value();

      } else {
        return this.defaultValue;
      }
    }
  , write: function (newValue) {
      this.overriden(true);
      this._value(newValue);
    }
  , owner: this
  }).extend({ dirty: false });

  // Helpers.
  //
  this.visible = ko.computed(function () {
    var filter = form.filter();

    if ('overriden' === filter) {
      return this.overriden();

    } else if ('inherited' === filter) {
      return this.inherited();

    } else if ('forced' === filter) {
      return this.forced();

    } else {
      return true;
    }
  }, this);

  this.isDirty = ko.computed(function () {
    if (this.overriden.isDirty()) {
      return true;
    }

    if (this.overriden()) {
      if (this.value.isDirty()) {
        return true;
      }
      if (this.forced.isDirty()) {
        return true;
      }
    }

    return false;
  }, this);
}

// Marks all trackable data slots as saved.
//
Setting.prototype.markClean = function markClean() {
  this.overriden.markClean();
  this.forced.markClean();
  this.value.markClean();
};

// Returns data suitable for the server.
//
Setting.prototype.getOutputData = function getOutputData() {
  return {
    value: this.value()
  , force: this.forced()
  };
};

// Named group of settings. (visible interface element)
//
// form (Form): See below for Form class.
// name (String): Title translation key.
// settings (Array): List of related settings.
//
function SettingCategory(form, name, settings) {
  this.name          = name;
  this.localizedName = N.runtime.t('admin.setting.category.' + name);

  this.settings = _.sortBy(settings, 'priority');
  this.priority = _(settings).pluck('priority').reduce(function (a, b) { return a + b; });
}

// Single known user group. Used either for displayed group and for groups in
// the "inherits" list.
//
function UserGroup(form, data) {
  data = data || {};

  var rawSettings = data.raw_settings || {}

  // Read-only slots.
  //
  this.objectId    = data._id || null;
  this.isProtected = data.is_protected || false;

  // Writable and savable slots.
  //
  this.name     = ko.observable(data.short_name || '').extend({ dirty: false });
  this.parentId = ko.observable(data.parent_group || null).extend({ dirty: false });

  // Computed values.
  //
  this.localizedName = ko.computed(function () {
    return this.name() ? N.runtime.t('users.usergroup.' + this.name()) : null;
  }, this);

  this.parentGroup = ko.computed({
    read:  function () { return form.groupsById[this.parentId()]; }
  , write: function (group) { this.parentId(group ? group.objectId : null); }
  , owner: this
  });

  // Related settings and categories lists.
  // See Setting and SettingCategory classes for details.
  //
  this.settings         = [];
  this.settingsByName   = {};
  this.categories       = [];
  this.categoriesByName = {};

  // Collect setting models.
  _.forEach(form.setting_schemas, function (schema, name) {
    var setting, overriden = _.has(rawSettings, name);

    setting = new Setting(form, name, schema, {
      group:     this
    , overriden: overriden
    , forced:    overriden ? rawSettings[name].force : false
    , value:     overriden ? rawSettings[name].value : schema['default']
    });

    this.settings.push(setting);
    this.settingsByName[name] = setting;
  }, this);

  this.settings = _.sortBy(this.settings, 'priority');

  // Collect category models.
  _(form.setting_schemas).pluck('category_key').unique().forEach(function (name) {
    var category = new SettingCategory(form, name, _.select(this.settings, { categoryKey: name }));

    this.categories.push(category);
    this.categoriesByName[name] = category;
  }, this);

  this.categories = _.sortBy(this.categories, 'priority');

  // Helpers.
  //
  this.isDirty = ko.computed(function () {
    return this.name.isDirty()     ||
           this.parentId.isDirty() ||
           _(this.settings).invoke('isDirty').any();
  }, this);
}

// Marks all trackable data slots and related settings as saved.
//
UserGroup.prototype.markClean = function markClean() {
  this.name.markClean();
  this.parentId.markClean();
  _.invoke(this.settings, 'markClean');
};

// Returns data suitable for the server.
//
UserGroup.prototype.getOutputData = function getOutputData() {
  var result = {
    short_name:   this.name()
  , parent_group: this.parentId()
  , raw_settings: {}
  };

  // Add `_id` property only when `this` is a persistent (i.e. saved) group.
  if (this.objectId) {
    result._id = this.objectId;
  }

  _.forEach(this.settings, function (setting) {
    if (setting.overriden()) {
      result.raw_settings[setting.name] = setting.getOutputData();
    }
  });

  return result;
};

// Root view model of the page. It has two modes: 'create' and 'update'.
//
// page_data.setting_schemas (Object): N.config.setting_schemas.usergroup
// page_data.groups_data (Array): List of all existent user groups.
// page_data.current_group_id (String, Optional): ObjectID of a group for edit.
// If this identifier is provided, the form will use 'update' mode.
//
function Form(page_data) {
  this.setting_schemas = page_data.setting_schemas;

  this.groups     = [];
  this.groupsById = {};

  this.filter = ko.observable('');

  this.createMode   = !_.has(page_data, 'current_group_id');
  this.currentGroup = null;
  this.otherGroups  = [];

  if (this.createMode) {
    this.currentGroup = new UserGroup(this);
  }

  _.forEach(page_data.groups_data, function (data) {
    var group = new UserGroup(this, data);

    this.groups.push(group);
    this.groupsById[group.objectId] = group;

    if (!this.createMode && group.objectId === page_data.current_group_id) {
      this.currentGroup = group;
    } else {
      this.otherGroups.push(group);
    }
  }, this);
}

// Sends 'create' request for `currentGroup`.
//
Form.prototype.create = function create() {
  var self = this;

  N.io.rpc('admin.users.usergroups.create', this.currentGroup.getOutputData(), function (err) {
    if (err) {
      N.wire.emit('notify', {
        type: 'error'
      , message: N.runtime.t(N.io.BAD_REQUEST === err.code ?
                             'admin.users.usergroups.form.error_bad_request' :
                             'admin.users.usergroups.form.error_unknown')
      });
      return;
    }

    self.currentGroup.markClean();

    N.wire.emit('notify', {
      type: 'info'
    , message: N.runtime.t('admin.users.usergroups.form.created')
    });

    // TODO: Replace this with the navigate.js when it will be ported for ACP.
    window.location = N.runtime.router.linkTo('admin.users.usergroups.show');
  });
};

// Sends 'update' request for `currentGroup`.
//
Form.prototype.update = function update() {
  var self = this;

  N.io.rpc('admin.users.usergroups.update', this.currentGroup.getOutputData(), function (err) {
    if (err) {
      N.wire.emit('notify', {
        type: 'error'
      , message: N.runtime.t(N.io.BAD_REQUEST === err.code ?
                             'admin.users.usergroups.form.error_bad_request' :
                             'admin.users.usergroups.form.error_unknown')
      });
      return;
    }

    self.currentGroup.markClean();

    N.wire.emit('notify', {
      type: 'info'
    , message: N.runtime.t('admin.users.usergroups.form.updated')
    });
  });
};

// Sends save request to the server.
//
Form.prototype.submit = function submit() {
  if (this.createMode) {
    this.create();
  } else {
    this.update();
  }
};

module.exports = Form;
<commit_msg>Lint.<commit_after>// Logic of the usergroup editor interface. It's used by "new" and "edit" pages.
//

'use strict';

var _  = require('lodash');
var ko = require('knockout');

// Single setting of a usergroup.
//
// form (Form): Root form object. See below for Form class.
// name (String): Unique identofier of the setting.
// schema (Object): N.config.setting_schemas.usergroup[name]
// config (Object): Must contain: `group`, `overriden`, `forced`, and `value`.
//
function Setting(form, name, schema, config) {
  var tName = 'admin.setting.' + name
    , tHelp = 'admin.setting.' + name + '_help';

  // Read-only slots.
  //
  this.elementId     = 'setting_' + name; // HTML id attribute.
  this.name          = name;
  this.defaultValue  = schema['default'];
  this.valueType     = schema.type;
  this.categoryKey   = schema.category_key;
  this.priority      = schema.priority;
  this.ownerGroup    = config.group;
  this.localizedName = N.runtime.t(tName);
  this.localizedHelp = N.runtime.t.exists(tHelp) ? N.runtime.t(tHelp) : null;

  // Setting model of `ownerGroup` from which the current setting should inherit
  // the value when it isn't overriden.
  this.parentSetting = ko.computed(function () {
    if (this.ownerGroup && this.ownerGroup.parentGroup()) {
      return this.ownerGroup.parentGroup().settingsByName[this.name];
    } else {
      return null;
    }
  }, this);

  // Private writable slots.
  //
  this._overriden = ko.observable(Boolean(config.overriden));
  this._forced    = ko.observable(Boolean(config.forced));
  this._value     = ko.observable('number' === schema.type ? Number(config.value) : config.value);

  // Public proxy slots. These allows dynamically recompute _shown_ data on
  // parent group change or overriden flag unmark AND keep user's own input
  // at the same time.
  //
  this.overriden = ko.computed({
    read:  function () { return this._overriden() || !this.ownerGroup.parentGroup(); }
  , write: this._overriden
  , owner: this
  }).extend({ dirty: false });

  this.inherited = ko.computed(function () { return !this.overriden(); }, this);

  this.forced = ko.computed({
    read:  function () { return this.overriden() && this._forced(); }
  , write: this._forced
  , owner: this
  }).extend({ dirty: false });

  this.value = ko.computed({
    read: function () {
      if (this.overriden()) {
        return 'number' === this.valueType ? Number(this._value()) : this._value();

      } else if (this.parentSetting()) {
        return this.parentSetting().value();

      } else {
        return this.defaultValue;
      }
    }
  , write: function (newValue) {
      this.overriden(true);
      this._value(newValue);
    }
  , owner: this
  }).extend({ dirty: false });

  // Helpers.
  //
  this.visible = ko.computed(function () {
    var filter = form.filter();

    if ('overriden' === filter) {
      return this.overriden();

    } else if ('inherited' === filter) {
      return this.inherited();

    } else if ('forced' === filter) {
      return this.forced();

    } else {
      return true;
    }
  }, this);

  this.isDirty = ko.computed(function () {
    if (this.overriden.isDirty()) {
      return true;
    }

    if (this.overriden()) {
      if (this.value.isDirty()) {
        return true;
      }
      if (this.forced.isDirty()) {
        return true;
      }
    }

    return false;
  }, this);
}

// Marks all trackable data slots as saved.
//
Setting.prototype.markClean = function markClean() {
  this.overriden.markClean();
  this.forced.markClean();
  this.value.markClean();
};

// Returns data suitable for the server.
//
Setting.prototype.getOutputData = function getOutputData() {
  return {
    value: this.value()
  , force: this.forced()
  };
};

// Named group of settings. (visible interface element)
//
// form (Form): See below for Form class.
// name (String): Title translation key.
// settings (Array): List of related settings.
//
function SettingCategory(form, name, settings) {
  this.name          = name;
  this.localizedName = N.runtime.t('admin.setting.category.' + name);

  this.settings = _.sortBy(settings, 'priority');
  this.priority = _(settings).pluck('priority').reduce(function (a, b) { return a + b; });
}

// Single known user group. Used either for displayed group and for groups in
// the "inherits" list.
//
function UserGroup(form, data) {
  data = data || {};

  var rawSettings = data.raw_settings || {};

  // Read-only slots.
  //
  this.objectId    = data._id || null;
  this.isProtected = data.is_protected || false;

  // Writable and savable slots.
  //
  this.name     = ko.observable(data.short_name || '').extend({ dirty: false });
  this.parentId = ko.observable(data.parent_group || null).extend({ dirty: false });

  // Computed values.
  //
  this.localizedName = ko.computed(function () {
    return this.name() ? N.runtime.t('users.usergroup.' + this.name()) : null;
  }, this);

  this.parentGroup = ko.computed({
    read:  function () { return form.groupsById[this.parentId()]; }
  , write: function (group) { this.parentId(group ? group.objectId : null); }
  , owner: this
  });

  // Related settings and categories lists.
  // See Setting and SettingCategory classes for details.
  //
  this.settings         = [];
  this.settingsByName   = {};
  this.categories       = [];
  this.categoriesByName = {};

  // Collect setting models.
  _.forEach(form.setting_schemas, function (schema, name) {
    var setting, overriden = _.has(rawSettings, name);

    setting = new Setting(form, name, schema, {
      group:     this
    , overriden: overriden
    , forced:    overriden ? rawSettings[name].force : false
    , value:     overriden ? rawSettings[name].value : schema['default']
    });

    this.settings.push(setting);
    this.settingsByName[name] = setting;
  }, this);

  this.settings = _.sortBy(this.settings, 'priority');

  // Collect category models.
  _(form.setting_schemas).pluck('category_key').unique().forEach(function (name) {
    var category = new SettingCategory(form, name, _.select(this.settings, { categoryKey: name }));

    this.categories.push(category);
    this.categoriesByName[name] = category;
  }, this);

  this.categories = _.sortBy(this.categories, 'priority');

  // Helpers.
  //
  this.isDirty = ko.computed(function () {
    return this.name.isDirty()     ||
           this.parentId.isDirty() ||
           _(this.settings).invoke('isDirty').any();
  }, this);
}

// Marks all trackable data slots and related settings as saved.
//
UserGroup.prototype.markClean = function markClean() {
  this.name.markClean();
  this.parentId.markClean();
  _.invoke(this.settings, 'markClean');
};

// Returns data suitable for the server.
//
UserGroup.prototype.getOutputData = function getOutputData() {
  var result = {
    short_name:   this.name()
  , parent_group: this.parentId()
  , raw_settings: {}
  };

  // Add `_id` property only when `this` is a persistent (i.e. saved) group.
  if (this.objectId) {
    result._id = this.objectId;
  }

  _.forEach(this.settings, function (setting) {
    if (setting.overriden()) {
      result.raw_settings[setting.name] = setting.getOutputData();
    }
  });

  return result;
};

// Root view model of the page. It has two modes: 'create' and 'update'.
//
// page_data.setting_schemas (Object): N.config.setting_schemas.usergroup
// page_data.groups_data (Array): List of all existent user groups.
// page_data.current_group_id (String, Optional): ObjectID of a group for edit.
// If this identifier is provided, the form will use 'update' mode.
//
function Form(page_data) {
  this.setting_schemas = page_data.setting_schemas;

  this.groups     = [];
  this.groupsById = {};

  this.filter = ko.observable('');

  this.createMode   = !_.has(page_data, 'current_group_id');
  this.currentGroup = null;
  this.otherGroups  = [];

  if (this.createMode) {
    this.currentGroup = new UserGroup(this);
  }

  _.forEach(page_data.groups_data, function (data) {
    var group = new UserGroup(this, data);

    this.groups.push(group);
    this.groupsById[group.objectId] = group;

    if (!this.createMode && group.objectId === page_data.current_group_id) {
      this.currentGroup = group;
    } else {
      this.otherGroups.push(group);
    }
  }, this);
}

// Sends 'create' request for `currentGroup`.
//
Form.prototype.create = function create() {
  var self = this;

  N.io.rpc('admin.users.usergroups.create', this.currentGroup.getOutputData(), function (err) {
    if (err) {
      N.wire.emit('notify', {
        type: 'error'
      , message: N.runtime.t(N.io.BAD_REQUEST === err.code ?
                             'admin.users.usergroups.form.error_bad_request' :
                             'admin.users.usergroups.form.error_unknown')
      });
      return;
    }

    self.currentGroup.markClean();

    N.wire.emit('notify', {
      type: 'info'
    , message: N.runtime.t('admin.users.usergroups.form.created')
    });

    // TODO: Replace this with the navigate.js when it will be ported for ACP.
    window.location = N.runtime.router.linkTo('admin.users.usergroups.show');
  });
};

// Sends 'update' request for `currentGroup`.
//
Form.prototype.update = function update() {
  var self = this;

  N.io.rpc('admin.users.usergroups.update', this.currentGroup.getOutputData(), function (err) {
    if (err) {
      N.wire.emit('notify', {
        type: 'error'
      , message: N.runtime.t(N.io.BAD_REQUEST === err.code ?
                             'admin.users.usergroups.form.error_bad_request' :
                             'admin.users.usergroups.form.error_unknown')
      });
      return;
    }

    self.currentGroup.markClean();

    N.wire.emit('notify', {
      type: 'info'
    , message: N.runtime.t('admin.users.usergroups.form.updated')
    });
  });
};

// Sends save request to the server.
//
Form.prototype.submit = function submit() {
  if (this.createMode) {
    this.create();
  } else {
    this.update();
  }
};

module.exports = Form;
<|endoftext|>


<commit_before>import { combineReducers } from 'redux';
import Authreducer from './reducer_auth';
import GameReducer from './reducer_game';
import SoloGame from './solo_selection';
// import { reducer as formReducer } from 'redux-form';

const rootReducer = combineReducers({
  game: GameReducer,
  auth: Authreducer,
  soloSelection: SoloGame
});

export default rootReducer;
<commit_msg>(feat) Add MultiSelection into rootReducer<commit_after>import { combineReducers } from 'redux';
import Authreducer from './reducer_auth';
import GameReducer from './reducer_game';
import SoloGame from './solo_selection';
import MultiGame from './multi_selection';
// import { reducer as formReducer } from 'redux-form';

const rootReducer = combineReducers({
  game: GameReducer,
  auth: Authreducer,
  soloSelection: SoloGame,
  multiSelection: MultiGame,
});

export default rootReducer;
<|endoftext|>


<commit_before>var mongoose = require.main.require('mongoose'),
	types = mongoose.Schema.Types,
	forms = require('forms'),
	fields = forms.fields,
	widgets = forms.widgets,
	validators = forms.validators,
	async = require('async');

var findFieldType = exports.findFieldType = function (field) {

	var type;

	// loop through all of the mongoose Schema Types and find a match for the current field
	// we're going in reverse because mongoose Schema Types are inherited
	// and the inherited ones are on the end of the list, and we want the extended types
	Object.keys(types).reverse().some(function (element, index, array) {

		// if this field matches the mongoose Schema Type
		// stop the loop (by returning true) and set the value of type
		if (field instanceof types[element]) {
			type = element;
			return true;
		}

		return false;

	});

	return type;

};

var generateFormFromModel = exports.generateFormFromModel = function (m, r, cb) {

	var formFields = {};

	async.eachSeries(Object.keys(m.schema.paths), function (fieldName, fn) {

		if (fieldName !== '_id' && fieldName !== '__v') {

			var field = m.schema.paths[fieldName];
			var schemaType = findFieldType(field);
			var formField;
			var choices;
			var defaultValue = null;

			// determine if the field has enumValues
			if (Array.isArray(field.enumValues) && field.enumValues.length) {

				// add the options to the choices object to be passed to the select box
				schemaType = 'Enum';
				choices = {};

				field.enumValues.forEach(function (val) {
					choices[val] = val;
				});

			}

			if (field.defaultValue !== undefined) {

				// work out the default value and use it
				if (typeof field.defaultValue !== 'function') {
					defaultValue = field.defaultValue;
				} else if (typeof field.defaultValue === 'function') {
					defaultValue = field.defaultValue();
				}

			}

			async.series([
				function (callback) {

					// if the field has a reference, we need to grab the values for that type and add them in
					if (field.options.ref) {

						mongoose.models[field.options.ref].find({}, function (err, docs) {

							schemaType = 'Enum';
							choices = {};

							docs.forEach(function (doc) {
								choices[doc._id.toString()] = doc.name;
							});

							callback(null);

						});

					} else {

						callback(null);

					}

				},
				function (callback) {

					switch (schemaType) {

						case 'Enum':
						formField = fields.string({
										widget: widgets.select({
											classes: ['form-control']
										}),
										cssClasses: {
											label: ['col-lg-2 control-label']
										},
										// need to use toString so that forms module matching (===) works, was erroring on numbers
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										choices: choices
									});
						break;

						case 'String':
						formField = fields.string({
										widget: widgets.text({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Date':
						formField = fields.date({
										widget: widgets.date({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || defaultValue,
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Text':
						formField = fields.string({
										widget: widgets.textarea({
											classes: ['form-control'],
											rows: '6'
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Number':
						formField = fields.number({
										widget: widgets.text({
											classes: ['form-control']
										}),
										// need to use toString() so that 0 doesn't eval to falsy
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Oid':
						formField = fields.string({
										widget: widgets.text({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

					}

					formFields[field.path] = formField;

					callback(null);

				}
			], function (err, results) {

				fn(null);

			});

		} else {

			fn(null);

		}

	}, function (err) {

		cb(forms.create(formFields));

	});

};

var bootstrapField = exports.bootstrapField = function (name, object) {

	var label = object.labelHTML(name);
	var error = object.error ? '
' + object.error + '
' : '' ;
	var widget = '
' + object.widget.toHTML(name, object) + error + '
';

	return '
' + label + widget + '
';

}<commit_msg>Improved enum support to look for either title, label, name or toString properties to use as the option label.<commit_after>var mongoose = require.main.require('mongoose'),
	types = mongoose.Schema.Types,
	forms = require('forms'),
	fields = forms.fields,
	widgets = forms.widgets,
	validators = forms.validators,
	async = require('async');

var findFieldType = exports.findFieldType = function (field) {

	var type;

	// loop through all of the mongoose Schema Types and find a match for the current field
	// we're going in reverse because mongoose Schema Types are inherited
	// and the inherited ones are on the end of the list, and we want the extended types
	Object.keys(types).reverse().some(function (element, index, array) {

		// if this field matches the mongoose Schema Type
		// stop the loop (by returning true) and set the value of type
		if (field instanceof types[element]) {
			type = element;
			return true;
		}

		return false;

	});

	return type;

};

var generateFormFromModel = exports.generateFormFromModel = function (m, r, cb) {

	var formFields = {};

	async.eachSeries(Object.keys(m.schema.paths), function (fieldName, fn) {

		if (fieldName !== '_id' && fieldName !== '__v') {

			var field = m.schema.paths[fieldName];
			var schemaType = findFieldType(field);
			var formField;
			var choices;
			var defaultValue = null;

			// determine if the field has enumValues
			if (Array.isArray(field.enumValues) && field.enumValues.length) {

				// add the options to the choices object to be passed to the select box
				schemaType = 'Enum';
				choices = {};

				field.enumValues.forEach(function (val) {
					choices[val] = val;
				});

			}

			if (field.defaultValue !== undefined) {

				// work out the default value and use it
				if (typeof field.defaultValue !== 'function') {
					defaultValue = field.defaultValue;
				} else if (typeof field.defaultValue === 'function') {
					defaultValue = field.defaultValue();
				}

			}

			async.series([
				function (callback) {

					// if the field has a reference, we need to grab the values for that type and add them in
					if (field.options.ref) {

						mongoose.models[field.options.ref].find({}, function (err, docs) {

							schemaType = 'Enum';
							choices = {};

							docs.forEach(function (doc) {
								choices[doc._id.toString()] = doc.title || doc.label || doc.name || doc.toString();
							});

							callback(null);

						});

					} else {

						callback(null);

					}

				},
				function (callback) {

					switch (schemaType) {

						case 'Enum':
						formField = fields.string({
										widget: widgets.select({
											classes: ['form-control']
										}),
										cssClasses: {
											label: ['col-lg-2 control-label']
										},
										// need to use toString so that forms module matching (===) works, was erroring on numbers
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										choices: choices
									});
						break;

						case 'String':
						formField = fields.string({
										widget: widgets.text({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Date':
						formField = fields.date({
										widget: widgets.date({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || defaultValue,
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Text':
						formField = fields.string({
										widget: widgets.textarea({
											classes: ['form-control'],
											rows: '6'
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Number':
						formField = fields.number({
										widget: widgets.text({
											classes: ['form-control']
										}),
										// need to use toString() so that 0 doesn't eval to falsy
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

						case 'Oid':
						formField = fields.string({
										widget: widgets.text({
											classes: ['form-control']
										}),
										value: (r[fieldName] != null ? r[fieldName].toString() : null) || (defaultValue != null ? defaultValue.toString() : defaultValue),
										cssClasses: {
											label: ['col-lg-2 control-label']
										}
									});
						break;

					}

					formFields[field.path] = formField;

					callback(null);

				}
			], function (err, results) {

				fn(null);

			});

		} else {

			fn(null);

		}

	}, function (err) {

		cb(forms.create(formFields));

	});

};

var bootstrapField = exports.bootstrapField = function (name, object) {

	var label = object.labelHTML(name);
	var error = object.error ? '
' + object.error + '
' : '' ;
	var widget = '
' + object.widget.toHTML(name, object) + error + '
';

	return '
' + label + widget + '
';

}<|endoftext|>


<commit_before>'use strict';

var fs = require('fs');
var _ = require('lodash');
var Handlebars = require('handlebars');

/**
 * Extract a value from a specific property
 */

exports.value = function(filepath, prop) {
  filepath = JSON.parse(fs.readFileSync(filepath, 'utf8'));
  prop = _.pick(filepath, prop);
  prop = _.pluck(prop);
  return new Handlebars.SafeString(prop);
};

/**
 * Extract a specific property
 */

exports.pick = function(arr, prop) {
  return String(_.pick(arr, prop));
};

/**
 * Stringify an object to JSON
 * @param  {Object|String} `data` Object to stringify (or path of JSON file)
 * @return {String}
 * @api public
 */

exports.stringify = function(data) {
  if (typeof data === 'string') {
    data = JSON.parse(fs.readFileSync(data, 'utf8'));
  }
  return new Handlebars.SafeString(JSON.stringify(data, null, 2));
};

/**
 * Contributed by github.com/keeganstreet
 */

exports.parseJSON = function(data, options) {
  return options.fn(JSON.parse(data));
};

exports.opt = function(key) {
  var opts = {};

  if (this && this.app && this.app.options) {
    opts = this.app.options;
  }

  return opts[key] || '';
};
<commit_msg>clean up date helpers<commit_after>'use strict';

/**
 * `JSON.stringify` exposed as a helper.
 *
 * @param  {Object} `obj` Object to stringify
 * @return {String}
 * @api public
 */

exports.stringify = function(obj) {
  return JSON.stringify(obj, null, 2);
};

/**
 * Contributed by github.com/keeganstreet
 */

exports.parseJSON = function(obj, options) {
  return options.fn(JSON.parse(obj));
};
<|endoftext|>


<commit_before>import {
    fieldset, legend, input,
    header, nav, a, form, button,
    label, select, option,
    div, ul, ol, li, dl, dd, dt,
    main, section, details, summary,
    dfn, sup, sub, i, strong, p, variable,
    htmldom, span
} from './htmldom';

import chai from 'chai';

const { Node, HTMLSpanElement, HTMLAnchorElement } = window;
const { TEXT_NODE, ELEMENT_NODE } = Node;
const { assert } = chai;

describe('HTMLDOM library', () => {
    describe('Authorization fieldset', () => {
        it('properly build form authorization fieldset', () => {
            const element =
                fieldset([
                    legend('Authorization'),
                    input({ type : 'email' }),
                    input({ type : 'password' })
                ]);
            assert.equal(element.outerHTML,
                '<fieldset>' +
                    '<legend>Authorization</legend>' +
                    '<input type="email">' +
                    '<input type="password">' +
                '</fieldset>');
        });
    });
    describe('Header with navigation', () => {
        it('properly build header with navigational links inside', () => {
            const element =
                header(nav([
                    a({ href : '/lib.html', textContent : 'Library' }),
                    a({ href : '/spec.html', textContent : 'Specifications' }),
                    a({ href : '/home.html', textContent : 'Go home' })
                ]));
            assert.equal(element.outerHTML,
                '' +
                    'Library' +
                    'Specifications' +
                    'Go home' +
                '');
        })
    });
    describe('Select box widget', () => {
        let widget, selected;
        const element =
            label([
                'Select technology ',
                widget = select([
                    option('DOM'),
                    option('XML'),
                    selected = option({ selected : true, textContent : 'HTML' }),
                    option('SVG'),
                    option('MathML'),
                    option('WAI-ARIA')
                ])
            ]);
        it('properly build label with select box option list inside', () => {
            assert.equal(element.outerHTML,
                '' +
                    'Select technology ' +
                    '<select>' +
                        '<option>DOM</option>' +
                        '<option>XML</option>' +
                        '<option>HTML</option>' +
                        '<option>SVG</option>' +
                        '<option>MathML</option>' +
                        '<option>WAI-ARIA</option>' +
                    '</select>' +
                '');
        });
        // victim of IE11 todo
        it('proper selected option reference', () => {
            assert.equal(widget.selectedOptions.length, 1);
            assert.equal(widget.selectedOptions[0], selected);
        });
    });
    describe('Search form', () => {
        it('properly build form with search input and submit button inside', () => {
            const element =
                form({
                    attrset : { role : 'search' },
                    children : [
                        input({ type : 'search' }),
                        button('Search')
                    ]
                });
            assert.equal(element.outerHTML,
                '<form role="search">' +
                    '<input type="search">' +
                    '<button>Search</button>' +
                '</form>');
        });
    });
    describe('Checkboxes', () => {
        let simple, checked, indeterminate;
        const element =
            div([
                simple = input({ type : 'checkbox' }),
                checked = input({ type : 'checkbox', checked : true }),
                indeterminate = input({ type : 'checkbox', indeterminate : true })
            ]);
        it('proper HTML rendered', () => {
            assert.equal(element.outerHTML,
                '
' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                '
');
        });
        it('check current state of widgets', () => {
            assert(!simple.checked);
            assert(checked.checked);
            assert(indeterminate.indeterminate);
        });
        it('proper initial state assignment', () => {
            const sample = input({ type : 'checkbox', attrset : { checked : '' } });
            const container = div({ innerHTML : '<input type=checkbox checked>' });
            assert(sample.isEqualNode(container.firstChild));
        });
    });
    describe('Various list examples', () => {
        it('properly build ul + li', () => {
            assert.equal(
                ul([
                    li('Ampeg'),
                    li('Fender'),
                    li('Warwick')
                ]).outerHTML,
                '
' +
                    '
Ampeg
' +
                    '
Fender
' +
                    '
Warwick
' +
                '
'
            );
        });
        it('properly build ol + li', () => {
            assert.equal(
                ol([
                    li('Moscow'),
                    li('Amsterdam'),
                    li('New York')
                ]).outerHTML,
                '
' +
                    '
Moscow
' +
                    '
Amsterdam
' +
                    '
New York
' +
                '
'
            );
        });
        it('properly build dl + dt + dd', () => {
            assert.equal(
                dl([
                    dt('DOM'),
                    dd('Document object model'),
                    dt('XML'),
                    dd('Extensible markup language'),
                    dt('HTML'),
                    dd('Hyper text markup language')
                ]).outerHTML,
                '
' +
                    '
DOM
' +
                    '
Document object model
' +
                    '
XML
' +
                    '
Extensible markup language
' +
                    '
HTML
' +
                    '
Hyper text markup language
' +
                '
'
            );
        });
    });
    describe('Details with summary', () => {
        it('properly build widget', () => {
            const element =
                details([
                    summary('Show details'),
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ',
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
                ]);
            assert.equal(element.outerHTML,
                '' +
                    'Show details' +
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' +
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +
                '');
        });
    });
    describe('Other', () => {
        it('property build DOM fragment', () => {
            const element =
                main([
                    section(dfn('Instance.js — simple and powerfull DOM Element interface')),
                    section(p([
                        variable('var'),
                        ' — is reserved JavaScript keyword, ',
                        'so we use `variable` function name instead.'
                    ])),
                    section([
                        sup('supertext'),
                        sub('subtext'),
                        i('alternative voice'),
                        strong('important!')
                    ])
                ]);
            assert.equal(element.outerHTML,
                '' +
                    '
<dfn>' +
                        'Instance.js — simple and powerfull DOM Element interface' +
                    '</dfn>
' +
                    '

' +
                        '<var>var</var>' +
                        ' — is reserved JavaScript keyword, ' +
                        'so we use `variable` function name instead.' +
                    '

' +
                    '
' +
                        'supertext' +
                        'subtext' +
                        'alternative voice' +
                        'important!' +
                    '
' +
                '')
        });
        it('htmldom, span', () => {
            const node1 = htmldom('span', {
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            const node2 = span({
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            [node1, node2].forEach(node => {
                assert.equal(node.nodeType, ELEMENT_NODE);
                assert.equal(node.tagName, 'SPAN');
                assert.equal(node.constructor, HTMLSpanElement);
                assert.equal(node.attributes.length, 3);
                assert.equal(node.id, '00101');
                assert.equal(node.className, 'fa fi fu');
                assert.equal(node.tabIndex, 0);
                assert.equal(node.children.length, 1);
                assert.equal(node.childNodes.length, 3);
                assert.equal(node.childNodes[0].nodeType, TEXT_NODE);
                assert.equal(node.childNodes[1].constructor, HTMLSpanElement);
                assert.equal(node.childNodes[2].textContent, 'b');
            });
            assert(node1.isEqualNode(node2), 'htmldom() and span() work differently');
        });
        it('a', () => {
            const node = a({
                href : 'html://www.aria.dom/math.svg',
                rel : 'next',
                title : 'om dom dom dom d',
                textContent : 'DOM module'
            });
            assert.equal(node.tagName, 'A');
            assert.equal(node.constructor, HTMLAnchorElement);
            assert.equal(node.href, 'html://www.aria.dom/math.svg');
            assert.equal(node.rel, 'next');
            assert.equal(node.title, 'om dom dom dom d');
            assert.equal(node.textContent, 'DOM module');
        });
    });
});
<commit_msg>redundant comment<commit_after>import {
    fieldset, legend, input,
    header, nav, a, form, button,
    label, select, option,
    div, ul, ol, li, dl, dd, dt,
    main, section, details, summary,
    dfn, sup, sub, i, strong, p, variable,
    htmldom, span
} from './htmldom';

import chai from 'chai';

const { Node, HTMLSpanElement, HTMLAnchorElement } = window;
const { TEXT_NODE, ELEMENT_NODE } = Node;
const { assert } = chai;

describe('HTMLDOM library', () => {
    describe('Authorization fieldset', () => {
        it('properly build form authorization fieldset', () => {
            const element =
                fieldset([
                    legend('Authorization'),
                    input({ type : 'email' }),
                    input({ type : 'password' })
                ]);
            assert.equal(element.outerHTML,
                '<fieldset>' +
                    '<legend>Authorization</legend>' +
                    '<input type="email">' +
                    '<input type="password">' +
                '</fieldset>');
        });
    });
    describe('Header with navigation', () => {
        it('properly build header with navigational links inside', () => {
            const element =
                header(nav([
                    a({ href : '/lib.html', textContent : 'Library' }),
                    a({ href : '/spec.html', textContent : 'Specifications' }),
                    a({ href : '/home.html', textContent : 'Go home' })
                ]));
            assert.equal(element.outerHTML,
                '' +
                    'Library' +
                    'Specifications' +
                    'Go home' +
                '');
        })
    });
    describe('Select box widget', () => {
        let widget, selected;
        const element =
            label([
                'Select technology ',
                widget = select([
                    option('DOM'),
                    option('XML'),
                    selected = option({ selected : true, textContent : 'HTML' }),
                    option('SVG'),
                    option('MathML'),
                    option('WAI-ARIA')
                ])
            ]);
        it('properly build label with select box option list inside', () => {
            assert.equal(element.outerHTML,
                '' +
                    'Select technology ' +
                    '<select>' +
                        '<option>DOM</option>' +
                        '<option>XML</option>' +
                        '<option>HTML</option>' +
                        '<option>SVG</option>' +
                        '<option>MathML</option>' +
                        '<option>WAI-ARIA</option>' +
                    '</select>' +
                '');
        });
        it('proper selected option reference', () => {
            assert.equal(widget.selectedOptions.length, 1);
            assert.equal(widget.selectedOptions[0], selected);
        });
    });
    describe('Search form', () => {
        it('properly build form with search input and submit button inside', () => {
            const element =
                form({
                    attrset : { role : 'search' },
                    children : [
                        input({ type : 'search' }),
                        button('Search')
                    ]
                });
            assert.equal(element.outerHTML,
                '<form role="search">' +
                    '<input type="search">' +
                    '<button>Search</button>' +
                '</form>');
        });
    });
    describe('Checkboxes', () => {
        let simple, checked, indeterminate;
        const element =
            div([
                simple = input({ type : 'checkbox' }),
                checked = input({ type : 'checkbox', checked : true }),
                indeterminate = input({ type : 'checkbox', indeterminate : true })
            ]);
        it('proper HTML rendered', () => {
            assert.equal(element.outerHTML,
                '
' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                '
');
        });
        it('check current state of widgets', () => {
            assert(!simple.checked);
            assert(checked.checked);
            assert(indeterminate.indeterminate);
        });
        it('proper initial state assignment', () => {
            const sample = input({ type : 'checkbox', attrset : { checked : '' } });
            const container = div({ innerHTML : '<input type=checkbox checked>' });
            assert(sample.isEqualNode(container.firstChild));
        });
    });
    describe('Various list examples', () => {
        it('properly build ul + li', () => {
            assert.equal(
                ul([
                    li('Ampeg'),
                    li('Fender'),
                    li('Warwick')
                ]).outerHTML,
                '
' +
                    '
Ampeg
' +
                    '
Fender
' +
                    '
Warwick
' +
                '
'
            );
        });
        it('properly build ol + li', () => {
            assert.equal(
                ol([
                    li('Moscow'),
                    li('Amsterdam'),
                    li('New York')
                ]).outerHTML,
                '
' +
                    '
Moscow
' +
                    '
Amsterdam
' +
                    '
New York
' +
                '
'
            );
        });
        it('properly build dl + dt + dd', () => {
            assert.equal(
                dl([
                    dt('DOM'),
                    dd('Document object model'),
                    dt('XML'),
                    dd('Extensible markup language'),
                    dt('HTML'),
                    dd('Hyper text markup language')
                ]).outerHTML,
                '
' +
                    '
DOM
' +
                    '
Document object model
' +
                    '
XML
' +
                    '
Extensible markup language
' +
                    '
HTML
' +
                    '
Hyper text markup language
' +
                '
'
            );
        });
    });
    describe('Details with summary', () => {
        it('properly build widget', () => {
            const element =
                details([
                    summary('Show details'),
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ',
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
                ]);
            assert.equal(element.outerHTML,
                '' +
                    'Show details' +
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' +
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +
                '');
        });
    });
    describe('Other', () => {
        it('property build DOM fragment', () => {
            const element =
                main([
                    section(dfn('Instance.js — simple and powerfull DOM Element interface')),
                    section(p([
                        variable('var'),
                        ' — is reserved JavaScript keyword, ',
                        'so we use `variable` function name instead.'
                    ])),
                    section([
                        sup('supertext'),
                        sub('subtext'),
                        i('alternative voice'),
                        strong('important!')
                    ])
                ]);
            assert.equal(element.outerHTML,
                '' +
                    '
<dfn>' +
                        'Instance.js — simple and powerfull DOM Element interface' +
                    '</dfn>
' +
                    '

' +
                        '<var>var</var>' +
                        ' — is reserved JavaScript keyword, ' +
                        'so we use `variable` function name instead.' +
                    '

' +
                    '
' +
                        'supertext' +
                        'subtext' +
                        'alternative voice' +
                        'important!' +
                    '
' +
                '')
        });
        it('htmldom, span', () => {
            const node1 = htmldom('span', {
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            const node2 = span({
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            [node1, node2].forEach(node => {
                assert.equal(node.nodeType, ELEMENT_NODE);
                assert.equal(node.tagName, 'SPAN');
                assert.equal(node.constructor, HTMLSpanElement);
                assert.equal(node.attributes.length, 3);
                assert.equal(node.id, '00101');
                assert.equal(node.className, 'fa fi fu');
                assert.equal(node.tabIndex, 0);
                assert.equal(node.children.length, 1);
                assert.equal(node.childNodes.length, 3);
                assert.equal(node.childNodes[0].nodeType, TEXT_NODE);
                assert.equal(node.childNodes[1].constructor, HTMLSpanElement);
                assert.equal(node.childNodes[2].textContent, 'b');
            });
            assert(node1.isEqualNode(node2), 'htmldom() and span() work differently');
        });
        it('a', () => {
            const node = a({
                href : 'html://www.aria.dom/math.svg',
                rel : 'next',
                title : 'om dom dom dom d',
                textContent : 'DOM module'
            });
            assert.equal(node.tagName, 'A');
            assert.equal(node.constructor, HTMLAnchorElement);
            assert.equal(node.href, 'html://www.aria.dom/math.svg');
            assert.equal(node.rel, 'next');
            assert.equal(node.title, 'om dom dom dom d');
            assert.equal(node.textContent, 'DOM module');
        });
    });
});
<|endoftext|>


<commit_before>import {
    address,
    fieldset, legend, input,
    header, nav, a, abbr, form, button,
    label, select, option,
    div, ul, ol, li, dl, dd, dt,
    main, section, details, summary,
    dfn, sup, sub, i, strong, p, variable,
    htmldom, span
} from './htmldom';

import { htmlEqualMarkup } from './util/htmlequalmarkup';

import chai from 'chai';

const { Node, HTMLSpanElement, HTMLAnchorElement } = window;
const { TEXT_NODE, ELEMENT_NODE } = Node;
const { assert } = chai;

describe('HTMLDOM library', () => {
    describe('Authorization fieldset', () => {
        it('properly build form authorization fieldset', () => {
            const element =
                fieldset([
                    legend('Authorization'),
                    input({ type : 'email' }),
                    input({ type : 'password' })
                ]);
            assert.equal(element.outerHTML,
                '<fieldset>' +
                    '<legend>Authorization</legend>' +
                    '<input type="email">' +
                    '<input type="password">' +
                '</fieldset>');
        });
    });
    describe('Header with navigation', () => {
        it('properly build header with navigational links inside', () => {
            const element =
                header(nav([
                    a({ href : '/lib.html', textContent : 'Library' }),
                    a({ href : '/spec.html', textContent : 'Specifications' }),
                    a({ href : '/home.html', textContent : 'Go home' })
                ]));
            assert.equal(element.outerHTML,
                '' +
                    'Library' +
                    'Specifications' +
                    'Go home' +
                '');
        });
    });
    describe('Select box widget', () => {
        let widget, selected;
        const element =
            label([
                'Select technology ',
                widget = select([
                    option('DOM'),
                    option('XML'),
                    selected = option({ selected : true, textContent : 'HTML' }),
                    option('SVG'),
                    option('MathML'),
                    option('WAI-ARIA')
                ])
            ]);
        it('properly build label with select box option list inside', () => {
            assert.equal(element.outerHTML,
                '' +
                    'Select technology ' +
                    '<select>' +
                        '<option>DOM</option>' +
                        '<option>XML</option>' +
                        '<option>HTML</option>' +
                        '<option>SVG</option>' +
                        '<option>MathML</option>' +
                        '<option>WAI-ARIA</option>' +
                    '</select>' +
                '');
        });
        it('proper selected option reference', () => {
            assert.equal(widget.selectedOptions.length, 1);
            assert.equal(widget.selectedOptions[0], selected);
        });
    });
    describe('Search form', () => {
        it('properly build form with search input and submit button inside', () => {
            const element =
                form({
                    attrset : { role : 'search' },
                    children : [
                        input({ type : 'search' }),
                        button('Search')
                    ]
                });
            assert.equal(element.outerHTML,
                '<form role="search">' +
                    '<input type="search">' +
                    '<button>Search</button>' +
                '</form>');
        });
    });
    describe('Checkboxes', () => {
        let simple, checked, indeterminate;
        const element =
            div([
                simple = input({ type : 'checkbox' }),
                checked = input({ type : 'checkbox', checked : true }),
                indeterminate = input({ type : 'checkbox', indeterminate : true })
            ]);
        it('proper HTML rendered', () => {
            assert.equal(element.outerHTML,
                '
' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                    '<input type="checkbox">' +
                '
');
        });
        it('check current state of widgets', () => {
            assert(!simple.checked);
            assert(checked.checked);
            assert(indeterminate.indeterminate);
        });
        it('proper initial state assignment', () => {
            const sample = input({ type : 'checkbox', attrset : { checked : '' } });
            const container = div({ innerHTML : '<input type=checkbox checked>' });
            assert(sample.isEqualNode(container.firstChild));
        });
    });
    describe('Various list examples', () => {
        it('properly build ul + li', () => {
            assert.equal(
                ul([
                    li('Ampeg'),
                    li('Fender'),
                    li('Warwick')
                ]).outerHTML,
                '
' +
                    '
Ampeg
' +
                    '
Fender
' +
                    '
Warwick
' +
                '
'
            );
        });
        it('properly build ol + li', () => {
            assert.equal(
                ol([
                    li('Moscow'),
                    li('Amsterdam'),
                    li('New York')
                ]).outerHTML,
                '
' +
                    '
Moscow
' +
                    '
Amsterdam
' +
                    '
New York
' +
                '
'
            );
        });
        it('properly build dl + dt + dd', () => {
            assert.equal(
                dl([
                    dt('DOM'),
                    dd('Document object model'),
                    dt('XML'),
                    dd('Extensible markup language'),
                    dt('HTML'),
                    dd('Hyper text markup language')
                ]).outerHTML,
                '
' +
                    '
DOM
' +
                    '
Document object model
' +
                    '
XML
' +
                    '
Extensible markup language
' +
                    '
HTML
' +
                    '
Hyper text markup language
' +
                '
'
            );
        });
    });
    describe('Details with summary', () => {
        it('properly build widget', () => {
            const element =
                details([
                    summary('Show details'),
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ',
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
                ]);
            assert.equal(element.outerHTML,
                '' +
                    'Show details' +
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' +
                    'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +
                '');
        });
    });
    describe('Other', () => {
        it('property build DOM fragment', () => {
            const element =
                main([
                    section(dfn('Instance.js — simple and powerfull DOM Element interface')),
                    section(p([
                        variable('var'),
                        ' — is reserved JavaScript keyword, ',
                        'so we use `variable` function name instead.'
                    ])),
                    section([
                        sup('supertext'),
                        sub('subtext'),
                        i('alternative voice'),
                        strong('important!')
                    ])
                ]);
            assert.equal(element.outerHTML,
                '' +
                    '
<dfn>' +
                        'Instance.js — simple and powerfull DOM Element interface' +
                    '</dfn>
' +
                    '

' +
                        '<var>var</var>' +
                        ' — is reserved JavaScript keyword, ' +
                        'so we use `variable` function name instead.' +
                    '

' +
                    '
' +
                        'supertext' +
                        'subtext' +
                        'alternative voice' +
                        'important!' +
                    '
' +
                '');
        });
        it('htmldom, span', () => {
            const node1 = htmldom('span', {
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            const node2 = span({
                id : '00101',
                className :'fa fi fu',
                tabIndex : 0,
                children : ['a', span('a b'), 'b']
            });
            [node1, node2].forEach(node => {
                assert.equal(node.nodeType, ELEMENT_NODE);
                assert.equal(node.tagName, 'SPAN');
                assert.equal(node.constructor, HTMLSpanElement);
                assert.equal(node.attributes.length, 3);
                assert.equal(node.id, '00101');
                assert.equal(node.className, 'fa fi fu');
                assert.equal(node.tabIndex, 0);
                assert.equal(node.children.length, 1);
                assert.equal(node.childNodes.length, 3);
                assert.equal(node.childNodes[0].nodeType, TEXT_NODE);
                assert.equal(node.childNodes[1].constructor, HTMLSpanElement);
                assert.equal(node.childNodes[2].textContent, 'b');
            });
            assert(node1.isEqualNode(node2), 'htmldom() and span() work differently');
        });
        it('a', () => {
            const node = a({
                href : 'html://www.aria.dom/math.svg',
                rel : 'next',
                title : 'om dom dom dom d',
                textContent : 'DOM module'
            });
            assert.equal(node.tagName, 'A');
            assert.equal(node.constructor, HTMLAnchorElement);
            assert.equal(node.href, 'html://www.aria.dom/math.svg');
            assert.equal(node.rel, 'next');
            assert.equal(node.title, 'om dom dom dom d');
            assert.equal(node.textContent, 'DOM module');
        });
        it('abbr', () => {
            const node = abbr({
                title : 'Hyper text markup language',
                children : 'HTML'
            });
            assert(htmlEqualMarkup(node, 'HTML'), 'properly build node');
        });
        it('address', () => {
            const node = address('vasily@pupkin.ru');
            assert(htmlEqualMarkup(node, '<address>vasily@pupkin.ru</address>'), 'properly build node');
        });
    });
});
<commit_msg>add some specs<commit_after>import {
    article, aside, address,
    fieldset, legend, input,
    header, nav, a, abbr, form, button,
    label, select, option,
    div, ul, ol, li, dl, dd, dt,
    main, section, details, summary,
    dfn, sup, sub, i, strong, p, variable,
    htmldom, span
} from './htmldom';

import { htmlEqualMarkup } from './util/htmlequalmarkup';

import chai from 'chai';

const { Node, HTMLSpanElement, HTMLAnchorElement } = window;
const { TEXT_NODE, ELEMENT_NODE } = Node;
const { assert } = chai;

describe('HTMLDOM library', () => {
    describe('Elements', () => {
        it('a', () => {
            const node = a({
                href : 'html://www.aria.dom/math.svg',
                rel : 'next',
                title : 'om dom dom dom d',
                textContent : 'DOM module'
            });
            assert.equal(node.tagName, 'A');
            assert.equal(node.constructor, HTMLAnchorElement);
            assert.equal(node.href, 'html://www.aria.dom/math.svg');
            assert.equal(node.rel, 'next');
            assert.equal(node.title, 'om dom dom dom d');
            assert.equal(node.textContent, 'DOM module');
            assert(
                htmlEqualMarkup(node,
                    'DOM module'));
        });
        it('abbr', () => {
            const node = abbr({
                title : 'Hyper text markup language',
                children : 'HTML'
            });
            assert(
                htmlEqualMarkup(node, 'HTML'),
                'properly build node');
        });
        it('address', () => {
            const node = address('vasily@pupkin.ru');
            assert(
                htmlEqualMarkup(node, '<address>vasily@pupkin.ru</address>'),
                'properly build node');
        });
        it('article', () => {
            const node = article('Hello world!');
            assert(
                htmlEqualMarkup(node, '
Hello world!
'),
                'properly build node');
        });
        it('aside', () => {
            const node = aside('Your advert may be here!');
            assert(
                htmlEqualMarkup(node, 'Your advert may be here!'),
                'properly build node');
        });
    });
    describe('Various structures', () => {
        describe('Authorization fieldset', () => {
            it('properly build form authorization fieldset', () => {
                const element =
                    fieldset([
                        legend('Authorization'),
                        input({ type : 'email' }),
                        input({ type : 'password' })
                    ]);
                assert.equal(element.outerHTML,
                    '<fieldset>' +
                        '<legend>Authorization</legend>' +
                        '<input type="email">' +
                        '<input type="password">' +
                    '</fieldset>');
            });
        });
        describe('Header with navigation', () => {
            it('properly build header with navigational links inside', () => {
                const element =
                    header(nav([
                        a({ href : '/lib.html', textContent : 'Library' }),
                        a({ href : '/spec.html', textContent : 'Specifications' }),
                        a({ href : '/home.html', textContent : 'Go home' })
                    ]));
                assert.equal(element.outerHTML,
                    '' +
                        'Library' +
                        'Specifications' +
                        'Go home' +
                    '');
            });
        });
        describe('Select box widget', () => {
            let widget, selected;
            const element =
                label([
                    'Select technology ',
                    widget = select([
                        option('DOM'),
                        option('XML'),
                        selected = option({ selected : true, textContent : 'HTML' }),
                        option('SVG'),
                        option('MathML'),
                        option('WAI-ARIA')
                    ])
                ]);
            it('properly build label with select box option list inside', () => {
                assert.equal(element.outerHTML,
                    '' +
                        'Select technology ' +
                        '<select>' +
                            '<option>DOM</option>' +
                            '<option>XML</option>' +
                            '<option>HTML</option>' +
                            '<option>SVG</option>' +
                            '<option>MathML</option>' +
                            '<option>WAI-ARIA</option>' +
                        '</select>' +
                    '');
            });
            it('proper selected option reference', () => {
                assert.equal(widget.selectedOptions.length, 1);
                assert.equal(widget.selectedOptions[0], selected);
            });
        });
        describe('Search form', () => {
            it('properly build form with search input and submit button inside', () => {
                const element =
                    form({
                        attrset : { role : 'search' },
                        children : [
                            input({ type : 'search' }),
                            button('Search')
                        ]
                    });
                assert.equal(element.outerHTML,
                    '<form role="search">' +
                        '<input type="search">' +
                        '<button>Search</button>' +
                    '</form>');
            });
        });
        describe('Checkboxes', () => {
            let simple, checked, indeterminate;
            const element =
                div([
                    simple = input({ type : 'checkbox' }),
                    checked = input({ type : 'checkbox', checked : true }),
                    indeterminate = input({ type : 'checkbox', indeterminate : true })
                ]);
            it('proper HTML rendered', () => {
                assert.equal(element.outerHTML,
                    '
' +
                        '<input type="checkbox">' +
                        '<input type="checkbox">' +
                        '<input type="checkbox">' +
                    '
');
            });
            it('check current state of widgets', () => {
                assert(!simple.checked);
                assert(checked.checked);
                assert(indeterminate.indeterminate);
            });
            it('proper initial state assignment', () => {
                const sample = input({ type : 'checkbox', attrset : { checked : '' } });
                const container = div({ innerHTML : '<input type=checkbox checked>' });
                assert(sample.isEqualNode(container.firstChild));
            });
        });
        describe('Various list examples', () => {
            it('properly build ul + li', () => {
                assert.equal(
                    ul([
                        li('Ampeg'),
                        li('Fender'),
                        li('Warwick')
                    ]).outerHTML,
                    '
' +
                        '
Ampeg
' +
                        '
Fender
' +
                        '
Warwick
' +
                    '
'
                );
            });
            it('properly build ol + li', () => {
                assert.equal(
                    ol([
                        li('Moscow'),
                        li('Amsterdam'),
                        li('New York')
                    ]).outerHTML,
                    '
' +
                        '
Moscow
' +
                        '
Amsterdam
' +
                        '
New York
' +
                    '
'
                );
            });
            it('properly build dl + dt + dd', () => {
                assert.equal(
                    dl([
                        dt('DOM'),
                        dd('Document object model'),
                        dt('XML'),
                        dd('Extensible markup language'),
                        dt('HTML'),
                        dd('Hyper text markup language')
                    ]).outerHTML,
                    '
' +
                        '
DOM
' +
                        '
Document object model
' +
                        '
XML
' +
                        '
Extensible markup language
' +
                        '
HTML
' +
                        '
Hyper text markup language
' +
                    '
'
                );
            });
        });
        describe('Details with summary', () => {
            it('properly build widget', () => {
                const element =
                    details([
                        summary('Show details'),
                        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ',
                        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
                    ]);
                assert.equal(element.outerHTML,
                    '' +
                        'Show details' +
                        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' +
                        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' +
                    '');
            });
        });
        describe('Other', () => {
            it('property build DOM fragment', () => {
                const element =
                    main([
                        section(dfn('htmlmodule — DOM assembler library')),
                        section(p([
                            variable('var'),
                            ' — is reserved JavaScript keyword, ',
                            'so we use `variable` function name instead.'
                        ])),
                        section([
                            sup('supertext'),
                            sub('subtext'),
                            i('alternative voice'),
                            strong('important!')
                        ])
                    ]);
                assert.equal(element.outerHTML,
                    '' +
                        '
<dfn>' +
                            'htmlmodule — DOM assembler library' +
                        '</dfn>
' +
                        '

' +
                            '<var>var</var>' +
                            ' — is reserved JavaScript keyword, ' +
                            'so we use `variable` function name instead.' +
                        '

' +
                        '
' +
                            'supertext' +
                            'subtext' +
                            'alternative voice' +
                            'important!' +
                        '
' +
                    '');
            });
            it('htmldom, span', () => {
                const node1 = htmldom('span', {
                    id : '00101',
                    className :'fa fi fu',
                    tabIndex : 0,
                    children : ['a', span('a b'), 'b']
                });
                const node2 = span({
                    id : '00101',
                    className :'fa fi fu',
                    tabIndex : 0,
                    children : ['a', span('a b'), 'b']
                });
                [node1, node2].forEach(node => {
                    assert.equal(node.nodeType, ELEMENT_NODE);
                    assert.equal(node.tagName, 'SPAN');
                    assert.equal(node.constructor, HTMLSpanElement);
                    assert.equal(node.attributes.length, 3);
                    assert.equal(node.id, '00101');
                    assert.equal(node.className, 'fa fi fu');
                    assert.equal(node.tabIndex, 0);
                    assert.equal(node.children.length, 1);
                    assert.equal(node.childNodes.length, 3);
                    assert.equal(node.childNodes[0].nodeType, TEXT_NODE);
                    assert.equal(node.childNodes[1].constructor, HTMLSpanElement);
                    assert.equal(node.childNodes[2].textContent, 'b');
                });
                assert(node1.isEqualNode(node2), 'htmldom() and span() work differently');
            });
        });
    });
});
<|endoftext|>