'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _reactDom = require('react-dom');

var _reactDom2 = _interopRequireDefault(_reactDom);

var _propTypes = require('prop-types');

var _propTypes2 = _interopRequireDefault(_propTypes);

var _stringifyObject = require('stringify-object');

var _stringifyObject2 = _interopRequireDefault(_stringifyObject);

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

var _loopProtect = require('loop-protect');

var _loopProtect2 = _interopRequireDefault(_loopProtect);

var _helpers = require('../CodeTester/helpers');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Babel = require('babel-standalone');
var BabelTransformObjectRestSpread = require('babel-plugin-transform-object-rest-spread');

var TestRunner = function (_Component) {
  _inherits(TestRunner, _Component);

  function TestRunner(props) {
    _classCallCheck(this, TestRunner);

    var _this = _possibleConstructorReturn(this, (TestRunner.__proto__ || Object.getPrototypeOf(TestRunner)).call(this, props));

    _this.getIframeDoc = _this.getIframeDoc.bind(_this);
    _this.runTests = _this.runTests.bind(_this);
    _this.initializeLoopProtect = _this.initializeLoopProtect.bind(_this);
    _this.handleNewCode = _this.handleNewCode.bind(_this);
    _this.transformCode = _this.transformCode.bind(_this);

    _this.onConsoleLog = _lodash2.default.debounce(_this.props.onConsoleLog.bind(_this), 500);

    _this.state = {
      consoleLogs: []
    };
    return _this;
  }

  _createClass(TestRunner, [{
    key: 'componentDidMount',
    value: function componentDidMount() {
      this.initializeLoopProtect();
      this.props.onTestRunnerRef(this);
      // Start the tests only after iFrame is loaded
      // https://stackoverflow.com/questions/12009423/what-does-status-canceled-for-a-resource-mean-in-chrome-developer-tools
      this.iframe.onload = this.handleNewCode.bind(this, this.props);
    }
  }, {
    key: 'componentWillReceiveProps',
    value: function componentWillReceiveProps(nextProps) {
      if (this.props.codeToTest !== nextProps.codeToTest && this.props.autoRun) {
        this.handleNewCode(nextProps);
      }
    }
  }, {
    key: 'shouldComponentUpdate',
    value: function shouldComponentUpdate() {
      // iframe does not need to re-render.
      // handleNewCode() will reload the iframe if necessary.
      // https://medium.com/@ebakhtarov/handling-of-iframes-in-react-f038be46ac24
      return false;
    }
  }, {
    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      this.props.onTestRunnerRef(null);
      _reactDom2.default.unmountComponentAtNode(this.iframe);
    }
  }, {
    key: 'onRunClick',
    value: function onRunClick() {
      this.handleNewCode();
    }
  }, {
    key: 'getIframeDoc',
    value: function getIframeDoc() {
      if (!this.iframe) return;
      var iFrame = this.iframe;
      var iFrameDoc = iFrame.contentDocument;
      if (!iFrameDoc && iFrame.contentWindow) {
        iFrameDoc = iFrame.contentWindow.document;
      }
      return iFrameDoc;
    }
  }, {
    key: 'initializeLoopProtect',
    value: function initializeLoopProtect() {
      // https://github.com/jsbin/loop-protect
      /**
       * handleInfiniteLoop will execute in the context of the iFrame if the timeout is hit.
       * In the iFrame context, it will not have access to this component, and therefore
       * cannot call it's methods. e.g this.props.onError(), toggleAutoRun()
       */
      var handleInfiniteLoop = function handleInfiniteLoop() {
        console.error('Your code is a while to run. Maybe you wrote an infinite loop.');
      };
      Babel.registerPlugin('loopProtection', (0, _loopProtect2.default)(this.props.infiniteLoopTimeout, handleInfiniteLoop));
    }
  }, {
    key: 'handleNewCode',
    value: function handleNewCode(nextProps) {
      this.reloadIframe(nextProps);
    }
  }, {
    key: 'transformCode',
    value: function transformCode(code) {
      if (!this.props.infLoopProtectEnabled) return code;
      // https://github.com/jsbin/loop-protect
      var transform = function transform(source) {
        return Babel.transform(source, {
          plugins: [BabelTransformObjectRestSpread, 'loopProtection']
        }).code;
      };

      // rewrite the user's JavaScript to protect loops
      return transform(code);
    }
  }, {
    key: 'reloadIframe',
    value: function reloadIframe(nextProps) {
      this.setState({ consoleLogs: [] }, this.props.onConsoleLog.bind(this, []));
      this.iframe.onload = this.runTests.bind(this, nextProps || this.props);
      // Need to clear Mocha tests
      this.iframe.contentWindow.location.reload();
    }
  }, {
    key: 'handleConsoleMessage',
    value: function handleConsoleMessage() {
      var _this2 = this;

      var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'log';
      var passThroughFunc = arguments[1];

      for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
        args[_key - 2] = arguments[_key];
      }

      var log = args.reduce(function (log, arg) {
        return log + '\n' + (0, _stringifyObject2.default)(arg);
      }, '');
      this.setState({ consoleLogs: this.state.consoleLogs.concat({ log: log, type: type }) }, function () {
        return _this2.onConsoleLog(_this2.state.consoleLogs);
      });
      passThroughFunc.apply(this.iframe.contentWindow.console, args);
    }

    // TODO: move to file where the new submit button can hit this

  }, {
    key: 'runTests',
    value: function runTests(_ref) {
      var codeToTest = _ref.codeToTest,
          testSpec = _ref.testSpec;
      var contentWindow = this.iframe.contentWindow;
      var _props = this.props,
          testHandlerUrl = _props.testHandlerUrl,
          onTestResults = _props.onTestResults;
      // Set up test results handler

      contentWindow.testHandler = onTestResults;
      // Set up console scrapper
      var nativeLog = contentWindow.console.log;
      var nativeErrorLog = contentWindow.console.error;

      contentWindow.console.log = this.handleConsoleMessage.bind(this, 'log', nativeLog);
      contentWindow.console.error = this.handleConsoleMessage.bind(this, 'error', nativeErrorLog);

      var doc = this.getIframeDoc();
      // Remove previous <body> elements
      while (doc.body.firstChild) {
        doc.body.removeChild(doc.body.firstChild);
      }
      var scripts = [{ type: 'script', content: this.transformCode(codeToTest), url: '' }, { type: 'script', content: testSpec, url: '' },
      // Append script to handle Mocha results
      { type: 'script', url: testHandlerUrl }];
      var mochaDiv = document.createElement('div');
      mochaDiv.id = 'mocha';
      doc.body.appendChild(mochaDiv);
      scripts.forEach(function (el) {
        var domElement = document.createElement(el.type);
        if (el.type === 'script' && el.url.length) {
          domElement.src = el.url;
        } else {
          domElement.innerHTML = el.content;
        }
        doc.body.appendChild(domElement);
      });
    }
  }, {
    key: 'render',
    value: function render() {
      var _this3 = this;

      return _react2.default.createElement(
        'div',
        { className: 'test-runner-container' },
        _react2.default.createElement('iframe', {
          title: 'test-runner',
          src: this.props.iFrameUrl,
          ref: function ref(iframe) {
            _this3.iframe = iframe;
          },
          frameBorder: '0',
          id: 'iframe'
        })
      );
    }
  }]);

  return TestRunner;
}(_react.Component);

