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

MODEL({
  package: 'foam.demos.sevenguisu2',
  name: 'Person',

  tableProperties: [ 'surname', 'name' ],

  properties: [
    { name: 'id', hidden: true },
                       // TODO: fix views
    { name: 'name',    view: { factory_: 'foam.ui.TextFieldView', onKeyMode: true } },
    { name: 'surname', view: { factory_: 'foam.ui.TextFieldView', onKeyMode: true } }
  ]
});

MODEL({
  package: 'foam.demos.sevenguisu2',
  name: 'CRUD',
  extends: 'foam.u2.Element',

  requires: [
    'foam.u2.DetailView',
    'foam.dao.EasyDAO',
    'foam.dao.IDBDAO', // TODO: This shouldn't be required
    'foam.demos.sevenguisu2.Person',
    'foam.u2.md.TableView'
  ],

  properties: [
    {
      name: 'prefix',
      label: 'Filter prefix',
      postSet: function(_, prefix) {
        this.filteredDAO = this.dao.where(STARTS_WITH_IC(this.Person.SURNAME, prefix));
      }
    },
    {
      model_: 'foam.core.types.DAOProperty',
      name: 'dao',
      factory: function() {
        return foam.dao.EasyDAO.create({
          model: foam.demos.sevenguisu2.Person,
          daoType: 'IDB',
          cache: true,
          seqNo: true
        });
      }
    },
    {
      model_: 'foam.core.types.DAOProperty',
      name: 'filteredDAO',
      // TODO: replace with foam.u2.TableView when available
      toPropertyE: function(X) {
        return X.lookup('foam.u2.md.TableView').create({
          title: '',
          scrollEnabed: true,
          editColumns: false
        });
      },
      factory: function() { return this.dao; }
    },
    {
      name: 'selection',
      postSet: function(_, s) { this.data.copyFrom(s); }
    },
    {
      name: 'data',
      toPropertyE: 'foam.u2.DetailView',
      factory: function() { return this.Person.create(); }
    }
  ],
  templates: [
    function CSS() {/*
      ^ { padding: 10px; }
      ^ .buttons { padding-left: 592px; }
      ^ .detailView { border: none; background: white; }
      ^ .content span { overflow: hidden !important; }
      ^ .content { width: 1000px; }
      ^ .detailPane { width: 45%; display: inline-block; margin-left: 50px; margin-top: 16px; }
      ^ .label { color: #039; font-size: 14px; padding-top: 6px; }
      ^ .prefix { margin-left: 10px; }
      ^ .summaryPane { width: 49%; display: inline-block; vertical-align: top; }
      ^ .tableView { height: 184px; outline: none; }
    */},
    function initE() {/*#U2

        Filter prefix:  <:prefix onKeyMode="true" type="search"/>

          <:filteredDAO hardSelection$={{this.selection$}}/>
          <:data/>

<:createItem/> <:updateItem/> <:deleteItem/>

    */}
  ],
  actions: [
    {
      name: 'createItem',
      label: 'Create',
      isEnabled: function() {
        var n = this.data.name, s = this.data.surname;
        return n && s;
      },
      code: function() {
        var data = this.data.clone();
        data.id = undefined;
        this.dao.put(data, {
          put: function(data) { this.data.copyFrom(data); }.bind(this)
        });
      }
    },
    {
      name: 'updateItem',
      label: 'Update',
      isEnabled: function() { return this.data.id; },
      code: function() {
        this.dao.put(this.data.clone(), {
          put: function(data) { self.data = data; }
        });
      }
    },
    {
      name: 'deleteItem',
      label: 'Delete',
      isEnabled: function() { return this.data.id; },
      code: function() {
        this.dao.remove(this.data.clone());
        this.data.id = this.data.name = this.data.surname = '';
      }
    }
  ]
});
<commit_msg>Updated U2 CRUD demo to use new U2 TableView.<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

MODEL({
  package: 'foam.demos.sevenguisu2',
  name: 'Person',

  tableProperties: [ 'surname', 'name' ],

  properties: [
    { name: 'id', hidden: true },
                       // TODO: fix views
    { name: 'name',    view: { factory_: 'foam.ui.TextFieldView', onKeyMode: true } },
    { name: 'surname', view: { factory_: 'foam.ui.TextFieldView', onKeyMode: true } }
  ]
});

MODEL({
  package: 'foam.demos.sevenguisu2',
  name: 'CRUD',
  extends: 'foam.u2.Element',

  requires: [
    'foam.u2.DetailView',
    'foam.dao.EasyDAO',
    'foam.dao.IDBDAO', // TODO: This shouldn't be required
    'foam.demos.sevenguisu2.Person',
    'foam.u2.TableView'
  ],

  properties: [
    {
      name: 'prefix',
      label: 'Filter prefix',
      postSet: function(_, prefix) {
        this.filteredDAO = this.dao.where(STARTS_WITH_IC(this.Person.SURNAME, prefix));
      }
    },
    {
      model_: 'foam.core.types.DAOProperty',
      name: 'dao',
      factory: function() {
        return foam.dao.EasyDAO.create({
          model: foam.demos.sevenguisu2.Person,
          daoType: 'IDB',
          cache: true,
          seqNo: true
        });
      }
    },
    {
      model_: 'foam.core.types.DAOProperty',
      name: 'filteredDAO',
      // TODO: replace with foam.u2.TableView when available
      toPropertyE: function(X) {
        return X.lookup('foam.u2.TableView').create({
          title: '',
          scrollEnabed: true,
          editColumns: false
        });
      },
      factory: function() { return this.dao; }
    },
    {
      name: 'selection',
      postSet: function(_, s) { this.data.copyFrom(s); }
    },
    {
      name: 'data',
      toPropertyE: 'foam.u2.DetailView',
      factory: function() { return this.Person.create(); }
    }
  ],
  templates: [
    function CSS() {/*
      ^ { padding: 10px; }
      ^ .buttons { padding-left: 592px; }
      ^ .detailView { border: none; background: white; }
      ^ .content span { overflow: hidden !important; }
      ^ .content { width: 1000px; }
      ^ .detailPane { width: 45%; display: inline-block; margin-left: 50px; margin-top: 16px; }
      ^ .label { color: #039; font-size: 14px; padding-top: 6px; }
      ^ .prefix { margin-left: 10px; }
      ^ .summaryPane { width: 49%; display: inline-block; vertical-align: top; }
      ^ .tableView { height: 184px; outline: none; }
    */},
    function initE() {/*#U2

        Filter prefix:  <:prefix onKeyMode="true" type="search"/>

          <:filteredDAO hardSelection$={{this.selection$}}/>
          <:data/>

<:createItem/> <:updateItem/> <:deleteItem/>

    */}
  ],
  actions: [
    {
      name: 'createItem',
      label: 'Create',
      isEnabled: function() {
        var n = this.data.name, s = this.data.surname;
        return n && s;
      },
      code: function() {
        var data = this.data.clone();
        data.id = undefined;
        this.dao.put(data, {
          put: function(data) { this.data.copyFrom(data); }.bind(this)
        });
      }
    },
    {
      name: 'updateItem',
      label: 'Update',
      isEnabled: function() { return this.data.id; },
      code: function() {
        this.dao.put(this.data.clone(), {
          put: function(data) { self.data = data; }
        });
      }
    },
    {
      name: 'deleteItem',
      label: 'Delete',
      isEnabled: function() { return this.data.id; },
      code: function() {
        this.dao.remove(this.data.clone());
        this.data.id = this.data.name = this.data.surname = '';
      }
    }
  ]
});
<|endoftext|>


<commit_before>import React from 'react';
import Link from 'react-router-dom/Link';

