<commit_before>import {
  ModifyCategoryModal, __GetDependency__
} from '../../../../../client-src/categories/components/modify-category-modal';
import generateWrapperGenerator from '../../../../services/generate-wrapper-generator';

const validate = __GetDependency__('validate');

describe('ModifyCategoryModal', function() {
  describe('validate', () => {
    describe('"empty" errors', () => {
      it('should error when the label is an empty string', () => {
        const errors = validate(
          {label: ''},
          {categories: []}
        );
        expect(errors.label).to.equal('empty');
      });

      it('should error when the label is only whitespace', () => {
        const errors = validate(
          {label: '   '},
          {categories: []}
        );
        expect(errors.label).to.equal('empty');
      });

      it('should not error when there is text', () => {
        const errors = validate(
          {label: ' a   '},
          {categories: []}
        );
        expect(errors).to.be.empty;
      });
    });

    describe('duplicate', () => {
      it('should error when the label matches another', () => {
        const errors = validate(
          {label: 'sandwich'},
          {
            category: {id: 1, label: 'pizza'},
            categories: [
              {id: 3, label: 'sandwich'},
              {id: 4, label: 'pasta'},
            ]
          }
        );
        expect(errors.label).to.equal('duplicate');
      });

      it('should not error when the label matches itself', () => {
        const errors = validate(
          {label: 'sandwich'},
          {
            category: {id: 1, label: 'sandwich'},
            categories: [
              {id: 1, label: 'sandwich'},
              {id: 3, label: 'pizza'},
              {id: 4, label: 'pasta'},
            ]
          }
        );
        expect(errors).to.be.empty;
      });
    });
  });

  describe('rendering with `isEditMode` false', () => {
    beforeEach(() => {
      this.handleSubmit = stub();
      this.onClickCancel = stub();

      this.defaultProps = {
        fields: {
          label: {}
        },
        handleSubmit: this.handleSubmit,
        confirmInProgress: false,
        onClickCancel: this.onClickCancel,
        isEditMode: false
      };

      this.generator = generateWrapperGenerator(this.defaultProps, ModifyCategoryModal);
    });

    it('should have the right class name', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.hasClass('createCategoryModal')).to.be.true;
    });

    it('should have the right modal title when edit mode is false', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.find('.modal-title').text()).to.equal('New category');
    });

    it('should have the right modal title when edit mode is true', () => {
      const wrapper = this.generator.shallow({isEditMode: true});
      expect(wrapper.find('.modal-title').text()).to.equal('Edit category');
    });

    it('should not render an error', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.find('.modal-error')).to.have.length(0);
    });

    it('should focus the label on mount', () => {
      const wrapper = this.generator.shallow();
      const focusStub = stub();
      wrapper.instance().labelInput = {
        focus: focusStub
      };
      wrapper.instance().componentDidMount();
      expect(focusStub).to.have.been.calledOnce;
    });

    it('should not render an error when the label has not been touched', () => {
      const props = {
        fields: {
          label: {
            error: 'sorry',
            touched: false
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      expect(wrapper.find('.modal-error')).to.have.length(0);
    });

    it('should render an empty error when the label has an "empty" error', () => {
      const props = {
        fields: {
          label: {
            error: 'empty',
            touched: true
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      const errorEl = wrapper.find('.modal-error');
      expect(errorEl).to.have.length(1);
      expect(errorEl.text()).to.equal('A name is required');
    });

    it('should render a duplicate error when the label has an "duplicate" error', () => {
      const props = {
        fields: {
          label: {
            error: 'duplicate',
            touched: true
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      const errorEl = wrapper.find('.modal-error');
      expect(errorEl).to.have.length(1);
      expect(errorEl.text()).to.equal('Category already exists');
    });

    describe('the form', () => {
      it('should contain an emoji picker', () => {
        const form = this.generator.shallow().find('form');
        expect(form.find('.createCategoryModal-emojiSelect')).to.have.length(1);
      });

      it('should contain a label input with the right props', () => {
        const form = this.generator.shallow().find('form');
        const labelInput = form.find('.newCategoryName');
        expect(labelInput).to.have.length(1);
        expect(labelInput.hasClass('text-input')).to.be.true;
        expect(labelInput.hasClass('invalid-input')).to.be.false;
        expect(labelInput.prop('type')).to.equal('text');
        expect(labelInput.prop('placeholder')).to.equal('Enter name');
        expect(labelInput.prop('disabled')).to.be.falsey;
      });

      it('should have a disabled input when a creation is in progress', () => {
        const props = {confirmInProgress: true};
        const form = this.generator.shallow(props).find('form');
        const labelInput = form.find('.newCategoryName');
        expect(labelInput.prop('disabled')).to.be.true;
      });

      it('should call preventDefault and `handleSubmit` when submitted', () => {
        const form = this.generator.shallow().find('form');
        const preventDefault = stub();
        form.simulate('submit', {preventDefault});
        expect(preventDefault).to.have.been.calledOnce;
        expect(this.handleSubmit).to.have.been.calledOnce;
      });

      describe('cancel button', () => {
        it('should exist', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn).to.have.length(1);
        });

        it('should not be disabled when no confirm is in progress', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.falsey;
        });

        it('should not be disabled even when there is an error', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: true
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.falsey;
        });

        it('should be disabled when a creation is in progress', () => {
          const props = {confirmInProgress: true};
          const wrapper = this.generator.shallow(props);
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.true;
        });

        it('should have the right text', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.text()).to.equal('Cancel');
        });

        it('should preventDefault and call `onClickCancel` when clicked', () => {
          const preventDefault = stub();
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          cancelBtn.simulate('click', {preventDefault});
          expect(preventDefault).to.have.been.calledOnce;
          expect(this.onClickCancel).to.have.been.calledOnce;
        });
      });

      describe('confirm button', () => {
        it('should exist', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn).to.have.length(1);
        });

        it('should be the submit button', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('type')).to.equal('submit');
        });

        it('should not be disabled when there are no errors & no save in progress', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.falsey;
        });

        it('should not be disabled when there is an error but the form has not been touched', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: false
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.falsey;
        });

        it('should be disabled when there is an error and the form has been touched', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: true
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.true;
        });

        it('should be disabled when a creation is in progress', () => {
          const props = {confirmInProgress: true};
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.true;
        });

        it('should have the right text', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Create');
        });

        it('should have the right text when edit mode is true', () => {
          const wrapper = this.generator.shallow({isEditMode: true});
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Edit');
        });

        it('should have the right text when a create is in progress', () => {
          const wrapper = this.generator.shallow({confirmInProgress: true});
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Creating...');
        });

        it('should have the right text when a create is in progress in edit mode', () => {
          const props = {
            confirmInProgress: true,
            isEditMode: true
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Editing...');
        });
      });
    });
  });
});
<commit_msg>Fix test for create new category modal<commit_after>import {
  ModifyCategoryModal, __GetDependency__
} from '../../../../../client-src/categories/components/modify-category-modal';
import generateWrapperGenerator from '../../../../services/generate-wrapper-generator';

const validate = __GetDependency__('validate');

