<commit_before>import React from 'react'
import _ from './utils'

const noop = () => {}
const reop = d => d

export const FormDefaultProps = {
  loadState: noop,
  defaultValues: {},
  preValidate: reop,
  validate: () => null,
  onValidationFail: noop,
  onChange: noop,
  saveState: noop,
  willUnmount: noop,
  preSubmit: reop,
  onSubmit: noop,
  postSubmit: noop,
  component: 'div'
}

export default React.createClass({
  displayName: 'Form',
  childContextTypes: {
    formAPI: React.PropTypes.object
  },
  getChildContext () {
    return {
      formAPI: this.getAPI()
    }
  },
  // Lifecycle
  getDefaultProps () {
    return FormDefaultProps
  },
  getInitialState () {
    const {
      defaultValues,
      values,
      loadState
    } = this.props
    const mergedValues = {
      ..._.clone(defaultValues),
      ..._.clone(values)
    }

    return loadState(this.props, this) || {
      values: mergedValues,
      touched: {},
      errors: this.validate(mergedValues),
      nestedErrors: {}
    }
  },
  componentWillMount () {
    this.emitChange(this.state, true)
  },
  componentWillReceiveProps (props) {
    if (props.values === this.props.values) {
      return
    }

    this.setFormState({
      values: _.clone(props.values) || {}
    }, true)
  },
  componentWillUnmount () {
    this.props.willUnmount(this.state, this.props, this)
  },

  // API
  setAllValues (values, noTouch) {
    if (noTouch) {
      return this.setFormState({values})
    }
    this.setFormState({values, touched: {}})
  },
  setValue (field, value, noTouch) {
    const state = this.state
    const values = _.set(state.values, field, value)
    // Also set touched since the value is changing
    if (noTouch) {
      return this.setFormState({values})
    }
    const touched = _.set(state.touched, field)
    this.setFormState({values, touched})
  },
  getValue (field, fallback) {
    const state = this.state
    const val = _.get(state.values, field)
    return typeof val !== 'undefined' ? val : fallback
  },
  setNestedError (field, value = true) {
    const nestedErrors = _.set(this.state.nestedErrors, field, value)
    this.setFormState({nestedErrors})
  },
  getError (field) {
    return _.get(this.state.errors, field)
  },
  setTouched (field, value = true) {
    const touched = _.set(this.state.touched, field, value)
    this.setFormState({touched})
  },
  getTouched (field) {
    const state = this.state
    if (this.state.dirty === true || this.props.touched === true) {
      return true
    }
    return _.get(state.touched, field)
  },
  addValue (field, value) {
    const state = this.state
    const values = _.set(state.values, field, [
      ..._.get(state.values, field, []),
      value
    ])
    this.setFormState({values})
  },
  removeValue (field, index) {
    const state = this.state
    const fieldValue = _.get(state.values, field, [])
    const values = _.set(state.values, field, [
      ...fieldValue.slice(0, index),
      ...fieldValue.slice(index + 1)
    ])
    this.setFormState({values})
  },
  swapValues (field, index, destIndex) {
    const state = this.state
    const fieldValues = _.get(state.values, field, [])
    const values = _.set(state.values, field, [
      ...fieldValues.slice(0, index),
      fieldValues[destIndex],
      ...fieldValues.slice(index + 1, destIndex),
      fieldValues[index],
      ...fieldValues.slice(destIndex + 1)
    ])
    this.setFormState({values})
  },
  setAllTouched (dirty = true, state) {
    this.setFormState({
      ...state,
      dirty: !!dirty
    })
  },
  resetForm () {
    return this.setFormState(this.getInitialState())
  },
  submitForm (e) {
    e && e.preventDefault && e.preventDefault(e)
    const state = this.state
    const errors = this.validate(state.values, state, this.props)
    if (errors) {
      if (!state.dirty) {
        this.setAllTouched(true, {errors})
      }
      return this.props.onValidationFail(state.values, state, this.props, this)
    }
    const preSubmitValues = this.props.preSubmit(state.values, state, this.props, this)
    this.props.onSubmit(preSubmitValues, state, this.props, this)
    this.props.postSubmit(preSubmitValues, state, this.props, this)
  },

  // Utils
  getAPI () {
    return {
      setAllValues: this.setAllValues,
      setValue: this.setValue,
      getValue: this.getValue,
      setNestedError: this.setNestedError,
      getError: this.getError,
      setTouched: this.setTouched,
      getTouched: this.getTouched,
      addValue: this.addValue,
      removeValue: this.removeValue,
      swapValues: this.swapValues,
      setAllTouched: this.setAllTouched,
      resetForm: this.resetForm,
      submitForm: this.submitForm
    }
  },
  setFormState (newState, silent) {
    if (newState && newState.values && !newState.errors) {
      newState.values = this.props.preValidate(newState.values, newState, this.props, this)
      newState.errors = this.validate(newState.values, newState, this.props)
    }
    this.setState(newState, () => {
      this.props.saveState(this.state, this.props, this)
      if (!silent) {
        this.emitChange(this.state, this.props)
      }
    })
  },
  emitChange (state, initial) {
    this.props.onChange(state, this.props, initial, this)
  },
  validate (values, state, props) {
    const errors = this.props.validate(
      removeNestedErrorValues(values, this.state ? this.state.nestedErrors : {}),
      state,
      props,
      this
    )
    return cleanErrors(errors)
  },
  // Render
  render () {
    const props = {
      ...this.props,
      ...this.state,
      ...this.getAPI()
    }
    const { component, children, ...rest } = props
    const resolvedChild = typeof children === 'function' ? children(rest) : children
    const RootEl = component
    if (!RootEl) {
      return resolvedChild
    }
    return (
      <RootEl className='ReactForm'>{resolvedChild}</RootEl>
    )
  }
})

// Utils

function cleanErrors (err) {
  if (_.isObject(err)) {
    const resolved = _.mapValues(err, cleanErrors)
    const found = _.pickBy(resolved, d => d)
    return Object.keys(found).length ? resolved : undefined
  }
  if (_.isArray(err)) {
    const resolved = err.map(cleanErrors)
    const found = resolved.find(d => d)
    return found ? resolved : undefined
  }
  return err
}

function removeNestedErrorValues (value, nestedErrors) {
  const recurse = (value, path = []) => {
    if (_.isObject(value)) {
      return _.mapValues(value, (d, i) => {
        return recurse(d, [...path, i])
      })
    }
    if (_.isArray(value)) {
      return value.map((d, key) => {
        return recurse(d, [...path, key])
      })
    }
    if (_.get(nestedErrors, path)) {
      return undefined
    }
    return value
  }
  return recurse(value)
}
<commit_msg>Fixed: removeNestedErrorValues is more accurate for fields nested in objects<commit_after>import React from 'react'
import _ from './utils'

const noop = () => {}
const reop = d => d

export const FormDefaultProps = {
  loadState: noop,
  defaultValues: {},
  preValidate: reop,
  validate: () => null,
  onValidationFail: noop,
  onChange: noop,
  saveState: noop,
  willUnmount: noop,
  preSubmit: reop,
  onSubmit: noop,
  postSubmit: noop,
  component: 'div'
}