export const Front = () => (

Welcome to whatever this is!

<Link to="/about">About</Link>

);
export default Front;
<commit_msg>Propose to change the welcome message<commit_after>import React from 'react';
import Link from 'react-router-dom/Link';

export const Front = () => (

Welcome, the Future Of Libraries Is OPEN!

<Link to="/about">About</Link>

);
export default Front;
<|endoftext|>


<commit_before>chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {

    chrome.storage.sync.get({
        server: '',
        projectId: null,
        issueTypeId: null
    }, function(configuration) {
        let server = configuration.server;

        if (!(server && configuration.projectId && configuration.issueTypeId)) {
            chrome.runtime.openOptionsPage();
            return sendResponse(JSON.stringify({
                error: 'Server not defined',
                showSettings: true
            }));
        }

        server = server.replace(/\/$/, "");

        const method = {
            getTicket: function(parameter) {

                const jql = 'summary ~ "\\"' + parameter.summary + '\\""';

                fetch(server + '/rest/api/2/search?jql=' + jql + '&maxResults=1', {
                    credentials: 'include'
                })
                .then(response => response.json())
                .then(json => {
                    debugger;

                    if (json.total >= 1) {
                        sendResponse({
                            meta: {
                                server: server,
                                issue: json.issues[0].key
                            }
                        });
                    } else {
                        sendResponse({
                            meta: {
                                server: server,
                                issue: undefined
                            }
                        });
                    }
                })
                .catch(err => {
                    sendResponse(JSON.stringify({
                        error: err
                    }))
                });

            },
            createIssue: function(parameter) {

                var xhr = new XMLHttpRequest();

                xhr.open('POST', server + '/rest/api/2/issue/', true);
                xhr.setRequestHeader('Content-Type', 'application/json');

                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4) {
                        if (xhr.status === 201) {
                            var jiraData = JSON.parse(xhr.responseText);
                            jiraData["meta"] = {
                                server: server,
                                projectId: jiraData.projectId,
                                issueTypeId: jiraData.issueTypeId
                            };

                            sendResponse(jiraData);

                            if (parameter.screenshot) {
                                attachScreenshot(jiraData.key, parameter.screenshot);
                            }

                            if (parameter.har) {
                                attachHar(jiraData.key, parameter.har);
                            }

                        } else {
                            sendResponse({
                                error: xhr.responseText || xhr.status || true
                            });
                        }
                    }
                };

                const fields = {
                    project: {
                        key: configuration.projectId
                    },
                    summary: parameter.summary,
                    description: parameter.description,
                    issuetype: {
                        id: configuration.issueTypeId
                    }
                };

                const component = parameter.component;
                if (component) {
                    fields.components = [{
                        name: component
                    }];
                }

                xhr.send(JSON.stringify({
                    fields: fields
                }));

                function attachData(blob, fileName, issueId) {

                    var xhr = new XMLHttpRequest();
                    xhr.open('POST', server + '/rest/api/2/issue/' + issueId + '/attachments', true);
                    xhr.setRequestHeader('X-Atlassian-Token', 'nocheck');

                    var formData = new FormData();
                    formData.append("file", blob, fileName);

                    xhr.send(formData);
                }

                function attachScreenshot(issueId, screenshot) {
                    attachData(base64toBlob(screenshot.replace(/^data.*?,\s*/i, ""), "image/png"), "screenshot.png", issueId);
                }

                function attachHar(issueId, harFile) {

                    var xhr = new XMLHttpRequest();
                    xhr.open("GET", harFile, true);
                    xhr.responseType = "arraybuffer";

                    xhr.onload = function() {
                        attachData(new Blob([xhr.response], {
                            type: "application/octet-stream"
                        }), "requests.har", issueId);
                    };

                    xhr.send();

                }

            }
        }[request.method];

        if (method) {
            method(request.data);
        } else {
            return sendResponse({
                error: 'Method "' + method +  '" not found.'
            });
        }

    });

    function base64toBlob(base64Data, contentType) {
        contentType = contentType || '';
        var sliceSize = 1024;
        var byteCharacters = atob(base64Data);
        var bytesLength = byteCharacters.length;
        var slicesCount = Math.ceil(bytesLength / sliceSize);
        var byteArrays = new Array(slicesCount);

        for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
            var begin = sliceIndex * sliceSize;
            var end = Math.min(begin + sliceSize, bytesLength);

            var bytes = new Array(end - begin);
            for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
                bytes[i] = byteCharacters[offset].charCodeAt(0);
            }
            byteArrays[sliceIndex] = new Uint8Array(bytes);
        }
        return new Blob(byteArrays, {type: contentType});
    }

    return true;

});

<commit_msg>assign ticket directly to creator<commit_after>chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {

    chrome.storage.sync.get({
        server: '',
        projectId: null,
        issueTypeId: null,
        user: null
    }, function(configuration) {
        let server = configuration.server;

        if (!(server && configuration.projectId && configuration.issueTypeId)) {
            chrome.runtime.openOptionsPage();
            return sendResponse(JSON.stringify({
                error: 'Server not defined',
                showSettings: true
            }));
        }

        server = server.replace(/\/$/, "");

        const method = {
            getTicket: function(parameter) {

                const jql = 'summary ~ "\\"' + parameter.summary + '\\""';

                fetch(server + '/rest/api/2/search?jql=' + jql + '&maxResults=1', {
                    credentials: 'include'
                })
                .then(response => response.json())
                .then(json => {
                    debugger;

                    if (json.total >= 1) {
                        sendResponse({
                            meta: {
                                server: server,
                                issue: json.issues[0].key
                            }
                        });
                    } else {
                        sendResponse({
                            meta: {
                                server: server,
                                issue: undefined
                            }
                        });
                    }
                })
                .catch(err => {
                    sendResponse(JSON.stringify({
                        error: err
                    }))
                });

            },
            createIssue: function(parameter) {

                var xhr = new XMLHttpRequest();

                xhr.open('POST', server + '/rest/api/2/issue/', true);
                xhr.setRequestHeader('Content-Type', 'application/json');

                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4) {
                        if (xhr.status === 201) {
                            var jiraData = JSON.parse(xhr.responseText);
                            jiraData["meta"] = {
                                server: server,
                                projectId: jiraData.projectId,
                                issueTypeId: jiraData.issueTypeId
                            };

                            sendResponse(jiraData);

                            if (parameter.screenshot) {
                                attachScreenshot(jiraData.key, parameter.screenshot);
                            }

                            if (parameter.har) {
                                attachHar(jiraData.key, parameter.har);
                            }

                        } else {
                            sendResponse({
                                error: xhr.responseText || xhr.status || true
                            });
                        }
                    }
                };

                var postIssue = function() {
                const fields = {
                    project: {
                        key: configuration.projectId
                    },
                    summary: parameter.summary,
                    description: parameter.description,
                    issuetype: {
                        id: configuration.issueTypeId
                        },
                        assignee: {
                            name: configuration.user
                    }
                };

                const component = parameter.component;
                if (component) {
                    fields.components = [{
                        name: component
                    }];
                }

                xhr.send(JSON.stringify({
                    fields: fields
                }));

                };

                if (configuration.user === null) {
                    fetch(server + '/rest/auth/latest/session', {
                        credentials: 'include'
                    })
                        .then(response => response.json())
                        .then(json => {
                            configuration.user = json.name;
                            postIssue();
                        });
                } else {
                    postIssue();
                }

                function attachData(blob, fileName, issueId) {

                    var xhr = new XMLHttpRequest();
                    xhr.open('POST', server + '/rest/api/2/issue/' + issueId + '/attachments', true);
                    xhr.setRequestHeader('X-Atlassian-Token', 'nocheck');

                    var formData = new FormData();
                    formData.append("file", blob, fileName);

                    xhr.send(formData);
                }

                function attachScreenshot(issueId, screenshot) {
                    attachData(base64toBlob(screenshot.replace(/^data.*?,\s*/i, ""), "image/png"), "screenshot.png", issueId);
                }

                function attachHar(issueId, harFile) {

                    var xhr = new XMLHttpRequest();
                    xhr.open("GET", harFile, true);
                    xhr.responseType = "arraybuffer";

                    xhr.onload = function() {
                        attachData(new Blob([xhr.response], {
                            type: "application/octet-stream"
                        }), "requests.har", issueId);
                    };

                    xhr.send();

                }

            }
        }[request.method];

        if (method) {
            method(request.data);
        } else {
            return sendResponse({
                error: 'Method "' + method +  '" not found.'
            });
        }

    });

    function base64toBlob(base64Data, contentType) {
        contentType = contentType || '';
        var sliceSize = 1024;
        var byteCharacters = atob(base64Data);
        var bytesLength = byteCharacters.length;
        var slicesCount = Math.ceil(bytesLength / sliceSize);
        var byteArrays = new Array(slicesCount);

        for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
            var begin = sliceIndex * sliceSize;
            var end = Math.min(begin + sliceSize, bytesLength);

            var bytes = new Array(end - begin);
            for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
                bytes[i] = byteCharacters[offset].charCodeAt(0);
            }
            byteArrays[sliceIndex] = new Uint8Array(bytes);
        }
        return new Blob(byteArrays, {type: contentType});
    }

    return true;

});