describe('ModifyCategoryModal', function() {
  describe('validate', () => {
    describe('"empty" errors', () => {
      it('should error when the label is an empty string', () => {
        const errors = validate(
          {label: ''},
          {categories: []}
        );
        expect(errors.label).to.equal('empty');
      });

      it('should error when the label is only whitespace', () => {
        const errors = validate(
          {label: '   '},
          {categories: []}
        );
        expect(errors.label).to.equal('empty');
      });

      it('should not error when there is text', () => {
        const errors = validate(
          {label: ' a   '},
          {categories: []}
        );
        expect(errors).to.be.empty;
      });
    });

    describe('duplicate', () => {
      it('should error when the label matches another', () => {
        const errors = validate(
          {label: 'sandwich'},
          {
            category: {id: 1, label: 'pizza'},
            categories: [
              {id: 3, label: 'sandwich'},
              {id: 4, label: 'pasta'},
            ]
          }
        );
        expect(errors.label).to.equal('duplicate');
      });

      it('should not error when the label matches itself', () => {
        const errors = validate(
          {label: 'sandwich'},
          {
            category: {id: 1, label: 'sandwich'},
            categories: [
              {id: 1, label: 'sandwich'},
              {id: 3, label: 'pizza'},
              {id: 4, label: 'pasta'},
            ]
          }
        );
        expect(errors).to.be.empty;
      });
    });
  });

  describe('rendering with `isEditMode` false', () => {
    beforeEach(() => {
      this.handleSubmit = stub();
      this.onClickCancel = stub();

      this.defaultProps = {
        fields: {
          label: {}
        },
        handleSubmit: this.handleSubmit,
        confirmInProgress: false,
        onClickCancel: this.onClickCancel,
        isEditMode: false
      };

      this.generator = generateWrapperGenerator(this.defaultProps, ModifyCategoryModal);
    });

    it('should have the right class name', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.hasClass('createCategoryModal')).to.be.true;
    });

    it('should have the right modal title when edit mode is false', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.find('.modal-title').text()).to.equal('Create new category');
    });

    it('should have the right modal title when edit mode is true', () => {
      const wrapper = this.generator.shallow({isEditMode: true});
      expect(wrapper.find('.modal-title').text()).to.equal('Edit category');
    });

    it('should not render an error', () => {
      const wrapper = this.generator.shallow();
      expect(wrapper.find('.modal-error')).to.have.length(0);
    });

    it('should focus the label on mount', () => {
      const wrapper = this.generator.shallow();
      const focusStub = stub();
      wrapper.instance().labelInput = {
        focus: focusStub
      };
      wrapper.instance().componentDidMount();
      expect(focusStub).to.have.been.calledOnce;
    });

    it('should not render an error when the label has not been touched', () => {
      const props = {
        fields: {
          label: {
            error: 'sorry',
            touched: false
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      expect(wrapper.find('.modal-error')).to.have.length(0);
    });

    it('should render an empty error when the label has an "empty" error', () => {
      const props = {
        fields: {
          label: {
            error: 'empty',
            touched: true
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      const errorEl = wrapper.find('.modal-error');
      expect(errorEl).to.have.length(1);
      expect(errorEl.text()).to.equal('A name is required');
    });

    it('should render a duplicate error when the label has an "duplicate" error', () => {
      const props = {
        fields: {
          label: {
            error: 'duplicate',
            touched: true
          }
        }
      };
      const wrapper = this.generator.shallow(props);
      const errorEl = wrapper.find('.modal-error');
      expect(errorEl).to.have.length(1);
      expect(errorEl.text()).to.equal('Category already exists');
    });

    describe('the form', () => {
      it('should contain an emoji picker', () => {
        const form = this.generator.shallow().find('form');
        expect(form.find('.createCategoryModal-emojiSelect')).to.have.length(1);
      });

      it('should contain a label input with the right props', () => {
        const form = this.generator.shallow().find('form');
        const labelInput = form.find('.newCategoryName');
        expect(labelInput).to.have.length(1);
        expect(labelInput.hasClass('text-input')).to.be.true;
        expect(labelInput.hasClass('invalid-input')).to.be.false;
        expect(labelInput.prop('type')).to.equal('text');
        expect(labelInput.prop('placeholder')).to.equal('Enter name');
        expect(labelInput.prop('disabled')).to.be.falsey;
      });

      it('should have a disabled input when a creation is in progress', () => {
        const props = {confirmInProgress: true};
        const form = this.generator.shallow(props).find('form');
        const labelInput = form.find('.newCategoryName');
        expect(labelInput.prop('disabled')).to.be.true;
      });

      it('should call preventDefault and `handleSubmit` when submitted', () => {
        const form = this.generator.shallow().find('form');
        const preventDefault = stub();
        form.simulate('submit', {preventDefault});
        expect(preventDefault).to.have.been.calledOnce;
        expect(this.handleSubmit).to.have.been.calledOnce;
      });

      describe('cancel button', () => {
        it('should exist', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn).to.have.length(1);
        });

        it('should not be disabled when no confirm is in progress', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.falsey;
        });

        it('should not be disabled even when there is an error', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: true
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.falsey;
        });

        it('should be disabled when a creation is in progress', () => {
          const props = {confirmInProgress: true};
          const wrapper = this.generator.shallow(props);
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.prop('disabled')).to.be.true;
        });

        it('should have the right text', () => {
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          expect(cancelBtn.text()).to.equal('Cancel');
        });

        it('should preventDefault and call `onClickCancel` when clicked', () => {
          const preventDefault = stub();
          const wrapper = this.generator.shallow();
          const cancelBtn = wrapper.find('.createCategoryModal-cancelBtn');
          cancelBtn.simulate('click', {preventDefault});
          expect(preventDefault).to.have.been.calledOnce;
          expect(this.onClickCancel).to.have.been.calledOnce;
        });
      });

      describe('confirm button', () => {
        it('should exist', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn).to.have.length(1);
        });

        it('should be the submit button', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('type')).to.equal('submit');
        });

        it('should not be disabled when there are no errors & no save in progress', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.falsey;
        });

        it('should not be disabled when there is an error but the form has not been touched', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: false
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.falsey;
        });

        it('should be disabled when there is an error and the form has been touched', () => {
          const props = {
            fields: {
              label: {
                error: 'sorry',
                touched: true
              }
            }
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.true;
        });

        it('should be disabled when a creation is in progress', () => {
          const props = {confirmInProgress: true};
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.prop('disabled')).to.be.true;
        });

        it('should have the right text', () => {
          const wrapper = this.generator.shallow();
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Create');
        });

        it('should have the right text when edit mode is true', () => {
          const wrapper = this.generator.shallow({isEditMode: true});
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Edit');
        });

        it('should have the right text when a create is in progress', () => {
          const wrapper = this.generator.shallow({confirmInProgress: true});
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Creating...');
        });

        it('should have the right text when a create is in progress in edit mode', () => {
          const props = {
            confirmInProgress: true,
            isEditMode: true
          };
          const wrapper = this.generator.shallow(props);
          const confirmBtn = wrapper.find('.createCategoryModal-confirmBtn');
          expect(confirmBtn.text()).to.equal('Editing...');
        });
      });
    });
  });
});
<|endoftext|>


<commit_before>import React from 'react';
import { t, Trans } from '@lingui/macro';

import SpellLink from 'common/SpellLink';
import { formatPercentage } from 'common/format';
import Analyzer from 'parser/core/Analyzer';
import SpellHistory from 'parser/shared/modules/SpellHistory';
import { i18n } from 'interface/RootLocalizationProvider';
import Tab from 'interface/others/Tab';
import CastEfficiencyComponent from 'interface/others/CastEfficiency';

import Abilities from './Abilities';
import AbilityTracker from './AbilityTracker';
import Haste from './Haste';

const DEFAULT_RECOMMENDED = 0.80;
const DEFAULT_AVERAGE_DOWNSTEP = 0.05;
const DEFAULT_MAJOR_DOWNSTEP = 0.15;
const seconds = ms => ms / 1000;
const minutes = ms => seconds(ms) / 60;

class CastEfficiency extends Analyzer {
  static dependencies = {
    abilityTracker: AbilityTracker,
    haste: Haste,
    spellHistory: SpellHistory,
    abilities: Abilities,
  };

  /**
   * Gets info about spell's cooldown behavior. All values are as of the current timestamp.
   * completedRechargeTime is the total ms of completed cooldowns
   * endingRechargeTime is the total ms into current cooldown
   * recharges is the total number of times the spell has recharged (either come off cooldown or gained a charge)
   * Only works on spells entered into CastEfficiency list.
   */
  _getCooldownInfo(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return {
        completedRechargeTime: 0,
        endingRechargeTime: 0,
        recharges: 0,
        casts: 0,
      };
    }

    let lastRechargeTimestamp = null;
    let recharges = 0;
    const completedRechargeTime = history
      .filter(event => event.type === 'updatespellusable')
      .reduce((acc, event) => {
        if (event.trigger === 'begincooldown') {
          lastRechargeTimestamp = event.timestamp;
          return acc;
        } else if (event.trigger === 'endcooldown') {
          const rechargingTime = (event.timestamp - lastRechargeTimestamp) || 0;
          recharges += 1;
          lastRechargeTimestamp = null;
          return acc + rechargingTime;
          // This might cause oddness if we add anything that externally refreshes charges, but so far nothing does
        } else if (event.trigger === 'restorecharge') {
          const rechargingTime = (event.timestamp - lastRechargeTimestamp) || 0;
          recharges += 1;
          lastRechargeTimestamp = event.timestamp;
          return acc + rechargingTime;
        } else {
          return acc;
        }
      }, 0);
    const endingRechargeTime = (!lastRechargeTimestamp) ? 0 : this.owner.currentTimestamp - lastRechargeTimestamp;

    const casts = history.filter(event => event.type === 'cast').length;

