<commit_before>const path = require('path')
const express = require('express')
const ms = require('ms')
const rootPath = require('app-root-path').path
const resolve = file => path.resolve(rootPath, file)
const request = require('request');

const cache = require('./utils/cache-instance')
const apiStatus = require('./utils/api-status')
const HTMLContent = require('../pages/Compilation')
const ssr = require('./utils/ssr-renderer')
let config = require('config')

const compileOptions = {
  escape: /{{([^{][\s\S]+?[^}])}}/g,
  interpolate: /{{{([\s\S]+?)}}}/g
}
const NOT_ALLOWED_SSR_EXTENSIONS_REGEX = new RegExp(`(.*)(${config.server.ssrDisabledFor.extensions.join('|')})$`)

const isProd = process.env.NODE_ENV === 'production'
process.noDeprecation = true

const app = express()

const templatesCache = ssr.initTemplatesCache(config, compileOptions)
let renderer

if (isProd) {
  // In production: create server renderer using server bundle and index HTML
  // template from real fs.
  // The server bundle is generated by vue-ssr-webpack-plugin.
  const clientManifest = require(resolve('dist/vue-ssr-client-manifest.json'))
  const bundle = require(resolve('dist/vue-ssr-bundle.json'))
  // src/index.template.html is processed by html-webpack-plugin to inject
  // build assets and output as dist/index.html.
  // TODO: Add dynamic templates loading from (config based?) list
  renderer = ssr.createRenderer(bundle, clientManifest)
} else {
  // In development: setup the dev server with watch and hot-reload,
  // and create a new renderer on bundle / index template update.
  require(resolve('core/build/dev-server'))(app, (bundle, template) => {
    templatesCache['default'] = ssr.compileTemplate(template, compileOptions) // Important Notice: template switching doesn't work with dev server because of the HMR
    renderer = ssr.createRenderer(bundle)
  })
}

function invalidateCache (req, res) {
  if (config.server.useOutputCache) {
    if (req.query.tag && req.query.key) { // clear cache pages for specific query tag
      if (req.query.key !== config.server.invalidateCacheKey) {
        console.error('Invalid cache invalidation key')
        apiStatus(res, 'Invalid cache invalidation key', 500)
        return
      }
      console.log(`Clear cache request for [${req.query.tag}]`)
      let tags = []
      if (req.query.tag === '*') {
        tags = config.server.availableCacheTags
      } else {
        tags = req.query.tag.split(',')
      }
      const subPromises = []
      tags.forEach(tag => {
        if (config.server.availableCacheTags.indexOf(tag) >= 0 || config.server.availableCacheTags.find(t => {
          return tag.indexOf(t) === 0
        })) {
          subPromises.push(cache.invalidate(tag).then(() => {
            console.log(`Tags invalidated successfully for [${tag}]`)
          }))
        } else {
          console.error(`Invalid tag name ${tag}`)
        }
      })
      Promise.all(subPromises).then(r => {
        apiStatus(res, `Tags invalidated successfully [${req.query.tag}]`, 200)
      }).catch(error => {
        apiStatus(res, error, 500)
        console.error(error)
      })

      if (config.server.invalidateCacheForwarding) { // forward invalidate request to the next server in the chain
        if (!req.query.forwardedFrom && config.server.invalidateCacheForwardUrl) { // don't forward forwarded requests

          request(config.server.invalidateCacheForwardUrl + req.query.tag + '&forwardedFrom=vs', {}, (err, res, body) => {
            if (err) { console.error(err); }
            try {
              if (body && JSON.parse(body).code !== 200) console.log(body);
            } catch (e) {
              console.error('Invalid Cache Invalidation response format', e)
            }
          });
        }
      }
    } else {
      apiStatus(res, 'Invalid parameters for Clear cache request', 500)
      console.error('Invalid parameters for Clear cache request')
    }
  } else {
    apiStatus(res, 'Cache invalidation is not required, output cache is disabled', 200)
  }
}

const serve = (path, cache, options) => express.static(resolve(path), Object.assign({
  fallthrough: false,
  setHeaders: cache && isProd ? function (res, path) {
    const mimeType = express.static.mime.lookup(path);
    let maxAge = config.expireHeaders.default;
    if (config.expireHeaders.hasOwnProperty(mimeType)) {
      maxAge = config.expireHeaders.get(mimeType);
    }
    res.setHeader('Cache-Control', 'public, max-age=' + ms(maxAge));
  } : null
}, options))

const themeRoot = require('../build/theme-path')

app.use('/dist', serve('dist', true))
app.use('/assets', serve(themeRoot + '/assets', true))
app.use('/service-worker.js', serve('dist/service-worker.js', false, {
  setHeaders: function (res, path, stat) {
    res.set('Content-Type', 'text/javascript; charset=UTF-8')
  }
}))

const serverExtensions = require(resolve('src/server'))
serverExtensions.registerUserServerRoutes(app)

app.post('/invalidate', invalidateCache)
app.get('/invalidate', invalidateCache)

app.get('*', (req, res, next) => {
  if (NOT_ALLOWED_SSR_EXTENSIONS_REGEX.test(req.url)) {
    apiStatus(res, 'Vue Storefront: Resource is not found', 404)
    return
  }

  const s = Date.now()
  const errorHandler = err => {
    if (err && err.code === 404) {
      if (NOT_ALLOWED_SSR_EXTENSIONS_REGEX.test(req.url)) {
        apiStatus(res, 'Vue Storefront: Resource is not found', 404)
        console.error(`Resource is not found : ${req.url}`)
        next()
      } else {
        res.redirect('/page-not-found')
        console.error(`Redirect for resource not found : ${req.url}`)
      }
    } else {
      res.redirect('/error')
      console.error(`Error during render : ${req.url}`)
      console.error(err)
      next()
    }
  }

  const dynamicRequestHandler = renderer => {
    if (!renderer) {
      res.setHeader('Content-Type', 'text/html')
      res.status(202).end(HTMLContent)
      return next()
    }
    const context = ssr.initSSRRequestContext(app, req, res, config)
    renderer.renderToString(context).then(output => {
      if (!res.get('content-type')) {
        res.setHeader('Content-Type', 'text/html')
      }
      let tagsArray = []
      if (config.server.useOutputCacheTagging && context.output.cacheTags !== null) {
        tagsArray = Array.from(context.output.cacheTags)
        const cacheTags = tagsArray.join(' ')
        res.setHeader('X-VS-Cache-Tags', cacheTags)
        console.log(`cache tags for the request: ${cacheTags}`)
      }
      output = ssr.applyAdvancedOutputProcessing(context, output, templatesCache, isProd);
      if (config.server.useOutputCache && cache) {
        cache.set(
          'page:' + req.url,
          { headers: res.getHeaders(), body: output },
          tagsArray
        ).catch(errorHandler)
      }
      res.end(output)
      console.log(`whole request [${req.url}]: ${Date.now() - s}ms`)
      next()
    }).catch(errorHandler)
  }

  const dynamicCacheHandler = () => {
    if (config.server.useOutputCache && cache) {
      cache.get(
        'page:' + req.url
      ).then(output => {
        if (output !== null) {
          if (output.headers) {
            for (const header of Object.keys(output.headers)) {
              res.setHeader(header, output.headers[header])
            }
          }
          res.setHeader('X-VS-Cache', 'Hit')
          if (output.body) {
            res.end(output.body)
          } else {
            res.setHeader('Content-Type', 'text/html')
            res.end(output)
          }
          console.log(`cache hit [${req.url}], cached request: ${Date.now() - s}ms`)
          next()
        } else {
          res.setHeader('X-VS-Cache', 'Miss')
          console.log(`cache miss [${req.url}], request: ${Date.now() - s}ms`)
          dynamicRequestHandler(renderer) // render response
        }
      }).catch(errorHandler)
    } else {
      dynamicRequestHandler(renderer)
    }
  }

  if (config.server.dynamicConfigReload) {
    const cachedConfigModule = require.cache[require.resolve('config')]
    if (cachedConfigModule) {
      delete cachedConfigModule.parent.children
      delete require.cache[require.resolve('config')]
    }
    config = require('config') // reload config
    if (typeof serverExtensions.configProvider === 'function') {
      serverExtensions.configProvider(req).then(loadedConfig => {
        config = config.util.extendDeep(config, loadedConfig)
        dynamicCacheHandler()
      }).catch(error => {
        if (config.server.dynamicConfigContinueOnError) {
          dynamicCacheHandler()
        } else {
          console.log('config provider error:', error)
          if (req.url !== '/error') {
            res.redirect('/error')
          }
          dynamicCacheHandler()
        }
      })
    } else {
      config = require('config') // reload config
      dynamicCacheHandler()
    }
  } else {
    dynamicCacheHandler()
  }
})

let port = process.env.PORT || config.server.port
const host = process.env.HOST || config.server.host
const start = () => {
  app.listen(port, host)
    .on('listening', () => {
      console.log(`\n\n----------------------------------------------------------`)
      console.log('|                                                        |')
      console.log(`| Vue Storefront Server started at http://${host}:${port} |`)
      console.log('|                                                        |')
      console.log(`----------------------------------------------------------\n\n`)
    })
    .on('error', (e) => {
      if (e.code === 'EADDRINUSE') {
        port = parseInt(port) + 1
        console.log(`The port is already in use, trying ${port}`)
        start()
      }
    })
}
start()
<commit_msg>Update server.js<commit_after>const path = require('path')
const express = require('express')
const ms = require('ms')
const rootPath = require('app-root-path').path
const resolve = file => path.resolve(rootPath, file)
const request = require('request');

const cache = require('./utils/cache-instance')
const apiStatus = require('./utils/api-status')
const HTMLContent = require('../pages/Compilation')
const ssr = require('./utils/ssr-renderer')
let config = require('config')

const compileOptions = {
  escape: /{{([^{][\s\S]+?[^}])}}/g,
  interpolate: /{{{([\s\S]+?)}}}/g
}
const NOT_ALLOWED_SSR_EXTENSIONS_REGEX = new RegExp(`(.*)(${config.server.ssrDisabledFor.extensions.join('|')})$`)

const isProd = process.env.NODE_ENV === 'production'
process.noDeprecation = true

const app = express()

const templatesCache = ssr.initTemplatesCache(config, compileOptions)
let renderer

