<commit_before>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-wdm';
import '@ciscospark/plugin-logger';
import '@ciscospark/plugin-messages';
import '@ciscospark/plugin-rooms';
import CiscoSpark, {SparkHttpError} from '@ciscospark/spark-core';
import {assert} from '@ciscospark/test-helper-chai';
import sinon from '@ciscospark/test-helper-sinon';
import testUsers from '@ciscospark/test-helper-test-users';
import fh from '@ciscospark/test-helper-file';
import {browserOnly, flaky, nodeOnly} from '@ciscospark/test-helper-mocha';

const debug = require('debug')('messages');

const KNOWN_HOSTED_IMAGE_URL = 'https://download.ciscospark.com/test/photo.png';

describe('plugin-messages', function () {
  this.timeout(60000);

  let spark;

  before(() => testUsers.create({count: 1})
    .then(([user]) => {
      spark = new CiscoSpark({credentials: user.token});
    }));

  describe('#messages', () => {
    let room;

    before(() => spark.rooms.create({title: 'Cisco Spark Test Room'})
      .then((r) => {
        room = r;

        return spark.messages.create({
          roomId: room.id,
          text: 'First Message'
        });
      }));

    after(() => {
      if (!room) {
        return Promise.resolve();
      }

      return spark.rooms.remove(room)
        .catch((reason) => {
          console.error('Failed to remove room', reason);
        });
    });

    describe('#create()', () => {
      it('posts a message in a room', () => {
        const text = 'A test message';

        return spark.messages.create({
          roomId: room.id,
          text
        })
          .then((message) => {
            assert.isDefined(message);
            assert.isMessage(message);
            assert.equal(message.text, text);
          });
      });

      it('posts a file to a room by specifying the file\'s url', () => spark.messages.create({
        roomId: room.id,
        file: KNOWN_HOSTED_IMAGE_URL
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      let blob, buffer;

      browserOnly(before)(() => fh.fetch('sample-image-small-one.png')
        .then((file) => {
          blob = file;

          return new Promise((resolve) => {
            /* global FileReader */
            const fileReader = new FileReader();

            fileReader.onload = function () {
              buffer = this.result;
              resolve();
            };
            fileReader.readAsArrayBuffer(blob);
          });
        }));

      nodeOnly(before)(() => fh.fetchWithoutMagic('sample-image-small-one.png')
        .then((file) => {
          buffer = file;
        }));

      browserOnly(it)('posts a file to a room by directly supplying its blob', () => spark.messages.create({
        roomId: room.id,
        file: blob
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      // Disabling it gating pipelines because it failes a lot and we get
      // mostly adequate coverage via blob upload
      flaky(it, process.env.SKIP_FLAKY_TESTS)('posts a file to a room by directly supplying its buffer', () => spark.messages.create({
        roomId: room.id,
        file: buffer
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      it('posts a file with a message to a room by specifying the file\'s url', () => spark.messages.create({
        roomId: room.id,
        file: KNOWN_HOSTED_IMAGE_URL,
        text: 'A File'
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);

          assert.property(message, 'text');
        }));
    });

    describe('get()', () => {
      let message;

      before(() => spark.messages.create({
        roomId: room.id,
        text: 'A test message'
      })
        .then((m) => {
          message = m;
        }));

      it('returns a single message', () => spark.messages.get(message)
        .then((m) => {
          assert.isMessage(m);
          assert.deepEqual(m, message);
        }));
    });

    describe('#list()', () => {
      let room;

      before(() => spark.rooms.create({
        title: 'Room List Test'
      })
        .then((r) => {
          room = r;
        }));

      before(() => [1, 2, 3].reduce((promise, value) => promise.then(() => spark.messages.create({
        roomId: room.id,
        text: `message: ${value}`
      })), Promise.resolve()));

      it('returns all messages for a room', () => spark.messages.list({roomId: room.id})
        .then((messages) => {
          assert.isDefined(messages);
          assert.lengthOf(messages, 3);
          for (const message of messages) {
            assert.isMessage(message);
          }
        }));

      it('returns a bounded set of messages for a room', () => {
        const spy = sinon.spy();

        return spark.messages.list({roomId: room.id, max: 2})
          .then((messages) => {
            assert.lengthOf(messages, 2);

            return (function f(page) {
              for (const message of page) {
                spy(message.id);
              }

              if (page.hasNext()) {
                return page.next().then(f);
              }

              return Promise.resolve();
            }(messages));
          })
          .then(() => {
            assert.calledThrice(spy);
          });
      });
    });

    describe('#remove()', () => {
      let message;

      beforeEach(() => spark.messages.create({
        roomId: room.id,
        text: 'A test message'
      })
        .then((m) => {
          message = m;
        }));

      it('deletes a single message', () => spark.messages.remove(message)
        .then((body) => {
          assert.notOk(body);

          return assert.isRejected(spark.messages.get(message));
        })
        .then((reason) => {
          assert.instanceOf(reason, SparkHttpError.NotFound);
        }));
    });

    describe('events', () => {
      describe('#listen()', () => {
        [
          'post',
          'share'
        ].forEach((verb) => {
          it(`emits a "created" event when a "${verb}" activity arrives`, () => {
            const props = {
              verb
            };
            const event = makeActivityEvent(props);
            const spy = sinon.spy();

            spark.messages.listen()
              .then(() => {
                spark.messages.on('created', spy);
                debug(`create mock activity event: ${JSON.stringify(event)}`);
                spark.internal.mercury._onmessage(event);

                return Promise.resolve()
                  .then(() => {
                    assert.calledOnce(spy);
                  });
              });
          });
        });
      });

      describe('#createMessagesEventData', () => {
        [
          {
            tags: ['ONE_ON_ONE'],
            roomType: 'direct'
          },
          {
            tags: [],
            roomType: 'group'
          }
        ].forEach((input) => {
          it(`creates a messages event for a ${input.roomType} room`, () => {
            const props = {
              target: {
                tags: input.tags
              }
            };
            const activity = makeActivity(props);
            const event = spark.messages.createMessagesEventData(activity);

            debug(`messages event created: ${JSON.stringify(event)}`);

            assert.isDefined(event.id);
            assert.isDefined(event.roomId);
            assert.isTrue(event.roomType === input.roomType);
            assert.isTrue(event.text === activity.object.displayName);
            assert.isDefined(event.personId);
            assert.isDefined(event.personEmail);
            assert.isDefined(event.created);
          });
        });

        it('creates a messages event with files', () => {
          const additionalProps = {
            object: {
              files: {
                items: [
                  {
                    displayName: 'file1.png'
                  },
                  {
                    displayName: 'file2.txt'
                  }
                ]
              }
            }
          };
          const activity = makeActivity(additionalProps);
          const event = spark.messages.createMessagesEventData(activity);

          assert.isArray(event.files);
          assert.isTrue(event.files.length === 2);
          event.files.forEach((file) => {
            assert.isTrue(file.startsWith('https://api.ciscospark.com/v1/contents/'));
          });
        });
      });
    });
  });
});

function makeActivityEvent(options) {
  return {
    data: {
      data: {
        activity: makeActivity(options),
        eventType: 'conversation.activity'
      }
    }
  };
}

function makeActivity(options) {
  const activity = {
    id: '88888888-4444-4444-4444-000000000001',
    verb: 'post',
    actor: {
      id: '88888888-4444-4444-4444-000000000002',
      entryEmail: 'alice@email.com'
    },
    object: {
      displayName: 'hi'
    },
    target: {
      tags: [
        'ONE_ON_ONE'
      ]
    },
    published: Date.now()
  };

  return Object.assign(activity, options);
}
<commit_msg>refactor(plugin-messages): clarify test function names<commit_after>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-wdm';
import '@ciscospark/plugin-logger';
import '@ciscospark/plugin-messages';
import '@ciscospark/plugin-rooms';
import CiscoSpark, {SparkHttpError} from '@ciscospark/spark-core';
import {assert} from '@ciscospark/test-helper-chai';
import sinon from '@ciscospark/test-helper-sinon';
import testUsers from '@ciscospark/test-helper-test-users';
import fh from '@ciscospark/test-helper-file';
import {browserOnly, flaky, nodeOnly} from '@ciscospark/test-helper-mocha';

const debug = require('debug')('messages');

const KNOWN_HOSTED_IMAGE_URL = 'https://download.ciscospark.com/test/photo.png';

describe('plugin-messages', function () {
  this.timeout(60000);

  let spark;

  before(() => testUsers.create({count: 1})
    .then(([user]) => {
      spark = new CiscoSpark({credentials: user.token});
    }));

  describe('#messages', () => {
    let room;

    before(() => spark.rooms.create({title: 'Cisco Spark Test Room'})
      .then((r) => {
        room = r;

        return spark.messages.create({
          roomId: room.id,
          text: 'First Message'
        });
      }));

    after(() => {
      if (!room) {
        return Promise.resolve();
      }

      return spark.rooms.remove(room)
        .catch((reason) => {
          console.error('Failed to remove room', reason);
        });
    });

    describe('#create()', () => {
      it('posts a message in a room', () => {
        const text = 'A test message';

        return spark.messages.create({
          roomId: room.id,
          text
        })
          .then((message) => {
            assert.isDefined(message);
            assert.isMessage(message);
            assert.equal(message.text, text);
          });
      });

      it('posts a file to a room by specifying the file\'s url', () => spark.messages.create({
        roomId: room.id,
        file: KNOWN_HOSTED_IMAGE_URL
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      let blob, buffer;

      browserOnly(before)(() => fh.fetch('sample-image-small-one.png')
        .then((file) => {
          blob = file;

          return new Promise((resolve) => {
            /* global FileReader */
            const fileReader = new FileReader();

            fileReader.onload = function () {
              buffer = this.result;
              resolve();
            };
            fileReader.readAsArrayBuffer(blob);
          });
        }));

      nodeOnly(before)(() => fh.fetchWithoutMagic('sample-image-small-one.png')
        .then((file) => {
          buffer = file;
        }));

      browserOnly(it)('posts a file to a room by directly supplying its blob', () => spark.messages.create({
        roomId: room.id,
        file: blob
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      // Disabling it gating pipelines because it failes a lot and we get
      // mostly adequate coverage via blob upload
      flaky(it, process.env.SKIP_FLAKY_TESTS)('posts a file to a room by directly supplying its buffer', () => spark.messages.create({
        roomId: room.id,
        file: buffer
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);
        }));

      it('posts a file with a message to a room by specifying the file\'s url', () => spark.messages.create({
        roomId: room.id,
        file: KNOWN_HOSTED_IMAGE_URL,
        text: 'A File'
      })
        .then((message) => {
          assert.property(message, 'files');
          assert.isDefined(message.files);
          assert.isArray(message.files);
          assert.lengthOf(message.files, 1);

          assert.property(message, 'text');
        }));
    });

    describe('get()', () => {
      let message;

      before(() => spark.messages.create({
        roomId: room.id,
        text: 'A test message'
      })
        .then((m) => {
          message = m;
        }));

      it('returns a single message', () => spark.messages.get(message)
        .then((m) => {
          assert.isMessage(m);
          assert.deepEqual(m, message);
        }));
    });

    describe('#list()', () => {
      let room;

      before(() => spark.rooms.create({
        title: 'Room List Test'
      })
        .then((r) => {
          room = r;
        }));

      before(() => [1, 2, 3].reduce((promise, value) => promise.then(() => spark.messages.create({
        roomId: room.id,
        text: `message: ${value}`
      })), Promise.resolve()));

      it('returns all messages for a room', () => spark.messages.list({roomId: room.id})
        .then((messages) => {
          assert.isDefined(messages);
          assert.lengthOf(messages, 3);
          for (const message of messages) {
            assert.isMessage(message);
          }
        }));

      it('returns a bounded set of messages for a room', () => {
        const spy = sinon.spy();

        return spark.messages.list({roomId: room.id, max: 2})
          .then((messages) => {
            assert.lengthOf(messages, 2);

            return (function f(page) {
              for (const message of page) {
                spy(message.id);
              }

              if (page.hasNext()) {
                return page.next().then(f);
              }

              return Promise.resolve();
            }(messages));
          })
          .then(() => {
            assert.calledThrice(spy);
          });
      });
    });

    describe('#remove()', () => {
      let message;

      beforeEach(() => spark.messages.create({
        roomId: room.id,
        text: 'A test message'
      })
        .then((m) => {
          message = m;
        }));

      it('deletes a single message', () => spark.messages.remove(message)
        .then((body) => {
          assert.notOk(body);

          return assert.isRejected(spark.messages.get(message));
        })
        .then((reason) => {
          assert.instanceOf(reason, SparkHttpError.NotFound);
        }));
    });

    describe('events', () => {
      describe('#listen()', () => {
        [
          'post',
          'share'
        ].forEach((verb) => {
          it(`emits a "created" event when a "${verb}" activity arrives`, () => {
            const props = {
              verb
            };
            const event = mockActivityEvent(props);
            const spy = sinon.spy();

            spark.messages.listen()
              .then(() => {
                spark.messages.on('created', spy);
                debug(`create mock activity event: ${JSON.stringify(event)}`);
                spark.internal.mercury._onmessage(event);

                return Promise.resolve()
                  .then(() => {
                    assert.calledOnce(spy);
                  });
              });
          });
        });
      });

      describe('#createMessagesEventData', () => {
        [
          {
            tags: ['ONE_ON_ONE'],
            roomType: 'direct'
          },
          {
            tags: [],
            roomType: 'group'
          }
        ].forEach((input) => {
          it(`creates a messages event for a ${input.roomType} room`, () => {
            const props = {
              target: {
                tags: input.tags
              }
            };
            const activity = mockActivity(props);
            const event = spark.messages.createMessagesEventData(activity);

            debug(`messages event created: ${JSON.stringify(event)}`);

            assert.isDefined(event.id);
            assert.isDefined(event.roomId);
            assert.isTrue(event.roomType === input.roomType);
            assert.isTrue(event.text === activity.object.displayName);
            assert.isDefined(event.personId);
            assert.isDefined(event.personEmail);
            assert.isDefined(event.created);
          });
        });

        it('creates a messages event with files', () => {
          const additionalProps = {
            object: {
              files: {
                items: [
                  {
                    displayName: 'file1.png'
                  },
                  {
                    displayName: 'file2.txt'
                  }
                ]
              }
            }
          };
          const activity = mockActivity(additionalProps);
          const event = spark.messages.createMessagesEventData(activity);

          assert.isArray(event.files);
          assert.isTrue(event.files.length === 2);
          event.files.forEach((file) => {
            assert.isTrue(file.startsWith('https://api.ciscospark.com/v1/contents/'));
          });
        });
      });
    });
  });
});

/**
 * Create a mock activity event.
 * Add and override properties to customize the default activity.
 * @param {Object} props
 * @returns {Object} activity
 */
function mockActivityEvent(props) {
  return {
    data: {
      data: {
        activity: mockActivity(props),
        eventType: 'conversation.activity'
      }
    }
  };
}

/**
 * Create a mock activity.
 * Add and override properties to customize the default activity.
 * @param {Object} props
 * @returns {Object} activity
 */
function mockActivity(props) {
  const activity = {
    id: '88888888-4444-4444-4444-000000000001',
    verb: 'post',
    actor: {
      id: '88888888-4444-4444-4444-000000000002',
      entryEmail: 'alice@email.com'
    },
    object: {
      displayName: 'hi'
    },
    target: {
      tags: [
        'ONE_ON_ONE'
      ]
    },
    published: Date.now()
  };

  return Object.assign(activity, props);
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

/* global Handlebars, escapeHTML */

(function(OC, OCA) {
	var TEMPLATE =
		'
' +
		'
' +
		'

' +
		'
{{emptyResultLabel}}

' +
		'<input type="button" class="showMore hidden" value="{{moreLabel}}"' +
		' name="show-more" id="show-more" />' +
		'

';

	var EDIT_COMMENT_TEMPLATE =
		'
' +
		'
' +
		'        {{#if avatarEnabled}}' +
		'

' +
		'        {{/if}}' +
		'
{{actorDisplayName}}
' +
		'{{#if isEditMode}}' +
		'        ' +
		'{{/if}}' +
		'
' +
		'    <form class="newCommentForm">' +
		'        <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{{message}}}" />' +
		'        <input class="submit icon-confirm" type="submit" value="" />' +
		'{{#if isEditMode}}' +
		'        <input class="cancel pull-right" type="button" value="{{cancelText}}" />' +
		'{{/if}}' +
		'

'+
		'    </form>' +
		'
';

	var COMMENT_TEMPLATE =
		'
' +
		'
' +
		'        {{#if avatarEnabled}}' +
		'

' +
		'        {{/if}}' +
		'
{{actorDisplayName}}
' +
		'{{#if isUserAuthor}}' +
		'        ' +
		'{{/if}}' +
		'
{{date}}
' +
		'
' +
		'
{{{formattedMessage}}}
' +
		'{{#if isLong}}' +
		'

' +
		'{{/if}}' +
		'
';

	/**
	 * @memberof OCA.Comments
	 */
	var CommentsTabView = OCA.Files.DetailTabView.extend(
		/** @lends OCA.Comments.CommentsTabView.prototype */ {
		id: 'commentsTabView',
		className: 'tab commentsTabView',

		events: {
			'submit .newCommentForm': '_onSubmitComment',
			'click .showMore': '_onClickShowMore',
			'click .action.edit': '_onClickEditComment',
			'click .action.delete': '_onClickDeleteComment',
			'click .cancel': '_onClickCloseComment',
			'click .comment': '_onClickComment'
		},

		_commentMaxLength: 1000,

		initialize: function() {
			OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
			this.collection = new OCA.Comments.CommentCollection();
			this.collection.on('request', this._onRequest, this);
			this.collection.on('sync', this._onEndRequest, this);
			this.collection.on('add', this._onAddModel, this);

			this._avatarsEnabled = !!OC.config.enable_avatars;

			this._commentMaxThreshold = this._commentMaxLength * 0.9;

			// TODO: error handling
			_.bindAll(this, '_onTypeComment');
		},

		template: function(params) {
			if (!this._template) {
				this._template = Handlebars.compile(TEMPLATE);
			}
			var currentUser = OC.getCurrentUser();
			return this._template(_.extend({
				avatarEnabled: this._avatarsEnabled,
				actorId: currentUser.uid,
				actorDisplayName: currentUser.displayName
			}, params));
		},

		editCommentTemplate: function(params) {
			if (!this._editCommentTemplate) {
				this._editCommentTemplate = Handlebars.compile(EDIT_COMMENT_TEMPLATE);
			}
			var currentUser = OC.getCurrentUser();
			return this._editCommentTemplate(_.extend({
				avatarEnabled: this._avatarsEnabled,
				actorId: currentUser.uid,
				actorDisplayName: currentUser.displayName,
				newMessagePlaceholder: t('comments', 'New comment …'),
				deleteTooltip: t('comments', 'Delete comment'),
				submitText: t('comments', 'Post'),
				cancelText: t('comments', 'Cancel')
			}, params));
		},

		commentTemplate: function(params) {
			if (!this._commentTemplate) {
				this._commentTemplate = Handlebars.compile(COMMENT_TEMPLATE);
			}

			params = _.extend({
				avatarEnabled: this._avatarsEnabled,
				editTooltip: t('comments', 'Edit comment'),
				isUserAuthor: OC.getCurrentUser().uid === params.actorId,
				isLong: this._isLong(params.message)
			}, params);

			if (params.actorType === 'deleted_users') {
				// makes the avatar a X
				params.actorId = null;
				params.actorDisplayName = t('comments', '[Deleted user]');
			}

			return this._commentTemplate(params);
		},

		getLabel: function() {
			return t('comments', 'Comments');
		},

		setFileInfo: function(fileInfo) {
			if (fileInfo) {
				this.model = fileInfo;
				this.render();
				this.collection.setObjectId(fileInfo.id);
				// reset to first page
				this.collection.reset([], {silent: true});
				this.nextPage();
			} else {
				this.model = null;
				this.render();
				this.collection.reset();
			}
		},

		render: function() {
			this.$el.html(this.template({
				emptyResultLabel: t('comments', 'No comments yet, start the conversation!'),
				moreLabel: t('comments', 'More comments …')
			}));
			this.$el.find('.comments').before(this.editCommentTemplate({}));
			this.$el.find('.has-tooltip').tooltip();
			this.$container = this.$el.find('ul.comments');
			if (this._avatarsEnabled) {
				this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 32);
			}
			this.delegateEvents();
			this.$el.find('.message').on('keydown input change', this._onTypeComment);
		},

		_formatItem: function(commentModel) {
			var timestamp = new Date(commentModel.get('creationDateTime')).getTime();
			var data = _.extend({
				date: OC.Util.relativeModifiedDate(timestamp),
				altDate: OC.Util.formatDate(timestamp),
				formattedMessage: this._formatMessage(commentModel.get('message'))
			}, commentModel.attributes);
			return data;
		},

		_toggleLoading: function(state) {
			this._loading = state;
			this.$el.find('.loading').toggleClass('hidden', !state);
		},

		_onRequest: function(type) {
			if (type === 'REPORT') {
				this._toggleLoading(true);
				this.$el.find('.showMore').addClass('hidden');
			}
		},

		_onEndRequest: function(type) {
			var fileInfoModel = this.model;
			this._toggleLoading(false);
			this.$el.find('.emptycontent').toggleClass('hidden', !!this.collection.length);
			this.$el.find('.showMore').toggleClass('hidden', !this.collection.hasMoreResults());

			if (type !== 'REPORT') {
				return;
			}

			// find first unread comment
			var firstUnreadComment = this.collection.findWhere({isUnread: true});
			if (firstUnreadComment) {
				// update read marker
				this.collection.updateReadMarker(
					null,
					{
						success: function() {
							fileInfoModel.set('commentsUnread', 0);
						}
					}
				);
			}
		},

		_onAddModel: function(model, collection, options) {
			var $el = $(this.commentTemplate(this._formatItem(model)));
			if (!_.isUndefined(options.at) && collection.length > 1) {
				this.$container.find('li').eq(options.at).before($el);
			} else {
				this.$container.append($el);
			}

			this._postRenderItem($el);
		},

		_postRenderItem: function($el) {
			$el.find('.has-tooltip').tooltip();
			if(this._avatarsEnabled) {
				$el.find('.avatar').each(function() {
					var $this = $(this);
					$this.avatar($this.attr('data-username'), 32);
				});
			}
		},

		/**
		 * Convert a message to be displayed in HTML,
		 * converts newlines to
 tags.
		 */
		_formatMessage: function(message) {
			return escapeHTML(message).replace(/\n/g, '
');
		},

		nextPage: function() {
			if (this._loading || !this.collection.hasMoreResults()) {
				return;
			}

			this.collection.fetchNext();
		},

		_onClickEditComment: function(ev) {
			ev.preventDefault();
			var $comment = $(ev.target).closest('.comment');
			var commentId = $comment.data('id');
			var commentToEdit = this.collection.get(commentId);
			var $formRow = $(this.editCommentTemplate(_.extend({
				isEditMode: true,
				submitText: t('comments', 'Save')
			}, commentToEdit.attributes)));

			$comment.addClass('hidden').removeClass('collapsed');
			// spawn form
			$comment.after($formRow);
			$formRow.data('commentEl', $comment);
			$formRow.find('textarea').on('keydown input change', this._onTypeComment);

			// copy avatar element from original to avoid flickering
			$formRow.find('.avatar').replaceWith($comment.find('.avatar').clone());
			$formRow.find('.has-tooltip').tooltip();

			return false;
		},

		_onTypeComment: function(ev) {
			var $field = $(ev.target);
			var len = $field.val().length;
			var $submitButton = $field.data('submitButtonEl');
			if (!$submitButton) {
				$submitButton = $field.closest('form').find('.submit');
				$field.data('submitButtonEl', $submitButton);
			}
			$field.tooltip('hide');
			if (len > this._commentMaxThreshold) {
				$field.attr('data-original-title', t('comments', 'Allowed characters {count} of {max}', {count: len, max: this._commentMaxLength}));
				$field.tooltip({trigger: 'manual'});
				$field.tooltip('show');
				$field.addClass('error');
			}

			var limitExceeded = (len > this._commentMaxLength);
			$field.toggleClass('error', limitExceeded);
			$submitButton.prop('disabled', limitExceeded);

			//submits form on ctrl+Enter or cmd+Enter
			if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) {
				$submitButton.click();
			}
		},

		_onClickComment: function(ev) {
			var $row = $(ev.target);
			if (!$row.is('.comment')) {
				$row = $row.closest('.comment');
			}
			$row.removeClass('collapsed');
		},

		_onClickCloseComment: function(ev) {
			ev.preventDefault();
			var $row = $(ev.target).closest('.comment');
			$row.data('commentEl').removeClass('hidden');
			$row.remove();
			return false;
		},

		_onClickDeleteComment: function(ev) {
			ev.preventDefault();
			var $comment = $(ev.target).closest('.comment');
			var commentId = $comment.data('id');
			var $loading = $comment.find('.submitLoading');

			$comment.addClass('disabled');
			$loading.removeClass('hidden');
			this.collection.get(commentId).destroy({
				success: function() {
					$comment.data('commentEl').remove();
					$comment.remove();
				},
				error: function() {
					$loading.addClass('hidden');
					$comment.removeClass('disabled');
					OC.Notification.showTemporary(t('comments', 'Error occurred while retrieving comment with id {id}', {id: commentId}));
				}
			});

			return false;
		},

		_onClickShowMore: function(ev) {
			ev.preventDefault();
			this.nextPage();
		},

		_onSubmitComment: function(e) {
			var self = this;
			var $form = $(e.target);
			var commentId = $form.closest('.comment').data('id');
			var currentUser = OC.getCurrentUser();
			var $submit = $form.find('.submit');
			var $loading = $form.find('.submitLoading');
			var $textArea = $form.find('.message');
			var message = $textArea.val().trim();
			e.preventDefault();

			if (!message.length || message.length > this._commentMaxLength) {
				return;
			}

			$textArea.prop('disabled', true);
			$submit.addClass('hidden');
			$loading.removeClass('hidden');

			if (commentId) {
				// edit mode
				var comment = this.collection.get(commentId);
				comment.save({
					message: $textArea.val()
				}, {
					success: function(model) {
						var $row = $form.closest('.comment');
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$row.data('commentEl')
							.removeClass('hidden')
							.find('.message')
							.html(self._formatMessage(model.get('message')));
						$row.remove();
					},
					error: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.prop('disabled', false);

						OC.Notification.showTemporary(t('comments', 'Error occurred while updating comment with id {id}', {id: commentId}));
					}
				});
			} else {
				this.collection.create({
					actorId: currentUser.uid,
					actorDisplayName: currentUser.displayName,
					actorType: 'users',
					verb: 'comment',
					message: $textArea.val(),
					creationDateTime: (new Date()).toUTCString()
				}, {
					at: 0,
					// wait for real creation before adding
					wait: true,
					success: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.val('').prop('disabled', false);
					},
					error: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.prop('disabled', false);

						OC.Notification.showTemporary(t('comments', 'Error occurred while posting comment'));
					}
				});
			}

			return false;
		},

		/**
		 * Returns whether the given message is long and needs
		 * collapsing
		 */
		_isLong: function(message) {
			return message.length > 250 || (message.match(/\n/g) || []).length > 1;
		}
	});

	OCA.Comments.CommentsTabView = CommentsTabView;
})(OC, OCA);
<commit_msg>Prevent parse error on editing an HTML comment<commit_after>/*
 * Copyright (c) 2016
 *
 * This file is licensed under the Affero General Public License version 3
 * or later.
 *
 * See the COPYING-README file.
 *
 */

/* global Handlebars, escapeHTML */

(function(OC, OCA) {
	var TEMPLATE =
		'
' +
		'
' +
		'

' +
		'
{{emptyResultLabel}}

' +
		'<input type="button" class="showMore hidden" value="{{moreLabel}}"' +
		' name="show-more" id="show-more" />' +
		'

';

	var EDIT_COMMENT_TEMPLATE =
		'
' +
		'
' +
		'        {{#if avatarEnabled}}' +
		'

' +
		'        {{/if}}' +
		'
{{actorDisplayName}}
' +
		'{{#if isEditMode}}' +
		'        ' +
		'{{/if}}' +
		'
' +
		'    <form class="newCommentForm">' +
		'        <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{message}}" />' +
		'        <input class="submit icon-confirm" type="submit" value="" />' +
		'{{#if isEditMode}}' +
		'        <input class="cancel pull-right" type="button" value="{{cancelText}}" />' +
		'{{/if}}' +
		'

'+
		'    </form>' +
		'
';

	var COMMENT_TEMPLATE =
		'
' +
		'
' +
		'        {{#if avatarEnabled}}' +
		'

' +
		'        {{/if}}' +
		'
{{actorDisplayName}}
' +
		'{{#if isUserAuthor}}' +
		'        ' +
		'{{/if}}' +
		'
{{date}}
' +
		'
' +
		'
{{{formattedMessage}}}
' +
		'{{#if isLong}}' +
		'

' +
		'{{/if}}' +
		'
';

	/**
	 * @memberof OCA.Comments
	 */
	var CommentsTabView = OCA.Files.DetailTabView.extend(
		/** @lends OCA.Comments.CommentsTabView.prototype */ {
		id: 'commentsTabView',
		className: 'tab commentsTabView',

		events: {
			'submit .newCommentForm': '_onSubmitComment',
			'click .showMore': '_onClickShowMore',
			'click .action.edit': '_onClickEditComment',
			'click .action.delete': '_onClickDeleteComment',
			'click .cancel': '_onClickCloseComment',
			'click .comment': '_onClickComment'
		},

		_commentMaxLength: 1000,

		initialize: function() {
			OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
			this.collection = new OCA.Comments.CommentCollection();
			this.collection.on('request', this._onRequest, this);
			this.collection.on('sync', this._onEndRequest, this);
			this.collection.on('add', this._onAddModel, this);

			this._avatarsEnabled = !!OC.config.enable_avatars;

			this._commentMaxThreshold = this._commentMaxLength * 0.9;

			// TODO: error handling
			_.bindAll(this, '_onTypeComment');
		},

		template: function(params) {
			if (!this._template) {
				this._template = Handlebars.compile(TEMPLATE);
			}
			var currentUser = OC.getCurrentUser();
			return this._template(_.extend({
				avatarEnabled: this._avatarsEnabled,
				actorId: currentUser.uid,
				actorDisplayName: currentUser.displayName
			}, params));
		},

		editCommentTemplate: function(params) {
			if (!this._editCommentTemplate) {
				this._editCommentTemplate = Handlebars.compile(EDIT_COMMENT_TEMPLATE);
			}
			var currentUser = OC.getCurrentUser();
			return this._editCommentTemplate(_.extend({
				avatarEnabled: this._avatarsEnabled,
				actorId: currentUser.uid,
				actorDisplayName: currentUser.displayName,
				newMessagePlaceholder: t('comments', 'New comment …'),
				deleteTooltip: t('comments', 'Delete comment'),
				submitText: t('comments', 'Post'),
				cancelText: t('comments', 'Cancel')
			}, params));
		},

		commentTemplate: function(params) {
			if (!this._commentTemplate) {
				this._commentTemplate = Handlebars.compile(COMMENT_TEMPLATE);
			}

			params = _.extend({
				avatarEnabled: this._avatarsEnabled,
				editTooltip: t('comments', 'Edit comment'),
				isUserAuthor: OC.getCurrentUser().uid === params.actorId,
				isLong: this._isLong(params.message)
			}, params);

			if (params.actorType === 'deleted_users') {
				// makes the avatar a X
				params.actorId = null;
				params.actorDisplayName = t('comments', '[Deleted user]');
			}

			return this._commentTemplate(params);
		},

		getLabel: function() {
			return t('comments', 'Comments');
		},

		setFileInfo: function(fileInfo) {
			if (fileInfo) {
				this.model = fileInfo;
				this.render();
				this.collection.setObjectId(fileInfo.id);
				// reset to first page
				this.collection.reset([], {silent: true});
				this.nextPage();
			} else {
				this.model = null;
				this.render();
				this.collection.reset();
			}
		},

		render: function() {
			this.$el.html(this.template({
				emptyResultLabel: t('comments', 'No comments yet, start the conversation!'),
				moreLabel: t('comments', 'More comments …')
			}));
			this.$el.find('.comments').before(this.editCommentTemplate({}));
			this.$el.find('.has-tooltip').tooltip();
			this.$container = this.$el.find('ul.comments');
			if (this._avatarsEnabled) {
				this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 32);
			}
			this.delegateEvents();
			this.$el.find('.message').on('keydown input change', this._onTypeComment);
		},

		_formatItem: function(commentModel) {
			var timestamp = new Date(commentModel.get('creationDateTime')).getTime();
			var data = _.extend({
				date: OC.Util.relativeModifiedDate(timestamp),
				altDate: OC.Util.formatDate(timestamp),
				formattedMessage: this._formatMessage(commentModel.get('message'))
			}, commentModel.attributes);
			return data;
		},

		_toggleLoading: function(state) {
			this._loading = state;
			this.$el.find('.loading').toggleClass('hidden', !state);
		},

		_onRequest: function(type) {
			if (type === 'REPORT') {
				this._toggleLoading(true);
				this.$el.find('.showMore').addClass('hidden');
			}
		},

		_onEndRequest: function(type) {
			var fileInfoModel = this.model;
			this._toggleLoading(false);
			this.$el.find('.emptycontent').toggleClass('hidden', !!this.collection.length);
			this.$el.find('.showMore').toggleClass('hidden', !this.collection.hasMoreResults());

			if (type !== 'REPORT') {
				return;
			}

			// find first unread comment
			var firstUnreadComment = this.collection.findWhere({isUnread: true});
			if (firstUnreadComment) {
				// update read marker
				this.collection.updateReadMarker(
					null,
					{
						success: function() {
							fileInfoModel.set('commentsUnread', 0);
						}
					}
				);
			}
		},

		_onAddModel: function(model, collection, options) {
			var $el = $(this.commentTemplate(this._formatItem(model)));
			if (!_.isUndefined(options.at) && collection.length > 1) {
				this.$container.find('li').eq(options.at).before($el);
			} else {
				this.$container.append($el);
			}

			this._postRenderItem($el);
		},

		_postRenderItem: function($el) {
			$el.find('.has-tooltip').tooltip();
			if(this._avatarsEnabled) {
				$el.find('.avatar').each(function() {
					var $this = $(this);
					$this.avatar($this.attr('data-username'), 32);
				});
			}
		},

		/**
		 * Convert a message to be displayed in HTML,
		 * converts newlines to
 tags.
		 */
		_formatMessage: function(message) {
			return escapeHTML(message).replace(/\n/g, '
');
		},

		nextPage: function() {
			if (this._loading || !this.collection.hasMoreResults()) {
				return;
			}

			this.collection.fetchNext();
		},

		_onClickEditComment: function(ev) {
			ev.preventDefault();
			var $comment = $(ev.target).closest('.comment');
			var commentId = $comment.data('id');
			var commentToEdit = this.collection.get(commentId);
			var $formRow = $(this.editCommentTemplate(_.extend({
				isEditMode: true,
				submitText: t('comments', 'Save')
			}, commentToEdit.attributes)));

			$comment.addClass('hidden').removeClass('collapsed');
			// spawn form
			$comment.after($formRow);
			$formRow.data('commentEl', $comment);
			$formRow.find('textarea').on('keydown input change', this._onTypeComment);

			// copy avatar element from original to avoid flickering
			$formRow.find('.avatar').replaceWith($comment.find('.avatar').clone());
			$formRow.find('.has-tooltip').tooltip();

			return false;
		},

		_onTypeComment: function(ev) {
			var $field = $(ev.target);
			var len = $field.val().length;
			var $submitButton = $field.data('submitButtonEl');
			if (!$submitButton) {
				$submitButton = $field.closest('form').find('.submit');
				$field.data('submitButtonEl', $submitButton);
			}
			$field.tooltip('hide');
			if (len > this._commentMaxThreshold) {
				$field.attr('data-original-title', t('comments', 'Allowed characters {count} of {max}', {count: len, max: this._commentMaxLength}));
				$field.tooltip({trigger: 'manual'});
				$field.tooltip('show');
				$field.addClass('error');
			}

			var limitExceeded = (len > this._commentMaxLength);
			$field.toggleClass('error', limitExceeded);
			$submitButton.prop('disabled', limitExceeded);

			//submits form on ctrl+Enter or cmd+Enter
			if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) {
				$submitButton.click();
			}
		},

		_onClickComment: function(ev) {
			var $row = $(ev.target);
			if (!$row.is('.comment')) {
				$row = $row.closest('.comment');
			}
			$row.removeClass('collapsed');
		},

		_onClickCloseComment: function(ev) {
			ev.preventDefault();
			var $row = $(ev.target).closest('.comment');
			$row.data('commentEl').removeClass('hidden');
			$row.remove();
			return false;
		},

		_onClickDeleteComment: function(ev) {
			ev.preventDefault();
			var $comment = $(ev.target).closest('.comment');
			var commentId = $comment.data('id');
			var $loading = $comment.find('.submitLoading');

			$comment.addClass('disabled');
			$loading.removeClass('hidden');
			this.collection.get(commentId).destroy({
				success: function() {
					$comment.data('commentEl').remove();
					$comment.remove();
				},
				error: function() {
					$loading.addClass('hidden');
					$comment.removeClass('disabled');
					OC.Notification.showTemporary(t('comments', 'Error occurred while retrieving comment with id {id}', {id: commentId}));
				}
			});

			return false;
		},

		_onClickShowMore: function(ev) {
			ev.preventDefault();
			this.nextPage();
		},

		_onSubmitComment: function(e) {
			var self = this;
			var $form = $(e.target);
			var commentId = $form.closest('.comment').data('id');
			var currentUser = OC.getCurrentUser();
			var $submit = $form.find('.submit');
			var $loading = $form.find('.submitLoading');
			var $textArea = $form.find('.message');
			var message = $textArea.val().trim();
			e.preventDefault();

			if (!message.length || message.length > this._commentMaxLength) {
				return;
			}

			$textArea.prop('disabled', true);
			$submit.addClass('hidden');
			$loading.removeClass('hidden');

			if (commentId) {
				// edit mode
				var comment = this.collection.get(commentId);
				comment.save({
					message: $textArea.val()
				}, {
					success: function(model) {
						var $row = $form.closest('.comment');
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$row.data('commentEl')
							.removeClass('hidden')
							.find('.message')
							.html(self._formatMessage(model.get('message')));
						$row.remove();
					},
					error: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.prop('disabled', false);

						OC.Notification.showTemporary(t('comments', 'Error occurred while updating comment with id {id}', {id: commentId}));
					}
				});
			} else {
				this.collection.create({
					actorId: currentUser.uid,
					actorDisplayName: currentUser.displayName,
					actorType: 'users',
					verb: 'comment',
					message: $textArea.val(),
					creationDateTime: (new Date()).toUTCString()
				}, {
					at: 0,
					// wait for real creation before adding
					wait: true,
					success: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.val('').prop('disabled', false);
					},
					error: function() {
						$submit.removeClass('hidden');
						$loading.addClass('hidden');
						$textArea.prop('disabled', false);

						OC.Notification.showTemporary(t('comments', 'Error occurred while posting comment'));
					}
				});
			}

			return false;
		},

		/**
		 * Returns whether the given message is long and needs
		 * collapsing
		 */
		_isLong: function(message) {
			return message.length > 250 || (message.match(/\n/g) || []).length > 1;
		}
	});

	OCA.Comments.CommentsTabView = CommentsTabView;
})(OC, OCA);
<|endoftext|>


<commit_before>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/plugin-phone';

import {assert} from '@ciscospark/test-helper-chai';
import CiscoSpark from '@ciscospark/spark-core';
import testUsers from '@ciscospark/test-helper-test-users';
import handleErrorEvent from '../lib/handle-error-event';
import {
  browserOnly,
  expectNEvents,
  maxWaitForEvent
} from '@ciscospark/test-helper-mocha';
import {
  expectCallIncomingEvent,
  expectChangeActiveParticipantsCountEvent,
  expectChangeLocusEvent,
  expectChangeStateEvent,
  expectInactiveEvent,
  expectMembershipEvent,
  expectMembershipConnectedEvent,
  expectMembershipDeclinedEvent,
  expectMembershipDisconnectedEvent,
  expectDisconnectedEvent
} from '../lib/event-expectations';

if (process.env.NODE_ENV !== `test`) {
  throw new Error(`Cannot run the plugin-phone test suite without NODE_ENV === "test"`);
}

browserOnly(describe)(`plugin-phone`, function() {
  this.timeout(120000);

  describe(`Phone`, () => {
    const users = {
      chekov: null,
      mccoy: null,
      spock: null,
      uhura: null
    };
    let chekov, mccoy, spock, uhura;

    before(`create users and register`, () => testUsers.create({count: Object.keys(users).length})
      .then((created) => Promise.all(Object.keys(users).map((name, index) => {
        const user = users[name] = created[index];
        user.spark = new CiscoSpark({
          credentials: {
            authorization: user.token
          }
        });

        return user.spark.phone.register();
      })))
      .then(() => {
        chekov = users.chekov;
        mccoy = users.mccoy;
        spock = users.spock;
        uhura = users.uhura;
      }));

    beforeEach(`enable group calling`, () => Object.values(users).forEach((user) => {
      user.spark.config.phone.enableExperimentalGroupCallingSupport = true;
    }));

    afterEach(`disable group calling`, () => Object.values(users).forEach((user) => {
      user.spark.config.phone.enableExperimentalGroupCallingSupport = false;
    }));

    after(`unregister users`, () => Promise.all(Object.keys(users).map((name) => {
      const user = users[name];
      return user && user.spark.phone.deregister()
        .catch((reason) => console.warn(`could not unregister ${user}`, reason));
    })));

    describe(`#dial()`, () => {
      let room;
      beforeEach(`create room via hydra`, () => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `rooms`,
        body: {
          title: `Call Test`
        }
      })
        .then((res) => {
          room = res.body;
        }));

      beforeEach(`add mccoy to room via hydra`, () => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `memberships`,
        body: {
          personId: mccoy.id,
          roomId: room.id
        }
      }));

      beforeEach(`refresh room details from hydra`, () => spock.spark.request({
        method: `GET`,
        service: `hydra`,
        resource: `rooms/${room.id}`
      })
        .then((res) => {
          room = res.body;
        }));

      it(`calls a room by hydra room id`, () => handleErrorEvent(spock.spark.phone.dial(room.id), (call) => {
        let mccoyCall;
        assert.isUndefined(call.state);
        return Promise.all([
          Promise.resolve()
            .then(() => call.locus || expectChangeLocusEvent(call))
            .then(() => call.state === `active` || expectChangeStateEvent(call))
            .then(() => assert.equal(call.state, `active`)),
          expectCallIncomingEvent(mccoy.spark.phone)
        ])
          // eslint-disable-next-line no-unused-vars
          .then(([_, mc]) => {
            mccoyCall = mc;

            assert.isFalse(mccoyCall.isCall, `McCoy sees the Call as a MEETING`);

            assert.isDefined(mccoyCall.locus, `McCoy's call has a locus`);
            assert.equal(mccoyCall.state, `active`, `McCoy's view of the call is active`);
            return Promise.all([
              mccoyCall.activeParticipantsCount === 2 || expectChangeActiveParticipantsCountEvent(mccoyCall),
              call.activeParticipantsCount === 2 || expectChangeActiveParticipantsCountEvent(call),
              mccoyCall.answer()
            ]);
          })
          .then(() => {
            // The find statements here are taking advantage of the fact
            // that this is, in fact, a two-party call
            assert.equal(call.memberships.find((m) => m._self).state, `connected`, `Spock sees Spock as connected`);
            assert.equal(mccoyCall.memberships.find((m) => m._self).state, `connected`, `McCoy sees McCoy as connected`);

            assert.equal(call.memberships.find((m) => !m._self).state, `connected`, `Spock sees McCoy as connected`);
            assert.equal(mccoyCall.memberships.find((m) => !m._self).state, `connected`, `McCoy sees Spock as connected`);

            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 2);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 2);
          })
          .then(() => Promise.all([
            maxWaitForEvent(5000, `change:activeParticipantsCount`, mccoyCall),
            maxWaitForEvent(5000, `change:activeParticipantsCount`, call),
            call.hangup()
          ]))
          .then(() => {
            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 1);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 1);

            assert.equal(call.state, `active`, `Spock sees the call as active after Spock leaves`);
            assert.equal(mccoyCall.state, `active`, `McCoy sees the call as active after Spock leaves`);
            return Promise.all([
              maxWaitForEvent(5000, `inactive`, call),
              maxWaitForEvent(5000, `inactive`, mccoyCall),
              mccoyCall.hangup()
            ]);
          })
          .then(() => {
            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 0);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 0);

            assert.equal(call.state, `inactive`, `Spock sees the call as inactive after McCoy leaves`);
            assert.equal(mccoyCall.state, `inactive`, `McCoy sees the call as inactive after McCoy leaves`);
          });
      }));

      it(`calls a room by sip uri`, () => handleErrorEvent(spock.spark.phone.dial(room.sipAddress), (call) => Promise.all([
        // Spock's view of the call
        expectMembershipConnectedEvent(call, mccoy.id, `spock expects mccoy to connect`)
          .then(() => expectMembershipDisconnectedEvent(call, spock.id, `spock expects mccoy to disconnect`))
          .then(() => Promise.all([
            call.hangup(),
            expectInactiveEvent(call, `spock expects the call to go inactive`)
          ])),

        // McCoy's view of the call
        expectCallIncomingEvent(mccoy.spark.phone, `mccoy expects an incoming call`)
          .then((mc) => Promise.all([
            expectMembershipConnectedEvent(mc, mccoy.id, `mccoy expects his own connection event`),
            mc.answer()
          ])
            .then(() => mc.hangup()))
      ])));
    });

    describe(`group calling events model`, () => {
      let room;

      // Reminder: this is a before each because of hangup latency in locus's
      // eventual consistency
      beforeEach(() => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `rooms`,
        body: {
          title: `Call Test`
        }
      })
        .then((res) => {
          room = res.body;
        })
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: mccoy.id
          }
        }))
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: chekov.id
          }
        }))
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: uhura.id
          }
        })));

      // So, yea, I'm breaking my rule on "don't use async yet" until we can
      // stop compiling it, but wow, this stabilizes these coordinated event
      // tests.
      it(`async: proceeds through a seriees of events`, () => handleErrorEvent(spock.spark.phone.dial(room.id), async (call) => {
        if (call.locus) {
          assert.equal(call.state, `active`);
          assert.equal(call.me.state, `connected`);
        }

        const [mc, cc, uc] = await Promise.all([
          expectCallIncomingEvent(mccoy.spark.phone, `mccoy expects a group call`),
          expectCallIncomingEvent(chekov.spark.phone, `chekov expects a group call`),
          expectCallIncomingEvent(uhura.spark.phone, `uhura expects a group call`),
          call.locus || expectChangeLocusEvent(call, `spock expects to receive a locus`)
            .then(() => {
              assert.equal(call.state, `active`);
              assert.equal(call.me.state, `connected`);
            })
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectMembershipDeclinedEvent(call, chekov.id, `spock expects chekov to decline the call`),
          expectMembershipDeclinedEvent(mc, chekov.id, `mccoy expects chekov to decline the call`),
          expectMembershipDeclinedEvent(uc, chekov.id, `uhura expects chekov to decline the call`),
          expectNEvents(20000, 2, `membership:connected`, call),
          expectMembershipConnectedEvent(mc, uhura.id, `mccoy expects uhura to join`),
          expectMembershipConnectedEvent(uc, mccoy.id, `uhura expects mccoy to join`),
          // FIXME the connected event seems really flaky
          // Consider the possibility that we're already connected?
          // expectConnectedEvent(mc, `mccoy expects to join`),
          // expectConnectedEvent(uc, `uhura expects to join`),
          mc.answer(),
          cc.decline(),
          uc.answer()
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectMembershipEvent(`membership:change`, call, mccoy.id, `spock expects mccoy to mute`)
            .then((membership) => {
              assert.equal(membership.personUuid, mccoy.id);
              assert.isTrue(membership.audioMuted, `spock sees mccoy's audio as muted`);
            }),
          expectMembershipEvent(`membership:change`, uc, mccoy.id, `uhura expects mccoy to mute`)
            .then((membership) => {
              assert.equal(membership.personUuid, mccoy.id);
              assert.isTrue(membership.audioMuted, `uhura sees mccoy's audio as muted`);
            }),
          mc.toggleSendingAudio()
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectMembershipEvent(`membership:disconnected`, call, uhura.id, `spock expects uhura to hangup`),
          expectMembershipEvent(`membership:disconnected`, mc, uhura.id, `mccoy expects uhura to hangup`),
          // expectDisconnectedEvent(uc, `uhura expects to disconnect`),
          uc.hangup()
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectDisconnectedEvent(mc, `mccoy expects to disconnect`),
          mc.hangup()
        ]);

        await call.hangup();
      }));
    });
  });
});
<commit_msg>test(@ciscospark/internal-plugin-phone): disable mute in roundtrip test<commit_after>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/plugin-phone';