export default React.createClass({
  displayName: 'Form',
  childContextTypes: {
    formAPI: React.PropTypes.object
  },
  getChildContext () {
    return {
      formAPI: this.getAPI()
    }
  },
  // Lifecycle
  getDefaultProps () {
    return FormDefaultProps
  },
  getInitialState () {
    const {
      defaultValues,
      values,
      loadState
    } = this.props
    const mergedValues = {
      ..._.clone(defaultValues),
      ..._.clone(values)
    }

    return loadState(this.props, this) || {
      values: mergedValues,
      touched: {},
      errors: this.validate(mergedValues),
      nestedErrors: {}
    }
  },
  componentWillMount () {
    this.emitChange(this.state, true)
  },
  componentWillReceiveProps (props) {
    if (props.values === this.props.values) {
      return
    }

    this.setFormState({
      values: _.clone(props.values) || {}
    }, true)
  },
  componentWillUnmount () {
    this.props.willUnmount(this.state, this.props, this)
  },

  // API
  setAllValues (values, noTouch) {
    if (noTouch) {
      return this.setFormState({values})
    }
    this.setFormState({values, touched: {}})
  },
  setValue (field, value, noTouch) {
    const state = this.state
    const values = _.set(state.values, field, value)
    // Also set touched since the value is changing
    if (noTouch) {
      return this.setFormState({values})
    }
    const touched = _.set(state.touched, field)
    this.setFormState({values, touched})
  },
  getValue (field, fallback) {
    const state = this.state
    const val = _.get(state.values, field)
    return typeof val !== 'undefined' ? val : fallback
  },
  setNestedError (field, value = true) {
    const nestedErrors = _.set(this.state.nestedErrors, field, value)
    this.setFormState({nestedErrors})
  },
  getError (field) {
    return _.get(this.state.errors, field)
  },
  setTouched (field, value = true) {
    const touched = _.set(this.state.touched, field, value)
    this.setFormState({touched})
  },
  getTouched (field) {
    const state = this.state
    if (this.state.dirty === true || this.props.touched === true) {
      return true
    }
    return _.get(state.touched, field)
  },
  addValue (field, value) {
    const state = this.state
    const values = _.set(state.values, field, [
      ..._.get(state.values, field, []),
      value
    ])
    this.setFormState({values})
  },
  removeValue (field, index) {
    const state = this.state
    const fieldValue = _.get(state.values, field, [])
    const values = _.set(state.values, field, [
      ...fieldValue.slice(0, index),
      ...fieldValue.slice(index + 1)
    ])
    this.setFormState({values})
  },
  swapValues (field, index, destIndex) {
    const state = this.state
    const fieldValues = _.get(state.values, field, [])
    const values = _.set(state.values, field, [
      ...fieldValues.slice(0, index),
      fieldValues[destIndex],
      ...fieldValues.slice(index + 1, destIndex),
      fieldValues[index],
      ...fieldValues.slice(destIndex + 1)
    ])
    this.setFormState({values})
  },
  setAllTouched (dirty = true, state) {
    this.setFormState({
      ...state,
      dirty: !!dirty
    })
  },
  resetForm () {
    return this.setFormState(this.getInitialState())
  },
  submitForm (e) {
    e && e.preventDefault && e.preventDefault(e)
    const state = this.state
    const errors = this.validate(state.values, state, this.props)
    if (errors) {
      if (!state.dirty) {
        this.setAllTouched(true, {errors})
      }
      return this.props.onValidationFail(state.values, state, this.props, this)
    }
    const preSubmitValues = this.props.preSubmit(state.values, state, this.props, this)
    this.props.onSubmit(preSubmitValues, state, this.props, this)
    this.props.postSubmit(preSubmitValues, state, this.props, this)
  },

  // Utils
  getAPI () {
    return {
      setAllValues: this.setAllValues,
      setValue: this.setValue,
      getValue: this.getValue,
      setNestedError: this.setNestedError,
      getError: this.getError,
      setTouched: this.setTouched,
      getTouched: this.getTouched,
      addValue: this.addValue,
      removeValue: this.removeValue,
      swapValues: this.swapValues,
      setAllTouched: this.setAllTouched,
      resetForm: this.resetForm,
      submitForm: this.submitForm
    }
  },
  setFormState (newState, silent) {
    if (newState && newState.values && !newState.errors) {
      newState.values = this.props.preValidate(newState.values, newState, this.props, this)
      newState.errors = this.validate(newState.values, newState, this.props)
    }
    this.setState(newState, () => {
      this.props.saveState(this.state, this.props, this)
      if (!silent) {
        this.emitChange(this.state, this.props)
      }
    })
  },
  emitChange (state, initial) {
    this.props.onChange(state, this.props, initial, this)
  },
  validate (values, state, props) {
    const errors = this.props.validate(
      removeNestedErrorValues(values, this.state ? this.state.nestedErrors : {}),
      state,
      props,
      this
    )
    return cleanErrors(errors)
  },
  // Render
  render () {
    const props = {
      ...this.props,
      ...this.state,
      ...this.getAPI()
    }
    const { component, children, ...rest } = props
    const resolvedChild = typeof children === 'function' ? children(rest) : children
    const RootEl = component
    if (!RootEl) {
      return resolvedChild
    }
    return (
      <RootEl className='ReactForm'>{resolvedChild}</RootEl>
    )
  }
})

// Utils

function cleanErrors (err) {
  if (_.isObject(err)) {
    const resolved = _.mapValues(err, cleanErrors)
    const found = _.pickBy(resolved, d => d)
    return Object.keys(found).length ? resolved : undefined
  }
  if (_.isArray(err)) {
    const resolved = err.map(cleanErrors)
    const found = resolved.find(d => d)
    return found ? resolved : undefined
  }
  return err
}

// removeNestedErrorValues recurses the values object and turns any
// field that has a truthy corresponding nested form error field into undefined.
// This allows properly validating a nested form by detecting that undefined value
// in the validation function
function removeNestedErrorValues (value, nestedErrors) {
  const recurse = (value, path = []) => {
    const errorVal = _.get(nestedErrors, path)
    if (!_.isObject(errorVal) && !_.isArray(errorVal) && errorVal) {
      return undefined
    }
    if (_.isObject(value)) {
      return _.mapValues(value, (d, i) => {
        return recurse(d, [...path, i])
      })
    }
    if (_.isArray(value)) {
      return value.map((d, key) => {
        return recurse(d, [...path, key])
      })
    }
    if (_.get(nestedErrors, path)) {
      return undefined
    }
    return value
  }
  return recurse(value)
}
<|endoftext|>


<commit_before>import { getIdByType, getProxyByType, getSignature, isColdType, updateProxyById } from './proxies';
import { hotComparisonOpen } from '../global/generation';
import {
  getElementType,
  isContextType,
  isForwardType,
  isLazyType,
  isMemoType,
  isReactClass,
  isReloadableComponent,
} from '../internal/reactUtils';
import { areSwappable } from './utils';
import { PROXY_KEY, UNWRAP_PROXY } from '../proxy';
import { resolveType } from './resolver';
import logger from '../logger';
import configuration from '../configuration';

const getInnerComponentType = component => {
  const unwrapper = component[UNWRAP_PROXY];
  return unwrapper ? unwrapper() : component;
};

function haveEqualSignatures(prevType, nextType) {
  try {
    const prevSignature = getSignature(prevType);
    const nextSignature = getSignature(nextType);

    if (prevSignature === undefined && nextSignature === undefined) {
      return true;
    }
    if (prevSignature === undefined || nextSignature === undefined) {
      return false;
    }
    if (prevSignature.key !== nextSignature.key) {
      return false;
    }

    // TODO: we might need to calculate previous signature earlier in practice,
    // such as during the first time a component is resolved. We'll revisit this.
    const prevCustomHooks = prevSignature.getCustomHooks();
    const nextCustomHooks = nextSignature.getCustomHooks();
    if (prevCustomHooks.length !== nextCustomHooks.length) {
      return false;
    }

    for (let i = 0; i < nextCustomHooks.length; i++) {
      if (!haveEqualSignatures(prevCustomHooks[i], nextCustomHooks[i])) {
        return false;
      }
    }
  } catch (e) {
    logger.error('React-Hot-Loader: error occurred while comparing hook signature', e);
    return false;
  }

  return true;
}

const areSignaturesCompatible = (a, b) => {
  // compare signatures of two components
  // non-equal component have to remount and there is two options to do it
  // - fail the comparison, remounting all tree below
  // - fulfill it, but set `_debugNeedsRemount` on a fiber to drop only local state
  // the second way is not published yet, so going with the first one
  if (!haveEqualSignatures(a, b)) {
    logger.warn('⚛🔥🎣 Hook order change detected: component', a, 'has been remounted');
    return false;
  }
  return true;
};

const compareRegistered = (a, b) => {
  if (getIdByType(a) === getIdByType(b)) {
    if (getProxyByType(a) !== getProxyByType(b)) {
      return false;
    }
  }

  return areSignaturesCompatible(a, b);
};

const areDeepSwappable = (oldType, newType) => {
  const type = { type: oldType };

  if (typeof oldType === 'function') {
    return areSwappable(oldType, newType);
  }

  if (isForwardType(type)) {
    return areDeepSwappable(oldType.render, newType.render);
  }

  if (isMemoType(type)) {
    return areDeepSwappable(oldType.type, newType.type);
  }

  // that's not safe
  // if (isLazyType(type)) {
  //   return areDeepSwappable(oldType._ctor, newType._ctor)
  // }

  return false;
};

const compareComponents = (oldType, newType, setNewType, baseType) => {
  let defaultResult = oldType === newType;

  if (
    (oldType && !newType) ||
    (!oldType && newType) ||
    typeof oldType !== typeof newType ||
    getElementType(oldType) !== getElementType(newType)
  ) {
    return defaultResult;
  }

  if (getIdByType(oldType)) {
    if (!compareRegistered(oldType, newType)) {
      return false;
    }
    defaultResult = true;
  }

  if (isForwardType({ type: oldType }) && isForwardType({ type: newType })) {
    if (!compareRegistered(oldType.render, newType.render)) {
      return false;
    }
    if (oldType.render === newType.render || areDeepSwappable(oldType, newType)) {
      setNewType(newType);
      return true;
    }
    return defaultResult;
  }

  if (isMemoType({ type: oldType }) && isMemoType({ type: newType })) {
    if (!compareRegistered(oldType.type, newType.type)) {
      return false;
    }
    if (oldType.type === newType.type || areDeepSwappable(oldType, newType)) {
      if (baseType) {
        // memo form different fibers, why?
        if (baseType.$$typeof === newType.$$typeof) {
          setNewType(newType);
        } else {
          setNewType(newType.type);
        }
      } else {
        logger.warn('Please update hot-loader/react-dom');
        if (isReactClass(newType.type)) {
          setNewType(newType);
        } else {
          setNewType(newType.type);
        }
      }

      return true;
    }
    return defaultResult;
  }

  if (isLazyType({ type: oldType })) {
    return defaultResult;
  }

  if (isContextType({ type: oldType })) {
    return defaultResult;
  }

  if (
    typeof newType === 'function' &&
    (defaultResult ||
      (newType !== oldType && areSignaturesCompatible(newType, oldType) && areSwappable(newType, oldType)))
  ) {
    const unwrapFactory = newType[UNWRAP_PROXY];
    const oldProxy = unwrapFactory && getProxyByType(unwrapFactory());
    if (oldProxy) {
      oldProxy.dereference();
      updateProxyById(oldType[PROXY_KEY] || getIdByType(oldType), getInnerComponentType(newType));
    } else {
      setNewType(newType);
    }
    return true;
  }

  return defaultResult;
};

const knownPairs = new WeakMap();
const emptyMap = new WeakMap();