<|endoftext|>


<commit_before>const xtend = require('xtend');
const url = require('url');

module.exports = function(params, envs) {
  if (typeof params !== 'object') {
    throw new Error('params is required');
  }

  if (typeof params.schema !== 'object') {
    throw new Error('schema is required');
  }

  const result = {};

  const keys = Object.keys(params.schema);

  //generate the result object
  keys.forEach(property => {
    const config = params.schema[property];

    if (!config.required && typeof config.default === 'undefined') {
      throw new Error(`${property} must either have a default or be required`);
    }

    if (property in envs) {
      if (config.parse && typeof envs[property] === 'string') {
        result[property] = config.parse(envs[property]);
      } else {
        result[property] = envs[property];
      }
    } else if ('default' in config) {
      result[property] = config.default;
    }
  });

  //validate required properties
  keys.forEach(property => {
    const config = params.schema[property];

    if (typeof config.required === 'undefined' || property in result) {
      return;
    }

    if (config.required === true) {
      throw new Error(`The required environment variable ${property} has not been defined`);
    }

    if (typeof config.required === 'function' && config.required(result)) {
      throw new Error(`The required environment variable ${property} has not been defined`);
    }
  });

  //validate schemas
  keys.forEach(property => {
    const config = params.schema[property];

    if (typeof config.validate !== 'function' ||
        typeof result[property] === 'undefined') {
      return;
    }

    if (!config.validate(result[property])) {
      throw new Error(`The environment variable ${property} has been defined with an invalid value`);
    }
  });

  return result;
};

module.exports.int = function(params) {
  return xtend({
    default:  0,
    parse:    parseInt,
    validate: n => n === parseInt(n, 10)
  }, params || {});
};

module.exports.float = function(params) {
  return xtend({
    default:  0,
    parse:    parseFloat,
    validate: n => n === parseFloat(n, 10)
  }, params || {});
};

module.exports.object = function(params) {
  return xtend({
    default:  0,
    parse:    JSON.parse,
    validate: v => typeof v === 'object'
  }, params || {});
};

module.exports.boolean = function(params) {
  return xtend({
    default:  false,
    parse:    v => Boolean(v),
    validate: v => typeof v === 'boolean'
  }, params || {});
};

module.exports.url = function(params) {
  return xtend({
    default:  0,
    parse:    url.parse,
    validate: v => {
      return typeof v === 'object' &&
            ('hostname' in v || 'host' in v) &&
            'port' in v;
    }
  }, params || {});
};
<commit_msg>allow to use helpers to create required schemas<commit_after>const xtend = require('xtend');
const url = require('url');

module.exports = function(params, envs) {
  if (typeof params !== 'object') {
    throw new Error('params is required');
  }

  if (typeof params.schema !== 'object') {
    throw new Error('schema is required');
  }

  const result = {};

  const keys = Object.keys(params.schema);

  //generate the result object
  keys.forEach(property => {
    const config = params.schema[property];

    if (!config.required && typeof config.default === 'undefined') {
      throw new Error(`${property} must either have a default or be required`);
    }

    if (property in envs) {
      if (config.parse && typeof envs[property] === 'string') {
        result[property] = config.parse(envs[property]);
      } else {
        result[property] = envs[property];
      }
    } else if ('default' in config) {
      result[property] = config.default;
    }
  });

  //validate required properties
  keys.forEach(property => {
    const config = params.schema[property];

    if (typeof config.required === 'undefined' || property in result) {
      return;
    }

    if (config.required === true) {
      throw new Error(`The required environment variable ${property} has not been defined`);
    }

    if (typeof config.required === 'function' && config.required(result)) {
      throw new Error(`The required environment variable ${property} has not been defined`);
    }
  });

  //validate schemas
  keys.forEach(property => {
    const config = params.schema[property];

    if (typeof config.validate !== 'function' ||
        typeof result[property] === 'undefined') {
      return;
    }

    if (!config.validate(result[property])) {
      throw new Error(`The environment variable ${property} has been defined with an invalid value`);
    }
  });

  return result;
};

module.exports.int = function(params) {
  const schema = xtend({
    default:  0,
    parse:    parseInt,
    validate: n => n === parseInt(n, 10)
  }, params || {});
  if (params.required) {
    delete schema.default;
  }
  return schema;
};

module.exports.float = function(params) {
  const schema = xtend({
    default:  0,
    parse:    parseFloat,
    validate: n => n === parseFloat(n, 10)
  }, params || {});
  if (params.required) {
    delete schema.default;
  }
  return schema;
};

module.exports.object = function(params) {
  const schema = xtend({
    default:  0,
    parse:    JSON.parse,
    validate: v => typeof v === 'object'
  }, params || {});
  if (params.required) {
    delete schema.default;
  }
  return schema;
};

module.exports.boolean = function(params) {
  const schema = xtend({
    default:  false,
    parse:    v => Boolean(v),
    validate: v => typeof v === 'boolean'
  }, params || {});
  if (params.required) {
    delete schema.default;
  }
  return schema;
};

module.exports.url = function(params) {
  const schema = xtend({
    default:  0,
    parse:    url.parse,
    validate: v => {
      return typeof v === 'object' &&
            ('hostname' in v || 'host' in v) &&
            'port' in v;
    }
  }, params || {});
  if (params.required) {
    delete schema.default;
  }
  return schema;
};
<|endoftext|>


<commit_before>'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const BootBot = require('../lib/BootBot');