import {assert} from '@ciscospark/test-helper-chai';
import CiscoSpark from '@ciscospark/spark-core';
import testUsers from '@ciscospark/test-helper-test-users';
import handleErrorEvent from '../lib/handle-error-event';
import {
  browserOnly,
  expectNEvents,
  maxWaitForEvent
} from '@ciscospark/test-helper-mocha';
import {
  expectCallIncomingEvent,
  expectChangeActiveParticipantsCountEvent,
  expectChangeLocusEvent,
  expectChangeStateEvent,
  expectInactiveEvent,
  expectMembershipEvent,
  expectMembershipConnectedEvent,
  expectMembershipDeclinedEvent,
  expectMembershipDisconnectedEvent
} from '../lib/event-expectations';

if (process.env.NODE_ENV !== `test`) {
  throw new Error(`Cannot run the plugin-phone test suite without NODE_ENV === "test"`);
}

browserOnly(describe)(`plugin-phone`, function() {
  this.timeout(120000);

  describe(`Phone`, () => {
    const users = {
      chekov: null,
      mccoy: null,
      spock: null,
      uhura: null
    };
    let chekov, mccoy, spock, uhura;

    before(`create users and register`, () => testUsers.create({count: Object.keys(users).length})
      .then((created) => Promise.all(Object.keys(users).map((name, index) => {
        const user = users[name] = created[index];
        user.spark = new CiscoSpark({
          credentials: {
            authorization: user.token
          }
        });

        return user.spark.phone.register();
      })))
      .then(() => {
        chekov = users.chekov;
        mccoy = users.mccoy;
        spock = users.spock;
        uhura = users.uhura;
      }));

    beforeEach(`enable group calling`, () => Object.values(users).forEach((user) => {
      user.spark.config.phone.enableExperimentalGroupCallingSupport = true;
    }));

    afterEach(`disable group calling`, () => Object.values(users).forEach((user) => {
      user.spark.config.phone.enableExperimentalGroupCallingSupport = false;
    }));

    after(`unregister users`, () => Promise.all(Object.keys(users).map((name) => {
      const user = users[name];
      return user && user.spark.phone.deregister()
        .catch((reason) => console.warn(`could not unregister ${user}`, reason));
    })));

    describe(`#dial()`, () => {
      let room;
      beforeEach(`create room via hydra`, () => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `rooms`,
        body: {
          title: `Call Test`
        }
      })
        .then((res) => {
          room = res.body;
        }));

      beforeEach(`add mccoy to room via hydra`, () => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `memberships`,
        body: {
          personId: mccoy.id,
          roomId: room.id
        }
      }));

      beforeEach(`refresh room details from hydra`, () => spock.spark.request({
        method: `GET`,
        service: `hydra`,
        resource: `rooms/${room.id}`
      })
        .then((res) => {
          room = res.body;
        }));

      it(`calls a room by hydra room id`, () => handleErrorEvent(spock.spark.phone.dial(room.id), (call) => {
        let mccoyCall;
        assert.isUndefined(call.state);
        return Promise.all([
          Promise.resolve()
            .then(() => call.locus || expectChangeLocusEvent(call))
            .then(() => call.state === `active` || expectChangeStateEvent(call))
            .then(() => assert.equal(call.state, `active`)),
          expectCallIncomingEvent(mccoy.spark.phone)
        ])
          // eslint-disable-next-line no-unused-vars
          .then(([_, mc]) => {
            mccoyCall = mc;

            assert.isFalse(mccoyCall.isCall, `McCoy sees the Call as a MEETING`);

            assert.isDefined(mccoyCall.locus, `McCoy's call has a locus`);
            assert.equal(mccoyCall.state, `active`, `McCoy's view of the call is active`);
            return Promise.all([
              mccoyCall.activeParticipantsCount === 2 || expectChangeActiveParticipantsCountEvent(mccoyCall),
              call.activeParticipantsCount === 2 || expectChangeActiveParticipantsCountEvent(call),
              mccoyCall.answer()
            ]);
          })
          .then(() => {
            // The find statements here are taking advantage of the fact
            // that this is, in fact, a two-party call
            assert.equal(call.memberships.find((m) => m._self).state, `connected`, `Spock sees Spock as connected`);
            assert.equal(mccoyCall.memberships.find((m) => m._self).state, `connected`, `McCoy sees McCoy as connected`);

            assert.equal(call.memberships.find((m) => !m._self).state, `connected`, `Spock sees McCoy as connected`);
            assert.equal(mccoyCall.memberships.find((m) => !m._self).state, `connected`, `McCoy sees Spock as connected`);

            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 2);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 2);
          })
          .then(() => Promise.all([
            maxWaitForEvent(5000, `change:activeParticipantsCount`, mccoyCall),
            maxWaitForEvent(5000, `change:activeParticipantsCount`, call),
            call.hangup()
          ]))
          .then(() => {
            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 1);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 1);

            assert.equal(call.state, `active`, `Spock sees the call as active after Spock leaves`);
            assert.equal(mccoyCall.state, `active`, `McCoy sees the call as active after Spock leaves`);
            return Promise.all([
              maxWaitForEvent(5000, `inactive`, call),
              maxWaitForEvent(5000, `inactive`, mccoyCall),
              mccoyCall.hangup()
            ]);
          })
          .then(() => {
            assert.lengthOf(call.memberships, 2);
            assert.equal(call.activeParticipantsCount, 0);
            assert.lengthOf(mccoyCall.memberships, 2);
            assert.equal(mccoyCall.activeParticipantsCount, 0);

            assert.equal(call.state, `inactive`, `Spock sees the call as inactive after McCoy leaves`);
            assert.equal(mccoyCall.state, `inactive`, `McCoy sees the call as inactive after McCoy leaves`);
          });
      }));

      it(`calls a room by sip uri`, () => handleErrorEvent(spock.spark.phone.dial(room.sipAddress), (call) => Promise.all([
        // Spock's view of the call
        expectMembershipConnectedEvent(call, mccoy.id, `spock expects mccoy to connect`)
          .then(() => expectMembershipDisconnectedEvent(call, spock.id, `spock expects mccoy to disconnect`))
          .then(() => Promise.all([
            call.hangup(),
            expectInactiveEvent(call, `spock expects the call to go inactive`)
          ])),

        // McCoy's view of the call
        expectCallIncomingEvent(mccoy.spark.phone, `mccoy expects an incoming call`)
          .then((mc) => Promise.all([
            expectMembershipConnectedEvent(mc, mccoy.id, `mccoy expects his own connection event`),
            mc.answer()
          ])
            .then(() => mc.hangup()))
      ])));
    });

    describe(`group calling events model`, () => {
      let room;

      // Reminder: this is a before each because of hangup latency in locus's
      // eventual consistency
      beforeEach(() => spock.spark.request({
        method: `POST`,
        service: `hydra`,
        resource: `rooms`,
        body: {
          title: `Call Test`
        }
      })
        .then((res) => {
          room = res.body;
        })
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: mccoy.id
          }
        }))
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: chekov.id
          }
        }))
        .then(() => spock.spark.request({
          method: `POST`,
          service: `hydra`,
          resource: `memberships`,
          body: {
            roomId: room.id,
            personId: uhura.id
          }
        })));

      // So, yea, I'm breaking my rule on "don't use async yet" until we can
      // stop compiling it, but wow, this stabilizes these coordinated event
      // tests.
      it(`async: proceeds through a seriees of events`, () => handleErrorEvent(spock.spark.phone.dial(room.id), async (call) => {
        if (call.locus) {
          assert.equal(call.state, `active`);
          assert.equal(call.me.state, `connected`);
        }

        const [mc, cc, uc] = await Promise.all([
          expectCallIncomingEvent(mccoy.spark.phone, `mccoy expects a group call`),
          expectCallIncomingEvent(chekov.spark.phone, `chekov expects a group call`),
          expectCallIncomingEvent(uhura.spark.phone, `uhura expects a group call`),
          call.locus || expectChangeLocusEvent(call, `spock expects to receive a locus`)
            .then(() => {
              assert.equal(call.state, `active`);
              assert.equal(call.me.state, `connected`);
            })
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectMembershipDeclinedEvent(call, chekov.id, `spock expects chekov to decline the call`),
          expectMembershipDeclinedEvent(mc, chekov.id, `mccoy expects chekov to decline the call`),
          expectMembershipDeclinedEvent(uc, chekov.id, `uhura expects chekov to decline the call`),
          expectNEvents(20000, 2, `membership:connected`, call),
          expectMembershipConnectedEvent(mc, uhura.id, `mccoy expects uhura to join`),
          expectMembershipConnectedEvent(uc, mccoy.id, `uhura expects mccoy to join`),
          // FIXME the connected event seems really flaky
          // Consider the possibility that we're already connected?
          // expectConnectedEvent(mc, `mccoy expects to join`),
          // expectConnectedEvent(uc, `uhura expects to join`),
          mc.answer(),
          cc.decline(),
          uc.answer()
        ]);

        assert.equal(call.me.state, `connected`);

        // FIXME this part of the test is really unstable but I think it's
        // simply a result of the disaster that is the updateMedia endpoint.
        // I'm disabling it for now until we can (a) reduce negotiations when
        // muting and/or (b) move to roap to avoid using that endpoint. Moreover
        // mute is in media-state-controls.
        // await Promise.all([
        //   expectMembershipEvent(`membership:change`, call, mccoy.id, `spock expects mccoy to mute`)
        //     .then((membership) => {
        //       assert.equal(membership.personUuid, mccoy.id);
        //       assert.isTrue(membership.audioMuted, `spock sees mccoy's audio as muted`);
        //     }),
        //   expectMembershipEvent(`membership:change`, uc, mccoy.id, `uhura expects mccoy to mute`)
        //     .then((membership) => {
        //       assert.equal(membership.personUuid, mccoy.id);
        //       assert.isTrue(membership.audioMuted, `uhura sees mccoy's audio as muted`);
        //     }),
        //   mc.toggleSendingAudio()
        // ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          expectMembershipEvent(`membership:disconnected`, call, uhura.id, `spock expects uhura to hangup`),
          expectMembershipEvent(`membership:disconnected`, mc, uhura.id, `mccoy expects uhura to hangup`),
          // FIXME the disconnected event seems really flaky
          // expectDisconnectedEvent(uc, `uhura expects to disconnect`),
          uc.hangup()
        ]);

        assert.equal(call.me.state, `connected`);

        await Promise.all([
          // FIXME the disconnected event seems really flaky
          // expectDisconnectedEvent(mc, `mccoy expects to disconnect`),
          mc.hangup()
        ]);

        await call.hangup();
      }));
    });
  });
});
<|endoftext|>