export const hotComponentCompare = (oldType, preNewType, setNewType, baseType) => {
  const hotActive = hotComparisonOpen();
  const newType = configuration.integratedResolver ? resolveType(preNewType) : preNewType;
  let result = oldType === newType;

  if (!hotActive) {
    return result;
  }

  if (
    !isReloadableComponent(oldType) ||
    !isReloadableComponent(newType) ||
    isColdType(oldType) ||
    isColdType(oldType) ||
    !oldType ||
    !newType ||
    0
  ) {
    return result;
  }

  // comparison should be active only if hot update window
  // or it would merge components it shall not
  if (hotActive) {
    result = compareComponents(oldType, newType, setNewType, baseType);
    const pair = knownPairs.get(oldType) || new WeakMap();
    pair.set(newType, result);
    knownPairs.set(oldType, pair);
    return result;
  }

  if (result) {
    return result;
  }

  const pair = knownPairs.get(oldType) || emptyMap;
  return pair.get(newType) || false;
};
<commit_msg>fix: regression of registered type comparison, fixes #1284<commit_after>import { getIdByType, getProxyByType, getSignature, isColdType, updateProxyById } from './proxies';
import { hotComparisonOpen } from '../global/generation';
import {
  getElementType,
  isContextType,
  isForwardType,
  isLazyType,
  isMemoType,
  isReactClass,
  isReloadableComponent,
} from '../internal/reactUtils';
import { areSwappable } from './utils';
import { PROXY_KEY, UNWRAP_PROXY } from '../proxy';
import { resolveType } from './resolver';
import logger from '../logger';
import configuration from '../configuration';

const getInnerComponentType = component => {
  const unwrapper = component[UNWRAP_PROXY];
  return unwrapper ? unwrapper() : component;
};

function haveEqualSignatures(prevType, nextType) {
  try {
    const prevSignature = getSignature(prevType);
    const nextSignature = getSignature(nextType);

    if (prevSignature === undefined && nextSignature === undefined) {
      return true;
    }
    if (prevSignature === undefined || nextSignature === undefined) {
      return false;
    }
    if (prevSignature.key !== nextSignature.key) {
      return false;
    }

    // TODO: we might need to calculate previous signature earlier in practice,
    // such as during the first time a component is resolved. We'll revisit this.
    const prevCustomHooks = prevSignature.getCustomHooks();
    const nextCustomHooks = nextSignature.getCustomHooks();
    if (prevCustomHooks.length !== nextCustomHooks.length) {
      return false;
    }

    for (let i = 0; i < nextCustomHooks.length; i++) {
      if (!haveEqualSignatures(prevCustomHooks[i], nextCustomHooks[i])) {
        return false;
      }
    }
  } catch (e) {
    logger.error('React-Hot-Loader: error occurred while comparing hook signature', e);
    return false;
  }

  return true;
}

const areSignaturesCompatible = (a, b) => {
  // compare signatures of two components
  // non-equal component have to remount and there is two options to do it
  // - fail the comparison, remounting all tree below
  // - fulfill it, but set `_debugNeedsRemount` on a fiber to drop only local state
  // the second way is not published yet, so going with the first one
  if (!haveEqualSignatures(a, b)) {
    logger.warn('⚛🔥🎣 Hook order change detected: component', a, 'has been remounted');
    return false;
  }
  return true;
};

const compareRegistered = (a, b) =>
  getIdByType(a) === getIdByType(b) && getProxyByType(a) === getProxyByType(b) && areSignaturesCompatible(a, b);

const areDeepSwappable = (oldType, newType) => {
  const type = { type: oldType };

  if (typeof oldType === 'function') {
    return areSwappable(oldType, newType);
  }

  if (isForwardType(type)) {
    return areDeepSwappable(oldType.render, newType.render);
  }

  if (isMemoType(type)) {
    return areDeepSwappable(oldType.type, newType.type);
  }

  // that's not safe
  // if (isLazyType(type)) {
  //   return areDeepSwappable(oldType._ctor, newType._ctor)
  // }

  return false;
};

const compareComponents = (oldType, newType, setNewType, baseType) => {
  let defaultResult = oldType === newType;

  if (
    (oldType && !newType) ||
    (!oldType && newType) ||
    typeof oldType !== typeof newType ||
    getElementType(oldType) !== getElementType(newType) ||
    0
  ) {
    return defaultResult;
  }

  if (getIdByType(newType) || getIdByType(oldType)) {
    if (!compareRegistered(oldType, newType)) {
      return false;
    }
    defaultResult = true;
  }

  if (isForwardType({ type: oldType }) && isForwardType({ type: newType })) {
    if (!compareRegistered(oldType.render, newType.render)) {
      return false;
    }
    if (oldType.render === newType.render || areDeepSwappable(oldType, newType)) {
      setNewType(newType);
      return true;
    }
    return defaultResult;
  }

  if (isMemoType({ type: oldType }) && isMemoType({ type: newType })) {
    if (!compareRegistered(oldType.type, newType.type)) {
      return false;
    }
    if (oldType.type === newType.type || areDeepSwappable(oldType, newType)) {
      if (baseType) {
        // memo form different fibers, why?
        if (baseType.$$typeof === newType.$$typeof) {
          setNewType(newType);
        } else {
          setNewType(newType.type);
        }
      } else {
        logger.warn('Please update hot-loader/react-dom');
        if (isReactClass(newType.type)) {
          setNewType(newType);
        } else {
          setNewType(newType.type);
        }
      }

      return true;
    }
    return defaultResult;
  }

  if (isLazyType({ type: oldType })) {
    return defaultResult;
  }

  if (isContextType({ type: oldType })) {
    return defaultResult;
  }

  if (
    typeof newType === 'function' &&
    (defaultResult ||
      (newType !== oldType && areSignaturesCompatible(newType, oldType) && areSwappable(newType, oldType)))
  ) {
    const unwrapFactory = newType[UNWRAP_PROXY];
    const oldProxy = unwrapFactory && getProxyByType(unwrapFactory());
    if (oldProxy) {
      oldProxy.dereference();
      updateProxyById(oldType[PROXY_KEY] || getIdByType(oldType), getInnerComponentType(newType));
    } else {
      setNewType(newType);
    }
    return true;
  }

  return defaultResult;
};

const knownPairs = new WeakMap();
const emptyMap = new WeakMap();

export const hotComponentCompare = (oldType, preNewType, setNewType, baseType) => {
  const hotActive = hotComparisonOpen();
  const newType = configuration.integratedResolver ? resolveType(preNewType) : preNewType;
  let result = oldType === newType;

  if (!hotActive) {
    return result;
  }

  if (
    !isReloadableComponent(oldType) ||
    !isReloadableComponent(newType) ||
    isColdType(oldType) ||
    isColdType(oldType) ||
    !oldType ||
    !newType ||
    0
  ) {
    return result;
  }

  // comparison should be active only if hot update window
  // or it would merge components it shall not
  if (hotActive) {
    result = compareComponents(oldType, newType, setNewType, baseType);
    const pair = knownPairs.get(oldType) || new WeakMap();
    pair.set(newType, result);
    knownPairs.set(oldType, pair);
    return result;
  }

  if (result) {
    return result;
  }

  const pair = knownPairs.get(oldType) || emptyMap;
  return pair.get(newType) || false;
};
<|endoftext|>


<commit_before>import fetch from 'isomorphic-fetch';
import 'es6-promise';
import { showStatus } from '../modules/statuses/statusProgress';
import { push } from 'react-router-redux';

/**
 * Middleware that performs an API call if the next action contains one.
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
export function apiMiddleware () {
  return next => action => {
    return isRequest(action) ? callApi(action, next) : next(action);
  };
}

/**
 * Checks if the next action is a request.
 * @param  {[string]} { url } [api url]
 * @return {Boolean}  [true if request, else false]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function isRequest ({ API_URL }) {
  return API_URL !== 'undefined' && typeof API_URL === 'string';
}

/**
 * Validates the http status from the API.
 * @param  {[object]} response [response from api]
 * @return {[object or error]}
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function checkStatus (response) {
  if (response.status >= 200 && response.status <= 304) {
    return response;
  } else {
    // Invalid response
    const error = new Error(response.statusText || response.status);
    error.response = response.json();
    throw error;
  }
}

/**
 * @param  {[object]} response  [from API]
 * @return {[object]}           [json object]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function parseJSON (response) {
  return response.json();
}

/**
 * Performs a call to the API.
 * @param  {[array]}  types     [status object and success action]
 * @param  {[type]}   method    [GET, POST and so on...]
 * @param  {[type]}   headers   [request headers]
 * @param  {[type]}   body      [body parameters]
 * @param  {[type]}   url       [api url]
 * @param  {Function} next      [dispatch]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function callApi (action, next) {
  const { types, method, headers, body, API_URL, ...rest } = action;
  const [ STATUS, SUCCESS, FAILURE ] = types;

  // Dispatch loading status if it's specified
  if (STATUS !== false) {
    next(showStatus(STATUS, true, null, null));
  }

  /* Specify api call parameters */
  let params = {};
  params.method = method || 'get';
  params.headers = headers || {};
  params.body = JSON.stringify(body) || {};

  /**
   * Perform the api call.
   */
  return fetch(API_URL, params)
  .then(checkStatus)
  .then(parseJSON)
  .then(data => {
    // Dispatch and show success status if it's specified
    if (STATUS !== false) {
      next(showStatus(STATUS, false, 'success', action.successMessage));
    }

    // Dispatch the success action
    next({ ...rest, payload: data, type: SUCCESS });

    // Redirect after success if it's specified
    if (action.redirectSuccess !== 'undefined') {
      next(push(action.redirectSuccess));
    }
  })
  .catch(error => {
    // Dispatch and show error status if it's specified
    if (STATUS !== false) {
      next(showStatus(STATUS, false, 'error', action.errorMessage));
    }

    // Dispatch error action
    next({ ...rest, error, type: FAILURE });

    // Redirect after error if it's specified
    if (action.redirectError !== 'undefined') {
      next(push(action.redirectError));
    }
  });
}
<commit_msg>Fixed failure action validation<commit_after>import fetch from 'isomorphic-fetch';
import 'es6-promise';
import { showStatus } from '../modules/statuses/statusProgress';
import { push } from 'react-router-redux';