    return {
      completedRechargeTime,
      endingRechargeTime,
      recharges,
      casts,
    };
  }

  _getTimeSpentCasting(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return 0;
    }

    let beginCastTimestamp = null;
    const timeSpentCasting = history
      .reduce((acc, event) => {
        if (event.type === 'begincast') {
          beginCastTimestamp = event.timestamp;
          return acc;
        } else if (event.type === 'cast') {
          const castTime = beginCastTimestamp ? (event.timestamp - beginCastTimestamp) : 0;
          beginCastTimestamp = null;
          return acc + castTime;
        } else {
          return acc;
        }
      }, 0);

    return timeSpentCasting;
  }

  _getTimeSpentOnGcd(spellId) {
    const ability = this.abilities.getAbility(spellId);

    if (ability) {
      const cdInfo = this._getCooldownInfo(ability);

      let casts;
      if (ability.castEfficiency.casts) {
        casts = ability.castEfficiency.casts(this.abilityTracker.getAbility(spellId), this.owner);
      } else {
        casts = cdInfo.casts;
      }

      const averagetimeSpentOnAbility = this._getTimeSpentCasting(ability) / casts;
      // If the abilities GCD is longer than the time spent casting, use the GCD
      if (ability.gcd && averagetimeSpentOnAbility < ability.gcd.base) {
        return ability.gcd.base * casts;
      }
    }
    return 0;
  }

  getTimeSpentCasting(abilityId) {
    const timeSpentCasting = this._getTimeSpentCasting({ primarySpell: { id: abilityId } });
    const gcdSpent = this._getTimeSpentOnGcd(abilityId);

    return {
      timeSpentCasting,
      gcdSpent,
    };
  }

  /**
   * Time spent waiting for a GCD that reset the cooldown of the spell to finish
   */
  _getTimeWaitingOnGCD(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return 0;
    }

    const timeWaitingOnGCD = history
      .reduce((acc, event) => {
        if (event.type === 'updatespellusable' && event.timeWaitingOnGCD) {
          return acc + event.timeWaitingOnGCD;
        } else {
          return acc;
        }
      }, 0);

    return timeWaitingOnGCD;
  }

  /**
   * Packs cast efficiency results for use by suggestions / tab
   */
  getCastEfficiency() {
    return this.abilities.activeAbilities
      .map(ability => this.getCastEfficiencyForAbility(ability))
      .filter(item => item !== null); // getCastEfficiencyForAbility can return null, remove those from the result
  }
  getCastEfficiencyForSpellId(spellId, includeNoCooldownEfficiency = false) {
    const ability = this.abilities.getAbility(spellId);
    return ability ? this.getCastEfficiencyForAbility(ability, includeNoCooldownEfficiency) : null;
  }
  getCastEfficiencyForAbility(ability, includeNoCooldownEfficiency = false) {
    const spellId = ability.primarySpell.id;
    const availableFightDuration = this.owner.fightDuration;

    const cooldown = ability.cooldown;
    const cooldownMs = !cooldown ? null : cooldown * 1000;
    const cdInfo = this._getCooldownInfo(ability);
    const timeSpentCasting = (cooldown && ability.charges < 2) ? this._getTimeSpentCasting(ability) : 0;
    const timeWaitingOnGCD = (cooldown && ability.charges < 2 && ability.gcd) ? this._getTimeWaitingOnGCD(ability) : 0;

    // ability.casts is used for special cases that show the wrong number of cast events, like Penance
    // and also for splitting up differently buffed versions of the same spell
    let casts;
    if (ability.castEfficiency.casts) {
      casts = ability.castEfficiency.casts(this.abilityTracker.getAbility(spellId), this.owner);
    } else {
      casts = cdInfo.casts;
    }
    const cpm = casts / minutes(availableFightDuration);
    const averageTimeSpentCasting = timeSpentCasting / casts;
    const averageTimeWaitingOnGCD = timeWaitingOnGCD / casts;

    if (ability.isUndetectable && casts === 0) {
      // Some spells (most notably Racials) can not be detected if a player has them. This hides those spells if they have 0 casts.
      return null;
    }

    // ability.maxCasts is used for special cases for spells that have a variable availability or CD based on state, like Void Bolt.
    // This same behavior should be managable using SpellUsable's interface, so maxCasts is deprecated.
    // Legacy support: if maxCasts is defined, cast efficiency will be calculated using casts/rawMaxCasts
    let rawMaxCasts;
    const averageCooldown = (cdInfo.recharges === 0) ? null : (cdInfo.completedRechargeTime / cdInfo.recharges);
    if (ability.castEfficiency.maxCasts) {
      // maxCasts expects cooldown in seconds
      rawMaxCasts = ability.castEfficiency.maxCasts(cooldown, availableFightDuration, this.abilityTracker.getAbility, this.owner);
    } else if (averageCooldown) { // no average CD if spell hasn't been cast
      rawMaxCasts = (availableFightDuration / (averageCooldown + averageTimeSpentCasting + averageTimeWaitingOnGCD)) + (ability.charges || 1) - 1;
    } else if (!includeNoCooldownEfficiency) {
      rawMaxCasts = (availableFightDuration / cooldownMs) + (ability.charges || 1) - 1;
    } else if (casts > 0) {
      let averagetimeSpentOnNoCooldownAbility = this._getTimeSpentCasting(ability) / casts;
      if (ability.gcd && averagetimeSpentOnNoCooldownAbility < ability.gcd.base) {
        // edge case for no cast time spells and spells with cast time lower than the gcd.
        averagetimeSpentOnNoCooldownAbility = ability.gcd.base;
      }
      rawMaxCasts = availableFightDuration / averagetimeSpentOnNoCooldownAbility;
    } else {
      // If we don't have any way to tell the cast time of the spell, return null.
      rawMaxCasts = null;
    }

    const maxCasts = Math.ceil(rawMaxCasts) || 0;
    const maxCpm = maxCasts / minutes(availableFightDuration);

    let efficiency;
    if (ability.castEfficiency.maxCasts) { // legacy support for custom maxCasts
      efficiency = Math.min(1, casts / rawMaxCasts);
    } else {
      // Cast efficiency calculated as the percent of fight time spell was unavailable
      // The spell is considered unavailable if it is on cooldown, the time since it came off cooldown is less than the cast time or the cooldown was reset through a proc during a GCD
      if (cooldown && availableFightDuration) {
        const timeOnCd = cdInfo.completedRechargeTime + cdInfo.endingRechargeTime;
        const timeUnavailable = timeOnCd + timeSpentCasting + timeWaitingOnGCD;
        efficiency = timeUnavailable / availableFightDuration;
      } else if (includeNoCooldownEfficiency) {
        efficiency = casts / rawMaxCasts;
      } else {
        efficiency = null;
      }
    }

    const recommendedEfficiency = ability.castEfficiency.recommendedEfficiency || DEFAULT_RECOMMENDED;
    const averageIssueEfficiency = ability.castEfficiency.averageIssueEfficiency || (recommendedEfficiency - DEFAULT_AVERAGE_DOWNSTEP);
    const majorIssueEfficiency = ability.castEfficiency.majorIssueEfficiency || (recommendedEfficiency - DEFAULT_MAJOR_DOWNSTEP);

    const gotMaxCasts = (casts === maxCasts);
    const canBeImproved = ability.castEfficiency.suggestion && efficiency !== null && efficiency < recommendedEfficiency && !gotMaxCasts;

    return {
      ability,
      cpm,
      maxCpm,
      casts,
      maxCasts,
      efficiency,
      recommendedEfficiency,
      averageIssueEfficiency,
      majorIssueEfficiency,
      gotMaxCasts,
      canBeImproved,
    };
  }

  suggestions(when) {
    const castEfficiencyInfo = this.getCastEfficiency();
    castEfficiencyInfo.forEach(abilityInfo => {
      if (!abilityInfo.ability.castEfficiency.suggestion || abilityInfo.efficiency === null || abilityInfo.gotMaxCasts) {
        return;
      }
      const ability = abilityInfo.ability;
      const mainSpell = (ability.spell instanceof Array) ? ability.spell[0] : ability.spell;

      const suggestionThresholds = {
        actual: abilityInfo.efficiency,
        isLessThan: {
          minor: abilityInfo.recommendedEfficiency,
          average: abilityInfo.averageIssueEfficiency,
          major: abilityInfo.majorIssueEfficiency,
        },
      };

      when(suggestionThresholds).addSuggestion((suggest, actual, recommended) => {
        return suggest(
          <>
            <Trans>Try to cast <SpellLink id={mainSpell.id} /> more often.</Trans> {ability.castEfficiency.extraSuggestion || ''}
          </>
        )
          .icon(mainSpell.icon)
          .actual(<Trans>{abilityInfo.casts} out of {abilityInfo.maxCasts} possible casts. You kept it on cooldown {formatPercentage(actual, 0)}% of the time.</Trans>)
          .recommended(<Trans>>{formatPercentage(recommended, 0)}% is recommended</Trans>)
          .staticImportance(ability.castEfficiency.importance);
      });
    });
  }

  tab() {
    return {
      title: i18n._(t`Abilities`),
      url: 'abilities',
      render: () => (
        <Tab>
          <CastEfficiencyComponent
            categories={this.abilities.constructor.SPELL_CATEGORIES}
            abilities={this.getCastEfficiency()}
          />
        </Tab>
      ),
    };
  }
}

export default CastEfficiency;
<commit_msg>implementing gcd<commit_after>import React from 'react';
import { t, Trans } from '@lingui/macro';

import SpellLink from 'common/SpellLink';
import { formatPercentage } from 'common/format';
import Analyzer from 'parser/core/Analyzer';
import SpellHistory from 'parser/shared/modules/SpellHistory';
import { i18n } from 'interface/RootLocalizationProvider';
import Tab from 'interface/others/Tab';
import CastEfficiencyComponent from 'interface/others/CastEfficiency';

import Abilities from './Abilities';
import AbilityTracker from './AbilityTracker';
import Haste from './Haste';

const DEFAULT_RECOMMENDED = 0.80;
const DEFAULT_AVERAGE_DOWNSTEP = 0.05;
const DEFAULT_MAJOR_DOWNSTEP = 0.15;
const seconds = ms => ms / 1000;
const minutes = ms => seconds(ms) / 60;

class CastEfficiency extends Analyzer {
  static dependencies = {
    abilityTracker: AbilityTracker,
    haste: Haste,
    spellHistory: SpellHistory,
    abilities: Abilities,
  };

  /**
   * Gets info about spell's cooldown behavior. All values are as of the current timestamp.
   * completedRechargeTime is the total ms of completed cooldowns
   * endingRechargeTime is the total ms into current cooldown
   * recharges is the total number of times the spell has recharged (either come off cooldown or gained a charge)
   * Only works on spells entered into CastEfficiency list.
   */
  _getCooldownInfo(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return {
        completedRechargeTime: 0,
        endingRechargeTime: 0,
        recharges: 0,
        casts: 0,
      };
    }

    let lastRechargeTimestamp = null;
    let recharges = 0;
    const completedRechargeTime = history
      .filter(event => event.type === 'updatespellusable')
      .reduce((acc, event) => {
        if (event.trigger === 'begincooldown') {
          lastRechargeTimestamp = event.timestamp;
          return acc;
        } else if (event.trigger === 'endcooldown') {
          const rechargingTime = (event.timestamp - lastRechargeTimestamp) || 0;
          recharges += 1;
          lastRechargeTimestamp = null;
          return acc + rechargingTime;
          // This might cause oddness if we add anything that externally refreshes charges, but so far nothing does
        } else if (event.trigger === 'restorecharge') {
          const rechargingTime = (event.timestamp - lastRechargeTimestamp) || 0;
          recharges += 1;
          lastRechargeTimestamp = event.timestamp;
          return acc + rechargingTime;
        } else {
          return acc;
        }
      }, 0);
    const endingRechargeTime = (!lastRechargeTimestamp) ? 0 : this.owner.currentTimestamp - lastRechargeTimestamp;

    const casts = history.filter(event => event.type === 'cast').length;