if (isProd) {
  // In production: create server renderer using server bundle and index HTML
  // template from real fs.
  // The server bundle is generated by vue-ssr-webpack-plugin.
  const clientManifest = require(resolve('dist/vue-ssr-client-manifest.json'))
  const bundle = require(resolve('dist/vue-ssr-bundle.json'))
  // src/index.template.html is processed by html-webpack-plugin to inject
  // build assets and output as dist/index.html.
  // TODO: Add dynamic templates loading from (config based?) list
  renderer = ssr.createRenderer(bundle, clientManifest)
} else {
  // In development: setup the dev server with watch and hot-reload,
  // and create a new renderer on bundle / index template update.
  require(resolve('core/build/dev-server'))(app, (bundle, template) => {
    templatesCache['default'] = ssr.compileTemplate(template, compileOptions) // Important Notice: template switching doesn't work with dev server because of the HMR
    renderer = ssr.createRenderer(bundle)
  })
}

function invalidateCache (req, res) {
  if (config.server.useOutputCache) {
    if (req.query.tag && req.query.key) { // clear cache pages for specific query tag
      if (req.query.key !== config.server.invalidateCacheKey) {
        console.error('Invalid cache invalidation key')
        apiStatus(res, 'Invalid cache invalidation key', 500)
        return
      }
      console.log(`Clear cache request for [${req.query.tag}]`)
      let tags = []
      if (req.query.tag === '*') {
        tags = config.server.availableCacheTags
      } else {
        tags = req.query.tag.split(',')
      }
      const subPromises = []
      tags.forEach(tag => {
        if (config.server.availableCacheTags.indexOf(tag) >= 0 || config.server.availableCacheTags.find(t => {
          return tag.indexOf(t) === 0
        })) {
          subPromises.push(cache.invalidate(tag).then(() => {
            console.log(`Tags invalidated successfully for [${tag}]`)
          }))
        } else {
          console.error(`Invalid tag name ${tag}`)
        }
      })
      Promise.all(subPromises).then(r => {
        apiStatus(res, `Tags invalidated successfully [${req.query.tag}]`, 200)
      }).catch(error => {
        apiStatus(res, error, 500)
        console.error(error)
      })

      if (config.server.invalidateCacheForwarding) { // forward invalidate request to the next server in the chain
        if (!req.query.forwardedFrom && config.server.invalidateCacheForwardUrl) { // don't forward forwarded requests
          request(config.server.invalidateCacheForwardUrl + req.query.tag + '&forwardedFrom=vs', {}, (err, res, body) => {
            if (err) { console.error(err); }
            try {
              if (body && JSON.parse(body).code !== 200) console.log(body);
            } catch (e) {
              console.error('Invalid Cache Invalidation response format', e)
            }
          });
        }
      }
    } else {
      apiStatus(res, 'Invalid parameters for Clear cache request', 500)
      console.error('Invalid parameters for Clear cache request')
    }
  } else {
    apiStatus(res, 'Cache invalidation is not required, output cache is disabled', 200)
  }
}

const serve = (path, cache, options) => express.static(resolve(path), Object.assign({
  fallthrough: false,
  setHeaders: cache && isProd ? function (res, path) {
    const mimeType = express.static.mime.lookup(path);
    let maxAge = config.expireHeaders.default;
    if (config.expireHeaders.hasOwnProperty(mimeType)) {
      maxAge = config.expireHeaders.get(mimeType);
    }
    res.setHeader('Cache-Control', 'public, max-age=' + ms(maxAge));
  } : null
}, options))

const themeRoot = require('../build/theme-path')

app.use('/dist', serve('dist', true))
app.use('/assets', serve(themeRoot + '/assets', true))
app.use('/service-worker.js', serve('dist/service-worker.js', false, {
  setHeaders: function (res, path, stat) {
    res.set('Content-Type', 'text/javascript; charset=UTF-8')
  }
}))

const serverExtensions = require(resolve('src/server'))
serverExtensions.registerUserServerRoutes(app)

app.post('/invalidate', invalidateCache)
app.get('/invalidate', invalidateCache)

app.get('*', (req, res, next) => {
  if (NOT_ALLOWED_SSR_EXTENSIONS_REGEX.test(req.url)) {
    apiStatus(res, 'Vue Storefront: Resource is not found', 404)
    return
  }

  const s = Date.now()
  const errorHandler = err => {
    if (err && err.code === 404) {
      if (NOT_ALLOWED_SSR_EXTENSIONS_REGEX.test(req.url)) {
        apiStatus(res, 'Vue Storefront: Resource is not found', 404)
        console.error(`Resource is not found : ${req.url}`)
        next()
      } else {
        res.redirect('/page-not-found')
        console.error(`Redirect for resource not found : ${req.url}`)
      }
    } else {
      res.redirect('/error')
      console.error(`Error during render : ${req.url}`)
      console.error(err)
      next()
    }
  }

  const dynamicRequestHandler = renderer => {
    if (!renderer) {
      res.setHeader('Content-Type', 'text/html')
      res.status(202).end(HTMLContent)
      return next()
    }
    const context = ssr.initSSRRequestContext(app, req, res, config)
    renderer.renderToString(context).then(output => {
      if (!res.get('content-type')) {
        res.setHeader('Content-Type', 'text/html')
      }
      let tagsArray = []
      if (config.server.useOutputCacheTagging && context.output.cacheTags !== null) {
        tagsArray = Array.from(context.output.cacheTags)
        const cacheTags = tagsArray.join(' ')
        res.setHeader('X-VS-Cache-Tags', cacheTags)
        console.log(`cache tags for the request: ${cacheTags}`)
      }
      output = ssr.applyAdvancedOutputProcessing(context, output, templatesCache, isProd);
      if (config.server.useOutputCache && cache) {
        cache.set(
          'page:' + req.url,
          { headers: res.getHeaders(), body: output },
          tagsArray
        ).catch(errorHandler)
      }
      res.end(output)
      console.log(`whole request [${req.url}]: ${Date.now() - s}ms`)
      next()
    }).catch(errorHandler)
  }

  const dynamicCacheHandler = () => {
    if (config.server.useOutputCache && cache) {
      cache.get(
        'page:' + req.url
      ).then(output => {
        if (output !== null) {
          if (output.headers) {
            for (const header of Object.keys(output.headers)) {
              res.setHeader(header, output.headers[header])
            }
          }
          res.setHeader('X-VS-Cache', 'Hit')
          if (output.body) {
            res.end(output.body)
          } else {
            res.setHeader('Content-Type', 'text/html')
            res.end(output)
          }
          console.log(`cache hit [${req.url}], cached request: ${Date.now() - s}ms`)
          next()
        } else {
          res.setHeader('X-VS-Cache', 'Miss')
          console.log(`cache miss [${req.url}], request: ${Date.now() - s}ms`)
          dynamicRequestHandler(renderer) // render response
        }
      }).catch(errorHandler)
    } else {
      dynamicRequestHandler(renderer)
    }
  }

  if (config.server.dynamicConfigReload) {
    const cachedConfigModule = require.cache[require.resolve('config')]
    if (cachedConfigModule) {
      delete cachedConfigModule.parent.children
      delete require.cache[require.resolve('config')]
    }
    config = require('config') // reload config
    if (typeof serverExtensions.configProvider === 'function') {
      serverExtensions.configProvider(req).then(loadedConfig => {
        config = config.util.extendDeep(config, loadedConfig)
        dynamicCacheHandler()
      }).catch(error => {
        if (config.server.dynamicConfigContinueOnError) {
          dynamicCacheHandler()
        } else {
          console.log('config provider error:', error)
          if (req.url !== '/error') {
            res.redirect('/error')
          }
          dynamicCacheHandler()
        }
      })
    } else {
      config = require('config') // reload config
      dynamicCacheHandler()
    }
  } else {
    dynamicCacheHandler()
  }
})

let port = process.env.PORT || config.server.port
const host = process.env.HOST || config.server.host
const start = () => {
  app.listen(port, host)
    .on('listening', () => {
      console.log(`\n\n----------------------------------------------------------`)
      console.log('|                                                        |')
      console.log(`| Vue Storefront Server started at http://${host}:${port} |`)
      console.log('|                                                        |')
      console.log(`----------------------------------------------------------\n\n`)
    })
    .on('error', (e) => {
      if (e.code === 'EADDRINUSE') {
        port = parseInt(port) + 1
        console.log(`The port is already in use, trying ${port}`)
        start()
      }
    })
}
start()
<|endoftext|>


