All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.index.js Maven / Gradle / Ivy

module.exports = function popper({ 
  tests = 'browserify test.js'
, farm = 'browserstack'
, notunnel = false
, runner = 'mocha'
, browsers = []
, globals = ''
, port = 1945
, watch = '.'
, opts = {}
, timeout
, ripple
} = {}){
   
  // defaults
  const wait = debounce(timeout = timeout || +env.POPPER_TIMEOUT || 20000)(quit)
      , maxRetries = 3

  ripple = (ripple || rijs)(extend({ dir, port })(opts))
  resdir(ripple, dir)
  browsers = browsers
    .map(canonical(farm))
    .filter(Boolean)

  // define data resources
  ripple('results', {}, { from })
  ripple('totals' , {})

  // watch files
  if (!isCI && watch) {
    log('watching', watch)

    chokidar.watch(watch, {
      ignored: [/^\.(.*)[^\/\\]/, /[\/\\]\./, /node_modules(.+)popper/]
    , ignoreInitial: true
    , usePolling: false
    , depth: 5
    })
    .on('change', debounce(generate))
  }

  // icons
  ripple(require('browser-icons'))

  // limit dashboard resources
  ripple.to = limit(ripple.to)
  
  // proxy errors and register agent details
  ripple.server.on('connected', connected)

  // serve assets
  ripple.server.express
    .use(compression())
    .use('/utilise.min.js', send(local('utilise', 'utilise.min.js')))
    .use('/utilise.js'    , send(local('utilise', 'utilise.js')))
    .use('/mocha.css'     , send(local('mocha', 'mocha.css')))
    .use('/mocha.js'      , send(local('mocha', 'mocha.js')))
    .use('/chai.js'       , send(local('chai', 'chai.js')))
    .use('/dashboard/:id' , send(local(`./client/${runner}/logs.html`)))
    .use('/dashboard'     , send(local('./client/dashboard.html')))
    .use('/'              , serve(local('./client')))
    .use('/'              , index())

  return generate()
       , spawn()
       , ripple

  function index(){
    const head = is.arr(globals) ? globals.join('\n') : globals
        , html = file(local(`./client/${runner}/index.html`))
            .replace('', head || '')

    return (req, res) => res.send(html)
  }

  function generate() {
    log('generating tests')

    const bundle = write(local('./client/tests.js'))
        , stream = is.fn(tests) 
            ? tests()
            : run('sh', ['-c', tests], { stdio: 'pipe' })

    if (stream.stderr) 
      stream.stderr.pipe(process.stderr)

    ;((stream.stdout || stream)
      .on('end', debounce(500)(reload))
      .pipe(bundle)
      .flow || noop)()
  }

  function from(req){
    return req.data.type == 'RERUN'  ? reload(req.data.value)
         : req.data.type == 'SAVE'   ? save(req.socket.platform, req.data.value)
                                     : false
  }

  function save(platform, result) {
    const { uid } = platform
        , results = ripple('results')
        , retries = uid in results ? results[uid].retries : 0

    log('received result from', uid)
    result.platform = platform
    result.retries = retries
    update(uid, result)(ripple('results'))
    totals()
    ci(result)
  }

  function ci(r) {
    if (!isCI || r.stats.running) return

    const browser = browsers
      .filter(d => {
        if (d._name       && d._name       !== r.platform.name)        return false
        if (d._version    && d._version    !== r.platform.version)     return false
        if (d._os         && d._os         !== r.platform.os.name)     return false
        if (d._os_version && d._os_version !== r.platform.os.version)  return false
        return true
      })
      .pop()

    if (!browser) return log('result not in matrix'.red, r.platform.uid)

    browser.passed_by = r.platform.uid
    browser.passed    = !r.stats.failures
    browser.passed
      ? log('browser passed:', r.platform.uid.green.bold)
      : err('browser failed:', r.platform.uid.red.bold)

    if (!browser.passed && r.retries < maxRetries) 
      return log('retrying'.yellow, r.platform.uid, ++r.retries, '/', str(maxRetries).grey) 
           , reload(r.platform.uid)

    if (farms[farm].status)
      farms[farm].status(browser, r.platform)

    const target   = browsers.length
        , passed   = browsers.filter(by('passed')).length
        , finished = browsers.filter(by('passed_by')).length

    log('ci targets', str(passed).green.bold, '/', str(target).grey)
    
      target === passed   ? time(3000, d => process.exit(0))
    : target === finished ? time(3000, d => (!env.POPPER_TIMEOUT && process.exit(1)))
                          : wait()
  }

  function connected(socket){
    socket.platform = parse(socket)
    socket.type = socket.handshake.url == '/dashboard' ? 'dashboard' : 'agent'
    log('connected', socket.platform.uid.green, socket.type.grey)

    socket.on('global err', (message, url, linenumber) => err('Global error: ', socket.platform.uid.bold, message, url, linenumber))

    if (debug) 
      socket.on('console', function(){ log(socket.platform.uid.bold, 'says:', '', arguments[0], to.arr(arguments[1]).map(str).join(' ')) })
  }

  function quit(){
    log('no updates received for', timeout/1000, 'seconds. timing out..')
    process.exit(1)
  }

  function reload(uid) { 
    const uids = uid ? [uid] : ripple.server.ws.sockets.map(d => d.platform.uid)
    
    uids
      .map(uid => update(`${uid}.stats.running`, true)(ripple('results')))

    const agents = ripple.server.ws.sockets
      .filter(not(by('handshake.url', '/dashboard')))
      .filter(by('platform.uid', is.in(uids)))
      .map(emitReload)
      .length 

    log('reloading', str(agents).cyan, 'agents', uid || '')
  }

  function totals() {
    const res = values(ripple('results'))
    return ripple('totals', { 
      tests: str(res.map(key('stats.tests')).filter(Boolean).pop() || '?')
    , browsers: str(res.length)
    , passing: str(res.map(key('stats.failures')).filter(is(0)).length || '0')
    })
  }

  function spawn(){
    ripple.server.once('listening').then(() => {
      log('running on port', ripple.server.http.address().port)
      !notunnel && require('ngrok').connect(ripple.server.http.address().port, (e, url) => {
        log('tunnelling', url && url.magenta)
        return e ? err('error setting up reverse tunnel', e.stack)
                 : browsers.map(boot(farm)(url))
      })
    })
  }

}