/**
 * Middleware that performs an API call if the next action contains one.
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
export function apiMiddleware () {
  return next => action => {
    return isRequest(action) ? callApi(action, next) : next(action);
  };
}

/**
 * Checks if the next action is a request.
 * @param  {[string]} { url } [api url]
 * @return {Boolean}  [true if request, else false]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function isRequest ({ API_URL }) {
  return API_URL !== 'undefined' && typeof API_URL === 'string';
}

/**
 * Validates the http status from the API.
 * @param  {[object]} response [response from api]
 * @return {[object or error]}
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function checkStatus (response) {
  if (response.status >= 200 && response.status <= 304) {
    return response;
  } else {
    // Invalid response
    const error = new Error(response.statusText || response.status);
    error.response = response.json();
    throw error;
  }
}

/**
 * @param  {[object]} response  [from API]
 * @return {[object]}           [json object]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function parseJSON (response) {
  return response.json();
}

/**
 * Performs a call to the API.
 * @param  {[array]}  types     [status object and success action]
 * @param  {[type]}   method    [GET, POST and so on...]
 * @param  {[type]}   headers   [request headers]
 * @param  {[type]}   body      [body parameters]
 * @param  {[type]}   url       [api url]
 * @param  {Function} next      [dispatch]
 * @author Johan Gustafsson <johan.gustafsson@solidio.se>
 */
function callApi (action, next) {
  const { types, method, headers, body, API_URL, ...rest } = action;
  const [ STATUS, SUCCESS, FAILURE ] = types;

  /* Specify api call parameters */
  let params = {};
  params.method = method || 'get';
  params.headers = headers || {};
  params.body = JSON.stringify(body) || {};

  // Dispatch loading status if specified
  if (STATUS !== false) {
    next(showStatus(STATUS, true, null, null));
  }

  /**
   * Perform the api call.
   */
  return fetch(API_URL, params)
  .then(checkStatus)
  .then(parseJSON)
  .then(data => {
    // Dispatch and show success status if specified
    if (STATUS !== false) {
      next(showStatus(STATUS, false, 'success', action.successMessage));
    }

    // Dispatch the success action
    next({ ...rest, payload: data, type: SUCCESS });

    // Redirect after success if specified
    if (action.redirectSuccess !== undefined) {
      next(push(action.redirectSuccess));
    }
  })
  .catch(error => {
    // Dispatch and show error status if specified
    if (STATUS !== false) {
      next(showStatus(STATUS, false, 'error', action.errorMessage));
    }

    /* The FAILURE action is optional.
    Set it to false if not specified so it can be dispatched
    (actions should not have an undefined "type" property). */
    next({ ...rest, error, type: FAILURE === undefined ? false : FAILURE });

    // Redirect after error if specified
    if (action.redirectError !== undefined) {
      next(push(action.redirectError));
    }
  });
}
<|endoftext|>


<commit_before>import { FrontSide, BackSide, DoubleSide, RGBAFormat, NearestFilter, PCFShadowMap, VSMShadowMap, RGBADepthPacking, NoBlending } from '../../constants.js';
import { WebGLRenderTarget } from '../WebGLRenderTarget.js';
import { MeshDepthMaterial } from '../../materials/MeshDepthMaterial.js';
import { MeshDistanceMaterial } from '../../materials/MeshDistanceMaterial.js';
import { ShaderMaterial } from '../../materials/ShaderMaterial.js';
import { BufferAttribute } from '../../core/BufferAttribute.js';
import { BufferGeometry } from '../../core/BufferGeometry.js';
import { Mesh } from '../../objects/Mesh.js';
import { Vector4 } from '../../math/Vector4.js';
import { Vector2 } from '../../math/Vector2.js';
import { Frustum } from '../../math/Frustum.js';

import * as vsm from '../shaders/ShaderLib/vsm.glsl.js';

function WebGLShadowMap( _renderer, _objects, _capabilities ) {

	let _frustum = new Frustum();

	const _shadowMapSize = new Vector2(),
		_viewportSize = new Vector2(),

		_viewport = new Vector4(),

		_depthMaterial = new MeshDepthMaterial( { depthPacking: RGBADepthPacking } ),
		_distanceMaterial = new MeshDistanceMaterial(),

		_materialCache = {},

		_maxTextureSize = _capabilities.maxTextureSize;

	const shadowSide = { 0: BackSide, 1: FrontSide, 2: DoubleSide };

	const shadowMaterialVertical = new ShaderMaterial( {
		defines: {
			VSM_SAMPLES: 8
		},
		uniforms: {
			shadow_pass: { value: null },
			resolution: { value: new Vector2() },
			radius: { value: 4.0 }
		},

		vertexShader: vsm.vertex,
		fragmentShader: vsm.fragment

	} );

	const shadowMaterialHorizontal = shadowMaterialVertical.clone();
	shadowMaterialHorizontal.defines.HORIZONTAL_PASS = 1;

	const fullScreenTri = new BufferGeometry();
	fullScreenTri.setAttribute(
		'position',
		new BufferAttribute(
			new Float32Array( [ - 1, - 1, 0.5, 3, - 1, 0.5, - 1, 3, 0.5 ] ),
			3
		)
	);

	const fullScreenMesh = new Mesh( fullScreenTri, shadowMaterialVertical );

	const scope = this;

	this.enabled = false;

	this.autoUpdate = true;
	this.needsUpdate = false;

	this.type = PCFShadowMap;

	this.render = function ( lights, scene, camera ) {

		if ( scope.enabled === false ) return;
		if ( scope.autoUpdate === false && scope.needsUpdate === false ) return;

		if ( lights.length === 0 ) return;

		const currentRenderTarget = _renderer.getRenderTarget();
		const activeCubeFace = _renderer.getActiveCubeFace();
		const activeMipmapLevel = _renderer.getActiveMipmapLevel();

		const _state = _renderer.state;

		// Set GL state for depth map.
		_state.setBlending( NoBlending );
		_state.buffers.color.setClear( 1, 1, 1, 1 );
		_state.buffers.depth.setTest( true );
		_state.setScissorTest( false );

		// render depth map

		for ( let i = 0, il = lights.length; i < il; i ++ ) {

			const light = lights[ i ];
			const shadow = light.shadow;

			if ( shadow === undefined ) {

				console.warn( 'THREE.WebGLShadowMap:', light, 'has no shadow.' );
				continue;

			}

			if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;

			_shadowMapSize.copy( shadow.mapSize );

			const shadowFrameExtents = shadow.getFrameExtents();

			_shadowMapSize.multiply( shadowFrameExtents );

			_viewportSize.copy( shadow.mapSize );

			if ( _shadowMapSize.x > _maxTextureSize || _shadowMapSize.y > _maxTextureSize ) {

				if ( _shadowMapSize.x > _maxTextureSize ) {

					_viewportSize.x = Math.floor( _maxTextureSize / shadowFrameExtents.x );
					_shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;
					shadow.mapSize.x = _viewportSize.x;

				}

				if ( _shadowMapSize.y > _maxTextureSize ) {

					_viewportSize.y = Math.floor( _maxTextureSize / shadowFrameExtents.y );
					_shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;
					shadow.mapSize.y = _viewportSize.y;

				}

			}

			if ( shadow.map === null && ! shadow.isPointLightShadow && this.type === VSMShadowMap ) {

				shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y );
				shadow.map.texture.name = light.name + '.shadowMap';

				shadow.mapPass = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y );

				shadow.camera.updateProjectionMatrix();

			}

			if ( shadow.map === null ) {

				const pars = { minFilter: NearestFilter, magFilter: NearestFilter, format: RGBAFormat };

				shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
				shadow.map.texture.name = light.name + '.shadowMap';

				shadow.camera.updateProjectionMatrix();

			}

			_renderer.setRenderTarget( shadow.map );
			_renderer.clear();

			const viewportCount = shadow.getViewportCount();

			for ( let vp = 0; vp < viewportCount; vp ++ ) {

				const viewport = shadow.getViewport( vp );

				_viewport.set(
					_viewportSize.x * viewport.x,
					_viewportSize.y * viewport.y,
					_viewportSize.x * viewport.z,
					_viewportSize.y * viewport.w
				);

				_state.viewport( _viewport );

				shadow.updateMatrices( light, vp );

				_frustum = shadow.getFrustum();

				renderObject( scene, camera, shadow.camera, light, this.type );

			}

			// do blur pass for VSM

			if ( ! shadow.isPointLightShadow && this.type === VSMShadowMap ) {

				VSMPass( shadow, camera );

			}

			shadow.needsUpdate = false;

		}

		scope.needsUpdate = false;

		_renderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );

	};

	function VSMPass( shadow, camera ) {

		const geometry = _objects.update( fullScreenMesh );

		if ( shadowMaterialVertical.defines.VSM_SAMPLES !== shadow.blurSamples ) {

			shadowMaterialVertical.defines.VSM_SAMPLES = shadow.blurSamples;
			shadowMaterialHorizontal.defines.VSM_SAMPLES = shadow.blurSamples;

			shadowMaterialVertical.needsUpdate = true;
			shadowMaterialHorizontal.needsUpdate = true;

		}

		// vertical pass

		shadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.texture;
		shadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialVertical.uniforms.radius.value = shadow.radius;
		_renderer.setRenderTarget( shadow.mapPass );
		_renderer.clear();
		_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null );

		// horizontal pass

		shadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;
		shadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialHorizontal.uniforms.radius.value = shadow.radius;
		_renderer.setRenderTarget( shadow.map );
		_renderer.clear();
		_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null );

	}

	function getDepthMaterial( object, material, light, shadowCameraNear, shadowCameraFar, type ) {

		let result = null;

		const customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial;

		if ( customMaterial !== undefined ) {

			result = customMaterial;

		} else {

			result = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;

		}

		if ( ( _renderer.localClippingEnabled && material.clipShadows === true && material.clippingPlanes.length !== 0 ) ||
			( material.displacementMap && material.displacementScale !== 0 ) ||
			( material.alphaMap && material.alphaTest > 0 ) ) {

			// in this case we need a unique material instance reflecting the
			// appropriate state

			const keyA = result.uuid, keyB = material.uuid;

			let materialsForVariant = _materialCache[ keyA ];

			if ( materialsForVariant === undefined ) {

				materialsForVariant = {};
				_materialCache[ keyA ] = materialsForVariant;

			}

			let cachedMaterial = materialsForVariant[ keyB ];

			if ( cachedMaterial === undefined ) {

				cachedMaterial = result.clone();
				materialsForVariant[ keyB ] = cachedMaterial;

			}

			result = cachedMaterial;

		}

		result.visible = material.visible;
		result.wireframe = material.wireframe;

		if ( type === VSMShadowMap ) {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;

		} else {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : shadowSide[ material.side ];

		}

		result.alphaMap = material.alphaMap;
		result.alphaTest = material.alphaTest;

		result.clipShadows = material.clipShadows;
		result.clippingPlanes = material.clippingPlanes;
		result.clipIntersection = material.clipIntersection;

		result.displacementMap = material.displacementMap;
		result.displacementScale = material.displacementScale;
		result.displacementBias = material.displacementBias;

		result.wireframeLinewidth = material.wireframeLinewidth;
		result.linewidth = material.linewidth;

		if ( light.isPointLight === true && result.isMeshDistanceMaterial === true ) {

			result.referencePosition.setFromMatrixPosition( light.matrixWorld );
			result.nearDistance = shadowCameraNear;
			result.farDistance = shadowCameraFar;

		}

		return result;

	}

	function renderObject( object, camera, shadowCamera, light, type ) {

		if ( object.visible === false ) return;

		const visible = object.layers.test( camera.layers );

		if ( visible && ( object.isMesh || object.isLine || object.isPoints ) ) {

			if ( ( object.castShadow || ( object.receiveShadow && type === VSMShadowMap ) ) && ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) ) {

				object.modelViewMatrix.multiplyMatrices( shadowCamera.matrixWorldInverse, object.matrixWorld );

				const geometry = _objects.update( object );
				const material = object.material;

				if ( Array.isArray( material ) ) {

					const groups = geometry.groups;

					for ( let k = 0, kl = groups.length; k < kl; k ++ ) {

						const group = groups[ k ];
						const groupMaterial = material[ group.materialIndex ];

						if ( groupMaterial && groupMaterial.visible ) {

							const depthMaterial = getDepthMaterial( object, groupMaterial, light, shadowCamera.near, shadowCamera.far, type );

							_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, group );

						}

					}

				} else if ( material.visible ) {

					const depthMaterial = getDepthMaterial( object, material, light, shadowCamera.near, shadowCamera.far, type );

					_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, null );

				}

			}

		}

		const children = object.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			renderObject( children[ i ], camera, shadowCamera, light, type );

		}

	}

}