<commit_before>define(["dojo/_base/declare", "./PersistentObject", "dojo/date"],
    function(declare, PersistentObject, dojoDate) {

      function internalReload(/*AuditableObject*/ self, /*Object*/ json) {
        if (json && json.createdAt && json.createdBy && json.lastModifiedAt && json.lastModifiedBy /* TODO json.... undefined, but not null */) {
          // all data or nothing;
          // TODO add precondition for this
          if ((self.createdAt || self.createdBy) &&
              ((self.createdAt && (dojoDate.compare(json.createdAt, self.createdAt) !== 0)) ||
               (self.createdBy && json.createdBy != self.createdBy))) {
            throw "ERROR cannot change from existing created information"; // TODO precondition
          }
          var jsonLastModifiedAt = new Date(json.lastModifiedAt);
          if (self.lastModifiedAt && jsonLastModifiedAt.getTime() - self.lastModifiedAt.getTime() < 1000) {
            // We use < 1000 (1s) instead of < 0, because the server stores dates only to the second.
            // With the current server implementation, we see that if we retrieve quickly after an
            // update or create, the retrieve lastModified at is later than the one in the response
            // of the action. The reason is, that the data in the action response comes from RAM,
            // and has values up to the nanosecond or so (23/6/2012 15:45:32.424242242), while the
            // data in the retrieve comes from the DB, which only stores up to the second
            // (23/6/2012 15:45:32). So, it seems that retrieve-date is earlier than the action
            // response date, which is impossible. By giving our comparison a 1 second leeway,
            // this is resolved.
            // IDEA solve in the server
            throw "ERROR cannot become an earlier modified version"; // TODO precondition
          }
          if (self.lastModifiedBy && !json.lastModifiedBy) {
            throw "ERROR cannot change to lastModifiedBy == null"; // TODO precondition
          }
          // this will happen with the JSON response from a creation or update, and during construction
          if (! self.createdBy) {
            //noinspection JSUnresolvedFunction
            self._changeAttrValue("createdAt", new Date(json.createdAt));
            //noinspection JSUnresolvedFunction
            self._changeAttrValue("createdBy", json.createdBy);
          }
          //noinspection JSUnresolvedFunction
          self._changeAttrValue("lastModifiedAt", jsonLastModifiedAt);
          //noinspection JSUnresolvedFunction
          self._changeAttrValue("lastModifiedBy", json.lastModifiedBy);
        }
      }

      var AuditableObject = declare([PersistentObject], {
        // created.. can change from null to value, but then no more
        // lastModified.. can change all the time, but ..At can only become bigger

        _c_invar: [
          function() {return this.hasOwnProperty("createdAt");},
          function() {return this.hasOwnProperty("createdBy");},
          function() {return this.hasOwnProperty("lastModifiedAt");},
          function() {return this.hasOwnProperty("lastModifiedBy");}
          // TODO should be strings and times
        ],

        constructor: function(/*Object*/ props) {
          /* we don't care about the format of the data here; we just keep it, and return it to the server
           like we got it. */

          this.createdAt = null;
          this.createdBy = null;
          this.lastModifiedAt = null;
          this.lastModifiedBy = null;
          internalReload(this, props);
        },

        reload: function(/*Object*/ json) {
          // created.. can change from null to an actual date and username number after create,
          // lastModified.. to, but then again with each update
          internalReload(this, json);
        },

        // getters are implicit; create when needed (for documentation)
        // TODO documentation

        _createdAtSetter: function() {
          // createdAt is read-only
          throw "ERROR";
        },

        _createdBySetter: function() {
          // createdBy is read-only
          throw "ERROR";
        },

        _lastModifiedAtSetter: function() {
          // lastModifiedAt is read-only
          throw "ERROR";
        },

        _lastModifiedBySetter: function() {
          // lastModifiedBy is read-only
          throw "ERROR";
        },

        _extendJsonObject: function(/*Object*/ json) {
          // it makes no senses whatsoever to send this data back to the back-end

          // HOWEVER due to an idiosyncrasy in our current server, it is much nicer
          //         if we do send back the created-attributes
          // In any case, it should be in nobody's way.
          json.createdBy = this.createdBy;
          json.createdAt = this.createdAt;
          // IDEA resolve this issue in the server
        },

        _stateToString: function(/*Array of String*/ toStrings) {
          toStrings.push("createdAt: " + this.createdAt);
          toStrings.push("createdBy: " + this.createdBy);
          toStrings.push("lastModifiedAt: " + this.lastModifiedAt);
          toStrings.push("lastModifiedBy: " + this.lastModifiedBy);
        }
      });

      return AuditableObject;
    }
);
<commit_msg>found a big bug in ppwcode persistence code in server temporary workaround to try on server<commit_after>define(["dojo/_base/declare", "./PersistentObject", "dojo/date"],
    function(declare, PersistentObject, dojoDate) {

      function internalReload(/*AuditableObject*/ self, /*Object*/ json) {
        if (json && json.createdAt && json.createdBy && json.lastModifiedAt && json.lastModifiedBy /* TODO json.... undefined, but not null */) {
          // all data or nothing;
          // TODO add precondition for this
          if ((self.createdAt || self.createdBy) &&
              ((self.createdAt && (dojoDate.compare(json.createdAt, self.createdAt) !== 0)) ||
               (self.createdBy && json.createdBy != self.createdBy))) {
            throw "ERROR cannot change from existing created information"; // TODO precondition
          }
          var jsonLastModifiedAt = new Date(json.lastModifiedAt);
          if (self.lastModifiedAt && jsonLastModifiedAt.getTime() - self.lastModifiedAt.getTime() < 3601000) { // MUDO !!!! see below
            // We use < 1000 (1s) instead of < 0, because the server stores dates only to the second.
            // With the current server implementation, we see that if we retrieve quickly after an
            // update or create, the retrieve lastModified at is later than the one in the response
            // of the action. The reason is, that the data in the action response comes from RAM,
            // and has values up to the nanosecond or so (23/6/2012 15:45:32.424242242), while the
            // data in the retrieve comes from the DB, which only stores up to the second
            // (23/6/2012 15:45:32). So, it seems that retrieve-date is earlier than the action
            // response date, which is impossible. By giving our comparison a 1 second leeway,
            // this is resolved.
            // IDEA solve in the server
            // MUDO There is a much worse problem: a RAM-created server Date is in the local time of the server
            //      While the time in the DB has no timezone. A RAM-created server Date that is sent over JSON
            //      contains timezone information. A date that is retrieved from the DB does not contain
            //      timezone in RAM, and thus does also not contain timezone information when it is sent over
            //      JSON.
            //      When JavaScript parses a string with timezone information, it takes it into account.
            //      When JavaScript parses a string without timezone information, it assumes the local
            //      timezone.
            //      When server and client are in a different timezone, when the JSON contains timezone information,
            //      the string is interpreted in the timezone of the server. When the JSON does not contain
            //      timezone information, the string is interpreted in the timezone of the client.
            //      For webapplications, this in unacceptable.
            // MUDO for now we work around this by allowing not a 1000ms offset, but a 3601000 offset
            // MUDO but it is obviously wrong
            throw "ERROR cannot become an earlier modified version"; // TODO precondition
          }
          if (self.lastModifiedBy && !json.lastModifiedBy) {
            throw "ERROR cannot change to lastModifiedBy == null"; // TODO precondition
          }
          // this will happen with the JSON response from a creation or update, and during construction
          if (! self.createdBy) {
            //noinspection JSUnresolvedFunction
            self._changeAttrValue("createdAt", new Date(json.createdAt));
            //noinspection JSUnresolvedFunction
            self._changeAttrValue("createdBy", json.createdBy);
          }
          //noinspection JSUnresolvedFunction
          self._changeAttrValue("lastModifiedAt", jsonLastModifiedAt);
          //noinspection JSUnresolvedFunction
          self._changeAttrValue("lastModifiedBy", json.lastModifiedBy);
        }
      }

      var AuditableObject = declare([PersistentObject], {
        // created.. can change from null to value, but then no more
        // lastModified.. can change all the time, but ..At can only become bigger

        _c_invar: [
          function() {return this.hasOwnProperty("createdAt");},
          function() {return this.hasOwnProperty("createdBy");},
          function() {return this.hasOwnProperty("lastModifiedAt");},
          function() {return this.hasOwnProperty("lastModifiedBy");}
          // TODO should be strings and times
        ],

        constructor: function(/*Object*/ props) {
          /* we don't care about the format of the data here; we just keep it, and return it to the server
           like we got it. */

          this.createdAt = null;
          this.createdBy = null;
          this.lastModifiedAt = null;
          this.lastModifiedBy = null;
          internalReload(this, props);
        },

        reload: function(/*Object*/ json) {
          // created.. can change from null to an actual date and username number after create,
          // lastModified.. to, but then again with each update
          internalReload(this, json);
        },

        // getters are implicit; create when needed (for documentation)
        // TODO documentation

        _createdAtSetter: function() {
          // createdAt is read-only
          throw "ERROR";
        },

        _createdBySetter: function() {
          // createdBy is read-only
          throw "ERROR";
        },

        _lastModifiedAtSetter: function() {
          // lastModifiedAt is read-only
          throw "ERROR";
        },

        _lastModifiedBySetter: function() {
          // lastModifiedBy is read-only
          throw "ERROR";
        },

        _extendJsonObject: function(/*Object*/ json) {
          // it makes no senses whatsoever to send this data back to the back-end

          // HOWEVER due to an idiosyncrasy in our current server, it is much nicer
          //         if we do send back the created-attributes
          // In any case, it should be in nobody's way.
          json.createdBy = this.createdBy;
          json.createdAt = this.createdAt;
          // IDEA resolve this issue in the server
        },

        _stateToString: function(/*Array of String*/ toStrings) {
          toStrings.push("createdAt: " + this.createdAt);
          toStrings.push("createdBy: " + this.createdBy);
          toStrings.push("lastModifiedAt: " + this.lastModifiedAt);
          toStrings.push("lastModifiedBy: " + this.lastModifiedBy);
        }
      });

      return AuditableObject;
    }
);
<|endoftext|>


<commit_before>import chalk from 'chalk';
import relativeId from '../../src/utils/relativeId.js';

if ( !process.stderr.isTTY ) chalk.enabled = false;
const warnSymbol = process.stderr.isTTY ? `⚠   ` : `Warning: `;
const errorSymbol = process.stderr.isTTY ? `🚨   ` : `Error: `;

// log to stderr to keep `rollup main.js > bundle.js` from breaking
export const stderr = console.error.bind( console ); // eslint-disable-line no-console

function log ( object, symbol ) {
	const message = object.plugin ? `(${object.plugin} plugin) ${object.message}` : object.message;

	stderr( `${symbol}${chalk.bold( message )}` );

	if ( object.url ) {
		stderr( chalk.cyan( object.url ) );
	}

	if ( object.loc ) {
		stderr( `${relativeId( object.loc.file )} (${object.loc.line}:${object.loc.column})` );
	} else if ( object.id ) {
		stderr( relativeId( object.id ) );
	}

	if ( object.frame ) {
		stderr( chalk.dim( object.frame ) );
	}

	stderr( '' );
}

export function handleWarning ( warning ) {
	log( warning, warnSymbol );
}

export function handleError ( err, recover ) {
	log( err, errorSymbol );
	if ( !recover ) process.exit( 1 );
}
<commit_msg>Allowing logging message to be type string<commit_after>import chalk from 'chalk';
import relativeId from '../../src/utils/relativeId.js';

if ( !process.stderr.isTTY ) chalk.enabled = false;
const warnSymbol = process.stderr.isTTY ? `⚠   ` : `Warning: `;
const errorSymbol = process.stderr.isTTY ? `🚨   ` : `Error: `;

// log to stderr to keep `rollup main.js > bundle.js` from breaking
export const stderr = console.error.bind( console ); // eslint-disable-line no-console

function log ( object, symbol ) {
	const message = (object.plugin ? `(${object.plugin} plugin) ${object.message}` : object.message) || object;

	stderr( `${symbol}${chalk.bold( message )}` );

	if ( object.url ) {
		stderr( chalk.cyan( object.url ) );
	}

	if ( object.loc ) {
		stderr( `${relativeId( object.loc.file )} (${object.loc.line}:${object.loc.column})` );
	} else if ( object.id ) {
		stderr( relativeId( object.id ) );
	}

	if ( object.frame ) {
		stderr( chalk.dim( object.frame ) );
	}

	stderr( '' );
}

