text:
'use strict'

const assert = require('chai').assert
const events = require('../../src/events')
const Promise = require('bluebird')
const spooks = require('spooks')

const modulePath = '../../src/eventify'

suite('eventify:', () => {
  let log

  setup(() => {
    log = {}
  })

  test('require does not throw', () => {
    assert.doesNotThrow(() => {
      require(modulePath)
    })
  })

  test('require returns function', () => {
    assert.isFunction(require(modulePath))
  })

  suite('require:', () => {
    let eventify

    setup(() => {
      eventify = require(modulePath)
    })

    test('eventify does not throw', () => {
      assert.doesNotThrow(() => {
        eventify()
      })
    })

    test('eventify returns EventEmitter', () => {
      assert.instanceOf(eventify(), require('events').EventEmitter)
    })

    test('EventEmitter is decorated with pause method', () => {
      assert.isFunction(eventify().pause)
      assert.lengthOf(eventify().pause, 0)
    })

    test('pause method returns continue function', () => {
      assert.isFunction(eventify().pause())
      assert.lengthOf(eventify().pause(), 0)
    })

    suite('undefined:', () => {
      setup(done => {
        const emitter = eventify(undefined)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('end event was dispatched correctly', () => {
        assert.lengthOf(log.args.end[0], 1)
        assert.isUndefined(log.args.end[0][0])
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('NaN:', () => {
      setup(done => {
        const emitter = eventify(NaN)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('end event was dispatched correctly', () => {
        assert.lengthOf(log.args.end[0], 1)
        assert.isUndefined(log.args.end[0][0])
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('Infinity:', () => {
      setup(done => {
        const emitter = eventify(Infinity)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('end event was dispatched correctly', () => {
        assert.lengthOf(log.args.end[0], 1)
        assert.isUndefined(log.args.end[0][0])
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('Number.NEGATIVE_INFINITY:', () => {
      setup(done => {
        const emitter = eventify(Number.NEGATIVE_INFINITY)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('end event was dispatched correctly', () => {
        assert.lengthOf(log.args.end[0], 1)
        assert.isUndefined(log.args.end[0][0])
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('function:', () => {
      setup(done => {
        const emitter = eventify(() => {})

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('symbol:', () => {
      setup(done => {
        const emitter = eventify(Symbol('foo'))

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('empty array:', () => {
      setup(done => {
        const emitter = eventify([])

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred once', () => {
        assert.strictEqual(log.counts.array, 1)
      })

      test('array event was dispatched correctly', () => {
        assert.lengthOf(log.args.array[0], 1)
        assert.isUndefined(log.args.array[0][0])
      })

      test('endArray event occurred once', () => {
        assert.strictEqual(log.counts.endArray, 1)
      })

      test('endArray event was dispatched correctly', () => {
        assert.lengthOf(log.args.endArray[0], 1)
        assert.isUndefined(log.args.endArray[0][0])
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('empty object:', () => {
      setup(done => {
        const emitter = eventify({})

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred once', () => {
        assert.strictEqual(log.counts.object, 1)
      })

      test('object event was dispatched correctly', () => {
        assert.lengthOf(log.args.object[0], 1)
        assert.isUndefined(log.args.object[0][0])
      })

      test('endObject event occurred once', () => {
        assert.strictEqual(log.counts.endObject, 1)
      })

      test('endObject event was dispatched correctly', () => {
        assert.lengthOf(log.args.endObject[0], 1)
        assert.isUndefined(log.args.endObject[0][0])
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('string:', () => {
      setup(done => {
        const emitter = eventify('foo')

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('string with special characters:', () => {
      setup(done => {
        const emitter = eventify('foo\nbar\t"baz"')

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], 'foo\\nbar\\t\\"baz\\"')
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('number:', () => {
      setup(done => {
        const emitter = eventify(42)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('number event occurred once', () => {
        assert.strictEqual(log.counts.number, 1)
      })

      test('number event was dispatched correctly', () => {
        assert.lengthOf(log.args.number[0], 1)
        assert.strictEqual(log.args.number[0][0], 42)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('false:', () => {
      setup(done => {
        const emitter = eventify(false)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('literal event occurred once', () => {
        assert.strictEqual(log.counts.literal, 1)
      })

      test('literal event was dispatched correctly', () => {
        assert.lengthOf(log.args.literal[0], 1)
        assert.isFalse(log.args.literal[0][0])
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('true:', () => {
      setup(done => {
        const emitter = eventify(true)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('literal event occurred once', () => {
        assert.strictEqual(log.counts.literal, 1)
      })

      test('literal event was dispatched correctly', () => {
        assert.isTrue(log.args.literal[0][0])
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('null:', () => {
      setup(done => {
        const emitter = eventify(null)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('literal event occurred once', () => {
        assert.strictEqual(log.counts.literal, 1)
      })

      test('literal event was dispatched correctly', () => {
        assert.isNull(log.args.literal[0][0])
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('array with items:', () => {
      setup(done => {
        const emitter = eventify([
          undefined,
          NaN,
          Number.POSITIVE_INFINITY,
          Number.NEGATIVE_INFINITY,
          'foo',
          () => {},
          'bar',
          Symbol('baz')
        ])

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred once', () => {
        assert.strictEqual(log.counts.array, 1)
      })

      test('literal event occurred six times', () => {
        assert.strictEqual(log.counts.literal, 6)
      })

      test('literal event was dispatched correctly first time', () => {
        assert.isNull(log.args.literal[0][0])
      })

      test('literal event was dispatched correctly second time', () => {
        assert.isNull(log.args.literal[1][0])
      })

      test('literal event was dispatched correctly third time', () => {
        assert.isNull(log.args.literal[2][0])
      })

      test('literal event was dispatched correctly fourth time', () => {
        assert.isNull(log.args.literal[3][0])
      })

      test('literal event was dispatched correctly fifth time', () => {
        assert.isNull(log.args.literal[4][0])
      })

      test('literal event was dispatched correctly sixth time', () => {
        assert.isNull(log.args.literal[5][0])
      })

      test('string event occurred twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('string event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('string event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.string[1][0], 'bar')
      })

      test('endArray event occurred once', () => {
        assert.strictEqual(log.counts.endArray, 1)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('object with properties:', () => {
      setup(done => {
        const emitter = eventify({ foo: 42,
          bar: undefined,
          baz: 3.14159265359,
          qux: Symbol('qux')
        })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred once', () => {
        assert.strictEqual(log.counts.object, 1)
      })

      test('property event occurred twice', () => {
        assert.strictEqual(log.counts.property, 2)
      })

      test('property event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.property[0][0], 'foo')
      })

      test('property event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.property[1][0], 'baz')
      })

      test('number event occurred twice', () => {
        assert.strictEqual(log.counts.number, 2)
      })

      test('number event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.number[0][0], 42)
      })

      test('number event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.number[1][0], 3.14159265359)
      })

      test('endObject event occurred once', () => {
        assert.strictEqual(log.counts.endObject, 1)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('nested array:', () => {
      setup(done => {
        const emitter = eventify([ 'foo', [ 'bar', [ 'baz', 'qux' ] ] ])

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred three times', () => {
        assert.strictEqual(log.counts.array, 3)
      })

      test('string event occurred four times', () => {
        assert.strictEqual(log.counts.string, 4)
      })

      test('string event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('string event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.string[1][0], 'bar')
      })

      test('string event was dispatched correctly third time', () => {
        assert.strictEqual(log.args.string[2][0], 'baz')
      })

      test('string event was dispatched correctly fourth time', () => {
        assert.strictEqual(log.args.string[3][0], 'qux')
      })

      test('endArray event occurred three times', () => {
        assert.strictEqual(log.counts.endArray, 3)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('nested object:', () => {
      setup(done => {
        const emitter = eventify({ foo: { bar: { baz: 1, qux: 2 }, wibble: 3 }, wobble: 4 })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred three times', () => {
        assert.strictEqual(log.counts.object, 3)
      })

      test('property event occurred six times', () => {
        assert.strictEqual(log.counts.property, 6)
      })

      test('property event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.property[0][0], 'foo')
      })

      test('property event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.property[1][0], 'bar')
      })

      test('property event was dispatched correctly third time', () => {
        assert.strictEqual(log.args.property[2][0], 'baz')
      })

      test('property event was dispatched correctly fourth time', () => {
        assert.strictEqual(log.args.property[3][0], 'qux')
      })

      test('property event was dispatched correctly fifth time', () => {
        assert.strictEqual(log.args.property[4][0], 'wibble')
      })

      test('property event was dispatched correctly sixth time', () => {
        assert.strictEqual(log.args.property[5][0], 'wobble')
      })

      test('number event occurred four times', () => {
        assert.strictEqual(log.counts.number, 4)
      })

      test('number event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.number[0][0], 1)
      })

      test('number event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.number[1][0], 2)
      })

      test('number event was dispatched correctly third time', () => {
        assert.strictEqual(log.args.number[2][0], 3)
      })

      test('number event was dispatched correctly fourth time', () => {
        assert.strictEqual(log.args.number[3][0], 4)
      })

      test('endObject event occurred three times', () => {
        assert.strictEqual(log.counts.endObject, 3)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('promise:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Promise(res => resolve = res), { poll: 4 })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)

        setTimeout(resolve.bind(null, 'foo'), 20)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('ignore promise:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Promise(res => resolve = res), { poll: 4, promises: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)

        setTimeout(resolve.bind(null, 'foo'), 20)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('buffer:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Buffer('foo bar baz qux'))

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], 'foo bar baz qux')
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('ignore buffer:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Buffer('foo bar baz qux'), { buffers: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('date:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Date('1977-06-10T10:30:00.000Z'))

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], '1977-06-10T10:30:00.000Z')
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('object with toJSON method:', () => {
      setup(done => {
        let resolve

        const emitter = eventify({ toJSON () { return 'foo' } })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.lengthOf(log.args.string[0], 1)
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('map:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Map([['foo','bar'],['baz','qux']]))

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred once', () => {
        assert.strictEqual(log.counts.object, 1)
      })

      test('property event occurred twice', () => {
        assert.strictEqual(log.counts.property, 2)
      })

      test('property event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.property[0][0], 'foo')
      })

      test('property event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.property[1][0], 'baz')
      })

      test('string event occurred twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('string event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.string[0][0], 'bar')
      })

      test('string event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.string[1][0], 'qux')
      })

      test('endObject event occurred once', () => {
        assert.strictEqual(log.counts.endObject, 1)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('ignore map:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Map([['foo','bar'],['baz','qux']]), { maps: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('set:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Set(['foo','bar']))

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred once', () => {
        assert.strictEqual(log.counts.array, 1)
      })

      test('string event occurred twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('string event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('string event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.string[1][0], 'bar')
      })

      test('endArray event occurred once', () => {
        assert.strictEqual(log.counts.endArray, 1)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('ignore set:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Set(['foo','bar']), { iterables: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('object event did not occur', () => {
        assert.strictEqual(log.counts.object, 0)
      })

      test('property event did not occur', () => {
        assert.strictEqual(log.counts.property, 0)
      })

      test('string event did not occur', () => {
        assert.strictEqual(log.counts.string, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('endObject event did not occur', () => {
        assert.strictEqual(log.counts.endObject, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('promise resolved to a map:', () => {
      setup(done => {
        let resolve

        const emitter = eventify(new Promise(res => resolve = res), { poll: 4 })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)

        setImmediate(resolve.bind(null, new Map([['foo','bar'],['baz','qux']])))
      })

      test('object event occurred once', () => {
        assert.strictEqual(log.counts.object, 1)
      })

      test('property event occurred twice', () => {
        assert.strictEqual(log.counts.property, 2)
      })

      test('property event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.property[0][0], 'foo')
      })

      test('property event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.property[1][0], 'baz')
      })

      test('string event occurred twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('string event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.string[0][0], 'bar')
      })

      test('string event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.string[1][0], 'qux')
      })

      test('endObject event occurred once', () => {
        assert.strictEqual(log.counts.endObject, 1)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event did not occur', () => {
        assert.strictEqual(log.counts.array, 0)
      })

      test('number event did not occur', () => {
        assert.strictEqual(log.counts.number, 0)
      })

      test('literal event did not occur', () => {
        assert.strictEqual(log.counts.literal, 0)
      })

      test('endArray event did not occur', () => {
        assert.strictEqual(log.counts.endArray, 0)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })

      test('endPrefix event did not occur', () => {
        assert.strictEqual(log.counts.endPrefix, 0)
      })
    })

    suite('array circular reference:', () => {
      setup(done => {
        const array = [ 'foo' ]
        array[1] = array
        const emitter = eventify(array)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred twice', () => {
        assert.strictEqual(log.counts.array, 2)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('string event was dispatched correctly', () => {
        assert.strictEqual(log.args.string[0][0], 'foo')
      })

      test('endArray event occurred twice', () => {
        assert.strictEqual(log.counts.endArray, 2)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('dataError event occurred once', () => {
        assert.strictEqual(log.counts.dataError, 1)
      })

      test('dataError event was dispatched correctly', () => {
        assert.lengthOf(log.args.dataError[0], 1)
        assert.instanceOf(log.args.dataError[0][0], Error)
        assert.strictEqual(log.args.dataError[0][0].message, 'Circular reference.')
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })
    })

    suite('object circular reference:', () => {
      setup(done => {
        const object = { foo: 'bar' }
        object.self = object
        const emitter = eventify(object)

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred twice', () => {
        assert.strictEqual(log.counts.object, 2)
      })

      test('property event occurred twice', () => {
        assert.strictEqual(log.counts.property, 2)
      })

      test('property event was dispatched correctly first time', () => {
        assert.strictEqual(log.args.property[0][0], 'foo')
      })

      test('property event was dispatched correctly second time', () => {
        assert.strictEqual(log.args.property[1][0], 'self')
      })

      test('endObject event occurred twice', () => {
        assert.strictEqual(log.counts.endObject, 2)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('dataError event occurred once', () => {
        assert.strictEqual(log.counts.dataError, 1)
      })

      test('dataError event was dispatched correctly', () => {
        assert.strictEqual(log.args.dataError[0][0].message, 'Circular reference.')
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })
    })

    suite('array circular reference with ignore set:', () => {
      setup(done => {
        const array = [ 'foo' ]
        array[1] = array
        const emitter = eventify(array, { circular: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred twice', () => {
        assert.strictEqual(log.counts.array, 2)
      })

      test('string event occurred once', () => {
        assert.strictEqual(log.counts.string, 1)
      })

      test('endArray event occurred twice', () => {
        assert.strictEqual(log.counts.endArray, 2)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('object circular reference with ignore set:', () => {
      setup(done => {
        const object = { foo: 'bar' }
        object.self = object
        const emitter = eventify(object, { circular: 'ignore' })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred twice', () => {
        assert.strictEqual(log.counts.object, 2)
      })

      test('property event occurred twice', () => {
        assert.strictEqual(log.counts.property, 2)
      })

      test('endObject event occurred twice', () => {
        assert.strictEqual(log.counts.endObject, 2)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('parallel array reference:', () => {
      setup(done => {
        const array = [ 'foo' ]
        const emitter = eventify([ array, array ])

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('array event occurred three times', () => {
        assert.strictEqual(log.counts.array, 3)
      })

      test('string event occurred twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('endArray event occurred three times', () => {
        assert.strictEqual(log.counts.endArray, 3)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('parallel object reference:', () => {
      setup(done => {
        const object = { foo: 'bar' }
        const emitter = eventify({ baz: object, qux: object })

        Object.keys(events).forEach(key => {
          emitter.on(events[key], spooks.fn({
            name: key,
            log: log
          }))
        })

        emitter.on(events.end, done)
      })

      test('object event occurred three times', () => {
        assert.strictEqual(log.counts.object, 3)
      })

      test('property event occurred four times', () => {
        assert.strictEqual(log.counts.property, 4)
      })

      test('endObject event occurred three times', () => {
        assert.strictEqual(log.counts.endObject, 3)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('error event did not occur', () => {
        assert.strictEqual(log.counts.error, 0)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })

    suite('throw errors from event handlers:', () => {
      setup(done => {
        const emitter = eventify([null,false,true,0,"",{"foo":"bar"}])

        Object.keys(events).forEach(key => {
          const event = events[key]
          emitter.on(event, spooks.fn({
            name: key,
            log: log
          }))
          if (event !== events.end) {
            emitter.on(event, () => { throw 0 })
          }
        })

        emitter.on(events.end, done)
      })

      test('end event occurred once', () => {
        assert.strictEqual(log.counts.end, 1)
      })

      test('array event occured once', () => {
        assert.strictEqual(log.counts.array, 1)
      })

      test('literal event occured three times', () => {
        assert.strictEqual(log.counts.literal, 3)
      })

      test('number event occured once', () => {
        assert.strictEqual(log.counts.number, 1)
      })

      test('string event occured twice', () => {
        assert.strictEqual(log.counts.string, 2)
      })

      test('object event occured once', () => {
        assert.strictEqual(log.counts.object, 1)
      })

      test('property event occured once', () => {
        assert.strictEqual(log.counts.property, 1)
      })

      test('endObject event occured once', () => {
        assert.strictEqual(log.counts.endObject, 1)
      })

      test('endArray event occured once', () => {
        assert.strictEqual(log.counts.endArray, 1)
      })

      test('error event occured eleven times', () => {
        assert.strictEqual(log.counts.error, 11)
      })

      test('dataError event did not occur', () => {
        assert.strictEqual(log.counts.dataError, 0)
      })
    })
  })
})


text:
/* global CustomEvent WebSocket File FileList Blob */
import {debounce} from '../utils'
import Path from 'function-tree/lib/Path'
const PLACEHOLDER_INITIAL_MODEL = 'PLACEHOLDER_INITIAL_MODEL'
const PLACEHOLDER_DEBUGGING_DATA = '$$DEBUGGING_DATA$$'
const VERSION = 'v1'

/*
  Connects to the Cerebral debugger
  - Triggers events with information from function tree execution
  - Stores data related to time travel, if activated
*/
class Devtools {
  constructor (options = {
    storeMutations: true,
    preventExternalMutations: true,
    verifyStrictRender: true,
    preventInputPropReplacement: false,
    bigComponentsWarning: {
      state: 5,
      signals: 5
    },
    remoteDebugger: null,
    multipleApps: true,
    allowedTypes: []
  }) {
    this.VERSION = VERSION
    this.debuggerComponentsMap = {}
    this.debuggerComponentDetailsId = 1
    this.storeMutations = typeof options.storeMutations === 'undefined' ? true : options.storeMutations
    this.preventExternalMutations = typeof options.preventExternalMutations === 'undefined' ? true : options.preventExternalMutations
    this.verifyStrictRender = typeof options.verifyStrictRender === 'undefined' ? true : options.verifyStrictRender
    this.preventInputPropReplacement = options.preventInputPropReplacement || false
    this.bigComponentsWarning = options.bigComponentsWarning || {state: 5, signals: 5}
    this.remoteDebugger = options.remoteDebugger || null
    this.multipleApps = typeof options.multipleApps === 'undefined' ? true : options.multipleApps
    this.backlog = []
    this.mutations = []
    this.latestExecutionId = null
    this.initialModelString = null
    this.isConnected = false
    this.controller = null
    this.originalRunTreeFunction = null
    this.ws = null
    this.isResettingDebugger = false
    this.isBrowserEnv = typeof document !== 'undefined' && typeof window !== 'undefined'
    this.allowedTypes = []
      .concat(this.isBrowserEnv ? [File, FileList, Blob] : [])
      .concat(options.allowedTypes || [])

    this.sendInitial = this.sendInitial.bind(this)
    this.sendComponentsMap = debounce(this.sendComponentsMap, 50)
  }
  /*
    To remember state Cerebral stores the initial model as stringified
    object. Since the model is mutable this is necessary. The debugger
    passes the execution id of the signal that was double clicked. This
    execution id is searched backwards in the array of mutations done.
    This is necessary as multiple mutations can be done on the same execution.
    Then all mutations are replayed to the model and all the components
    will be rerendered using the "flush" event and "force" flag.

    It will also replace the "runTree" method of the controller to
    prevent any new signals firing off when in "remember state"
  */
  remember (executionId) {
    this.controller.model.state = JSON.parse(this.initialModelString)
    let lastMutationIndex

    if (executionId === this.latestExecutionId) {
      this.controller.runTree = this.originalRunTreeFunction
      lastMutationIndex = this.mutations.length - 1
    } else {
      for (lastMutationIndex = this.mutations.length - 1; lastMutationIndex >= 0; lastMutationIndex--) {
        if (this.mutations[lastMutationIndex].executionId === executionId) {
          break
        }
      }

      this.controller.runTree = (name) => {
        console.warn(`The signal "${name}" fired while debugger is remembering state, it was ignored`)
      }
    }

    for (let x = 0; x <= lastMutationIndex; x++) {
      const mutation = JSON.parse(this.mutations[x].data)

      this.controller.model[mutation.method](...mutation.args)
    }

    this.controller.flush(true)
  }
  /*

  */
  reset () {
    this.controller.model.state = JSON.parse(this.initialModelString)
    this.backlog = []
    this.mutations = []
    this.controller.flush(true)
  }
  /*
    Sets up the listeners to Chrome Extension or remote debugger
  */
  addListeners () {
    if (this.remoteDebugger) {
      this.ws = new WebSocket(`ws://${this.remoteDebugger}`)
      this.ws.onmessage = (event) => {
        const message = JSON.parse(event.data)
        switch (message.type) {
          case 'changeModel':
            this.controller.model.set(message.data.path, message.data.value)
            this.controller.flush()
            break
          case 'remember':
            if (!this.storeMutations) {
              console.warn('Cerebral Devtools - You tried to time travel, but you have turned of storing of mutations')
            }
            this.remember(message.data)
            break
          case 'reset':
            this.reset()
            break
          case 'pong':
            this.sendInitial()
            break
          case 'ping':
            this.sendInitial()
            break
        }
      }
    } else {
      window.addEventListener('cerebral2.debugger.changeModel', (event) => {
        this.controller.model.set(event.detail.path, event.detail.value)
        this.controller.flush()
      })
      window.addEventListener('cerebral2.debugger.remember', (event) => {
        if (!this.storeMutations) {
          console.warn('Cerebral Devtools - You tried to time travel, but you have turned of storing of mutations')
        }
        this.remember(event.detail)
      })
      window.addEventListener('cerebral2.debugger.reset', (event) => {
        this.reset()
      })
      // When debugger responds a client ping
      window.addEventListener('cerebral2.debugger.pong', this.sendInitial)
      // When debugger pings the client
      window.addEventListener('cerebral2.debugger.ping', this.sendInitial)
    }
  }
  /*
    The debugger might be ready or it might not. The initial communication
    with the debugger requires a "ping" -> "pong" to identify that it
    is ready to receive messages.
    1. Debugger is open when app loads
      - Devtools sends "ping"
      - Debugger sends "pong"
      - Devtools sends "init"
    2. Debugger is opened after app load
      - Debugger sends "ping"
      - Devtools sends "init"
  */
  init (controller) {
    this.controller = controller
    this.originalRunTreeFunction = controller.runTree

    if (this.storeMutations) {
      this.initialModelString = JSON.stringify(controller.model.get())
    }

    this.addListeners()

    if (this.remoteDebugger) {
      this.ws.onopen = () => {
        this.ws.send(JSON.stringify({type: 'ping'}))
      }
      this.ws.onclose = () => {
        if (this.isBrowserEnv) {
          console.warn('You have configured remoteDebugger, but could not connect. Falling back to Chrome extension')
          this.reInit()
        } else {
          console.warn('You have configured remoteDebugger, but could not connect. Please start the remote debugger and make sure you connect to correct port')
        }
      }
      this.ws.onerror = () => {
        if (this.isBrowserEnv) {
          this.reInit()
        }
      }
    } else if (this.isBrowserEnv) {
      const event = new CustomEvent('cerebral2.client.message', {
        detail: JSON.stringify({type: 'ping'})
      })
      window.dispatchEvent(event)
    } else {
      console.warn('The Cerebral devtools is running in a non browser environment. You have to add the remoteDebugger option')
    }

    if (this.isBrowserEnv && this.multipleApps) {
      let hidden, visibilityChange
      if (typeof document.hidden !== 'undefined') {
        hidden = 'hidden'
        visibilityChange = 'visibilitychange'
      } else if (typeof document.msHidden !== 'undefined') {
        hidden = 'msHidden'
        visibilityChange = 'msvisibilitychange'
      } else if (typeof document.webkitHidden !== 'undefined') {
        hidden = 'webkitHidden'
        visibilityChange = 'webkitvisibilitychange'
      }

      document.addEventListener(visibilityChange, () => {
        if (!document[hidden]) {
          this.isResettingDebugger = true
          this.backlog.forEach((message) => {
            this.sendMessage(message)
          })
          this.isResettingDebugger = false
        }
      }, false)
    }

    this.watchExecution()
  }
  /*
    When websocket close, reinit with chrome extension
  */
  reInit () {
    this.remoteDebugger = null
    this.addListeners()

    const event = new CustomEvent('cerebral2.client.message', {
      detail: JSON.stringify({type: 'ping'})
    })
    window.dispatchEvent(event)
  }
  /*
    Sends message to chrome extension or remote debugger
  */
  sendMessage (stringifiedMessage) {
    if (this.multipleApps && !this.isResettingDebugger) {
      this.backlog.push(stringifiedMessage)
    }

    if (this.remoteDebugger) {
      this.ws.send(stringifiedMessage)
    } else {
      const event = new CustomEvent('cerebral2.client.message', {detail: stringifiedMessage})
      window.dispatchEvent(event)
    }
  }
  /*
    Watches function tree for execution of signals. This is passed to
    debugger to prevent time travelling when executing. It also tracks
    latest executed signal for "remember" to know when signals can be
    called again
  */
  watchExecution () {
    this.controller.runTree.on('start', (execution) => {
      const message = JSON.stringify({
        type: 'executionStart',
        data: {
          execution: {
            executionId: execution.id,
            name: execution.name,
            staticTree: execution.staticTree,
            datetime: execution.datetime
          }
        }
      })

      if (this.isConnected) {
        this.sendMessage(message)
      } else {
        this.backlog.push(message)
      }
    })
    this.controller.runTree.on('end', (execution) => {
      const message = JSON.stringify({
        type: 'executionEnd',
        data: {
          execution: {
            executionId: execution.id
          }
        }
      })
      this.latestExecutionId = execution.id

      if (this.isConnected) {
        this.sendMessage(message)
      } else {
        this.backlog.push(message)
      }
    })
    this.controller.runTree.on('pathStart', (path, execution, funcDetails) => {
      const message = JSON.stringify({
        type: 'executionPathStart',
        data: {
          execution: {
            executionId: execution.id,
            functionIndex: funcDetails.functionIndex,
            path
          }
        }
      })

      if (this.isConnected) {
        this.sendMessage(message)
      } else {
        this.backlog.push(message)
      }
    })
    this.controller.runTree.on('functionStart', (execution, funcDetails, payload) => {
      const message = JSON.stringify({
        type: 'execution',
        data: {
          execution: {
            executionId: execution.id,
            functionIndex: funcDetails.functionIndex,
            payload,
            data: null
          }
        }
      })

      if (this.isConnected) {
        this.sendMessage(message)
      } else {
        this.backlog.push(message)
      }
    })
    this.controller.runTree.on('functionEnd', (execution, funcDetails, payload, result) => {
      if (!result || (result instanceof Path && !result.payload)) {
        return
      }

      const message = JSON.stringify({
        type: 'executionFunctionEnd',
        data: {
          execution: {
            executionId: execution.id,
            functionIndex: funcDetails.functionIndex,
            output: result instanceof Path ? result.payload : result
          }
        }
      })

      if (this.isConnected) {
        this.sendMessage(message)
      } else {
        this.backlog.push(message)
      }
    })
  }
  /*
    Send initial model. If model has already been stringified we reuse it. Any
    backlogged executions will also be triggered
  */
  sendInitial () {
    const initialModel = this.controller.model.get()
    const message = JSON.stringify({
      type: 'init',
      version: this.VERSION,
      data: {
        initialModel: this.initialModelString ? PLACEHOLDER_INITIAL_MODEL : initialModel
      }
    }).replace(`"${PLACEHOLDER_INITIAL_MODEL}"`, this.initialModelString)

    this.isResettingDebugger = true
    this.sendMessage(message)
    this.backlog.forEach((backlogItem) => {
      this.sendMessage(backlogItem)
    })
    this.isResettingDebugger = false

    if (!this.multipleApps) {
      this.backlog = []
    }

    this.isConnected = true

    this.sendMessage(JSON.stringify({
      type: 'components',
      data: {
        map: this.debuggerComponentsMap,
        render: {
          components: []
        }
      }
    }))
  }
  /*
    Create the stringified message for the debugger. As we need to
    store mutations with the default true "storeMutations" option used
    by time travel and jumping between Cerebral apps, we are careful
    not doing unnecessary stringifying.
  */
  createExecutionMessage (debuggingData, context, functionDetails, payload) {
    const type = 'execution'
    let mutationString = ''

    if (this.storeMutations && debuggingData && debuggingData.type === 'mutation') {
      mutationString = JSON.stringify(debuggingData)
    }

    const data = {
      execution: {
        executionId: context.execution.id,
        functionIndex: functionDetails.functionIndex,
        payload: payload,
        datetime: context.execution.datetime,
        data: mutationString ? PLACEHOLDER_DEBUGGING_DATA : debuggingData
      }
    }

    if (mutationString) {
      this.mutations.push({
        executionId: context.execution.id,
        data: mutationString
      })
    }

    return JSON.stringify({
      type: type,
      version: this.VERSION,
      data: data
    }).replace(`"${PLACEHOLDER_DEBUGGING_DATA}"`, mutationString)
  }
  /*
    Sends execution data to the debugger. Whenever a signal starts
    it will send a message to the debugger, but any functions in the
    function tree might also use this to send debugging data. Like when
    mutations are done or any wrapped methods run.
  */
  sendExecutionData (debuggingData, context, functionDetails, payload) {
    const message = this.createExecutionMessage(debuggingData, context, functionDetails, payload)

    if (this.isConnected) {
      this.sendMessage(message)
    } else {
      this.backlog.push(message)
    }
  }
  /*
    The container will listen to "flush" events from the controller
    and send an event to debugger about initial registered components
  */
  extractComponentName (component) {
    return component.constructor.displayName.replace('CerebralWrapping_', '')
  }
  /*
    Updates the map the represents what active state paths and
    components are in your app. Used by the debugger
  */
  updateComponentsMap (component, nextDeps, prevDeps) {
    const componentDetails = {
      name: this.extractComponentName(component),
      renderCount: component.renderCount ? component.renderCount + 1 : 1,
      id: component.componentDetailsId || this.debuggerComponentDetailsId++
    }
    component.componentDetailsId = componentDetails.id
    component.renderCount = componentDetails.renderCount

    if (prevDeps) {
      for (const depsKey in prevDeps) {
        const debuggerComponents = this.debuggerComponentsMap[prevDeps[depsKey]]

        for (let x = 0; x < debuggerComponents.length; x++) {
          if (debuggerComponents[x].id === component.componentDetailsId) {
            debuggerComponents.splice(x, 1)
            if (debuggerComponents.length === 0) {
              delete this.debuggerComponentsMap[prevDeps[depsKey]]
            }
            break
          }
        }
      }
    }

    if (nextDeps) {
      for (const depsKey in nextDeps) {
        this.debuggerComponentsMap[nextDeps[depsKey]] = (
          this.debuggerComponentsMap[nextDeps[depsKey]]
            ? this.debuggerComponentsMap[nextDeps[depsKey]].concat(componentDetails)
            : [componentDetails]
        )
      }
    }
  }
  /*
    Sends components map to debugger. It is debounced (check constructor).
    It needs to wait because React updates async. Instead of tracking
    callbacks we just wait 50ms as it is not that important when
    debugger updates
  */
  sendComponentsMap (componentsToRender, changes, start, end) {
    this.sendMessage(JSON.stringify({
      type: 'components',
      data: {
        map: this.debuggerComponentsMap,
        render: {
          start: start,
          duration: end - start,
          changes: changes,
          components: componentsToRender.map(this.extractComponentName)
        }
      }
    }))
  }
}

export default function (...args) {
  return new Devtools(...args)
}

redpajama_set_name: RedPajamaGithub


text:
/* Marathon Map */

/* Model */

//   Custom locations can be added independently of Google Places by adding data manually
//   to the model. The custom category "beaches" has been added as an example.

var model = [
     {
        name : 'Sombrero Beach',
        lat : 24.691533,
        long : -81.086107,
        cat : ['beaches','park'],
        imgUrl : '',
        address : 'Sombrero Beach Rd., Marathon',
        placeid : '001',
        website : 'http://www.ci.marathon.fl.us/government/parks/city-parks-and-beaches/',
        phoneNumber : '(305) 743-0033'
     },
     {
        name : 'Coco Plum Beach',
        lat : 24.730240,
        long : -81.001592,
        cat : ['beaches', 'park'],
        imgUrl : '',
        address : 'Coco Plum Dr., Marathon',
        placeid : '002',
        website : 'http://www.ci.marathon.fl.us/government/parks/city-parks-and-beaches/',
        phoneNumber : '(305) 743-0033'
     },
     {
        name : 'Jesse Hobbs Park',
        lat : 24.713604,
        long : -81.087036,
        cat : ['park'],
        imgUrl : '',
        address : '41st St. and Overseas Hwy, Marathon',
        placeid : '003',
        website : 'http://www.ci.marathon.fl.us/government/parks/city-parks-and-beaches/',
        phoneNumber : '(305) 743-0033'
     },
     {
        name : 'Building Department',
        lat : 24.711935,
        long : -81.09753899999998,
        cat : ['local_government_office'],
        imgUrl : '',
        address : '2798 Overseas Hwy., Suite 330',
        placeid : '004',
        website : 'http://www.monroecounty-fl.gov/Directory.aspx?did=6',
        phoneNumber : '(305) 289-2501'
     }
];

/* Google Map View */

// Define these variables globally for use in several functions.

var map = new google.maps.Map(document.getElementById('map-canvas'), {
        zoom: 12,
        center: mapCenter
    });

//To customize for a different city or neighborhood, change these lat/long coordinates.
var mapCenter = new google.maps.LatLng(24.723009, -81.038884);

var markers = [];
var marker;
var request;
var service = new google.maps.places.PlacesService(map);
var infowindow = new google.maps.InfoWindow();
var infoWindowIsOpen = false;

// Sometimes Google Places returns duplicate locations or locations that no longer exist.
// Their place-ids are added manually to the badId array, which will be used by the
// createMarker() function to prevent duplicates from a appearing on the map.
var badID = ['ChIJ40NtAb_e0IgRXF3FSvPYdPU','ChIJR2JaoIDZ0IgRVYFo3Et9jGY','ChIJh72I2H_Z0IgRc_2GCzQ4ojA',
 'ChIJ0x-jPCra0IgRPj4_dJeO8Jc','ChIJyX_gNmPZ0IgR8Vq2WN5q3m4', 'ChIJV2D-j07Z0IgRnFu34GH4Jfg',
 'ChIJifxZz0zZ0IgR_KJsoTes2fU', 'ChIJOW7ImUTZ0IgRwK52f_32ivU', 'ChIJQbj4sOre0IgRIa8HNNSYAbA',
  'ChIJ8ZtRVZXe0IgRc7VP0JUYXTk', 'ChIJlytLLbHe0IgRnsMbhZIyrto','ChIJl3iw0bne0IgRiepETBQaOxQ'];

// The availableTags array keeps a list of all location-names that are added to the model.
// It serves as a data-pool for the jQuery-UI Autocomplete widget.
var availableTags = [];

view = {

    // Check width of map to determine which labels to use on navigation menu.
    // Smaller mobile devices will use icons for menu tabs.

    init : function() {
        var categories = ['hospitality', 'culture', 'recreation', 'government', 'services'];
        var icons = ['☽', '☮', '☺', '✪', '$'];
        var labels = ['Hospitality ', 'Culture ', 'Recreation ', 'Government ', 'Services '];
        for (i=0; i < categories.length; i++) {
            if ($('#map-canvas').width() > 570)  {
                $( '#' + categories[i] ).replaceWith( '' + labels[i] + '⌄' +'' );
                $('#' + categories[i] ).css('text-align', 'left');
            } else {
                $( '#' + categories[i] ).replaceWith( '' + icons[i]  + '' );
                $('#' + categories[i] ).css('text-align', 'center');
                $('#' + categories[i] ).css('line-height', '30px');
            }
        }
    },

    // Send a Places Search request to Google when a category tab is clicked in navigation menu.
    getLocations : function (category) {

        var request = {
            location: map.center,
            radius: '20000',
            types: [category]
        };
        service.nearbySearch(request, callback);

        // Callback function returns Places Search results.
        // Loop through results and send the data for each place to the createMarker() function.

        function callback(results, status) {
            if (status == google.maps.places.PlacesServiceStatus.OK) {
                for (var i = 0; i < results.length; i++) {
                    var place = results[i];
                    createMarker(results[i]);
                }
            }
        }

        /* This function collects and saves the data needed to create a marker for each place.
           Later, the data will be used by the addMarkers() function to place markers on the map.
           Details of each place are saved to the model and to the locations array. */

        function createMarker(place) {
            var name = place.name;
            var lat = place.geometry.location.lat();
            var long = place.geometry.location.lng();
            var address = place.vicinity;
            var cat = request.types;
            var imgUrl = '';
            var placeid = place.place_id;

            // Check if a photo was found by the Places Search before saving as var imgUrl.
            // If no photo exists, imgUrl =''.

            if (place.hasOwnProperty('photos') ) {
                imgUrl = place.photos[0].getUrl({'maxWidth':100, 'maxHeight':100});
            }

            /* Details of each place are saved as a new Location object (see viewModel) and placed
              in the locations array. The locations array holds basically the same information as
              the markers array, but it is ko.observable and needed to make the model-view-viewModel
              system work. */

            if (jQuery.inArray(placeid, badID) === -1) {
                locations.push(new Location(name,lat,long,cat,address,imgUrl,placeid));

            /* Place details are also added to the model. This makes it possible to search
             for locations after the markers have been removed from the map. It also allows for
             custom locations to be created independently of Google's API. */

                var obj = {
                    name : name,
                    lat : lat,
                    long : long,
                    cat : cat,
                    address : address,
                    imgUrl : imgUrl,
                    placeid : placeid
                };

                model.push(obj);
                autoComplete();
            }
        }
    },

    /* Add a marker to the map for each place, using data prepared by
       the createMarker() function above. The addMarkers() function
       is triggered each time a new Location object is added to the
       locations array. */

    addMarkers : function(name,lat,long,cat,address,imgUrl,placeid) {
        marker = new google.maps.Marker({
            position: new google.maps.LatLng(lat, long),
            name: name,
            map : map,
            cat : cat,
            address : address,
            imgUrl : imgUrl,
            placeid : placeid
        });

        // Make each marker clickable. The click event triggers a series
        // functions to add content to the infowindow and open it.
        google.maps.event.addListener(marker, 'click', (function(marker) {
            return function() {
            view.sortClickedMarkers(placeid);
            };
        })(marker));

        // Each marker's data is saved as a json in the markers array.
        markers.push(marker);
    },

    /* The getPlaceDetails() function is triggered when the user clicks on a location name
       rendered in the results area. It sends an additional request to Google for more
       detailed information about the location. The phone number, website, and Google place_id
       are collected and saved from the returned results, and these data are passed on to the
       openInfoWindow() function in the viewModel.  */

    getPlaceDetails: function (placeid) {
        var phoneNumber ='';
        var website = '';

        var request = {
            placeId : placeid()
        }
        service.getDetails(request, callback);
        function callback(place, status) {
            if (status == google.maps.places.PlacesServiceStatus.OK) {
                var name = place.name;
                phoneNumber = place.formatted_phone_number;
                website = place.website;
                placeid = place.place_id;
                openInfoWindow(phoneNumber, placeid, website);
            }

        }
    },

    /* The next two functions are triggered when a user clicks on a map-marker. The function
       sortClickedMarkers() checks whether the clicked marker represents a custom location
       from the model or a Google Places location delivered by Google's API. If the marker
       belongs to a Google location, getClickedPlaceDetails() is called. If it belongs
       to a custom location, the phone number and website URL are collected from the model and
       sent straight to the openInfoWindow() function. */

    sortClickedMarkers : function (placeid) {
        // A Google place-id must be 27 characters in length.
        if (ko.utils.unwrapObservable(placeid).length > 6) {
            view.getClickedPlaceDetails(placeid);
        }
        else {
            for (var place in model) {
                if (model[place].placeid === ko.utils.unwrapObservable(placeid)) {
                    var phoneNumber = model[place].phoneNumber;
                    var website = model[place].website;
                    openInfoWindow(phoneNumber, ko.utils.unwrapObservable(placeid), website);
                }
            }
        }
    },

    /* The getClickedPlaceDetails() function sends a Place Details request to Google
       when a marker is clicked. The returned results (phone number, place-id, website) are
       used as arguments when the openInfoWindow() function is called. */

    getClickedPlaceDetails : function (placeid) {
        var phoneNumber ='';
        var website = '';

        var request = {
            placeId : placeid
        }
        service.getDetails(request, callback);
        function callback(place, status) {
            if (status == google.maps.places.PlacesServiceStatus.OK) {
                phoneNumber = place.formatted_phone_number;
                website = place.website;
                var name = place.name;
                placeid = place.place_id;
                openInfoWindow(phoneNumber, placeid, website);
            }
        }
    }

}

/* View Model */

var viewModel = function() {
    var self = this;

    // The locations array holds data collected from a Google Places search
    // (see the createMarker() function above).

    this.locations = ko.observableArray([]);

    // The query string is created when a user submits input from the search box.

    this.query = ko.observable('');

    // Create an object for each location found by Google Places search.

    this.Location = function (name, lat, long, cat, address, imgUrl,placeid) {
        this.name = ko.observable(name);
        this.lat = lat;
        this.long = long;
        this.cat = ko.observable(cat);
        this.imgUrl = ko.observable(imgUrl);
        this.placeid = ko.observable(placeid);

        // Call the addMarkers() function to add a marker to the map.
        view.addMarkers(name, lat, long, cat, address, imgUrl,placeid);
    };

    /*Remove the markers from the map and clear the results area. The emptyLocations()
      function is triggered when a new category tab in the menu is clicked, or when the
      map is reset by the user. */

    this.emptyLocations = function() {
         self.recenterMap();
         for (var i=0; i< markers.length; i++){
           markers[i].setMap(null);
         }
         markers.length = 0;
         self.locations.removeAll();
    };

    /* The getPlaces() function is called when a user clicks on a custom category tab
       (such as "beaches") in the navigation menu. It collects place details from
       the model and pushes a new Location object into the locations array, which
       results in a new marker being added to the map. */

    this.getPlaces = function(category){
        for (var place in model) {
            for(i=0; i < model[place].cat.length; i++) {
                if (model[place].cat[i] ===category) {
                    locations.push(new Location(model[place].name, model[place].lat, model[place].long, model[place].cat, model[place].address, model[place].imgUrl, model[place].placeid));
                    self.autoComplete;
                }
            }
        }
    };

    /* The sortPlaces() function is triggered when the user clicks on a location name
       rendered in the results area. It checks whether the place-id belongs to a custom
       location or to a Google Places location. In the latter case, a Place Details
       search request is sent to Google. Otherwise, the necessary details are retrieved
       directly from the model and sent to openInfoWindow().  */

    this.sortPlaces = function(placeid) {
        if (ko.utils.unwrapObservable(placeid).length > 6) {
            view.getPlaceDetails(placeid);
        }

        else {
            for (var place in model) {
                if (model[place].placeid === ko.utils.unwrapObservable(placeid)) {
                    var phoneNumber = model[place].phoneNumber;
                    var website = model[place].website;
                    openInfoWindow(phoneNumber, ko.utils.unwrapObservable(placeid), website);
                }
            }
        }
    };

    /* Recenter the map. This function is triggered when a new category tab on the menu
       is selected, or when the recenter-map button is clicked. */

    this.recenterMap = function() {
        map.setCenter(mapCenter);
        map.setOptions({ 'zoom' : 12 });
        self.closeInfoWindow();
    };

    /* The searchBox() function handles user input from the search box. It scans the
       model for a location name matching the query string. Since all locations rendered
       on the map are saved in the model, the user can search for a specific location
       even after its marker has been cleared from the map.*/

    this.searchBox = function(query) {
        var search = this.query().toLowerCase();
        for (place in model) {
            if (model[place].name.toLowerCase() === search) {
                self.emptyLocations();
                locations.push(new Location(model[place].name, model[place].lat, model[place].long, model[place].cat, model[place].address, model[place].imgUrl, model[place].placeid));
            }
        }
        self.closeInfoWindow();
        this.recenterMap();
    };

    // Clear the search box.
    this.clearSearchBox = function() {
    	$('#autocomplete').val('');
    }

    // Push location names from the model into the availableTags array,
    // which is used by the jQuery-UI Autocomplete widget
    this.autoComplete = function() {
    	for (place in model) {
    		if (jQuery.inArray(model[place].name, availableTags) === -1) {
    		    availableTags.push(model[place].name);
    		}
    	}
    };

    // Close the infoWindow.

    this.closeInfoWindow = function() {
        if (infoWindowIsOpen === true) {
            infowindow.close();
        }
        infoWindowIsOpen = false;
    };

    /* The openInfoWindow() function is the last in a series of functions triggered when
       the user clicks a marker on the map or a location name appearing in the results area.
       It takes the place details collected by preceeding functions  and turns them into
       a content string for the infoWindow.  */

    this.openInfoWindow = function(phoneNumber, placeid, website) {
        // Search the model for a matching place-id.
        for (place in model) {
            // If a match is found, collect details from the model and save as variables.
            if (model[place].placeid === placeid) {
                var currentLat = model[place].lat;
                var currentLong = model[place].long;
                var currentName = model[place].name;
                var currentAddress = model[place].address;
                var currentPlaceId = model[place].placeid;

                // Check for a mobile display. If the screen is too small, leave the image out.
                if ($('#map-canvas').height() >= 350) {
                    var currentImage = model[place].imgUrl;
                } else {
                    currentImage = '';
                }

                // Check if a phone number was returned by Google Places.
                if (phoneNumber === undefined) {
                    var currentPhone = '';
                }
                else {
                    currentPhone = phoneNumber;
                }
                // Check if a website address was returned by Google Places
                if (website === undefined) {
                    var currentWebsite = '';
                }
                else {
                    currentWebsite = website;
                }
            }
        }

        // Construct the content string according to what details are available.

        if (currentImage === '') {
            if (currentWebsite === '') {
                var content = '
' + currentName +'

' + currentAddress + '
' + currentPhone + '
'
            }
            else {
                content ='
' + currentName +'

' + currentAddress + '
' + currentPhone + '
Website
';
            }
        }
        else if (currentWebsite === '') {
            content = '
' + currentName + '

' + currentAddress +'
'+ currentPhone +'
';
        }
        else {
            content = '
' + currentName + '

' + currentAddress +'
'+ currentPhone +'
Website
';
        }

        // Set the infoWindow's position, add content, and open it.

        infowindow.setPosition({lat: currentLat, lng: currentLong});
        infowindow.setOptions({pixelOffset : new google.maps.Size(0,-35)});
        infowindow.setContent(content);
        infowindow.open(map);
        infoWindowIsOpen = true;

        // Zoom to the corresponding place on the map.
        map.setOptions({
            'zoom' : 15,
            'center' : {lat: currentLat, lng: currentLong}
        });

        // Scroll to the top of the screen to see the map.
        $('html, body').animate({ scrollTop: 0 }, 'fast');
    };

};

view.init();
ko.applyBindings(viewModel());

// Adjust menu styling automatically when screen sizes changes.
$( window ).resize(function() {
  view.init();
});

$( "#autocomplete" ).autocomplete({
	source: availableTags
});