const { values, key, str, not, by, grep, lo, is, debounce, extend, falsy, send, file, noop, update, identity, time, includes } = require('utilise/pure')
    , write = require('fs').createWriteStream
    , run = require('child_process').spawn
    , { stringify } = require('cryonic')
    , { resolve } = require('path')
    , compression = require('compression')
    , browserify = require('browserify')
    , platform = require('platform')
    , chokidar = require('chokidar')
    , express = require('express')
    , resdir = require('rijs.resdir')
    , serve = require('serve-static')
    , farms = require('./farms')
    , wd = require('wd')
    , rijs = opts => require('rijs.npm')(require('rijs')(opts))

const log = require('utilise/log')('[popper]')
    , err = require('utilise/err')('[popper]')
    , old = grep(console, 'log', /^(?!.*\[ri\/)/)
    , env = process.env
    , dir = __dirname
    , isCI  = env.CI === 'true'
    , debug  = lo(env.NODE_ENV) == 'debug'

const heartbeat = vm => setInterval(d => vm.eval('', e => { if (e) console.error(e) }), 30000)

const canonical = farm => browser => is.str(browser) 
  ? farms[farm].browsers[browser]
  : browser

const local = (module, file) => {
  const base = !file ? __dirname : require.resolve(module) 
      , read = !file ? module : '../'+file
  return resolve(base, read)
}

const emitReload = socket => socket.send(stringify({ data: { exec: () => location.reload() }}))

const parse = socket => {
  const ua = socket.handshake.headers['user-agent']
      , p = platform.parse(ua)
      , o = { 
          name: lo(p.name)
        , version: major(p.version)
        , os: { 
            name: lo(p.os.family.split(' ').shift())
          , version: major(p.os.version, p.os.family)
          }
        }

  if (o.os.name == 'os') o.os.name = 'osx'
  if (o.name == 'chrome mobile') o.name = 'chrome'
  if (o.name == 'microsoft edge') o.name = 'ie'

  const uid = o.name
      + '-' + o.version
      + '-' + o.os.name
      + '-' + o.os.version

  o.uid = uid

  return o
}

const major = (v, f) => 
    v                     ? v.split('.').shift() 
  : includes('xp')(lo(f)) ? 'xp'
                          : '?'

const limit = next => (req, socket) => {
  return socket.handshake.url == '/dashboard' ? next(req, socket) : false
}

const boot = farm => url => opts => {
  const { _name = '?', _version = '?', _os = '?' } = opts
      , { connect, parse = identity } = farms[farm]
      , id = `${_name.cyan} ${_version.cyan} on ${_os}`
      , vm = opts.vm = connect(wd)

  if (!vm) err('failed to connect to ' + farm), process.exit(1)

  log(`booting up ${id}`)
  
  vm.init(parse(opts), e => {
    if (e) return err(e, id)
    log('initialised', id)
    vm.get(url, e => {
      if (e) return err(e, id)
      log('opened to test page', id.cyan)
      heartbeat(vm)
    })
  })
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy