<commit_before>psc.namespace('admin');

psc.admin.UserAdmin = (function ($) {
  var user;

  var provisionableSites = new Array();
  PROVISIONABLE_SITES.each(function(site) {
    provisionableSites.push(site.identifier)
  });

  function selectRole(roleKey) {
    $('a.role').removeClass('selected');
    $('#role-' + roleKey).addClass('selected');

    if (roleKey === 'multiple-roles') {
      startEditingMultiple();
      $('input.roles-to-edit').change(selectRoleCheckbox);

    } else {
      startEditing(roleKey);
    }
  }

  function selectRoleTab(evt) {
    var roleKey = evt.target.id.substring("role-".length)
    selectRole(roleKey);
    syncAllVsOne();
    return false;
  }

  function selectRoleCheckbox(evt) {
    var checked = _($('input.roles-to-edit:checked')).map(function(elt) {
      return $(elt).val();
    });

    addRoleAndScopesToMultipleTemplate(checked);
    syncAllVsOne();
    return false;
  }

  function determineProvisionableStudies(role) {
    var isTM = _(role.uses || []).indexOf("template_management") >= 0;
    var isSP = _(role.uses || []).indexOf("site_participation") >= 0;
    if (isTM && isSP) {
      return PROVISIONABLE_STUDIES['template_management+site_participation'];
    } else if (isTM) {
      return PROVISIONABLE_STUDIES['template_management'];
    } else {
      // use participation even for roles that don't have uses
      return PROVISIONABLE_STUDIES['site_participation'];
    }
  }

  function startEditing(roleKey) {
    var role = _.detect(PROVISIONABLE_ROLES, function (role) { return role.key === roleKey });
    if (role) {
      var enableRoleControl = isControlEnabled('role-control', role);
      var enableSitesControl = isControlEnabled('sites-control', role);

      $('#role-editor-pane').html(resigTemplate('role_editor_template', {
          role: role, sites: PROVISIONABLE_SITES, studies: determineProvisionableStudies(role),
          enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl
        })).
        find('input.role-group-membership').attr('checked', !!user.memberships[role.key]).
        click(_(updateGroupMembership).bind(this, function(evt) {return $(evt.target).attr('checked')})).end().
        parent().attr('role', roleKey);
      registerScopeControls('#role-editor-pane', role, 'site', 'sites');
      registerScopeControls('#role-editor-pane', role, 'study', 'studies');
    } else {
      alert("No such role " + roleKey);
    }
  }

  function startEditingMultiple(roleKeys) {
    roleKeys = roleKeys || []
    var roles = _.select(PROVISIONABLE_ROLES, function (role) { return _.include(roleKeys, role.key)});

    var enableRoleControl = _(roles).any(function(r) {return isControlEnabled('role-control', r)});
    var enableSitesControl = _(roles).any(function(r) {return isControlEnabled('sites-control', r)});
    $('#role-editor-pane').html(resigTemplate('multiple_role_editor_template', {
      roles: roles, sites: PROVISIONABLE_SITES,
      studies: uniqueStudies(_(roles).map(function(r) {return determineProvisionableStudies(r)}).flatten()),
      enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl,
      utils: {mapRoleKeys: mapRoleKeys, mapRoleNames: mapRoleNames, escapeIdSpaces: escapeIdSpaces}
    }));
  }

  function addRoleAndScopesToMultipleTemplate(roleKeys) {
    roleKeys = roleKeys || []
    var roles = _.select(PROVISIONABLE_ROLES, function (role) { return _.include(roleKeys, role.key)});

    var enableRoleControl = _(roles).any(function(r) {return isControlEnabled('role-control', r)});
    var enableSitesControl = _(roles).any(function(r) {return isControlEnabled('sites-control', r)});
    $('#scope-container').html(resigTemplate('role_and_scope_assignment_template', {
      roles: roles, sites: PROVISIONABLE_SITES,
      studies: uniqueStudies(_(roles).map(function(r) {return determineProvisionableStudies(r)}).flatten()),
      enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl,
      utils: {mapRoleKeys: mapRoleKeys, mapRoleNames: mapRoleNames, escapeIdSpaces: escapeIdSpaces}
    }))

    registerMultipleGroupControl('#role-editor-pane', roles);
    registerMultipleScopeControls('#role-editor-pane', roles, 'site', 'sites')
    registerMultipleScopeControls('#role-editor-pane', roles, 'study', 'studies')

  }

  function uniqueStudies(studies) {
    var unique = [];
    _(studies).each(function(s) {
      if (!_(unique).pluck('identifier').include(s.identifier)) {
        unique.push(s)
      }
    });
    return unique;
  }

  function isControlEnabled(controlKey, role) {
      var enableRoleControl = true;
      var enableSitesControl = true;
      var provisionableStudies = new Array();
      determineProvisionableStudies(role).each(function(study) {
          provisionableStudies.push(study.identifier)
      });
      var mem = user.memberships[role.key]
      if (mem != null) {
        // Disable role membership control when the user we are provisioning is a
        // member of sites and/or studies that are not in the list of provisionable studies.
        if (mem['sites'] != null && mem['studies'] != null) {
          if (!(isSubsetOfProvision(provisionableSites, mem['sites'])
              && isSubsetOfProvision(provisionableStudies, mem['studies']))) {
            enableRoleControl = false;
          }
        } else if (mem['studies'] != null) {
          if (!isSubsetOfProvision(provisionableStudies, mem['studies'])) {
            enableRoleControl = false;
          }
        } else if (mem['sites'] != null) {
          if (!isSubsetOfProvision(provisionableSites, mem['sites'])) {
            enableRoleControl = false;
          }
        }

        // Disable sites membership control when the user we are provisioning is a
        // member of all-sites and the all-sites membership is not in the list of provisionable sites.
        if (!_.isEmpty(provisionableSites) && !_.isEmpty(mem['sites'])) {
          if (!_.include(provisionableSites,'__ALL__') && _.include(mem['sites'],'__ALL__')) {
            enableSitesControl = false;
          }
        }
      }

    switch(controlKey) {
      case 'role-control': return enableRoleControl;
      case 'sites-control': return enableSitesControl;
      default: return null;
    }
  }

  function isSubsetOfProvision(provisionableList, membershipList) {
    if(!_.isEmpty(provisionableList) && !_.isEmpty(membershipList)) {
      return _.all(membershipList, function(ele){
        return _.include(provisionableList,ele)
      })
    }
  }

  function updateGroupMembership(isCheckedFn, evt) {
    if (!isCheckedFn) {return;}

    var roleKeys = evt.target.value ? evt.target.value.split(',') : [];

    _(roleKeys).each(function(k) {
      if (isCheckedFn(evt)) {
        user.add(k);
      } else {
        user.remove(k);
      }
    });
  }

  function registerMultipleGroupControl(pane, roles) {
    var state = determineTristateCheckboxState(roles);

    var input = $(pane).find('#multiple-group-membership:first');

    $(input).tristate({initialState: state});

    updateIntermediateStateLabel(state, pane, '#partial-multiple-group-membership-info', roles);

    $(input).bind('tristate-state-change',
        _(updateGroupMembership).bind(this, function(evt) {return $(evt.target).tristate('state') == 'checked'}));
    $(input).bind('tristate-state-change', _(function (pane, roles, evt) {
      var state = $(evt.target).attr('state');
      updateIntermediateStateLabel(state, pane, '#partial-multiple-group-membership-info', roles);
    }).bind(this, pane, roles));
  }

  function updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue) {
    var label = $(pane).find(label + ':first');

    if (state == 'intermediate') {
      var roleKeys = mapRoleKeys(roles);
      var scope = buildScopeObject(scopeType, scopeValue);
      var matchingRoleKeys = user.matchingMemberships(roleKeys, scope);
      var matchingRoles = _(PROVISIONABLE_ROLES).select(function (role) {return _(matchingRoleKeys).include(role.key)});
      var nonMatchingRoles = _(roles).reject(function (role) {return _(matchingRoleKeys).include(role.key)});
      $(label).html('(Applies to ' +  mapRoleNames(matchingRoles).join(', ') + ', but not ' + mapRoleNames(nonMatchingRoles).join(', ') + ')').show();
    } else {
      $(label).hide().empty();
    }
  }

  function determineTristateCheckboxState(roles, scopeType, scopeValue) {
    var roleKeys = mapRoleKeys(roles);
    var scope = buildScopeObject(scopeType, scopeValue);

    var status = user.membershipsStatus(roleKeys, scope);
    console.log("Tri-state checkbox status", status, scopeType, scopeValue);
    switch(status) {
      case 'FULL':    return 'checked';
      case 'PARTIAL': return 'intermediate';
      case 'NONE':    return 'unchecked';
      default:
        console.log("Memberships status unknown: ", status);
        return 'unchecked';
    }
  }

  function registerScopeControls(pane, role, scopeType, scopeTypePlural) {
    $(pane).find('input.scope-' + scopeType).each(function (i, input) {
      var qMembership = {}; qMembership[scopeType] = $(input).attr(scopeType + '-identifier');
      $(input).attr(
          'checked',
          user.hasMembership(role.key, qMembership));
    }).click(_(updateMembershipScope).bind(this, role.key, scopeType, scopeTypePlural));
  }

  function registerMultipleScopeControls(pane, roles, scopeType, scopeTypePlural) {
    $(pane).find('input.scope-' + scopeType).each(function (i, input) {
      var scopeValue = $(input).attr(scopeType + '-identifier');
      var state = determineTristateCheckboxState(roles, scopeType, scopeValue);
      var label = '#partial-scope-' + scopeType + '-' + escapeIdSpaces(scopeValue) + '-info';
      $(input).tristate({initialState: state});
      updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue);
    }).bind('tristate-state-change', _(function(roles, scopeType, scopeTypePlural, evt) {
      updateMultipleMembershipScope(_(roles).map(function(r){return r.key}), scopeType, scopeTypePlural, evt);
      var scopeValue = $(evt.target).attr(scopeType + '-identifier');
      var state = determineTristateCheckboxState(roles, scopeType, scopeValue);
      var label = '#partial-scope-' + scopeType + '-' + escapeIdSpaces(scopeValue) + '-info';
      updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue);
    }).bind(this, roles, scopeType, scopeTypePlural));
  }

  function buildScopeObject(scopeType, scopeValue) {
    var scope = null;
    if (scopeType) {
      scope = {}; scope[scopeType] = scopeValue;
    }
    return scope;
  }

  function mapRoleKeys(roles) {
    return roles.map(function(r){return r.key});
  }

  function mapRoleNames(roles) {
    return _(roles).map(function(r){return r.name});
  }

  function escapeIdSpaces(id) {
    return id.replace(' ', '--space--');
  }

  function updateMembershipScope(roleKey, scopeType, scopeListName, evt) {
    console.log("Updating user obj", roleKey, scopeType, scopeListName, evt);
    var scope = {}; scope[scopeListName] = [ evt.target.getAttribute(scopeType + '-identifier') ];
    if ($(evt.target).attr('checked')) {
      user.add(roleKey, scope);
    } else {
      user.remove(roleKey, scope);
    }
  }

  function updateMultipleMembershipScope(roleKeys, scopeType, scopeListName, evt) {
    var scope = {}; scope[scopeListName] = [ evt.target.getAttribute(scopeType + '-identifier') ];
    var state = $(evt.target).attr('state')

    console.log("Updating user obj", roleKeys, scopeType, scopeListName, scope, state, evt);

    _(roleKeys).each(function(roleKey) {
      switch(state) {
      case 'checked':
        user.add(roleKey, scope); break;
      case 'unchecked':
        user.remove(roleKey, scope); break;
      default:
        console.log("State does not exist", state);
      }
    });
  }

  function syncRoleTabOnChange(evt, data) {
    if (user.hasMembership(data.role)) {
      $('a#role-' + data.role).addClass('member');
    } else {
      $('a#role-' + data.role).removeClass('member');
    }
  }

  function syncRoleEditorOnChange(evt, data) {
    var role = data.role;
    var editorRole = $('#role-editor').attr('role');
    if (role !== editorRole && editorRole.indexOf(',') !== -1) { return; }
    var input;
    if (data.scopeType) {
      input = $('#role-editor input#scope-' + data.scopeType + '-' + data.scopeIdentifier);
      setTimeout(syncAllVsOne, 0);
    } else {
      input = $('#role-editor input#group-' + data.role);
      if ($(input).length <= 0) {
        input = $('#role-editor input#multiple-group-membership');
      }
    }
    input.attr('checked', data.kind === 'add');
    if (input.attr('state')) {
      input.attr('state', data.kind === 'add' ? 'checked' : 'unchecked');
      $(input).parents('.row').find('.partial-membership').empty();
    }
  }

  function syncAllVsOne() {
    _(['scope-study', 'scope-site']).each(function (scopeClass) {
      var isAll = $('#role-editor input.all.' + scopeClass + ":checked").length > 0;
      if (isAll) {
        var oneSelector = '#role-editor input.one.' + scopeClass;

        // TODO: Find way to get tri-state checkboxes without using the state attribute.
        var regular = $(oneSelector).not('[state]').
          filter(':checked').click().end()

        var tristate = $(oneSelector + '[state]').
          tristate('state', 'unchecked')

        regular.add(tristate).
          attr("disabled", true).
          closest('div.row').addClass('disabled');
      } else {
        $('#role-editor input.one.' + scopeClass).
          attr("disabled", false).
          closest('div.row').removeClass('disabled');
      }
    });
  }

  function syncUsername() {
    user.username = $('input#username').val();
  }

  function toggleRoleView(evt, action) {
    if(!action) {
      action = $('#show-all-toggle:contains(Show)').length > 0 ? 'hide' : 'show';
    }

    if (action === 'show') {
      $('#show-all-toggle').text('Show Suite Roles');
      $("div.role-tab[role-type='suite']").hide();
    } else {
      $('#show-all-toggle').text('Hide Suite Roles')
      $("div.role-tab[role-type='suite']").show();
    }
    return false;
  }

  return {
    init: function (u) {
      user = u;
      _(user.memberships).each(function (scopes, roleKey) {
        $('#role-' + roleKey).addClass('member');
      }, this);
      var firstRole;
      if ($("div.role-tab[role-type='psc'] a.role.member").length > 0) {
        firstRole = $('a.role.member')[0].id.substring("role-".length);
      } else {
        firstRole = 'study_subject_calendar_manager';
      }
      selectRole(firstRole);
      $('a.role').click(selectRoleTab);
      $(user).bind('membership-change', syncRoleTabOnChange).
        bind('membership-change', syncRoleEditorOnChange);
      $('input#username').keyup(syncUsername);
      syncAllVsOne();
      toggleRoleView(null, 'show');
      $('#show-all-toggle').click(toggleRoleView);
    },

    serializeRoleChanges: function () {
      if (user.username) {
        var changes = {}; changes[user.username] = user.changes;
        return YAHOO.lang.JSON.stringify(changes);
      } else {
        return "{}";
      }
    }
  };
})(jQuery);
<commit_msg>Add missing semi-colons.<commit_after>psc.namespace('admin');