<commit_before>
var TFS = Titanium.Filesystem;
var TA  = Titanium.App;
var Drillbit = Titanium.Drillbit;

var frontend = {
	test_status:function(name,classname)
	{
		var el = $('#suite_'+name+'_status');
		el.html(classname);
		el.removeClass('untested').removeClass('failed').removeClass('running')
			.removeClass('passed').addClass(classname.toLowerCase());
		$('#suite_'+name).removeClass('suite-untested').removeClass('suite-failed')
			.removeClass('suite-running').removeClass('suite-passed').addClass('suite-'+classname.toLowerCase());
	},

	suite_started:function(suite)
	{
		this.test_status(suite, 'running');
	},

	total_progress:function(passed,failed,total)
	{
		$('#passed-count').html('  '+passed+' passed');
		$('#failed-count').html('  '+failed+' failed');
	},

	show_current_test:function(suite_name, test_name)
	{
		$('#current-test').html(''+suite_name + ':  ' + test_name);
	},

	add_assertion:function(test_name, line_number)
	{
		$('#assertion-count').html(Drillbit.total_assertions+' assertions');
	}
};

function show_test_details(name)
{
	var w = Titanium.UI.currentWindow.createWindow();
	w.setHeight(600);
	w.setWidth(600);
	w.setURL('app://test_results/' + name + '.html');
	w.open();
}