    return {
      completedRechargeTime,
      endingRechargeTime,
      recharges,
      casts,
    };
  }

  _getTimeSpentCasting(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return 0;
    }

    let beginCastTimestamp = null;
    const timeSpentCasting = history
      .reduce((acc, event) => {
        if (event.type === 'begincast') {
          beginCastTimestamp = event.timestamp;
          return acc;
        } else if (event.type === 'cast') {
          const castTime = beginCastTimestamp ? (event.timestamp - beginCastTimestamp) : 0;
          beginCastTimestamp = null;
          return acc + castTime;
        } else {
          return acc;
        }
      }, 0);

    return timeSpentCasting;
  }

  _getTimeSpentOnGcd(spellId) {
    const ability = this.abilities.getAbility(spellId);

    if (ability) {
      const cdInfo = this._getCooldownInfo(ability);

      let casts;
      if (ability.castEfficiency.casts) {
        casts = ability.castEfficiency.casts(this.abilityTracker.getAbility(spellId), this.owner);
      } else {
        casts = cdInfo.casts;
      }

      const averagetimeSpentOnAbility = this._getTimeSpentCasting(ability) / casts;
      // If the abilities GCD is longer than the time spent casting, use the GCD
      if (ability.gcd && ability.gcd.base) {
        const gcdReduction = ability.gcd.base * this.haste.current;
        const gcdActual = ability.gcd.base - gcdReduction;

        if (averagetimeSpentOnAbility < gcdActual) {
          return gcdActual * casts;
        }
      }
    }
    return 0;
  }

  getTimeSpentCasting(abilityId) {
    const timeSpentCasting = this._getTimeSpentCasting({ primarySpell: { id: abilityId } });
    const gcdSpent = this._getTimeSpentOnGcd(abilityId);

    return {
      timeSpentCasting,
      gcdSpent,
    };
  }

  /**
   * Time spent waiting for a GCD that reset the cooldown of the spell to finish
   */
  _getTimeWaitingOnGCD(ability) {
    const mainSpellId = ability.primarySpell.id;
    const history = this.spellHistory.historyBySpellId[mainSpellId];
    if (!history) { // spell either never been cast, or not in abilities list
      return 0;
    }

    const timeWaitingOnGCD = history
      .reduce((acc, event) => {
        if (event.type === 'updatespellusable' && event.timeWaitingOnGCD) {
          return acc + event.timeWaitingOnGCD;
        } else {
          return acc;
        }
      }, 0);

    return timeWaitingOnGCD;
  }

  /**
   * Packs cast efficiency results for use by suggestions / tab
   */
  getCastEfficiency() {
    return this.abilities.activeAbilities
      .map(ability => this.getCastEfficiencyForAbility(ability))
      .filter(item => item !== null); // getCastEfficiencyForAbility can return null, remove those from the result
  }
  getCastEfficiencyForSpellId(spellId, includeNoCooldownEfficiency = false) {
    const ability = this.abilities.getAbility(spellId);
    return ability ? this.getCastEfficiencyForAbility(ability, includeNoCooldownEfficiency) : null;
  }
  getCastEfficiencyForAbility(ability, includeNoCooldownEfficiency = false) {
    const spellId = ability.primarySpell.id;
    const availableFightDuration = this.owner.fightDuration;

    const cooldown = ability.cooldown;
    const cooldownMs = !cooldown ? null : cooldown * 1000;
    const cdInfo = this._getCooldownInfo(ability);
    const timeSpentCasting = (cooldown && ability.charges < 2) ? this._getTimeSpentCasting(ability) : 0;
    const timeWaitingOnGCD = (cooldown && ability.charges < 2 && ability.gcd) ? this._getTimeWaitingOnGCD(ability) : 0;

    // ability.casts is used for special cases that show the wrong number of cast events, like Penance
    // and also for splitting up differently buffed versions of the same spell
    let casts;
    if (ability.castEfficiency.casts) {
      casts = ability.castEfficiency.casts(this.abilityTracker.getAbility(spellId), this.owner);
    } else {
      casts = cdInfo.casts;
    }
    const cpm = casts / minutes(availableFightDuration);
    const averageTimeSpentCasting = timeSpentCasting / casts;
    const averageTimeWaitingOnGCD = timeWaitingOnGCD / casts;

    if (ability.isUndetectable && casts === 0) {
      // Some spells (most notably Racials) can not be detected if a player has them. This hides those spells if they have 0 casts.
      return null;
    }

    // ability.maxCasts is used for special cases for spells that have a variable availability or CD based on state, like Void Bolt.
    // This same behavior should be managable using SpellUsable's interface, so maxCasts is deprecated.
    // Legacy support: if maxCasts is defined, cast efficiency will be calculated using casts/rawMaxCasts
    let rawMaxCasts;
    const averageCooldown = (cdInfo.recharges === 0) ? null : (cdInfo.completedRechargeTime / cdInfo.recharges);
    if (ability.castEfficiency.maxCasts) {
      // maxCasts expects cooldown in seconds
      rawMaxCasts = ability.castEfficiency.maxCasts(cooldown, availableFightDuration, this.abilityTracker.getAbility, this.owner);
    } else if (averageCooldown) { // no average CD if spell hasn't been cast
      rawMaxCasts = (availableFightDuration / (averageCooldown + averageTimeSpentCasting + averageTimeWaitingOnGCD)) + (ability.charges || 1) - 1;
    } else if (!includeNoCooldownEfficiency) {
      rawMaxCasts = (availableFightDuration / cooldownMs) + (ability.charges || 1) - 1;
    } else if (casts > 0) {
      let averagetimeSpentOnNoCooldownAbility = this._getTimeSpentCasting(ability) / casts;
      if (ability.gcd && averagetimeSpentOnNoCooldownAbility < ability.gcd.base) {
        // edge case for no cast time spells and spells with cast time lower than the gcd.
        averagetimeSpentOnNoCooldownAbility = ability.gcd.base;
      }
      rawMaxCasts = availableFightDuration / averagetimeSpentOnNoCooldownAbility;
    } else {
      // If we don't have any way to tell the cast time of the spell, return null.
      rawMaxCasts = null;
    }

    const maxCasts = Math.ceil(rawMaxCasts) || 0;
    const maxCpm = maxCasts / minutes(availableFightDuration);

    let efficiency;
    if (ability.castEfficiency.maxCasts) { // legacy support for custom maxCasts
      efficiency = Math.min(1, casts / rawMaxCasts);
    } else {
      // Cast efficiency calculated as the percent of fight time spell was unavailable
      // The spell is considered unavailable if it is on cooldown, the time since it came off cooldown is less than the cast time or the cooldown was reset through a proc during a GCD
      if (cooldown && availableFightDuration) {
        const timeOnCd = cdInfo.completedRechargeTime + cdInfo.endingRechargeTime;
        const timeUnavailable = timeOnCd + timeSpentCasting + timeWaitingOnGCD;
        efficiency = timeUnavailable / availableFightDuration;
      } else if (includeNoCooldownEfficiency) {
        efficiency = casts / rawMaxCasts;
      } else {
        efficiency = null;
      }
    }

    const recommendedEfficiency = ability.castEfficiency.recommendedEfficiency || DEFAULT_RECOMMENDED;
    const averageIssueEfficiency = ability.castEfficiency.averageIssueEfficiency || (recommendedEfficiency - DEFAULT_AVERAGE_DOWNSTEP);
    const majorIssueEfficiency = ability.castEfficiency.majorIssueEfficiency || (recommendedEfficiency - DEFAULT_MAJOR_DOWNSTEP);

    const gotMaxCasts = (casts === maxCasts);
    const canBeImproved = ability.castEfficiency.suggestion && efficiency !== null && efficiency < recommendedEfficiency && !gotMaxCasts;

    return {
      ability,
      cpm,
      maxCpm,
      casts,
      maxCasts,
      efficiency,
      recommendedEfficiency,
      averageIssueEfficiency,
      majorIssueEfficiency,
      gotMaxCasts,
      canBeImproved,
    };
  }

  suggestions(when) {
    const castEfficiencyInfo = this.getCastEfficiency();
    castEfficiencyInfo.forEach(abilityInfo => {
      if (!abilityInfo.ability.castEfficiency.suggestion || abilityInfo.efficiency === null || abilityInfo.gotMaxCasts) {
        return;
      }
      const ability = abilityInfo.ability;
      const mainSpell = (ability.spell instanceof Array) ? ability.spell[0] : ability.spell;

      const suggestionThresholds = {
        actual: abilityInfo.efficiency,
        isLessThan: {
          minor: abilityInfo.recommendedEfficiency,
          average: abilityInfo.averageIssueEfficiency,
          major: abilityInfo.majorIssueEfficiency,
        },
      };

      when(suggestionThresholds).addSuggestion((suggest, actual, recommended) => {
        return suggest(
          <>
            <Trans>Try to cast <SpellLink id={mainSpell.id} /> more often.</Trans> {ability.castEfficiency.extraSuggestion || ''}
          </>
        )
          .icon(mainSpell.icon)
          .actual(<Trans>{abilityInfo.casts} out of {abilityInfo.maxCasts} possible casts. You kept it on cooldown {formatPercentage(actual, 0)}% of the time.</Trans>)
          .recommended(<Trans>>{formatPercentage(recommended, 0)}% is recommended</Trans>)
          .staticImportance(ability.castEfficiency.importance);
      });
    });
  }

  tab() {
    return {
      title: i18n._(t`Abilities`),
      url: 'abilities',
      render: () => (
        <Tab>
          <CastEfficiencyComponent
            categories={this.abilities.constructor.SPELL_CATEGORIES}
            abilities={this.getCastEfficiency()}
          />
        </Tab>
      ),
    };
  }
}

export default CastEfficiency;
<|endoftext|>


<commit_before>import {afterUpdateActionList} from '../doNotKill';
import {bias} from '../assets';
import calf from '../../support/calf';
import evalAnalysis from './evalAnalysis';
import evalArmour from './evalArmour';
import evalAttack from './evalAttack';
import evalCa from './evalCa';
import evalDamage from './evalDamage';
import evalDefence from './evalDefence';
import evalExtraBuffs from './evalExtraBuffs';
import evalHtml from './evalHtml';
import {getElementById} from '../../common/getElement';
import getValue from '../../system/getValue';
import intValue from '../../system/intValue';
import {playerDataString} from '../../common/common';
import {
  createDocument,
  findNode,
  setValue,
  xmlhttp
} from '../../system/system';

function getBiasGeneral(combat) {
  if (bias[combat.combatEvaluatorBias]) {
    return bias[combat.combatEvaluatorBias].generalVariable;
  }
  return 1.1053;
}

function getBiasHp(combat) {
  if (bias[combat.combatEvaluatorBias]) {
    return bias[combat.combatEvaluatorBias].hpVariable;
  }
  return 1.1;
}

function creatureData(ses) { // jQuery
  var obj = {};
  obj.name = $('#dialog-viewcreature').find('h2.name').text();
  obj.class = $('#dialog-viewcreature')
    .find('span.classification')
    .text();
  obj.attack = intValue($('#dialog-viewcreature')
    .find('dd.attribute-atk').text());
  obj.defense = intValue($('#dialog-viewcreature')
    .find('dd.attribute-def').text());
  obj.armor = intValue($('#dialog-viewcreature')
    .find('dd.attribute-arm').text());
  obj.damage = intValue($('#dialog-viewcreature')
    .find('dd.attribute-dmg').text());
  obj.hp = intValue($('#dialog-viewcreature')
    .find('p.health-max').text());
  // reduce stats if critter is a SE and player has SES cast on them.
  if (obj.name.search('Super Elite') !== -1) {
    obj.attack -= Math.ceil(obj.attack * ses);
    obj.defense -= Math.ceil(obj.defense * ses);
    obj.armor -= Math.ceil(obj.armor * ses);
    obj.damage -= Math.ceil(obj.damage * ses);
    obj.hp -= Math.ceil(obj.hp * ses);
  }
  return obj;
}