describe('BootBot', () => {
  let server;
  let userId;
  let bot;

  beforeEach(() => {
    const options = {
      accessToken: '1234',
      verifyToken: '5678',
      appSecret: 'foobar'
    };
    userId = 1234;
    bot = new BootBot(options);
    server = sinon.fakeServer.create();
    server.autoRespond = true;
  });

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

  it('creates a bot instance', () => {
    expect(bot instanceof BootBot).to.equal(true);
  });

  it('throws an error if there are missing tokens', () => {
    expect(() => new BootBot()).to.throw(Error);
  });

  it('can send a text message', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Hello world!';
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        text
      }
    };

    bot.sendTextMessage(userId, text);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a text message with quick replies', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Hello world!';

    // Quick Replies as strings with auto-generated payload
    const quickReplies1 = ['Red', 'Green', 'Blue'];
    const expected1 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'text',
          title: 'Red',
          payload: 'BOOTBOT_QR_RED'
        }, {
          content_type: 'text',
          title: 'Green',
          payload: 'BOOTBOT_QR_GREEN'
        }, {
          content_type: 'text',
          title: 'Blue',
          payload: 'BOOTBOT_QR_BLUE'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies1);
    expect(spy.calledWith(expected1)).to.equal(true);

    // Quick Replies as objects with partial information
    const quickReplies2 = [{
      title: 'Purple'
    }, {
      title: 'Yellow',
      payload: 'CUSTOM_YELLOW'
    }, {
      title: 'Image',
      image_url: 'http://google.com/image.png'
    }];
    const expected2 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'text',
          title: 'Purple',
          payload: 'BOOTBOT_QR_PURPLE'
        }, {
          content_type: 'text',
          title: 'Yellow',
          payload: 'CUSTOM_YELLOW'
        }, {
          content_type: 'text',
          title: 'Image',
          payload: 'BOOTBOT_QR_IMAGE',
          image_url: 'http://google.com/image.png'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies2);
    expect(spy.calledWith(expected2)).to.equal(true);

    // Quick Replies as custom object
    const quickReplies3 = [{
      content_type: 'location'
    }, {
      content_type: 'bootbot',
      payload: 'THIS_IS_JUST_A_TEST'
    }, {
      foo: 'bar'
    }];
    const expected3 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'location'
        }, {
          content_type: 'bootbot',
          payload: 'THIS_IS_JUST_A_TEST'
        }, {
          foo: 'bar'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies3);
    expect(spy.calledWith(expected3)).to.equal(true);
  });

  it('can send a button template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Choose an option';
    const buttons = [
      { type: 'postback', title: 'Red', payload: 'COLOR_RED' },
      { type: 'postback', title: 'Green', payload: 'COLOR_GREEN' },
      { type: 'postback', title: 'Blue', payload: 'COLOR_BLUE' }
    ];
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'button',
            text,
            buttons
          }
        }
      }
    };

    bot.sendButtonTemplate(userId, text, buttons);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a generic template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const elements = [
      {
        "title": "Welcome to Peter\'s Hats",
        "image_url": "http://petersapparel.parseapp.com/img/item100-thumb.png",
        "subtitle": "We\'ve got the right hat for everyone.",
        "buttons": [
          {
            "type": "web_url",
            "url": "https://petersapparel.parseapp.com/view_item?item_id=100",
            "title": "View Website"
          },
          {
            "type": "postback",
            "title": "Start Chatting",
            "payload": "USER_DEFINED_PAYLOAD"
          }
        ]
      }
    ];
    const options = {
      imageAspectRatio: 'horizontal'
    };
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'generic',
            image_aspect_ratio: 'horizontal',
            elements
          }
        }
      }
    };

    bot.sendGenericTemplate(userId, elements, options);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a list template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const elements = [{
      "title": "Classic T-Shirt Collection",
      "image_url": "https://peterssendreceiveapp.ngrok.io/img/collection.png",
      "subtitle": "See all our colors",
      "default_action": {
        "type": "web_url",
        "url": "https://peterssendreceiveapp.ngrok.io/shop_collection",
        "messenger_extensions": true,
        "webview_height_ratio": "tall",
        "fallback_url": "https://peterssendreceiveapp.ngrok.io/"
      },
      "buttons": [{
        "title": "View",
        "type": "web_url",
        "url": "https://peterssendreceiveapp.ngrok.io/collection",
        "messenger_extensions": true,
        "webview_height_ratio": "tall",
        "fallback_url": "https://peterssendreceiveapp.ngrok.io/"
      }]
    }];
    const buttons = [{
      "title": "View More",
      "type": "postback",
      "payload": "payload"
    }]
    const options = {
      topElementStyle: 'compact'
    };
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'list',
            top_element_style: 'compact',
            elements,
            buttons
          }
        }
      }
    };

    bot.sendListTemplate(userId, elements, buttons, options);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send an attachment', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const type = 'image';
    const url = 'http://petersapparel.parseapp.com/img/item100-thumb.png';
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type,
          payload: {
            url
          }
        }
      }
    };

    bot.sendAttachment(userId, type, url);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('emits a referral event', () => {
    const callback = sinon.spy();
    const event = {
      "sender": {
        "id": "USER_ID"
      },
      "recipient": {
        "id": "PAGE_ID"
      },
      "timestamp": 1458692752478,
      "referral": {
        "ref": "MY_REF",
        "source": "SHORTLINK",
        "type": "OPEN_THREAD"
      }
    };
    bot.on('referral', callback);
    bot._handleEvent('referral', event);
    expect(callback.calledWith(event)).to.equal(true);
  });

  describe('bot.say() tests', () => {
    it('should call sendTextMessage when calling .say() with a string', () => {
      const spy = sinon.spy(bot, 'sendTextMessage');
      const recipentId = 1234;
      const message = 'hello world';
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message, [], options)).to.equal(true)
    });

    it('should call sendTextMessage with quick replies when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendTextMessage');
      const recipentId = 1234;
      const message = {
        text: 'hello world',
        quickReplies: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.text, message.quickReplies, options)).to.equal(true)
    });

    it('should call sendButtonTemplate when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendButtonTemplate');
      const recipentId = 1234;
      const message = {
        text: 'hello world',
        buttons: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.text, message.buttons, options)).to.equal(true)
    });

    it('should call sendAttachment when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendAttachment');
      const recipentId = 1234;
      const message = {
        attachment: 'image',
        url: 'https://google.com/logo.png',
        quickReplies: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.attachment, message.url, message.quickReplies, options)).to.equal(true)
    });
  });

  describe('Messenger Profile API', () => {
    it('can set a greeting text', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const text = 'Hello, world!';
      const expected = {
        greeting: [{
          locale: 'default',
          text
        }]
      };

      bot.setGreetingText(text);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can set the Get Started button', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const callback = () => {};
      const expected = {
        get_started: {
          payload: 'BOOTBOT_GET_STARTED'
        }
      };

      // With a callback
      bot.setGetStartedButton(callback);
      expect(spy.calledWith(expected)).to.equal(true);

      const payload = 'CUSTOM_GET_STARTED_PAYLOAD';
      const expected2 = {
        get_started: {
          payload
        }
      };

      // With a payload
      bot.setGetStartedButton(payload);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can delete the Get Started button', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const expected = {
        fields: [ 'get_started' ]
      };

      bot.deleteGetStartedButton();
      expect(spy.calledWith(expected, 'DELETE')).to.equal(true);
    });

    it('can set the Persistent Menu', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const buttons = [
        {
          title: 'My Account',
          type: 'nested',
          call_to_actions: [
            {
              title: 'Pay Bill',
              type: 'postback',
              payload: 'PAYBILL_PAYLOAD'
            },
            {
              title: 'History',
              type: 'postback',
              payload: 'HISTORY_PAYLOAD'
            },
            {
              title: 'Contact Info',
              type: 'postback',
              payload: 'CONTACT_INFO_PAYLOAD'
            }
          ]
        },
        {
          title: 'Go to Website',
          type: 'web_url',
          url: 'http://purple.com'
        }
      ];
      const disableInput = false;
      const expected = {
        persistent_menu: [{
          locale: 'default',
          composer_input_disabled: disableInput,
          call_to_actions: buttons
        }]
      };

      bot.setPersistentMenu(buttons, disableInput);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can delete the Persistent Menu', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const expected = {
        fields: [ 'persistent_menu' ]
      };

      bot.deletePersistentMenu();
      expect(spy.calledWith(expected, 'DELETE')).to.equal(true);
    });
  });

  describe('Checkbox Plugin support', () => {
    it('uses the user_ref param as the recipient when replying to a checkbox authentication event', () => {
      const spy = sinon.spy(bot, 'sendMessage');
      const event = {
        "recipient": {
          "id": "PAGE_ID"
        },
        "timestamp": 1234567890,
        "optin": {
          "ref": "PASS_THROUGH_PARAM",
          "user_ref": "UNIQUE_REF_PARAM"
        }
      };
      const expectedRecipient = { user_ref: 'UNIQUE_REF_PARAM' };
      const expectedMessage = { text: 'hello' };

      bot.on('authentication', (payload, chat) => {
        chat.say('hello');
      });
      bot._handleEvent('authentication', event);
      expect(spy.calledWith(expectedRecipient, expectedMessage)).to.equal(true);
    });

    it('uses the sender ID if the authentication event contains one', () => {
      const spy = sinon.spy(bot, 'sendMessage');
      const event = {
        "sender": {
          "id": "USER_ID"
        },
        "recipient": {
          "id": "PAGE_ID"
        },
        "timestamp": 1234567890,
        "optin": {
          "ref":"PASS_THROUGH_PARAM"
        }
      };
      const expectedRecipient = 'USER_ID';
      const expectedMessage = { text: 'hello' };

      bot.on('authentication', (payload, chat) => {
        chat.say('hello');
      });
      bot._handleEvent('authentication', event);
      expect(spy.calledWith(expectedRecipient, expectedMessage)).to.equal(true);
    });
  });
});
<commit_msg>Added tests for calling .say() with an array<commit_after>'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const BootBot = require('../lib/BootBot');