function toggle_test_includes()
{
	$.each($("div.suites img"),function()
	{
		if ($(this).attr('src').indexOf('check_on') == -1)
		{
			$(this).attr('src', 'images/check_on.png');
		}
		else
		{
			$(this).attr('src', 'images/check_off.png');
		}
	});
}

function select_tests(tests)
{
	// clear the table
	$("div.suites img").attr('src', 'images/check_off.png');

	//select tests
	for (var t = 0; t < tests.length; t++) {
		var test = tests[t];
		$('#suite_'+test+' img').attr('src', 'images/check_on.png');
	}
}

function clear_current_test()
{
	$('#current-test').html('<no tests currently running>')
}

function reset_all()
{
	$('div[class^=suite] img').attr('src', 'images/check_on.png');
	$('div[id^=suite_]').removeClass().addClass('suite');
	$('span[id^=suite_]').removeClass().addClass('untested').html('untested');
	$('#assertion-count').html('0 assertions');
	$('#passed-count').html('  0 passed');
	$('#failed-count').html('  0 failed');
	clear_current_test();
	Drillbit.reset();
}

var tests = {};
$(window).ready(function()
{
	Drillbit.run_tests_async = true;
	Drillbit.frontend = frontend;
	Drillbit.window = window;

	var test_dir = TFS.getFile(TA.appURLToPath('app://tests'));
	var dir_list = test_dir.getDirectoryListing();

	Drillbit.loadTests(dir_list);
	Drillbit.setupTestHarness(TFS.getFile(TFS.getApplicationDirectory(), 'manifest_harness'));

	$("#test-count").html(Drillbit.total_tests + ' tests in ' + Drillbit.total_files + ' files');

	var suites_html = '';
	for (var c=0;c<Drillbit.test_names.length;c++)
	{
		var name = Drillbit.test_names[c];
		var entry = Drillbit.tests[name];

		suites_html +=
		'
'+
			' '+name+'
'+
			''+entry.description+'
'+
			'untested'+
		'
';
	}

	$('div.suites').html(suites_html);
	$('div[id^=suite_]').click(function()
	{
		if ($(this).find('img').attr('src').indexOf('check_on') != -1)
		{
			$(this).find('img').attr('src', 'images/check_off.png');
		}
		else
		{
			$(this).find('img').attr('src', 'images/check_on.png');
		}
	});
	$('div[id^=suite_]').dblclick(function()
	{
		var suite_name = $(this).attr('id').substr(6);
		show_test_details(suite_name);
	});

	var run_link = $('#run-link');
	$('#toggle-link').click(function() {
		toggle_test_includes();
	});
	$('#reset-link').click(function() {
		reset_all();
	});

	run_link.click(function ()
	{
		if (!run_link.disabled)
		{
			run_link.disabled = true;
			var tests = [];
			$.each($('div[id^=suite_]'),function()
			{
				var name = $(this).attr('id').substr(6);
				frontend.test_status(name,'untested');
				if ($(this).find('img').attr('src').indexOf('check_on') != -1)
				{
					tests.push({suite: name, tests:'all'});
				}
			});

			Drillbit.runTests(tests, true);
		}
		else
		{
			alert("Tests are currently running");
		}
	});

	// if you pass in --autorun, just go ahead and start
	for (var c=0;c<Titanium.App.arguments.length;c++)
	{
		var arg = Titanium.App.arguments[c];

		if (arg == '--autorun')
		{
			run_link.click();
		}
		else if (arg == '--autoclose')
		{
			Drillbit.auto_close = true;
		}
		else if (arg == '--debug-tests')
		{
			Drillbit.debug_tests = true;
		}
		else if (arg == '--console')
		{
			Titanium.UI.currentWindow.showInspector(true);
		}
		else if (arg.indexOf('--tests=')==0)
		{
			specific_tests = arg.substring(8).split(',');
			select_tests(specific_tests);
		}
	}
});