function checkForCreatureEvaluatorGroup() { // Legacy
  if ($('#creatureEvaluatorGroup').length === 0) {
    $('#dialog-viewcreature')
      .append('

');
  }
}

function checkForCreatureEvaluator() { // Legacy
  if ($('#creatureEvaluator').length === 0) {
    $('#dialog-viewcreature')
      .append('

');
  }
}

function getCreaturePlayerData(responseText, callback) { // Legacy
  var combat = {};
  combat.callback = callback;
  // playerdata
  combat.player = playerDataString(responseText);
  combat.combatEvaluatorBias = getValue('combatEvaluatorBias');
  combat.attackVariable = 1.1053;
  combat.generalVariable = getBiasGeneral(combat);
  combat.hpVariable = getBiasHp(combat);
  combat.creature =
    creatureData(combat.player.superEliteSlayerMultiplier);
  combat = evalExtraBuffs(combat);
  combat = evalAttack(combat);
  combat = evalDamage(combat);
  combat = evalDefence(combat);
  combat = evalArmour(combat);
  combat = evalAnalysis(combat);
  combat = evalCa(combat);
  combat.evaluatorHTML = evalHtml(combat);
  var tempdata;
  if (callback.groupEvaluation) {
    checkForCreatureEvaluatorGroup();
    tempdata = combat.evaluatorHTML.replace(/'/g, '\\\'');
    $('#creatureEvaluatorGroup').html(tempdata);
  } else {
    checkForCreatureEvaluator();
    tempdata = combat.evaluatorHTML.replace(/'/g, '\\\'');
    $('#creatureEvaluator').html(tempdata);
  }
}

function getCreatureGroupData(responseText) { // Legacy
  var doc = createDocument(responseText);
  var groupAttackValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Attack:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupDefenseValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Defense:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupArmorValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Armor:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupDamageValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Damage:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupHPValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"HP:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  xmlhttp('index.php?no_mobile=1&cmd=profile', getCreaturePlayerData, {
    groupExists: true,
    groupAttackValue: groupAttackValue,
    groupDefenseValue: groupDefenseValue,
    groupArmorValue: groupArmorValue,
    groupDamageValue: groupDamageValue,
    groupHPValue: groupHPValue,
    groupEvaluation: true
  });
}

function checkIfGroupExists(responseText) { // Hybrid
  var doc = createDocument(responseText);
  var groupExistsIMG = $(doc)
    .find('img[title="Disband Group (Cancel Attack)"]');
  if (groupExistsIMG.length > 0) {
    var groupHref = groupExistsIMG.parents('td:first').find('a:first')
      .attr('href');
    xmlhttp(groupHref, getCreatureGroupData);
  }
}

function addRemoveCreatureToDoNotKillList(evt) {
  var creatureName = evt.target.getAttribute('creatureName');
  var ind = calf.doNotKillList.indexOf(creatureName);
  if (ind !== -1) {
    calf.doNotKillList.splice(ind, 1);
    evt.target.innerHTML = 'Add to the do not kill list';
  } else {
    calf.doNotKillList.push(creatureName);
    evt.target.innerHTML = 'Remove from do not kill list';
  }
  setValue('doNotKillList', calf.doNotKillList.join());
  // refresh the action list
  afterUpdateActionList();
}

export default function readyViewCreature() { // Hybrid
  $('#creatureEvaluator').html('');
  $('#creatureEvaluatorGroup').html('');

  xmlhttp('index.php?no_mobile=1&cmd=profile', getCreaturePlayerData, {
    groupExists: false,
    groupAttackValue: 0,
    groupDefenseValue: 0,
    groupArmorValue: 0,
    groupDamageValue: 0,
    groupHPValue: 0,
    groupEvaluation: false
  });
  xmlhttp('index.php?no_mobile=1&cmd=guild&subcmd=groups',
    checkIfGroupExists);

  $('#addRemoveCreatureToDoNotKillList').html('');
  if ($('#addRemoveCreatureToDoNotKillList').length === 0) {
    var doNotKillElement = '

';
    $(doNotKillElement).insertAfter($('#dialog-viewcreature')
      .find('p.description'));
  }
  var creatureName = $('#dialog-viewcreature').find('h2.name')
    .text();
  $('#addRemoveCreatureToDoNotKillList')
    .attr('creatureName', creatureName);
  var extraText = 'Add to the do not kill list';
  if (calf.doNotKillList.indexOf(creatureName) !== -1) {
    extraText = 'Remove from do not kill list';
  }
  $('#addRemoveCreatureToDoNotKillList').html(extraText);
  getElementById('addRemoveCreatureToDoNotKillList')
    .addEventListener('click',
      addRemoveCreatureToDoNotKillList, true);
}
<commit_msg>replace call to xmlhttp with retryAjax in viewCreature<commit_after>import {afterUpdateActionList} from '../doNotKill';
import {bias} from '../assets';
import calf from '../../support/calf';
import evalAnalysis from './evalAnalysis';
import evalArmour from './evalArmour';
import evalAttack from './evalAttack';
import evalCa from './evalCa';
import evalDamage from './evalDamage';
import evalDefence from './evalDefence';
import evalExtraBuffs from './evalExtraBuffs';
import evalHtml from './evalHtml';
import {getElementById} from '../../common/getElement';
import getValue from '../../system/getValue';
import intValue from '../../system/intValue';
import {playerDataString} from '../../common/common';
import retryAjax from '../../ajax/retryAjax';
import {
  createDocument,
  findNode,
  setValue
} from '../../system/system';

function getBiasGeneral(combat) {
  if (bias[combat.combatEvaluatorBias]) {
    return bias[combat.combatEvaluatorBias].generalVariable;
  }
  return 1.1053;
}

function getBiasHp(combat) {
  if (bias[combat.combatEvaluatorBias]) {
    return bias[combat.combatEvaluatorBias].hpVariable;
  }
  return 1.1;
}

function creatureData(ses) { // jQuery
  var obj = {};
  obj.name = $('#dialog-viewcreature').find('h2.name').text();
  obj.class = $('#dialog-viewcreature')
    .find('span.classification')
    .text();
  obj.attack = intValue($('#dialog-viewcreature')
    .find('dd.attribute-atk').text());
  obj.defense = intValue($('#dialog-viewcreature')
    .find('dd.attribute-def').text());
  obj.armor = intValue($('#dialog-viewcreature')
    .find('dd.attribute-arm').text());
  obj.damage = intValue($('#dialog-viewcreature')
    .find('dd.attribute-dmg').text());
  obj.hp = intValue($('#dialog-viewcreature')
    .find('p.health-max').text());
  // reduce stats if critter is a SE and player has SES cast on them.
  if (obj.name.search('Super Elite') !== -1) {
    obj.attack -= Math.ceil(obj.attack * ses);
    obj.defense -= Math.ceil(obj.defense * ses);
    obj.armor -= Math.ceil(obj.armor * ses);
    obj.damage -= Math.ceil(obj.damage * ses);
    obj.hp -= Math.ceil(obj.hp * ses);
  }
  return obj;
}

function checkForCreatureEvaluatorGroup() { // Legacy
  if ($('#creatureEvaluatorGroup').length === 0) {
    $('#dialog-viewcreature')
      .append('

');
  }
}

function checkForCreatureEvaluator() { // Legacy
  if ($('#creatureEvaluator').length === 0) {
    $('#dialog-viewcreature')
      .append('

');
  }
}

function getCreaturePlayerData(responseText, callback) { // Legacy
  var combat = {};
  combat.callback = callback;
  // playerdata
  combat.player = playerDataString(responseText);
  combat.combatEvaluatorBias = getValue('combatEvaluatorBias');
  combat.attackVariable = 1.1053;
  combat.generalVariable = getBiasGeneral(combat);
  combat.hpVariable = getBiasHp(combat);
  combat.creature =
    creatureData(combat.player.superEliteSlayerMultiplier);
  combat = evalExtraBuffs(combat);
  combat = evalAttack(combat);
  combat = evalDamage(combat);
  combat = evalDefence(combat);
  combat = evalArmour(combat);
  combat = evalAnalysis(combat);
  combat = evalCa(combat);
  combat.evaluatorHTML = evalHtml(combat);
  var tempdata;
  if (callback.groupEvaluation) {
    checkForCreatureEvaluatorGroup();
    tempdata = combat.evaluatorHTML.replace(/'/g, '\\\'');
    $('#creatureEvaluatorGroup').html(tempdata);
  } else {
    checkForCreatureEvaluator();
    tempdata = combat.evaluatorHTML.replace(/'/g, '\\\'');
    $('#creatureEvaluator').html(tempdata);
  }
}

function getCreatureGroupData(responseText) { // Legacy
  var doc = createDocument(responseText);
  var groupAttackValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Attack:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupDefenseValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Defense:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupArmorValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Armor:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupDamageValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"Damage:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  var groupHPValue = Number(findNode('//table[@width="400"]/tbody' +
    '/tr/td[contains(.,"HP:")]', doc).nextSibling.textContent
    .replace(/,/, ''));
  retryAjax('index.php?no_mobile=1&cmd=profile').done(function(html) {
    getCreaturePlayerData(html, {
      groupExists: true,
      groupAttackValue: groupAttackValue,
      groupDefenseValue: groupDefenseValue,
      groupArmorValue: groupArmorValue,
      groupDamageValue: groupDamageValue,
      groupHPValue: groupHPValue,
      groupEvaluation: true
    });
  });
}

function checkIfGroupExists(responseText) { // Hybrid
  var doc = createDocument(responseText);
  var groupExistsIMG = $(doc)
    .find('img[title="Disband Group (Cancel Attack)"]');
  if (groupExistsIMG.length > 0) {
    var groupHref = groupExistsIMG.parents('td:first').find('a:first')
      .attr('href');
    retryAjax(groupHref).done(getCreatureGroupData);
  }
}

function addRemoveCreatureToDoNotKillList(evt) {
  var creatureName = evt.target.getAttribute('creatureName');
  var ind = calf.doNotKillList.indexOf(creatureName);
  if (ind !== -1) {
    calf.doNotKillList.splice(ind, 1);
    evt.target.innerHTML = 'Add to the do not kill list';
  } else {
    calf.doNotKillList.push(creatureName);
    evt.target.innerHTML = 'Remove from do not kill list';
  }
  setValue('doNotKillList', calf.doNotKillList.join());
  // refresh the action list
  afterUpdateActionList();
}