TestRunner.propTypes = {
  autoRun: _propTypes2.default.bool.isRequired,
  // eslint-disable-next-line
  testSpec: _propTypes2.default.string.isRequired,
  codeToTest: _propTypes2.default.string,
  onTestResults: _propTypes2.default.func,
  iFrameUrl: _propTypes2.default.string.isRequired,
  testHandlerUrl: _propTypes2.default.string.isRequired,
  // eslint-disable-next-line
  onError: _propTypes2.default.func,
  infiniteLoopTimeout: _propTypes2.default.number,
  infLoopProtectEnabled: _propTypes2.default.bool,
  // eslint-disable-next-line
  onConsoleLog: _propTypes2.default.func,
  onTestRunnerRef: _propTypes2.default.func.isRequired
};

TestRunner.defaultProps = {
  codeToTest: '',
  onTestResults: function onTestResults(passCount, failCount, testDuration) {}, // eslint-disable-line no-unused-vars
  onError: function onError(err) {
    var message = _lodash2.default.get(err, 'message') ? err.message : 'Unknown';
    console.error('There was an error running your tests: ', message);
  },
  infLoopProtectEnabled: true,
  infiniteLoopTimeout: 3000,
  onConsoleLog: _helpers.defaultFunc
};

exports.default = TestRunner;