export function handleWarning ( warning ) {
	log( warning, warnSymbol );
}

export function handleError ( err, recover ) {
	log( err, errorSymbol );
	if ( !recover ) process.exit( 1 );
}
<|endoftext|>


<commit_before>#!/usr/bin/env node
// Copyright 2014 Technical Machine, Inc. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

var common = require('../src/cli')
  , colors = require('colors')
  , util = require('util')
  ;

// Setup cli.
common.basic();

// Command-line arguments
var argv = require("nomnom")
  .script('tessel wifi')
  .option('list', {
    abbr: 'l',
    flag: true,
    help: '[Tessel] List available wifi networks.',
  })
  .option('network', {
    abbr: 'n',
    help: '[Tessel] The network to connect to.'
  })
  .option('password', {
    abbr: 'p',
    help: '[Tessel] Password of the network. Omit for unsecured networks.'
  })
  .option('security', {
    abbr: 's',
    default: 'wpa2',
    help: '[Tessel] Security type of the network, one of (wpa2|wpa|wep). Omit for unsecured networks.'
  })
  .option('timeout', {
    abbr: 't',
    default: 20,
    help: '[Tessel] Sets timeout before retrying connection to network.'
  })
  .option('help', {
    abbr: 'h',
    help: '[Tessel] Show usage for tessel wifi'
  })
  .parse();

function usage () {
  console.error(require('nomnom').getUsage());
  process.exit(1);
}

common.controller(false, function (err, client) {
  if (argv.list) {
    client.listen(true, null); // TODO: should use [20, 21, 22, 86] once firmware logs at the right level
    client.wifiStatus(function (err, data) {
      Object.keys(data).map(function (key) {
        if (key.toUpperCase() == "IP"){
          // reverse key.data
          data[key] = data[key].split(".").reverse().join(".");
        }
        console.log(key.replace(/^./, function (a) { return a.toUpperCase(); }) + ':', data[key]);
      })
      client.close();
    })

  } else {
    if (!argv.network){
      usage();
    }

    function retry () {
      var ssid = argv.network;
      var pass = argv.password || "";
      var security = (argv.security || (pass ? 'wpa2' : 'unsecure')).toLowerCase();

      client.configureWifi(ssid, pass, security, {
        timeout: argv.timeout
      }, function (data) {
        if (data.event == 'error') {
          console.error('Error in connecting (%d). Please try again.', data.error);
          process.on('exit', function () {
            process.exit(1);
          })
          client.close();
        } else if (!data.connected) {
          console.error('Could not connect. Check that your network and password are correct.');
          client.close();
        } else {
          console.error('Connected!\n');

          console.log('IP\t', data.ip);
          console.log('DNS\t', data.dns);
          console.log('DHCP\t', data.dhcp);
          console.log('Gateway\t', data.gateway);
          client.close();
        }
      });
    }

    // Flush earlier USB network messages.
    // TODO: Actually flush these.
    setTimeout(retry, 2000);
  }
})
<commit_msg>Wording was misleading.<commit_after>#!/usr/bin/env node
// Copyright 2014 Technical Machine, Inc. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

var common = require('../src/cli')
  , colors = require('colors')
  , util = require('util')
  ;

// Setup cli.
common.basic();

// Command-line arguments
var argv = require("nomnom")
  .script('tessel wifi')
  .option('list', {
    abbr: 'l',
    flag: true,
    help: '[Tessel] List available wifi networks.',
  })
  .option('network', {
    abbr: 'n',
    help: '[Tessel] The network to connect to.'
  })
  .option('password', {
    abbr: 'p',
    help: '[Tessel] Password of the network. Omit for unsecured networks.'
  })
  .option('security', {
    abbr: 's',
    default: 'wpa2',
    help: '[Tessel] Security type of the network, one of (wpa2|wpa|wep). Omit for unsecured networks.'
  })
  .option('timeout', {
    abbr: 't',
    default: 20,
    help: '[Tessel] Sets timeout before retrying connection to network.'
  })
  .option('help', {
    abbr: 'h',
    help: '[Tessel] Show usage for tessel wifi'
  })
  .parse();

function usage () {
  console.error(require('nomnom').getUsage());
  process.exit(1);
}

common.controller(false, function (err, client) {
  if (argv.list) {
    client.listen(true, null); // TODO: should use [20, 21, 22, 86] once firmware logs at the right level
    client.wifiStatus(function (err, data) {
      Object.keys(data).map(function (key) {
        if (key.toUpperCase() == "IP"){
          // reverse key.data
          data[key] = data[key].split(".").reverse().join(".");
        }
        console.log(key.replace(/^./, function (a) { return a.toUpperCase(); }) + ':', data[key]);
      })
      client.close();
    })

  } else {
    if (!argv.network){
      usage();
    }

    function retry () {
      var ssid = argv.network;
      var pass = argv.password || "";
      var security = (argv.security || (pass ? 'wpa2' : 'unsecure')).toLowerCase();

      client.configureWifi(ssid, pass, security, {
        timeout: argv.timeout
      }, function (data) {
        if (data.event == 'error') {
          console.error('Error in connecting (%d). Please try again.', data.error);
          process.on('exit', function () {
            process.exit(1);
          })
          client.close();
        } else if (!data.connected) {
          console.error('Connection failed. Check that your ssid and password are correct. Try moving closer to your router for a quicker connection.');
          client.close();
        } else {
          console.error('Connected!\n');

          console.log('IP\t', data.ip);
          console.log('DNS\t', data.dns);
          console.log('DHCP\t', data.dhcp);
          console.log('Gateway\t', data.gateway);
          client.close();
        }
      });
    }

    // Flush earlier USB network messages.
    // TODO: Actually flush these.
    setTimeout(retry, 2000);
  }
})
<|endoftext|>


<commit_before>var fs = require ('fs');
var exec    = require('child_process').exec;

var ServeMe = require("serve-me");

var dir = './www';
var cmd = '';
var cmdPrepare = '';
var host = '';
var confFile = 'config.xml';

if (process.argv[2] === 'ios') {
	dir        = './platforms/ios/www';
	// cmd = 'ios-sim launch ./platforms/ios/build/emulator/HelloCordova.app --log ./console.log --devicetypeid iPhone-6 --exit';
	cmd        = 'cordova emulate ios';
	cmdPrepare = 'cordova prepare ios';
	host       = '127.0.0.1';
} else if (process.argv[2] === 'android') {
	dir        = './platforms/android/assets/www';
	cmd        = 'cordova emulate android';
	cmdPrepare = 'cordova prepare android';
	host       = '10.0.2.2';
} else if (process.argv[2] === 'windows') {
	dir        = './platforms/windows/www';
	cmd        = 'cordova emulate windows';
	cmdPrepare = 'cordova prepare windows';
	host       = '127.0.0.1';
	confFile   = 'www/windows.html'
}

var serveMe = ServeMe ({
	// debug: true,
	// log: true,
	directory: dir,
	secure: false
});

fs.readFile (confFile, function (err, buf) {
	if (err) process.exit (1);
	serveMe.start("0", function() {

		var port = serveMe.server.server.address().port;

		var configXml = buf.toString('utf-8').replace (/<content src="[^"]+/m, '<content src="http://' + host + ':' + port);

		// console.log (configXml);

		fs.writeFile (confFile, configXml, function () {
			if (err) process.exit (1);

			console.log ('Changes applied to the', confFile);

			exec(cmdPrepare, function callback(error, stdout, stderr){
				if (error) process.exit (1);

				console.log ('Prepare completed');
				exec(cmd, function callback(error, stdout, stderr){
					if (error) process.exit (1);

					console.log ('Emulator running');
					setTimeout (function () {process.exit(1)}, 30000);
				});
			});
		});
	});
})

serveMe.on("http_request", function(data){
	// console.log("http request", data);
});

serveMe.get ("/test/success", function(){
	console.log ("test success");
	process.exit ();
});

serveMe.get ("/test/fail", function (req, res){
	console.log ("test failures: ", req.url);
	process.exit (1);
});
<commit_msg>show command std* on error<commit_after>var fs = require ('fs');
var exec    = require('child_process').exec;

var ServeMe = require("serve-me");

var dir = './www';
var cmd = '';
var cmdPrepare = '';
var host = '';
var confFile = 'config.xml';

if (process.argv[2] === 'ios') {
	dir        = './platforms/ios/www';
	// cmd = 'ios-sim launch ./platforms/ios/build/emulator/HelloCordova.app --log ./console.log --devicetypeid iPhone-6 --exit';
	cmd        = 'cordova emulate ios';
	cmdPrepare = 'cordova prepare ios';
	host       = '127.0.0.1';
} else if (process.argv[2] === 'android') {
	dir        = './platforms/android/assets/www';
	cmd        = 'cordova emulate android';
	cmdPrepare = 'cordova prepare android';
	host       = '10.0.2.2';
} else if (process.argv[2] === 'windows') {
	dir        = './platforms/windows/www';
	cmd        = 'cordova emulate windows';
	cmdPrepare = 'cordova prepare windows';
	host       = '127.0.0.1';
	confFile   = 'www/windows.html'
}

var serveMe = ServeMe ({
	// debug: true,
	// log: true,
	directory: dir,
	secure: false
});

function handleError (err, stdout, stderr) {
	if (err) {
		if (stdout) console.log (stdout);
		if (stderr) console.error (stderr);
		process.exit (1);
	}
}

fs.readFile (confFile, function (err, buf) {
	handleError (err, null, err);

	serveMe.start("0", function() {

		var port = serveMe.server.server.address().port;

		var configXml = buf.toString('utf-8').replace (/<content src="[^"]+/m, '<content src="http://' + host + ':' + port);

		// console.log (configXml);

		fs.writeFile (confFile, configXml, function (err) {
			handleError (err, null, err);

			console.log ('Changes applied to the', confFile);

			exec(cmdPrepare, function callback(error, stdout, stderr){
				handleError (error, stdout, stderr);

				console.log ('Prepare completed');
				exec(cmd, function callback(error, stdout, stderr){
					handleError (error, stdout, stderr);

					console.log ('Emulator running');
					setTimeout (function () {process.exit(1)}, 30000);
				});
			});
		});
	});
})