<commit_msg>Adjust drillbit results window size.<commit_after>
var TFS = Titanium.Filesystem;
var TA  = Titanium.App;
var Drillbit = Titanium.Drillbit;

var frontend = {
	test_status:function(name,classname)
	{
		var el = $('#suite_'+name+'_status');
		el.html(classname);
		el.removeClass('untested').removeClass('failed').removeClass('running')
			.removeClass('passed').addClass(classname.toLowerCase());
		$('#suite_'+name).removeClass('suite-untested').removeClass('suite-failed')
			.removeClass('suite-running').removeClass('suite-passed').addClass('suite-'+classname.toLowerCase());
	},

	suite_started:function(suite)
	{
		this.test_status(suite, 'running');
	},

	total_progress:function(passed,failed,total)
	{
		$('#passed-count').html('  '+passed+' passed');
		$('#failed-count').html('  '+failed+' failed');
	},

	show_current_test:function(suite_name, test_name)
	{
		$('#current-test').html(''+suite_name + ':  ' + test_name);
	},

	add_assertion:function(test_name, line_number)
	{
		$('#assertion-count').html(Drillbit.total_assertions+' assertions');
	}
};

function show_test_details(name)
{
	var w = Titanium.UI.currentWindow.createWindow();
	w.setHeight(600);
	w.setWidth(850);
	w.setURL('app://test_results/' + name + '.html');
	w.open();
}