psc.admin.UserAdmin = (function ($) {
  var user;

  var provisionableSites = new Array();
  PROVISIONABLE_SITES.each(function(site) {
    provisionableSites.push(site.identifier)
  });

  function selectRole(roleKey) {
    $('a.role').removeClass('selected');
    $('#role-' + roleKey).addClass('selected');

    if (roleKey === 'multiple-roles') {
      startEditingMultiple();
      $('input.roles-to-edit').change(selectRoleCheckbox);

    } else {
      startEditing(roleKey);
    }
  }

  function selectRoleTab(evt) {
    var roleKey = evt.target.id.substring("role-".length)
    selectRole(roleKey);
    syncAllVsOne();
    return false;
  }

  function selectRoleCheckbox(evt) {
    var checked = _($('input.roles-to-edit:checked')).map(function(elt) {
      return $(elt).val();
    });

    addRoleAndScopesToMultipleTemplate(checked);
    syncAllVsOne();
    return false;
  }

  function determineProvisionableStudies(role) {
    var isTM = _(role.uses || []).indexOf("template_management") >= 0;
    var isSP = _(role.uses || []).indexOf("site_participation") >= 0;
    if (isTM && isSP) {
      return PROVISIONABLE_STUDIES['template_management+site_participation'];
    } else if (isTM) {
      return PROVISIONABLE_STUDIES['template_management'];
    } else {
      // use participation even for roles that don't have uses
      return PROVISIONABLE_STUDIES['site_participation'];
    }
  }

  function startEditing(roleKey) {
    var role = _.detect(PROVISIONABLE_ROLES, function (role) { return role.key === roleKey });
    if (role) {
      var enableRoleControl = isControlEnabled('role-control', role);
      var enableSitesControl = isControlEnabled('sites-control', role);

      $('#role-editor-pane').html(resigTemplate('role_editor_template', {
          role: role, sites: PROVISIONABLE_SITES, studies: determineProvisionableStudies(role),
          enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl
        })).
        find('input.role-group-membership').attr('checked', !!user.memberships[role.key]).
        click(_(updateGroupMembership).bind(this, function(evt) {return $(evt.target).attr('checked')})).end().
        parent().attr('role', roleKey);
      registerScopeControls('#role-editor-pane', role, 'site', 'sites');
      registerScopeControls('#role-editor-pane', role, 'study', 'studies');
    } else {
      alert("No such role " + roleKey);
    }
  }

  function startEditingMultiple(roleKeys) {
    roleKeys = roleKeys || []
    var roles = _.select(PROVISIONABLE_ROLES, function (role) { return _.include(roleKeys, role.key)});

    var enableRoleControl = _(roles).any(function(r) {return isControlEnabled('role-control', r)});
    var enableSitesControl = _(roles).any(function(r) {return isControlEnabled('sites-control', r)});
    $('#role-editor-pane').html(resigTemplate('multiple_role_editor_template', {
      roles: roles, sites: PROVISIONABLE_SITES,
      studies: uniqueStudies(_(roles).map(function(r) {return determineProvisionableStudies(r)}).flatten()),
      enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl,
      utils: {mapRoleKeys: mapRoleKeys, mapRoleNames: mapRoleNames, escapeIdSpaces: escapeIdSpaces}
    }));
  }

  function addRoleAndScopesToMultipleTemplate(roleKeys) {
    roleKeys = roleKeys || []
    var roles = _.select(PROVISIONABLE_ROLES, function (role) { return _.include(roleKeys, role.key)});

    var enableRoleControl = _(roles).any(function(r) {return isControlEnabled('role-control', r)});
    var enableSitesControl = _(roles).any(function(r) {return isControlEnabled('sites-control', r)});
    $('#scope-container').html(resigTemplate('role_and_scope_assignment_template', {
      roles: roles, sites: PROVISIONABLE_SITES,
      studies: uniqueStudies(_(roles).map(function(r) {return determineProvisionableStudies(r)}).flatten()),
      enableRoleControl: enableRoleControl, enableSitesControl: enableSitesControl,
      utils: {mapRoleKeys: mapRoleKeys, mapRoleNames: mapRoleNames, escapeIdSpaces: escapeIdSpaces}
    }))

    registerMultipleGroupControl('#role-editor-pane', roles);
    registerMultipleScopeControls('#role-editor-pane', roles, 'site', 'sites');
    registerMultipleScopeControls('#role-editor-pane', roles, 'study', 'studies');

  }

  function uniqueStudies(studies) {
    var unique = [];
    _(studies).each(function(s) {
      if (!_(unique).pluck('identifier').include(s.identifier)) {
        unique.push(s)
      }
    });
    return unique;
  }

  function isControlEnabled(controlKey, role) {
      var enableRoleControl = true;
      var enableSitesControl = true;
      var provisionableStudies = new Array();
      determineProvisionableStudies(role).each(function(study) {
          provisionableStudies.push(study.identifier)
      });
      var mem = user.memberships[role.key]
      if (mem != null) {
        // Disable role membership control when the user we are provisioning is a
        // member of sites and/or studies that are not in the list of provisionable studies.
        if (mem['sites'] != null && mem['studies'] != null) {
          if (!(isSubsetOfProvision(provisionableSites, mem['sites'])
              && isSubsetOfProvision(provisionableStudies, mem['studies']))) {
            enableRoleControl = false;
          }
        } else if (mem['studies'] != null) {
          if (!isSubsetOfProvision(provisionableStudies, mem['studies'])) {
            enableRoleControl = false;
          }
        } else if (mem['sites'] != null) {
          if (!isSubsetOfProvision(provisionableSites, mem['sites'])) {
            enableRoleControl = false;
          }
        }

        // Disable sites membership control when the user we are provisioning is a
        // member of all-sites and the all-sites membership is not in the list of provisionable sites.
        if (!_.isEmpty(provisionableSites) && !_.isEmpty(mem['sites'])) {
          if (!_.include(provisionableSites,'__ALL__') && _.include(mem['sites'],'__ALL__')) {
            enableSitesControl = false;
          }
        }
      }

    switch(controlKey) {
      case 'role-control': return enableRoleControl;
      case 'sites-control': return enableSitesControl;
      default: return null;
    }
  }

  function isSubsetOfProvision(provisionableList, membershipList) {
    if(!_.isEmpty(provisionableList) && !_.isEmpty(membershipList)) {
      return _.all(membershipList, function(ele){
        return _.include(provisionableList,ele)
      })
    }
  }

  function updateGroupMembership(isCheckedFn, evt) {
    if (!isCheckedFn) {return;}

    var roleKeys = evt.target.value ? evt.target.value.split(',') : [];

    _(roleKeys).each(function(k) {
      if (isCheckedFn(evt)) {
        user.add(k);
      } else {
        user.remove(k);
      }
    });
  }

  function registerMultipleGroupControl(pane, roles) {
    var state = determineTristateCheckboxState(roles);

    var input = $(pane).find('#multiple-group-membership:first');

    $(input).tristate({initialState: state});

    updateIntermediateStateLabel(state, pane, '#partial-multiple-group-membership-info', roles);

    $(input).bind('tristate-state-change',
        _(updateGroupMembership).bind(this, function(evt) {return $(evt.target).tristate('state') == 'checked'}));
    $(input).bind('tristate-state-change', _(function (pane, roles, evt) {
      var state = $(evt.target).attr('state');
      updateIntermediateStateLabel(state, pane, '#partial-multiple-group-membership-info', roles);
    }).bind(this, pane, roles));
  }

  function updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue) {
    var label = $(pane).find(label + ':first');

    if (state == 'intermediate') {
      var roleKeys = mapRoleKeys(roles);
      var scope = buildScopeObject(scopeType, scopeValue);
      var matchingRoleKeys = user.matchingMemberships(roleKeys, scope);
      var matchingRoles = _(PROVISIONABLE_ROLES).select(function (role) {return _(matchingRoleKeys).include(role.key)});
      var nonMatchingRoles = _(roles).reject(function (role) {return _(matchingRoleKeys).include(role.key)});
      $(label).html('(Applies to ' +  mapRoleNames(matchingRoles).join(', ') + ', but not ' + mapRoleNames(nonMatchingRoles).join(', ') + ')').show();
    } else {
      $(label).hide().empty();
    }
  }

  function determineTristateCheckboxState(roles, scopeType, scopeValue) {
    var roleKeys = mapRoleKeys(roles);
    var scope = buildScopeObject(scopeType, scopeValue);

    var status = user.membershipsStatus(roleKeys, scope);
    console.log("Tri-state checkbox status", status, scopeType, scopeValue);
    switch(status) {
      case 'FULL':    return 'checked';
      case 'PARTIAL': return 'intermediate';
      case 'NONE':    return 'unchecked';
      default:
        console.log("Memberships status unknown: ", status);
        return 'unchecked';
    }
  }

  function registerScopeControls(pane, role, scopeType, scopeTypePlural) {
    $(pane).find('input.scope-' + scopeType).each(function (i, input) {
      var qMembership = {}; qMembership[scopeType] = $(input).attr(scopeType + '-identifier');
      $(input).attr(
          'checked',
          user.hasMembership(role.key, qMembership));
    }).click(_(updateMembershipScope).bind(this, role.key, scopeType, scopeTypePlural));
  }

  function registerMultipleScopeControls(pane, roles, scopeType, scopeTypePlural) {
    $(pane).find('input.scope-' + scopeType).each(function (i, input) {
      var scopeValue = $(input).attr(scopeType + '-identifier');
      var state = determineTristateCheckboxState(roles, scopeType, scopeValue);
      var label = '#partial-scope-' + scopeType + '-' + escapeIdSpaces(scopeValue) + '-info';
      $(input).tristate({initialState: state});
      updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue);
    }).bind('tristate-state-change', _(function(roles, scopeType, scopeTypePlural, evt) {
      updateMultipleMembershipScope(_(roles).map(function(r){return r.key}), scopeType, scopeTypePlural, evt);
      var scopeValue = $(evt.target).attr(scopeType + '-identifier');
      var state = determineTristateCheckboxState(roles, scopeType, scopeValue);
      var label = '#partial-scope-' + scopeType + '-' + escapeIdSpaces(scopeValue) + '-info';
      updateIntermediateStateLabel(state, pane, label, roles, scopeType, scopeValue);
    }).bind(this, roles, scopeType, scopeTypePlural));
  }

  function buildScopeObject(scopeType, scopeValue) {
    var scope = null;
    if (scopeType) {
      scope = {}; scope[scopeType] = scopeValue;
    }
    return scope;
  }

  function mapRoleKeys(roles) {
    return roles.map(function(r){return r.key});
  }

  function mapRoleNames(roles) {
    return _(roles).map(function(r){return r.name});
  }

  function escapeIdSpaces(id) {
    return id.replace(' ', '--space--');
  }

  function updateMembershipScope(roleKey, scopeType, scopeListName, evt) {
    console.log("Updating user obj", roleKey, scopeType, scopeListName, evt);
    var scope = {}; scope[scopeListName] = [ evt.target.getAttribute(scopeType + '-identifier') ];
    if ($(evt.target).attr('checked')) {
      user.add(roleKey, scope);
    } else {
      user.remove(roleKey, scope);
    }
  }

  function updateMultipleMembershipScope(roleKeys, scopeType, scopeListName, evt) {
    var scope = {}; scope[scopeListName] = [ evt.target.getAttribute(scopeType + '-identifier') ];
    var state = $(evt.target).attr('state')

    console.log("Updating user obj", roleKeys, scopeType, scopeListName, scope, state, evt);

    _(roleKeys).each(function(roleKey) {
      switch(state) {
      case 'checked':
        user.add(roleKey, scope); break;
      case 'unchecked':
        user.remove(roleKey, scope); break;
      default:
        console.log("State does not exist", state);
      }
    });
  }

  function syncRoleTabOnChange(evt, data) {
    if (user.hasMembership(data.role)) {
      $('a#role-' + data.role).addClass('member');
    } else {
      $('a#role-' + data.role).removeClass('member');
    }
  }

  function syncRoleEditorOnChange(evt, data) {
    var role = data.role;
    var editorRole = $('#role-editor').attr('role');
    if (role !== editorRole && editorRole.indexOf(',') !== -1) { return; }
    var input;
    if (data.scopeType) {
      input = $('#role-editor input#scope-' + data.scopeType + '-' + data.scopeIdentifier);
      setTimeout(syncAllVsOne, 0);
    } else {
      input = $('#role-editor input#group-' + data.role);
      if ($(input).length <= 0) {
        input = $('#role-editor input#multiple-group-membership');
      }
    }
    input.attr('checked', data.kind === 'add');
    if (input.attr('state')) {
      input.attr('state', data.kind === 'add' ? 'checked' : 'unchecked');
      $(input).parents('.row').find('.partial-membership').empty();
    }
  }

  function syncAllVsOne() {
    _(['scope-study', 'scope-site']).each(function (scopeClass) {
      var isAll = $('#role-editor input.all.' + scopeClass + ":checked").length > 0;
      if (isAll) {
        var oneSelector = '#role-editor input.one.' + scopeClass;

        // TODO: Find way to get tri-state checkboxes without using the state attribute.
        var regular = $(oneSelector).not('[state]').
          filter(':checked').click().end()

        var tristate = $(oneSelector + '[state]').
          tristate('state', 'unchecked')

        regular.add(tristate).
          attr("disabled", true).
          closest('div.row').addClass('disabled');
      } else {
        $('#role-editor input.one.' + scopeClass).
          attr("disabled", false).
          closest('div.row').removeClass('disabled');
      }
    });
  }

  function syncUsername() {
    user.username = $('input#username').val();
  }

  function toggleRoleView(evt, action) {
    if(!action) {
      action = $('#show-all-toggle:contains(Show)').length > 0 ? 'hide' : 'show';
    }

    if (action === 'show') {
      $('#show-all-toggle').text('Show Suite Roles');
      $("div.role-tab[role-type='suite']").hide();
    } else {
      $('#show-all-toggle').text('Hide Suite Roles')
      $("div.role-tab[role-type='suite']").show();
    }
    return false;
  }

  return {
    init: function (u) {
      user = u;
      _(user.memberships).each(function (scopes, roleKey) {
        $('#role-' + roleKey).addClass('member');
      }, this);
      var firstRole;
      if ($("div.role-tab[role-type='psc'] a.role.member").length > 0) {
        firstRole = $('a.role.member')[0].id.substring("role-".length);
      } else {
        firstRole = 'study_subject_calendar_manager';
      }
      selectRole(firstRole);
      $('a.role').click(selectRoleTab);
      $(user).bind('membership-change', syncRoleTabOnChange).
        bind('membership-change', syncRoleEditorOnChange);
      $('input#username').keyup(syncUsername);
      syncAllVsOne();
      toggleRoleView(null, 'show');
      $('#show-all-toggle').click(toggleRoleView);
    },

    serializeRoleChanges: function () {
      if (user.username) {
        var changes = {}; changes[user.username] = user.changes;
        return YAHOO.lang.JSON.stringify(changes);
      } else {
        return "{}";
      }
    }
  };
})(jQuery);
<|endoftext|>