serveMe.on("http_request", function(data){
	// console.log("http request", data);
});

serveMe.get ("/test/success", function(){
	console.log ("test success");
	process.exit ();
});

serveMe.get ("/test/fail", function (req, res){
	console.log ("test failures: ", req.url);
	process.exit (1);
});
<|endoftext|>


<commit_before>// @TODO: Wait to node.js achieve harmony module.

const BashChildProcess = require('./BashChildProcess.js');

class HooksProcess {
  constructor(pluginNames = []) {
    this.bashChildProcessArray = [];
    this.[Symbol.for('traversingPluginNames')](pluginNames);
  },

  static [Symbol.for('getPathByPluginName')](pluginName = '') {
    // @TODO: Get plugin main script path by pluginName.
  },

  [Symbol.for('traversingPluginNames')](pluginNames = []) {
    let rejectPromises = pluginNames.map((pluginName, idx, arr) => {
      let scriptPath = this.[Symbol.for('getPathByPluginName')](pluginName);
      let isStdout = idx ? true : false;
      let bashChildProcess = new BashChildProcess({ isStdout, scriptPath });

      this.bashChildProcessArray = BashChildProcess;
      return BashChildProcess.getErrorPromise();
    });
    this.[Symbol.for('bindExitCallback')](rejectPromises);
  },

  [Symbol.for('bindCloseCallback')](closePromises= []) {
    closePromise.forEach((promise, idx, arr) {
      promise.then((flag) => {
        if (flag) this.[Symbol.for('switchChildProcessOutput')](idx);
      });
    });
  },

  [Symbol.for('switchChildProcessOutput')](currentIdx = 0) {
    let prevChildProcess = this.bashChildProcessArray[currentIdx - 1];
    let outputIterator = nextChildProcess.getOutputIterator();

    if (prevChildProcess === void 0 || prevChildProcess.isClose()) {
      this.[Symbol.for('traversingNextUnCloseChildProcess')](currentIdx + 1);
    }
  },

  [Symbol.for('traversingNextUnCloseChildProcess')](nextIdx = 1) {
    const bashChildProcessArray = this.bashChildProcessArray;

    const setProcessAsStdout = this.[Symbol.for('setProcessAsStdout')];

    while (let nextChildProcess = bashChildProcessArray[nextIdx]) {
      if (!nextChildProcess.isClose()) {
        break;
      }
      else {
        nextIdx = nextIdx + 1;
        setProcessAsStdout(nextChildProcess);
      }
    }
  },

  [Symbol.for('setProcessAsStdout')](nextChildProcess) {
    // ouput caching results
    for (let output of nextChildProcess.getOutputIterator()) {
      console.log(ouput);
    }

    // switching output child process
    nextChildProcess.setStdout(true);
  },

  [Symbol.for('bindExitCallback')](rejectPromises = []) {
    const getAnyRejectPromise = this.[Symbol.for('getAnyRejectPromise')];

    anyRejectPromise = getAnyRejectPromise(rejectPromises);
    anyRejectPromise.reject((error) => {
      console.log('hook process abnormal exit.');
      process.exit(1);
    });
  },

  [Symbol.for('getAnyRejectPromise')](promises = []) {
    let anyRejectPromise = new Promise((fulfillOuter, rejectOuter) => {
      for (let promise of promises) {
        promise.reject((error) => {
          rejectOuter(error);
        });
      }
    });
    return anyRejectPromise;
  }
}

export HooksProcess;
<commit_msg>bugfix<commit_after>// @TODO: Wait to node.js achieve harmony module.

const BashChildProcess = require('./BashChildProcess.js');

class HooksProcess {
  constructor(pluginNames = []) {
    this.bashChildProcessArray = [];
    this.[Symbol.for('traversingPluginNames')](pluginNames);
  },

  static [Symbol.for('getPathByPluginName')](pluginName = '') {
    // @TODO: Get plugin main script path by pluginName.
  },

  [Symbol.for('traversingPluginNames')](pluginNames = []) {
    let rejectPromises = pluginNames.map((pluginName, idx, arr) => {
      let scriptPath = this.[Symbol.for('getPathByPluginName')](pluginName);
      let isStdout = idx ? true : false;
      let bashChildProcess = new BashChildProcess({ isStdout, scriptPath });

      this.bashChildProcessArray = BashChildProcess;
      return BashChildProcess.getErrorPromise();
    });
    this.[Symbol.for('bindExitCallback')](rejectPromises);
  },

  [Symbol.for('bindCloseCallback')](closePromises= []) {
    closePromise.forEach((promise, idx, arr) {
      promise.then((flag) => {
        if (flag) this.[Symbol.for('switchChildProcessOutput')](idx);
      });
    });
  },

  [Symbol.for('switchChildProcessOutput')](currentIdx = 0) {
    let prevChildProcess = this.bashChildProcessArray[currentIdx - 1];
    let outputIterator = nextChildProcess.getOutputIterator();

    if (prevChildProcess === void 0 || prevChildProcess.isClose()) {
      this.[Symbol.for('traversingNextUnCloseChildProcess')](currentIdx + 1);
    }
  },

  [Symbol.for('traversingNextUnCloseChildProcess')](nextIdx = 1) {
    const bashChildProcessArray = this.bashChildProcessArray;
    const setProcessAsStdout = this.[Symbol.for('setProcessAsStdout')];

    while (let nextChildProcess = bashChildProcessArray[nextIdx]) {
      if (!nextChildProcess.isClose()) {
        break;
      }
      else {
        nextIdx = nextIdx + 1;
        setProcessAsStdout(nextChildProcess);
      }
    }
  },

  [Symbol.for('setProcessAsStdout')](nextChildProcess) {
    // ouput caching results
    for (let output of nextChildProcess.getOutputIterator()) {
      console.log(ouput);
    }
    // switching output child process
    nextChildProcess.setStdout(true);
  },

  [Symbol.for('bindExitCallback')](rejectPromises = []) {
    const getAnyRejectPromise = this.[Symbol.for('getAnyRejectPromise')];

    anyRejectPromise = getAnyRejectPromise(rejectPromises);
    anyRejectPromise.reject((error) => {
      console.log('hook process abnormal exit.');
      process.exit(1);
    });
  },

  [Symbol.for('getAnyRejectPromise')](promises = []) {
    let anyRejectPromise = new Promise((fulfillOuter, rejectOuter) => {
      for (let promise of promises) {
        promise.reject((error) => {
          rejectOuter(error);
        });
      }
    });
    return anyRejectPromise;
  }
}

export HooksProcess;
<|endoftext|>


<commit_before>var urllite = require('urllite');
require('urllite/lib/extensions/toString');

/**
 * A utility for hijacking link clicks and forwarding them to the router.
 */
function LinkHijacker(router, el) {
  this.router = router;
  this.element = el || window;
  this.handleClick = this.handleClick.bind(this);
  this.start();
}

LinkHijacker.prototype.start = function() {
  // This handler works by trying to route the URL and then, if it was
  // successful, updating the history. If the history object being used doesn't
  // support that, don't bother adding the event listener.
  if (!this.router.history.push) return;

  this.element.addEventListener('click', this.handleClick);
};

LinkHijacker.prototype.stop = function() {
  this.element.removeEventListener('click', this.handleClick);
};

LinkHijacker.prototype.handleClick = function(event) {
  // Ignore canceled events, modified clicks, and right clicks.
  if (event.defaultPrevented) return;
  if (event.metaKey || event.ctrlKey || event.shiftKey) return;
  if (event.button !== 0) return;

  // Get the <a> element.
  var el = event.target;
  while (el && el.nodeName !== 'A') {
    el = el.parentNode;
  }

  // Ignore clicks from non-a elements.
  if (!el) return;

  // Ignore the click if the element has a target.
  if (el.target && el.target !== '_self') return;

  // Ignore the click if it's a download link. (We use this method of
  // detecting the presence of the attribute for old IE versions.)
  if (!!el.attributes.download) return;

  // Use a regular expression to parse URLs instead of relying on the browser
  // to do it for us (because IE).
  var url = urllite(el.href);
  var windowURL = urllite(window.location.href);

  // Ignore links that don't share a protocol and host with ours.
  if (url.protocol !== windowURL.protocol || url.host !== windowURL.host)
    return;

  // Ignore URLs that don't share the router's rootURL
  if (url.pathname.indexOf(this.router.rootURL) !== 0) return;

  // Ignore 'rel="external"' links.
  if (el.rel && /(?:^|\s+)external(?:\s+|$)/.test(el.rel)) return;

  event.preventDefault();

  // Dispatch the URL.
  var fullPath = url.pathname + url.search + url.hash;
  this.router.history.push(fullPath);
};

module.exports = LinkHijacker;
<commit_msg>Check rootURL against fullPath<commit_after>var urllite = require('urllite');
require('urllite/lib/extensions/toString');

/**
 * A utility for hijacking link clicks and forwarding them to the router.
 */
function LinkHijacker(router, el) {
  this.router = router;
  this.element = el || window;
  this.handleClick = this.handleClick.bind(this);
  this.start();
}

LinkHijacker.prototype.start = function() {
  // This handler works by trying to route the URL and then, if it was
  // successful, updating the history. If the history object being used doesn't
  // support that, don't bother adding the event listener.
  if (!this.router.history.push) return;

  this.element.addEventListener('click', this.handleClick);
};

LinkHijacker.prototype.stop = function() {
  this.element.removeEventListener('click', this.handleClick);
};

LinkHijacker.prototype.handleClick = function(event) {
  // Ignore canceled events, modified clicks, and right clicks.
  if (event.defaultPrevented) return;
  if (event.metaKey || event.ctrlKey || event.shiftKey) return;
  if (event.button !== 0) return;

  // Get the <a> element.
  var el = event.target;
  while (el && el.nodeName !== 'A') {
    el = el.parentNode;
  }

  // Ignore clicks from non-a elements.
  if (!el) return;

  // Ignore the click if the element has a target.
  if (el.target && el.target !== '_self') return;

  // Ignore the click if it's a download link. (We use this method of
  // detecting the presence of the attribute for old IE versions.)
  if (!!el.attributes.download) return;

  // Use a regular expression to parse URLs instead of relying on the browser
  // to do it for us (because IE).
  var url = urllite(el.href);
  var windowURL = urllite(window.location.href);

  // Ignore links that don't share a protocol and host with ours.
  if (url.protocol !== windowURL.protocol || url.host !== windowURL.host)
    return;

  var fullPath = url.pathname + url.search + url.hash;

  // Ignore URLs that don't share the router's rootURL
  if (fullPath.indexOf(this.router.rootURL) !== 0) return;

  // Ignore 'rel="external"' links.
  if (el.rel && /(?:^|\s+)external(?:\s+|$)/.test(el.rel)) return;

  event.preventDefault();

  // Dispatch the URL.
  this.router.history.push(fullPath);
};