describe('BootBot', () => {
  let server;
  let userId;
  let bot;

  beforeEach(() => {
    const options = {
      accessToken: '1234',
      verifyToken: '5678',
      appSecret: 'foobar'
    };
    userId = 1234;
    bot = new BootBot(options);
    server = sinon.fakeServer.create();
    server.autoRespond = true;
  });

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

  it('creates a bot instance', () => {
    expect(bot instanceof BootBot).to.equal(true);
  });

  it('throws an error if there are missing tokens', () => {
    expect(() => new BootBot()).to.throw(Error);
  });

  it('can send a text message', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Hello world!';
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        text
      }
    };

    bot.sendTextMessage(userId, text);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a text message with quick replies', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Hello world!';

    // Quick Replies as strings with auto-generated payload
    const quickReplies1 = ['Red', 'Green', 'Blue'];
    const expected1 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'text',
          title: 'Red',
          payload: 'BOOTBOT_QR_RED'
        }, {
          content_type: 'text',
          title: 'Green',
          payload: 'BOOTBOT_QR_GREEN'
        }, {
          content_type: 'text',
          title: 'Blue',
          payload: 'BOOTBOT_QR_BLUE'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies1);
    expect(spy.calledWith(expected1)).to.equal(true);

    // Quick Replies as objects with partial information
    const quickReplies2 = [{
      title: 'Purple'
    }, {
      title: 'Yellow',
      payload: 'CUSTOM_YELLOW'
    }, {
      title: 'Image',
      image_url: 'http://google.com/image.png'
    }];
    const expected2 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'text',
          title: 'Purple',
          payload: 'BOOTBOT_QR_PURPLE'
        }, {
          content_type: 'text',
          title: 'Yellow',
          payload: 'CUSTOM_YELLOW'
        }, {
          content_type: 'text',
          title: 'Image',
          payload: 'BOOTBOT_QR_IMAGE',
          image_url: 'http://google.com/image.png'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies2);
    expect(spy.calledWith(expected2)).to.equal(true);

    // Quick Replies as custom object
    const quickReplies3 = [{
      content_type: 'location'
    }, {
      content_type: 'bootbot',
      payload: 'THIS_IS_JUST_A_TEST'
    }, {
      foo: 'bar'
    }];
    const expected3 = {
      recipient: {
        id: userId
      },
      message: {
        text,
        quick_replies: [{
          content_type: 'location'
        }, {
          content_type: 'bootbot',
          payload: 'THIS_IS_JUST_A_TEST'
        }, {
          foo: 'bar'
        }]
      }
    };

    bot.sendTextMessage(userId, text, quickReplies3);
    expect(spy.calledWith(expected3)).to.equal(true);
  });

  it('can send a button template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const text = 'Choose an option';
    const buttons = [
      { type: 'postback', title: 'Red', payload: 'COLOR_RED' },
      { type: 'postback', title: 'Green', payload: 'COLOR_GREEN' },
      { type: 'postback', title: 'Blue', payload: 'COLOR_BLUE' }
    ];
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'button',
            text,
            buttons
          }
        }
      }
    };

    bot.sendButtonTemplate(userId, text, buttons);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a generic template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const elements = [
      {
        "title": "Welcome to Peter\'s Hats",
        "image_url": "http://petersapparel.parseapp.com/img/item100-thumb.png",
        "subtitle": "We\'ve got the right hat for everyone.",
        "buttons": [
          {
            "type": "web_url",
            "url": "https://petersapparel.parseapp.com/view_item?item_id=100",
            "title": "View Website"
          },
          {
            "type": "postback",
            "title": "Start Chatting",
            "payload": "USER_DEFINED_PAYLOAD"
          }
        ]
      }
    ];
    const options = {
      imageAspectRatio: 'horizontal'
    };
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'generic',
            image_aspect_ratio: 'horizontal',
            elements
          }
        }
      }
    };

    bot.sendGenericTemplate(userId, elements, options);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send a list template', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const elements = [{
      "title": "Classic T-Shirt Collection",
      "image_url": "https://peterssendreceiveapp.ngrok.io/img/collection.png",
      "subtitle": "See all our colors",
      "default_action": {
        "type": "web_url",
        "url": "https://peterssendreceiveapp.ngrok.io/shop_collection",
        "messenger_extensions": true,
        "webview_height_ratio": "tall",
        "fallback_url": "https://peterssendreceiveapp.ngrok.io/"
      },
      "buttons": [{
        "title": "View",
        "type": "web_url",
        "url": "https://peterssendreceiveapp.ngrok.io/collection",
        "messenger_extensions": true,
        "webview_height_ratio": "tall",
        "fallback_url": "https://peterssendreceiveapp.ngrok.io/"
      }]
    }];
    const buttons = [{
      "title": "View More",
      "type": "postback",
      "payload": "payload"
    }]
    const options = {
      topElementStyle: 'compact'
    };
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type: 'template',
          payload: {
            template_type: 'list',
            top_element_style: 'compact',
            elements,
            buttons
          }
        }
      }
    };

    bot.sendListTemplate(userId, elements, buttons, options);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('can send an attachment', () => {
    const spy = sinon.spy(bot, 'sendRequest');
    const type = 'image';
    const url = 'http://petersapparel.parseapp.com/img/item100-thumb.png';
    const expected = {
      recipient: {
        id: userId
      },
      message: {
        attachment: {
          type,
          payload: {
            url
          }
        }
      }
    };

    bot.sendAttachment(userId, type, url);
    expect(spy.calledWith(expected)).to.equal(true);
  });

  it('emits a referral event', () => {
    const callback = sinon.spy();
    const event = {
      "sender": {
        "id": "USER_ID"
      },
      "recipient": {
        "id": "PAGE_ID"
      },
      "timestamp": 1458692752478,
      "referral": {
        "ref": "MY_REF",
        "source": "SHORTLINK",
        "type": "OPEN_THREAD"
      }
    };
    bot.on('referral', callback);
    bot._handleEvent('referral', event);
    expect(callback.calledWith(event)).to.equal(true);
  });

  describe('bot.say() tests', () => {
    it('should call sendTextMessage when calling .say() with a string', () => {
      const spy = sinon.spy(bot, 'sendTextMessage');
      const recipentId = 1234;
      const message = 'hello world';
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message, [], options)).to.equal(true)
    });

    it('should call sendTextMessage with quick replies when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendTextMessage');
      const recipentId = 1234;
      const message = {
        text: 'hello world',
        quickReplies: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.text, message.quickReplies, options)).to.equal(true)
    });

    it('should call sendButtonTemplate when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendButtonTemplate');
      const recipentId = 1234;
      const message = {
        text: 'hello world',
        buttons: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.text, message.buttons, options)).to.equal(true)
    });

    it('should call sendAttachment when calling .say() with an object ', () => {
      const spy = sinon.spy(bot, 'sendAttachment');
      const recipentId = 1234;
      const message = {
        attachment: 'image',
        url: 'https://google.com/logo.png',
        quickReplies: ['a', 'b', 'c']
      };
      const options = {};

      expect(spy.called).to.equal(false)

      bot.say(recipentId, message, options)

      expect(spy.calledWith(recipentId, message.attachment, message.url, message.quickReplies, options)).to.equal(true)
    });

    it('should make subsequent calls to .say() when calling it with an array', (done) => {
      bot.sendMessage = () => Promise.resolve()
      const spy = sinon.spy(bot, 'say');
      const recipentId = 1234;
      const messages = [
        'hello',
        'world',
        '!'
      ];
      const options = { typing: true };

      expect(spy.called).to.equal(false)

      bot.say(recipentId, messages, options).then(() => {
        expect(spy.callCount).to.equal(4)
        expect(spy.getCall(0).args).to.deep.equal([ recipentId, messages, options ])
        expect(spy.getCall(1).args).to.deep.equal([ recipentId, messages[0], options ])
        expect(spy.getCall(2).args).to.deep.equal([ recipentId, messages[1], options ])
        expect(spy.getCall(3).args).to.deep.equal([ recipentId, messages[2], options ])
        done()
      })
    });
  });

  describe('Messenger Profile API', () => {
    it('can set a greeting text', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const text = 'Hello, world!';
      const expected = {
        greeting: [{
          locale: 'default',
          text
        }]
      };

      bot.setGreetingText(text);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can set the Get Started button', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const callback = () => {};
      const expected = {
        get_started: {
          payload: 'BOOTBOT_GET_STARTED'
        }
      };

      // With a callback
      bot.setGetStartedButton(callback);
      expect(spy.calledWith(expected)).to.equal(true);

      const payload = 'CUSTOM_GET_STARTED_PAYLOAD';
      const expected2 = {
        get_started: {
          payload
        }
      };

      // With a payload
      bot.setGetStartedButton(payload);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can delete the Get Started button', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const expected = {
        fields: [ 'get_started' ]
      };

      bot.deleteGetStartedButton();
      expect(spy.calledWith(expected, 'DELETE')).to.equal(true);
    });

    it('can set the Persistent Menu', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const buttons = [
        {
          title: 'My Account',
          type: 'nested',
          call_to_actions: [
            {
              title: 'Pay Bill',
              type: 'postback',
              payload: 'PAYBILL_PAYLOAD'
            },
            {
              title: 'History',
              type: 'postback',
              payload: 'HISTORY_PAYLOAD'
            },
            {
              title: 'Contact Info',
              type: 'postback',
              payload: 'CONTACT_INFO_PAYLOAD'
            }
          ]
        },
        {
          title: 'Go to Website',
          type: 'web_url',
          url: 'http://purple.com'
        }
      ];
      const disableInput = false;
      const expected = {
        persistent_menu: [{
          locale: 'default',
          composer_input_disabled: disableInput,
          call_to_actions: buttons
        }]
      };

      bot.setPersistentMenu(buttons, disableInput);
      expect(spy.calledWith(expected)).to.equal(true);
    });

    it('can delete the Persistent Menu', () => {
      const spy = sinon.spy(bot, 'sendProfileRequest');
      const expected = {
        fields: [ 'persistent_menu' ]
      };

      bot.deletePersistentMenu();
      expect(spy.calledWith(expected, 'DELETE')).to.equal(true);
    });
  });

  describe('Checkbox Plugin support', () => {
    it('uses the user_ref param as the recipient when replying to a checkbox authentication event', () => {
      const spy = sinon.spy(bot, 'sendMessage');
      const event = {
        "recipient": {
          "id": "PAGE_ID"
        },
        "timestamp": 1234567890,
        "optin": {
          "ref": "PASS_THROUGH_PARAM",
          "user_ref": "UNIQUE_REF_PARAM"
        }
      };
      const expectedRecipient = { user_ref: 'UNIQUE_REF_PARAM' };
      const expectedMessage = { text: 'hello' };

      bot.on('authentication', (payload, chat) => {
        chat.say('hello');
      });
      bot._handleEvent('authentication', event);
      expect(spy.calledWith(expectedRecipient, expectedMessage)).to.equal(true);
    });

    it('uses the sender ID if the authentication event contains one', () => {
      const spy = sinon.spy(bot, 'sendMessage');
      const event = {
        "sender": {
          "id": "USER_ID"
        },
        "recipient": {
          "id": "PAGE_ID"
        },
        "timestamp": 1234567890,
        "optin": {
          "ref":"PASS_THROUGH_PARAM"
        }
      };
      const expectedRecipient = 'USER_ID';
      const expectedMessage = { text: 'hello' };

      bot.on('authentication', (payload, chat) => {
        chat.say('hello');
      });
      bot._handleEvent('authentication', event);
      expect(spy.calledWith(expectedRecipient, expectedMessage)).to.equal(true);
    });
  });
});
<|endoftext|>