export default function readyViewCreature() { // Hybrid
  $('#creatureEvaluator').html('');
  $('#creatureEvaluatorGroup').html('');

  retryAjax('index.php?no_mobile=1&cmd=profile').done(function(html) {
    getCreaturePlayerData(html, {
      groupExists: false,
      groupAttackValue: 0,
      groupDefenseValue: 0,
      groupArmorValue: 0,
      groupDamageValue: 0,
      groupHPValue: 0,
      groupEvaluation: false
    });
  });
  retryAjax('index.php?no_mobile=1&cmd=guild&subcmd=groups')
    .done(checkIfGroupExists);

  $('#addRemoveCreatureToDoNotKillList').html('');
  if ($('#addRemoveCreatureToDoNotKillList').length === 0) {
    var doNotKillElement = '

';
    $(doNotKillElement).insertAfter($('#dialog-viewcreature')
      .find('p.description'));
  }
  var creatureName = $('#dialog-viewcreature').find('h2.name')
    .text();
  $('#addRemoveCreatureToDoNotKillList')
    .attr('creatureName', creatureName);
  var extraText = 'Add to the do not kill list';
  if (calf.doNotKillList.indexOf(creatureName) !== -1) {
    extraText = 'Remove from do not kill list';
  }
  $('#addRemoveCreatureToDoNotKillList').html(extraText);
  getElementById('addRemoveCreatureToDoNotKillList')
    .addEventListener('click',
      addRemoveCreatureToDoNotKillList, true);
}
<|endoftext|>


<commit_before>'use strict';
const yeoman = require('yeoman-generator');

module.exports = yeoman.Base.extend({
  prompting() {
    const done = this.async();
    this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        // Defaults to the project's folder name if not specified
        default: this.appname,
      },
      {
        type: 'input',
        name: 'description',
        message: 'Please enter a description for your project',
        default: '',
      },
      {
        type: 'list',
        name: 'language',
        message: 'Which language would you like to use?',
        default: 'es2015',
        choices: [
          {
            name: 'ES2015',
            value: 'es2015',
            short: 'es2015',
          },
          {
            name: 'TypeScript',
            value: 'ts',
            short: 'ts',
          },
        ],
      },
      {
        type: 'confirm',
        name: 'installDeps',
        message: 'Would you like to install all dependencies now?',
        default: false,
      },
    ], (answers) => {
      this.props = answers;
      done();
    });
  },

  writing() {
    const baseDevDeps = [
      /* eslint-disable quotes */
      `"clean-webpack-plugin": "^0.1.8",`,
      `"css-loader": "^0.23.1",`,
      `"eslint": "^2.5.3",`,
      `"eslint-plugin-react": "^4.2.3",`,
      `"extract-text-webpack-plugin": "^1.0.1",`,
      `"html-webpack-plugin": "^2.15.0",`,
      `"npm-install-webpack-plugin": "^3.0.0",`,
      `"rimraf": "^2.5.2",`,
      `"style-loader": "^0.13.1",`,
      `"webpack": "^1.12.14",`,
      `"webpack-dev-server": "^1.14.1",`,
      `"webpack-merge": "^0.8.4"`,
      /* eslint-enable quotes */
    ];

    const devDeps = (function buildPackageDeps(commonDeps, language) {
      let fullDeps;
      if (language === 'es2015') {
        fullDeps = commonDeps.concat(
          [
            /* eslint-disable quotes */
            `"babel-cli": "^6.6.5",`,
            `"babel-eslint": "^6.0.0",`,
            `"babel-loader": "^6.2.4",`,
            `"babel-plugin-array-includes": "^2.0.3",`,
            `"babel-plugin-transform-decorators-legacy": "^1.3.4",`,
            `"babel-plugin-transform-object-assign": "^6.5.0",`,
            `"babel-preset-es2015": "^6.6.0",`,
            `"babel-preset-react": "^6.5.0",`,
            `"babel-preset-react-hmre": "^1.1.1",`,
            `"babel-preset-stage-1": "^6.5.0",`,
            `"eslint-config-airbnb": "^6.2.0"`,
            /* eslint-enable quotes */
          ]
        );
      } else if (language === 'ts') {
        fullDeps = commonDeps.concat(
          [
            /* eslint-disable quotes */
            `"ts-loader": "^0.8.0",`,
            `"typescript": "^1.8.0",`,
            `"typings": "^0.7.0"`,
            /* eslint-enable quotes */
          ]
        );
      } else {
        throw new Error('No language was specified!');
      }

      return fullDeps.sort().join(`\n    `);
    }(baseDevDeps, this.props.language));

    // Package.json
    this.fs.copyTpl(
      this.templatePath('_package.json'),
      this.destinationPath('package.json'), {
        name: this.props.name,
        description: this.props.description,
        devDeps,
      }
    );

    // .gitignore
    this.fs.copy(
      this.templatePath('_gitignore'),
      this.destinationPath('.gitignore')
    );

    // set up eslint if using es2015
    if (this.props.language === 'es2015') {
      this.fs.copy(
        this.templatePath('_eslintrc.js'),
        this.destinationPath('.eslintrc.js')
      );
    }

    // webpack config
    this.fs.copyTpl(
      this.templatePath('_webpack.config.js'),
      this.destinationPath('webpack.config.js'),
      {
        name: this.props.name,
        language: this.props.language,
      }
    );

    // copy app src, dependenct on language choice
    this.fs.copyTpl(
      this.templatePath(`src_${this.props.language === 'es2015' ? 'es2015' : 'ts'}`),
      this.destinationPath('src'),
      {
        name: this.props.name,
        description: this.props.description,
      }
    );
  },

  install() {
    if (this.props.installDeps) {
      this.npmInstall();
    } else {
      this.log(`Skipping the install step. Run \`npm install\` inside the project root when
        you're ready.`);
    }
  },
});
<commit_msg>Fix string templating in `_package.json`<commit_after>'use strict';
const yeoman = require('yeoman-generator');

module.exports = yeoman.Base.extend({
  prompting() {
    const done = this.async();
    this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        // Defaults to the project's folder name if not specified
        default: this.appname,
      },
      {
        type: 'input',
        name: 'description',
        message: 'Please enter a description for your project',
        default: '',
      },
      {
        type: 'list',
        name: 'language',
        message: 'Which language would you like to use?',
        default: 'es2015',
        choices: [
          {
            name: 'ES2015',
            value: 'es2015',
            short: 'es2015',
          },
          {
            name: 'TypeScript',
            value: 'ts',
            short: 'ts',
          },
        ],
      },
      {
        type: 'confirm',
        name: 'installDeps',
        message: 'Would you like to install all dependencies now?',
        default: false,
      },
    ], (answers) => {
      this.props = answers;
      done();
    });
  },

  writing() {
    const baseDevDeps = [
      /* eslint-disable quotes */
      `"clean-webpack-plugin": "^0.1.8"`,
      `"css-loader": "^0.23.1"`,
      `"eslint": "^2.5.3"`,
      `"eslint-plugin-react": "^4.2.3"`,
      `"extract-text-webpack-plugin": "^1.0.1"`,
      `"html-webpack-plugin": "^2.15.0"`,
      `"npm-install-webpack-plugin": "^3.0.0"`,
      `"rimraf": "^2.5.2"`,
      `"style-loader": "^0.13.1"`,
      `"webpack": "^1.12.14"`,
      `"webpack-dev-server": "^1.14.1"`,
      `"webpack-merge": "^0.8.4"`,
      /* eslint-enable quotes */
    ];

    const devDeps = (function buildPackageDeps(commonDeps, language) {
      let fullDeps;
      if (language === 'es2015') {
        fullDeps = commonDeps.concat(
          [
            /* eslint-disable quotes */
            `"babel-cli": "^6.6.5"`,
            `"babel-eslint": "^6.0.0"`,
            `"babel-loader": "^6.2.4"`,
            `"babel-plugin-array-includes": "^2.0.3"`,
            `"babel-plugin-transform-decorators-legacy": "^1.3.4"`,
            `"babel-plugin-transform-object-assign": "^6.5.0"`,
            `"babel-preset-es2015": "^6.6.0"`,
            `"babel-preset-react": "^6.5.0"`,
            `"babel-preset-react-hmre": "^1.1.1"`,
            `"babel-preset-stage-1": "^6.5.0"`,
            `"eslint-config-airbnb": "^6.2.0"`,
            /* eslint-enable quotes */
          ]
        );
      } else if (language === 'ts') {
        fullDeps = commonDeps.concat(
          [
            /* eslint-disable quotes */
            `"ts-loader": "^0.8.0"`,
            `"typescript": "^1.8.0"`,
            `"typings": "^0.7.0"`,
            /* eslint-enable quotes */
          ]
        );
      } else {
        throw new Error('No language was specified!');
      }

      return fullDeps.sort().join(`,\n    `);
    }(baseDevDeps, this.props.language));

    // Package.json
    this.fs.copyTpl(
      this.templatePath('_package.json'),
      this.destinationPath('package.json'), {
        name: this.props.name,
        description: this.props.description,
        devDeps,
      }
    );

    // .gitignore
    this.fs.copy(
      this.templatePath('_gitignore'),
      this.destinationPath('.gitignore')
    );

    // set up eslint if using es2015
    if (this.props.language === 'es2015') {
      this.fs.copy(
        this.templatePath('_eslintrc.js'),
        this.destinationPath('.eslintrc.js')
      );
    }

    // webpack config
    this.fs.copyTpl(
      this.templatePath('_webpack.config.js'),
      this.destinationPath('webpack.config.js'),
      {
        name: this.props.name,
        language: this.props.language,
      }
    );

    // copy app src, dependenct on language choice
    this.fs.copyTpl(
      this.templatePath(`src_${this.props.language === 'es2015' ? 'es2015' : 'ts'}`),
      this.destinationPath('src'),
      {
        name: this.props.name,
        description: this.props.description,
      }
    );
  },

  install() {
    if (this.props.installDeps) {
      this.npmInstall();
    } else {
      this.log(`Skipping the install step. Run \`npm install\` inside the project root when
        you're ready.`);
    }
  },
});
<|endoftext|>