<commit_before>// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License.  You may obtain a copy
// of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
// License for the specific language governing permissions and limitations under
// the License.

couchTests.users_db = function(debug) {
  // This tests the users db, especially validations
  // this should also test that you can log into the couch

  var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"});

  // test that you can treat "_user" as a db-name
  // this can complicate people who try to secure the users db with
  // an http proxy and fail to get both the actual db and the _user path
  // maybe it's not the right approach...
  // hard to know what else to do, as we don't let non-admins inspect the config
  // to determine the actual users db name.

  function testFun() {
    // test that the validation function is installed
    var ddoc = usersDb.open("_design/_auth");
    T(ddoc.validate_doc_update);

    // test that you can login as a user using basic auth
    var jchrisUserDoc = CouchDB.prepareUserDoc({
      name: "jchris@apache.org"
    }, "funnybone");
    T(usersDb.save(jchrisUserDoc).ok);

    T(CouchDB.session().userCtx.name == null);

    // test that you can use basic auth aginst the users db
    var s = CouchDB.session({
      headers : {
        //                 base64_encode("jchris@apache.org:funnybone")
        "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l"
      }
    });
    T(s.userCtx.name == "jchris@apache.org");
    T(s.info.authenticated == "default");
    T(s.info.authentication_db == "test_suite_users");
    TEquals(["oauth", "cookie", "default"], s.info.authentication_handlers);
    var s = CouchDB.session({
      headers : {
        "Authorization" : "Basic Xzpf" // name and pass of _:_
      }
    });
    T(s.name == null);
    T(s.info.authenticated == "default");

    // ok, now create a conflicting edit on the jchris doc, and make sure there's no login.
    var jchrisUser2 = JSON.parse(JSON.stringify(jchrisUserDoc));
    jchrisUser2.foo = "bar";
    T(usersDb.save(jchrisUser2).ok);
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should be an update conflict")
    } catch(e) {
      T(true);
    }
    // save as bulk with new_edits=false to force conflict save
    var resp = usersDb.bulkSave([jchrisUserDoc],{all_or_nothing : true});

    var jchrisWithConflict = usersDb.open(jchrisUserDoc._id, {conflicts : true});
    T(jchrisWithConflict._conflicts.length == 1)

    // no login with conflicted user doc
    try {
      var s = CouchDB.session({
        headers : {
          "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l"
        }
      });
      T(false && "this will throw")
    } catch(e) {
      T(e.error == "unauthorized")
      T(/conflict/.test(e.reason))
    }

    // you can delete a user doc
    s = CouchDB.session().userCtx;
    T(s.name == null);
    T(s.roles.indexOf("_admin") !== -1);
    T(usersDb.deleteDoc(jchrisWithConflict).ok);

    // you can't change doc from type "user"
    jchrisUserDoc = usersDb.open(jchrisUserDoc._id);
    jchrisUserDoc.type = "not user";
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should only allow us to save doc when type == 'user'");
    } catch(e) {
      T(e.reason == "doc.type must be user");
    }
    jchrisUserDoc.type = "user";

    // "roles" must be an array
    jchrisUserDoc.roles = "not an array";
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should only allow us to save doc when roles is an array");
    } catch(e) {
      T(e.reason == "doc.roles must be an array");
    }
    jchrisUserDoc.roles = [];
  };

  usersDb.deleteDb();
  run_on_modified_server(
    [{section: "couch_httpd_auth",
      key: "authentication_db", value: usersDb.name}],
    testFun
  );
  usersDb.deleteDb(); // cleanup

}
<commit_msg>Add missing semicolons to users_db.js test. This is part of the patch at COUCHDB-790 submitted by Gabriel Farrell.<commit_after>// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License.  You may obtain a copy
// of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
// License for the specific language governing permissions and limitations under
// the License.