module.exports = LinkHijacker;
<|endoftext|>


<commit_before>const MERGE_IMG_SEGMENT_HEIGHT = 2000;
const puppeteer = require('puppeteer');

const mergeImg = require('merge-img');
const fs = require('./fs');
const path = require('path');
const chalk = require('chalk');
const ensureDirectoryPath = require('./ensureDirectoryPath');
const injectBackstopTools = require('../../capture/backstopTools.js');
const engineTools = require('./engineTools');

const MIN_CHROME_VERSION = 62;
const TEST_TIMEOUT = 60000;
const DEFAULT_FILENAME_TEMPLATE = '{configId}_{scenarioLabel}_{selectorIndex}_{selectorLabel}_{viewportIndex}_{viewportLabel}';
const DEFAULT_BITMAPS_TEST_DIR = 'bitmaps_test';
const DEFAULT_BITMAPS_REFERENCE_DIR = 'bitmaps_reference';
const SELECTOR_NOT_FOUND_PATH = '/capture/resources/notFound.png';
const HIDDEN_SELECTOR_PATH = '/capture/resources/notVisible.png';
const ERROR_SELECTOR_PATH = '/capture/resources/unexpectedErrorSm.png';
const BODY_SELECTOR = 'body';
const DOCUMENT_SELECTOR = 'document';
const NOCLIP_SELECTOR = 'body:noclip';
const VIEWPORT_SELECTOR = 'viewport';

module.exports = function (args) {
  const scenario = args.scenario;
  const viewport = args.viewport;
  const config = args.config;
  const scenarioLabelSafe = engineTools.makeSafe(scenario.label);
  const variantOrScenarioLabelSafe = scenario._parent ? engineTools.makeSafe(scenario._parent.label) : scenarioLabelSafe;

  config._bitmapsTestPath = config.paths.bitmaps_test || DEFAULT_BITMAPS_TEST_DIR;
  config._bitmapsReferencePath = config.paths.bitmaps_reference || DEFAULT_BITMAPS_REFERENCE_DIR;
  config._fileNameTemplate = config.fileNameTemplate || DEFAULT_FILENAME_TEMPLATE;
  config._outputFileFormatSuffix = '.' + ((config.outputFormat && config.outputFormat.match(/jpg|jpeg/)) || 'png');
  config._configId = config.id || engineTools.genHash(config.backstopConfigFileName);

  return processScenarioView(scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config);
};

async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config) {
  if (!config.paths) {
    config.paths = {};
  }

  if (typeof viewport.label !== 'string') {
    viewport.label = viewport.name || '';
  }

  const engineScriptsPath = config.env.engine_scripts || config.env.engine_scripts_default;
  const isReference = config.isReference;

  const VP_W = viewport.width || viewport.viewport.width;
  const VP_H = viewport.height || viewport.viewport.height;

  const puppeteerArgs = Object.assign(
    {},
    {
      ignoreHTTPSErrors: true,
      headless: !config.debugWindow
    },
    config.engineOptions
  );

  const browser = await puppeteer.launch(puppeteerArgs);
  const page = await browser.newPage();

  page.setViewport({ width: VP_W, height: VP_H });
  page.setDefaultNavigationTimeout(engineTools.getEngineOption(config, 'waitTimeout', TEST_TIMEOUT));

  if (isReference) {
    console.log(chalk.blue('CREATING NEW REFERENCE FILE'));
  }

  // --- set up console output and ready event ---
  const readyEvent = scenario.readyEvent || config.readyEvent;
  let readyResolve, readyPromise;
  if (readyEvent) {
    readyPromise = new Promise(resolve => {
      readyResolve = resolve;
    });
  }

  page.on('console', msg => {
    for (let i = 0; i < msg.args().length; ++i) {
      const line = msg.args()[i];
      console.log(`Browser Console Log ${i}: ${line}`);
      if (readyEvent && new RegExp(readyEvent).test(line)) {
        readyResolve();
      }
    }
  });

  let chromeVersion = await page.evaluate(_ => {
    let v = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
    return v ? parseInt(v[2], 10) : 0;
  });

  if (chromeVersion < MIN_CHROME_VERSION) {
    console.warn(`***WARNING! CHROME VERSION ${MIN_CHROME_VERSION} OR GREATER IS REQUIRED. PLEASE UPDATE YOUR CHROME APP!***`);
  }

  let result;
  const puppetCommands = async () => {
    // --- BEFORE SCRIPT ---
    var onBeforeScript = scenario.onBeforeScript || config.onBeforeScript;
    if (onBeforeScript) {
      var beforeScriptPath = path.resolve(engineScriptsPath, onBeforeScript);
      if (fs.existsSync(beforeScriptPath)) {
        await require(beforeScriptPath)(page, scenario, viewport, isReference, browser, config);
      } else {
        console.warn('WARNING: script not found: ' + beforeScriptPath);
      }
    }

    //  --- OPEN URL ---
    var url = scenario.url;
    if (isReference && scenario.referenceUrl) {
      url = scenario.referenceUrl;
    }
    await page.goto(translateUrl(url));

    await injectBackstopTools(page);

    //  --- WAIT FOR READY EVENT ---
    if (readyEvent) {
      await page.evaluate(`window._readyEvent = '${readyEvent}'`);

      await readyPromise;

      await page.evaluate(_ => console.info('readyEvent ok'));
    }

    // --- WAIT FOR SELECTOR ---
    if (scenario.readySelector) {
      await page.waitFor(scenario.readySelector);
    }
    //

    // --- DELAY ---
    if (scenario.delay > 0) {
      await page.waitFor(scenario.delay);
    }

    // --- REMOVE SELECTORS ---
    if (scenario.hasOwnProperty('removeSelectors')) {
      const removeSelectors = async () => {
        return Promise.all(
          scenario.removeSelectors.map(async (selector) => {
            await page
              .evaluate((sel) => {
                document.querySelectorAll(sel).forEach(s => {
                  s.style.cssText = 'display: none !important;';
                  s.classList.add('__86d');
                });
              }, selector);
          })
        );
      };

      await removeSelectors();
    }

    //  --- ON READY SCRIPT ---
    var onReadyScript = scenario.onReadyScript || config.onReadyScript;
    if (onReadyScript) {
      var readyScriptPath = path.resolve(engineScriptsPath, onReadyScript);
      if (fs.existsSync(readyScriptPath)) {
        await require(readyScriptPath)(page, scenario, viewport, isReference, browser, config);
      } else {
        console.warn('WARNING: script not found: ' + readyScriptPath);
      }
    }

    // reinstall tools in case onReadyScript has loaded a new URL.
    await injectBackstopTools(page);

    // --- HIDE SELECTORS ---
    if (scenario.hasOwnProperty('hideSelectors')) {
      const hideSelectors = async () => {
        return Promise.all(
          scenario.hideSelectors.map(async (selector) => {
            await page
              .evaluate((sel) => {
                document.querySelectorAll(sel).forEach(s => {
                  s.style.visibility = 'hidden';
                });
              }, selector);
          })
        );
      };
      await hideSelectors();
    }

    // --- HANDLE NO-SELECTORS ---
    if (!scenario.hasOwnProperty('selectors') || !scenario.selectors.length) {
      scenario.selectors = [DOCUMENT_SELECTOR];
    }

    await page.evaluate(`window._selectorExpansion = '${scenario.selectorExpansion}'`);
    await page.evaluate(`window._backstopSelectors = '${scenario.selectors}'`);
    result = await page.evaluate(() => {
      if (window._selectorExpansion.toString() === 'true') {
        window._backstopSelectorsExp = window._backstopTools.expandSelectors(window._backstopSelectors);
      } else {
        window._backstopSelectorsExp = window._backstopSelectors;
      }
      if (!Array.isArray(window._backstopSelectorsExp)) {
        window._backstopSelectorsExp = window._backstopSelectorsExp.split(',');
      }
      window._backstopSelectorsExpMap = window._backstopSelectorsExp.reduce((acc, selector) => {
        acc[selector] = {
          exists: window._backstopTools.exists(selector),
          isVisible: window._backstopTools.isVisible(selector)
        };
        return acc;
      }, {});
      return {
        backstopSelectorsExp: window._backstopSelectorsExp,
        backstopSelectorsExpMap: window._backstopSelectorsExpMap
      };
    });
  };

  let error;
  await puppetCommands().catch(e => {
    console.log(chalk.red(`Puppeteer encountered an error while running scenario "${scenario.label}"`));
    console.log(chalk.red(e));
    error = e;
  });

  let compareConfig;
  if (!error) {
    try {
      compareConfig = await delegateSelectors(
        page,
        browser,
        scenario,
        viewport,
        variantOrScenarioLabelSafe,
        scenarioLabelSafe,
        config,
        result.backstopSelectorsExp,
        result.backstopSelectorsExpMap
      );
    } catch (e) {
      error = e;
    }
  } else {
    await browser.close();
  }

  if (error) {
    const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, 0, `${scenario.selectors.join('__')}`);
    const filePath = config.isReference ? testPair.reference : testPair.test;
    testPair.engineErrorMsg = error.message;

    compareConfig = {
      testPairs: [ testPair ]
    };
    fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
  }

  return Promise.resolve(compareConfig);
}