<commit_before>import { sleep } from "../utils";

export const create = async (params) => {
    console.log('Starting to create outcome RXF brief');
    await createBrief();
    await fillWhoCanRespond();
    await fillAbout(params.title, params.locations);
    await fillResponseFormats();
    await fillRequirements();
    await fillTimeframesAndBudget();
    await fillEvaluationCriteria();
    await fillClosingDate();
    await fillPublishBrief();

}

const createBrief = async () => {
    await clickLink('/2/outcome-choice', true);
    await clickLink('/2/buyer-rfx/create', true);
    await clickLink('Create and publish request');
    await clickButton('Start now');
    }

const fillWhoCanRespond = async () => {
    await clickSaveContinue();
    await matchText('li', 'You must select at least one panel category');
    await selectDropBox();
}

const selectDropBox = async() => {
    const rfxPanelCategory = process.env.RFX_PANEL_CATEGORY;
    await page.select(`#seller-search-category-select`, rfxPanelCategory);

    const rfxSellerName = process.env.RFX_SELLER_NAME;
    await sleep(100);
    await typeInReactInput('seller-search', { value: rfxSellerName });
    let searchResult = await getElementHandles(`//input[@id="seller-search"]/../../ul/li[1]/a`);
    let sr = searchResult[0];
    sr.click();

    await typeInReactInput('seller-search', { value: '%%%' });
    searchResult = await getElementHandles('//input[@id="seller-search"]/../../ul/li');
    let resultCount =searchResult.length;
    for (let i=1; i<=resultCount; i++) {
        if (i>1){
            await sleep(100);
            await typeInReactInput('seller-search', { value: '%%%' });
        }
        searchResult = await getElementHandles(`//input[@id="seller-search"]/../../ul/li[${i}]/a`);
        sr = searchResult[0];
        sr.click();
    }
    await clickSaveContinue();
}