function toggle_test_includes()
{
	$.each($("div.suites img"),function()
	{
		if ($(this).attr('src').indexOf('check_on') == -1)
		{
			$(this).attr('src', 'images/check_on.png');
		}
		else
		{
			$(this).attr('src', 'images/check_off.png');
		}
	});
}

function select_tests(tests)
{
	// clear the table
	$("div.suites img").attr('src', 'images/check_off.png');

	//select tests
	for (var t = 0; t < tests.length; t++) {
		var test = tests[t];
		$('#suite_'+test+' img').attr('src', 'images/check_on.png');
	}
}

function clear_current_test()
{
	$('#current-test').html('<no tests currently running>')
}

function reset_all()
{
	$('div[class^=suite] img').attr('src', 'images/check_on.png');
	$('div[id^=suite_]').removeClass().addClass('suite');
	$('span[id^=suite_]').removeClass().addClass('untested').html('untested');
	$('#assertion-count').html('0 assertions');
	$('#passed-count').html('  0 passed');
	$('#failed-count').html('  0 failed');
	clear_current_test();
	Drillbit.reset();
}

var tests = {};
$(window).ready(function()
{
	Drillbit.run_tests_async = true;
	Drillbit.frontend = frontend;
	Drillbit.window = window;

	var test_dir = TFS.getFile(TA.appURLToPath('app://tests'));
	var dir_list = test_dir.getDirectoryListing();

	Drillbit.loadTests(dir_list);
	Drillbit.setupTestHarness(TFS.getFile(TFS.getApplicationDirectory(), 'manifest_harness'));

	$("#test-count").html(Drillbit.total_tests + ' tests in ' + Drillbit.total_files + ' files');

	var suites_html = '';
	for (var c=0;c<Drillbit.test_names.length;c++)
	{
		var name = Drillbit.test_names[c];
		var entry = Drillbit.tests[name];

		suites_html +=
		'
'+
			' '+name+'
'+
			''+entry.description+'
'+
			'untested'+
		'
';
	}

	$('div.suites').html(suites_html);
	$('div[id^=suite_]').click(function()
	{
		if ($(this).find('img').attr('src').indexOf('check_on') != -1)
		{
			$(this).find('img').attr('src', 'images/check_off.png');
		}
		else
		{
			$(this).find('img').attr('src', 'images/check_on.png');
		}
	});
	$('div[id^=suite_]').dblclick(function()
	{
		var suite_name = $(this).attr('id').substr(6);
		show_test_details(suite_name);
	});

	var run_link = $('#run-link');
	$('#toggle-link').click(function() {
		toggle_test_includes();
	});
	$('#reset-link').click(function() {
		reset_all();
	});

	run_link.click(function ()
	{
		if (!run_link.disabled)
		{
			run_link.disabled = true;
			var tests = [];
			$.each($('div[id^=suite_]'),function()
			{
				var name = $(this).attr('id').substr(6);
				frontend.test_status(name,'untested');
				if ($(this).find('img').attr('src').indexOf('check_on') != -1)
				{
					tests.push({suite: name, tests:'all'});
				}
			});

			Drillbit.runTests(tests, true);
		}
		else
		{
			alert("Tests are currently running");
		}
	});

	// if you pass in --autorun, just go ahead and start
	for (var c=0;c<Titanium.App.arguments.length;c++)
	{
		var arg = Titanium.App.arguments[c];

		if (arg == '--autorun')
		{
			run_link.click();
		}
		else if (arg == '--autoclose')
		{
			Drillbit.auto_close = true;
		}
		else if (arg == '--debug-tests')
		{
			Drillbit.debug_tests = true;
		}
		else if (arg == '--console')
		{
			Titanium.UI.currentWindow.showInspector(true);
		}
		else if (arg.indexOf('--tests=')==0)
		{
			specific_tests = arg.substring(8).split(',');
			select_tests(specific_tests);
		}
	}
});