couchTests.users_db = function(debug) {
  // This tests the users db, especially validations
  // this should also test that you can log into the couch

  var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"});

  // test that you can treat "_user" as a db-name
  // this can complicate people who try to secure the users db with
  // an http proxy and fail to get both the actual db and the _user path
  // maybe it's not the right approach...
  // hard to know what else to do, as we don't let non-admins inspect the config
  // to determine the actual users db name.

  function testFun() {
    // test that the validation function is installed
    var ddoc = usersDb.open("_design/_auth");
    T(ddoc.validate_doc_update);

    // test that you can login as a user using basic auth
    var jchrisUserDoc = CouchDB.prepareUserDoc({
      name: "jchris@apache.org"
    }, "funnybone");
    T(usersDb.save(jchrisUserDoc).ok);

    T(CouchDB.session().userCtx.name == null);

    // test that you can use basic auth aginst the users db
    var s = CouchDB.session({
      headers : {
        //                 base64_encode("jchris@apache.org:funnybone")
        "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l"
      }
    });
    T(s.userCtx.name == "jchris@apache.org");
    T(s.info.authenticated == "default");
    T(s.info.authentication_db == "test_suite_users");
    TEquals(["oauth", "cookie", "default"], s.info.authentication_handlers);
    var s = CouchDB.session({
      headers : {
        "Authorization" : "Basic Xzpf" // name and pass of _:_
      }
    });
    T(s.name == null);
    T(s.info.authenticated == "default");

    // ok, now create a conflicting edit on the jchris doc, and make sure there's no login.
    var jchrisUser2 = JSON.parse(JSON.stringify(jchrisUserDoc));
    jchrisUser2.foo = "bar";
    T(usersDb.save(jchrisUser2).ok);
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should be an update conflict");
    } catch(e) {
      T(true);
    }
    // save as bulk with new_edits=false to force conflict save
    var resp = usersDb.bulkSave([jchrisUserDoc],{all_or_nothing : true});

    var jchrisWithConflict = usersDb.open(jchrisUserDoc._id, {conflicts : true});
    T(jchrisWithConflict._conflicts.length == 1);

    // no login with conflicted user doc
    try {
      var s = CouchDB.session({
        headers : {
          "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l"
        }
      });
      T(false && "this will throw");
    } catch(e) {
      T(e.error == "unauthorized");
      T(/conflict/.test(e.reason));
    }

    // you can delete a user doc
    s = CouchDB.session().userCtx;
    T(s.name == null);
    T(s.roles.indexOf("_admin") !== -1);
    T(usersDb.deleteDoc(jchrisWithConflict).ok);

    // you can't change doc from type "user"
    jchrisUserDoc = usersDb.open(jchrisUserDoc._id);
    jchrisUserDoc.type = "not user";
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should only allow us to save doc when type == 'user'");
    } catch(e) {
      T(e.reason == "doc.type must be user");
    }
    jchrisUserDoc.type = "user";

    // "roles" must be an array
    jchrisUserDoc.roles = "not an array";
    try {
      usersDb.save(jchrisUserDoc);
      T(false && "should only allow us to save doc when roles is an array");
    } catch(e) {
      T(e.reason == "doc.roles must be an array");
    }
    jchrisUserDoc.roles = [];
  };

  usersDb.deleteDb();
  run_on_modified_server(
    [{section: "couch_httpd_auth",
      key: "authentication_db", value: usersDb.name}],
    testFun
  );
  usersDb.deleteDb(); // cleanup

}
<|endoftext|>


<commit_before>import Ember from "ember-metal/core"; // FEATURES
import { get } from "ember-metal/property_get";
import { set } from "ember-metal/property_set";
import { tryInvoke } from "ember-metal/utils";

import EmberObject from "ember-runtime/system/object";
import environment from "ember-metal/environment";

import {
  supportsHashChange,
  supportsHistory,
  getPath,
  getHash,
  getQuery,
  getFullPath,
  replacePath
} from "ember-routing/location/util";

/**
@module ember
@submodule ember-routing
*/