// TODO: Should be in engineTools
async function delegateSelectors (
  page,
  browser,
  scenario,
  viewport,
  variantOrScenarioLabelSafe,
  scenarioLabelSafe,
  config,
  selectors,
  selectorMap
) {
  let compareConfig = { testPairs: [] };
  let captureDocument = false;
  let captureViewport = false;
  let captureList = [];
  let captureJobs = [];

  selectors.forEach(function (selector, selectorIndex) {
    const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, selectorIndex, selector);
    const filePath = config.isReference ? testPair.reference : testPair.test;

    if (!config.isReference) {
      compareConfig.testPairs.push(testPair);
    }

    selectorMap[selector].filePath = filePath;
    if (selector === BODY_SELECTOR || selector === DOCUMENT_SELECTOR) {
      captureDocument = selector;
    } else if (selector === VIEWPORT_SELECTOR) {
      captureViewport = selector;
    } else {
      captureList.push(selector);
    }
  });

  if (captureDocument) {
    captureJobs.push(function () { return captureScreenshot(page, browser, captureDocument, selectorMap, config, []); });
  }
  // TODO: push captureViewport into captureList (instead of calling captureScreenshot()) to improve perf.
  if (captureViewport) {
    captureJobs.push(function () { return captureScreenshot(page, browser, captureViewport, selectorMap, config, []); });
  }
  if (captureList.length) {
    captureJobs.push(function () { return captureScreenshot(page, browser, null, selectorMap, config, captureList); });
  }

  return new Promise(function (resolve, reject) {
    var job = null;
    var errors = [];
    var next = function () {
      if (captureJobs.length === 0) {
        if (errors.length === 0) {
          resolve();
        } else {
          reject(errors);
        }
        return;
      }
      job = captureJobs.shift();
      job().catch(function (e) {
        console.log(e);
        errors.push(e);
      }).then(function () {
        next();
      });
    };
    next();
  }).then(async () => {
    console.log(chalk.green('x Close Browser'));
    await browser.close();
  }).catch(async (err) => {
    console.log(chalk.red(err));
    await browser.close();
  }).then(_ => compareConfig);
}

async function captureScreenshot (page, browser, selector, selectorMap, config, selectors) {
  let filePath;
  let fullPage = (selector === NOCLIP_SELECTOR || selector === DOCUMENT_SELECTOR);
  if (selector) {
    filePath = selectorMap[selector].filePath;
    ensureDirectoryPath(filePath);

    /*
      Setting config.mergeImgHack == true will use an alternate screen grab method which
      takes multple screenshots at periodic scroll positions then stitches them all togther
      into one single screenshot.
     */
    if (fullPage && config.mergeImgHack) {
      // Safer version of `document` selector
      // see https://github.com/garris/BackstopJS/issues/820
      try {
        const screenHeight = typeof config.mergeImgHack === 'number' ?  config.mergeImgHack : MERGE_IMG_SEGMENT_HEIGHT;

        const bodyHandle = await page.$('body');
        const { width, height: totalHeight } = await bodyHandle.boundingBox();

        const screens = [];
        let cumHeight = 0;
        for (let i = 0; i * screenHeight < totalHeight; i++) {
          cumHeight += screenHeight;
          console.log(`screenshot part ${i} (${Math.min(cumHeight, totalHeight)} / ${totalHeight})`);
          const screen = await page.screenshot({
            path: totalHeight > screenHeight ? undefined : filePath, // if only 1 screen is needed with save immediately
            fullPage: false,
            clip: {
              x: 0,
              y: i * screenHeight,
              width,
              height:
                cumHeight > totalHeight
                  ? Math.ceil(screenHeight - (cumHeight - totalHeight))
                  : screenHeight
            }
          });
          screens.push(screen);
        }

        // if there was only 1 screen we already saved the file during screenshot
        if (screens.length > 1) {
          const img = await mergeImg(screens, {
            direction: true
          });

          // Note: mergeImg relies on an old version of Jimp without writeAsync support
          // See https://github.com/preco21/merge-img/issues/6
          await new Promise((resolve, reject) => {
            img.write(filePath, err => {
              if (err) return reject(err);
              resolve();
            });
          });
        }
        await bodyHandle.dispose();
      } catch (e) {
        console.log(chalk.red(`Error capturing..`), e);
        return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
      }
    } else {
      try {
        await page.screenshot({
          path: filePath,
          fullPage: fullPage
        });
      } catch (e) {
        console.log(chalk.red(`Error capturing..`), e);
        return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
      }
    }
  } else {
    // OTHER-SELECTOR screenshot
    const selectorShot = async (s, path) => {
      const el = await page.$(s);
      if (el) {
        const box = await el.boundingBox();
        if (box) {
          var type = config.puppeteerOffscreenCaptureFix ? page : el;
          var params = config.puppeteerOffscreenCaptureFix ? { path: path, clip: box } : { path: path };
          await type.screenshot(params);
        } else {
          console.log(chalk.yellow(`Element not visible for capturing: ${s}`));
          return fs.copy(config.env.backstop + HIDDEN_SELECTOR_PATH, path);
        }
      } else {
        console.log(chalk.magenta(`Element not found for capturing: ${s}`));
        return fs.copy(config.env.backstop + SELECTOR_NOT_FOUND_PATH, path);
      }
    };

    const selectorsShot = async () => {
      return Promise.all(
        selectors.map(async selector => {
          filePath = selectorMap[selector].filePath;
          ensureDirectoryPath(filePath);
          try {
            await selectorShot(selector, filePath);
          } catch (e) {
            console.log(chalk.red(`Error capturing Element ${selector}`), e);
            return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
          }
        })
      );
    };
    await selectorsShot();
  }
}

// handle relative file name
function translateUrl (url) {
  const RE = new RegExp('^[./]');
  if (RE.test(url)) {
    const fileUrl = 'file://' + path.join(process.cwd(), url);
    console.log('Relative filename detected -- translating to ' + fileUrl);
    return fileUrl;
  } else {
    return url;
  }
}
<commit_msg>lint!<commit_after>const MERGE_IMG_SEGMENT_HEIGHT = 2000;
const puppeteer = require('puppeteer');

const mergeImg = require('merge-img');
const fs = require('./fs');
const path = require('path');
const chalk = require('chalk');
const ensureDirectoryPath = require('./ensureDirectoryPath');
const injectBackstopTools = require('../../capture/backstopTools.js');
const engineTools = require('./engineTools');

const MIN_CHROME_VERSION = 62;
const TEST_TIMEOUT = 60000;
const DEFAULT_FILENAME_TEMPLATE = '{configId}_{scenarioLabel}_{selectorIndex}_{selectorLabel}_{viewportIndex}_{viewportLabel}';
const DEFAULT_BITMAPS_TEST_DIR = 'bitmaps_test';
const DEFAULT_BITMAPS_REFERENCE_DIR = 'bitmaps_reference';
const SELECTOR_NOT_FOUND_PATH = '/capture/resources/notFound.png';
const HIDDEN_SELECTOR_PATH = '/capture/resources/notVisible.png';
const ERROR_SELECTOR_PATH = '/capture/resources/unexpectedErrorSm.png';
const BODY_SELECTOR = 'body';
const DOCUMENT_SELECTOR = 'document';
const NOCLIP_SELECTOR = 'body:noclip';
const VIEWPORT_SELECTOR = 'viewport';

module.exports = function (args) {
  const scenario = args.scenario;
  const viewport = args.viewport;
  const config = args.config;
  const scenarioLabelSafe = engineTools.makeSafe(scenario.label);
  const variantOrScenarioLabelSafe = scenario._parent ? engineTools.makeSafe(scenario._parent.label) : scenarioLabelSafe;

  config._bitmapsTestPath = config.paths.bitmaps_test || DEFAULT_BITMAPS_TEST_DIR;
  config._bitmapsReferencePath = config.paths.bitmaps_reference || DEFAULT_BITMAPS_REFERENCE_DIR;
  config._fileNameTemplate = config.fileNameTemplate || DEFAULT_FILENAME_TEMPLATE;
  config._outputFileFormatSuffix = '.' + ((config.outputFormat && config.outputFormat.match(/jpg|jpeg/)) || 'png');
  config._configId = config.id || engineTools.genHash(config.backstopConfigFileName);

  return processScenarioView(scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config);
};