<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import {
  ICON_TYPE_VIDEO_OUTLINE
} from '@ciscospark/react-component-icon';
import Avatar from '@ciscospark/react-component-avatar';
import Button from '@ciscospark/react-component-button';
import messages from '../../messages';
import styles from './styles.css';

export default function SpaceItem({
  avatarUrl,
  formatMessage,
  id,
  isUnread,
  lastActivityTime,
  latestActivity,
  name,
  onClick,
  onCallClick,
  teamName,
  teamColor,
  type
}) {

  function handleClick(e) {
    // If there is a keypress and it's not the Enter key, don't do anything
    if (e.keyCode && e.keyCode !== 13 && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
      return false;
    }
    return onClick(id);
  }

  function handleCallClick(e) {
    if (type === `direct`) {
      e.stopPropagation();
      return onCallClick(id);
    }
    return false;
  }

  const actorName = latestActivity.actorName;
  let activityText = {actorName}{latestActivity.text};

  switch (latestActivity.type) {
  case `update`: {
    if (latestActivity.object.objectType === `locusSessionSummary`) {
      if (latestActivity.object.duration < 2) {
        activityText = formatMessage(messages.unavailable, {actorName: name});
      }
    }
    break;
  }
  case `share`: {
    activityText = formatMessage(messages.sharedFile);
    break;
  }
  case `add`: {
    activityText = formatMessage(messages.addedToSpace, {actorName, targetName: latestActivity.text.split(` `)[0]});
    break;
  }
  default:
  }

  const hasCallSupport = typeof onCallClick === `function`;
  const isDirectSpace = type === `direct`;

  return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
    <div
      className={classNames(`space-item`, styles.item, {
        [styles.directSpace]: isDirectSpace,
        [styles.hasCallSupport]: !!hasCallSupport
      })}
      onClick={handleClick}
      onKeyDown={handleClick}
      role="button"
      tabIndex="0"
    >
      {
        isUnread &&
        <div className={classNames(`space-unread-indicator`, styles.unreadIndicator)} />
      }
      <div className={classNames(`space-avatar-wrapper`, styles.avatarWrapper)}>
        <div className={classNames(`space-avatar-border`, type === `group` && styles.avatarGroup)}>
          <Avatar baseColor={teamColor} image={avatarUrl} name={name} />

      <div className={classNames(`space-item-meta`, styles.meta)}>
        {
          teamName &&
          <div
            className={classNames(`space-team-name`, styles.teamName)}
            style={teamColor && {color: teamColor}}
          >
            {teamName}

        }
        <div className={classNames(`space-title`, styles.title, isUnread ? styles.isUnread : ``)}>
          {name}

        <div className={classNames(`space-last-activity`, styles.lastActivity)}>
          {activityText}

      <div className={classNames(`space-last-activity-time`, styles.timestamp)}>
        {lastActivityTime}

      {
        isDirectSpace && hasCallSupport &&
        <div className={classNames(`space-actions`, styles.actions)}>
          <Button
            buttonClassName={classNames(styles.actionButton, styles.callButton)}
            iconType={ICON_TYPE_VIDEO_OUTLINE}
            onClick={handleCallClick}
          />

      }

  );
}