const fillAbout = async (role, locations) => {

    await clickSaveContinue();
    await matchText('li', 'You must add a title');
    await matchText('li', 'You must add the name of your department, agency or organisation');
    await matchText('li', 'You must add a summary of work to be done');
    await matchText('li', 'You must add the working arrangements');
    await matchText('li', 'You must select a location of where the work can be done');
    await typeInReactInput('title', { value: role });
    await typeInReactInput('organisation', { numberOfCharacters: 100 });
    await typeInReactInput('summary', { numberOfWords: 150 });

    for (let i in locations) {
        let location = locations[i];
        await selectCheck(location);
    }
    await typeInReactInput('working_arrangements', {numberOfCharacters: 150});
    await typeInReactInput('clearance', {numberOfCharacters: 100});
    await clickSaveContinue();
}

const fillResponseFormats = async () => {
    await clickSaveContinue();
    await matchText('li', 'You must choose what you would like sellers to provide through the Marketplace');
    await selectCheck('Written proposal');
    await clickSaveContinue();
    await matchText('li', 'You must select at least one proposal type.');
    await selectCheck('Breakdown of costs');
    await selectCheck('Case study');
    await selectCheck('References');
    await selectCheck('Résumés');
    await selectCheck('Response template');
    await selectCheck('Presentation');
    await clickSaveContinue();
}

const fillRequirements = async () =>{
    await clickSaveContinue();
    await matchText('li', 'You must upload a requirements document');
    await upload('file_0', 'document.pdf', 'Requirements document');
    await upload('file_0', 'document.pdf', 'Response template');
    await upload('file_0', 'document.pdf', 'Additional documents (optional)');
    await typeInReactInput('industryBriefing', { numberOfWords: 150 });
    await clickSaveContinue();

}
const fillTimeframesAndBudget = async () =>{
    await clickSaveContinue();
    await matchText('li', 'You must add an estimated start date');
    await matchText('li', 'You must add a contract length');
    await typeInReactInput('start_date', {numberOfCharacters: 150});
    await typeInReactInput('contract_length-label', {numberOfCharacters: 50});
    await typeInReactInput('contract_extensions', {numberOfCharacters: 50});
    await typeInReactInput('budget_range', {numberOfWords: 150});
    await clickSaveContinue();
}

const fillEvaluationCriteria = async () =>{

    await clickSaveContinue();
    await matchText('li', 'You must not have any empty criteria.');
    await matchText('li', 'Weightings must be greater than 0.');
    await matchText('li', 'You must not have any empty criteria.');
    await clickLink('Add another criteria');
    await typeInReactInput('criteria_0', { numberOfWords:50 });
    await typeInReactInput('weighting_0', { value: '50' });
    await typeInReactInput('criteria_1', { numberOfWords:50 });
    await typeInReactInput('weighting_1', { value: '50' });
    await clickSaveContinue();
}

const fillClosingDate = async() =>{
    await clickSaveContinue();
    await matchText('li', 'You must add a closing date at least 2 days from now');
    await matchText('li', 'You must add a contact number');
    let now = new Date();
    let future = new Date(now.setDate(now.getDate() + 14));
    await typeInReactInput('day', { value: `${future.getDate()}` });
    await typeInReactInput('month', { value: `${future.getMonth() + 1}` });
    await typeInReactInput('year', { value: `${future.getFullYear()}` });
    await typeInReactInput('contact', { value: '0123456789' });
    await clickSaveContinue();
}

const fillPublishBrief = async () => {
    await clickButton('Publish');
    await matchText('h1', 'Your opportunity is now live, and the invited sellers have been notified.');
}

const clickSaveContinue = async () => {
    console.log("here");
    await clickButton('Save and continue');
}

    <commit_msg>Update fixtures/flows/brief/rfx.js<commit_after>import { sleep } from "../utils";

export const create = async (params) => {
    console.log('Starting to create outcome RXF brief');
    await createBrief();
    await fillWhoCanRespond();
    await fillAbout(params.title, params.locations);
    await fillResponseFormats();
    await fillRequirements();
    await fillTimeframesAndBudget();
    await fillEvaluationCriteria();
    await fillClosingDate();
    await fillPublishBrief();
}

const createBrief = async () => {
    await clickLink('/2/outcome-choice', true);
    await clickLink('/2/buyer-rfx/create', true);
    await clickLink('Create and publish request');
    await clickButton('Start now');
    }

const fillWhoCanRespond = async () => {
    await clickSaveContinue();
    await matchText('li', 'You must select at least one panel category');
    await selectDropBox();
}

const selectDropBox = async() => {
    const rfxPanelCategory = process.env.RFX_PANEL_CATEGORY;
    await page.select(`#seller-search-category-select`, rfxPanelCategory);

    const rfxSellerName = process.env.RFX_SELLER_NAME;
    await sleep(100);
    await typeInReactInput('seller-search', { value: rfxSellerName });
    let searchResult = await getElementHandles(`//input[@id="seller-search"]/../../ul/li[1]/a`);
    let sr = searchResult[0];
    sr.click();

    await typeInReactInput('seller-search', { value: '%%%' });
    searchResult = await getElementHandles('//input[@id="seller-search"]/../../ul/li');
    let resultCount =searchResult.length;
    for (let i=1; i<=resultCount; i++) {
        if (i>1){
            await sleep(100);
            await typeInReactInput('seller-search', { value: '%%%' });
        }
        searchResult = await getElementHandles(`//input[@id="seller-search"]/../../ul/li[${i}]/a`);
        sr = searchResult[0];
        sr.click();
    }
    await clickSaveContinue();
}

const fillAbout = async (role, locations) => {

    await clickSaveContinue();
    await matchText('li', 'You must add a title');
    await matchText('li', 'You must add the name of your department, agency or organisation');
    await matchText('li', 'You must add a summary of work to be done');
    await matchText('li', 'You must add the working arrangements');
    await matchText('li', 'You must select a location of where the work can be done');
    await typeInReactInput('title', { value: role });
    await typeInReactInput('organisation', { numberOfCharacters: 100 });
    await typeInReactInput('summary', { numberOfWords: 150 });

    for (let i in locations) {
        let location = locations[i];
        await selectCheck(location);
    }
    await typeInReactInput('working_arrangements', {numberOfCharacters: 150});
    await typeInReactInput('clearance', {numberOfCharacters: 100});
    await clickSaveContinue();
}

const fillResponseFormats = async () => {
    await clickSaveContinue();
    await matchText('li', 'You must choose what you would like sellers to provide through the Marketplace');
    await selectCheck('Written proposal');
    await clickSaveContinue();
    await matchText('li', 'You must select at least one proposal type.');
    await selectCheck('Breakdown of costs');
    await selectCheck('Case study');
    await selectCheck('References');
    await selectCheck('Résumés');
    await selectCheck('Response template');
    await selectCheck('Presentation');
    await clickSaveContinue();
}

const fillRequirements = async () =>{
    await clickSaveContinue();
    await matchText('li', 'You must upload a requirements document');
    await upload('file_0', 'document.pdf', 'Requirements document');
    await upload('file_0', 'document.pdf', 'Response template');
    await upload('file_0', 'document.pdf', 'Additional documents (optional)');
    await typeInReactInput('industryBriefing', { numberOfWords: 150 });
    await clickSaveContinue();

}
const fillTimeframesAndBudget = async () =>{
    await clickSaveContinue();
    await matchText('li', 'You must add an estimated start date');
    await matchText('li', 'You must add a contract length');
    await typeInReactInput('start_date', {numberOfCharacters: 150});
    await typeInReactInput('contract_length-label', {numberOfCharacters: 50});
    await typeInReactInput('contract_extensions', {numberOfCharacters: 50});
    await typeInReactInput('budget_range', {numberOfWords: 150});
    await clickSaveContinue();
}