export { WebGLShadowMap };
<commit_msg>WebGLShadowMap: Fix enabling VSM. (#24223)<commit_after>import { FrontSide, BackSide, DoubleSide, NearestFilter, PCFShadowMap, VSMShadowMap, RGBADepthPacking, NoBlending } from '../../constants.js';
import { WebGLRenderTarget } from '../WebGLRenderTarget.js';
import { MeshDepthMaterial } from '../../materials/MeshDepthMaterial.js';
import { MeshDistanceMaterial } from '../../materials/MeshDistanceMaterial.js';
import { ShaderMaterial } from '../../materials/ShaderMaterial.js';
import { BufferAttribute } from '../../core/BufferAttribute.js';
import { BufferGeometry } from '../../core/BufferGeometry.js';
import { Mesh } from '../../objects/Mesh.js';
import { Vector4 } from '../../math/Vector4.js';
import { Vector2 } from '../../math/Vector2.js';
import { Frustum } from '../../math/Frustum.js';

import * as vsm from '../shaders/ShaderLib/vsm.glsl.js';

function WebGLShadowMap( _renderer, _objects, _capabilities ) {

	let _frustum = new Frustum();

	const _shadowMapSize = new Vector2(),
		_viewportSize = new Vector2(),

		_viewport = new Vector4(),

		_depthMaterial = new MeshDepthMaterial( { depthPacking: RGBADepthPacking } ),
		_distanceMaterial = new MeshDistanceMaterial(),

		_materialCache = {},

		_maxTextureSize = _capabilities.maxTextureSize;

	const shadowSide = { 0: BackSide, 1: FrontSide, 2: DoubleSide };

	const shadowMaterialVertical = new ShaderMaterial( {
		defines: {
			VSM_SAMPLES: 8
		},
		uniforms: {
			shadow_pass: { value: null },
			resolution: { value: new Vector2() },
			radius: { value: 4.0 }
		},

		vertexShader: vsm.vertex,
		fragmentShader: vsm.fragment

	} );

	const shadowMaterialHorizontal = shadowMaterialVertical.clone();
	shadowMaterialHorizontal.defines.HORIZONTAL_PASS = 1;

	const fullScreenTri = new BufferGeometry();
	fullScreenTri.setAttribute(
		'position',
		new BufferAttribute(
			new Float32Array( [ - 1, - 1, 0.5, 3, - 1, 0.5, - 1, 3, 0.5 ] ),
			3
		)
	);

	const fullScreenMesh = new Mesh( fullScreenTri, shadowMaterialVertical );

	const scope = this;

	this.enabled = false;

	this.autoUpdate = true;
	this.needsUpdate = false;

	this.type = PCFShadowMap;

	this.render = function ( lights, scene, camera ) {

		if ( scope.enabled === false ) return;
		if ( scope.autoUpdate === false && scope.needsUpdate === false ) return;

		if ( lights.length === 0 ) return;

		const currentRenderTarget = _renderer.getRenderTarget();
		const activeCubeFace = _renderer.getActiveCubeFace();
		const activeMipmapLevel = _renderer.getActiveMipmapLevel();

		const _state = _renderer.state;

		// Set GL state for depth map.
		_state.setBlending( NoBlending );
		_state.buffers.color.setClear( 1, 1, 1, 1 );
		_state.buffers.depth.setTest( true );
		_state.setScissorTest( false );

		// render depth map

		for ( let i = 0, il = lights.length; i < il; i ++ ) {

			const light = lights[ i ];
			const shadow = light.shadow;

			if ( shadow === undefined ) {

				console.warn( 'THREE.WebGLShadowMap:', light, 'has no shadow.' );
				continue;

			}

			if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;

			_shadowMapSize.copy( shadow.mapSize );

			const shadowFrameExtents = shadow.getFrameExtents();

			_shadowMapSize.multiply( shadowFrameExtents );

			_viewportSize.copy( shadow.mapSize );

			if ( _shadowMapSize.x > _maxTextureSize || _shadowMapSize.y > _maxTextureSize ) {

				if ( _shadowMapSize.x > _maxTextureSize ) {

					_viewportSize.x = Math.floor( _maxTextureSize / shadowFrameExtents.x );
					_shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;
					shadow.mapSize.x = _viewportSize.x;

				}

				if ( _shadowMapSize.y > _maxTextureSize ) {

					_viewportSize.y = Math.floor( _maxTextureSize / shadowFrameExtents.y );
					_shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;
					shadow.mapSize.y = _viewportSize.y;

				}

			}

			if ( shadow.map === null ) {

				const pars = ( this.type !== VSMShadowMap ) ? { minFilter: NearestFilter, magFilter: NearestFilter } : {};

				shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
				shadow.map.texture.name = light.name + '.shadowMap';

				shadow.camera.updateProjectionMatrix();

			}

			_renderer.setRenderTarget( shadow.map );
			_renderer.clear();

			const viewportCount = shadow.getViewportCount();

			for ( let vp = 0; vp < viewportCount; vp ++ ) {

				const viewport = shadow.getViewport( vp );

				_viewport.set(
					_viewportSize.x * viewport.x,
					_viewportSize.y * viewport.y,
					_viewportSize.x * viewport.z,
					_viewportSize.y * viewport.w
				);

				_state.viewport( _viewport );

				shadow.updateMatrices( light, vp );

				_frustum = shadow.getFrustum();

				renderObject( scene, camera, shadow.camera, light, this.type );

			}

			// do blur pass for VSM

			if ( shadow.isPointLightShadow !== true && this.type === VSMShadowMap ) {

				VSMPass( shadow, camera );

			}

			shadow.needsUpdate = false;

		}

		scope.needsUpdate = false;

		_renderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );

	};

	function VSMPass( shadow, camera ) {

		const geometry = _objects.update( fullScreenMesh );

		if ( shadowMaterialVertical.defines.VSM_SAMPLES !== shadow.blurSamples ) {

			shadowMaterialVertical.defines.VSM_SAMPLES = shadow.blurSamples;
			shadowMaterialHorizontal.defines.VSM_SAMPLES = shadow.blurSamples;

			shadowMaterialVertical.needsUpdate = true;
			shadowMaterialHorizontal.needsUpdate = true;

		}

		if ( shadow.mapPass === null ) {

			shadow.mapPass = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y );

		}

		// vertical pass

		shadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.texture;
		shadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialVertical.uniforms.radius.value = shadow.radius;
		_renderer.setRenderTarget( shadow.mapPass );
		_renderer.clear();
		_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null );

		// horizontal pass

		shadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;
		shadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;
		shadowMaterialHorizontal.uniforms.radius.value = shadow.radius;
		_renderer.setRenderTarget( shadow.map );
		_renderer.clear();
		_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null );

	}

	function getDepthMaterial( object, material, light, shadowCameraNear, shadowCameraFar, type ) {

		let result = null;

		const customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial;

		if ( customMaterial !== undefined ) {

			result = customMaterial;

		} else {

			result = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;

		}

		if ( ( _renderer.localClippingEnabled && material.clipShadows === true && material.clippingPlanes.length !== 0 ) ||
			( material.displacementMap && material.displacementScale !== 0 ) ||
			( material.alphaMap && material.alphaTest > 0 ) ) {

			// in this case we need a unique material instance reflecting the
			// appropriate state

			const keyA = result.uuid, keyB = material.uuid;

			let materialsForVariant = _materialCache[ keyA ];

			if ( materialsForVariant === undefined ) {

				materialsForVariant = {};
				_materialCache[ keyA ] = materialsForVariant;

			}

			let cachedMaterial = materialsForVariant[ keyB ];

			if ( cachedMaterial === undefined ) {

				cachedMaterial = result.clone();
				materialsForVariant[ keyB ] = cachedMaterial;

			}

			result = cachedMaterial;

		}

		result.visible = material.visible;
		result.wireframe = material.wireframe;

		if ( type === VSMShadowMap ) {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;

		} else {

			result.side = ( material.shadowSide !== null ) ? material.shadowSide : shadowSide[ material.side ];

		}

		result.alphaMap = material.alphaMap;
		result.alphaTest = material.alphaTest;

		result.clipShadows = material.clipShadows;
		result.clippingPlanes = material.clippingPlanes;
		result.clipIntersection = material.clipIntersection;

		result.displacementMap = material.displacementMap;
		result.displacementScale = material.displacementScale;
		result.displacementBias = material.displacementBias;

		result.wireframeLinewidth = material.wireframeLinewidth;
		result.linewidth = material.linewidth;

		if ( light.isPointLight === true && result.isMeshDistanceMaterial === true ) {

			result.referencePosition.setFromMatrixPosition( light.matrixWorld );
			result.nearDistance = shadowCameraNear;
			result.farDistance = shadowCameraFar;

		}

		return result;

	}

	function renderObject( object, camera, shadowCamera, light, type ) {

		if ( object.visible === false ) return;

		const visible = object.layers.test( camera.layers );

		if ( visible && ( object.isMesh || object.isLine || object.isPoints ) ) {

			if ( ( object.castShadow || ( object.receiveShadow && type === VSMShadowMap ) ) && ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) ) {

				object.modelViewMatrix.multiplyMatrices( shadowCamera.matrixWorldInverse, object.matrixWorld );

				const geometry = _objects.update( object );
				const material = object.material;

				if ( Array.isArray( material ) ) {

					const groups = geometry.groups;

					for ( let k = 0, kl = groups.length; k < kl; k ++ ) {

						const group = groups[ k ];
						const groupMaterial = material[ group.materialIndex ];

						if ( groupMaterial && groupMaterial.visible ) {

							const depthMaterial = getDepthMaterial( object, groupMaterial, light, shadowCamera.near, shadowCamera.far, type );

							_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, group );

						}

					}

				} else if ( material.visible ) {

					const depthMaterial = getDepthMaterial( object, material, light, shadowCamera.near, shadowCamera.far, type );

					_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, null );

				}

			}

		}

		const children = object.children;

		for ( let i = 0, l = children.length; i < l; i ++ ) {

			renderObject( children[ i ], camera, shadowCamera, light, type );

		}

	}

}

