@@ -3,17 +3,16 @@ import { Adapter } from 'ember-pouch';
 import PouchAdapterUtils from "hospitalrun/mixins/pouch-adapter-utils";

 export default Adapter.extend(PouchAdapterUtils, {
-    pouchDBService: Ember.inject.service('pouchdb'),
-
-    mainDB: Ember.computed.alias('pouchDBService.mainDB'),
-    db:  Ember.computed.alias('pouchDBService.mainDB'),
-
+    database: Ember.inject.service(),
+    mainDB: Ember.computed.alias('database.mainDB'),
+    db:  Ember.computed.alias('database.mainDB'),
+
     _specialQueries: [
         'containsValue',
         'mapReduce',
         'searchIndex'
     ],
-
+
     _executeContainsSearch: function(store, type, query) {
          return new Ember.RSVP.Promise(function(resolve, reject){
             var searchUrl = '/search/hrdb/'+type.typeKey+'/_search';

import Ember from "ember";
import { Adapter } from 'ember-pouch';
import PouchAdapterUtils from "hospitalrun/mixins/pouch-adapter-utils";

export default Adapter.extend(PouchAdapterUtils, {
    pouchDBService: Ember.inject.service('pouchdb'),

    mainDB: Ember.computed.alias('pouchDBService.mainDB'),
    db:  Ember.computed.alias('pouchDBService.mainDB'),

    _specialQueries: [
        'containsValue',
        'mapReduce',
        'searchIndex'
    ],

    _executeContainsSearch: function(store, type, query) {
         return new Ember.RSVP.Promise(function(resolve, reject){
            var searchUrl = '/search/hrdb/'+type.typeKey+'/_search';
            if (query.containsValue && query.containsValue.value) {
                var queryString = '';
                query.containsValue.keys.forEach(function(key) {
                    if (!Ember.isEmpty(queryString)) {
                        queryString += ' OR ';
                    }
                    queryString += key+':'+query.containsValue.value;
                });
                Ember.$.ajax(searchUrl, {
                    dataType: 'json',
                    data: {
                        q:queryString
                    },
                    success: function (results) {
                        if (results && results.hits && results.hits.hits) {
                            var resultDocs = Ember.A(results.hits.hits).map(function(hit) {
                                var mappedResult = hit._source;
                                mappedResult.id = mappedResult._id;
                                return mappedResult;
                            });
                            var response = {
                                rows: resultDocs
                            };
                            this._handleQueryResponse(response, store, type).then(resolve,reject);
                        } else if (results.rows) {
                            this._handleQueryResponse(results, store, type).then(resolve,reject);
                        } else {
                            reject('Search results are not valid');
                        }
                    }.bind(this)
                });
            } else {
                reject('invalid query');
            }
        }.bind(this));
    },

    _handleQueryResponse: function(response, store, type) {
        var pouchDBService = this.get('pouchDBService');
        return new Ember.RSVP.Promise(function(resolve, reject){
            if (response.rows.length > 0) {
                var ids = response.rows.map(function(row) {
                    return pouchDBService.getEmberId(row.id);
                });
                this.findRecord(store, type, ids).then(function(findResponse) {
                    var primaryRecordName = type.modelName.camelize().pluralize(),
                        sortedValues = [];
                    //Sort response in order of ids
                    ids.forEach(function(id) {
                        var resolvedRecord = findResponse[primaryRecordName].findBy('id',id);
                        sortedValues.push(resolvedRecord);
                    });
                    findResponse[primaryRecordName] = sortedValues;
                    resolve(findResponse);
                }.bind(this), reject);
            } else {
                var emptyResponse = {};
                emptyResponse[type.modelName] = [];
                resolve(emptyResponse);
            }
        }.bind(this));
    },

    /**
     * Look for nulls and maxvalues in start key because those keys can't be handled by the sort/list function
     */
    _doesStartKeyContainSpecialCharacters: function(startkey) {
        var haveSpecialCharacters = false,
            maxValue = this.get('maxValue');
        if (!Ember.isEmpty(startkey) && Ember.isArray(startkey)) {
            startkey.forEach(function(keyvalue) {
                if (keyvalue === null || keyvalue === maxValue) {
                    haveSpecialCharacters = true;
                }
            });
        }
        return haveSpecialCharacters;
    },

    findQuery: function(store, type, query, options) {
        var specialQuery = false;
        for (var i=0;i< this._specialQueries.length; i++) {
            if (Ember.get(query,this._specialQueries[i])) {
                specialQuery = true;
                break;
            }
        }
        if (!specialQuery) {
            if (query.options) {
                this._init(store, type);
                var recordTypeName = this.getRecordTypeName(type);
                return this.get('db').rel.find(recordTypeName, query.options);
            } else {
                return this._super(store, type, query, options);
            }
        } else {
            var mapReduce = null,
                queryParams = {};
            if (query.searchIndex) {
                queryParams = query.searchIndex;
            }
            if (query.options) {
                queryParams = Ember.copy(query.options);
                if (query.sortKey || query.filterBy) {
                    if (query.sortDesc) {
                        queryParams.sortDesc = query.sortDesc;
                    }
                    if (query.sortKey) {
                        queryParams.sortKey = query.sortKey;
                    }
                    if (!this._doesStartKeyContainSpecialCharacters(queryParams.startkey)) {
                        queryParams.sortLimit = queryParams.limit;
                        delete queryParams.limit;
                        queryParams.sortStartKey = JSON.stringify(queryParams.startkey);
                        delete queryParams.startkey;
                    } else if (queryParams.startkey) {
                        queryParams.startkey = JSON.stringify(queryParams.startkey);
                    }
                    if (query.filterBy) {
                        queryParams.filterBy = JSON.stringify(query.filterBy);
                    }
                    if (queryParams.endkey) {
                        queryParams.endkey = JSON.stringify(queryParams.endkey);
                    }
                    query.useList = true;
                }
            }
            queryParams.reduce  = false;
            queryParams.include_docs = false;

            if (query.mapReduce) {
                mapReduce = query.mapReduce;
            } else if (query.containsValue) {
                return this._executeContainsSearch(store, type, query);
            }
            return new Ember.RSVP.Promise(function(resolve, reject){
                var db = this.get('db');
                    try {
                        if (mapReduce) {
                            if (query.useList) {
                                queryParams.include_docs = true;
                                var listParams = {
                                    query: queryParams
                                };
                                db.list(mapReduce+'/sort/'+mapReduce, listParams, function(err, response) {
                                    if (err) {
                                        this._pouchError(reject)(err);
                                    } else {
                                        this._handleQueryResponse(response.json, store, type).then(resolve, reject);
                                    }
                                }.bind(this));
                            } else {
                                db.query(mapReduce, queryParams, function(err, response) {
                                    if (err) {
                                        this._pouchError(reject)(err);
                                    } else {
                                        this._handleQueryResponse(response, store, type).then(resolve, reject);
                                    }
                                }.bind(this));
                            }
                        } else {
                            db.allDocs(queryParams, function(err, response) {
                                if (err) {
                                    this._pouchError(reject)(err);
                                } else {
                                    this._handleQueryResponse(response, store, type).then(resolve, reject);
                                }
                            }.bind(this));
                        }
                    } catch (err){
                        this._pouchError(reject)(err);
                    }
            }.bind(this), "findQuery in application-pouchdb-adapter");
        }
    }
});

can we merge mainDB and db into one since they point to the same thing.


@@ -9,7 +9,7 @@ const build = (buildConfig = config.defaultBuildConfig, options) => {
     util.updateBranding()
   }

-  util.buildMuon()
+  util.buildMuon('brave')
 }

 module.exports = build

const config = require('../lib/config')
const util = require('../lib/util')

const build = (buildConfig = config.defaultBuildConfig, options) => {
  config.buildConfig = buildConfig
  config.update(options)

  if (!options.no_branding_update) {
    util.updateBranding()
  }

  util.buildMuon()
}

module.exports = build

pls pr a name change fro muon here too separately.


@@ -4,6 +4,7 @@ const config = require('./config')
 const fs = require('fs-extra')

 const runGClient = (args, options = {}) => {
+  if (options.verbose) args.push('--verbose')
   options.cwd = options.cwd || config.rootDir
   options = mergeWithDefault(options)
   options.env.GCLIENT_FILE = config.gClientFile

const path = require('path')
const spawnSync = require('child_process').spawnSync
const config = require('./config')
const fs = require('fs-extra')

const runGClient = (args, options = {}) => {
  options.cwd = options.cwd || config.rootDir
  options = mergeWithDefault(options)
  options.env.GCLIENT_FILE = config.gClientFile
  util.run('gclient', args, options)
}

const mergeWithDefault = (options) => {
  return Object.assign({}, config.defaultOptions, options)
}

const util = {
  run: (cmd, args = [], options = {}) => {
    console.log(cmd, args.join(' '))
    const continueOnFail = options.continueOnFail
    delete options.continueOnFail

    const prog = spawnSync(cmd, args, options)
    if (prog.status !== 0) {
      if (!continueOnFail) {
        console.log(prog.stdout && prog.stdout.toString())
        console.error(prog.stderr && prog.stderr.toString())
        process.exit(1)
      }
    }
    return prog
  },

  buildGClientConfig: () => {
    function replacer(key, value) {
      return value;
    }

    let solutions = config.projectNames.filter((projectName) => config.projects[projectName].ref).map((projectName) => {
      let project = config.projects[projectName]
      return {
        managed: "%False%",
        name: project.gclientName,
        url: project.url,
        custom_deps: project.custom_deps
      }
    })

    const out = 'solutions = ' + JSON.stringify(solutions, replacer, 2)
      .replace(/"%None%"/g, "None")
      .replace(/"%False%"/g, "False")
    fs.writeFileSync(config.defaultGClientFile, out)
  },

  updateBranding: () => {
    console.log('update branding...')
    const chromeComponentsDir = path.join(config.srcDir, 'components')
    const chromeAppDir = path.join(config.srcDir, 'chrome', 'app')
    const braveAppDir = path.join(config.projects['brave-core'].dir, 'app')
    fs.copySync(path.join(braveAppDir, 'brave_strings.grd'), path.join(chromeAppDir, 'brave_strings.grd'))
    fs.copySync(path.join(braveAppDir, 'settings_brave_strings.grdp'), path.join(chromeAppDir, 'settings_brave_strings.grdp'))
    fs.copySync(path.join(braveAppDir, 'components_brave_strings.grd'), path.join(config.srcDir, 'components', 'components_brave_strings.grd'))
    fs.copySync(path.join(braveAppDir, 'theme', 'brave'), path.join(chromeAppDir, 'theme', 'brave'))
    fs.copySync(path.join(braveAppDir, 'theme', 'default_100_percent', 'brave'), path.join(chromeAppDir, 'theme', 'default_100_percent', 'brave'))
    fs.copySync(path.join(braveAppDir, 'theme', 'default_200_percent', 'brave'), path.join(chromeAppDir, 'theme', 'default_200_percent', 'brave'))
    fs.copySync(path.join(braveAppDir, 'vector_icons', 'brave'), path.join(chromeAppDir, 'vector_icons', 'brave'))
    // Copy XTB files for app/brave_strings.grd => chromium_strings.grd
    fs.copySync(path.join(braveAppDir, 'resources'), path.join(chromeAppDir, 'resources'))
    // Copy XTB files for brave/app/components_brave_strings.grd => components/components_chromium_strings.grd
    fs.copySync(path.join(braveAppDir, 'strings'), path.join(chromeComponentsDir, 'strings'))
  },

  buildMuon: (options = config.defaultOptions) => {
    console.log('building brave...')

    const args = util.buildArgsToString(config.buildArgs())
    util.run('gn', ['gen', config.outputDir, '--args="' + args + '"'], options)
    util.run('ninja', ['-C', config.outputDir, 'brave'], options)
  },

  submoduleSync: (options = { cwd: config.rootDir }) => {
    options = mergeWithDefault(options)
    util.run('git', ['submodule', 'sync'], options)
    util.run('git', ['submodule', 'update', '--init', '--recursive'], options)
    util.run('git', ['clean', '-fxd'], Object.assign(options, {cwd: config.depotToolsDir}))
    util.run('git', ['reset', '--hard', 'HEAD'], Object.assign(options, {cwd: config.depotToolsDir}))
  },

  gclientSync: (options = {}) => {
    runGClient(['sync', '--force', '--nohooks', '--with_branch_heads'], options)
  },

  gclientRunhooks: (options = {}) => {
    runGClient(['runhooks'], options)
  },

  fetch: (options = {}) => {
    options = mergeWithDefault(options)
    util.run('git', ['fetch', '--all', '--tags'], options)
  },

  setVersion: (version, options = {}) => {
    util.run('git', ['clean', '-f'], options)
    util.run('git', ['reset', '--hard', version], options)
  },

  setDepVersion: (dir, version) => {
    const options = { cwd: dir }
    util.fetch(options)
    util.setVersion(version, options)
  },

  buildArgsToString: (buildArgs) => {
    let args = ''
    for (let arg in buildArgs) {
      let val = buildArgs[arg]
      if (typeof val === 'string') {
        val = '"' + val + '"'
      } else {
        val = JSON.stringify(val)
      }
      args += arg + '=' + val + ' '
    }
    return args.replace(/"/g,'\\"')
  }
}

module.exports = util

we might as well just access `config.gClientVerbose` directly here. If any other options are passed to `gclientSync` or `gclientRunhooks` then the verbose option will be lost the way it's used now


@@ -1,4 +1,4 @@
-define(['browser', 'css!./emby-collapse', 'registerElement', 'emby-button'], function (browser) {
+define(['browser', 'css!elements/emby-collapse/emby-collapse', 'registerElement', 'emby-button'], function (browser) {
     'use strict';

     var EmbyButtonPrototype = Object.create(HTMLDivElement.prototype);

define(['browser', 'css!./emby-collapse', 'registerElement', 'emby-button'], function (browser) {
    'use strict';

    var EmbyButtonPrototype = Object.create(HTMLDivElement.prototype);

    function slideDownToShow(button, elem) {

        elem.classList.remove('hide');
        elem.classList.add('expanded');
        elem.style.height = 'auto';
        var height = elem.offsetHeight + 'px';
        elem.style.height = '0';

        // trigger reflow
        var newHeight = elem.offsetHeight;
        elem.style.height = height;

        setTimeout(function () {
            if (elem.classList.contains('expanded')) {
                elem.classList.remove('hide');
            } else {
                elem.classList.add('hide');
            }
            elem.style.height = 'auto';
        }, 300);

        var icon = button.querySelector('i');
        //icon.innerHTML = 'expand_less';
        icon.classList.add('emby-collapse-expandIconExpanded');
    }

    function slideUpToHide(button, elem) {

        elem.style.height = elem.offsetHeight + 'px';
        // trigger reflow
        var newHeight = elem.offsetHeight;

        elem.classList.remove('expanded');
        elem.style.height = '0';

        setTimeout(function () {
            if (elem.classList.contains('expanded')) {
                elem.classList.remove('hide');
            } else {
                elem.classList.add('hide');
            }
        }, 300);

        var icon = button.querySelector('i');
        //icon.innerHTML = 'expand_more';
        icon.classList.remove('emby-collapse-expandIconExpanded');
    }

    function onButtonClick(e) {

        var button = this;
        var collapseContent = button.parentNode.querySelector('.collapseContent');

        if (collapseContent.expanded) {
            collapseContent.expanded = false;
            slideUpToHide(button, collapseContent);
        } else {
            collapseContent.expanded = true;
            slideDownToShow(button, collapseContent);
        }
    }

    EmbyButtonPrototype.attachedCallback = function () {

        if (this.classList.contains('emby-collapse')) {
            return;
        }

        this.classList.add('emby-collapse');

        var collapseContent = this.querySelector('.collapseContent');
        if (collapseContent) {
            collapseContent.classList.add('hide');
        }

        var title = this.getAttribute('title');

        var html = '<button is="emby-button" type="button" on-click="toggleExpand" id="expandButton" class="emby-collapsible-button iconRight">
' + title + '
</button>';

        this.insertAdjacentHTML('afterbegin', html);

        var button = this.querySelector('.emby-collapsible-button');

        button.addEventListener('click', onButtonClick);

        if (this.getAttribute('data-expanded') === 'true') {
            onButtonClick.call(button);
        }
    };

    document.registerElement('emby-collapse', {
        prototype: EmbyButtonPrototype,
        extends: 'div'
    });
});

I know frameworks that support current directory when loading dependencies, is this a limitation of the requirejs loader or can we fix it somehow?


@@ -1,18 +1,15 @@
+"use strict";
+const express = require("express");
+const mongoose = require("../../../lib");

-'use strict';
-const express = require('express');
-const mongoose = require('../../../lib');
-
-const uri = 'mongodb://localhost/mongoose-shared-connection';
+const uri = "mongodb://localhost/mongoose-shared-connection";
 global.db = mongoose.createConnection(uri);

-const routes = require('./routes');
+const routes = require("./routes");

 const app = express();
-app.get('/', routes.home);
-app.get('/insert', routes.insert);
-app.get('/name', routes.modelName);
+app.get("/", routes.home);
+app.get("/insert", routes.insert);
+app.get("/name", routes.modelName);

-app.listen(8000, function() {
-  console.log('listening on http://localhost:8000');
-});
+app.listen(8000, console.log("listening on http://localhost:8000"));

'use strict';
const express = require('express');
const mongoose = require('../../../lib');

const uri = 'mongodb://localhost/mongoose-shared-connection';
global.db = mongoose.createConnection(uri);

const routes = require('./routes');

const app = express();
app.get('/', routes.home);
app.get('/insert', routes.insert);
app.get('/name', routes.modelName);

app.listen(8000, function() {
  console.log('listening on http://localhost:8000');
});

Per the package.json file, it looks like the coding standards for this repo indicate that single quotes should be used instead of double quotes?


@@ -126,14 +126,18 @@ module.exports = function(config, auth, storage) {
   })

   // Search
-  app.get('/-/search/:anything', function(req, res, next) {
+  app.get('/-/search/:anything', can('access'), function(req, res, next) {
     var results = Search.query(req.params.anything)
     var packages = []

     var getData = function(i) {
       storage.get_package(results[i].ref, function(err, entry) {
         if (!err && entry) {
-          packages.push(entry.versions[entry['dist-tags'].latest])
+          auth.allow_access(entry.name, req.remote_user, function(err, allowed) { // TODO: This may cause performance issue?
+            if (err || !allowed) return
+
+            packages.push(entry.versions[entry['dist-tags'].latest])
+          })
         }

         if (i >= results.length - 1) {

var async         = require('async')
var bodyParser    = require('body-parser')
var Cookies       = require('cookies')
var express       = require('express')
var fs            = require('fs')
var Handlebars    = require('handlebars')
var renderReadme  = require('render-readme')
var Search        = require('./search')
var Middleware    = require('./middleware')
var match         = Middleware.match
var validate_name = Middleware.validate_name
var validate_pkg  = Middleware.validate_package

module.exports = function(config, auth, storage) {
  var app = express.Router()
  var can = Middleware.allow(auth)

  // validate all of these params as a package name
  // this might be too harsh, so ask if it causes trouble
  app.param('package',  validate_pkg)
  app.param('filename', validate_name)
  app.param('version',  validate_name)
  app.param('anything', match(/.*/))

  app.use(Cookies.express())
  app.use(bodyParser.urlencoded({ extended: false }))
  app.use(auth.cookie_middleware())
  app.use(function(req, res, next) {
    // disable loading in frames (clickjacking, etc.)
    res.header('X-Frame-Options', 'deny')
    next()
  })

  Search.configureStorage(storage)

  Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('./GUI/entry.hbs'), 'utf8'))

  if(config.web && config.web.template) {
    var template = Handlebars.compile(fs.readFileSync(config.web.template, 'utf8'));
  }
  else {
    var template = Handlebars.compile(fs.readFileSync(require.resolve('./GUI/index.hbs'), 'utf8'))
  }
  app.get('/', function(req, res, next) {
    var base = config.url_prefix
             ? config.url_prefix.replace(/\/$/, '')
             : req.protocol + '://' + req.get('host')
    res.setHeader('Content-Type', 'text/html')

    storage.get_local(function(err, packages) {
      if (err) throw err // that function shouldn't produce any
      async.filterSeries(packages, function(package, cb) {
        auth.allow_access(package.name, req.remote_user, function(err, allowed) {
          setImmediate(function () {
            if (err) {
              cb(null, false);
            } else {
              cb(err, allowed)
            }
          })
        })
      }, function(err, packages) {
        if (err) throw err
        packages.sort(function(p1, p2) {
          if (p1.name < p2.name) {
            return -1;
          }
          else {
            return 1;
          }
        });

        next(template({
          name:       config.web && config.web.title ? config.web.title : 'Verdaccio',
          tagline:    config.web && config.web.tagline ? config.web.tagline : '',
          packages:   packages,
          baseUrl:    base,
          username:   req.remote_user.name,
        }))
      })
    })
  })

  // Static
  app.get('/-/static/:filename', function(req, res, next) {
    var file = __dirname + '/static/' + req.params.filename
    res.sendFile(file, function(err) {
      if (!err) return
      if (err.status === 404) {
        next()
      } else {
        next(err)
      }
    })
  })

  app.get('/-/logo', function(req, res, next) {
    res.sendFile( config.web && config.web.logo
                ? config.web.logo
                : __dirname + '/static/logo-sm.png' )
  })

  app.post('/-/login', function(req, res, next) {
    auth.authenticate(req.body.user, req.body.pass, function(err, user) {
      if (!err) {
        req.remote_user = user
        //res.cookies.set('token', auth.issue_token(req.remote_user))

        var str = req.body.user + ':' + req.body.pass
        res.cookies.set('token', auth.aes_encrypt(str).toString('base64'))
      }

      var base = config.url_prefix
               ? config.url_prefix.replace(/\/$/, '')
               : req.protocol + '://' + req.get('host')
      res.redirect(base)
    })
  })

  app.post('/-/logout', function(req, res, next) {
    var base = config.url_prefix
             ? config.url_prefix.replace(/\/$/, '')
             : req.protocol + '://' + req.get('host')
    res.cookies.set('token', '')
    res.redirect(base)
  })

  // Search
  app.get('/-/search/:anything', function(req, res, next) {
    var results = Search.query(req.params.anything)
    var packages = []

    var getData = function(i) {
      storage.get_package(results[i].ref, function(err, entry) {
        if (!err && entry) {
          packages.push(entry.versions[entry['dist-tags'].latest])
        }

        if (i >= results.length - 1) {
          next(packages)
        } else {
          getData(i + 1)
        }
      })
    }

    if (results.length) {
      getData(0)
    } else {
      next([])
    }
  })

  app.get('/-/readme(/@:scope?)?/:package/:version?', can('access'), function(req, res, next) {
    var packageName = req.params.package;
    if (req.params.scope) packageName = "@"+ req.params.scope + "/" + packageName;
    storage.get_package(packageName, {req: req}, function(err, info) {
      if (err) return next(err)
      next( renderReadme(info.readme || 'ERROR: No README data found!') )
    })
  })
  return app
}


@@ -30,17 +30,14 @@ define(['layoutManager', 'cardBuilder', 'apphost', 'imageLoader', 'loading', 'sc
             recordingItems.classList.add('vertical-wrap');
         }

-        var supportsImageAnalysis = appHost.supports('imageanalysis');
-        var cardLayout = appHost.preferVisualCards || supportsImageAnalysis;
-        cardLayout = false;
         recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({
             items: recordings,
             shape: enableScrollX() ? 'autooverflow' : 'auto',
             showTitle: true,
             showParentTitle: true,
             coverImage: true,
-            cardLayout: cardLayout,
-            centerText: !cardLayout,
+            cardLayout: appHost.preferVisualCards,
+            centerText: !appHost.preferVisualCards,
             allowBottomPadding: !enableScrollX(),
             preferThumb: 'auto'
         }, cardOptions || {}));

define(['layoutManager', 'cardBuilder', 'apphost', 'imageLoader', 'loading', 'scripts/livetvcomponents', 'emby-button', 'emby-itemscontainer'], function (layoutManager, cardBuilder, appHost, imageLoader, loading) {
    'use strict';

    function enableScrollX() {
        return !layoutManager.desktop;
    }

    function renderRecordings(elem, recordings, cardOptions) {
        if (recordings.length) {
            elem.classList.remove('hide');
        } else {
            elem.classList.add('hide');
        }

        var recordingItems = elem.querySelector('.recordingItems');

        if (enableScrollX()) {
            recordingItems.classList.add('scrollX');

            if (layoutManager.tv) {
                recordingItems.classList.add('smoothScrollX');
            }

            recordingItems.classList.add('hiddenScrollX');
            recordingItems.classList.remove('vertical-wrap');
        } else {
            recordingItems.classList.remove('scrollX');
            recordingItems.classList.remove('smoothScrollX');
            recordingItems.classList.remove('hiddenScrollX');
            recordingItems.classList.add('vertical-wrap');
        }

        var supportsImageAnalysis = appHost.supports('imageanalysis');
        var cardLayout = appHost.preferVisualCards || supportsImageAnalysis;
        cardLayout = false;
        recordingItems.innerHTML = cardBuilder.getCardsHtml(Object.assign({
            items: recordings,
            shape: enableScrollX() ? 'autooverflow' : 'auto',
            showTitle: true,
            showParentTitle: true,
            coverImage: true,
            cardLayout: cardLayout,
            centerText: !cardLayout,
            allowBottomPadding: !enableScrollX(),
            preferThumb: 'auto'
        }, cardOptions || {}));
        imageLoader.lazyChildren(recordingItems);
    }

    function getBackdropShape() {
        return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
    }

    function renderActiveRecordings(context, promise) {
        promise.then(function (result) {
            renderRecordings(context.querySelector('#activeRecordings'), result.Items, {
                shape: enableScrollX() ? 'autooverflow' : 'auto',
                defaultShape: getBackdropShape(),
                showParentTitle: false,
                showParentTitleOrTitle: true,
                showTitle: false,
                showAirTime: true,
                showAirEndTime: true,
                showChannelName: true,
                coverImage: true,
                overlayText: false,
                overlayMoreButton: true
            });
        });
    }

    function renderTimers(context, timers, options) {
        LiveTvHelpers.getTimersHtml(timers, options).then(function (html) {
            var elem = context;

            if (html) {
                elem.classList.remove('hide');
            } else {
                elem.classList.add('hide');
            }

            elem.querySelector('.recordingItems').innerHTML = html;
            imageLoader.lazyChildren(elem);
        });
    }

    function renderUpcomingRecordings(context, promise) {
        promise.then(function (result) {
            renderTimers(context.querySelector('#upcomingRecordings'), result.Items);
            loading.hide();
        });
    }

    return function (view, params, tabContent) {
        var activeRecordingsPromise;
        var upcomingRecordingsPromise;
        var self = this;
        tabContent.querySelector('#upcomingRecordings .recordingItems').addEventListener('timercancelled', function () {
            self.preRender();
            self.renderTab();
        });

        self.preRender = function () {
            activeRecordingsPromise = ApiClient.getLiveTvRecordings({
                UserId: Dashboard.getCurrentUserId(),
                IsInProgress: true,
                Fields: 'CanDelete,PrimaryImageAspectRatio,BasicSyncInfo',
                EnableTotalRecordCount: false,
                EnableImageTypes: 'Primary,Thumb,Backdrop'
            });
            upcomingRecordingsPromise = ApiClient.getLiveTvTimers({
                IsActive: false,
                IsScheduled: true
            });
        };

        self.renderTab = function () {
            loading.show();
            renderActiveRecordings(tabContent, activeRecordingsPromise);
            renderUpcomingRecordings(tabContent, upcomingRecordingsPromise);
        };
    };
});

Logic changed: `cardLayout = false;` overrides expression with `appHost.preferVisualCards`.


@@ -43,7 +43,8 @@ module.exports = function(config, auth, storage) {
     const base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix);
     let webPage = template
       .replace(/ToReplaceByVerdaccio/g, base)
-      .replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : WEB_TITLE);
+      .replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : WEB_TITLE)
+      .replace(/ToReplaceByScope/g, _.get(config, 'web.scope') ? config.web.scope : '');

     res.setHeader('Content-Type', 'text/html');

import express from 'express';
import _ from 'lodash';
import fs from 'fs';
import Search from '../../lib/search';
import * as Utils from '../../lib/utils';
import {WEB_TITLE} from '../../lib/constants';

const {securityIframe} = require('../middleware');
/* eslint new-cap:off */
const router = express.Router();
const env = require('../../config/env');
const template = fs.readFileSync(`${env.DIST_PATH}/index.html`).toString();
const spliceURL = require('../../utils/string').spliceURL;

module.exports = function(config, auth, storage) {
  Search.configureStorage(storage);

  router.use(auth.webUIJWTmiddleware());
  router.use(securityIframe);

  // Static
  router.get('/-/static/:filename', function(req, res, next) {
    const file = `${env.APP_ROOT}/static/${req.params.filename}`;
    res.sendFile(file, function(err) {
      if (!err) {
        return;
      }
      if (err.status === 404) {
        next();
      } else {
        next(err);
      }
    });
  });

  router.get('/-/verdaccio/logo', function(req, res) {
    const installPath = _.get(config, 'url_prefix', '');

    res.send(_.get(config, 'web.logo') || spliceURL(installPath, '/-/static/logo.png'));
  });

  router.get('/', function(req, res) {
    const base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix);
    let webPage = template
      .replace(/ToReplaceByVerdaccio/g, base)
      .replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : WEB_TITLE);

    res.setHeader('Content-Type', 'text/html');

    res.send(webPage);
  });

  return router;
};

I think here force to use either scope or title is not good. A user might want both.


@@ -3,11 +3,12 @@
 const Aspect = require('./operation').Aspect;
 const OperationBase = require('./operation').OperationBase;
 const resolveReadPreference = require('../utils').resolveReadPreference;
+const serverLacksFeature = require('../utils').serverLacksFeature;
 const ReadConcern = require('../read_concern');
 const WriteConcern = require('../write_concern');
 const maxWireVersion = require('../core/utils').maxWireVersion;
 const commandSupportsReadConcern = require('../core/sessions').commandSupportsReadConcern;
-const MongoError = require('../error').MongoError;
+const MongoError = require('../core').MongoError;

 const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;

'use strict';

const Aspect = require('./operation').Aspect;
const OperationBase = require('./operation').OperationBase;
const resolveReadPreference = require('../utils').resolveReadPreference;
const ReadConcern = require('../read_concern');
const WriteConcern = require('../write_concern');
const maxWireVersion = require('../core/utils').maxWireVersion;
const commandSupportsReadConcern = require('../core/sessions').commandSupportsReadConcern;
const MongoError = require('../error').MongoError;

const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;

class CommandOperationV2 extends OperationBase {
  constructor(parent, options, operationOptions) {
    super(options);

    this.ns = parent.s.namespace.withCollection('$cmd');
    this.readPreference = resolveReadPreference(parent, this.options);
    this.readConcern = resolveReadConcern(parent, this.options);
    this.writeConcern = resolveWriteConcern(parent, this.options);
    this.explain = false;

    if (operationOptions && typeof operationOptions.fullResponse === 'boolean') {
      this.fullResponse = true;
    }

    // TODO: A lot of our code depends on having the read preference in the options. This should
    //       go away, but also requires massive test rewrites.
    this.options.readPreference = this.readPreference;

    // TODO(NODE-2056): make logger another "inheritable" property
    if (parent.s.logger) {
      this.logger = parent.s.logger;
    } else if (parent.s.db && parent.s.db.logger) {
      this.logger = parent.s.db.logger;
    }
  }

  executeCommand(server, cmd, callback) {
    // TODO: consider making this a non-enumerable property
    this.server = server;

    const options = this.options;
    const serverWireVersion = maxWireVersion(server);
    const inTransaction = this.session && this.session.inTransaction();

    if (this.readConcern && commandSupportsReadConcern(cmd) && !inTransaction) {
      Object.assign(cmd, { readConcern: this.readConcern });
    }

    if (options.collation && serverWireVersion < SUPPORTS_WRITE_CONCERN_AND_COLLATION) {
      callback(
        new MongoError(
          `Server ${server.name}, which reports wire version ${serverWireVersion}, does not support collation`
        )
      );
      return;
    }

    if (serverWireVersion >= SUPPORTS_WRITE_CONCERN_AND_COLLATION) {
      if (this.writeConcern && this.hasAspect(Aspect.WRITE_OPERATION)) {
        Object.assign(cmd, { writeConcern: this.writeConcern });
      }

      if (options.collation && typeof options.collation === 'object') {
        Object.assign(cmd, { collation: options.collation });
      }
    }

    if (typeof options.maxTimeMS === 'number') {
      cmd.maxTimeMS = options.maxTimeMS;
    }

    if (typeof options.comment === 'string') {
      cmd.comment = options.comment;
    }

    if (this.logger && this.logger.isDebug()) {
      this.logger.debug(`executing command ${JSON.stringify(cmd)} against ${this.ns}`);
    }

    server.command(this.ns.toString(), cmd, this.options, (err, result) => {
      if (err) {
        callback(err, null);
        return;
      }

      if (this.fullResponse) {
        callback(null, result);
        return;
      }

      callback(null, result.result);
    });
  }
}

function resolveWriteConcern(parent, options) {
  return WriteConcern.fromOptions(options) || parent.writeConcern;
}

function resolveReadConcern(parent, options) {
  return ReadConcern.fromOptions(options) || parent.readConcern;
}

module.exports = CommandOperationV2;

let's actually use the direct include: `require('../core/error').MongoError;`


@@ -7,20 +7,32 @@
  * @private
  */
 function getIncompleteReason(checkData, messages) {
+	function getDefaultMsg(messages) {
+		if (messages.incomplete && messages.incomplete.default) {
+			// fall back to the default message if no reason specified
+			return messages.incomplete.default;
+		} else {
+			// TODO: localize it
+			return 'aXe couldn\'t tell the reason. Time to break out the element inspector!';
+		}
+	}
 	if (checkData && checkData.missingData) {
-		var missingReason;
 		try {
-			missingReason = checkData.missingData[0].reason;
-		} finally {
+			var msg = messages.incomplete[checkData.missingData[0].reason];
+			if (!msg) {
+				throw new Error();
+			}
+			return msg;
+		} catch (e) {
 			if (typeof checkData.missingData === 'string')  {
-				missingReason = checkData.missingData;
+				// return a string with the appropriate reason
+				return messages.incomplete[checkData.missingData];
+			} else {
+				return getDefaultMsg(messages);
 			}
 		}
-		// return a function with the appropriate message
-		return messages.incomplete[missingReason];
 	} else {
-		// fall back to the default message if no reason specified
-		return messages.incomplete.default;
+		return getDefaultMsg(messages);
 	}
 }
 /**

/**
 * Construct incomplete message from check.data
 * @param  {Object} checkData Check result with reason specified
 * @param  {Object} messages Source data object with message options
 * @return  {String}
 * @private
 */
function getIncompleteReason(checkData, messages) {
	if (checkData && checkData.missingData) {
		var missingReason;
		try {
			missingReason = checkData.missingData[0].reason;
		} finally {
			if (typeof checkData.missingData === 'string')  {
				missingReason = checkData.missingData;
			}
		}
		// return a function with the appropriate message
		return messages.incomplete[missingReason];
	} else {
		// fall back to the default message if no reason specified
		return messages.incomplete.default;
	}
}
/**
 * Extend checksData with the correct result message
 * @param  {Object} checksData The check result data
 * @param  {Boolean} shouldBeTrue Result of pass/fail check run
 * @return {Function}
 * @private
 */
function extender(checksData, shouldBeTrue) {
	'use strict';
	return function (check) {
		var sourceData = checksData[check.id] || {};
		var messages = sourceData.messages || {};
		var data = Object.assign({}, sourceData);
		delete data.messages;
		if (check.result === undefined) {
			if (typeof messages.incomplete === 'object') {
				data.message = function() { return getIncompleteReason(check.data, messages); };
			} else {
				// fall back to string message
				data.message = messages.incomplete;
			}
		} else {
			data.message = check.result === shouldBeTrue ? messages.pass : messages.fail;
		}
		axe.utils.extendMetaData(check, data);
	};
}

/**
 * Publish metadata from axe._audit.data
 * @param  {RuleResult} result Result to publish to
 * @private
 */
axe.utils.publishMetaData = function (ruleResult) {
	'use strict';

	var checksData = axe._audit.data.checks || {};
	var rulesData = axe._audit.data.rules || {};
	var rule = axe.utils.findBy(axe._audit.rules, 'id', ruleResult.id) || {};

	ruleResult.tags = axe.utils.clone(rule.tags || []);

	var shouldBeTrue = extender(checksData, true);
	var shouldBeFalse = extender(checksData, false);
	ruleResult.nodes.forEach(function (detail) {
		detail.any.forEach(shouldBeTrue);
		detail.all.forEach(shouldBeTrue);
		detail.none.forEach(shouldBeFalse);
	});
	axe.utils.extendMetaData(ruleResult, axe.utils.clone(rulesData[ruleResult.id] || {}));
};

This string could be anything. Feel free to suggest alternatives if it's not helpful enough / too cheeky.


@@ -82,9 +82,9 @@ class Button extends Component {
 				aria-controls={ ariaControls }
 				role={ 'a' === SemanticButton ? 'button' : undefined }
 			>
-				{ icon && icon }
+				{ icon }
 				{ children }
-				{ trailingIcon && trailingIcon }
+				{ trailingIcon }
 			</SemanticButton>
 		);
 	}

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

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';
/**
 * WordPress dependencies
 */
import { Component, createRef } from '@wordpress/element';
/**
 * Internal dependencies
 */
import { MDCRipple } from '../material-components';

class Button extends Component {
	constructor( props ) {
		super( props );
		this.buttonRef = createRef();
	}

	componentDidMount() {
		new MDCRipple( this.buttonRef.current );
	}

	render() {
		const {
			onClick,
			children,
			href,
			text,
			className,
			danger,
			disabled,
			target,
			id,
			icon,
			trailingIcon,
			ariaHaspopup,
			ariaExpanded,
			ariaControls,
		} = this.props;

		// Use a button if disabled, even if a href is provided to ensure expected behavior.
		const SemanticButton = ( href && ! disabled ) ? 'a' : 'button';

		return (
			<SemanticButton
				className={ classnames(
					'mdc-button',
					className,
					{
						'mdc-button--raised': ! text,
						'mdc-button--danger': danger,
					}
				) }
				onClick={ onClick }
				href={ disabled ? undefined : href }
				ref={ this.buttonRef }
				disabled={ !! disabled }
				target={ target || '_self' }
				id={ id }
				aria-haspopup={ ariaHaspopup }
				aria-expanded={ ariaExpanded }
				aria-controls={ ariaControls }
				role={ 'a' === SemanticButton ? 'button' : undefined }
			>
				{ icon && icon }
				{ children }
				{ trailingIcon && trailingIcon }
			</SemanticButton>
		);
	}
}

Button.propTypes = {
	onClick: PropTypes.func,
	children: PropTypes.string.isRequired,
	href: PropTypes.string,
	text: PropTypes.bool,
	className: PropTypes.string,
	danger: PropTypes.bool,
	disabled: PropTypes.bool,
	icon: PropTypes.element,
	trailingIcon: PropTypes.element,
	ariaHaspopup: PropTypes.string,
	ariaExpanded: PropTypes.bool,
	ariaControls: PropTypes.string,
};

Button.defaultProps = {
	onClick: null,
	href: null,
	text: false,
	className: '',
	danger: false,
	disabled: false,
	icon: null,
	trailingIcon: null,
	ariaHaspopup: '',
	ariaExpanded: false,
	ariaControls: '',
};

export default Button;

Why were these changed? I'm not saying this is necessarily a problem, but as far as I remember JSX behaves slightly different from regular JS logic for those types of comparisons - there were cases like this one before which resulted in unexpected behavior. Are we 100% sure removing these checks won't render something like `undefined` or `0` or `false` out as a string in the HTML?


@@ -1,11 +1,11 @@
 module.exports = {
-	hash: '964177e31d959b03bc63f11216c61e3e', files: {
-		'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
-		'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
-		'katex/fonts/KaTeX_Main-Regular.woff2': { data: require('./katex/fonts/KaTeX_Main-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
-		'katex/fonts/KaTeX_Math-Italic.woff2': { data: require('./katex/fonts/KaTeX_Math-Italic.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
-		'katex/fonts/KaTeX_Size1-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size1-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
-		'katex/fonts/KaTeX_Size2-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size2-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
-		'katex/katex.css': { data: require('./katex/katex.css.base64.js'), mime: 'text/css', encoding: 'base64' },
-	},
-};
+hash:"964177e31d959b03bc63f11216c61e3e", files: {
+'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
+'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
+'katex/fonts/KaTeX_Main-Regular.woff2': { data: require('./katex/fonts/KaTeX_Main-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
+'katex/fonts/KaTeX_Math-Italic.woff2': { data: require('./katex/fonts/KaTeX_Math-Italic.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
+'katex/fonts/KaTeX_Size1-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size1-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
+'katex/fonts/KaTeX_Size2-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size2-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
+'katex/katex.css': { data: require('./katex/katex.css.base64.js'), mime: 'text/css', encoding: 'base64' },
+}
+};

module.exports = {
	hash: '964177e31d959b03bc63f11216c61e3e', files: {
		'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
		'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
		'katex/fonts/KaTeX_Main-Regular.woff2': { data: require('./katex/fonts/KaTeX_Main-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
		'katex/fonts/KaTeX_Math-Italic.woff2': { data: require('./katex/fonts/KaTeX_Math-Italic.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
		'katex/fonts/KaTeX_Size1-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size1-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
		'katex/fonts/KaTeX_Size2-Regular.woff2': { data: require('./katex/fonts/KaTeX_Size2-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
		'katex/katex.css': { data: require('./katex/katex.css.base64.js'), mime: 'text/css', encoding: 'base64' },
	},
};

Note to reviewer: I didn't touch this file; these lines were added automatically when I installed `react-native-quick-actions` via npm and linked the project in xcode (as described in the OP of this PR)


@@ -114,7 +114,10 @@ const baseResolvers = {
 			return;
 		}

-		yield fetchGetURLChannelsStore.actions.fetchGetURLChannels( accountID, clientID );
+		const { error } = yield fetchGetURLChannelsStore.actions.fetchGetURLChannels( accountID, clientID );
+		if ( error ) {
+			yield errorStoreActions.receiveError( error, 'getURLChannels', [ accountID, clientID ] );
+		}
 	},
 };

/**
 * `modules/adsense` data store: URL channels.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { STORE_NAME } from './constants';
import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store';
import { actions as errorStoreActions } from '../../../googlesitekit/data/create-error-store';

const fetchGetURLChannelsStore = createFetchStore( {
	baseName: 'getURLChannels',
	controlCallback: ( { accountID, clientID } ) => {
		return API.get( 'modules', 'adsense', 'urlchannels', { accountID, clientID }, {
			useCache: false,
		} );
	},
	reducerCallback: ( state, urlchannels, { accountID, clientID } ) => {
		return {
			...state,
			urlchannels: {
				...state.urlchannels,
				[ `${ accountID }::${ clientID }` ]: [ ...urlchannels ],
			},
		};
	},
	argsToParams: ( accountID, clientID ) => {
		return { accountID, clientID };
	},
	validateParams: ( { accountID, clientID } = {} ) => {
		invariant( accountID, 'accountID is required.' );
		invariant( clientID, 'clientID is required.' );
	},
} );

// Actions
const RESET_URLCHANNELS = 'RESET_URLCHANNELS';

const baseInitialState = {
	urlchannels: {},
};

const baseActions = {
	*resetURLChannels() {
		const { dispatch } = yield Data.commonActions.getRegistry();

		yield {
			payload: {},
			type: RESET_URLCHANNELS,
		};

		yield errorStoreActions.clearErrors( 'getURLChannels' );

		return dispatch( STORE_NAME )
			.invalidateResolutionForStoreSelector( 'getURLChannels' );
	},
};

const baseReducer = ( state, { type } ) => {
	switch ( type ) {
		case RESET_URLCHANNELS: {
			const {
				siteStatus,
				siteSetupComplete,
			} = state.savedSettings || {};
			return {
				...state,
				urlchannels: initialState.urlchannels,
				settings: {
					...( state.settings || {} ),
					siteStatus,
					siteSetupComplete,
				},
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getURLChannels( accountID, clientID ) {
		if ( undefined === accountID || undefined === clientID ) {
			return;
		}

		const registry = yield Data.commonActions.getRegistry();
		const existingURLChannels = registry.select( STORE_NAME ).getURLChannels( accountID, clientID );
		if ( existingURLChannels ) {
			return;
		}

		yield fetchGetURLChannelsStore.actions.fetchGetURLChannels( accountID, clientID );
	},
};

const baseSelectors = {
	/**
	 * Gets all Google AdSense URL channels for this account and client.
	 *
	 * @since 1.9.0
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The AdSense Account ID to fetch URL channels for.
	 * @param {string} clientID  The AdSense Client ID to fetch URL channels for.
	 * @return {(Array.<Object>|undefined)} An array of AdSense URL channels; `undefined` if not loaded.
	 */
	getURLChannels( state, accountID, clientID ) {
		if ( undefined === accountID || undefined === clientID ) {
			return undefined;
		}

		return state.urlchannels[ `${ accountID }::${ clientID }` ];
	},
};

const store = Data.combineStores(
	fetchGetURLChannelsStore,
	{
		initialState: baseInitialState,
		actions: baseActions,
		reducer: baseReducer,
		resolvers: baseResolvers,
		selectors: baseSelectors,
	}
);

export const initialState = store.initialState;
export const actions = store.actions;
export const controls = store.controls;
export const reducer = store.reducer;
export const resolvers = store.resolvers;
export const selectors = store.selectors;

export default store;

@eugene-manuilov Isn't this already taken care of by `fetchGetURLChannels` via `createFetchStore`? Why is the extra `receiveError` call needed here? (This was already in the IB, but just struck me while reviewing here.)


@@ -1,13 +1,24 @@
 /* global aria, axe, dom */
-function findDomNode(node, functor) {
-	if (functor(node)) {
-		return node;
+const idRefsRegex = /^idrefs?$/;
+
+function cacheIdRefs(node, refAttrs) {
+	if (node.hasAttribute) {
+		if (node.nodeName.toUpperCase() === 'LABEL' && node.hasAttribute('for')) {
+			axe._cache.idRefs[node.getAttribute('for')] = 1;
+		}
+
+		refAttrs
+			.filter(attr => node.hasAttribute(attr))
+			.forEach(attr => {
+				const attrValue = node.getAttribute(attr);
+				axe.utils.tokenList(attrValue).forEach(id => {
+					axe._cache.idRefs[id] = 1;
+				});
+			});
 	}
+
 	for (let i = 0; i < node.children.length; i++) {
-		const out = findDomNode(node.children[i], functor);
-		if (out) {
-			return out;
-		}
+		cacheIdRefs(node.children[i], refAttrs);
 	}
 }

/* global aria, axe, dom */
function findDomNode(node, functor) {
	if (functor(node)) {
		return node;
	}
	for (let i = 0; i < node.children.length; i++) {
		const out = findDomNode(node.children[i], functor);
		if (out) {
			return out;
		}
	}
}

/**
 * Check that a DOM node is a reference in the accessibility tree
 * @param {Element} node
 * @returns {Boolean}
 */
aria.isAccessibleRef = function isAccessibleRef(node) {
	node = node.actualNode || node;
	let root = dom.getRootNode(node);
	root = root.documentElement || root; // account for shadow roots
	const id = node.id;

	// Get all idref(s) attributes on the lookup table
	const refAttrs = Object.keys(aria.lookupTable.attributes).filter(attr => {
		const { type } = aria.lookupTable.attributes[attr];
		return /^idrefs?$/.test(type);
	});

	// Find the first element that IDREF(S) the node
	let refElm = findDomNode(root, elm => {
		if (elm.nodeType !== 1) {
			// Elements only
			return;
		}
		if (
			elm.nodeName.toUpperCase() === 'LABEL' &&
			elm.getAttribute('for') === id
		) {
			return true;
		}
		// See if there are any aria attributes that reference the node
		return refAttrs
			.filter(attr => elm.hasAttribute(attr))
			.some(attr => {
				const attrValue = elm.getAttribute(attr);
				if (aria.lookupTable.attributes[attr].type === 'idref') {
					return attrValue === id;
				}
				return axe.utils.tokenList(attrValue).includes(id);
			});
	});
	return typeof refElm !== 'undefined';
};

what happens if my `for` value is an `empty` space? ;) `<label for=' '>Temp</label>`


@@ -17,6 +17,13 @@ const getHasListItem = (hasListItem, tagName, isListItemRole) => {
 	return hasListItem || (tagName === 'LI' && isListItemRole) || isListItemRole;
 };

+const getIsHidden = actualNode => {
+	return (
+		window.getComputedStyle(actualNode, null).getPropertyValue('display') ===
+		'none'
+	);
+};
+
 let base = {
 	badNodes: [],
 	isEmpty: true,

const ALLOWED_TAGS = [
	'STYLE',
	'META',
	'LINK',
	'MAP',
	'AREA',
	'SCRIPT',
	'DATALIST',
	'TEMPLATE'
];

const getIsListItemRole = (role, tagName) => {
	return role === 'listitem' || (tagName === 'LI' && !role);
};

const getHasListItem = (hasListItem, tagName, isListItemRole) => {
	return hasListItem || (tagName === 'LI' && isListItemRole) || isListItemRole;
};

let base = {
	badNodes: [],
	isEmpty: true,
	hasNonEmptyTextNode: false,
	hasListItem: false,
	liItemsWithRole: 0
};

let out = virtualNode.children.reduce((out, { actualNode }) => {
	/*eslint
		max-statements: ["error", 20]
		complexity: ["error", 11]
		*/
	const tagName = actualNode.nodeName.toUpperCase();

	if (actualNode.nodeType === 1) {
		if (!ALLOWED_TAGS.includes(tagName)) {
			const role = (actualNode.getAttribute('role') || '').toLowerCase();
			const isListItemRole = getIsListItemRole(role, tagName);

			out.hasListItem = getHasListItem(
				out.hasListItem,
				tagName,
				isListItemRole
			);

			if (isListItemRole) {
				out.isEmpty = false;
			}
			if (tagName === 'LI' && !isListItemRole) {
				out.liItemsWithRole++;
			}
			if (tagName !== 'LI' && !isListItemRole) {
				out.badNodes.push(actualNode);
			}
		}
	}
	if (actualNode.nodeType === 3) {
		if (actualNode.nodeValue.trim() !== '') {
			out.hasNonEmptyTextNode = true;
		}
	}

	return out;
}, base);

const virtualNodeChildrenOfTypeLi = virtualNode.children.filter(
	({ actualNode }) => {
		return actualNode.nodeName.toUpperCase() === 'LI';
	}
);

const allLiItemsHaveRole =
	out.liItemsWithRole > 0 &&
	virtualNodeChildrenOfTypeLi.length === out.liItemsWithRole;

if (out.badNodes.length) {
	this.relatedNodes(out.badNodes);
}

const isInvalidListItem = !(
	out.hasListItem ||
	(out.isEmpty && !allLiItemsHaveRole)
);
return isInvalidListItem || !!out.badNodes.length || out.hasNonEmptyTextNode;

DRY, worth extracting this method to axe.utils


@@ -10,6 +10,8 @@ export function assign(obj, props) {
 	return /** @type {O & P} */ (obj);
 }

+export const IS_NON_DIMENSIONAL = /^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;
+
 /**
  * Check if two objects have a different shape
  * @param {object} a

/**
 * Assign properties from `props` to `obj`
 * @template O, P The obj and props types
 * @param {O} obj The object to copy properties to
 * @param {P} props The object to copy properties from
 * @returns {O & P}
 */
export function assign(obj, props) {
	for (let i in props) obj[i] = props[i];
	return /** @type {O & P} */ (obj);
}

/**
 * Check if two objects have a different shape
 * @param {object} a
 * @param {object} b
 * @returns {boolean}
 */
export function shallowDiffers(a, b) {
	for (let i in a) if (i !== '__source' && !(i in b)) return true;
	for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true;
	return false;
}

Is this the regex that was suggested a while back? I am wondering if the increased repetition will impact performance.


@@ -5,7 +5,7 @@ setResolver(resolver);

 /* jshint ignore:start */
 mocha.setup({
-    timeout: 15000,
+    timeout: 25000,
     slow: 500
 });
 /* jshint ignore:end */

import resolver from './helpers/resolver';
import {setResolver} from 'ember-mocha';

setResolver(resolver);

/* jshint ignore:start */
mocha.setup({
    timeout: 15000,
    slow: 500
});
/* jshint ignore:end */

Were you having trouble with timeouts in general acceptance tests or only the editor test? It's possible to set timeouts on a per-test basis by using `this.timeout(25000)` within the `it()` function. I'd like to drop the global timeout in the future if possible rather than increase it - in some circumstances a failing test can have a knock-on effect which makes all later tests timeout so Travis can get hung up for a long time if no one notices and cancels the build.


@@ -1,4 +1,21 @@
+/**
+ * Utilities for the Tag Manager module.
+ *
+ * Site Kit by Google, Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './validation';
 export { default as getContainers } from './get-containers';
-export { default as isValidAccountID } from './is-valid-account-id';
-export { default as isValidContainerID } from './is-valid-container-id';
 export { default as tagMatchers } from './tag-matchers';

export { default as getContainers } from './get-containers';
export { default as isValidAccountID } from './is-valid-account-id';
export { default as isValidContainerID } from './is-valid-container-id';
export { default as tagMatchers } from './tag-matchers';

Thanks! Not sure why these were left around


@@ -27,7 +27,7 @@ const main = async url => {
 		// Inject and run axe-core
 		const handle = await page.evaluateHandle(`
 			// Inject axe source code
-			${axeCore.source}
+			var axe = ${axeCore.source}
 			// Run axe
 			axe.run()
 		`);

const puppeteer = require('puppeteer');
const axeCore = require('axe-core');
const { parse: parseURL } = require('url');
const assert = require('assert');

// Cheap URL validation
const isValidURL = input => {
	const u = parseURL(input);
	return u.protocol && u.host;
};

// node axe-puppeteer.js <url>
const url = process.argv[2];
assert(isValidURL(url), 'Invalid URL');

const main = async url => {
	let browser;
	let results;
	try {
		// Setup Puppeteer
		browser = await puppeteer.launch();

		// Get new page
		const page = await browser.newPage();
		await page.goto(url);

		// Inject and run axe-core
		const handle = await page.evaluateHandle(`
			// Inject axe source code
			${axeCore.source}
			// Run axe
			axe.run()
		`);

		// Get the results from `axe.run()`.
		results = await handle.jsonValue();
		// Destroy the handle & return axe results.
		await handle.dispose();
	} catch (err) {
		// Ensure we close the puppeteer connection when possible
		if (browser) {
			await browser.close();
		}

		// Re-throw
		throw err;
	}

	await browser.close();
	return results;
};

main(url)
	.then(results => {
		console.log(results);
	})
	.catch(err => {
		console.error('Error running axe-core:', err.message);
		process.exit(1);
	});

Why is this necessary? I don't think I understand what's happening here.


@@ -34,7 +34,9 @@ import { isValidStringsOrObjects } from '../../../util/report-validation';
 export function isValidMetrics( metrics ) {
 	return isValidStringsOrObjects( metrics, ( metric ) => {
 		const validExpression = metric.hasOwnProperty( 'expression' ) && typeof metric.expression === 'string';
-		const validAlias = metric.hasOwnProperty( 'alias' ) && typeof metric.alias === 'string';
+
+		// 'alias' is optional; if provided, it must be a string.
+		const validAlias = ! metric.hasOwnProperty( 'alias' ) || typeof metric.alias === 'string';
 		return validExpression && validAlias;
 	} );
 }

/**
 * Reporting API validation utilities.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { isValidStringsOrObjects } from '../../../util/report-validation';

/**
 * Verifies that provided metrics match allowed values. Metrics can be a string,
 * an array of string, an array of objects or mix of strings and objects. Objects
 * must have "expression" and "alias" properties in order to be considered as valid.
 *
 * @since 1.13.0
 *
 * @param {string|string[]|Object|Object[]} metrics The metrics to check.
 * @return {boolean} TRUE if metrics are valid, otherwise FALSE.
 */
export function isValidMetrics( metrics ) {
	return isValidStringsOrObjects( metrics, ( metric ) => {
		const validExpression = metric.hasOwnProperty( 'expression' ) && typeof metric.expression === 'string';
		const validAlias = metric.hasOwnProperty( 'alias' ) && typeof metric.alias === 'string';
		return validExpression && validAlias;
	} );
}

/**
 * Verifies provided dimensions to make sure it matches allowed values. It can be a string,
 * array of strings, an object with "name" field, array of such objects or an array of strings
 * and objects.
 *
 * @since 1.13.0
 *
 * @param {string|string[]|Object|Object[]} dimensions The dimensions to check.
 * @return {boolean} TRUE if dimensions are valid, otherwise FALSE.
 */
export function isValidDimensions( dimensions ) {
	return isValidStringsOrObjects( dimensions, ( dimension ) => {
		return dimension.hasOwnProperty( 'name' ) && typeof dimension.name === 'string';
	} );
}

although the doc block should be updated to reflect this as it currently contradicts this: > Objects must have "expression" and "alias" properties in order to be considered as valid.


@@ -16,9 +16,16 @@ module.exports = function(environment) {
     APP: {
       // Here you can pass flags/options to your application instance
       // when it is created
+    },
+
+    'simple-auth': {
+
+    },
+    'ember-cli-mirage': {
+      enabled: false
     }
   };
-
+
 ENV.APP.manifest = {
   enabled: true,
   appcacheFile: "/manifest.appcache",

/* jshint node: true */

module.exports = function(environment) {
  var ENV = {
    modulePrefix: 'hospitalrun',
    environment: environment,
    baseURL: '/',
    locationType: 'hash', //Auto incompatible with google login right now
    EmberENV: {
      FEATURES: {
        // Here you can enable experimental features on an ember canary build
        // e.g. 'with-controller': true
      }
    },

    APP: {
      // Here you can pass flags/options to your application instance
      // when it is created
    }
  };

ENV.APP.manifest = {
  enabled: true,
  appcacheFile: "/manifest.appcache",
  excludePaths: ['index.html', 'someother.html'],
  includePaths: ['/'],
  network: ['api/'],
  showCreateDate: true
};

  if (environment === 'development') {
    // ENV.APP.LOG_RESOLVER = true;
    // ENV.APP.LOG_ACTIVE_GENERATION = true;
    // ENV.APP.LOG_TRANSITIONS = true;
    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
    // ENV.APP.LOG_VIEW_LOOKUPS = true;
  }

  if (environment === 'test') {
    // Testem prefers this...
    ENV.baseURL = '/';
    ENV.locationType = 'none';

    // keep test console output quieter
    ENV.APP.LOG_ACTIVE_GENERATION = false;
    ENV.APP.LOG_VIEW_LOOKUPS = false;

    ENV.APP.rootElement = '#ember-testing';
  }

  if (environment === 'production') {

  }

ENV.manifest = {
  enabled: true,
  appcacheFile: "/manifest.appcache",
  excludePaths: ['index.html', 'tests', 'dymo','robots.txt','testem.js'],
  //includePaths: ['/'],
  showCreateDate: true
};

  return ENV;
};


@@ -9,8 +9,8 @@ const expect = require('chai').expect,
   MongoError = require('../../../lib/core/error').MongoError,
   ReadPreference = require('../../../lib/core/topologies/read_preference');

-const rsWithPrimaryPath = f('%s/../spec/max-staleness/ReplicaSetWithPrimary', __dirname);
-const rsWithoutPrimaryPath = f('%s/../spec/max-staleness/ReplicaSetNoPrimary', __dirname);
+const rsWithPrimaryPath = f('%s/../../spec/max-staleness/ReplicaSetWithPrimary', __dirname);
+const rsWithoutPrimaryPath = f('%s/../../spec/max-staleness/ReplicaSetNoPrimary', __dirname);

 describe('Max Staleness', function() {
   describe('ReplicaSet without primary', function() {

'use strict';

const expect = require('chai').expect,
  p = require('path'),
  f = require('util').format,
  fs = require('fs'),
  Server = require('../../../lib/core/topologies/server'),
  ReplSetState = require('../../../lib/core/topologies/replset_state'),
  MongoError = require('../../../lib/core/error').MongoError,
  ReadPreference = require('../../../lib/core/topologies/read_preference');

const rsWithPrimaryPath = f('%s/../spec/max-staleness/ReplicaSetWithPrimary', __dirname);
const rsWithoutPrimaryPath = f('%s/../spec/max-staleness/ReplicaSetNoPrimary', __dirname);

describe('Max Staleness', function() {
  describe('ReplicaSet without primary', function() {
    fs
      .readdirSync(rsWithoutPrimaryPath)
      .filter(x => x.indexOf('.json') !== -1)
      .forEach(x => {
        it(p.basename(x, '.json'), function(done) {
          executeEntry(f('%s/%s', rsWithoutPrimaryPath, x), done);
        });
      });
  });

  describe('ReplicaSet with primary', function() {
    fs
      .readdirSync(rsWithPrimaryPath)
      .filter(x => x.indexOf('.json') !== -1)
      .filter(x => x.indexOf('LongHeartbeat2.jwson') === -1)
      .forEach(x => {
        it(p.basename(x, '.json'), function(done) {
          executeEntry(f('%s/%s', rsWithPrimaryPath, x), done);
        });
      });
  });
});

function convert(mode) {
  if (mode === undefined) return 'primary';
  if (mode.toLowerCase() === 'primarypreferred') return 'primaryPreferred';
  if (mode.toLowerCase() === 'secondarypreferred') return 'secondaryPreferred';
  return mode.toLowerCase();
}

function executeEntry(path, callback) {
  // Read and parse the json file
  var file = require(path);

  // Let's pick out the parts of the selection specification
  var error = file.error;
  var heartbeatFrequencyMS = file.heartbeatFrequencyMS || 10000;
  var inLatencyWindow = file.in_latency_window;
  var readPreference = file.read_preference;
  var topologyDescription = file.topology_description;

  try {
    // Create a Replset and populate it with dummy topology servers
    var replset = new ReplSetState({
      heartbeatFrequencyMS: heartbeatFrequencyMS
    });

    replset.topologyType = topologyDescription.type;
    // For each server add them to the state
    topologyDescription.servers.forEach(function(s) {
      var server = new Server({
        host: s.address.split(':')[0],
        port: parseInt(s.address.split(':')[1], 10)
      });

      // Add additional information
      if (s.avg_rtt_ms) server.lastIsMasterMS = s.avg_rtt_ms;
      if (s.lastUpdateTime) server.lastUpdateTime = s.lastUpdateTime;
      // Set the last write
      if (s.lastWrite) {
        server.lastWriteDate = s.lastWrite.lastWriteDate.$numberLong;
      }

      server.ismaster = {};
      if (s.tags) server.ismaster.tags = s.tags;
      if (s.maxWireVersion) server.ismaster.maxWireVersion = s.maxWireVersion;
      // Ensure the server looks connected
      server.isConnected = function() {
        return true;
      };

      if (s.type === 'RSSecondary') {
        server.ismaster.secondary = true;
        replset.secondaries.push(server);
      } else if (s.type === 'RSPrimary') {
        server.ismaster.ismaster = true;
        replset.primary = server;
      } else if (s.type === 'RSArbiter') {
        server.ismaster.arbiterOnly = true;
        replset.arbiters.push(server);
      }
    });

    // Calculate staleness
    replset.updateSecondariesMaxStaleness(heartbeatFrequencyMS);

    // Create read preference
    var rp = new ReadPreference(convert(readPreference.mode), readPreference.tag_sets, {
      maxStalenessSeconds: readPreference.maxStalenessSeconds
    });

    // Perform a pickServer
    var server = replset.pickServer(rp);
    var foundWindow = null;

    // We expect an error
    if (error) {
      expect(server).to.be.an.instanceof(MongoError);
      return callback(null, null);
    }

    // server should be in the latency window
    for (var i = 0; i < inLatencyWindow.length; i++) {
      var w = inLatencyWindow[i];

      if (server.name === w.address) {
        foundWindow = w;
        break;
      }
    }

    if (
      ['ReplicaSetNoPrimary', 'Primary', 'ReplicaSetWithPrimary'].indexOf(
        topologyDescription.type
      ) !== -1 &&
      inLatencyWindow.length === 0
    ) {
      if (server instanceof MongoError) {
        expect(server.message).to.equal('maxStalenessSeconds must be set to at least 90 seconds');
      } else {
        expect(server).to.be.null;
      }
    } else {
      expect(foundWindow).to.not.be.null;
    }
  } catch (err) {
    if (file.error) return callback(null, null);
    return callback(err, null);
  }

  callback(null, null);
}

Since we're here, can we use a template?


@@ -18,6 +18,7 @@ bitcore.encoding.BufferWriter = require('./lib/encoding/bufferwriter');
 bitcore.encoding.Varint = require('./lib/encoding/varint');

 // main bitcoin library
+bitcore.Unit = require('./lib/unit');
 bitcore.Address = require('./lib/address');
 bitcore.BIP32 = require('./lib/bip32');
 bitcore.Block = require('./lib/block');

var bitcore = module.exports;

// crypto
bitcore.crypto = {};
bitcore.crypto.BN = require('./lib/crypto/bn');
bitcore.crypto.ECDSA = require('./lib/crypto/ecdsa');
bitcore.crypto.Hash = require('./lib/crypto/hash');
bitcore.crypto.Random = require('./lib/crypto/random');
bitcore.crypto.Point = require('./lib/crypto/point');

// encoding
bitcore.encoding = {};
bitcore.encoding.Base58 = require('./lib/encoding/base58');
bitcore.encoding.Base58Check = require('./lib/encoding/base58check');
bitcore.encoding.BufferReader = require('./lib/encoding/bufferreader');
bitcore.encoding.BufferWriter = require('./lib/encoding/bufferwriter');
bitcore.encoding.Varint = require('./lib/encoding/varint');

// main bitcoin library
bitcore.Address = require('./lib/address');
bitcore.BIP32 = require('./lib/bip32');
bitcore.Block = require('./lib/block');
bitcore.Blockheader = require('./lib/blockheader');
bitcore.Networks = require('./lib/networks');
bitcore.Opcode = require('./lib/opcode');
bitcore.PrivateKey = require('./lib/privatekey');
bitcore.PublicKey = require('./lib/publickey');
bitcore.Script = require('./lib/script');
bitcore.Signature = require('./lib/signature');
bitcore.Transaction = require('./lib/transaction');
bitcore.Txin = require('./lib/txin');
bitcore.Txout = require('./lib/txout');

//dependencies, subject to change
bitcore.deps = {};
bitcore.deps.bnjs = require('bn.js');
bitcore.deps.bs58 = require('bs58');
bitcore.deps.Buffer = Buffer;
bitcore.deps.elliptic = require('elliptic');

//bitcore.scriptexec = require('lib/scriptexec');
//bitcore.tx = require('lib/tx');
//bitcore.txpartial = require('lib/txpartial');

//bitcore.bip70 = require('lib/bip70');


@@ -41,14 +41,48 @@ describe( 'SettingsApp', () => {
 		registry.dispatch( CORE_USER ).receiveGetAuthentication( { needsReauthentication: false } );
 		registry.dispatch( CORE_USER ).receiveConnectURL( 'test-url' );

-		provideModules( registry, [ {
-			slug: 'analytics',
-			name: 'Analytics',
-			active: true,
-			connected: true,
-			setupComplete: true,
-			SettingsEditComponent: () =>
edit
,
-		} ] );
+		provideModules( registry, [
+			{
+				slug: 'analytics',
+				name: 'Analytics',
+				active: true,
+				connected: true,
+				setupComplete: true,
+				SettingsEditComponent: () =>
edit
,
+			},
+			{
+				slug: 'optimize',
+				name: 'Optimize',
+				active: true,
+				connected: true,
+				setupComplete: true,
+				SettingsEditComponent: () =>
edit
,
+			},
+			{
+				slug: 'tagmanager',
+				name: 'Tag Manager',
+				active: true,
+				connected: true,
+				setupComplete: true,
+				SettingsEditComponent: () =>
edit
,
+			},
+			{
+				slug: 'adsense',
+				name: 'AdSense',
+				active: true,
+				connected: true,
+				setupComplete: true,
+				SettingsEditComponent: () =>
edit
,
+			},
+			{
+				slug: 'pagespeed-insights',
+				name: 'PageSpeed Insights',
+				active: true,
+				connected: true,
+				setupComplete: true,
+				SettingsEditComponent: () =>
edit
,
+			},
+		] );

 		global._googlesitekitLegacyData.modules.analytics = {
 			...global._googlesitekitLegacyData.modules.analytics,

/**
 * SettingsApp component tests.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import SettingsApp from './SettingsApp';
import { render, fireEvent, createTestRegistry, provideModules, waitFor } from '../../../../tests/js/test-utils';
import { CORE_USER } from '../../googlesitekit/datastore/user/constants';
import * as fixtures from '../../modules/analytics/datastore/__fixtures__';

describe( 'SettingsApp', () => {
	let registry;

	const features = [ 'storeErrorNotifications' ];
	const validResponse = {
		accountID: 'pub-12345678',
		clientID: 'ca-pub-12345678',
		useSnippet: true,
	};

	beforeEach( () => {
		global.location.hash = '';

		registry = createTestRegistry();
		registry.dispatch( CORE_USER ).receiveGetAuthentication( { needsReauthentication: false } );
		registry.dispatch( CORE_USER ).receiveConnectURL( 'test-url' );

		provideModules( registry, [ {
			slug: 'analytics',
			name: 'Analytics',
			active: true,
			connected: true,
			setupComplete: true,
			SettingsEditComponent: () =>
edit
,
		} ] );

		global._googlesitekitLegacyData.modules.analytics = {
			...global._googlesitekitLegacyData.modules.analytics,
			active: true,
			setupComplete: true,
		};
	} );

	it( 'should change location hash & DOM correctly when module accordion clicked and opened', async () => {
		fetchMock.getOnce(
			/^\/google-site-kit\/v1\/modules\/analytics\/data\/settings/,
			{ body: validResponse, status: 200 }
		);

		const { getByRole, findByRole } = render( <SettingsApp />, { features, registry } );

		fireEvent.click( getByRole( 'tab', { name: /analytics/i } ) );
		expect( global.location.hash ).toEqual( '#settings/analytics/view' );

		const analyticsPanel = await findByRole( 'tabpanel', { hidden: false } );
		expect( analyticsPanel ).toHaveAttribute( 'id', 'googlesitekit-settings-module__content--analytics' );
	} );

	it( 'should change location hash & DOM correctly when module accordion clicked and closed', async () => {
		const { getByRole, findByRole, queryByRole } = render( <SettingsApp />, { features, registry } );

		fireEvent.click( getByRole( 'tab', { name: /analytics/i } ) );

		const analyticsPanel = await findByRole( 'tabpanel' );
		expect( analyticsPanel ).toBeInTheDocument();

		expect( global.location.hash ).toEqual( '#settings/analytics/view' );

		fireEvent.click( getByRole( 'tab', { name: /analytics/i } ) );
		// A closed state should not be reflected in the hash as it is implied as default.
		expect( global.location.hash ).toEqual( '#settings' );

		await waitFor( () => {
			expect( queryByRole( 'tabpanel' ) ).toBeNull();
		} );
	} );

	it( 'should change location hash & DOM correctly when module is being edited', async () => {
		fetchMock.getOnce(
			/^\/google-site-kit\/v1\/modules\/analytics\/data\/accounts-properties-profiles/,
			{ body: fixtures.accountsPropertiesProfiles, status: 200 }
		);

		const { getByRole, findByRole, queryByTestID } = render( <SettingsApp />, { features, registry } );

		fireEvent.click( getByRole( 'tab', { name: /analytics/i } ) );

		const analyticsPanel = await findByRole( 'tabpanel' );
		expect( analyticsPanel ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /edit/i } ) );

		expect( global.location.hash ).toEqual( '#settings/analytics/edit' );

		await waitFor( () => {
			expect( queryByTestID( 'edit-component' ) ).toBeInTheDocument();
		} );
	} );

	it( 'should change location hash & DOM correctly when module is no longer being edited', async () => {
		fetchMock.getOnce(
			/^\/google-site-kit\/v1\/modules\/analytics\/data\/accounts-properties-profiles/,
			{ body: fixtures.accountsPropertiesProfiles, status: 200 }
		);

		const { getByRole, findByRole } = render( <SettingsApp />, { features, registry } );

		fireEvent.click( getByRole( 'tab', { name: /analytics/i } ) );

		const analyticsPanel = await findByRole( 'tabpanel' );
		expect( analyticsPanel ).toBeInTheDocument();

		fireEvent.click( getByRole( 'button', { name: /edit/i } ) );
		expect( global.location.hash ).toEqual( '#settings/analytics/edit' );

		fireEvent.click( getByRole( 'button', { name: /cancel/i } ) );
		expect( global.location.hash ).toEqual( '#settings/analytics/view' );

		const finalAnalyticsPanel = await findByRole( 'tabpanel' );
		expect( finalAnalyticsPanel ).toBeInTheDocument();
	} );

	it( 'should change location hash & DOM correctly when tab is clicked and changed', async () => {
		const { findByText, getAllByRole } = render( <SettingsApp />, { features, registry } );

		fireEvent.click( getAllByRole( 'tab' )[ 1 ] );
		expect( global.location.hash ).toEqual( '#connect' );

		const allConnected = await findByText( /congrats, you’ve connected all services/i );
		expect( allConnected ).toBeInTheDocument();

		fireEvent.click( getAllByRole( 'tab' )[ 2 ] );
		expect( global.location.hash ).toEqual( '#admin' );

		const pluginStatus = await findByText( /plugin status/i );
		expect( pluginStatus ).toBeInTheDocument();
	} );
} );

`setupComplete` is an invalid key as this structure uses the new schema.


@@ -2,10 +2,36 @@

 const expect = require('chai').expect;
 const { getSymbolFrom } = require('../tools/utils');
-const MongoNetworkError = require('../../src/error').MongoNetworkError;
+const { MongoNetworkError } = require('../../src/index');
+const {
+  PoolClosedError: MongoPoolClosedError,
+  WaitQueueTimeoutError: MongoWaitQueueTimeoutError
+} = require('../../src/cmap/errors');

-describe('MongoErrors', function () {
-  describe('MongoNetworkError', function () {
+describe('MongoErrors', () => {
+  // import errors as object
+  let errorClasses = Object.fromEntries(
+    Object.entries(require('../../src/index')).filter(([key]) => key.endsWith('Error'))
+  );
+  errorClasses = { ...errorClasses, MongoPoolClosedError, MongoWaitQueueTimeoutError };
+
+  for (const errorName in errorClasses) {
+    describe(errorName, () => {
+      it(`name should be read-only`, () => {
+        // Dynamically create error class with message
+        let error = new errorClasses[errorName]('generated by test');
+        // expect name property to be class name
+        expect(error).to.have.property('name', errorName);
+
+        try {
+          error.name = 'renamed by test';
+          expect(error).to.have.property('name', 'generated by test');
+        } catch (err) {}
+      });
+    });
+  }
+
+  describe('when MongoNetworkError is constructed', () => {
     it('should only define beforeHandshake symbol if boolean option passed in', function () {
       const errorWithOptionTrue = new MongoNetworkError('', { beforeHandshake: true });
       expect(getSymbolFrom(errorWithOptionTrue, 'beforeHandshake', false)).to.be.a('symbol');

'use strict';

const expect = require('chai').expect;
const { getSymbolFrom } = require('../tools/utils');
const MongoNetworkError = require('../../src/error').MongoNetworkError;

describe('MongoErrors', function () {
  describe('MongoNetworkError', function () {
    it('should only define beforeHandshake symbol if boolean option passed in', function () {
      const errorWithOptionTrue = new MongoNetworkError('', { beforeHandshake: true });
      expect(getSymbolFrom(errorWithOptionTrue, 'beforeHandshake', false)).to.be.a('symbol');

      const errorWithOptionFalse = new MongoNetworkError('', { beforeHandshake: false });
      expect(getSymbolFrom(errorWithOptionFalse, 'beforeHandshake', false)).to.be.a('symbol');

      const errorWithBadOption = new MongoNetworkError('', { beforeHandshake: 'not boolean' });
      expect(getSymbolFrom(errorWithBadOption, 'beforeHandshake', false)).to.be.an('undefined');

      const errorWithoutOption = new MongoNetworkError('');
      expect(getSymbolFrom(errorWithoutOption, 'beforeHandshake', false)).to.be.an('undefined');
    });
  });
});

this expect should be outside the try/catch block since you don't want to swallow the error thrown by the expect in the case of failure, otherwise looks good :)


@@ -15,6 +15,7 @@ export default AbstractModel.extend(DOBDays, PatientName, {
   bloodType: DS.attr('string'),
   clinic: DS.attr('string'),
   country: DS.attr('string'),
+  createOutpatientVisit: DS.attr('boolean'),
   dateOfBirth: DS.attr('date'),
   economicClassification: DS.attr('string'),
   email: DS.attr('string'),

import AbstractModel from 'hospitalrun/models/abstract';
import DOBDays from 'hospitalrun/mixins/dob-days';
import EmailValidation from 'hospitalrun/utils/email-validation';
import Ember from 'ember';
import DS from 'ember-data';
import PatientName from 'hospitalrun/mixins/patient-name';

export default AbstractModel.extend(DOBDays, PatientName, {
  admitted: DS.attr('boolean', { defaultValue: false }),
  additionalContacts: DS.attr(),
  address: DS.attr('string'),
  address2: DS.attr('string'),
  address3: DS.attr('string'),
  address4: DS.attr('string'),
  bloodType: DS.attr('string'),
  clinic: DS.attr('string'),
  country: DS.attr('string'),
  dateOfBirth: DS.attr('date'),
  economicClassification: DS.attr('string'),
  email: DS.attr('string'),
  expenses: DS.attr(),
  externalPatientId: DS.attr('string'),
  familySupport1: DS.attr('string'),
  familySupport2: DS.attr('string'),
  familySupport3: DS.attr('string'),
  familySupport4: DS.attr('string'),
  familySupport5: DS.attr('string'),
  friendlyId: DS.attr('string'),
  familyInfo: DS.attr(),
  firstName: DS.attr('string'),
  sex: DS.attr('string'),
  occupation: DS.attr('string'),
  history: DS.attr('string'),
  insurance: DS.attr('string'),
  lastName: DS.attr('string'),
  livingArrangement: DS.attr('string'),
  middleName: DS.attr('string'),
  notes: DS.attr('string'),
  otherIncome: DS.attr('string'),
  payments: DS.hasMany('payment', {
    async: true
  }),
  patientType: DS.attr('string'),
  parent: DS.attr('string'),
  paymentProfile: DS.belongsTo('price-profile', {
    async: false
  }),
  phone: DS.attr('string'),
  placeOfBirth: DS.attr('string'),
  referredDate: DS.attr('date'),
  referredBy: DS.attr('string'),
  religion: DS.attr('string'),
  socialActionTaken: DS.attr('string'),
  socialRecommendation: DS.attr('string'),
  status: DS.attr('string'),

  age: function() {
    var dob = this.get('dateOfBirth');
    return this.convertDOBToText(dob);
  }.property('dateOfBirth'),

  displayAddress: function() {
    var addressFields = this.getProperties('address', 'address2', 'address3', 'address4'),
      displayAddress = '';
    for (var prop in addressFields) {
      if (!Ember.isEmpty(addressFields[prop])) {
        if (!Ember.isEmpty(displayAddress)) {
          displayAddress += ', ';
        }
        displayAddress += addressFields[prop];
      }
    }
    return displayAddress;
  }.property('address', 'address2', 'address3', 'address4'),

  displayName: function() {
    return this.getPatientDisplayName(this);
  }.property('firstName', 'lastName', 'middleName'),

  displayPatientId: function() {
    return this.getPatientDisplayId(this);
  }.property('id', 'externalPatientId', 'friendlyId'),

  validations: {
    email: {
      format: {
        with: EmailValidation.emailRegex,
        allowBlank: true,
        message: 'please enter a valid email address'
      }
    },
    friendlyId: {
      presence: true
    },
    firstName: {
      presence: true
    },
    lastName: {
      presence: true
    }
  }

});

This attribute should not be here because we do not want to (or need to) save this value to the database.


@@ -6,6 +6,17 @@ let null_dict = null_dictionary || {}
 // eslint-disable-next-line no-undef
 assert_true(dict.X !== undefined && dict.doSomething !== undefined);  // Testing successful object creation.

+
+/*
+    It seems that Object.values(dict) is not supported on JSC for non-static fields, it would be interesting to
+    review this in the future.
+    verify_object_fields(Object.values(dict).length)
+ */
+
+// eslint-disable-next-line no-undef
+verify_object_fields(Object.keys(dict).length)
+
+
 null_dict.hello(true);  // Testing method call from object.
 null_dict.alo(true); // Testing method call from object <again>.

// eslint-disable-next-line no-undef,strict
let dict = dictionary || {}
// eslint-disable-next-line no-undef
let null_dict = null_dictionary || {}

// eslint-disable-next-line no-undef
assert_true(dict.X !== undefined && dict.doSomething !== undefined);  // Testing successful object creation.

null_dict.hello(true);  // Testing method call from object.
null_dict.alo(true); // Testing method call from object <again>.

dict.doSomething(28850);

/* Testing accessors. */
dict.X=666;

// eslint-disable-next-line no-undef
test_accessor(dict, 'X', 666);

assert_enumerate(JSON.stringify(dict))

/*
    Testing exception mechanism.
*/
try {
    dict.X = -1  // testing wrong value.
}catch(error){
    assert_exception(error.message)
}

You can create an issue for it and put it on the backlog so we don't forget it.


@@ -9,6 +9,10 @@ const databaseNamespace = require('./shared').databaseNamespace;
 const isTransactionCommand = require('../transactions').isTransactionCommand;
 const applySession = require('../sessions').applySession;

+function isClientEncryptionEnabled(server) {
+  return server && server.s && server.s.options && server.s.options.autoEncrypter;
+}
+
 function command(server, ns, cmd, options, callback) {
   if (typeof options === 'function') (callback = options), (options = {});
   options = options || {};

'use strict';

const Query = require('../connection/commands').Query;
const Msg = require('../connection/msg').Msg;
const MongoError = require('../error').MongoError;
const getReadPreference = require('./shared').getReadPreference;
const isSharded = require('./shared').isSharded;
const databaseNamespace = require('./shared').databaseNamespace;
const isTransactionCommand = require('../transactions').isTransactionCommand;
const applySession = require('../sessions').applySession;

function command(server, ns, cmd, options, callback) {
  if (typeof options === 'function') (callback = options), (options = {});
  options = options || {};

  if (cmd == null) {
    return callback(new MongoError(`command ${JSON.stringify(cmd)} does not return a cursor`));
  }

  const bson = server.s.bson;
  const pool = server.s.pool;
  const readPreference = getReadPreference(cmd, options);
  const shouldUseOpMsg = supportsOpMsg(server);
  const session = options.session;

  let clusterTime = server.clusterTime;
  let finalCmd = Object.assign({}, cmd);
  if (hasSessionSupport(server) && session) {
    if (
      session.clusterTime &&
      session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
    ) {
      clusterTime = session.clusterTime;
    }

    const err = applySession(session, finalCmd, options);
    if (err) {
      return callback(err);
    }
  }

  // if we have a known cluster time, gossip it
  if (clusterTime) {
    finalCmd.$clusterTime = clusterTime;
  }

  if (
    isSharded(server) &&
    !shouldUseOpMsg &&
    readPreference &&
    readPreference.preference !== 'primary'
  ) {
    finalCmd = {
      $query: finalCmd,
      $readPreference: readPreference.toJSON()
    };
  }

  const commandOptions = Object.assign(
    {
      command: true,
      numberToSkip: 0,
      numberToReturn: -1,
      checkKeys: false
    },
    options
  );

  // This value is not overridable
  commandOptions.slaveOk = readPreference.slaveOk();

  const cmdNs = `${databaseNamespace(ns)}.$cmd`;
  const message = shouldUseOpMsg
    ? new Msg(bson, cmdNs, finalCmd, commandOptions)
    : new Query(bson, cmdNs, finalCmd, commandOptions);

  const inTransaction = session && (session.inTransaction() || isTransactionCommand(finalCmd));
  const commandResponseHandler = inTransaction
    ? function(err) {
        if (
          !cmd.commitTransaction &&
          err &&
          err instanceof MongoError &&
          err.hasErrorLabel('TransientTransactionError')
        ) {
          session.transaction.unpinServer();
        }

        return callback.apply(null, arguments);
      }
    : callback;

  try {
    pool.write(message, commandOptions, commandResponseHandler);
  } catch (err) {
    commandResponseHandler(err);
  }
}

function hasSessionSupport(topology) {
  if (topology == null) return false;
  if (topology.description) {
    return topology.description.maxWireVersion >= 6;
  }

  return topology.ismaster == null ? false : topology.ismaster.maxWireVersion >= 6;
}

function supportsOpMsg(topologyOrServer) {
  const description = topologyOrServer.ismaster
    ? topologyOrServer.ismaster
    : topologyOrServer.description;

  if (description == null) {
    return false;
  }

  return description.maxWireVersion >= 6 && description.__nodejs_mock_server__ == null;
}

module.exports = command;

This could be follow-on work, but it would be nice to have `server.autoEncrypter` similar to `collection.readConcern` so we don't have to do this long check here.


@@ -1,5 +1,6 @@
 const allowedE2EModules = [
   'window',
+  'hyperformula*',
   'jasmine-co',
   'jest-matcher-utils',
   'html-parse-stringify',

const allowedE2EModules = [
  'window',
  'jasmine-co',
  'jest-matcher-utils',
  'html-parse-stringify',
  'core-js/*',
  'regenerator-runtime/runtime*',
  '@babel/runtime/*',
  './htmlNormalize',
  './common',
  './mouseEvents',
  './../bootstrap',
  './helpers/custom-matchers',
  './asciiTable',
  './MemoryLeakTest',
  '../MemoryLeakTest',
];

const babelPresetConfig = () => ({
  targets: {
    chrome: '41',
    firefox: '34',
    ie: '9',
    safari: '9'
  },
  modules: false,
  debug: false,
  useBuiltIns: 'usage',
  corejs: 3,
});

module.exports = {
  presets: [
    ['@babel/preset-env', babelPresetConfig()]
  ],
  plugins: [
    ['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
    ['transform-inline-environment-variables'],
    ['@babel/plugin-proposal-class-properties']
  ],
  env: {
    // Environment for unit testing, source code and languages building via webpack (UMD).
    commonjs: {
      plugins: [
        ['@babel/plugin-transform-runtime', {
          corejs: false,
          helpers: true,
          regenerator: true,
          useESModules: false,
        }],
        ['@babel/plugin-transform-modules-commonjs', { loose: true }]
      ]
    },
    // Environment for transpiling files to be compatible with CommonJS.
    commonjs_dist: {
      plugins: [
        ['@babel/plugin-transform-modules-commonjs', { loose: true }],
        ['babel-plugin-transform-require-ignore', { extensions: ['.css'] }]
      ],
      ignore: [
        '**/__tests__/**',
        '**/test/**',
        '**/dist/**',
      ]
    },
    // Environment for transpiling files to be compatible with ES Modules.
    es: {
      plugins: [
        ['babel-plugin-transform-require-ignore', { extensions: ['.css'] }],
        ['./.config/plugin/babel/add-import-extension.js', { extension: 'mjs' }]
      ],
      ignore: [
        '**/__tests__/**',
        '**/test/**',
        '**/dist/**',
      ],
    },
    // Environment for transpiling only legacy language files (e.q. import `languages/pl-PL`)
    // which need to be compatible with ES Modules. That format, by default, automatically
    // registers the language pack. It's not suitable to use with the modularized version of
    // the Handsontable.
    es_languages: {
      plugins: [
        ['babel-plugin-transform-require-ignore', { extensions: ['.css'] }],
        ['./.config/plugin/babel/add-import-extension.js', { extension: 'mjs' }],
        ['./.config/plugin/babel/add-language-registration.js'],
      ],
    },
    // Environment for building E2E tests (UMD).
    commonjs_e2e: {
      plugins: [
        ['@babel/plugin-transform-runtime', {
          corejs: false,
          helpers: true,
          regenerator: true,
          useESModules: false,
        }],
        ['@babel/plugin-transform-modules-commonjs', { loose: true }],
        ['babel-plugin-forbidden-imports', {
          allowedModules: allowedE2EModules
        }]
      ],
    },
  },
  ignore: [
    'src/3rdparty/walkontable/dist/',
    'src/3rdparty/walkontable/test/dist/',
  ],
};

Is `*` necessary after change to the hf npm module?