/**
  Ember.AutoLocation will select the best location option based off browser
  support with the priority order: history, hash, none.

  Clean pushState paths accessed by hashchange-only browsers will be redirected
  to the hash-equivalent and vice versa so future transitions are consistent.

  Keep in mind that since some of your users will use `HistoryLocation`, your
  server must serve the Ember app at all the routes you define.

  @class AutoLocation
  @namespace Ember
  @static
*/
export default EmberObject.extend({
  /**
    @private

    The browser's `location` object. This is typically equivalent to
    `window.location`, but may be overridden for testing.

    @property location
    @default environment.location
  */
  location: environment.location,

  /**
    @private

    The browser's `history` object. This is typically equivalent to
    `window.history`, but may be overridden for testing.

    @since 1.5.1
    @property history
    @default environment.history
  */
  history: environment.history,

  /**
   @private

   The user agent's global variable. In browsers, this will be `window`.

   @since 1.11
   @property global
   @default environment.global
  */
  global: environment.global,

  /**
    @private

    The browser's `userAgent`. This is typically equivalent to
    `navigator.userAgent`, but may be overridden for testing.

    @since 1.5.1
    @property userAgent
    @default environment.history
  */
  userAgent: environment.userAgent,

  /**
    @private

    This property is used by the router to know whether to cancel the routing
    setup process, which is needed while we redirect the browser.

    @since 1.5.1
    @property cancelRouterSetup
    @default false
  */
  cancelRouterSetup: false,

  /**
    @private

    Will be pre-pended to path upon state change.

    @since 1.5.1
    @property rootURL
    @default '/'
  */
  rootURL: '/',

  /**
   Called by the router to instruct the location to do any feature detection
   necessary. In the case of AutoLocation, we detect whether to use history
   or hash concrete implementations.
  */
  detect: function() {
    var rootURL = this.rootURL;

    Ember.assert('rootURL must end with a trailing forward slash e.g. "/app/"',
                 rootURL.charAt(rootURL.length-1) === '/');

    var implementation = detectImplementation({
      location: this.location,
      history: this.history,
      userAgent: this.userAgent,
      rootURL: rootURL,
      documentMode: this.documentMode,
      global: this.global
    });

    if (implementation === false) {
      set(this, 'cancelRouterSetup', true);
      implementation = 'none';
    }

    var concrete = this.container.lookup('location:' + implementation);
    Ember.assert("Could not find location '" + implementation + "'.", !!concrete);

    set(this, 'concreteImplementation', concrete);
  },

  initState: delegateToConcreteImplementation('initState'),
  getURL: delegateToConcreteImplementation('getURL'),
  setURL: delegateToConcreteImplementation('setURL'),
  replaceURL: delegateToConcreteImplementation('replaceURL'),
  onUpdateURL: delegateToConcreteImplementation('onUpdateURL'),
  formatURL: delegateToConcreteImplementation('formatURL'),

  willDestroy: function() {
    var concreteImplementation = get(this, 'concreteImplementation');

    if (concreteImplementation) {
      concreteImplementation.destroy();
    }
  }
});

function delegateToConcreteImplementation(methodName) {
  return function() {
    var concreteImplementation = get(this, 'concreteImplementation');
    Ember.assert("AutoLocation's detect() method should be called before calling any other hooks.", !!concreteImplementation);
    tryInvoke(concreteImplementation, methodName, arguments);
  };
}

/**
  Given the browser's `location`, `history` and `userAgent`, and a configured
  root URL, this function detects whether the browser supports the [History
  API](https://developer.mozilla.org/en-US/docs/Web/API/History) and returns a
  string representing the Location object to use based on its determination.

  For example, if the page loads in an evergreen browser, this function would
  return the string "history", meaning the history API and thus HistoryLocation
  should be used. If the page is loaded in IE8, it will return the string
  "hash," indicating that the History API should be simulated by manipulating the
  hash portion of the location.

*/

function detectImplementation(options) {
  var location = options.location,
      userAgent = options.userAgent,
      history = options.history,
      documentMode = options.documentMode,
      global = options.global,
      rootURL = options.rootURL;

  var implementation = 'none';
  var cancelRouterSetup = false;
  var currentPath = getFullPath(location);

  if (supportsHistory(userAgent, history)) {
    var historyPath = getHistoryPath(rootURL, location);

    // If the browser supports history and we have a history path, we can use
    // the history location with no redirects.
    if (currentPath === historyPath) {
      return 'history';
    } else {
      if (currentPath.substr(0, 2) === '/#') {
        history.replaceState({ path: historyPath }, null, historyPath);
        implementation = 'history';
      } else {
        cancelRouterSetup = true;
        replacePath(location, historyPath);
      }
    }
  } else if (supportsHashChange(documentMode, global)) {
    var hashPath = getHashPath(rootURL, location);

    // Be sure we're using a hashed path, otherwise let's switch over it to so
    // we start off clean and consistent. We'll count an index path with no
    // hash as "good enough" as well.
    if (currentPath === hashPath || (currentPath === '/' && hashPath === '/#/')) {
      implementation = 'hash';
    } else {
      // Our URL isn't in the expected hash-supported format, so we want to
      // cancel the router setup and replace the URL to start off clean
      cancelRouterSetup = true;
      replacePath(location, hashPath);
    }
  }

  if (cancelRouterSetup) {
    return false;
  }

  return implementation;
}

/**
  @private

  Returns the current path as it should appear for HistoryLocation supported
  browsers. This may very well differ from the real current path (e.g. if it
  starts off as a hashed URL)
*/
export function getHistoryPath(rootURL, location) {
  var path = getPath(location);
  var hash = getHash(location);
  var query = getQuery(location);
  var rootURLIndex = path.indexOf(rootURL);
  var routeHash, hashParts;

  Ember.assert('Path ' + path + ' does not start with the provided rootURL ' + rootURL, rootURLIndex === 0);

  // By convention, Ember.js routes using HashLocation are required to start
  // with `#/`. Anything else should NOT be considered a route and should
  // be passed straight through, without transformation.
  if (hash.substr(0, 2) === '#/') {
    // There could be extra hash segments after the route
    hashParts = hash.substr(1).split('#');
    // The first one is always the route url
    routeHash = hashParts.shift();

    // If the path already has a trailing slash, remove the one
    // from the hashed route so we don't double up.
    if (path.slice(-1) === '/') {
      routeHash = routeHash.substr(1);
    }

    // This is the "expected" final order
    path += routeHash;
    path += query;

    if (hashParts.length) {
      path += '#' + hashParts.join('#');
    }
  } else {
    path += query;
    path += hash;
  }

  return path;
}

/**
  @private

  Returns the current path as it should appear for HashLocation supported
  browsers. This may very well differ from the real current path.

  @method _getHashPath
*/
export function getHashPath(rootURL, location) {
  var path = rootURL;
  var historyPath = getHistoryPath(rootURL, location);
  var routePath = historyPath.substr(rootURL.length);

  if (routePath !== '') {
    if (routePath.charAt(0) !== '/') {
      routePath = '/' + routePath;
    }

    path += '#' + routePath;
  }

  return path;
}
<commit_msg>Fewer string allocations<commit_after>import Ember from "ember-metal/core"; // FEATURES
import { get } from "ember-metal/property_get";
import { set } from "ember-metal/property_set";
import { tryInvoke } from "ember-metal/utils";

import EmberObject from "ember-runtime/system/object";
import environment from "ember-metal/environment";

import {
  supportsHashChange,
  supportsHistory,
  getPath,
  getHash,
  getQuery,
  getFullPath,
  replacePath
} from "ember-routing/location/util";

/**
@module ember
@submodule ember-routing
*/

/**
  Ember.AutoLocation will select the best location option based off browser
  support with the priority order: history, hash, none.

  Clean pushState paths accessed by hashchange-only browsers will be redirected
  to the hash-equivalent and vice versa so future transitions are consistent.

  Keep in mind that since some of your users will use `HistoryLocation`, your
  server must serve the Ember app at all the routes you define.

  @class AutoLocation
  @namespace Ember
  @static
*/
export default EmberObject.extend({
  /**
    @private

    The browser's `location` object. This is typically equivalent to
    `window.location`, but may be overridden for testing.

    @property location
    @default environment.location
  */
  location: environment.location,

  /**
    @private

    The browser's `history` object. This is typically equivalent to
    `window.history`, but may be overridden for testing.

    @since 1.5.1
    @property history
    @default environment.history
  */
  history: environment.history,

  /**
   @private

   The user agent's global variable. In browsers, this will be `window`.

   @since 1.11
   @property global
   @default environment.global
  */
  global: environment.global,

  /**
    @private

    The browser's `userAgent`. This is typically equivalent to
    `navigator.userAgent`, but may be overridden for testing.

    @since 1.5.1
    @property userAgent
    @default environment.history
  */
  userAgent: environment.userAgent,

  /**
    @private

    This property is used by the router to know whether to cancel the routing
    setup process, which is needed while we redirect the browser.

    @since 1.5.1
    @property cancelRouterSetup
    @default false
  */
  cancelRouterSetup: false,

  /**
    @private

    Will be pre-pended to path upon state change.

    @since 1.5.1
    @property rootURL
    @default '/'
  */
  rootURL: '/',

  /**
   Called by the router to instruct the location to do any feature detection
   necessary. In the case of AutoLocation, we detect whether to use history
   or hash concrete implementations.
  */
  detect: function() {
    var rootURL = this.rootURL;

    Ember.assert('rootURL must end with a trailing forward slash e.g. "/app/"',
                 rootURL.charAt(rootURL.length-1) === '/');

    var implementation = detectImplementation({
      location: this.location,
      history: this.history,
      userAgent: this.userAgent,
      rootURL: rootURL,
      documentMode: this.documentMode,
      global: this.global
    });

    if (implementation === false) {
      set(this, 'cancelRouterSetup', true);
      implementation = 'none';
    }

    var concrete = this.container.lookup('location:' + implementation);
    Ember.assert("Could not find location '" + implementation + "'.", !!concrete);

    set(this, 'concreteImplementation', concrete);
  },

  initState: delegateToConcreteImplementation('initState'),
  getURL: delegateToConcreteImplementation('getURL'),
  setURL: delegateToConcreteImplementation('setURL'),
  replaceURL: delegateToConcreteImplementation('replaceURL'),
  onUpdateURL: delegateToConcreteImplementation('onUpdateURL'),
  formatURL: delegateToConcreteImplementation('formatURL'),

  willDestroy: function() {
    var concreteImplementation = get(this, 'concreteImplementation');

    if (concreteImplementation) {
      concreteImplementation.destroy();
    }
  }
});