export { WebGLShadowMap };
<|endoftext|>


<commit_before>/*
 * responsive-carousel pagination extension
 * https://github.com/filamentgroup/responsive-carousel
 *
 * Copyright (c) 2012 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

(function( $, undefined ) {
	var pluginName = "carousel",
		initSelector = "." + pluginName + "[data-paginate]",
		paginationClass = pluginName + "-pagination",
		activeClass = pluginName + "-active-page",
		paginationMethods = {
			_createPagination: function(){
				var nav = $( this ).find( "." + pluginName + "-nav" ),
					items = $( this ).find( "." + pluginName + "-item" ),
					pNav = $( "

" ),
					num, thumb, content, itemType;

				// remove any existing nav
				nav.find( "." + paginationClass ).remove();

				items.each(function(i){
						num = i + 1;
						thumb = $( this ).attr( "data-thumb" );
						itemType = $( this ).attr( "data-type" );
						content = num;
						if( thumb ){
							content = "";
						}
						pNav.append( "
" + (itemType ? itemType : content )+ "" );
					if( itemType ){
						nav.addClass( "has-" + itemType );
					}
				});

				if( thumb ){
					pNav.addClass( pluginName + "-nav-thumbs" );
				}

				nav
					.addClass( pluginName + "-nav-paginated" )
					.find( "a" ).first().after( pNav );

			},
			_bindPaginationEvents: function(){
				$( this )
					.bind( "click", function( e ){
						var pagLink = $( e.target );

						if( e.target.nodeName === "IMG" ){
							pagLink = pagLink.parent();
						}

						pagLink = pagLink.closest( "a" );
						var href = pagLink.attr( "href" );

						if( pagLink.closest( "." + paginationClass ).length && href ){
							$( this )[ pluginName ]( "goTo", parseFloat( href.split( "#" )[ 1 ] ) );
							e.preventDefault();
						}
					} )
					// update pagination on page change
					.bind( "goto." + pluginName, function( e, to  ){
						var index = to ? $( this ).find( "div.carousel-item" ).index( to.get( 0 ) ) : 0;

						$( this ).find( "ol." + paginationClass + " li" )
							.removeClass( activeClass )
							.eq( index )
								.addClass( activeClass );
					} )
					// initialize pagination
					.trigger( "goto." + pluginName );
			}
		};

	// add methods
	$.extend( $.fn[ pluginName ].prototype, paginationMethods );

	// create pagination on create and update
	$( document )
		.bind( "create." + pluginName, function( e ){
			$( e.target )
				[ pluginName ]( "_createPagination" )
				[ pluginName ]( "_bindPaginationEvents" );
		} )
		.bind( "update." + pluginName, function( e ){
			$( e.target )[ pluginName ]( "_createPagination" );
		} );

}(jQuery));<commit_msg>the element handed off with the goto event is not a jquery object<commit_after>/*
 * responsive-carousel pagination extension
 * https://github.com/filamentgroup/responsive-carousel
 *
 * Copyright (c) 2012 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

(function( $, undefined ) {
	var pluginName = "carousel",
		initSelector = "." + pluginName + "[data-paginate]",
		paginationClass = pluginName + "-pagination",
		activeClass = pluginName + "-active-page",
		paginationMethods = {
			_createPagination: function(){
				var nav = $( this ).find( "." + pluginName + "-nav" ),
					items = $( this ).find( "." + pluginName + "-item" ),
					pNav = $( "

" ),
					num, thumb, content, itemType;

				// remove any existing nav
				nav.find( "." + paginationClass ).remove();

				items.each(function(i){
						num = i + 1;
						thumb = $( this ).attr( "data-thumb" );
						itemType = $( this ).attr( "data-type" );
						content = num;
						if( thumb ){
							content = "";
						}
						pNav.append( "
" + (itemType ? itemType : content )+ "" );
					if( itemType ){
						nav.addClass( "has-" + itemType );
					}
				});

				if( thumb ){
					pNav.addClass( pluginName + "-nav-thumbs" );
				}

				nav
					.addClass( pluginName + "-nav-paginated" )
					.find( "a" ).first().after( pNav );

			},
			_bindPaginationEvents: function(){
				$( this )
					.bind( "click", function( e ){
						var pagLink = $( e.target );

						if( e.target.nodeName === "IMG" ){
							pagLink = pagLink.parent();
						}

						pagLink = pagLink.closest( "a" );
						var href = pagLink.attr( "href" );

						if( pagLink.closest( "." + paginationClass ).length && href ){
							$( this )[ pluginName ]( "goTo", parseFloat( href.split( "#" )[ 1 ] ) );
							e.preventDefault();
						}
					} )
					// update pagination on page change
					.bind( "goto." + pluginName, function( e, to  ){
						to = $( to );

						var index = to ? $( this ).find( "div.carousel-item" ).index( to.get( 0 ) ) : 0;

						$( this ).find( "ol." + paginationClass + " li" )
							.removeClass( activeClass )
							.eq( index )
								.addClass( activeClass );
					} )
					// initialize pagination
					.trigger( "goto." + pluginName );
			}
		};

	// add methods
	$.extend( $.fn[ pluginName ].prototype, paginationMethods );

	// create pagination on create and update
	$( document )
		.bind( "create." + pluginName, function( e ){
			$( e.target )
				[ pluginName ]( "_createPagination" )
				[ pluginName ]( "_bindPaginationEvents" );
		} )
		.bind( "update." + pluginName, function( e ){
			$( e.target )[ pluginName ]( "_createPagination" );
		} );

}(jQuery));
<|endoftext|>


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

import {ActivityIframePort} from 'web-activities/activity-ports';
import {AnalyticsEvent,
    AnalyticsRequest,
    EventOriginator} from '../proto/api_messages';
import {AnalyticsService} from './analytics-service';
import {ConfiguredRuntime} from './runtime';
import {PageConfig} from '../model/page-config';
import {feArgs, feUrl} from './services';
import {getStyle} from '../utils/style';
import {setExperimentsStringForTesting, setExperiment} from './experiments';
import {ClientEventManager} from './client-event-manager';
import {ExperimentFlags} from './experiment-flags';

describes.realWin('AnalyticsService', {}, env => {
  let win;
  let src;
  let activityPorts;
  let activityIframePort;
  let analyticsService;
  let pageConfig;
  let messageCallback;
  let runtime;
  const productId = 'pub1:label1';
  const event = {
    eventType: AnalyticsEvent.ACTION_SUBSCRIBE,
    eventOriginator: EventOriginator.SWG_CLIENT,
    isFromUserAction: null,
    additionalParameters: {},
  };

  beforeEach(() => {
    win = env.win;
    src = '/serviceiframe';
    pageConfig = new PageConfig(productId);
    runtime = new ConfiguredRuntime(win, pageConfig);
    activityPorts = runtime.activities();
    analyticsService = new AnalyticsService(runtime);
    activityIframePort = new ActivityIframePort(
        analyticsService.getElement(),
        feUrl(src), activityPorts);

    sandbox.stub(
        activityPorts,
        'openIframe',
        () => Promise.resolve(activityIframePort));

    sandbox.stub(
        activityIframePort,
        'whenReady',
        () => Promise.resolve(true));

    sandbox.stub(
        activityIframePort,
        'onMessage',
        cb => {
          messageCallback = cb;
        });
  });

  afterEach(() => {
    setExperimentsStringForTesting('');
  });

  describe('AnalyticsService', () => {
    it('should have analyticsService constructed', () => {
      const activityIframe = analyticsService.getElement();
      const transactionId = analyticsService.getTransactionId();
      expect(analyticsService.getTransactionId()).to.equal(transactionId);
      expect(analyticsService.getTransactionId())
          .to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/g);
      expect(activityIframe.nodeType).to.equal(1);
      expect(activityIframe.nodeName).to.equal('IFRAME');
      expect(getStyle(activityIframe, 'display')).to.equal('none');
      const txId = 'tx-id-101';
      analyticsService.setTransactionId(txId);
      expect(analyticsService.getTransactionId()).to.equal(txId);
    });

    it('should yield onMessage callback and call openIframe', () => {
      let messageReceived;
      analyticsService.onMessage(data => {
        messageReceived = data;
      });
      return analyticsService.lastAction_.then(() => {
        messageCallback({'something': 'irrelevant'});
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        const firstArgument = activityPorts.openIframe.getCall(0).args[0];
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        expect(firstArgument.nodeName).to.equal('IFRAME');
        const secondArgument = activityPorts.openIframe.getCall(0).args[1];
        expect(secondArgument).to.equal(feUrl(src));
        const thirdArgument = activityPorts.openIframe.getCall(0).args[2];
        expect(thirdArgument).to.deep.equal(feArgs({
          publicationId: pageConfig.getPublicationId(),
        }));
        return activityIframePort.whenReady();
      }).then(() => {
        expect(messageReceived).to.deep.equal({'something': 'irrelevant'});
      });
    });

    it('should send message on port and openIframe called only once', () => {
      sandbox.stub(
          activityIframePort,
          'message'
      );
      analyticsService.listener_({
        eventType: AnalyticsEvent.UNKNOWN,
        eventOriginator: EventOriginator.UNKNOWN_CLIENT,
        isFromUserAction: null,
        additionalParameters: {},
      });
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
          new AnalyticsRequest(firstArgument['buf']);
        expect(request.getEvent()).to.deep.equal(AnalyticsEvent.UNKNOWN);
        analyticsService.listener_({
          eventType: AnalyticsEvent.IMPRESSION_PAYWALL,
          eventOriginator: EventOriginator.SWG_CLIENT,
          isFromUserAction: null,
          additionalParameters: {},
        });
        return analyticsService.lastAction_;
      }).then(() => {
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        const firstArgument = activityPorts.openIframe.getCall(0).args[0];
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        expect(firstArgument.nodeName).to.equal('IFRAME');
        const secondArgument = activityPorts.openIframe.getCall(0).args[1];
        expect(secondArgument).to.equal(feUrl(src));
        const thirdArgument = activityPorts.openIframe.getCall(0).args[2];
        expect(thirdArgument).to.deep.equal(feArgs({
          publicationId: pageConfig.getPublicationId(),
        }));
        const messageArgument = activityIframePort.message.getCall(1).args[0];
        expect(messageArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
          new AnalyticsRequest(messageArgument['buf']);
        expect(request.getEvent()).to.deep.equal(
            AnalyticsEvent.IMPRESSION_PAYWALL);
      });
    });

    it('should create correct context for logging', () => {
      sandbox.stub(
          activityIframePort,
          'message'
      );
      AnalyticsService.prototype.getQueryString_ = () => {
        return '?utm_source=scenic&utm_medium=email&utm_campaign=campaign';
      };
      AnalyticsService.prototype.getReferrer_ = () => {
        return 'https://scenic-2017.appspot.com/landing.html';
      };
      analyticsService.setReadyToPay(true);
      analyticsService.setSku('basic');
      analyticsService.listener_(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getEvent()).to.deep.equal(
            AnalyticsEvent.ACTION_SUBSCRIBE);
        expect(request.getContext()).to.not.be.null;
        expect(request.getContext().getReferringOrigin()).to.equal(
            'https://scenic-2017.appspot.com');
        expect(request.getContext().getUtmMedium()).to.equal('email');
        expect(request.getContext().getUtmSource()).to.equal('scenic');
        expect(request.getContext().getUtmCampaign()).to.equal('campaign');
        expect(request.getContext().getTransactionId())
            .to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/g);
        expect(request.getContext().getSku()).to.equal('basic');
        expect(request.getContext().getReadyToPay()).to.be.true;
      });
    });

    it('should set context for empty experiments', () => {
      setExperimentsStringForTesting('');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      analyticsService.listener_(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList()).to.deep.equal([]);
      });
    });

    it('should set context for non-empty experiments', () => {
      setExperimentsStringForTesting('experiment-A,experiment-B');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      analyticsService.listener_(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['experiment-A', 'experiment-B']);
      });
    });

    it('should add additional labels to experiments', () => {
      analyticsService.addLabels(['L1', 'L2']);
      setExperimentsStringForTesting('E1,E2');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      analyticsService.listener_(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        const request = new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['L1', 'L2', 'E1', 'E2']);

        analyticsService.addLabels(['L3', 'L4']);
        analyticsService.listener_(event);
        return analyticsService.lastAction_;
      }).then(() => {
        const firstArgument = activityIframePort.message.getCall(1).args[0];
        const request = new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['L1', 'L2', 'E1', 'E2', 'L3', 'L4']);
      });
    });

    it('should dedupe duplicate labels', () => {
      analyticsService.addLabels(['L1', 'L2', 'L1', 'L2']);
      expect(analyticsService.context_.getLabelList())
          .to.deep.equal(['L1', 'L2']);
      analyticsService.addLabels(['L1', 'L2', 'L3']);
      expect(analyticsService.context_.getLabelList())
          .to.deep.equal(['L1', 'L2', 'L3']);
    });

    it('should be listening for events from events manager', () => {
      let receivedEvent = null;
      sandbox.stub(AnalyticsService.prototype, 'listener_',
          event => receivedEvent = event);
      analyticsService.eventManager_.logEvent(event);
      expect(receivedEvent).to.deep.equal(event);
    });

    it('should pass events along to events manager', () => {
      let receivedEvent = null;
      sandbox.stub(ClientEventManager.prototype, 'logEvent',
          event => receivedEvent = event);
      analyticsService.logEvent(AnalyticsEvent.ACTION_ACCOUNT_CREATED);
      expect(receivedEvent).to.deep.equal({
        eventType: AnalyticsEvent.ACTION_ACCOUNT_CREATED,
        eventOriginator: EventOriginator.SWG_CLIENT,
        isFromUserAction: null,
        additionalParameters: {},
      });
    });

    it('should respect the Propensity logging experiment flag', () => {
      //should log all clients but propensity
      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.SWG_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.AMP_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.PROPENSITY_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.be.null;

      //should now log all clients
      setExperiment(win, ExperimentFlags.LOG_PROPENSITY_TO_SWG, true);
      analyticsService = new AnalyticsService(runtime);
      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.SWG_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.AMP_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.PROPENSITY_CLIENT;
      analyticsService.listener_(event);
      expect(analyticsService.lastAction_).to.not.be.null;
    });
  });
});
<commit_msg>avoid directly calling private function in test<commit_after>/**
 * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ActivityIframePort} from 'web-activities/activity-ports';
import {AnalyticsEvent,
    AnalyticsRequest,
    EventOriginator} from '../proto/api_messages';
import {AnalyticsService} from './analytics-service';
import {ConfiguredRuntime} from './runtime';
import {PageConfig} from '../model/page-config';
import {feArgs, feUrl} from './services';
import {getStyle} from '../utils/style';
import {setExperimentsStringForTesting, setExperiment} from './experiments';
import {ClientEventManager} from './client-event-manager';
import {ExperimentFlags} from './experiment-flags';

describes.realWin('AnalyticsService', {}, env => {
  let win;
  let src;
  let activityPorts;
  let activityIframePort;
  let analyticsService;
  let pageConfig;
  let messageCallback;
  let runtime;
  let registeredCallback;

  const productId = 'pub1:label1';
  const event = {
    eventType: AnalyticsEvent.ACTION_SUBSCRIBE,
    eventOriginator: EventOriginator.SWG_CLIENT,
    isFromUserAction: null,
    additionalParameters: {},
  };

  beforeEach(() => {
    sandbox.stub(ClientEventManager.prototype, 'registerEventListener',
        callback => registeredCallback = callback);
    win = env.win;
    src = '/serviceiframe';
    pageConfig = new PageConfig(productId);
    runtime = new ConfiguredRuntime(win, pageConfig);
    activityPorts = runtime.activities();
    analyticsService = new AnalyticsService(runtime);
    activityIframePort = new ActivityIframePort(
        analyticsService.getElement(),
        feUrl(src), activityPorts);

    sandbox.stub(
        activityPorts,
        'openIframe',
        () => Promise.resolve(activityIframePort));

    sandbox.stub(
        activityIframePort,
        'whenReady',
        () => Promise.resolve(true));

    sandbox.stub(
        activityIframePort,
        'onMessage',
        cb => {
          messageCallback = cb;
        });
  });

  afterEach(() => {
    setExperimentsStringForTesting('');
  });

  describe('AnalyticsService', () => {
    it('should be listening for events from events manager', () => {
      expect(registeredCallback).to.not.be.null;
    });

    it('should have analyticsService constructed', () => {
      const activityIframe = analyticsService.getElement();
      const transactionId = analyticsService.getTransactionId();
      expect(analyticsService.getTransactionId()).to.equal(transactionId);
      expect(analyticsService.getTransactionId())
          .to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/g);
      expect(activityIframe.nodeType).to.equal(1);
      expect(activityIframe.nodeName).to.equal('IFRAME');
      expect(getStyle(activityIframe, 'display')).to.equal('none');
      const txId = 'tx-id-101';
      analyticsService.setTransactionId(txId);
      expect(analyticsService.getTransactionId()).to.equal(txId);
    });

    it('should yield onMessage callback and call openIframe', () => {
      let messageReceived;
      analyticsService.onMessage(data => {
        messageReceived = data;
      });
      return analyticsService.lastAction_.then(() => {
        messageCallback({'something': 'irrelevant'});
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        const firstArgument = activityPorts.openIframe.getCall(0).args[0];
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        expect(firstArgument.nodeName).to.equal('IFRAME');
        const secondArgument = activityPorts.openIframe.getCall(0).args[1];
        expect(secondArgument).to.equal(feUrl(src));
        const thirdArgument = activityPorts.openIframe.getCall(0).args[2];
        expect(thirdArgument).to.deep.equal(feArgs({
          publicationId: pageConfig.getPublicationId(),
        }));
        return activityIframePort.whenReady();
      }).then(() => {
        expect(messageReceived).to.deep.equal({'something': 'irrelevant'});
      });
    });

    it('should send message on port and openIframe called only once', () => {
      sandbox.stub(
          activityIframePort,
          'message'
      );
      registeredCallback({
        eventType: AnalyticsEvent.UNKNOWN,
        eventOriginator: EventOriginator.UNKNOWN_CLIENT,
        isFromUserAction: null,
        additionalParameters: {},
      });
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
          new AnalyticsRequest(firstArgument['buf']);
        expect(request.getEvent()).to.deep.equal(AnalyticsEvent.UNKNOWN);
        registeredCallback({
          eventType: AnalyticsEvent.IMPRESSION_PAYWALL,
          eventOriginator: EventOriginator.SWG_CLIENT,
          isFromUserAction: null,
          additionalParameters: {},
        });
        return analyticsService.lastAction_;
      }).then(() => {
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        const firstArgument = activityPorts.openIframe.getCall(0).args[0];
        expect(activityPorts.openIframe).to.have.been.calledOnce;
        expect(firstArgument.nodeName).to.equal('IFRAME');
        const secondArgument = activityPorts.openIframe.getCall(0).args[1];
        expect(secondArgument).to.equal(feUrl(src));
        const thirdArgument = activityPorts.openIframe.getCall(0).args[2];
        expect(thirdArgument).to.deep.equal(feArgs({
          publicationId: pageConfig.getPublicationId(),
        }));
        const messageArgument = activityIframePort.message.getCall(1).args[0];
        expect(messageArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
          new AnalyticsRequest(messageArgument['buf']);
        expect(request.getEvent()).to.deep.equal(
            AnalyticsEvent.IMPRESSION_PAYWALL);
      });
    });

    it('should create correct context for logging', () => {
      sandbox.stub(
          activityIframePort,
          'message'
      );
      AnalyticsService.prototype.getQueryString_ = () => {
        return '?utm_source=scenic&utm_medium=email&utm_campaign=campaign';
      };
      AnalyticsService.prototype.getReferrer_ = () => {
        return 'https://scenic-2017.appspot.com/landing.html';
      };
      analyticsService.setReadyToPay(true);
      analyticsService.setSku('basic');
      registeredCallback(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getEvent()).to.deep.equal(
            AnalyticsEvent.ACTION_SUBSCRIBE);
        expect(request.getContext()).to.not.be.null;
        expect(request.getContext().getReferringOrigin()).to.equal(
            'https://scenic-2017.appspot.com');
        expect(request.getContext().getUtmMedium()).to.equal('email');
        expect(request.getContext().getUtmSource()).to.equal('scenic');
        expect(request.getContext().getUtmCampaign()).to.equal('campaign');
        expect(request.getContext().getTransactionId())
            .to.match(/^.{8}-.{4}-.{4}-.{4}-.{12}$/g);
        expect(request.getContext().getSku()).to.equal('basic');
        expect(request.getContext().getReadyToPay()).to.be.true;
      });
    });

    it('should set context for empty experiments', () => {
      setExperimentsStringForTesting('');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      registeredCallback(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList()).to.deep.equal([]);
      });
    });

    it('should set context for non-empty experiments', () => {
      setExperimentsStringForTesting('experiment-A,experiment-B');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      registeredCallback(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        expect(activityIframePort.message).to.be.calledOnce;
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        expect(firstArgument['buf']).to.not.be.null;
        const /* {?AnalyticsRequest} */ request =
            new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['experiment-A', 'experiment-B']);
      });
    });

    it('should add additional labels to experiments', () => {
      analyticsService.addLabels(['L1', 'L2']);
      setExperimentsStringForTesting('E1,E2');
      sandbox.stub(
          activityIframePort,
          'message'
      );
      registeredCallback(event);
      return analyticsService.lastAction_.then(() => {
        return activityIframePort.whenReady();
      }).then(() => {
        const firstArgument = activityIframePort.message.getCall(0).args[0];
        const request = new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['L1', 'L2', 'E1', 'E2']);

        analyticsService.addLabels(['L3', 'L4']);
        registeredCallback(event);
        return analyticsService.lastAction_;
      }).then(() => {
        const firstArgument = activityIframePort.message.getCall(1).args[0];
        const request = new AnalyticsRequest(firstArgument['buf']);
        expect(request.getContext().getLabelList())
            .to.deep.equal(['L1', 'L2', 'E1', 'E2', 'L3', 'L4']);
      });
    });

    it('should dedupe duplicate labels', () => {
      analyticsService.addLabels(['L1', 'L2', 'L1', 'L2']);
      expect(analyticsService.context_.getLabelList())
          .to.deep.equal(['L1', 'L2']);
      analyticsService.addLabels(['L1', 'L2', 'L3']);
      expect(analyticsService.context_.getLabelList())
          .to.deep.equal(['L1', 'L2', 'L3']);
    });

    it('should pass events along to events manager', () => {
      let receivedEvent = null;
      sandbox.stub(ClientEventManager.prototype, 'logEvent',
          event => receivedEvent = event);
      analyticsService.logEvent(AnalyticsEvent.ACTION_ACCOUNT_CREATED);
      expect(receivedEvent).to.deep.equal({
        eventType: AnalyticsEvent.ACTION_ACCOUNT_CREATED,
        eventOriginator: EventOriginator.SWG_CLIENT,
        isFromUserAction: null,
        additionalParameters: {},
      });
    });

    it('should not log Propensity events by default', () => {
      //should log all clients but propensity
      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.SWG_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.AMP_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.PROPENSITY_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.be.null;
    });

    it('should log Propensity events if experiment is on', () => {
      //reinitialize the service after turning the experiment on
      setExperiment(win, ExperimentFlags.LOG_PROPENSITY_TO_SWG, true);
      analyticsService = new AnalyticsService(runtime);
      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.SWG_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.AMP_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.not.be.null;

      analyticsService.lastAction_ = null;
      event.eventOriginator = EventOriginator.PROPENSITY_CLIENT;
      registeredCallback(event);
      expect(analyticsService.lastAction_).to.not.be.null;
    });
  });
});
<|endoftext|>