async function processScenarioView (scenario, variantOrScenarioLabelSafe, scenarioLabelSafe, viewport, config) {
  if (!config.paths) {
    config.paths = {};
  }

  if (typeof viewport.label !== 'string') {
    viewport.label = viewport.name || '';
  }

  const engineScriptsPath = config.env.engine_scripts || config.env.engine_scripts_default;
  const isReference = config.isReference;

  const VP_W = viewport.width || viewport.viewport.width;
  const VP_H = viewport.height || viewport.viewport.height;

  const puppeteerArgs = Object.assign(
    {},
    {
      ignoreHTTPSErrors: true,
      headless: !config.debugWindow
    },
    config.engineOptions
  );

  const browser = await puppeteer.launch(puppeteerArgs);
  const page = await browser.newPage();

  page.setViewport({ width: VP_W, height: VP_H });
  page.setDefaultNavigationTimeout(engineTools.getEngineOption(config, 'waitTimeout', TEST_TIMEOUT));

  if (isReference) {
    console.log(chalk.blue('CREATING NEW REFERENCE FILE'));
  }

  // --- set up console output and ready event ---
  const readyEvent = scenario.readyEvent || config.readyEvent;
  let readyResolve, readyPromise;
  if (readyEvent) {
    readyPromise = new Promise(resolve => {
      readyResolve = resolve;
    });
  }

  page.on('console', msg => {
    for (let i = 0; i < msg.args().length; ++i) {
      const line = msg.args()[i];
      console.log(`Browser Console Log ${i}: ${line}`);
      if (readyEvent && new RegExp(readyEvent).test(line)) {
        readyResolve();
      }
    }
  });

  let chromeVersion = await page.evaluate(_ => {
    let v = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
    return v ? parseInt(v[2], 10) : 0;
  });

  if (chromeVersion < MIN_CHROME_VERSION) {
    console.warn(`***WARNING! CHROME VERSION ${MIN_CHROME_VERSION} OR GREATER IS REQUIRED. PLEASE UPDATE YOUR CHROME APP!***`);
  }

  let result;
  const puppetCommands = async () => {
    // --- BEFORE SCRIPT ---
    var onBeforeScript = scenario.onBeforeScript || config.onBeforeScript;
    if (onBeforeScript) {
      var beforeScriptPath = path.resolve(engineScriptsPath, onBeforeScript);
      if (fs.existsSync(beforeScriptPath)) {
        await require(beforeScriptPath)(page, scenario, viewport, isReference, browser, config);
      } else {
        console.warn('WARNING: script not found: ' + beforeScriptPath);
      }
    }

    //  --- OPEN URL ---
    var url = scenario.url;
    if (isReference && scenario.referenceUrl) {
      url = scenario.referenceUrl;
    }
    await page.goto(translateUrl(url));

    await injectBackstopTools(page);

    //  --- WAIT FOR READY EVENT ---
    if (readyEvent) {
      await page.evaluate(`window._readyEvent = '${readyEvent}'`);

      await readyPromise;

      await page.evaluate(_ => console.info('readyEvent ok'));
    }

    // --- WAIT FOR SELECTOR ---
    if (scenario.readySelector) {
      await page.waitFor(scenario.readySelector);
    }
    //

    // --- DELAY ---
    if (scenario.delay > 0) {
      await page.waitFor(scenario.delay);
    }

    // --- REMOVE SELECTORS ---
    if (scenario.hasOwnProperty('removeSelectors')) {
      const removeSelectors = async () => {
        return Promise.all(
          scenario.removeSelectors.map(async (selector) => {
            await page
              .evaluate((sel) => {
                document.querySelectorAll(sel).forEach(s => {
                  s.style.cssText = 'display: none !important;';
                  s.classList.add('__86d');
                });
              }, selector);
          })
        );
      };

      await removeSelectors();
    }

    //  --- ON READY SCRIPT ---
    var onReadyScript = scenario.onReadyScript || config.onReadyScript;
    if (onReadyScript) {
      var readyScriptPath = path.resolve(engineScriptsPath, onReadyScript);
      if (fs.existsSync(readyScriptPath)) {
        await require(readyScriptPath)(page, scenario, viewport, isReference, browser, config);
      } else {
        console.warn('WARNING: script not found: ' + readyScriptPath);
      }
    }

    // reinstall tools in case onReadyScript has loaded a new URL.
    await injectBackstopTools(page);

    // --- HIDE SELECTORS ---
    if (scenario.hasOwnProperty('hideSelectors')) {
      const hideSelectors = async () => {
        return Promise.all(
          scenario.hideSelectors.map(async (selector) => {
            await page
              .evaluate((sel) => {
                document.querySelectorAll(sel).forEach(s => {
                  s.style.visibility = 'hidden';
                });
              }, selector);
          })
        );
      };
      await hideSelectors();
    }

    // --- HANDLE NO-SELECTORS ---
    if (!scenario.hasOwnProperty('selectors') || !scenario.selectors.length) {
      scenario.selectors = [DOCUMENT_SELECTOR];
    }

    await page.evaluate(`window._selectorExpansion = '${scenario.selectorExpansion}'`);
    await page.evaluate(`window._backstopSelectors = '${scenario.selectors}'`);
    result = await page.evaluate(() => {
      if (window._selectorExpansion.toString() === 'true') {
        window._backstopSelectorsExp = window._backstopTools.expandSelectors(window._backstopSelectors);
      } else {
        window._backstopSelectorsExp = window._backstopSelectors;
      }
      if (!Array.isArray(window._backstopSelectorsExp)) {
        window._backstopSelectorsExp = window._backstopSelectorsExp.split(',');
      }
      window._backstopSelectorsExpMap = window._backstopSelectorsExp.reduce((acc, selector) => {
        acc[selector] = {
          exists: window._backstopTools.exists(selector),
          isVisible: window._backstopTools.isVisible(selector)
        };
        return acc;
      }, {});
      return {
        backstopSelectorsExp: window._backstopSelectorsExp,
        backstopSelectorsExpMap: window._backstopSelectorsExpMap
      };
    });
  };

  let error;
  await puppetCommands().catch(e => {
    console.log(chalk.red(`Puppeteer encountered an error while running scenario "${scenario.label}"`));
    console.log(chalk.red(e));
    error = e;
  });

  let compareConfig;
  if (!error) {
    try {
      compareConfig = await delegateSelectors(
        page,
        browser,
        scenario,
        viewport,
        variantOrScenarioLabelSafe,
        scenarioLabelSafe,
        config,
        result.backstopSelectorsExp,
        result.backstopSelectorsExpMap
      );
    } catch (e) {
      error = e;
    }
  } else {
    await browser.close();
  }

  if (error) {
    const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, 0, `${scenario.selectors.join('__')}`);
    const filePath = config.isReference ? testPair.reference : testPair.test;
    testPair.engineErrorMsg = error.message;

    compareConfig = {
      testPairs: [ testPair ]
    };
    fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
  }

  return Promise.resolve(compareConfig);
}

// TODO: Should be in engineTools
async function delegateSelectors (
  page,
  browser,
  scenario,
  viewport,
  variantOrScenarioLabelSafe,
  scenarioLabelSafe,
  config,
  selectors,
  selectorMap
) {
  let compareConfig = { testPairs: [] };
  let captureDocument = false;
  let captureViewport = false;
  let captureList = [];
  let captureJobs = [];

  selectors.forEach(function (selector, selectorIndex) {
    const testPair = engineTools.generateTestPair(config, scenario, viewport, variantOrScenarioLabelSafe, scenarioLabelSafe, selectorIndex, selector);
    const filePath = config.isReference ? testPair.reference : testPair.test;

    if (!config.isReference) {
      compareConfig.testPairs.push(testPair);
    }

    selectorMap[selector].filePath = filePath;
    if (selector === BODY_SELECTOR || selector === DOCUMENT_SELECTOR) {
      captureDocument = selector;
    } else if (selector === VIEWPORT_SELECTOR) {
      captureViewport = selector;
    } else {
      captureList.push(selector);
    }
  });

  if (captureDocument) {
    captureJobs.push(function () { return captureScreenshot(page, browser, captureDocument, selectorMap, config, []); });
  }
  // TODO: push captureViewport into captureList (instead of calling captureScreenshot()) to improve perf.
  if (captureViewport) {
    captureJobs.push(function () { return captureScreenshot(page, browser, captureViewport, selectorMap, config, []); });
  }
  if (captureList.length) {
    captureJobs.push(function () { return captureScreenshot(page, browser, null, selectorMap, config, captureList); });
  }

  return new Promise(function (resolve, reject) {
    var job = null;
    var errors = [];
    var next = function () {
      if (captureJobs.length === 0) {
        if (errors.length === 0) {
          resolve();
        } else {
          reject(errors);
        }
        return;
      }
      job = captureJobs.shift();
      job().catch(function (e) {
        console.log(e);
        errors.push(e);
      }).then(function () {
        next();
      });
    };
    next();
  }).then(async () => {
    console.log(chalk.green('x Close Browser'));
    await browser.close();
  }).catch(async (err) => {
    console.log(chalk.red(err));
    await browser.close();
  }).then(_ => compareConfig);
}

async function captureScreenshot (page, browser, selector, selectorMap, config, selectors) {
  let filePath;
  let fullPage = (selector === NOCLIP_SELECTOR || selector === DOCUMENT_SELECTOR);
  if (selector) {
    filePath = selectorMap[selector].filePath;
    ensureDirectoryPath(filePath);

    /*
      Setting config.mergeImgHack == true will use an alternate screen grab method which
      takes multple screenshots at periodic scroll positions then stitches them all togther
      into one single screenshot.
     */
    if (fullPage && config.mergeImgHack) {
      // Safer version of `document` selector
      // see https://github.com/garris/BackstopJS/issues/820
      try {
        const screenHeight = typeof config.mergeImgHack === 'number' ? config.mergeImgHack : MERGE_IMG_SEGMENT_HEIGHT;

        const bodyHandle = await page.$('body');
        const { width, height: totalHeight } = await bodyHandle.boundingBox();

        const screens = [];
        let cumHeight = 0;
        for (let i = 0; i * screenHeight < totalHeight; i++) {
          cumHeight += screenHeight;
          console.log(`screenshot part ${i} (${Math.min(cumHeight, totalHeight)} / ${totalHeight})`);
          const screen = await page.screenshot({
            path: totalHeight > screenHeight ? undefined : filePath, // if only 1 screen is needed with save immediately
            fullPage: false,
            clip: {
              x: 0,
              y: i * screenHeight,
              width,
              height:
                cumHeight > totalHeight
                  ? Math.ceil(screenHeight - (cumHeight - totalHeight))
                  : screenHeight
            }
          });
          screens.push(screen);
        }

        // if there was only 1 screen we already saved the file during screenshot
        if (screens.length > 1) {
          const img = await mergeImg(screens, {
            direction: true
          });

          // Note: mergeImg relies on an old version of Jimp without writeAsync support
          // See https://github.com/preco21/merge-img/issues/6
          await new Promise((resolve, reject) => {
            img.write(filePath, err => {
              if (err) return reject(err);
              resolve();
            });
          });
        }
        await bodyHandle.dispose();
      } catch (e) {
        console.log(chalk.red(`Error capturing..`), e);
        return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
      }
    } else {
      try {
        await page.screenshot({
          path: filePath,
          fullPage: fullPage
        });
      } catch (e) {
        console.log(chalk.red(`Error capturing..`), e);
        return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
      }
    }
  } else {
    // OTHER-SELECTOR screenshot
    const selectorShot = async (s, path) => {
      const el = await page.$(s);
      if (el) {
        const box = await el.boundingBox();
        if (box) {
          var type = config.puppeteerOffscreenCaptureFix ? page : el;
          var params = config.puppeteerOffscreenCaptureFix ? { path: path, clip: box } : { path: path };
          await type.screenshot(params);
        } else {
          console.log(chalk.yellow(`Element not visible for capturing: ${s}`));
          return fs.copy(config.env.backstop + HIDDEN_SELECTOR_PATH, path);
        }
      } else {
        console.log(chalk.magenta(`Element not found for capturing: ${s}`));
        return fs.copy(config.env.backstop + SELECTOR_NOT_FOUND_PATH, path);
      }
    };

    const selectorsShot = async () => {
      return Promise.all(
        selectors.map(async selector => {
          filePath = selectorMap[selector].filePath;
          ensureDirectoryPath(filePath);
          try {
            await selectorShot(selector, filePath);
          } catch (e) {
            console.log(chalk.red(`Error capturing Element ${selector}`), e);
            return fs.copy(config.env.backstop + ERROR_SELECTOR_PATH, filePath);
          }
        })
      );
    };
    await selectorsShot();
  }
}

// handle relative file name
function translateUrl (url) {
  const RE = new RegExp('^[./]');
  if (RE.test(url)) {
    const fileUrl = 'file://' + path.join(process.cwd(), url);
    console.log('Relative filename detected -- translating to ' + fileUrl);
    return fileUrl;
  } else {
    return url;
  }
}
<|endoftext|>