const fillEvaluationCriteria = async () =>{

    await clickSaveContinue();
    await matchText('li', 'You must not have any empty criteria.');
    await matchText('li', 'Weightings must be greater than 0.');
    await matchText('li', 'You must not have any empty criteria.');
    await clickLink('Add another criteria');
    await typeInReactInput('criteria_0', { numberOfWords:50 });
    await typeInReactInput('weighting_0', { value: '50' });
    await typeInReactInput('criteria_1', { numberOfWords:50 });
    await typeInReactInput('weighting_1', { value: '50' });
    await clickSaveContinue();
}

const fillClosingDate = async() =>{
    await clickSaveContinue();
    await matchText('li', 'You must add a closing date at least 2 days from now');
    await matchText('li', 'You must add a contact number');
    let now = new Date();
    let future = new Date(now.setDate(now.getDate() + 14));
    await typeInReactInput('day', { value: `${future.getDate()}` });
    await typeInReactInput('month', { value: `${future.getMonth() + 1}` });
    await typeInReactInput('year', { value: `${future.getFullYear()}` });
    await typeInReactInput('contact', { value: '0123456789' });
    await clickSaveContinue();
}

const fillPublishBrief = async () => {
    await clickButton('Publish');
    await matchText('h1', 'Your opportunity is now live, and the invited sellers have been notified.');
}

const clickSaveContinue = async () => {
    console.log("here");
    await clickButton('Save and continue');
}

<|endoftext|>


<commit_before>var ErrorStyleAbove = require('kwf/frontend-form/error-style/above');
var kwfExtend = require('kwf/extend');
var errorStyleRegistry = require('kwf/frontend-form/error-style-registry');

var ErrorStyleBelowField = kwfExtend(ErrorStyleAbove, {
    showErrors: function(r) {

        for (var fieldName in r.errorFields) {
            var field = this.form.findField(fieldName);
            field.el.addClass('kwfUp-kwfFieldError');
            if (!field.errorEl) {
                if (field.el.up('.kwfFormContainerColumn')) {
                    field.errorEl = field.el.up('.kwfFormContainerColumn').up('.kwfFormContainerColumns')
                        .append('

');
                } else {
                    field.errorEl = field.el.append('

');
                }
            }
            field.errorEl.show();
            field.errorEl.update(r.errorFields[fieldName]);

        }
        if (r.errorMessages && r.errorMessages.length) {
            this._showErrorMessagesAbove(r.errorMessages, r);
        }
    },
    hideFieldError: function(field)
    {
        field.el.removeClass('kwfUp-kwfFieldError');
        if (field.errorEl) field.errorEl.hide();
    }
});

errorStyleRegistry.register('belowField', ErrorStyleBelowField);
module.exports = ErrorStyleBelowField;
<commit_msg>Fix Form error style below<commit_after>var ErrorStyleAbove = require('kwf/frontend-form/error-style/above');
var kwfExtend = require('kwf/extend');
var errorStyleRegistry = require('kwf/frontend-form/error-style-registry');

var ErrorStyleBelowField = kwfExtend(ErrorStyleAbove, {
    showErrors: function(r) {

        for (var fieldName in r.errorFields) {
            var field = this.form.findField(fieldName);
            field.el.addClass('kwfUp-kwfFieldError');
            if (!field.errorEl) {
                field.errorEl = $('

').appendTo(field.el);
            }
            field.errorEl.show();
            field.errorEl.html(r.errorFields[fieldName]);
        }
        if (r.errorMessages && r.errorMessages.length) {
            this._showErrorMessagesAbove(r.errorMessages, r);
        }
    },
    hideFieldError: function(field)
    {
        field.el.removeClass('kwfUp-kwfFieldError');
        if (field.errorEl) field.errorEl.hide();
    }
});

errorStyleRegistry.register('belowField', ErrorStyleBelowField);
module.exports = ErrorStyleBelowField;
<|endoftext|>


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

CLASS({
  package: 'foam.tutorials.todo',
  name: 'TodoApp',
  extendsModel: 'foam.browser.ui.BrowserView',
  requires: [
    'foam.browser.BrowserConfig',
    'foam.browser.ui.DAOController',
    'foam.core.dao.ChromeStorageDAO',
    'foam.dao.EasyDAO',
    'foam.mlang.CannedQuery',
    'foam.tutorials.todo.model.Todo',
    'foam.tutorials.todo.ui.TodoCitationView',
    'foam.tutorials.todo.ui.TodoDetailView',
    'foam.ui.DAOListView',
    'foam.ui.ProgressView',
    'foam.ui.SpinnerView',
    'foam.ui.TextFieldView',
    'foam.ui.Tooltip',
    'foam.ui.md.CannedQueryCitationView',
    'foam.ui.md.CheckboxView',
    'foam.ui.md.PopupView',
  ],
  properties: [
    {
      name: 'data',
      factory: function() {
        return this.BrowserConfig.create({
          dao: this.EasyDAO.create({
            model: this.Todo,
            daoType: 'LOCAL',
            cache: true,
            seqNo: true
          }),
          listView: {
            factory_: 'foam.ui.DAOListView',
            rowView: 'foam.tutorials.todo.ui.TodoCitationView'
          },
          cannedQueryDAO: [
            this.CannedQuery.create({
              label: 'Todo',
              expression: EQ(this.Todo.IS_COMPLETED, false)
            }),
            this.CannedQuery.create({
              label: 'Done',
              expression: EQ(this.Todo.IS_COMPLETED, true)
            }),
            this.CannedQuery.create({
              label: 'Everything',
              expression: TRUE
            }),
          ]
        });
      }
    }
  ]
});
<commit_msg>Fixes for the tutorial app in the repo<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.tutorials.todo',
  name: 'TodoApp',
  extendsModel: 'foam.browser.ui.BrowserView',
  requires: [
    'foam.browser.BrowserConfig',
    'foam.browser.ui.DAOController',
    'foam.core.dao.ChromeStorageDAO',
    'foam.dao.EasyDAO',
    'foam.mlang.CannedQuery',
    'foam.tutorials.todo.model.Todo',
    'foam.tutorials.todo.ui.TodoCitationView',
    'foam.ui.DAOListView',
    'foam.ui.TextFieldView',
    'foam.ui.Tooltip',
    'foam.ui.md.CannedQueryCitationView',
    'foam.ui.md.CheckboxView',
    'foam.ui.md.PopupView',
  ],
  properties: [
    {
      name: 'data',
      factory: function() {
        return this.BrowserConfig.create({
          model: this.Todo,
          dao: this.EasyDAO.create({
            model: this.Todo,
            daoType: 'LOCAL',
            cache: true,
            seqNo: true
          }),
          listView: {
            factory_: 'foam.ui.DAOListView',
            rowView: 'foam.tutorials.todo.ui.TodoCitationView'
          },
          cannedQueryDAO: [
            this.CannedQuery.create({
              label: 'Todo',
              expression: EQ(this.Todo.IS_COMPLETED, false)
            }),
            this.CannedQuery.create({
              label: 'Done',
              expression: EQ(this.Todo.IS_COMPLETED, true)
            }),
            this.CannedQuery.create({
              label: 'Everything',
              expression: TRUE
            }),
          ]
        });
      }
    }
  ]
});
<|endoftext|>