function delegateToConcreteImplementation(methodName) {
  return function() {
    var concreteImplementation = get(this, 'concreteImplementation');
    Ember.assert("AutoLocation's detect() method should be called before calling any other hooks.", !!concreteImplementation);
    tryInvoke(concreteImplementation, methodName, arguments);
  };
}

/**
  Given the browser's `location`, `history` and `userAgent`, and a configured
  root URL, this function detects whether the browser supports the [History
  API](https://developer.mozilla.org/en-US/docs/Web/API/History) and returns a
  string representing the Location object to use based on its determination.

  For example, if the page loads in an evergreen browser, this function would
  return the string "history", meaning the history API and thus HistoryLocation
  should be used. If the page is loaded in IE8, it will return the string
  "hash," indicating that the History API should be simulated by manipulating the
  hash portion of the location.

*/

function detectImplementation(options) {
  var location = options.location,
      userAgent = options.userAgent,
      history = options.history,
      documentMode = options.documentMode,
      global = options.global,
      rootURL = options.rootURL;

  var implementation = 'none';
  var cancelRouterSetup = false;
  var currentPath = getFullPath(location);

  if (supportsHistory(userAgent, history)) {
    var historyPath = getHistoryPath(rootURL, location);

    // If the browser supports history and we have a history path, we can use
    // the history location with no redirects.
    if (currentPath === historyPath) {
      return 'history';
    } else {
      if (currentPath.substr(0, 2) === '/#') {
        history.replaceState({ path: historyPath }, null, historyPath);
        implementation = 'history';
      } else {
        cancelRouterSetup = true;
        replacePath(location, historyPath);
      }
    }
  } else if (supportsHashChange(documentMode, global)) {
    var hashPath = getHashPath(rootURL, location);

    // Be sure we're using a hashed path, otherwise let's switch over it to so
    // we start off clean and consistent. We'll count an index path with no
    // hash as "good enough" as well.
    if (currentPath === hashPath || (currentPath === '/' && hashPath === '/#/')) {
      implementation = 'hash';
    } else {
      // Our URL isn't in the expected hash-supported format, so we want to
      // cancel the router setup and replace the URL to start off clean
      cancelRouterSetup = true;
      replacePath(location, hashPath);
    }
  }

  if (cancelRouterSetup) {
    return false;
  }

  return implementation;
}

/**
  @private

  Returns the current path as it should appear for HistoryLocation supported
  browsers. This may very well differ from the real current path (e.g. if it
  starts off as a hashed URL)
*/
export function getHistoryPath(rootURL, location) {
  var path = getPath(location);
  var hash = getHash(location);
  var query = getQuery(location);
  var rootURLIndex = path.indexOf(rootURL);
  var routeHash, hashParts;

  Ember.assert('Path ' + path + ' does not start with the provided rootURL ' + rootURL, rootURLIndex === 0);

  // By convention, Ember.js routes using HashLocation are required to start
  // with `#/`. Anything else should NOT be considered a route and should
  // be passed straight through, without transformation.
  if (hash.substr(0, 2) === '#/') {
    // There could be extra hash segments after the route
    hashParts = hash.substr(1).split('#');
    // The first one is always the route url
    routeHash = hashParts.shift();

    // If the path already has a trailing slash, remove the one
    // from the hashed route so we don't double up.
    if (path.slice(-1) === '/') {
      routeHash = routeHash.substr(1);
    }

    // This is the "expected" final order
    path = path + routeHash + query;

    if (hashParts.length) {
      path += '#' + hashParts.join('#');
    }
  } else {
    path = path + query + hash;
  }

  return path;
}

/**
  @private

  Returns the current path as it should appear for HashLocation supported
  browsers. This may very well differ from the real current path.

  @method _getHashPath
*/
export function getHashPath(rootURL, location) {
  var path = rootURL;
  var historyPath = getHistoryPath(rootURL, location);
  var routePath = historyPath.substr(rootURL.length);

  if (routePath !== '') {
    if (routePath.charAt(0) !== '/') {
      routePath = '/' + routePath;
    }

    path += '#' + routePath;
  }

  return path;
}
<|endoftext|>


<commit_before>import * as React from 'react';
import { expect } from 'chai';
import { spy, useFakeTimers } from 'sinon';
import {
  findOutermostIntrinsic,
  getClasses,
  wrapsIntrinsicElement,
  createMount,
  createClientRender,
  act,
  fireEvent,
  describeConformance,
} from 'test/utils';
import Icon from '@material-ui/core/Icon';
import Fab from '@material-ui/core/Fab';
import SpeedDial from './SpeedDial';
import SpeedDialAction from '../SpeedDialAction';