<commit_before>
Tinytest.add(
  'Client Side - Error Manager - Utils - stackFramesFilter()',
  function (test) {
    test.equal(typeof stackFramesFilter, 'function');
    test.equal(stackFramesFilter('/packages/zones/assets/zone.js'), false);
    test.equal(stackFramesFilter('/packages/foo/bar.js'), true);
  }
);

Tinytest.add(
  'Client Side - Error Manager - Utils - getCurrentOrigin()',
  function (test) {
    test.equal(typeof getCurrentOrigin, 'function');
    // Simple regex to catch origin part of the url
    // (may not work for all cases but sufficient for tests)
    var regex = /^https?:\/\/[a-zA-Z0-9\.]+:?[0-9]*$/;
    var origin = getCurrentOrigin();
    test.equal(typeof origin, 'string');
    test.equal(!!origin.match(regex), true);
  }
);

Tinytest.add(
  'Client Side - Error Manager - Utils - formatTraceLine()',
  function (test) {
    test.equal(typeof formatTraceLine, 'function');
    var lines = [
      '{anonymous}()@'+getCurrentOrigin()+'/foo/bar.js:12:34',
      'funName@'+getCurrentOrigin()+'/foo/bar.js:12:34'
    ];
    var expected = [
      '    at foo/bar.js:12:34',
      '    at funName (foo/bar.js:12:34)',
    ];
    for(var i=lines.length; i-->0;) {
      test.equal(expected[i], formatTraceLine(lines[i]));
    }
  }
);
<commit_msg>Add test for getNormalizedStacktrace()<commit_after>
Tinytest.add(
  'Client Side - Error Manager - Utils - stackFramesFilter()',
  function (test) {
    test.equal(typeof stackFramesFilter, 'function');
    test.equal(stackFramesFilter('/packages/zones/assets/zone.js'), false);
    test.equal(stackFramesFilter('/packages/foo/bar.js'), true);
  }
);

Tinytest.add(
  'Client Side - Error Manager - Utils - getCurrentOrigin()',
  function (test) {
    test.equal(typeof getCurrentOrigin, 'function');
    // Simple regex to catch origin part of the url
    // (may not work for all cases but sufficient for tests)
    var regex = /^https?:\/\/[a-zA-Z0-9\.]+:?[0-9]*$/;
    var origin = getCurrentOrigin();
    test.equal(typeof origin, 'string');
    test.equal(!!origin.match(regex), true);
  }
);

Tinytest.add(
  'Client Side - Error Manager - Utils - formatTraceLine()',
  function (test) {
    test.equal(typeof formatTraceLine, 'function');
    var lines = [
      '{anonymous}()@'+getCurrentOrigin()+'/foo/bar.js:12:34',
      'funName@'+getCurrentOrigin()+'/foo/bar.js:12:34'
    ];
    var expected = [
      '    at foo/bar.js:12:34',
      '    at funName (foo/bar.js:12:34)',
    ];
    for(var i=lines.length; i-->0;) {
      test.equal(expected[i], formatTraceLine(lines[i]));
    }
  }
);

Tinytest.add(
  'Client Side - Error Manager - Utils - getNormalizedStacktrace()',
  function (test) {
    test.equal(typeof getNormalizedStacktrace, 'function');
    var expected = [
      '    at foo/bar.js:12:34',
      '    at funName (foo/bar.js:12:34)',
    ].join('\n');
    var original_printStackTrace = printStackTrace;
    printStackTrace = mock_printStackTrace;
    test.equal(expected, getNormalizedStacktrace());
    printStackTrace = original_printStackTrace;

    function mock_printStackTrace() {
      var o = getCurrentOrigin();
      return [
        '{anonymous}()@'+o+'/packages/zones/assets/bar.js:12:34',
        'funName@'+o+'/packages/zones/assets/bar.js:12:34',
        '{anonymous}()@'+o+'/foo/bar.js:12:34',
        'funName@'+o+'/foo/bar.js:12:34'
      ];
    }
  }
);
<|endoftext|>


<commit_before>var path = require('path');
var templateData = require(path.resolve(__dirname, '..', '_config', 'templateData.json'));
var templateHelpers = require(path.resolve(__dirname, '..', '_config', 'templateHelpers.js'))();

var WebsiteController = function (website) {
	// Public functions
	var website = website;
	this.get = function(request, response) {
		if (!request.body) return response.sendStatus(400);
		var url = parseUrl(request.params.loc);

		response.render(url, createModel());
	};

	function createModel(params){
		var model = {
			layout: false,
			data: templateData,
			helpers: templateHelpers
		}

		return model;
	}

	function parseUrl(url){
		if(url === '/' || url === '' || url === undefined || url === 'favicon.ico'){
			// change to index or homepage when ready to work on site pages
			url = 'styleguide'
		}
		return url;
	}

};

module.exports = function(website) {
	var controller = new WebsiteController(website);
	website.get(['/','/:loc'], controller.get);

};<commit_msg>added * to deplink urls<commit_after>var path = require('path');
var templateData = require(path.resolve(__dirname, '..', '_config', 'templateData.json'));
var templateHelpers = require(path.resolve(__dirname, '..', '_config', 'templateHelpers.js'))();

var WebsiteController = function (website) {
	// Public functions
	var website = website;
	this.get = function(request, response) {
		if (!request.body) return response.sendStatus(400);
		var url = parseUrl(request.params.loc);

		response.render(url, createModel());
	};

	function createModel(params){
		var model = {
			layout: false,
			data: templateData,
			helpers: templateHelpers
		}

		return model;
	}

	function parseUrl(url){
		if(url === '/' || url === '' || url === undefined || url === 'favicon.ico'){
			// change to index or homepage when ready to work on site pages
			url = 'styleguide'
		}
		return url;
	}

};

module.exports = function(website) {
	var controller = new WebsiteController(website);
	website.get(['/*','/:loc'], controller.get);

};<|endoftext|>


<commit_before>import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
import EmberObject, { get, set, computed } from '@ember/object';
import DefaultSchema from 'ember-m3/services/m3-schema';
import { Errors as ModelErrors } from '@ember-data/model/-private';
import { Errors as StoreErrors } from '@ember-data/store/-private';

const Errors = ModelErrors || StoreErrors;

class TestSchemaFlagOn extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }

  useUnderlyingErrorsValue() {
    return true;
  }
}

class TestSchemaFlagOff extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }

  useUnderlyingErrorsValue() {
    return false;
  }
}

class TestSchemaNoOverride extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }
}

const errorsArray = [
  {
    path: ['talentHiringProjectsByCriteria'],
    locations: [{ column: 3, line: 2 }],
    message: 'Error calling findByCriteria.',
  },
];

module('unit/model/errors-attribute', function (hooks) {
  setupTest(hooks);

  hooks.beforeEach(function () {
    this.store = this.owner.lookup('service:store');
  });

  test('schema with flag set to true returns errors from payload', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            title: 'Harry Potter',
            errors: errorsArray,
          },
        },
      });
    });
    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      "schema's useUnderlyingErrorsValue value set to true should return payload errors array"
    );

    model.set('title', 'Goblin');

    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      'errors should still be accessible after setting a property'
    );
  });

  test('schema with flag set to true but no errors in payload returns undefined', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
          },
        },
      });
    });
    assert.equal(
      get(model, 'errors'),
      undefined,
      "schema's useUnderlyingErrorsValue returns true but missing data payload should return undefined"
    );
  });

  test('schema with flag set to true returns errors from payload', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      "schema's useUnderlyingErrorsValue returns true should return payload errors array"
    );
  });

  test('schema with flag set to false returns Errors object instance', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOff);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.ok(
      get(model, 'errors') instanceof ModelErrors,
      "schema's useUnderlyingErrorsValue returns false should return ModelsError object instance"
    );
  });

  test('schema with no flag property returns Errors object instance', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaNoOverride);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.ok(
      get(model, 'errors') instanceof ModelErrors,
      "schema's default useUnderlyingErrorsValue return value should return ModelsError object instance"
    );
  });
});
<commit_msg>fix: linting errors<commit_after>import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
import { get } from '@ember/object';
import DefaultSchema from 'ember-m3/services/m3-schema';
import { Errors as ModelErrors } from '@ember-data/model/-private';

class TestSchemaFlagOn extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }

  useUnderlyingErrorsValue() {
    return true;
  }
}

class TestSchemaFlagOff extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }

  useUnderlyingErrorsValue() {
    return false;
  }
}

class TestSchemaNoOverride extends DefaultSchema {
  includesModel(modelName) {
    return /^com.example.bookstore\./i.test(modelName);
  }
}

const errorsArray = [
  {
    path: ['talentHiringProjectsByCriteria'],
    locations: [{ column: 3, line: 2 }],
    message: 'Error calling findByCriteria.',
  },
];

module('unit/model/errors-attribute', function (hooks) {
  setupTest(hooks);

  hooks.beforeEach(function () {
    this.store = this.owner.lookup('service:store');
  });

  test('schema with flag set to true returns errors from payload', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            title: 'Harry Potter',
            errors: errorsArray,
          },
        },
      });
    });
    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      "schema's useUnderlyingErrorsValue value set to true should return payload errors array"
    );

    model.set('title', 'Goblin');

    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      'errors should still be accessible after setting a property'
    );
  });

  test('schema with flag set to true but no errors in payload returns undefined', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
          },
        },
      });
    });
    assert.equal(
      get(model, 'errors'),
      undefined,
      "schema's useUnderlyingErrorsValue returns true but missing data payload should return undefined"
    );
  });

  test('schema with flag set to true returns errors from payload', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOn);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.deepEqual(
      get(model, 'errors').get('firstObject'),
      errorsArray[0],
      "schema's useUnderlyingErrorsValue returns true should return payload errors array"
    );
  });

  test('schema with flag set to false returns Errors object instance', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaFlagOff);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.ok(
      get(model, 'errors') instanceof ModelErrors,
      "schema's useUnderlyingErrorsValue returns false should return ModelsError object instance"
    );
  });

  test('schema with no flag property returns Errors object instance', function (assert) {
    this.owner.register('service:m3-schema', TestSchemaNoOverride);

    let model = run(() => {
      return this.store.push({
        data: {
          id: 'urn:book:1',
          type: 'com.example.bookstore.Book',
          attributes: {
            author: ['urn:author:1'],
            errors: errorsArray,
          },
        },
      });
    });
    assert.ok(
      get(model, 'errors') instanceof ModelErrors,
      "schema's default useUnderlyingErrorsValue return value should return ModelsError object instance"
    );
  });
});
<|endoftext|>