SpaceItem.propTypes = {
  avatarUrl: PropTypes.string,
  formatMessage: PropTypes.func,
  id: PropTypes.string,
  isUnread: PropTypes.bool,
  lastActivityTime: PropTypes.string,
  latestActivity: PropTypes.object,
  name: PropTypes.string,
  onCallClick: PropTypes.func,
  onClick: PropTypes.func,
  teamColor: PropTypes.string,
  teamName: PropTypes.string,
  type: PropTypes.string
};
<commit_msg>refactor(widget-recents): Add additional method for keydown<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import {
  ICON_TYPE_VIDEO_OUTLINE
} from '@ciscospark/react-component-icon';
import Avatar from '@ciscospark/react-component-avatar';
import Button from '@ciscospark/react-component-button';
import messages from '../../messages';
import styles from './styles.css';

export default function SpaceItem({
  avatarUrl,
  formatMessage,
  id,
  isUnread,
  lastActivityTime,
  latestActivity,
  name,
  onClick,
  onCallClick,
  teamName,
  teamColor,
  type
}) {

  function handleClick() {
    return onClick(id);
  }

  function handleKeyDown(e) {
    // If there is a keypress is Enter key
    if (e.keyCode && e.keyCode === 13) {
      return onClick(id);
    }
    return false;
  }

  function handleCallClick(e) {
    if (type === `direct`) {
      e.stopPropagation();
      return onCallClick(id);
    }
    return false;
  }

  const actorName = latestActivity.actorName;
  let activityText = {actorName}{latestActivity.text};

  switch (latestActivity.type) {
  case `update`: {
    if (latestActivity.object.objectType === `locusSessionSummary`) {
      if (latestActivity.object.duration < 2) {
        activityText = formatMessage(messages.unavailable, {actorName: name});
      }
    }
    break;
  }
  case `share`: {
    activityText = formatMessage(messages.sharedFile);
    break;
  }
  case `add`: {
    activityText = formatMessage(messages.addedToSpace, {actorName, targetName: latestActivity.text.split(` `)[0]});
    break;
  }
  default:
  }

  const hasCallSupport = typeof onCallClick === `function`;
  const isDirectSpace = type === `direct`;

  return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
    <div
      className={classNames(`space-item`, styles.item, {
        [styles.directSpace]: isDirectSpace,
        [styles.hasCallSupport]: !!hasCallSupport
      })}
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      role="button"
      tabIndex="0"
    >
      {
        isUnread &&
        <div className={classNames(`space-unread-indicator`, styles.unreadIndicator)} />
      }
      <div className={classNames(`space-avatar-wrapper`, styles.avatarWrapper)}>
        <div className={classNames(`space-avatar-border`, type === `group` && styles.avatarGroup)}>
          <Avatar baseColor={teamColor} image={avatarUrl} name={name} />

      <div className={classNames(`space-item-meta`, styles.meta)}>
        {
          teamName &&
          <div
            className={classNames(`space-team-name`, styles.teamName)}
            style={teamColor && {color: teamColor}}
          >
            {teamName}

        }
        <div className={classNames(`space-title`, styles.title, isUnread ? styles.isUnread : ``)}>
          {name}

        <div className={classNames(`space-last-activity`, styles.lastActivity)}>
          {activityText}

      <div className={classNames(`space-last-activity-time`, styles.timestamp)}>
        {lastActivityTime}

      {
        isDirectSpace && hasCallSupport &&
        <div className={classNames(`space-actions`, styles.actions)}>
          <Button
            buttonClassName={classNames(styles.actionButton, styles.callButton)}
            iconType={ICON_TYPE_VIDEO_OUTLINE}
            onClick={handleCallClick}
          />

      }

  );
}

SpaceItem.propTypes = {
  avatarUrl: PropTypes.string,
  formatMessage: PropTypes.func,
  id: PropTypes.string,
  isUnread: PropTypes.bool,
  lastActivityTime: PropTypes.string,
  latestActivity: PropTypes.object,
  name: PropTypes.string,
  onCallClick: PropTypes.func,
  onClick: PropTypes.func,
  teamColor: PropTypes.string,
  teamName: PropTypes.string,
  type: PropTypes.string
};
<|endoftext|>


<commit_before>'use strict';
// Parse Init
Parse.initialize(CONFIG.PARSE_APP_ID, CONFIG.PARSE_JS_KEY);

// Angular Init
var app = angular.module('app', ['ng',
									'ui.router',
									'ui.bootstrap',
									'textareaNoreturn',
									'parse.user',
									'ngSanitize',
									'restangular',
									'ngHtmlCompile',
									'prismic.io',
									'ngCsv'
								]);
console.log('starting app');

app.config(function($locationProvider, PrismicProvider){
		$locationProvider.html5Mode(true);
		$locationProvider.hashPrefix('!');

		if (CONFIG.PRISMIC) {
			PrismicProvider.setApiEndpoint(CONFIG.PRISMIC.API_ENDPOINT);
		}
	    PrismicProvider.setLinkResolver(function(ctx, doc) {
	        return 'detail.html?id=' + doc.id + '&slug=' + doc.slug + ctx.maybeRefParam;
	    });
	}
);

app.run(function($rootScope,
                 $state,
                 $stateParams,
                 parseUser,
                 $window,
                 $location,
                 $timeout
	) {
		// Default away value
		$rootScope.isAway = false;

		$rootScope.$state = $state;
		$rootScope.$stateParams = $stateParams;

		// loading animation
		$rootScope.setLoading = function() {
			$rootScope.isViewLoading = true;
		};
		$rootScope.unsetLoading = function() {
			$rootScope.isViewLoading = false;
		};

		$rootScope.isViewLoading = false;

		$rootScope.$on('$stateChangeStart', function(ev, to, toParams, from, fromParams) {
			$rootScope.setLoading();
			if(to.name === 'home')
				$rootScope.isAway = false;
			else
				$rootScope.isAway = true;
		});

		$rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
			$rootScope.unsetLoading();
		});

		$rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) {
			$rootScope.unsetLoading();
			console.log('Error transitioning to state', to.controller, error.message);
			console.log(error.stack);
		});
	}
);<commit_msg>testing prerender settings<commit_after>'use strict';
// prerender.io init
window.prerenderReady = false;

// Parse Init
Parse.initialize(CONFIG.PARSE_APP_ID, CONFIG.PARSE_JS_KEY);

// Angular Init
var app = angular.module('app', ['ng',
									'ui.router',
									'ui.bootstrap',
									'textareaNoreturn',
									'parse.user',
									'ngSanitize',
									'restangular',
									'ngHtmlCompile',
									'prismic.io',
									'ngCsv'
								]);
console.log('starting app');

app.config(function($locationProvider, PrismicProvider){
		$locationProvider.html5Mode(true);
		$locationProvider.hashPrefix('!');

		if (CONFIG.PRISMIC) {
			PrismicProvider.setApiEndpoint(CONFIG.PRISMIC.API_ENDPOINT);
		}
	    PrismicProvider.setLinkResolver(function(ctx, doc) {
	        return 'detail.html?id=' + doc.id + '&slug=' + doc.slug + ctx.maybeRefParam;
	    });
	}
);

app.run(function($rootScope,
                 $state,
                 $stateParams,
                 parseUser,
                 $window,
                 $location,
                 $timeout
	) {
		// Default away value
		$rootScope.isAway = false;

		$rootScope.$state = $state;
		$rootScope.$stateParams = $stateParams;

		// loading animation
		$rootScope.setLoading = function() {
			$rootScope.isViewLoading = true;
		};
		$rootScope.unsetLoading = function() {
			$rootScope.isViewLoading = false;
		};

		$rootScope.isViewLoading = false;

		$rootScope.$on('$stateChangeStart', function(ev, to, toParams, from, fromParams) {
			$rootScope.setLoading();
			if(to.name === 'home')
				$rootScope.isAway = false;
			else
				$rootScope.isAway = true;
		});

		$rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
			$rootScope.unsetLoading();
			window.prerenderReady = true;
		});

		$rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) {
			$rootScope.unsetLoading();
			console.log('Error transitioning to state', to.controller, error.message);
			console.log(error.stack);
		});
	}
);<|endoftext|>