describe('<SpeedDial />', () => {
  // StrictModeViolation: not using act(), prefer test/utils/createClientRender
  const mount = createMount({ strict: false });
  const render = createClientRender({ strict: false });
  let classes;

  const icon = <Icon>font_icon</Icon>;
  const FakeAction = () =>
;
  const defaultProps = {
    open: true,
    icon,
    ariaLabel: 'mySpeedDial',
  };

  before(() => {
    classes = getClasses(
      <SpeedDial {...defaultProps}>

      </SpeedDial>,
    );
  });

  describeConformance(<SpeedDial {...defaultProps} />, () => ({
    classes,
    inheritComponent: 'div',
    mount,
    refInstanceof: window.HTMLDivElement,
    skip: [
      'componentProp', // react-transition-group issue
      'reactTestRenderer',
    ],
  }));

  it('should render a Fade transition', () => {
    const wrapper = mount(
      <SpeedDial {...defaultProps}>
        <FakeAction />
      </SpeedDial>,
    );
    expect(findOutermostIntrinsic(wrapper).type()).to.equal('div');
  });

  it('should render a Fab', () => {
    const wrapper = mount(
      <SpeedDial {...defaultProps}>
        <FakeAction />
      </SpeedDial>,
    );
    const buttonWrapper = wrapper.find('[aria-expanded]').first();
    expect(buttonWrapper.type()).to.equal(Fab);
  });

  it('should render with a null child', () => {
    const wrapper = mount(
      <SpeedDial {...defaultProps}>
        <SpeedDialAction icon={icon} tooltipTitle="One" />
        {null}
        <SpeedDialAction icon={icon} tooltipTitle="Three" />
      </SpeedDial>,
    );
    expect(wrapper.find(SpeedDialAction).length).to.equal(2);
  });

  it('should pass the open prop to its children', () => {
    const actionClasses = { fabClosed: 'is-closed' };
    const wrapper = mount(
      <SpeedDial {...defaultProps}>
        <SpeedDialAction classes={actionClasses} icon={icon} tooltipTitle="SpeedDialAction1" />
        <SpeedDialAction classes={actionClasses} icon={icon} tooltipTitle="SpeedDialAction2" />
      </SpeedDial>,
    );
    const actions = wrapper.find('[role="menuitem"]').filterWhere(wrapsIntrinsicElement);
    expect(actions.some('.is-closed')).to.equal(false);
  });

  describe('prop: onKeyDown', () => {
    it('should be called when a key is pressed', () => {
      const handleKeyDown = spy();
      const wrapper = mount(
        <SpeedDial {...defaultProps} onKeyDown={handleKeyDown}>
          <FakeAction />
        </SpeedDial>,
      );
      const buttonWrapper = wrapper.find('[aria-expanded]').first();
      const eventMock = 'something-to-match';
      buttonWrapper.simulate('keyDown', {
        key: ' ',
        eventMock,
      });
      expect(handleKeyDown.callCount).to.equal(1);
      expect(handleKeyDown.calledWithMatch({ eventMock })).to.equal(true);
    });
  });

  describe('prop: direction', () => {
    [
      ['up', 'directionUp'],
      ['down', 'directionDown'],
      ['left', 'directionLeft'],
      ['right', 'directionRight'],
    ].forEach(([direction, className]) => {
      it(`should place actions in the correct position when direction=${direction}`, () => {
        const wrapper = mount(
          <SpeedDial {...defaultProps} direction={direction.toLowerCase()}>
            <SpeedDialAction icon={icon} tooltipTitle="action1" />
            <SpeedDialAction icon={icon} tooltipTitle="action2" />
          </SpeedDial>,
        );
        expect(findOutermostIntrinsic(wrapper).hasClass(classes[className])).to.equal(true);
      });
    });
  });

  describe('keyboard', () => {
    let clock;

    beforeEach(() => {
      clock = useFakeTimers();
    });

    afterEach(() => {
      clock.restore();
    });

    it('should open the speed dial and move to the first action without closing', () => {
      const handleOpen = spy();
      const { getByRole, getAllByRole } = render(
        <SpeedDial ariaLabel="mySpeedDial" onOpen={handleOpen}>
          <SpeedDialAction tooltipTitle="action1" />
          <SpeedDialAction tooltipTitle="action2" />
        </SpeedDial>,
      );
      const fab = getByRole('button');
      fab.focus();
      act(() => {
        clock.tick();
      });
      expect(handleOpen.callCount).to.equal(1);
      const actions = getAllByRole('menuitem');
      expect(actions.length).to.equal(2);
      fireEvent.keyDown(fab, { key: 'ArrowUp' });
      expect(document.activeElement).to.equal(actions[0]);
      expect(fab).to.have.attribute('aria-expanded', 'true');
    });
  });

  describe('dial focus', () => {
    let actionRefs;
    let dialButtonRef;
    let onkeydown;
    let wrapper;

    const mountSpeedDial = (direction = 'up', actionCount = 4) => {
      actionRefs = [];
      dialButtonRef = undefined;
      onkeydown = spy();

      wrapper = mount(
        <SpeedDial
          {...defaultProps}
          FabProps={{
            ref: (ref) => {
              dialButtonRef = ref;
            },
          }}
          direction={direction}
          onKeyDown={onkeydown}
        >
          {Array.from({ length: actionCount }, (_, i) => (
            <SpeedDialAction
              key={i}
              FabProps={{
                ref: (ref) => {
                  actionRefs[i] = ref;
                },
              }}
              icon={icon}
              data-test={i}
              tooltipTitle={`action${i}`}
            />
          ))}
        </SpeedDial>,
      );
    };

    /**
     * @returns the button of SpeedDial
     */
    const getDialButton = () => wrapper.find('[aria-controls]').first();
    /**
     *
     * @param actionIndex
     * @returns the button of the nth SpeedDialAction or the Fab if -1
     */
    const getActionButton = (actionIndex) => {
      if (actionIndex === -1) {
        return getDialButton();
      }
      return wrapper.find(SpeedDialAction).at(actionIndex).find(Fab);
    };
    /**
     * @returns true if the button of the nth action is focused
     */
    const isActionFocused = (index) => {
      const expectedFocusedElement = index === -1 ? dialButtonRef : actionRefs[index];
      return expectedFocusedElement === window.document.activeElement;
    };

    const resetDialToOpen = (direction) => {
      if (wrapper && wrapper.exists()) {
        wrapper.unmount();
      }

      mountSpeedDial(direction);
      dialButtonRef.focus();
    };

    it('displays the actions on focus gain', () => {
      resetDialToOpen();
      expect(wrapper.find(SpeedDial).props().open).to.equal(true);
    });

    describe('first item selection', () => {
      it('considers arrow keys with the same initial orientation', () => {
        resetDialToOpen();
        getDialButton().simulate('keydown', { key: 'left' });
        expect(isActionFocused(0)).to.equal(true);
        getDialButton().simulate('keydown', { key: 'up' });
        expect(isActionFocused(0)).to.equal(true);
        getDialButton().simulate('keydown', { key: 'left' });
        expect(isActionFocused(1)).to.equal(true);
        getDialButton().simulate('keydown', { key: 'right' });
        expect(isActionFocused(0)).to.equal(true);
      });
    });

    // eslint-disable-next-line func-names
    describe('actions navigation', function () {
      this.timeout(5000); // These tests are really slow.

      /**
       * tests a combination of arrow keys on a focused SpeedDial
       */
      const testCombination = async (
        dialDirection,
        [firstKey, ...combination],
        [firstFocusedAction, ...foci],
      ) => {
        resetDialToOpen(dialDirection);

        getDialButton().simulate('keydown', { key: firstKey });
        expect(isActionFocused(firstFocusedAction)).to.equal(
          true,
          `focused action initial ${firstKey} should be ${firstFocusedAction}`,
        );

        combination.forEach((arrowKey, i) => {
          const previousFocusedAction = foci[i - 1] || firstFocusedAction;
          const expectedFocusedAction = foci[i];
          const combinationUntilNot = [firstKey, ...combination.slice(0, i + 1)];

          getActionButton(previousFocusedAction).simulate('keydown', {
            key: arrowKey,
          });
          expect(isActionFocused(expectedFocusedAction)).to.equal(
            true,
            `focused action after ${combinationUntilNot.join(
              ',',
            )} should be ${expectedFocusedAction}`,
          );
        });
      };

      it('considers the first arrow key press as forward navigation', async () => {
        await testCombination('up', ['ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowDown'], [0, 1, 2, 1]);
        await testCombination(
          'up',
          ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'],
          [0, 1, 2, 1],
        );

        await testCombination(
          'right',
          ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, 1, 2, 1],
        );
        await testCombination(
          'right',
          ['ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, 1, 2, 1],
        );

        await testCombination(
          'down',
          ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'],
          [0, 1, 2, 1],
        );
        await testCombination('down', ['ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowDown'], [0, 1, 2, 1]);

        await testCombination(
          'left',
          ['ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, 1, 2, 1],
        );
        await testCombination(
          'left',
          ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, 1, 2, 1],
        );
      });

      it('ignores array keys orthogonal to the direction', async () => {
        await testCombination(
          'up',
          ['ArrowUp', 'ArrowLeft', 'ArrowRight', 'ArrowUp'],
          [0, 0, 0, 1],
        );
        await testCombination(
          'right',
          ['ArrowRight', 'ArrowUp', 'ArrowDown', 'ArrowRight'],
          [0, 0, 0, 1],
        );
        await testCombination(
          'down',
          ['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowDown'],
          [0, 0, 0, 1],
        );
        await testCombination(
          'left',
          ['ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowLeft'],
          [0, 0, 0, 1],
        );
      });

      it('does not wrap around', async () => {
        await testCombination(
          'up',
          ['ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowUp'],
          [0, -1, -1, 0],
        );
        await testCombination(
          'right',
          ['ArrowRight', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, -1, -1, 0],
        );
        await testCombination(
          'down',
          ['ArrowDown', 'ArrowUp', 'ArrowUp', 'ArrowDown'],
          [0, -1, -1, 0],
        );
        await testCombination(
          'left',
          ['ArrowLeft', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, -1, -1, 0],
        );
      });
    });
  });
});
<commit_msg>[test] Migrate SpeedDial to react-testing-library (#23021)<commit_after>import * as React from 'react';
import { expect } from 'chai';
import { spy, useFakeTimers } from 'sinon';
import {
  getClasses,
  createMount,
  createClientRender,
  act,
  fireEvent,
  screen,
  describeConformance,
} from 'test/utils';
import Icon from '@material-ui/core/Icon';
import SpeedDial from './SpeedDial';
import SpeedDialAction from '../SpeedDialAction';

describe('<SpeedDial />', () => {
  // StrictModeViolation: not using act(), prefer test/utils/createClientRender
  const mount = createMount({ strict: false });
  const render = createClientRender({ strict: false });
  let classes;

  const icon = <Icon>font_icon</Icon>;
  const FakeAction = () =>
;
  const defaultProps = {
    open: true,
    icon,
    ariaLabel: 'mySpeedDial',
  };

  before(() => {
    classes = getClasses(
      <SpeedDial {...defaultProps}>

      </SpeedDial>,
    );
  });

  describeConformance(<SpeedDial {...defaultProps} />, () => ({
    classes,
    inheritComponent: 'div',
    mount,
    refInstanceof: window.HTMLDivElement,
    skip: [
      'componentProp', // react-transition-group issue
      'reactTestRenderer',
    ],
  }));

  it('should render a Fade transition', () => {
    const { container } = render(
      <SpeedDial {...defaultProps}>
        <FakeAction />
      </SpeedDial>,
    );

    expect(container.firstChild.tagName).to.equal('DIV');
  });

  it('should render a Fab', () => {
    const { getByRole } = render(
      <SpeedDial {...defaultProps}>
        <FakeAction />
      </SpeedDial>,
    );
    expect(getByRole('button', { expanded: true })).not.to.equal(null);
  });

  it('should render with a null child', () => {
    const { getByRole, getAllByRole } = render(
      <SpeedDial {...defaultProps}>
        <SpeedDialAction icon={icon} tooltipTitle="One" />
        {null}
        <SpeedDialAction icon={icon} tooltipTitle="Three" />
      </SpeedDial>,
    );
    expect(getByRole('menu').children).to.have.lengthOf(2);
    expect(getAllByRole('menuitem')).to.have.lengthOf(2);
  });

  it('should pass the open prop to its children', () => {
    const actionClasses = { fabClosed: 'is-closed' };
    const { getAllByRole } = render(
      <SpeedDial {...defaultProps}>
        <SpeedDialAction classes={actionClasses} icon={icon} tooltipTitle="SpeedDialAction1" />
        <SpeedDialAction classes={actionClasses} icon={icon} tooltipTitle="SpeedDialAction2" />
      </SpeedDial>,
    );
    const actions = getAllByRole('menuitem');
    expect(actions).to.have.lengthOf(2);
    expect(actions.map((element) => element.className)).not.to.contain('is-closed');
  });

  describe('prop: onKeyDown', () => {
    it('should be called when a key is pressed', () => {
      const handleKeyDown = spy();
      const { getByRole } = render(
        <SpeedDial {...defaultProps} onKeyDown={handleKeyDown}>
          <FakeAction />
        </SpeedDial>,
      );
      const buttonWrapper = getByRole('button', { expanded: true });
      act(() => {
        fireEvent.keyDown(document.body, { key: 'TAB' });
        buttonWrapper.focus();
      });
      fireEvent.keyDown(buttonWrapper, { key: ' ' });
      expect(handleKeyDown.callCount).to.equal(1);
      expect(handleKeyDown.calledWithMatch({ key: ' ' })).to.equal(true);
    });
  });

  describe('prop: direction', () => {
    [
      ['up', 'directionUp'],
      ['down', 'directionDown'],
      ['left', 'directionLeft'],
      ['right', 'directionRight'],
    ].forEach(([direction, className]) => {
      it(`should place actions in the correct position when direction=${direction}`, () => {
        const { getByRole } = render(
          <SpeedDial {...defaultProps} direction={direction.toLowerCase()}>
            <SpeedDialAction icon={icon} tooltipTitle="action1" />
            <SpeedDialAction icon={icon} tooltipTitle="action2" />
          </SpeedDial>,
        );
        expect(getByRole('presentation')).to.have.class(classes[className]);
      });
    });
  });

  describe('keyboard', () => {
    let clock;

    beforeEach(() => {
      clock = useFakeTimers();
    });

    afterEach(() => {
      clock.restore();
    });

    it('should open the speed dial and move to the first action without closing', () => {
      const handleOpen = spy();
      const { getByRole, getAllByRole } = render(
        <SpeedDial ariaLabel="mySpeedDial" onOpen={handleOpen}>
          <SpeedDialAction tooltipTitle="action1" />
          <SpeedDialAction tooltipTitle="action2" />
        </SpeedDial>,
      );
      const fab = getByRole('button');
      fab.focus();
      act(() => {
        clock.tick();
      });
      expect(handleOpen.callCount).to.equal(1);
      const actions = getAllByRole('menuitem');
      expect(actions.length).to.equal(2);
      fireEvent.keyDown(fab, { key: 'ArrowUp' });
      expect(document.activeElement).to.equal(actions[0]);
      expect(fab).to.have.attribute('aria-expanded', 'true');
    });
  });

  describe('dial focus', () => {
    let actionButtons;
    let fabButton;

    const renderSpeedDial = (direction = 'up', actionCount = 4) => {
      actionButtons = [];
      fabButton = undefined;

      render(
        <SpeedDial
          {...defaultProps}
          FabProps={{
            ref: (element) => {
              fabButton = element;
            },
          }}
          direction={direction}
        >
          {Array.from({ length: actionCount }, (_, index) => (
            <SpeedDialAction
              key={index}
              FabProps={{
                ref: (element) => {
                  actionButtons[index] = element;
                },
              }}
              icon={icon}
              tooltipTitle={`action${index}`}
            />
          ))}
        </SpeedDial>,
      );
      fabButton.focus();
    };

    /**
     *
     * @param actionIndex
     * @returns the button of the nth SpeedDialAction or the Fab if -1
     */
    const getActionButton = (actionIndex) => {
      if (actionIndex === -1) {
        return fabButton;
      }
      return actionButtons[actionIndex];
    };
    /**
     * @returns true if the button of the nth action is focused
     */
    const isActionFocused = (index) => {
      const expectedFocusedElement = index === -1 ? fabButton : actionButtons[index];
      return expectedFocusedElement === document.activeElement;
    };

    it('displays the actions on focus gain', () => {
      renderSpeedDial();
      expect(screen.getAllByRole('menuitem')).to.have.lengthOf(4);
      expect(screen.getByRole('menu')).not.to.have.class(classes.actionsClosed);
    });

    it('considers arrow keys with the same initial orientation', () => {
      renderSpeedDial();
      fireEvent.keyDown(fabButton, { key: 'left' });
      expect(isActionFocused(0)).to.equal(true);
      fireEvent.keyDown(getActionButton(0), { key: 'up' });
      expect(isActionFocused(0)).to.equal(true);
      fireEvent.keyDown(getActionButton(0), { key: 'left' });
      expect(isActionFocused(1)).to.equal(true);
      fireEvent.keyDown(getActionButton(1), { key: 'right' });
      expect(isActionFocused(0)).to.equal(true);
    });

    describe('actions navigation', () => {
      /**
       * tests a combination of arrow keys on a focused SpeedDial
       */
      const itTestCombination = (dialDirection, keys, expected) => {
        it(`start dir ${dialDirection} with keys ${keys.join(',')}`, () => {
          const [firstKey, ...combination] = keys;
          const [firstFocusedAction, ...foci] = expected;

          renderSpeedDial(dialDirection);

          fireEvent.keyDown(fabButton, { key: firstKey });
          expect(isActionFocused(firstFocusedAction)).to.equal(
            true,
            `focused action initial ${firstKey} should be ${firstFocusedAction}`,
          );

          combination.forEach((arrowKey, i) => {
            const previousFocusedAction = foci[i - 1] || firstFocusedAction;
            const expectedFocusedAction = foci[i];
            const combinationUntilNot = [firstKey, ...combination.slice(0, i + 1)];

            fireEvent.keyDown(getActionButton(previousFocusedAction), {
              key: arrowKey,
            });
            expect(isActionFocused(expectedFocusedAction)).to.equal(
              true,
              `focused action after ${combinationUntilNot.join(
                ',',
              )} should be ${expectedFocusedAction}`,
            );
          });
        });
      };

      describe('considers the first arrow key press as forward navigation', () => {
        itTestCombination('up', ['ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowDown'], [0, 1, 2, 1]);
        itTestCombination('up', ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'], [0, 1, 2, 1]);

        itTestCombination(
          'right',
          ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, 1, 2, 1],
        );
        itTestCombination(
          'right',
          ['ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, 1, 2, 1],
        );

        itTestCombination('down', ['ArrowDown', 'ArrowDown', 'ArrowDown', 'ArrowUp'], [0, 1, 2, 1]);
        itTestCombination('down', ['ArrowUp', 'ArrowUp', 'ArrowUp', 'ArrowDown'], [0, 1, 2, 1]);

        itTestCombination(
          'left',
          ['ArrowLeft', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, 1, 2, 1],
        );
        itTestCombination(
          'left',
          ['ArrowRight', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, 1, 2, 1],
        );
      });

      describe('ignores array keys orthogonal to the direction', () => {
        itTestCombination('up', ['ArrowUp', 'ArrowLeft', 'ArrowRight', 'ArrowUp'], [0, 0, 0, 1]);
        itTestCombination(
          'right',
          ['ArrowRight', 'ArrowUp', 'ArrowDown', 'ArrowRight'],
          [0, 0, 0, 1],
        );
        itTestCombination(
          'down',
          ['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowDown'],
          [0, 0, 0, 1],
        );
        itTestCombination('left', ['ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowLeft'], [0, 0, 0, 1]);
      });

      describe('does not wrap around', () => {
        itTestCombination('up', ['ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowUp'], [0, -1, -1, 0]);
        itTestCombination(
          'right',
          ['ArrowRight', 'ArrowLeft', 'ArrowLeft', 'ArrowRight'],
          [0, -1, -1, 0],
        );
        itTestCombination('down', ['ArrowDown', 'ArrowUp', 'ArrowUp', 'ArrowDown'], [0, -1, -1, 0]);
        itTestCombination(
          'left',
          ['ArrowLeft', 'ArrowRight', 'ArrowRight', 'ArrowLeft'],
          [0, -1, -1, 0],
        );
      });
    });
  });
});
<|endoftext|>


<commit_before>var test = require("tape");
var PicoQueue = require("./PicoQueue");

test("PicoQueue", function(t){

    var log = [];

    var pq = PicoQueue(function(pico_id, data, callback){
        log.push("working_0 [" + pico_id + "] " + data);
        process.nextTick(function(){
            log.push("working_1 [" + pico_id + "] " + data);
            process.nextTick(function(){
                log.push("working_2 [" + pico_id + "] " + data);
                callback();
            });
        });
    });

    var enqueue = function(pico_id, data, done){
        log.push("enqueue [" + pico_id + "] " + data);
        pq.enqueue(pico_id, data, function(){
            log.push("done [" + pico_id + "] " + data);
            if(done){
                done();
            }
        });
    };

    enqueue("A", 0);
    enqueue("A", 1);
    enqueue("B", 0);
    enqueue("A", 2, function(){
        t.deepEquals(log, [
            "enqueue [A] 0",
            "enqueue [A] 1",
            "enqueue [B] 0",
            "enqueue [A] 2",
            "working_0 [A] 0",
            "working_0 [B] 0",//different picos can be concurrent
            "working_1 [A] 0",
            "working_1 [B] 0",
            "working_2 [A] 0",//Now pico A finished work on event 0
            "working_2 [B] 0",
            "working_0 [A] 1",//Now pico A can start on event 1
            "done [A] 0",
            "done [B] 0",
            "working_1 [A] 1",
            "working_2 [A] 1",
            "working_0 [A] 2",
            "done [A] 1",
            "working_1 [A] 2",
            "working_2 [A] 2",
            "done [A] 2"
        ]);
        t.end();
    });
});
<commit_msg>testing PicoQueue error handling<commit_after>var test = require("tape");
var PicoQueue = require("./PicoQueue");

test("PicoQueue", function(t){

    var log = [];

    var pq = PicoQueue(function(pico_id, data, callback){
        log.push("working_0 [" + pico_id + "] " + data);
        process.nextTick(function(){
            log.push("working_1 [" + pico_id + "] " + data);
            process.nextTick(function(){
                log.push("working_2 [" + pico_id + "] " + data);
                callback();
            });
        });
    });

    var enqueue = function(pico_id, data, done){
        log.push("enqueue [" + pico_id + "] " + data);
        pq.enqueue(pico_id, data, function(){
            log.push("done [" + pico_id + "] " + data);
            if(done){
                done();
            }
        });
    };

    enqueue("A", 0);
    enqueue("A", 1);
    enqueue("B", 0);
    enqueue("A", 2, function(){
        t.deepEquals(log, [
            "enqueue [A] 0",
            "enqueue [A] 1",
            "enqueue [B] 0",
            "enqueue [A] 2",
            "working_0 [A] 0",
            "working_0 [B] 0",//different picos can be concurrent
            "working_1 [A] 0",
            "working_1 [B] 0",
            "working_2 [A] 0",//Now pico A finished work on event 0
            "working_2 [B] 0",
            "working_0 [A] 1",//Now pico A can start on event 1
            "done [A] 0",
            "done [B] 0",
            "working_1 [A] 1",
            "working_2 [A] 1",
            "working_0 [A] 2",
            "done [A] 1",
            "working_1 [A] 2",
            "working_2 [A] 2",
            "done [A] 2"
        ]);
        t.end();
    });
});

test("PicoQueue - error", function(t){
    var pq = PicoQueue(function(pico_id, data, callback){
        process.nextTick(function(){
            if(data === "foobar"){
                callback("ERROR: foobar");
                return;
            }
            callback(null, data);
        });
    });
    t.plan(6);
    pq.enqueue("A", "baz", function(err, data){
        t.equals(err, null);
        t.equals(data, "baz");
    });
    pq.enqueue("A", "foobar", function(err, data){
        t.equals(err, "ERROR: foobar");
        t.equals(data, void 0);
    });
    pq.enqueue("A", "qux", function(err, data){
        t.equals(err, null);
        t.equals(data, "qux");
    });
});
<|endoftext|>