<commit_before>(function () {
    "use strict";

    var gpii = fluid.registerNamespace("gpii");

    fluid.defaults("gpii.nexusSensorSonificationPanel", {
        gradeNames: ["gpii.nexusWebSocketBoundComponent", "fluid.viewComponent"],
        numberLocale: "en",
        events: {
            onSensorAppearance: null,
            onSensorRemoval: null
        },
        dynamicComponents: {
            sensorSonifier: {
                type: "gpii.sensorPlayer",
                createOnEvent: "onSensorAppearance",
                options: "@expand:gpii.nexusSensorSonificationPanel.getSensorOptions({arguments}.0)"
            }
        },
        members: {
            nexusPeerComponentPath: "scienceLabCollector",
            nexusBoundModelPath: "sensors",
            receivesChangesFromNexus: true,
            sendsChangesToNexus: false,
            attachedSensors: {}
        },
        strings: {
            noSensorsConnected: "No sensors connected"
        },
        modelListeners: {
            sensors: {
                funcName: "gpii.nexusSensorSonificationPanel.updateSonifications",
                args: [
                    "{that}",
                    "{change}.value"
                ]
            }
        }
    });

    gpii.nexusSensorSonificationPanel.getSensorOptions = function (sensorId) {

        var sensorModel = {
            sensorId: sensorId,
            description: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".name",
            simulateChanges: false,
            sensorValue: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".value",
            sensorMax: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".rangeMax",
            sensorMin: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".rangeMin"
        };

        var sensorContainerClass = "nexus-nexusSensorSonificationPanel-sensorDisplay-" + sensorId;

        var sensorPlayerOptions =
        {
            events: {
                onSensorDisplayContainerAppended: null
            },
            listeners: {
                "{nexusSensorSonificationPanel}.events.onSensorRemoval": {
                   funcName: "gpii.nexusSensorSonificationPanel.checkForRemoval",
                   args: ["{that}", "{that}.sensor", "{arguments}.0"],
                   namespace: "removeSensorPlayer"
               },
               "onCreate.appendSensorDisplayContainer": {
                   "this": "{nexusSensorSonificationPanel}.container",
                   "method": "append",
                   "args": ["

"]
               },
               "onCreate.fireOnSensorDisplayContainerAppended": {
                   funcName: "{that}.events.onSensorDisplayContainerAppended.fire"
               },
               "onDestroy.removeSensorDisplayContainer": {
                   funcName: "gpii.nexusSensorSonificationPanel.removeSensorDisplayContainer",
                   args: ["{nexusSensorSonificationPanel}", sensorContainerClass]
               }
           },
            components: {
                sensor: {
                    options: {
                        model: sensorModel
                    }
                },
                sensorDisplayDebug: {
                    type: "gpii.sensorPlayer.sensorDisplayDebug",
                    container: "." + sensorContainerClass,
                    createOnEvent: "{sensorPlayer}.events.onSensorDisplayContainerAppended",
                    options: {
                        // listeners: {
                        //     "onDestroy.removeDisplayContainer": {
                        //         "this": "{that}.container",
                        //         "method": "empty"
                        //     }
                        // }
                    }
                }
            }
        };
        return sensorPlayerOptions;
    };

    gpii.nexusSensorSonificationPanel.checkForRemoval = function (sensorPlayer, sensor, removedSensorId) {
        console.log("gpii.nexusSensorSonificationPanel.checkForRemoval");
        console.log(sensorPlayer, sensor, removedSensorId);
        if(fluid.get(sensor.model, "sensorId") === removedSensorId) {
            console.log("this sensorPlayer should be removed");
            sensorPlayer.destroy();
            console.log(sensorPlayer);
        }
    };

    gpii.nexusSensorSonificationPanel.removeSensorDisplayContainer = function (nexusSensorSonificationPanel, sensorContainerClass) {
        var removedSensorContainer = nexusSensorSonificationPanel.container.find("." + sensorContainerClass).remove();
        console.log(removedSensorContainer);
        console.log(nexusSensorSonificationPanel, sensorContainerClass);
    };

    gpii.nexusSensorSonificationPanel.updateSonifications = function (that, sensors) {

        var sensorsArray = fluid.hashToArray(
            sensors,
            "sensorId"
        );

        fluid.each(sensorsArray, function (sensor) {
            var sensorId = sensor.sensorId;

            // Add new sensor sonifiers
            if(! that.attachedSensors[sensorId]) {
                console.log("New sensorPlayer to add: " + sensorId);
                that.events.onSensorAppearance.fire(sensorId);
                that.attachedSensors[sensorId] = true;
            }
        });

        // Remove any sensor sonifiers for removed sensors
        fluid.each(that.attachedSensors, function (attachedSensor, attachedSensorId) {
            if (! sensors[attachedSensorId]) {
                console.log("Sensor to remove: " + attachedSensorId);
                console.log(that);
                that.events.onSensorRemoval.fire(attachedSensorId);
                that.attachedSensors[attachedSensorId] = false;
            }
        });
    };

}());
<commit_msg>GPII-2190: fade-out effect.<commit_after>(function () {
    "use strict";

    var gpii = fluid.registerNamespace("gpii");

    fluid.defaults("gpii.nexusSensorSonificationPanel", {
        gradeNames: ["gpii.nexusWebSocketBoundComponent", "fluid.viewComponent"],
        numberLocale: "en",
        events: {
            onSensorAppearance: null,
            onSensorRemoval: null
        },
        dynamicComponents: {
            sensorSonifier: {
                type: "gpii.sensorPlayer",
                createOnEvent: "onSensorAppearance",
                options: "@expand:gpii.nexusSensorSonificationPanel.getSensorOptions({arguments}.0)"
            }
        },
        members: {
            nexusPeerComponentPath: "scienceLabCollector",
            nexusBoundModelPath: "sensors",
            receivesChangesFromNexus: true,
            sendsChangesToNexus: false,
            attachedSensors: {}
        },
        strings: {
            noSensorsConnected: "No sensors connected"
        },
        modelListeners: {
            sensors: {
                funcName: "gpii.nexusSensorSonificationPanel.updateSonifications",
                args: [
                    "{that}",
                    "{change}.value"
                ]
            }
        }
    });

    gpii.nexusSensorSonificationPanel.getSensorOptions = function (sensorId) {

        var sensorModel = {
            sensorId: sensorId,
            description: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".name",
            simulateChanges: false,
            sensorValue: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".value",
            sensorMax: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".rangeMax",
            sensorMin: "{nexusSensorSonificationPanel}.model.sensors." + sensorId + ".rangeMin"
        };

        var sensorContainerClass = "nexus-nexusSensorSonificationPanel-sensorDisplay-" + sensorId;

        var sensorPlayerOptions =
        {
            events: {
                onSensorDisplayContainerAppended: null
            },
            listeners: {
                "{nexusSensorSonificationPanel}.events.onSensorRemoval": {
                   funcName: "gpii.nexusSensorSonificationPanel.checkForRemoval",
                   args: ["{that}", "{that}.sensor", "{arguments}.0"],
                   namespace: "removeSensorPlayer"
               },
               "onCreate.appendSensorDisplayContainer": {
                   "this": "{nexusSensorSonificationPanel}.container",
                   "method": "append",
                   "args": ["

"]
               },
               "onCreate.fireOnSensorDisplayContainerAppended": {
                   funcName: "{that}.events.onSensorDisplayContainerAppended.fire",
                   priority: "after:appendSensorDisplayContainer"
               },
               "onDestroy.removeSensorDisplayContainer": {
                   funcName: "gpii.nexusSensorSonificationPanel.removeSensorDisplayContainer",
                   args: ["{nexusSensorSonificationPanel}", sensorContainerClass]
               }
           },
            components: {
                sensor: {
                    options: {
                        model: sensorModel
                    }
                },
                sensorDisplayDebug: {
                    type: "gpii.sensorPlayer.sensorDisplayDebug",
                    container: "." + sensorContainerClass,
                    createOnEvent: "{sensorPlayer}.events.onSensorDisplayContainerAppended",
                    options: {
                        // listeners: {
                        //     "onDestroy.removeDisplayContainer": {
                        //         "this": "{that}.container",
                        //         "method": "empty"
                        //     }
                        // }
                    }
                }
            }
        };
        return sensorPlayerOptions;
    };

    gpii.nexusSensorSonificationPanel.checkForRemoval = function (sensorPlayer, sensor, removedSensorId) {
        console.log("gpii.nexusSensorSonificationPanel.checkForRemoval");
        console.log(sensorPlayer, sensor, removedSensorId);
        if(fluid.get(sensor.model, "sensorId") === removedSensorId) {
            console.log("this sensorPlayer should be removed");
            sensorPlayer.destroy();
            console.log(sensorPlayer);
        }
    };

    gpii.nexusSensorSonificationPanel.removeSensorDisplayContainer = function (nexusSensorSonificationPanel, sensorContainerClass) {
        var removedSensorContainer = nexusSensorSonificationPanel.container.find("." + sensorContainerClass);
        removedSensorContainer.fadeOut(function() {
            removedSensorContainer.remove();
        });
        console.log(removedSensorContainer);
        console.log(nexusSensorSonificationPanel, sensorContainerClass);
    };

    gpii.nexusSensorSonificationPanel.updateSonifications = function (that, sensors) {

        var sensorsArray = fluid.hashToArray(
            sensors,
            "sensorId"
        );

        fluid.each(sensorsArray, function (sensor) {
            var sensorId = sensor.sensorId;

            // Add new sensor sonifiers
            if(! that.attachedSensors[sensorId]) {
                console.log("New sensorPlayer to add: " + sensorId);
                that.events.onSensorAppearance.fire(sensorId);
                that.attachedSensors[sensorId] = true;
            }
        });

        // Remove any sensor sonifiers for removed sensors
        fluid.each(that.attachedSensors, function (attachedSensor, attachedSensorId) {
            if (! sensors[attachedSensorId]) {
                console.log("Sensor to remove: " + attachedSensorId);
                console.log(that);
                that.events.onSensorRemoval.fire(attachedSensorId);
                that.attachedSensors[attachedSensorId] = false;
            }
        });
    };

}());
<|endoftext|>